API Docs for:
Show:

File: addon/-private/system/relationships/relationship-payloads-manager.js

import { get } from '@ember/object';
// import { DEBUG } from '@glimmer/env';
import { assert } from '@ember/debug';
import { default as RelationshipPayloads, TypeCache } from './relationship-payloads';

/**
  Manages relationship payloads for a given store, for uninitialized
  relationships.  Acts as a single source of truth (of payloads) for both sides
  of an uninitialized relationship so they can agree on the most up-to-date
  payload received without needing too much eager processing when those payloads
  are pushed into the store.

  This minimizes the work spent on relationships that are never initialized.

  Once relationships are initialized, their state is managed in a relationship
  state object (eg BelongsToRelationship or ManyRelationship).


  @example

    let relationshipPayloadsManager = new RelationshipPayloadsManager(store);

    const User = DS.Model.extend({
      hobbies: DS.hasMany('hobby')
    });

    const Hobby = DS.Model.extend({
      user: DS.belongsTo('user')
    });

    let userPayload = {
      data: {
        id: 1,
        type: 'user',
        relationships: {
          hobbies: {
            data: [{
              id: 2,
              type: 'hobby'
            }]
          }
        }
      },
    };
    relationshipPayloadsManager.push('user', 1, userPayload.data.relationships);

    relationshipPayloadsManager.get('hobby', 2, 'user') === {
      {
        data: {
          id: 1,
          type: 'user'
        }
      }
    }

  @private
  @class RelationshipPayloadsManager
*/
export default class RelationshipPayloadsManager {
  constructor(store) {
    this._store = store;
    // cache of `RelationshipPayload`s
    this._cache = Object.create(null);
    this._inverseLookupCache = new TypeCache();
  }

  /**
    Find the payload for the given relationship of the given model.

    Returns the payload for the given relationship, whether raw or computed from
    the payload of the inverse relationship.

    @example

      relationshipPayloadsManager.get('hobby', 2, 'user') === {
        {
          data: {
            id: 1,
            type: 'user'
          }
        }
      }

    @method
  */
  get(modelName, id, relationshipName) {
    let relationshipPayloads = this._getRelationshipPayloads(modelName, relationshipName, false);
    return relationshipPayloads && relationshipPayloads.get(modelName, id, relationshipName);
  }

  /**
    Push a model's relationships payload into this cache.

    @example

      let userPayload = {
        data: {
          id: 1,
          type: 'user',
          relationships: {
            hobbies: {
              data: [{
                id: 2,
                type: 'hobby'
              }]
            }
          }
        },
      };
      relationshipPayloadsManager.push('user', 1, userPayload.data.relationships);

    @method
  */
  push(modelName, id, relationshipsData) {
    if (!relationshipsData) { return; }

    Object.keys(relationshipsData).forEach(key => {
      let relationshipPayloads = this._getRelationshipPayloads(modelName, key, true);
      if (relationshipPayloads) {
        relationshipPayloads.push(modelName, id, key, relationshipsData[key]);
      }
    });
  }

  /**
    Unload a model's relationships payload.

    @method
  */
  unload(modelName, id) {
    let modelClass = this._store._modelFor(modelName);
    let relationshipsByName = get(modelClass, 'relationshipsByName');
    relationshipsByName.forEach((_, relationshipName) => {
      let relationshipPayloads = this._getRelationshipPayloads(modelName, relationshipName, false);
      if (relationshipPayloads) {
        relationshipPayloads.unload(modelName, id, relationshipName);
      }
    });
  }

  /**
    Find the RelationshipPayloads object for the given relationship.  The same
    RelationshipPayloads object is returned for either side of a relationship.

    @example

      const User = DS.Model.extend({
        hobbies: DS.hasMany('hobby')
      });

      const Hobby = DS.Model.extend({
        user: DS.belongsTo('user')
      });

      relationshipPayloads.get('user', 'hobbies') === relationshipPayloads.get('hobby', 'user');

    The signature has a somewhat large arity to avoid extra work, such as
      a)  string manipulation & allocation with `modelName` and
         `relationshipName`
      b)  repeatedly getting `relationshipsByName` via `Ember.get`


    @private
    @method
  */
  _getRelationshipPayloads(modelName, relationshipName, init) {
    let relInfo = this.getRelationshipInfo(modelName, relationshipName);

    if (relInfo === null) {
      return;
    }

    let cache = this._cache[relInfo.lhs_key];

    if (!cache && init) {
      return this._initializeRelationshipPayloads(relInfo);
    }

    return cache;
  }

