API Docs for: v3.5.1
Show:

File: addon/-private/system/many-array.js

/**
  @module ember-data
*/
import { all } from 'rsvp';

import Evented from '@ember/object/evented';
import MutableArray from '@ember/array/mutable';
import EmberObject, { get } from '@ember/object';
import { assert } from '@ember/debug';
import { PromiseArray } from './promise-proxies';
import { _objectIsAlive } from './store/common';
import diffArray from './diff-array';

/**
  A `ManyArray` is a `MutableArray` that represents the contents of a has-many
  relationship.

  The `ManyArray` is instantiated lazily the first time the relationship is
  requested.

  ### Inverses

  Often, the relationships in Ember Data applications will have
  an inverse. For example, imagine the following models are
  defined:

  ```app/models/post.js
  import DS from 'ember-data';

  export default DS.Model.extend({
    comments: DS.hasMany('comment')
  });
  ```

  ```app/models/comment.js
  import DS from 'ember-data';

  export default DS.Model.extend({
    post: DS.belongsTo('post')
  });
  ```

  If you created a new instance of `App.Post` and added
  a `App.Comment` record to its `comments` has-many
  relationship, you would expect the comment's `post`
  property to be set to the post that contained
  the has-many.

  We call the record to which a relationship belongs-to the
  relationship's _owner_.

  @class ManyArray
  @namespace DS
  @extends Ember.Object
  @uses Ember.MutableArray, Ember.Evented
*/
export default EmberObject.extend(MutableArray, Evented, {
  init() {
    this._super(...arguments);

    /**
    The loading state of this array

    @property {Boolean} isLoaded
    */
    this.isLoaded = false;
    this.length = 0;

    /**
    Used for async `hasMany` arrays
    to keep track of when they will resolve.

    @property {Ember.RSVP.Promise} promise
    @private
    */
    this.promise = null;

    /**
    Metadata associated with the request for async hasMany relationships.

    Example

    Given that the server returns the following JSON payload when fetching a
    hasMany relationship:

    ```js
    {
      "comments": [{
        "id": 1,
        "comment": "This is the first comment",
      }, {
    // ...
      }],

      "meta": {
        "page": 1,
        "total": 5
      }
    }
    ```

    You can then access the metadata via the `meta` property:

    ```js
    post.get('comments').then(function(comments) {
      var meta = comments.get('meta');

    // meta.page => 1
    // meta.total => 5
    });
    ```

    @property {Object} meta
    @public
    */
    // TODO this is likely broken in our refactor
    this.meta = this.meta || null;

    /**
    `true` if the relationship is polymorphic, `false` otherwise.

    @property {Boolean} isPolymorphic
    @private
    */
    this.isPolymorphic = this.isPolymorphic || false;

    /**
    The relationship which manages this array.

    @property {ManyRelationship} relationship
    @private
    */
    this.currentState = [];
    this.flushCanonical(this.initialState, false);
  },

  // TODO: if(DEBUG)
  anyUnloaded() {
    let unloaded = this.currentState.find(im => im._isDematerializing || !im.isLoaded());
    return !!unloaded;
  },

  removeUnloadedInternalModel() {
    for (let i = 0; i < this.currentState.length; ++i) {
      let internalModel = this.currentState[i];
      if (internalModel._isDematerializing || !internalModel.isLoaded()) {
        this.arrayContentWillChange(i, 1, 0);
        this.currentState.splice(i, 1);
        this.set('length', this.currentState.length);
        this.arrayContentDidChange(i, 1, 0);
        return true;
      }
    }
    return false;
  },

  objectAt(index) {
    // TODO we likely need to force flush here
    /*
    if (this.relationship._willUpdateManyArray) {
      this.relationship._flushPendingManyArrayUpdates();
    }
    */
    let internalModel = this.currentState[index];
    if (internalModel === undefined) {
      return;
    }

    return internalModel.getRecord();
  },

  flushCanonical(toSet, isInitialized = true) {
    // It’s possible the parent side of the relationship may have been unloaded by this point
    if (!_objectIsAlive(this)) {
      return;
    }
    // diff to find changes
    let diff = diffArray(this.currentState, toSet);
    if (diff.firstChangeIndex !== null) {
      // it's null if no change found
      // we found a change
      this.arrayContentWillChange(diff.firstChangeIndex, diff.removedCount, diff.addedCount);
      this.set('length', toSet.length);
      this.currentState = toSet.slice();
      this.arrayContentDidChange(diff.firstChangeIndex, diff.removedCount, diff.addedCount);
      if (isInitialized && diff.addedCount > 0) {
        //notify only on additions
        //TODO only notify if unloaded
        this.internalModel.manyArrayRecordAdded(this.get('key'));
      }
    }
  },

  replace(idx, amt, objects) {
    let internalModels;
    if (amt > 0) {
      internalModels = this.currentState.slice(idx, idx + amt);
      this.get('recordData').removeFromHasMany(
        this.get('key'),
        internalModels.map(im => im._recordData)
      );
    }
    if (objects) {
      this.get('recordData').addToHasMany(
        this.get('key'),
        objects.map(obj => obj._internalModel._recordData),
        idx
      );
      //this.get('relationship').addInternalModels(objects.map(obj => obj._internalModel), idx);
    }
    this.retrieveLatest();
  },

  // Ok this is kinda funky because if buggy we might lose positions, etc.
  // but current code is this way so shouldn't be too big of a problem
  retrieveLatest() {
    let jsonApi = this.get('recordData').getHasMany(this.get('key'));
    // TODO this is odd, why should ManyArray ever tell itself to resync?
    let internalModels = this.store._getHasManyByJsonApiResource(jsonApi);
    if (jsonApi.meta) {
      this.set('meta', jsonApi.meta);
    }
    this.flushCanonical(internalModels, true);
  },

  /**
    Reloads all of the records in the manyArray. If the manyArray
    holds a relationship that was originally fetched using a links url
    Ember Data will revisit the original links url to repopulate the
    relationship.

    If the manyArray holds the result of a `store.query()` reload will
    re-run the original query.

    Example

    ```javascript
    var user = store.peekRecord('user', 1)
    user.login().then(function() {
      user.get('permissions').then(function(permissions) {
        return permissions.reload();
      });
    });
    ```

    @method reload
    @public
  */
  reload(options) {
    // TODO this is odd, we don't ask the store for anything else like this?
    return this.get('store').reloadManyArray(
      this,
      this.get('internalModel'),
      this.get('key'),
      options
    );
  },

  /**
    Saves all of the records in the `ManyArray`.

    Example

    ```javascript
    store.findRecord('inbox', 1).then(function(inbox) {
      inbox.get('messages').then(function(messages) {
        messages.forEach(function(message) {
          message.set('isRead', true);
        });
        messages.save()
      });
    });
    ```

    @method save
    @return {DS.PromiseArray} promise
  */
  save() {
    let manyArray = this;
    let promiseLabel = 'DS: ManyArray#save ' + get(this, 'type');
    let promise = all(this.invoke('save'), promiseLabel).then(
      () => manyArray,
      null,
      'DS: ManyArray#save return ManyArray'
    );

    return PromiseArray.create({ promise });
  },

  /**
    Create a child record within the owner

    @method createRecord
    @private
    @param {Object} hash
    @return {DS.Model} record
  */
  createRecord(hash) {
    const store = get(this, 'store');
    const type = get(this, 'type');

    assert(
      `You cannot add '${type.modelName}' records to this polymorphic relationship.`,
      !get(this, 'isPolymorphic')
    );
    let record = store.createRecord(type.modelName, hash);
    this.pushObject(record);

    return record;
  },
});