/**
@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 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
*/
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.relationship = this.relationship || null;
this.currentState = [];
this.flushCanonical(false);
},
objectAt(index) {
let internalModel = this.currentState[index];
if (internalModel === undefined) { return; }
return internalModel.getRecord();
},
flushCanonical(isInitialized = true) {
// It’s possible the parent side of the relationship may have been unloaded by this point
if (!_objectIsAlive(this)) {
return;
}
let toSet = this.canonicalState;
//a hack for not removing new records
//TODO remove once we have proper diffing
let newInternalModels = this.currentState.filter(
// only add new internalModels which are not yet in the canonical state of this
// relationship (a new internalModel can be in the canonical state if it has
// been 'acknowleged' to be in the relationship via a store.push)
(internalModel) => internalModel.isNew() && toSet.indexOf(internalModel) === -1
);
toSet = toSet.concat(newInternalModels);
// 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;
this.arrayContentDidChange(diff.firstChangeIndex, diff.removedCount, diff.addedCount);
if (isInitialized && diff.addedCount > 0) {
//notify only on additions
//TODO only notify if unloaded
this.relationship.notifyHasManyChanged();
}
}
},
internalReplace(idx, amt, objects) {
if (!objects) {
objects = [];
}
this.arrayContentWillChange(idx, amt, objects.length);
this.currentState.splice.apply(this.currentState, [idx, amt].concat(objects));
this.set('length', this.currentState.length);
this.arrayContentDidChange(idx, amt, objects.length);
},
//TODO(Igor) optimize
_removeInternalModels(internalModels) {
for (let i=0; i < internalModels.length; i++) {
let index = this.currentState.indexOf(internalModels[i]);
this.internalReplace(index, 1);
}
},
//TODO(Igor) optimize
_addInternalModels(internalModels, idx) {
if (idx === undefined) {
idx = this.currentState.length;
}
this.internalReplace(idx, 0, internalModels);
},
replace(idx, amt, objects) {
let internalModels;
if (amt > 0) {
internalModels = this.currentState.slice(idx, idx+amt);
this.get('relationship').removeInternalModels(internalModels);
}
if (objects) {
this.get('relationship').addInternalModels(objects.map(obj => obj._internalModel), idx);
}
},
/**
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() {
return this.relationship.reload();
},
/**
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;
}
});