  getRelationshipInfo(modelName, relationshipName) {
    let inverseCache = this._inverseLookupCache;
    let store = this._store;
    let cached = inverseCache.get(modelName, relationshipName);

    // CASE: We have a cached resolution (null if no relationship exists)
    if (cached !== undefined) {
      return cached;
    }

    let modelClass = store._modelFor(modelName);
    let relationshipsByName = get(modelClass, 'relationshipsByName');

    // CASE: We don't have a relationship at all
    if (!relationshipsByName.has(relationshipName)) {
      inverseCache.set(modelName, relationshipName, null);
      return null;
    }

    let inverseMeta = modelClass.inverseFor(relationshipName, store);
    let relationshipMeta = relationshipsByName.get(relationshipName);
    let selfIsPolymorphic = relationshipMeta.options !== undefined && relationshipMeta.options.polymorphic === true;
    let inverseBaseModelName = relationshipMeta.type;

    // CASE: We have no inverse
    if (!inverseMeta) {
      let info = {
        lhs_key: `${modelName}:${relationshipName}`,
        lhs_modelNames: [modelName],
        lhs_baseModelName: modelName,
        lhs_relationshipName: relationshipName,
        lhs_relationshipMeta: relationshipMeta,
        lhs_isPolymorphic: selfIsPolymorphic,
        rhs_key: '',
        rhs_modelNames: [],
        rhs_baseModelName: inverseBaseModelName,
        rhs_relationshipName: '',
        rhs_relationshipMeta: null,
        rhs_isPolymorphic: false,
        hasInverse: false,
        isSelfReferential: false, // modelName === inverseBaseModelName,
        isReflexive: false
      };

      inverseCache.set(modelName, relationshipName, info);

      return info;
    }

    // CASE: We do have an inverse

    let inverseRelationshipName = inverseMeta.name;
    let inverseRelationshipMeta = get(inverseMeta.type, 'relationshipsByName').get(inverseRelationshipName);
    let baseModelName = inverseRelationshipMeta.type;
    let isSelfReferential = baseModelName === inverseBaseModelName;

    // TODO we want to assert this but this breaks all of our shoddily written tests
    /*
    if (DEBUG) {
      let inverseDoubleCheck = inverseMeta.type.inverseFor(inverseRelationshipName, store);

      assert(`The ${inverseBaseModelName}:${inverseRelationshipName} relationship declares 'inverse: null', but it was resolved as the inverse for ${baseModelName}:${relationshipName}.`, inverseDoubleCheck);
    }
    */

    // CASE: We may have already discovered the inverse for the baseModelName
    // CASE: We have already discovered the inverse
    cached = inverseCache.get(baseModelName, relationshipName) ||
      inverseCache.get(inverseBaseModelName, inverseRelationshipName);
    if (cached) {
      // TODO this assert can be removed if the above assert is enabled
      assert(`The ${inverseBaseModelName}:${inverseRelationshipName} relationship declares 'inverse: null', but it was resolved as the inverse for ${baseModelName}:${relationshipName}.`, cached.hasInverse !== false);

      let isLHS = cached.lhs_baseModelName === baseModelName;
      let modelNames = isLHS ? cached.lhs_modelNames : cached.rhs_modelNames;
      // make this lookup easier in the future by caching the key
      modelNames.push(modelName);
      inverseCache.set(modelName, relationshipName, cached);

      return cached;
    }

    let info = {
      lhs_key: `${baseModelName}:${relationshipName}`,
      lhs_modelNames: [modelName],
      lhs_baseModelName: baseModelName,
      lhs_relationshipName: relationshipName,
      lhs_relationshipMeta: relationshipMeta,
      lhs_isPolymorphic: selfIsPolymorphic,
      rhs_key: `${inverseBaseModelName}:${inverseRelationshipName}`,
      rhs_modelNames: [],
      rhs_baseModelName: inverseBaseModelName,
      rhs_relationshipName: inverseRelationshipName,
      rhs_relationshipMeta: inverseRelationshipMeta,
      rhs_isPolymorphic: inverseRelationshipMeta.options !== undefined && inverseRelationshipMeta.options.polymorphic === true,
      hasInverse: true,
      isSelfReferential,
      isReflexive: isSelfReferential && relationshipName === inverseRelationshipName
    };

    // Create entries for the baseModelName as well as modelName to speed up
    //  inverse lookups
    inverseCache.set(baseModelName, relationshipName, info);
    inverseCache.set(modelName, relationshipName, info);

    // Greedily populate the inverse
    inverseCache.set(inverseBaseModelName, inverseRelationshipName, info);

    return info;
  }

  /**
    Create the `RelationshipsPayload` for the relationship `modelName`, `relationshipName`, and its inverse.

    @private
    @method
  */
  _initializeRelationshipPayloads(relInfo) {
    let lhsKey = relInfo.lhs_key;
    let rhsKey = relInfo.rhs_key;
    let existingPayloads = this._cache[lhsKey];

    if (relInfo.hasInverse === true && relInfo.rhs_isPolymorphic === true) {
      existingPayloads = this._cache[rhsKey];

      if (existingPayloads !== undefined) {
        this._cache[lhsKey] = existingPayloads;
        return existingPayloads;
      }
    }

    // populate the cache for both sides of the relationship, as they both use
    // the same `RelationshipPayloads`.
    //
    // This works out better than creating a single common key, because to
    // compute that key we would need to do work to look up the inverse
    //
    let cache = this._cache[lhsKey] = new RelationshipPayloads(relInfo);

    if (relInfo.hasInverse === true) {
      this._cache[rhsKey] = cache;
    }

    return cache;
  }
}