API Docs for: v3.16.0-alpha.2
Show:

File: ../store/addon/-private/system/store/finders.js

import { A } from '@ember/array';
import { Promise } from 'rsvp';
import { assert, warn, deprecate } from '@ember/debug';
import { DEBUG } from '@glimmer/env';
import Ember from 'ember';
import coerceId from '../coerce-id';

import { _bind, _guard, _objectIsAlive, guardDestroyedStore } from './common';

import { normalizeResponseHelper } from './serializer-response';
import { assign } from '@ember/polyfills';
import { IDENTIFIERS } from '@ember-data/canary-features';
import { REQUEST_SERVICE } from '@ember-data/canary-features';

/**
  @module @ember-data/store
*/

function payloadIsNotBlank(adapterPayload) {
  if (Array.isArray(adapterPayload)) {
    return true;
  } else {
    return Object.keys(adapterPayload || {}).length;
  }
}

export function _find(adapter, store, modelClass, id, internalModel, options) {
  if (REQUEST_SERVICE) {
    // assert here
  }
  let snapshot = internalModel.createSnapshot(options);
  let { modelName } = internalModel;
  let promise = Promise.resolve().then(() => {
    return adapter.findRecord(store, modelClass, id, snapshot);
  });
  let label = `DS: Handle Adapter#findRecord of '${modelName}' with id: '${id}'`;
  const { identifier } = internalModel;

  promise = guardDestroyedStore(promise, store, label);

  return promise.then(
    adapterPayload => {
      assert(
        `You made a 'findRecord' request for a '${modelName}' with id '${id}', but the adapter's response did not have any data`,
        payloadIsNotBlank(adapterPayload)
      );
      let serializer = store.serializerFor(modelName);
      let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, id, 'findRecord');
      assert(
        `Ember Data expected the primary data returned from a 'findRecord' response to be an object but instead it found an array.`,
        !Array.isArray(payload.data)
      );

      warn(
        `You requested a record of type '${modelName}' with id '${id}' but the adapter returned a payload with primary data having an id of '${payload.data.id}'. Use 'store.findRecord()' when the requested id is the same as the one returned by the adapter. In other cases use 'store.queryRecord()' instead.`,
        coerceId(payload.data.id) === coerceId(id),
        {
          id: 'ds.store.findRecord.id-mismatch',
        }
      );

      if (IDENTIFIERS) {
        // ensure that regardless of id returned we assign to the correct record
        payload.data.lid = identifier.lid;
      }

      return store._push(payload);
    },
    error => {
      internalModel.notFound();
      if (internalModel.isEmpty()) {
        internalModel.unloadRecord();
      }

      throw error;
    },
    `DS: Extract payload of '${modelName}'`
  );
}

export function _findMany(adapter, store, modelName, ids, internalModels, optionsMap) {
  let snapshots = A(internalModels.map(internalModel => internalModel.createSnapshot(optionsMap.get(internalModel))));
  let modelClass = store.modelFor(modelName); // `adapter.findMany` gets the modelClass still
  let promise = adapter.findMany(store, modelClass, ids, snapshots);
  let label = `DS: Handle Adapter#findMany of '${modelName}'`;

  if (promise === undefined) {
    throw new Error('adapter.findMany returned undefined, this was very likely a mistake');
  }

  promise = guardDestroyedStore(promise, store, label);

  return promise.then(
    adapterPayload => {
      assert(
        `You made a 'findMany' request for '${modelName}' records with ids '[${ids}]', but the adapter's response did not have any data`,
        payloadIsNotBlank(adapterPayload)
      );
      let serializer = store.serializerFor(modelName);
      let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findMany');
      return store._push(payload);
    },
    null,
    `DS: Extract payload of ${modelName}`
  );
}

function iterateData(data, fn) {
  if (Array.isArray(data)) {
    return data.map(fn);
  } else {
    return fn(data);
  }
}

// sync
// iterate over records in payload.data
// for each record
//   assert that record.relationships[inverse] is either undefined (so we can fix it)
//     or provide a data: {id, type} that matches the record that requested it
//   return the relationship data for the parent
function syncRelationshipDataFromLink(store, payload, parentInternalModel, relationship) {
  // ensure the right hand side (incoming payload) points to the parent record that
  // requested this relationship
  let relationshipData = iterateData(payload.data, (data, index) => {
    const { id, type } = data;
    ensureRelationshipIsSetToParent(data, parentInternalModel, store, relationship, index);
    return { id, type };
  });

  // now, push the left hand side (the parent record) to ensure things are in sync, since
  // the payload will be pushed with store._push
  const parentPayload = {
    id: parentInternalModel.id,
    type: parentInternalModel.modelName,
    relationships: {
      [relationship.key]: {
        meta: payload.meta,
        links: payload.links,
        data: relationshipData,
      },
    },
  };

  if (!Array.isArray(payload.included)) {
    payload.included = [];
  }
  payload.included.push(parentPayload);

  return payload;
}

