UNPKG

27.9 kBPlain TextView Raw
1/**
2 @module @ember-data/record-data
3*/
4import { assert, inspect, warn } from '@ember/debug';
5import { assign } from '@ember/polyfills';
6import { run } from '@ember/runloop';
7import { isEqual } from '@ember/utils';
8import { DEBUG } from '@glimmer/env';
9
10import { RECORD_DATA_ERRORS, RECORD_DATA_STATE } from '@ember-data/canary-features';
11
12import coerceId from './coerce-id';
13import Relationships from './relationships/state/create';
14
15type RecordIdentifier = import('@ember-data/store/-private/ts-interfaces/identifier').RecordIdentifier;
16type RecordDataStoreWrapper = import('@ember-data/store/-private/ts-interfaces/record-data-store-wrapper').RecordDataStoreWrapper;
17type RelationshipRecordData = import('./ts-interfaces/relationship-record-data').RelationshipRecordData;
18type DefaultSingleResourceRelationship = import('./ts-interfaces/relationship-record-data').DefaultSingleResourceRelationship;
19type DefaultCollectionResourceRelationship = import('./ts-interfaces/relationship-record-data').DefaultCollectionResourceRelationship;
20type JsonApiResource = import('@ember-data/store/-private/ts-interfaces/record-data-json-api').JsonApiResource;
21type JsonApiValidationError = import('@ember-data/store/-private/ts-interfaces/record-data-json-api').JsonApiValidationError;
22type AttributesHash = import('@ember-data/store/-private/ts-interfaces/record-data-json-api').AttributesHash;
23type RecordData = import('@ember-data/store/-private/ts-interfaces/record-data').RecordData;
24type ChangedAttributesHash = import('@ember-data/store/-private/ts-interfaces/record-data').ChangedAttributesHash;
25type Relationship = import('./relationships/state/relationship').default;
26type ManyRelationship = import('./relationships/state/has-many').default;
27type BelongsToRelationship = import('./relationships/state/belongs-to').default;
28
29let nextBfsId = 1;
30
31export default class RecordDataDefault implements RelationshipRecordData {
32 _errors?: JsonApiValidationError[];
33 __relationships: Relationships | null;
34 __implicitRelationships: { [key: string]: Relationship } | null;
35 modelName: string;
36 clientId: string;
37 id: string | null;
38 isDestroyed: boolean;
39 _isNew: boolean;
40 _bfsId: number;
41 __attributes: any;
42 __inFlightAttributes: any;
43 __data: any;
44 _scheduledDestroy: any;
45 _isDeleted: boolean;
46 _isDeletionCommited: boolean;
47
48 constructor(private identifier: RecordIdentifier, public storeWrapper: RecordDataStoreWrapper) {
49 this.modelName = identifier.type;
50 this.clientId = identifier.lid;
51 this.id = identifier.id;
52
53 this.__relationships = null;
54 this.__implicitRelationships = null;
55 this.isDestroyed = false;
56 this._isNew = false;
57 this._isDeleted = false;
58 // Used during the mark phase of unloading to avoid checking the same internal
59 // model twice in the same scan
60 this._bfsId = 0;
61 this.reset();
62 }
63
64 // PUBLIC API
65 getResourceIdentifier(): RecordIdentifier {
66 return this.identifier;
67 }
68
69 pushData(data: JsonApiResource, calculateChange: boolean) {
70 let changedKeys;
71
72 if (this._isNew) {
73 this._isNew = false;
74 this.notifyStateChange();
75 }
76
77 if (calculateChange) {
78 changedKeys = this._changedKeys(data.attributes);
79 }
80
81 assign(this._data, data.attributes);
82 if (this.__attributes) {
83 // only do if we have attribute changes
84 this._updateChangedAttributes();
85 }
86
87 if (data.relationships) {
88 this._setupRelationships(data);
89 }
90
91 if (data.id) {
92 this.id = coerceId(data.id);
93 }
94
95 return changedKeys;
96 }
97
98 willCommit() {
99 this._inFlightAttributes = this._attributes;
100 this._attributes = null;
101 }
102
103 hasChangedAttributes() {
104 return this.__attributes !== null && Object.keys(this.__attributes).length > 0;
105 }
106
107 _clearErrors() {
108 if (RECORD_DATA_ERRORS) {
109 if (this._errors) {
110 this._errors = undefined;
111 this.storeWrapper.notifyErrorsChange(this.modelName, this.id, this.clientId);
112 }
113 }
114 }
115
116 getErrors(): JsonApiValidationError[] {
117 assert('Can not call getErrors unless the RECORD_DATA_ERRORS feature flag is on', !!RECORD_DATA_ERRORS);
118 if (RECORD_DATA_ERRORS) {
119 let errors: JsonApiValidationError[] = this._errors || [];
120 return errors;
121 } else {
122 return [];
123 }
124 }
125
126 // this is a hack bc we don't have access to the state machine
127 // and relationships need this info and @runspired didn't see
128 // how to get it just yet from storeWrapper.
129 isEmpty() {
130 return this.__attributes === null && this.__inFlightAttributes === null && this.__data === null;
131 }
132
133 deleteRecord() {
134 this._isDeleted = true;
135 this.notifyStateChange();
136 }
137
138 isDeleted() {
139 return this._isDeleted;
140 }
141
142 setIsDeleted(isDeleted: boolean): void {
143 this._isDeleted = isDeleted;
144 if (this._isNew) {
145 this._deletionConfirmed();
146 }
147 this.notifyStateChange();
148 }
149
150 isDeletionCommitted(): boolean {
151 return this._isDeletionCommited;
152 }
153
154 reset() {
155 this.__attributes = null;
156 this.__inFlightAttributes = null;
157 this.__data = null;
158 this._errors = undefined;
159 }
160
161 _setupRelationships(data) {
162 let relationships = this.storeWrapper.relationshipsDefinitionFor(this.modelName);
163 let keys = Object.keys(relationships);
164 for (let i = 0; i < keys.length; i++) {
165 let relationshipName = keys[i];
166
167 if (!data.relationships[relationshipName]) {
168 continue;
169 }
170
171 // in debug, assert payload validity eagerly
172 let relationshipData = data.relationships[relationshipName];
173
174 if (DEBUG) {
175 let storeWrapper = this.storeWrapper;
176 let recordData = this;
177 let relationshipMeta = relationships[relationshipName];
178 if (!relationshipData || !relationshipMeta) {
179 continue;
180 }
181
182 if (relationshipData.links) {
183 let isAsync = relationshipMeta.options && relationshipMeta.options.async !== false;
184 let relationship = this._relationships.get(relationshipName);
185 warn(
186 `You pushed a record of type '${this.modelName}' with a relationship '${relationshipName}' 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.`,
187 isAsync || relationshipData.data || relationship.hasAnyRelationshipData,
188 {
189 id: 'ds.store.push-link-for-sync-relationship',
190 }
191 );
192 } else if (relationshipData.data) {
193 if (relationshipMeta.kind === 'belongsTo') {
194 assert(
195 `A ${
196 this.modelName
197 } record was pushed into the store with the value of ${relationshipName} being ${inspect(
198 relationshipData.data
199 )}, but ${relationshipName} is a belongsTo relationship so the value must not be an array. You should probably check your data payload or serializer.`,
200 !Array.isArray(relationshipData.data)
201 );
202 assertRelationshipData(storeWrapper, recordData, relationshipData.data, relationshipMeta);
203 } else if (relationshipMeta.kind === 'hasMany') {
204 assert(
205 `A ${
206 this.modelName
207 } record was pushed into the store with the value of ${relationshipName} being '${inspect(
208 relationshipData.data
209 )}', but ${relationshipName} is a hasMany relationship so the value must be an array. You should probably check your data payload or serializer.`,
210 Array.isArray(relationshipData.data)
211 );
212 if (Array.isArray(relationshipData.data)) {
213 for (let i = 0; i < relationshipData.data.length; i++) {
214 assertRelationshipData(storeWrapper, recordData, relationshipData.data[i], relationshipMeta);
215 }
216 }
217 }
218 }
219 }
220
221 let relationship = this._relationships.get(relationshipName);
222
223 relationship.push(relationshipData);
224 }
225 }
226
227 /*
228 Checks if the attributes which are considered as changed are still
229 different to the state which is acknowledged by the server.
230
231 This method is needed when data for the internal model is pushed and the
232 pushed data might acknowledge dirty attributes as confirmed.
233
234 @method updateChangedAttributes
235 @private
236 */
237 _updateChangedAttributes() {
238 let changedAttributes = this.changedAttributes();
239 let changedAttributeNames = Object.keys(changedAttributes);
240 let attrs = this._attributes;
241
242 for (let i = 0, length = changedAttributeNames.length; i < length; i++) {
243 let attribute = changedAttributeNames[i];
244 let data = changedAttributes[attribute];
245 let oldData = data[0];
246 let newData = data[1];
247
248 if (oldData === newData) {
249 delete attrs[attribute];
250 }
251 }
252 }
253
254 /*
255 Returns an object, whose keys are changed properties, and value is an
256 [oldProp, newProp] array.
257
258 @method changedAttributes
259 @private
260 */
261 changedAttributes(): ChangedAttributesHash {
262 let oldData = this._data;
263 let currentData = this._attributes;
264 let inFlightData = this._inFlightAttributes;
265 let newData = assign({}, inFlightData, currentData);
266 let diffData = Object.create(null);
267 let newDataKeys = Object.keys(newData);
268
269 for (let i = 0, length = newDataKeys.length; i < length; i++) {
270 let key = newDataKeys[i];
271 diffData[key] = [oldData[key], newData[key]];
272 }
273
274 return diffData;
275 }
276
277 isNew() {
278 return this._isNew;
279 }
280
281 rollbackAttributes() {
282 let dirtyKeys;
283 this._isDeleted = false;
284
285 if (this.hasChangedAttributes()) {
286 dirtyKeys = Object.keys(this._attributes);
287 this._attributes = null;
288 }
289
290 if (this.isNew()) {
291 this.removeFromInverseRelationships(true);
292 this._isDeleted = true;
293 this._isNew = false;
294 }
295
296 this._inFlightAttributes = null;
297
298 this._clearErrors();
299 this.notifyStateChange();
300
301 return dirtyKeys;
302 }
303
304 _deletionConfirmed() {
305 this.removeFromInverseRelationships();
306 }
307
308 didCommit(data: JsonApiResource | null) {
309 if (this._isDeleted) {
310 this._deletionConfirmed();
311 this._isDeletionCommited = true;
312 }
313
314 this._isNew = false;
315 let newCanonicalAttributes: AttributesHash | null = null;
316 if (data) {
317 // this.store._internalModelDidReceiveRelationshipData(this.modelName, this.id, data.relationships);
318 if (data.relationships) {
319 this._setupRelationships(data);
320 }
321 if (data.id) {
322 // didCommit provided an ID, notify the store of it
323 this.storeWrapper.setRecordId(this.modelName, data.id, this.clientId);
324 this.id = coerceId(data.id);
325 }
326 newCanonicalAttributes = data.attributes || null;
327 }
328 let changedKeys = this._changedKeys(newCanonicalAttributes);
329
330 assign(this._data, this.__inFlightAttributes, newCanonicalAttributes);
331
332 this._inFlightAttributes = null;
333
334 this._updateChangedAttributes();
335 this._clearErrors();
336
337 this.notifyStateChange();
338 return changedKeys;
339 }
340
341 notifyStateChange() {
342 if (RECORD_DATA_STATE) {
343 this.storeWrapper.notifyStateChange(this.modelName, this.id, this.clientId);
344 }
345 }
346
347 // get ResourceIdentifiers for "current state"
348 getHasMany(key): DefaultCollectionResourceRelationship {
349 return (this._relationships.get(key) as ManyRelationship).getData();
350 }
351
352 // set a new "current state" via ResourceIdentifiers
353 setDirtyHasMany(key, recordDatas) {
354 let relationship = this._relationships.get(key);
355 relationship.clear();
356 relationship.addRecordDatas(recordDatas);
357 }
358
359 // append to "current state" via RecordDatas
360 addToHasMany(key, recordDatas, idx) {
361 this._relationships.get(key).addRecordDatas(recordDatas, idx);
362 }
363
364 // remove from "current state" via RecordDatas
365 removeFromHasMany(key, recordDatas) {
366 this._relationships.get(key).removeRecordDatas(recordDatas);
367 }
368
369 commitWasRejected(identifier?, errors?: JsonApiValidationError[]) {
370 let keys = Object.keys(this._inFlightAttributes);
371 if (keys.length > 0) {
372 let attrs = this._attributes;
373 for (let i = 0; i < keys.length; i++) {
374 if (attrs[keys[i]] === undefined) {
375 attrs[keys[i]] = this._inFlightAttributes[keys[i]];
376 }
377 }
378 }
379 this._inFlightAttributes = null;
380 if (RECORD_DATA_ERRORS) {
381 if (errors) {
382 this._errors = errors;
383 }
384 this.storeWrapper.notifyErrorsChange(this.modelName, this.id, this.clientId);
385 }
386 }
387
388 getBelongsTo(key: string): DefaultSingleResourceRelationship {
389 return (this._relationships.get(key) as BelongsToRelationship).getData();
390 }
391
392 setDirtyBelongsTo(key: string, recordData: RelationshipRecordData) {
393 (this._relationships.get(key) as BelongsToRelationship).setRecordData(recordData);
394 }
395
396 setDirtyAttribute(key: string, value: any) {
397 let originalValue;
398 // Add the new value to the changed attributes hash
399 this._attributes[key] = value;
400
401 if (key in this._inFlightAttributes) {
402 originalValue = this._inFlightAttributes[key];
403 } else {
404 originalValue = this._data[key];
405 }
406 // If we went back to our original value, we shouldn't keep the attribute around anymore
407 if (value === originalValue) {
408 delete this._attributes[key];
409 }
410 }
411
412 // internal set coming from the model
413 __setId(id: string) {
414 if (this.id !== id) {
415 this.id = id;
416 }
417 }
418
419 getAttr(key: string): string {
420 if (key in this._attributes) {
421 return this._attributes[key];
422 } else if (key in this._inFlightAttributes) {
423 return this._inFlightAttributes[key];
424 } else {
425 return this._data[key];
426 }
427 }
428
429 hasAttr(key: string): boolean {
430 return key in this._attributes || key in this._inFlightAttributes || key in this._data;
431 }
432
433 unloadRecord() {
434 if (this.isDestroyed) {
435 return;
436 }
437 this._destroyRelationships();
438 this.reset();
439 if (!this._scheduledDestroy) {
440 this._scheduledDestroy = run.backburner.schedule('destroy', this, '_cleanupOrphanedRecordDatas');
441 }
442 }
443
444 _cleanupOrphanedRecordDatas() {
445 let relatedRecordDatas = this._allRelatedRecordDatas();
446 if (areAllModelsUnloaded(relatedRecordDatas)) {
447 for (let i = 0; i < relatedRecordDatas.length; ++i) {
448 let recordData = relatedRecordDatas[i];
449 if (!recordData.isDestroyed) {
450 recordData.destroy();
451 }
452 }
453 }
454 this._scheduledDestroy = null;
455 }
456
457 destroy() {
458 this._relationships.forEach((name, rel) => rel.destroy());
459 this.isDestroyed = true;
460 this.storeWrapper.disconnectRecord(this.modelName, this.id, this.clientId);
461 }
462
463 isRecordInUse() {
464 return this.storeWrapper.isRecordInUse(this.modelName, this.id, this.clientId);
465 }
466
467 /**
468 Iterates over the set of internal models reachable from `this` across exactly one
469 relationship.
470 */
471 _directlyRelatedRecordDatasIterable = () => {
472 const initializedRelationships = this._relationships.initializedRelationships;
473 const relationships = Object.keys(initializedRelationships).map(key => initializedRelationships[key]);
474
475 let i = 0;
476 let j = 0;
477 let k = 0;
478
479 const findNext = () => {
480 while (i < relationships.length) {
481 while (j < 2) {
482 let members = j === 0 ? relationships[i].members.list : relationships[i].canonicalMembers.list;
483 while (k < members.length) {
484 return members[k++];
485 }
486 k = 0;
487 j++;
488 }
489 j = 0;
490 i++;
491 }
492 return undefined;
493 };
494
495 return {
496 iterator() {
497 return {
498 next: () => {
499 const value = findNext();
500 return { value, done: value === undefined };
501 },
502 };
503 },
504 };
505 };
506
507 /**
508 Computes the set of internal models reachable from this internal model.
509
510 Reachability is determined over the relationship graph (ie a graph where
511 nodes are internal models and edges are belongs to or has many
512 relationships).
513
514 @return {Array} An array including `this` and all internal models reachable
515 from `this`.
516 */
517 _allRelatedRecordDatas(): RecordDataDefault[] {
518 let array: RecordDataDefault[] = [];
519 let queue: RecordDataDefault[] = [];
520 let bfsId = nextBfsId++;
521 queue.push(this);
522 this._bfsId = bfsId;
523 while (queue.length > 0) {
524 let node = queue.shift() as RecordDataDefault;
525 array.push(node);
526
527 const iterator = this._directlyRelatedRecordDatasIterable().iterator();
528 for (let obj = iterator.next(); !obj.done; obj = iterator.next()) {
529 const recordData = obj.value;
530 if (recordData instanceof RecordDataDefault) {
531 assert('Internal Error: seen a future bfs iteration', recordData._bfsId <= bfsId);
532 if (recordData._bfsId < bfsId) {
533 queue.push(recordData);
534 recordData._bfsId = bfsId;
535 }
536 }
537 }
538 }
539
540 return array;
541 }
542
543 isAttrDirty(key: string): boolean {
544 if (this._attributes[key] === undefined) {
545 return false;
546 }
547 let originalValue;
548 if (this._inFlightAttributes[key] !== undefined) {
549 originalValue = this._inFlightAttributes[key];
550 } else {
551 originalValue = this._data[key];
552 }
553
554 return originalValue !== this._attributes[key];
555 }
556
557 get _attributes() {
558 if (this.__attributes === null) {
559 this.__attributes = Object.create(null);
560 }
561 return this.__attributes;
562 }
563
564 set _attributes(v) {
565 this.__attributes = v;
566 }
567
568 get _relationships() {
569 if (this.__relationships === null) {
570 this.__relationships = new Relationships(this);
571 }
572
573 return this.__relationships;
574 }
575
576 get _data() {
577 if (this.__data === null) {
578 this.__data = Object.create(null);
579 }
580 return this.__data;
581 }
582
583 set _data(v) {
584 this.__data = v;
585 }
586
587 /*
588 implicit relationships are relationship which have not been declared but the inverse side exists on
589 another record somewhere
590 For example if there was
591
592 ```app/models/comment.js
593 import Model, { attr } from '@ember-data/model';
594
595 export default Model.extend({
596 name: attr()
597 });
598 ```
599
600 but there is also
601
602 ```app/models/post.js
603 import Model, { attr, hasMany } from '@ember-data/model';
604
605 export default Model.extend({
606 name: attr(),
607 comments: hasMany('comment')
608 });
609 ```
610
611 would have a implicit post relationship in order to be do things like remove ourselves from the post
612 when we are deleted
613 */
614 get _implicitRelationships() {
615 if (this.__implicitRelationships === null) {
616 let relationships = Object.create(null);
617 this.__implicitRelationships = relationships;
618 return relationships;
619 }
620 return this.__implicitRelationships;
621 }
622
623 get _inFlightAttributes() {
624 if (this.__inFlightAttributes === null) {
625 this.__inFlightAttributes = Object.create(null);
626 }
627 return this.__inFlightAttributes;
628 }
629
630 set _inFlightAttributes(v) {
631 this.__inFlightAttributes = v;
632 }
633
634 /**
635 * Receives options passed to `store.createRecord` and is given the opportunity
636 * to handle them.
637 *
638 * The return value is an object of options to pass to `Record.create()`
639 *
640 * @param options
641 * @private
642 */
643 _initRecordCreateOptions(options) {
644 let createOptions = {};
645
646 if (options !== undefined) {
647 let { modelName, storeWrapper } = this;
648 let attributeDefs = storeWrapper.attributesDefinitionFor(modelName);
649 let relationshipDefs = storeWrapper.relationshipsDefinitionFor(modelName);
650 let relationships = this._relationships;
651 let propertyNames = Object.keys(options);
652
653 for (let i = 0; i < propertyNames.length; i++) {
654 let name = propertyNames[i];
655 let propertyValue = options[name];
656
657 if (name === 'id') {
658 this.id = propertyValue;
659 continue;
660 }
661
662 let fieldType = relationshipDefs[name] || attributeDefs[name];
663 let kind = fieldType !== undefined ? fieldType.kind : null;
664 let relationship;
665
666 switch (kind) {
667 case 'attribute':
668 this.setDirtyAttribute(name, propertyValue);
669 break;
670 case 'belongsTo':
671 this.setDirtyBelongsTo(name, propertyValue);
672 relationship = relationships.get(name);
673 relationship.setHasAnyRelationshipData(true);
674 relationship.setRelationshipIsEmpty(false);
675 break;
676 case 'hasMany':
677 this.setDirtyHasMany(name, propertyValue);
678 relationship = relationships.get(name);
679 relationship.setHasAnyRelationshipData(true);
680 relationship.setRelationshipIsEmpty(false);
681 break;
682 default:
683 // reflect back (pass-thru) unknown properties
684 createOptions[name] = propertyValue;
685 }
686 }
687 }
688
689 return createOptions;
690 }
691
692 /*
693
694
695 TODO IGOR AND DAVID this shouldn't be public
696 This method should only be called by records in the `isNew()` state OR once the record
697 has been deleted and that deletion has been persisted.
698
699 It will remove this record from any associated relationships.
700
701 If `isNew` is true (default false), it will also completely reset all
702 relationships to an empty state as well.
703
704 @method removeFromInverseRelationships
705 @param {Boolean} isNew whether to unload from the `isNew` perspective
706 @private
707 */
708 removeFromInverseRelationships(isNew = false) {
709 this._relationships.forEach((name, rel) => {
710 rel.removeCompletelyFromInverse();
711 if (isNew === true) {
712 rel.clear();
713 }
714 });
715 this.__relationships = null;
716
717 let implicitRelationships = this._implicitRelationships;
718 this.__implicitRelationships = null;
719
720 Object.keys(implicitRelationships).forEach(key => {
721 let rel = implicitRelationships[key];
722
723 rel.removeCompletelyFromInverse();
724 if (isNew === true) {
725 rel.clear();
726 }
727 });
728 }
729
730 _destroyRelationships() {
731 let relationships = this._relationships;
732 relationships.forEach((name, rel) => destroyRelationship(rel));
733
734 let implicitRelationships = this._implicitRelationships;
735 this.__implicitRelationships = null;
736 Object.keys(implicitRelationships).forEach(key => {
737 let rel = implicitRelationships[key];
738 destroyRelationship(rel);
739 });
740 }
741
742 clientDidCreate() {
743 this._isNew = true;
744 }
745
746 /*
747 Ember Data has 3 buckets for storing the value of an attribute on an internalModel.
748
749 `_data` holds all of the attributes that have been acknowledged by
750 a backend via the adapter. When rollbackAttributes is called on a model all
751 attributes will revert to the record's state in `_data`.
752
753 `_attributes` holds any change the user has made to an attribute
754 that has not been acknowledged by the adapter. Any values in
755 `_attributes` are have priority over values in `_data`.
756
757 `_inFlightAttributes`. When a record is being synced with the
758 backend the values in `_attributes` are copied to
759 `_inFlightAttributes`. This way if the backend acknowledges the
760 save but does not return the new state Ember Data can copy the
761 values from `_inFlightAttributes` to `_data`. Without having to
762 worry about changes made to `_attributes` while the save was
763 happenign.
764
765
766 Changed keys builds a list of all of the values that may have been
767 changed by the backend after a successful save.
768
769 It does this by iterating over each key, value pair in the payload
770 returned from the server after a save. If the `key` is found in
771 `_attributes` then the user has a local changed to the attribute
772 that has not been synced with the server and the key is not
773 included in the list of changed keys.
774
775
776
777 If the value, for a key differs from the value in what Ember Data
778 believes to be the truth about the backend state (A merger of the
779 `_data` and `_inFlightAttributes` objects where
780 `_inFlightAttributes` has priority) then that means the backend
781 has updated the value and the key is added to the list of changed
782 keys.
783
784 @method _changedKeys
785 @private
786 */
787 /*
788 TODO IGOR DAVID
789 There seems to be a potential bug here, where we will return keys that are not
790 in the schema
791 */
792 _changedKeys(updates) {
793 let changedKeys: string[] = [];
794
795 if (updates) {
796 let original, i, value, key;
797 let keys = Object.keys(updates);
798 let length = keys.length;
799 let hasAttrs = this.hasChangedAttributes();
800 let attrs;
801 if (hasAttrs) {
802 attrs = this._attributes;
803 }
804
805 original = assign(Object.create(null), this._data, this.__inFlightAttributes);
806
807 for (i = 0; i < length; i++) {
808 key = keys[i];
809 value = updates[key];
810
811 // A value in _attributes means the user has a local change to
812 // this attributes. We never override this value when merging
813 // updates from the backend so we should not sent a change
814 // notification if the server value differs from the original.
815 if (hasAttrs === true && attrs[key] !== undefined) {
816 continue;
817 }
818
819 if (!isEqual(original[key], value)) {
820 changedKeys.push(key);
821 }
822 }
823 }
824
825 return changedKeys;
826 }
827
828 toString() {
829 return `<${this.modelName}:${this.id}>`;
830 }
831}
832
833function assertRelationshipData(store, recordData, data, meta) {
834 assert(
835 `A ${recordData.modelName} record was pushed into the store with the value of ${meta.key} being '${JSON.stringify(
836 data
837 )}', but ${
838 meta.key
839 } is a belongsTo relationship so the value must not be an array. You should probably check your data payload or serializer.`,
840 !Array.isArray(data)
841 );
842 assert(
843 `Encountered a relationship identifier without a type for the ${meta.kind} relationship '${
844 meta.key
845 }' on ${recordData}, expected a json-api identifier with type '${meta.type}' but found '${JSON.stringify(
846 data
847 )}'. Please check your serializer and make sure it is serializing the relationship payload into a JSON API format.`,
848 data === null || (typeof data.type === 'string' && data.type.length)
849 );
850 assert(
851 `Encountered a relationship identifier without an id for the ${meta.kind} relationship '${
852 meta.key
853 }' on ${recordData}, expected a json-api identifier but found '${JSON.stringify(
854 data
855 )}'. Please check your serializer and make sure it is serializing the relationship payload into a JSON API format.`,
856 data === null || !!coerceId(data.id)
857 );
858 assert(
859 `Encountered a relationship identifier with type '${data.type}' for the ${meta.kind} relationship '${meta.key}' on ${recordData}, Expected a json-api identifier with type '${meta.type}'. No model was found for '${data.type}'.`,
860 data === null || !data.type || store._hasModelFor(data.type)
861 );
862}
863
864// Handle dematerialization for relationship `rel`. In all cases, notify the
865// relationship of the dematerialization: this is done so the relationship can
866// notify its inverse which needs to update state
867//
868// If the inverse is sync, unloading this record is treated as a client-side
869// delete, so we remove the inverse records from this relationship to
870// disconnect the graph. Because it's not async, we don't need to keep around
871// the internalModel as an id-wrapper for references and because the graph is
872// disconnected we can actually destroy the internalModel when checking for
873// orphaned models.
874function destroyRelationship(rel) {
875 rel.recordDataDidDematerialize();
876
877 if (rel._inverseIsSync()) {
878 rel.removeAllRecordDatasFromOwn();
879 rel.removeAllCanonicalRecordDatasFromOwn();
880 }
881}
882
883function areAllModelsUnloaded(recordDatas) {
884 for (let i = 0; i < recordDatas.length; ++i) {
885 if (recordDatas[i].isRecordInUse()) {
886 return false;
887 }
888 }
889 return true;
890}