var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /** Manages the payloads for both sides of a single relationship, across all model instances. For example, with const User = DS.Model.extend({ hobbies: DS.hasMany('hobby') }); const Hobby = DS.Model.extend({ user: DS.belongsTo('user') }); let relationshipPayloads = new RelationshipPayloads('user', 'hobbies', 'hobby', 'user'); let userPayload = { data: { id: 1, type: 'user', relationships: { hobbies: { data: [{ id: 2, type: 'hobby', }] } } } }; // here we expect the payload of the individual relationship relationshipPayloads.push('user', 1, 'hobbies', userPayload.data.relationships.hobbies); relationshipPayloads.get('user', 1, 'hobbies'); relationshipPayloads.get('hobby', 2, 'user'); @class RelationshipPayloads @private */ var RelationshipPayloads = function () { function RelationshipPayloads(store, modelName, relationshipName, relationshipMeta, inverseModelName, inverseRelationshipName, inverseRelationshipMeta) { _classCallCheck(this, RelationshipPayloads); this._store = store; this._lhsModelName = modelName; this._lhsRelationshipName = relationshipName; this._lhsRelationshipMeta = relationshipMeta; this._rhsModelName = inverseModelName; this._rhsRelationshipName = inverseRelationshipName; this._rhsRelationshipMeta = inverseRelationshipMeta; // a map of id -> payloads for the left hand side of the relationship. this._lhsPayloads = Object.create(null); if (modelName !== inverseModelName || relationshipName !== inverseRelationshipName) { // The common case of a non-reflexive relationship, or a reflexive // relationship whose inverse is not itself this._rhsPayloads = Object.create(null); this._isReflexive = false; } else { // Edge case when we have a reflexive relationship to itself // eg user hasMany friends inverse friends // // In this case there aren't really two sides to the relationship, but // we set `_rhsPayloads = _lhsPayloads` to make things easier to reason // about this._rhsPayloads = this._lhsPayloads; this._isReflexive = true; } // When we push relationship payloads, just stash them in a queue until // somebody actually asks for one of them. // // This is a queue of the relationship payloads that have been pushed for // either side of this relationship this._pendingPayloads = []; } /** Get the payload for the relationship of an individual record. This might return the raw payload as pushed into the store, or one computed from the payload of the inverse relationship. @method */ RelationshipPayloads.prototype.get = function get(modelName, id, relationshipName) { this._flushPending(); if (this._isLHS(modelName, relationshipName)) { return this._lhsPayloads[id]; } else { (true && !(this._isRHS(modelName, relationshipName)) && Ember.assert(modelName + ':' + relationshipName + ' is not either side of this relationship, ' + this._lhsModelName + ':' + this._lhsRelationshipName + '<->' + this._rhsModelName + ':' + this._rhsRelationshipName, this._isRHS(modelName, relationshipName))); return this._rhsPayloads[id]; } }; /** Push a relationship payload for an individual record. This will make the payload available later for both this relationship and its inverse. @method */ RelationshipPayloads.prototype.push = function push(modelName, id, relationshipName, relationshipData) { this._pendingPayloads.push([modelName, id, relationshipName, relationshipData]); }; /** Unload the relationship payload for an individual record. This does not unload the inverse relationship payload. @method */ RelationshipPayloads.prototype.unload = function unload(modelName, id, relationshipName) { this._flushPending(); if (this._isLHS(modelName, relationshipName)) { delete this._lhsPayloads[id]; } else { (true && !(this._isRHS(modelName, relationshipName)) && Ember.assert(modelName + ':' + relationshipName + ' is not either side of this relationship, ' + this._lhsModelName + ':' + this._lhsRelationshipName + '<->' + this._rhsModelName + ':' + this._rhsRelationshipName, this._isRHS(modelName, relationshipName))); delete this._rhsPayloads[id]; } }; /** @return {boolean} true iff `modelName` and `relationshipName` refer to the left hand side of this relationship, as opposed to the right hand side. @method */ RelationshipPayloads.prototype._isLHS = function _isLHS(modelName, relationshipName) { return modelName === this._lhsModelName && relationshipName === this._lhsRelationshipName; }; /** @return {boolean} true iff `modelName` and `relationshipName` refer to the right hand side of this relationship, as opposed to the left hand side. @method */ RelationshipPayloads.prototype._isRHS = function _isRHS(modelName, relationshipName) { return modelName === this._rhsModelName && relationshipName === this._rhsRelationshipName; }; RelationshipPayloads.prototype._flushPending = function _flushPending() { if (this._pendingPayloads.length === 0) { return; } var payloadsToBeProcessed = this._pendingPayloads.splice(0, this._pendingPayloads.length); for (var i = 0; i < payloadsToBeProcessed.length; ++i) { var modelName = payloadsToBeProcessed[i][0]; var id = payloadsToBeProcessed[i][1]; var relationshipName = payloadsToBeProcessed[i][2]; var relationshipData = payloadsToBeProcessed[i][3]; // TODO: maybe delay this allocation slightly? var inverseRelationshipData = { data: { id: id, type: modelName } // start flushing this individual payload. The logic is the same whether // it's for the left hand side of the relationship or the right hand side, // except the role of primary and inverse idToPayloads is reversed // };var previousPayload = void 0; var idToPayloads = void 0; var inverseIdToPayloads = void 0; var inverseIsMany = void 0; if (this._isLHS(modelName, relationshipName)) { previousPayload = this._lhsPayloads[id]; idToPayloads = this._lhsPayloads; inverseIdToPayloads = this._rhsPayloads; inverseIsMany = this._rhsRelationshipIsMany; } else { (true && !(this._isRHS(modelName, relationshipName)) && Ember.assert(modelName + ':' + relationshipName + ' is not either side of this relationship, ' + this._lhsModelName + ':' + this._lhsRelationshipName + '<->' + this._rhsModelName + ':' + this._rhsRelationshipName, this._isRHS(modelName, relationshipName))); previousPayload = this._rhsPayloads[id]; idToPayloads = this._rhsPayloads; inverseIdToPayloads = this._lhsPayloads; inverseIsMany = this._lhsRelationshipIsMany; } // actually flush this individual payload // // We remove the previous inverse before populating our current one // because we may have multiple payloads for the same relationship, in // which case the last one wins. // // eg if user hasMany helicopters, and helicopter belongsTo user and we see // // [{ // data: { // id: 1, // type: 'helicopter', // relationships: { // user: { // id: 2, // type: 'user' // } // } // } // }, { // data: { // id: 1, // type: 'helicopter', // relationships: { // user: { // id: 4, // type: 'user' // } // } // } // }] // // Then we will initially have set user:2 as having helicopter:1, which we // need to remove before adding helicopter:1 to user:4 // // only remove relationship information before adding if there is relationshipData.data // * null is considered new information "empty", and it should win // * undefined is NOT considered new information, we should keep original state // * anything else is considered new information, and it should win if (relationshipData.data !== undefined) { this._removeInverse(id, previousPayload, inverseIdToPayloads); } idToPayloads[id] = relationshipData; this._populateInverse(relationshipData, inverseRelationshipData, inverseIdToPayloads, inverseIsMany); } }; /** Populate the inverse relationship for `relationshipData`. If `relationshipData` is an array (eg because the relationship is hasMany) this means populate each inverse, otherwise populate only the single inverse. @private @method */ RelationshipPayloads.prototype._populateInverse = function _populateInverse(relationshipData, inversePayload, inverseIdToPayloads, inverseIsMany) { if (!relationshipData.data) { // This id doesn't have an inverse, eg a belongsTo with a payload // { data: null }, so there's nothing to populate return; } if (Array.isArray(relationshipData.data)) { for (var i = 0; i < relationshipData.data.length; ++i) { var inverseId = relationshipData.data[i].id; this._addToInverse(inversePayload, inverseId, inverseIdToPayloads, inverseIsMany); } } else { var _inverseId = relationshipData.data.id; this._addToInverse(inversePayload, _inverseId, inverseIdToPayloads, inverseIsMany); } }; /** Actually add `inversePayload` to `inverseIdToPayloads`. This is part of `_populateInverse` after we've normalized the case of `relationshipData` being either an array or a pojo. We still have to handle the case that the *inverse* relationship payload may be an array or pojo. @private @method */ RelationshipPayloads.prototype._addToInverse = function _addToInverse(inversePayload, inverseId, inverseIdToPayloads, inverseIsMany) { if (this._isReflexive && inversePayload.data.id === inverseId) { // eg <user:1>.friends = [{ id: 1, type: 'user' }] return; } var existingPayload = inverseIdToPayloads[inverseId]; var existingData = existingPayload && existingPayload.data; if (existingData) { // There already is an inverse, either add or overwrite depehnding on // whether the inverse is a many relationship or not // if (Array.isArray(existingData)) { existingData.push(inversePayload.data); } else { inverseIdToPayloads[inverseId] = inversePayload; } } else { // first time we're populating the inverse side // if (inverseIsMany) { inverseIdToPayloads[inverseId] = { data: [inversePayload.data] }; } else { inverseIdToPayloads[inverseId] = inversePayload; } } }; /** Remove the relationship in `previousPayload` from its inverse(s), because this relationship payload has just been updated (eg because the same relationship had multiple payloads pushed before the relationship was initialized). @method */ RelationshipPayloads.prototype._removeInverse = function _removeInverse(id, previousPayload, inverseIdToPayloads) { var data = previousPayload && previousPayload.data; if (!data) { // either this is the first time we've seen a payload for this id, or its // previous payload indicated that it had no inverse, eg a belongsTo // relationship with payload { data: null } // // In either case there's nothing that needs to be removed from the // inverse map of payloads return; } if (Array.isArray(data)) { // TODO: diff rather than removeall addall? for (var i = 0; i < data.length; ++i) { this._removeFromInverse(id, data[i].id, inverseIdToPayloads); } } else { this._removeFromInverse(id, data.id, inverseIdToPayloads); } }; /** Remove `id` from its inverse record with id `inverseId`. If the inverse relationship is a belongsTo, this means just setting it to null, if the inverse relationship is a hasMany, then remove that id from its array of ids. @method */ RelationshipPayloads.prototype._removeFromInverse = function _removeFromInverse(id, inverseId, inversePayloads) { var inversePayload = inversePayloads[inverseId]; var data = inversePayload && inversePayload.data; if (!data) { return; } if (Array.isArray(data)) { inversePayload.data = data.filter(function (x) { return x.id !== id; }); } else { inversePayloads[inverseId] = { data: null }; } }; _createClass(RelationshipPayloads, [{ key: '_lhsRelationshipIsMany', get: function get() { return this._lhsRelationshipMeta && this._lhsRelationshipMeta.kind === 'hasMany'; } }, { key: '_rhsRelationshipIsMany', get: function get() { return this._rhsRelationshipMeta && this._rhsRelationshipMeta.kind === 'hasMany'; } }]); return RelationshipPayloads; }(); export default RelationshipPayloads;