function ensureRelationshipIsSetToParent(payload, parentInternalModel, store, parentRelationship, index) {
  let { id, type } = payload;

  if (!payload.relationships) {
    payload.relationships = {};
  }
  let { relationships } = payload;

  let inverse = getInverse(store, parentInternalModel, parentRelationship, type);
  if (inverse) {
    let { inverseKey, kind } = inverse;

    let relationshipData = relationships[inverseKey] && relationships[inverseKey].data;

    if (
      DEBUG &&
      typeof relationshipData !== 'undefined' &&
      !relationshipDataPointsToParent(relationshipData, parentInternalModel)
    ) {
      let quotedType = Ember.inspect(type);
      let quotedInverse = Ember.inspect(inverseKey);
      let expected = Ember.inspect({
        id: parentInternalModel.id,
        type: parentInternalModel.modelName,
      });
      let expectedModel = Ember.inspect(parentInternalModel);
      let got = Ember.inspect(relationshipData);
      let prefix = typeof index === 'number' ? `data[${index}]` : `data`;
      let path = `${prefix}.relationships.${inverseKey}.data`;
      let other = relationshipData ? `<${relationshipData.type}:${relationshipData.id}>` : null;
      let relationshipFetched = `${Ember.inspect(parentInternalModel)}.${parentRelationship.kind}("${
        parentRelationship.name
      }")`;
      let includedRecord = `<${type}:${id}>`;
      let message = [
        `Encountered mismatched relationship: Ember Data expected ${path} in the payload from ${relationshipFetched} to include ${expected} but got ${got} instead.\n`,
        `The ${includedRecord} record loaded at ${prefix} in the payload specified ${other} as its ${quotedInverse}, but should have specified ${expectedModel} (the record the relationship is being loaded from) as its ${quotedInverse} instead.`,
        `This could mean that the response for ${relationshipFetched} may have accidentally returned ${quotedType} records that aren't related to ${expectedModel} and could be related to a different ${parentInternalModel.modelName} record instead.`,
        `Ember Data has corrected the ${includedRecord} record's ${quotedInverse} relationship to ${expectedModel} so that ${relationshipFetched} will include ${includedRecord}.`,
        `Please update the response from the server or change your serializer to either ensure that the response for only includes ${quotedType} records that specify ${expectedModel} as their ${quotedInverse}, or omit the ${quotedInverse} relationship from the response.`,
      ].join('\n');

      // this should eventually throw instead of deprecating.
      deprecate(message + '\n', false, {
        id: 'mismatched-inverse-relationship-data-from-payload',
        until: '3.8',
      });
    }

    if (kind !== 'hasMany' || typeof relationshipData !== 'undefined') {
      relationships[inverseKey] = relationships[inverseKey] || {};
      relationships[inverseKey].data = fixRelationshipData(relationshipData, kind, parentInternalModel);
    }
  }
}

function getInverse(store, parentInternalModel, parentRelationship, type) {
  return recordDataFindInverseRelationshipInfo(store, parentInternalModel, parentRelationship, type);
}

function recordDataFindInverseRelationshipInfo({ _storeWrapper }, parentInternalModel, parentRelationship, type) {
  let { name: lhs_relationshipName } = parentRelationship;
  let { modelName } = parentInternalModel;
  let inverseKey = _storeWrapper.inverseForRelationship(modelName, lhs_relationshipName);

  if (inverseKey) {
    let {
      meta: { kind },
    } = _storeWrapper.relationshipsDefinitionFor(type)[inverseKey];
    return {
      inverseKey,
      kind,
    };
  }
}

function relationshipDataPointsToParent(relationshipData, internalModel) {
  if (relationshipData === null) {
    return false;
  }

  if (Array.isArray(relationshipData)) {
    if (relationshipData.length === 0) {
      return false;
    }
    for (let i = 0; i < relationshipData.length; i++) {
      let entry = relationshipData[i];
      if (validateRelationshipEntry(entry, internalModel)) {
        return true;
      }
    }
  } else {
    return validateRelationshipEntry(relationshipData, internalModel);
  }

  return false;
}

function fixRelationshipData(relationshipData, relationshipKind, { id, modelName }) {
  let parentRelationshipData = {
    id,
    type: modelName,
  };

  let payload;

  if (relationshipKind === 'hasMany') {
    payload = relationshipData || [];
    payload.push(parentRelationshipData);
  } else {
    payload = relationshipData || {};
    assign(payload, parentRelationshipData);
  }

  return payload;
}

function validateRelationshipEntry({ id }, { id: parentModelID }) {
  return id && id.toString() === parentModelID;
}

