#

API Docs for: v3.16.0-alpha.2
Show: Inherited Protected Private Deprecated

# File: ../record-data/addon/-private/relationships/state/relationship.ts

    import { guidFor } from '@ember/object/internals';
    import { get } from '@ember/object';
    import { relationshipStateFor, implicitRelationshipStateFor } from '../../record-data-for';
    import { assert, warn } from '@ember/debug';
    import OrderedSet from '../../ordered-set';
    import _normalizeLink from '../../normalize-link';
    import { RelationshipRecordData } from '../../ts-interfaces/relationship-record-data';
    import { JsonApiRelationship } from '@ember-data/store/-private/ts-interfaces/record-data-json-api';
    import { RelationshipSchema } from '@ember-data/store/-private/ts-interfaces/record-data-schemas';
    import { CUSTOM_MODEL_CLASS } from '@ember-data/canary-features';
    import { PaginationLinks } from '@ember-data/store/-private/ts-interfaces/ember-data-json-api';

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

    interface ImplicitRelationshipMeta {
      key?: string;
      kind?: string;
      options: any;
    }

    export default class Relationship {
      inverseIsAsync: boolean | undefined;
      kind?: string;
      recordData: RelationshipRecordData;
      members: OrderedSet<RelationshipRecordData>;
      canonicalMembers: OrderedSet<RelationshipRecordData>;
      store: any;
      key: string | null;
      inverseKey: string | null;
      isAsync: boolean;
      isPolymorphic: boolean;
      relationshipMeta: ImplicitRelationshipMeta | RelationshipSchema;
      inverseKeyForImplicit: string;
      meta: any;
      __inverseMeta: any;
      _tempModelName: string;
      shouldForceReload: boolean = false;
      relationshipIsStale: boolean;
      hasDematerializedInverse: boolean;
      hasAnyRelationshipData: boolean;
      relationshipIsEmpty: boolean;
      hasFailedLoadAttempt: boolean = false;
      links?: PaginationLinks;
      willSync?: boolean;

      constructor(
        store: any,
        inverseKey: string | null,
        relationshipMeta: ImplicitRelationshipMeta,
        recordData: RelationshipRecordData,
        inverseIsAsync?: boolean
      ) {
        this.inverseIsAsync = inverseIsAsync;
        this.kind = relationshipMeta.kind;
        let async = relationshipMeta.options.async;
        let polymorphic = relationshipMeta.options.polymorphic;
        this.recordData = recordData;
        this.members = new OrderedSet();
        this.canonicalMembers = new OrderedSet();
        this.store = store;
        this.key = relationshipMeta.key || null;
        this.inverseKey = inverseKey;
        this.isAsync = typeof async === 'undefined' ? true : async;
        this.isPolymorphic = typeof polymorphic === 'undefined' ? false : polymorphic;
        this.relationshipMeta = relationshipMeta;
        //This probably breaks for polymorphic relationship in complex scenarios, due to
        //multiple possible modelNames
        this.inverseKeyForImplicit = this._tempModelName + this.key;
        this.meta = null;
        this.__inverseMeta = undefined;

        /*
         This flag forces fetch. `true` for a single request once `reload()`
           has been called `false` at all other times.
        */
        // this.shouldForceReload = false;

        /*
           This flag indicates whether we should
            re-fetch the relationship the next time
            it is accessed.

            The difference between this flag and `shouldForceReload`
            is in how we treat the presence of partially missing data:
              - for a forced reload, we will reload the link or EVERY record
              - for a stale reload, we will reload the link (if present) else only MISSING records

            Ideally these flags could be merged, but because we don't give the
            request layer the option of deciding how to resolve the data being queried
            we are forced to differentiate for now.

            It is also possible for a relationship to remain stale after a forced reload; however,
            in this case `hasFailedLoadAttempt` ought to be `true`.

          false when
            => recordData.isNew() on initial setup
            => a previously triggered request has resolved
            => we get relationship data via push

          true when
            => !recordData.isNew() on initial setup
            => an inverse has been unloaded
            => we get a new link for the relationship

          TODO @runspired unskip the acceptance tests and fix these flags
         */
        this.relationshipIsStale = false;

        /*
         This flag indicates whether we should
          **partially** re-fetch the relationship the
          next time it is accessed.

        false when
          => initial setup
          => a previously triggered request has resolved

        true when
          => an inverse has been unloaded
        */
        this.hasDematerializedInverse = false;

        /*
          This flag indicates whether we should consider the content
           of this relationship "known".

          If we have no relationship knowledge, and the relationship
           is `async`, we will attempt to fetch the relationship on
           access if it is also stale.

         Snapshot uses this to tell the difference between unknown
          (`undefined`) or empty (`null`). The reason for this is that
          we wouldn't want to serialize  unknown relationships as `null`
          as that might overwrite remote state.

          All relationships for a newly created (`store.createRecord()`) are
           considered known (`hasAnyRelationshipData === true`).

          true when
            => we receive a push with either new data or explicit empty (`[]` or `null`)
            => the relationship is a belongsTo and we have received data from
                 the other side.

          false when
            => we have received no signal about what data belongs in this relationship
            => the relationship is a hasMany and we have only received data from
                the other side.
         */
        this.hasAnyRelationshipData = false;

        /*
          Flag that indicates whether an empty relationship is explicitly empty
            (signaled by push giving us an empty array or null relationship)
            e.g. an API response has told us that this relationship is empty.

          Thus far, it does not appear that we actually need this flag; however,
            @runspired has found it invaluable when debugging relationship tests
            to determine whether (and why if so) we are in an incorrect state.

          true when
            => we receive a push with explicit empty (`[]` or `null`)
            => we have received no signal about what data belongs in this relationship
            => on initial create (as no signal is known yet)

          false at all other times
         */
        this.relationshipIsEmpty = true;

        /*
          Flag def here for reference, defined as getter in has-many.js / belongs-to.js

          true when
            => hasAnyRelationshipData is true
            AND
            => members (NOT canonicalMembers) @each !isEmpty

          TODO, consider changing the conditional here from !isEmpty to !hiddenFromRecordArrays
        */

        // TODO do we want this anymore? Seems somewhat useful
        //   especially if we rename to `hasUpdatedLink`
        //   which would tell us slightly more about why the
        //   relationship is stale
        // this.updatedLink = false;
      }

      get isNew(): boolean {
        return this.recordData.isNew();
      }

      _inverseIsAsync(): boolean {
        return !!this.inverseIsAsync;
      }

      _inverseIsSync(): boolean {
        return !!(this.inverseKey && !this.inverseIsAsync);
      }

      _hasSupportForImplicitRelationships(recordData: RelationshipRecordData): boolean {
        return recordData._implicitRelationships !== undefined && recordData._implicitRelationships !== null;
      }

      _hasSupportForRelationships(recordData: RelationshipRecordData): recordData is RelationshipRecordData {
        return recordData._relationships !== undefined && recordData._relationships !== null;
      }

      get _inverseMeta(): RelationshipSchema {
        if (this.__inverseMeta === undefined) {
          let inverseMeta = null;

          if (this.inverseKey) {
            // We know we have a full inverse relationship
            let type = (this.relationshipMeta as RelationshipSchema).type;
            let inverseModelClass = this.store.modelFor(type);
            let inverseRelationships = get(inverseModelClass, 'relationshipsByName');
            inverseMeta = inverseRelationships.get(this.inverseKey);
          }

          this.__inverseMeta = inverseMeta;
        }
        return this.__inverseMeta;
      }

      recordDataDidDematerialize() {
        const inverseKey = this.inverseKey;
        if (!inverseKey) {
          return;
        }

        // we actually want a union of members and canonicalMembers
        // they should be disjoint but currently are not due to a bug
        this.forAllMembers(inverseRecordData => {
          if (!this._hasSupportForRelationships(inverseRecordData)) {
            return;
          }
          let relationship = relationshipStateFor(inverseRecordData, inverseKey);
          let belongsToRelationship = inverseRecordData.getBelongsTo(inverseKey)._relationship;

          // For canonical members, it is possible that inverseRecordData has already been associated to
          // to another record. For such cases, do not dematerialize the inverseRecordData
          if (
            !belongsToRelationship ||
            !belongsToRelationship.inverseRecordData ||
            this.recordData === belongsToRelationship.inverseRecordData
          ) {
            relationship.inverseDidDematerialize(this.recordData);
          }
        });
      }

      forAllMembers(callback: (im: RelationshipRecordData) => void) {
        let seen = Object.create(null);

        for (let i = 0; i < this.members.list.length; i++) {
          const inverseInternalModel = this.members.list[i];
          const id = guidFor(inverseInternalModel);
          if (!seen[id]) {
            seen[id] = true;
            callback(inverseInternalModel);
          }
        }

        for (let i = 0; i < this.canonicalMembers.list.length; i++) {
          const inverseInternalModel = this.canonicalMembers.list[i];
          const id = guidFor(inverseInternalModel);
          if (!seen[id]) {
            seen[id] = true;
            callback(inverseInternalModel);
          }
        }
      }

      inverseDidDematerialize(inverseRecordData: RelationshipRecordData | null) {
        if (!this.isAsync || (inverseRecordData && inverseRecordData.isNew())) {
          // unloading inverse of a sync relationship is treated as a client-side
          // delete, so actually remove the models don't merely invalidate the cp
          // cache.
          // if the record being unloaded only exists on the client, we similarly
          // treat it as a client side delete
          this.removeRecordDataFromOwn(inverseRecordData);
          this.removeCanonicalRecordDataFromOwn(inverseRecordData);
          this.setRelationshipIsEmpty(true);
        } else {
          this.setHasDematerializedInverse(true);
        }
      }

      updateMeta(meta: any) {
        this.meta = meta;
      }

      clear() {
        let members = this.members.list;
        while (members.length > 0) {
          let member = members[0];
          this.removeRecordData(member);
        }

        let canonicalMembers = this.canonicalMembers.list;
        while (canonicalMembers.length > 0) {
          let member = canonicalMembers[0];
          this.removeCanonicalRecordData(member);
        }
      }

      removeAllRecordDatasFromOwn() {
        this.setRelationshipIsStale(true);
        this.members.clear();
      }

      removeAllCanonicalRecordDatasFromOwn() {
        this.canonicalMembers.clear();
        this.flushCanonicalLater();
      }

      removeRecordDatas(recordDatas: RelationshipRecordData[]) {
        recordDatas.forEach(recordData => this.removeRecordData(recordData));
      }

      addRecordDatas(recordDatas: RelationshipRecordData[], idx?: number) {
        recordDatas.forEach(recordData => {
          this.addRecordData(recordData, idx);
          if (idx !== undefined) {
            idx++;
          }
        });
      }

      addCanonicalRecordDatas(recordDatas: RelationshipRecordData[], idx: number) {
        for (let i = 0; i < recordDatas.length; i++) {
          if (idx !== undefined) {
            this.addCanonicalRecordData(recordDatas[i], i + idx);
          } else {
            this.addCanonicalRecordData(recordDatas[i]);
          }
        }
      }

      addCanonicalRecordData(recordData: RelationshipRecordData, idx?: number) {
        if (!this.canonicalMembers.has(recordData)) {
          this.canonicalMembers.add(recordData);
          this.setupInverseRelationship(recordData);
        }
        this.flushCanonicalLater();
        this.setHasAnyRelationshipData(true);
      }

      setupInverseRelationship(recordData: RelationshipRecordData) {
        if (this.inverseKey) {
          if (!this._hasSupportForRelationships(recordData)) {
            return;
          }
          let relationship = relationshipStateFor(recordData, this.inverseKey);
          // if we have only just initialized the inverse relationship, then it
          // already has this.recordData in its canonicalMembers, so skip the
          // unnecessary work.  The exception to this is polymorphic
          // relationships whose members are determined by their inverse, as those
          // relationships cannot efficiently find their inverse payloads.
          relationship.addCanonicalRecordData(this.recordData);
        } else {
          if (!this._hasSupportForImplicitRelationships(recordData)) {
            return;
          }
          let relationships = recordData._implicitRelationships;
          let relationship = relationships[this.inverseKeyForImplicit];
          if (!relationship) {
            relationship = relationships[this.inverseKeyForImplicit] = new Relationship(
              this.store,
              this.key,
              { options: { async: this.isAsync } },
              recordData
            );
          }
          relationship.addCanonicalRecordData(this.recordData);
        }
      }

      removeCanonicalRecordDatas(recordDatas: RelationshipRecordData[], idx?: number) {
        for (let i = 0; i < recordDatas.length; i++) {
          if (idx !== undefined) {
            this.removeCanonicalRecordData(recordDatas[i], i + idx);
          } else {
            this.removeCanonicalRecordData(recordDatas[i]);
          }
        }
      }

      removeCanonicalRecordData(recordData: RelationshipRecordData, idx?: number) {
        if (this.canonicalMembers.has(recordData)) {
          this.removeCanonicalRecordDataFromOwn(recordData);
          if (this.inverseKey) {
            this.removeCanonicalRecordDataFromInverse(recordData);
          } else {
            if (
              this._hasSupportForImplicitRelationships(recordData) &&
              recordData._implicitRelationships[this.inverseKeyForImplicit]
            ) {
              recordData._implicitRelationships[this.inverseKeyForImplicit].removeCanonicalRecordData(this.recordData);
            }
          }
        }
        this.flushCanonicalLater();
      }

      addRecordData(recordData: RelationshipRecordData, idx?: number) {
        if (!this.members.has(recordData)) {
          this.members.addWithIndex(recordData, idx);
          this.notifyRecordRelationshipAdded(recordData, idx);
          if (this._hasSupportForRelationships(recordData) && this.inverseKey) {
            relationshipStateFor(recordData, this.inverseKey).addRecordData(this.recordData);
          } else {
            if (this._hasSupportForImplicitRelationships(recordData)) {
              if (!recordData._implicitRelationships[this.inverseKeyForImplicit]) {
                recordData._implicitRelationships[this.inverseKeyForImplicit] = new Relationship(
                  this.store,
                  this.key,
                  { options: { async: this.isAsync } },
                  recordData,
                  this.isAsync
                );
              }
              recordData._implicitRelationships[this.inverseKeyForImplicit].addRecordData(this.recordData);
            }
          }
        }
        this.setHasAnyRelationshipData(true);
      }

      removeRecordData(recordData: RelationshipRecordData) {
        if (this.members.has(recordData)) {
          this.removeRecordDataFromOwn(recordData);
          if (this.inverseKey) {
            this.removeRecordDataFromInverse(recordData);
          } else {
            if (
              this._hasSupportForImplicitRelationships(recordData) &&
              recordData._implicitRelationships[this.inverseKeyForImplicit]
            ) {
              recordData._implicitRelationships[this.inverseKeyForImplicit].removeRecordData(this.recordData);
            }
          }
        }
      }

      removeRecordDataFromInverse(recordData: RelationshipRecordData) {
        if (!this._hasSupportForRelationships(recordData)) {
          return;
        }
        if (this.inverseKey) {
          let inverseRelationship = relationshipStateFor(recordData, this.inverseKey);
          //Need to check for existence, as the record might unloading at the moment
          if (inverseRelationship) {
            inverseRelationship.removeRecordDataFromOwn(this.recordData);
          }
        }
      }

      removeRecordDataFromOwn(recordData: RelationshipRecordData | null, idx?: number) {
        this.members.delete(recordData);
      }

      removeCanonicalRecordDataFromInverse(recordData: RelationshipRecordData) {
        if (!this._hasSupportForRelationships(recordData)) {
          return;
        }
        if (this.inverseKey) {
          let inverseRelationship = relationshipStateFor(recordData, this.inverseKey);
          //Need to check for existence, as the record might unloading at the moment
          if (inverseRelationship) {
            inverseRelationship.removeCanonicalRecordDataFromOwn(this.recordData);
          }
        }
      }

      removeCanonicalRecordDataFromOwn(recordData: RelationshipRecordData | null, idx?: number) {
        this.canonicalMembers.delete(recordData);
        this.flushCanonicalLater();
      }

      /*
        Call this method once a record deletion has been persisted
        to purge it from BOTH current and canonical state of all
        relationships.

        @method removeCompletelyFromInverse
        @private
       */
      removeCompletelyFromInverse() {
        if (!this.inverseKey && !this.inverseKeyForImplicit) {
          return;
        }

        // we actually want a union of members and canonicalMembers
        // they should be disjoint but currently are not due to a bug
        let seen = Object.create(null);
        const recordData = this.recordData;

        let unload;
        if (this.inverseKey) {
          unload = inverseRecordData => {
            const id = guidFor(inverseRecordData);

            if (this._hasSupportForRelationships(inverseRecordData) && seen[id] === undefined) {
              if (this.inverseKey) {
                const relationship = relationshipStateFor(inverseRecordData, this.inverseKey);
                relationship.removeCompletelyFromOwn(recordData);
              }
              seen[id] = true;
            }
          };
        } else {
          unload = inverseRecordData => {
            const id = guidFor(inverseRecordData);

            if (this._hasSupportForImplicitRelationships(inverseRecordData) && seen[id] === undefined) {
              const relationship = implicitRelationshipStateFor(inverseRecordData, this.inverseKeyForImplicit);
              relationship.removeCompletelyFromOwn(recordData);
              seen[id] = true;
            }
          };
        }

        this.members.forEach(unload);
        this.canonicalMembers.forEach(unload);

        if (!this.isAsync) {
          this.clear();
        }
      }

      /*
        Removes the given RecordData from BOTH canonical AND current state.

        This method is useful when either a deletion or a rollback on a new record
        needs to entirely purge itself from an inverse relationship.
       */
      removeCompletelyFromOwn(recordData: RelationshipRecordData) {
        this.canonicalMembers.delete(recordData);
        this.members.delete(recordData);
      }

      flushCanonical() {
        let list = this.members.list as RelationshipRecordData[];
        this.willSync = false;
        //a hack for not removing new RecordDatas
        //TODO remove once we have proper diffing
        let newRecordDatas: RelationshipRecordData[] = [];
        for (let i = 0; i < list.length; i++) {
          // TODO Igor deal with this
          if (list[i].isNew()) {
            newRecordDatas.push(list[i]);
          }
        }

        //TODO(Igor) make this less abysmally slow
        this.members = this.canonicalMembers.copy();
        for (let i = 0; i < newRecordDatas.length; i++) {
          this.members.add(newRecordDatas[i]);
        }
      }

      flushCanonicalLater() {
        if (this.willSync) {
          return;
        }
        this.willSync = true;
        // Reaching back into the store to use ED's runloop
        this.store._updateRelationshipState(this);
      }

      updateLinks(links: PaginationLinks): void {
        this.links = links;
      }

      updateRecordDatasFromAdapter(recordDatas?: RelationshipRecordData[]) {
        this.setHasAnyRelationshipData(true);
        //TODO(Igor) move this to a proper place
        //TODO Once we have adapter support, we need to handle updated and canonical changes
        this.computeChanges(recordDatas);
      }

      computeChanges(recordDatas?: RelationshipRecordData[]) {}

      notifyRecordRelationshipAdded(recordData?, idxs?) {}

      setHasAnyRelationshipData(value: boolean) {
        this.hasAnyRelationshipData = value;
      }

      setHasDematerializedInverse(value: boolean) {
        this.hasDematerializedInverse = value;
      }

      setRelationshipIsStale(value: boolean) {
        this.relationshipIsStale = value;
      }

      setRelationshipIsEmpty(value: boolean) {
        this.relationshipIsEmpty = value;
      }

      setShouldForceReload(value: boolean) {
        this.shouldForceReload = value;
      }

      setHasFailedLoadAttempt(value: boolean) {
        this.hasFailedLoadAttempt = value;
      }

      /*
       `push` for a relationship allows the store to push a JSON API Relationship
       Object onto the relationship. The relationship will then extract and set the
       meta, data and links of that relationship.

       `push` use `updateMeta`, `updateData` and `updateLink` to update the state
       of the relationship.
       */
      push(payload: JsonApiRelationship, initial?: boolean) {
        let hasRelationshipDataProperty = false;
        let hasLink = false;

        if (payload.meta) {
          this.updateMeta(payload.meta);
        }

        if (payload.data !== undefined) {
          hasRelationshipDataProperty = true;
          this.updateData(payload.data, initial);
        } else if (this.isAsync === false && !this.hasAnyRelationshipData) {
          hasRelationshipDataProperty = true;
          let data = this.kind === 'hasMany' ? [] : null;

          this.updateData(data, initial);
        }

        if (payload.links) {
          let originalLinks = this.links;
          this.updateLinks(payload.links);
          if (payload.links.related) {
            let relatedLink = _normalizeLink(payload.links.related);
            let currentLink = originalLinks && originalLinks.related ? _normalizeLink(originalLinks.related) : null;
            let currentLinkHref = currentLink ? currentLink.href : null;

            if (relatedLink && relatedLink.href && relatedLink.href !== currentLinkHref) {
              warn(
                `You pushed a record of type '${this.recordData.modelName}' with a relationship '${this.key}' configured as 'async: false'. You've included a link but no primary data, this may be an error in your payload. EmberData will treat this relationship as known-to-be-empty.`,
                this.isAsync || this.hasAnyRelationshipData,
                {
                  id: 'ds.store.push-link-for-sync-relationship',
                }
              );
              assert(
                `You have pushed a record of type '${this.recordData.modelName}' with '${this.key}' as a link, but the value of that link is not a string.`,
                typeof relatedLink.href === 'string' || relatedLink.href === null
              );
              hasLink = true;
            }
          }
        }

        /*
         Data being pushed into the relationship might contain only data or links,
         or a combination of both.

         IF contains only data
         IF contains both links and data
          relationshipIsEmpty -> true if is empty array (has-many) or is null (belongs-to)
          hasAnyRelationshipData -> true
          hasDematerializedInverse -> false
          relationshipIsStale -> false
          allInverseRecordsAreLoaded -> run-check-to-determine

         IF contains only links
          relationshipIsStale -> true
         */
        this.setHasFailedLoadAttempt(false);
        if (hasRelationshipDataProperty) {
          let relationshipIsEmpty = payload.data === null || (Array.isArray(payload.data) && payload.data.length === 0);

          this.setHasAnyRelationshipData(true);
          this.setRelationshipIsStale(false);
          this.setHasDematerializedInverse(false);
          this.setRelationshipIsEmpty(relationshipIsEmpty);
        } else if (hasLink) {
          this.setRelationshipIsStale(true);

          if (!initial) {
            let recordData = this.recordData;
            let storeWrapper = this.recordData.storeWrapper;
            if (CUSTOM_MODEL_CLASS) {
              storeWrapper.notifyBelongsToChange(recordData.modelName, recordData.id, recordData.clientId, this.key!);
            } else {
              storeWrapper.notifyPropertyChange(
                recordData.modelName,
                recordData.id,
                recordData.clientId,
                // We know we are not an implicit relationship here
                this.key!
              );
            }
          }
        }
      }

      localStateIsEmpty() {}

      updateData(payload?, initial?) {}

      destroy() {}
    }