export function _findHasMany(adapter, store, internalModel, link, relationship, options) {
  let snapshot = internalModel.createSnapshot(options);
  let modelClass = store.modelFor(relationship.type);
  let useLink = !link || typeof link === 'string';
  let relatedLink = useLink ? link : link.href;
  let promise = adapter.findHasMany(store, snapshot, relatedLink, relationship);
  let label = `DS: Handle Adapter#findHasMany of '${internalModel.modelName}' : '${relationship.type}'`;

  promise = guardDestroyedStore(promise, store, label);
  promise = _guard(promise, _bind(_objectIsAlive, internalModel));

  return promise.then(
    adapterPayload => {
      assert(
        `You made a 'findHasMany' request for a ${internalModel.modelName}'s '${relationship.key}' relationship, using link '${link}' , but the adapter's response did not have any data`,
        payloadIsNotBlank(adapterPayload)
      );
      let serializer = store.serializerFor(relationship.type);
      let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findHasMany');

      payload = syncRelationshipDataFromLink(store, payload, internalModel, relationship);

      let internalModelArray = store._push(payload);
      return internalModelArray;
    },
    null,
    `DS: Extract payload of '${internalModel.modelName}' : hasMany '${relationship.type}'`
  );
}

export function _findBelongsTo(adapter, store, internalModel, link, relationship, options) {
  let snapshot = internalModel.createSnapshot(options);
  let modelClass = store.modelFor(relationship.type);
  let useLink = !link || typeof link === 'string';
  let relatedLink = useLink ? link : link.href;
  let promise = adapter.findBelongsTo(store, snapshot, relatedLink, relationship);
  let label = `DS: Handle Adapter#findBelongsTo of ${internalModel.modelName} : ${relationship.type}`;

  promise = guardDestroyedStore(promise, store, label);
  promise = _guard(promise, _bind(_objectIsAlive, internalModel));

  return promise.then(
    adapterPayload => {
      let serializer = store.serializerFor(relationship.type);
      let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findBelongsTo');

      if (!payload.data) {
        return null;
      }

      payload = syncRelationshipDataFromLink(store, payload, internalModel, relationship);

      return store._push(payload);
    },
    null,
    `DS: Extract payload of ${internalModel.modelName} : ${relationship.type}`
  );
}

export function _findAll(adapter, store, modelName, options) {
  let modelClass = store.modelFor(modelName); // adapter.findAll depends on the class
  let recordArray = store.peekAll(modelName);
  let snapshotArray = recordArray._createSnapshot(options);
  let promise = Promise.resolve().then(() => adapter.findAll(store, modelClass, null, snapshotArray));
  let label = 'DS: Handle Adapter#findAll of ' + modelClass;

  promise = guardDestroyedStore(promise, store, label);

  return promise.then(
    adapterPayload => {
      assert(
        `You made a 'findAll' request for '${modelName}' records, but the adapter's response did not have any data`,
        payloadIsNotBlank(adapterPayload)
      );
      let serializer = store.serializerFor(modelName);
      let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'findAll');

      store._push(payload);
      store._didUpdateAll(modelName);

      return recordArray;
    },
    null,
    'DS: Extract payload of findAll ${modelName}'
  );
}

export function _query(adapter, store, modelName, query, recordArray, options) {
  let modelClass = store.modelFor(modelName); // adapter.query needs the class

  recordArray = recordArray || store.recordArrayManager.createAdapterPopulatedRecordArray(modelName, query);
  let promise = Promise.resolve().then(() => adapter.query(store, modelClass, query, recordArray, options));

  let label = `DS: Handle Adapter#query of ${modelName}`;
  promise = guardDestroyedStore(promise, store, label);

  return promise.then(
    adapterPayload => {
      let serializer = store.serializerFor(modelName);
      let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'query');
      let internalModels = store._push(payload);

      assert(
        'The response to store.query is expected to be an array but it was a single record. Please wrap your response in an array or use `store.queryRecord` to query for a single record.',
        Array.isArray(internalModels)
      );
      if (recordArray) {
        recordArray._setInternalModels(internalModels, payload);
      } else {
        recordArray = store.recordArrayManager.createAdapterPopulatedRecordArray(
          modelName,
          query,
          internalModels,
          payload
        );
      }

      return recordArray;
    },
    null,
    `DS: Extract payload of query ${modelName}`
  );
}

export function _queryRecord(adapter, store, modelName, query, options) {
  let modelClass = store.modelFor(modelName); // adapter.queryRecord needs the class
  let promise = Promise.resolve().then(() => adapter.queryRecord(store, modelClass, query, options));

  let label = `DS: Handle Adapter#queryRecord of ${modelName}`;
  promise = guardDestroyedStore(promise, store, label);

  return promise.then(
    adapterPayload => {
      let serializer = store.serializerFor(modelName);
      let payload = normalizeResponseHelper(serializer, store, modelClass, adapterPayload, null, 'queryRecord');

      assert(
        `Expected the primary data returned by the serializer for a 'queryRecord' response to be a single object or null but instead it was an array.`,
        !Array.isArray(payload.data),
        {
          id: 'ds.store.queryRecord-array-response',
        }
      );

      return store._push(payload);
    },
    null,
    `DS: Extract payload of queryRecord ${modelName}`
  );
}