@ember-data/store 4.6.1 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/addon/-debug/index.js +35 -13
- package/addon/-private/{identifier-cache.ts → caches/identifier-cache.ts} +148 -73
- package/addon/-private/caches/instance-cache.ts +690 -0
- package/addon/-private/{record-data-for.ts → caches/record-data-for.ts} +2 -7
- package/addon/-private/index.ts +44 -24
- package/addon/-private/{model → legacy-model-support}/record-reference.ts +15 -13
- package/addon/-private/{schema-definition-service.ts → legacy-model-support/schema-definition-service.ts} +13 -9
- package/addon/-private/{model → legacy-model-support}/shim-model-class.ts +18 -11
- package/addon/-private/managers/record-array-manager.ts +377 -0
- package/addon/-private/managers/record-data-manager.ts +845 -0
- package/addon/-private/managers/record-data-store-wrapper.ts +421 -0
- package/addon/-private/managers/record-notification-manager.ts +109 -0
- package/addon/-private/network/fetch-manager.ts +567 -0
- package/addon/-private/{finders.js → network/finders.js} +14 -17
- package/addon/-private/{request-cache.ts → network/request-cache.ts} +21 -18
- package/addon/-private/{snapshot-record-array.ts → network/snapshot-record-array.ts} +14 -31
- package/addon/-private/{snapshot.ts → network/snapshot.ts} +40 -49
- package/addon/-private/{promise-proxies.ts → proxies/promise-proxies.ts} +76 -15
- package/addon/-private/{promise-proxy-base.js → proxies/promise-proxy-base.js} +0 -0
- package/addon/-private/record-arrays/identifier-array.ts +924 -0
- package/addon/-private/{core-store.ts → store-service.ts} +574 -215
- package/addon/-private/{coerce-id.ts → utils/coerce-id.ts} +1 -1
- package/addon/-private/{common.js → utils/common.js} +1 -2
- package/addon/-private/utils/construct-resource.ts +2 -2
- package/addon/-private/{identifer-debug-consts.ts → utils/identifer-debug-consts.ts} +0 -0
- package/addon/-private/utils/is-non-empty-string.ts +1 -1
- package/addon/-private/{normalize-model-name.ts → utils/normalize-model-name.ts} +1 -3
- package/addon/-private/utils/promise-record.ts +5 -6
- package/addon/-private/{serializer-response.ts → utils/serializer-response.ts} +2 -2
- package/addon/-private/utils/uuid-polyfill.ts +73 -0
- package/package.json +12 -8
- package/addon/-private/backburner.js +0 -25
- package/addon/-private/errors-utils.js +0 -146
- package/addon/-private/fetch-manager.ts +0 -597
- package/addon/-private/identity-map.ts +0 -54
- package/addon/-private/instance-cache.ts +0 -387
- package/addon/-private/internal-model-factory.ts +0 -359
- package/addon/-private/internal-model-map.ts +0 -121
- package/addon/-private/model/internal-model.ts +0 -602
- package/addon/-private/record-array-manager.ts +0 -444
- package/addon/-private/record-arrays/adapter-populated-record-array.ts +0 -130
- package/addon/-private/record-arrays/record-array.ts +0 -318
- package/addon/-private/record-data-store-wrapper.ts +0 -243
- package/addon/-private/record-notification-manager.ts +0 -73
- package/addon/-private/weak-cache.ts +0 -125
|
@@ -1,602 +0,0 @@
|
|
|
1
|
-
import { assert } from '@ember/debug';
|
|
2
|
-
import { _backburner as emberBackburner, cancel, run } from '@ember/runloop';
|
|
3
|
-
import { DEBUG } from '@glimmer/env';
|
|
4
|
-
|
|
5
|
-
import { HAS_MODEL_PACKAGE } from '@ember-data/private-build-infra';
|
|
6
|
-
import type { DSModel } from '@ember-data/types/q/ds-model';
|
|
7
|
-
import type { StableRecordIdentifier } from '@ember-data/types/q/identifier';
|
|
8
|
-
import type { ChangedAttributesHash, RecordData } from '@ember-data/types/q/record-data';
|
|
9
|
-
import type { JsonApiResource, JsonApiValidationError } from '@ember-data/types/q/record-data-json-api';
|
|
10
|
-
import type { RecordInstance } from '@ember-data/types/q/record-instance';
|
|
11
|
-
|
|
12
|
-
import type Store from '../core-store';
|
|
13
|
-
import { errorsHashToArray } from '../errors-utils';
|
|
14
|
-
import { internalModelFactoryFor } from '../internal-model-factory';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
@module @ember-data/store
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
function isDSModel(record: RecordInstance | null): record is DSModel {
|
|
21
|
-
return (
|
|
22
|
-
HAS_MODEL_PACKAGE &&
|
|
23
|
-
!!record &&
|
|
24
|
-
'constructor' in record &&
|
|
25
|
-
'isModel' in record.constructor &&
|
|
26
|
-
record.constructor.isModel === true
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export default class InternalModel {
|
|
31
|
-
declare _id: string | null;
|
|
32
|
-
declare modelName: string;
|
|
33
|
-
declare clientId: string;
|
|
34
|
-
declare hasRecordData: boolean;
|
|
35
|
-
declare _isDestroyed: boolean;
|
|
36
|
-
declare isError: boolean;
|
|
37
|
-
declare _pendingRecordArrayManagerFlush: boolean;
|
|
38
|
-
declare _isDematerializing: boolean;
|
|
39
|
-
declare _doNotDestroy: boolean;
|
|
40
|
-
declare isDestroying: boolean;
|
|
41
|
-
declare _isUpdatingId: boolean;
|
|
42
|
-
declare _deletedRecordWasNew: boolean;
|
|
43
|
-
|
|
44
|
-
// Not typed yet
|
|
45
|
-
declare _scheduledDestroy: any;
|
|
46
|
-
declare _modelClass: any;
|
|
47
|
-
declare __recordArrays: any;
|
|
48
|
-
declare error: any;
|
|
49
|
-
declare store: Store;
|
|
50
|
-
declare identifier: StableRecordIdentifier;
|
|
51
|
-
declare hasRecord: boolean;
|
|
52
|
-
|
|
53
|
-
constructor(store: Store, identifier: StableRecordIdentifier) {
|
|
54
|
-
this.store = store;
|
|
55
|
-
this.identifier = identifier;
|
|
56
|
-
this._id = identifier.id;
|
|
57
|
-
this._isUpdatingId = false;
|
|
58
|
-
this.modelName = identifier.type;
|
|
59
|
-
this.clientId = identifier.lid;
|
|
60
|
-
this.hasRecord = false;
|
|
61
|
-
|
|
62
|
-
this.hasRecordData = false;
|
|
63
|
-
|
|
64
|
-
this._isDestroyed = false;
|
|
65
|
-
this._doNotDestroy = false;
|
|
66
|
-
this.isError = false;
|
|
67
|
-
this._pendingRecordArrayManagerFlush = false; // used by the recordArrayManager
|
|
68
|
-
|
|
69
|
-
// During dematerialization we don't want to rematerialize the record. The
|
|
70
|
-
// reason this might happen is that dematerialization removes records from
|
|
71
|
-
// record arrays, and Ember arrays will always `objectAt(0)` and
|
|
72
|
-
// `objectAt(len - 1)` to test whether or not `firstObject` or `lastObject`
|
|
73
|
-
// have changed.
|
|
74
|
-
this._isDematerializing = false;
|
|
75
|
-
this._scheduledDestroy = null;
|
|
76
|
-
|
|
77
|
-
this.error = null;
|
|
78
|
-
|
|
79
|
-
// caches for lazy getters
|
|
80
|
-
this._modelClass = null;
|
|
81
|
-
this.__recordArrays = null;
|
|
82
|
-
|
|
83
|
-
this.error = null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
get id(): string | null {
|
|
87
|
-
return this.identifier.id;
|
|
88
|
-
}
|
|
89
|
-
set id(value: string | null) {
|
|
90
|
-
if (value !== this._id) {
|
|
91
|
-
let newIdentifier = { type: this.identifier.type, lid: this.identifier.lid, id: value };
|
|
92
|
-
// TODO potentially this needs to handle merged result
|
|
93
|
-
this.store.identifierCache.updateRecordIdentifier(this.identifier, newIdentifier);
|
|
94
|
-
this.notifyPropertyChange('id');
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
get modelClass() {
|
|
99
|
-
if (this.store.modelFor) {
|
|
100
|
-
return this._modelClass || (this._modelClass = this.store.modelFor(this.modelName));
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
get _recordData(): RecordData {
|
|
105
|
-
return this.store._instanceCache.getRecordData(this.identifier);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
isHiddenFromRecordArrays() {
|
|
109
|
-
// During dematerialization we don't want to rematerialize the record.
|
|
110
|
-
// recordWasDeleted can cause other records to rematerialize because it
|
|
111
|
-
// removes the internal model from the array and Ember arrays will always
|
|
112
|
-
// `objectAt(0)` and `objectAt(len -1)` to check whether `firstObject` or
|
|
113
|
-
// `lastObject` have changed. When this happens we don't want those
|
|
114
|
-
// models to rematerialize their records.
|
|
115
|
-
|
|
116
|
-
// eager checks to avoid instantiating record data if we are empty or loading
|
|
117
|
-
if (this.isEmpty) {
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (this.isLoading) {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
let isRecordFullyDeleted = this._isRecordFullyDeleted();
|
|
126
|
-
return this._isDematerializing || this.hasScheduledDestroy() || this.isDestroyed || isRecordFullyDeleted;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
_isRecordFullyDeleted(): boolean {
|
|
130
|
-
if (this._recordData.isDeletionCommitted && this._recordData.isDeletionCommitted()) {
|
|
131
|
-
return true;
|
|
132
|
-
} else if (
|
|
133
|
-
this._recordData.isNew &&
|
|
134
|
-
this._recordData.isDeleted &&
|
|
135
|
-
this._recordData.isNew() &&
|
|
136
|
-
this._recordData.isDeleted()
|
|
137
|
-
) {
|
|
138
|
-
return true;
|
|
139
|
-
} else {
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
isDeleted(): boolean {
|
|
145
|
-
if (this._recordData.isDeleted) {
|
|
146
|
-
return this._recordData.isDeleted();
|
|
147
|
-
} else {
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
isNew(): boolean {
|
|
153
|
-
if (this.hasRecordData && this._recordData.isNew) {
|
|
154
|
-
return this._recordData.isNew();
|
|
155
|
-
} else {
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
get isEmpty(): boolean {
|
|
161
|
-
return !this.hasRecordData || ((!this.isNew() || this.isDeleted()) && this._recordData.isEmpty?.()) || false;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
get isLoading() {
|
|
165
|
-
const req = this.store.getRequestStateService();
|
|
166
|
-
const { identifier } = this;
|
|
167
|
-
// const fulfilled = req.getLastRequestForRecord(identifier);
|
|
168
|
-
|
|
169
|
-
return (
|
|
170
|
-
!this.isLoaded &&
|
|
171
|
-
// fulfilled === null &&
|
|
172
|
-
req.getPendingRequestsForRecord(identifier).some((req) => req.type === 'query')
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
get isLoaded() {
|
|
177
|
-
// if we are new we must consider ourselves loaded
|
|
178
|
-
if (this.isNew()) {
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
|
-
// even if we have a past request, if we are now empty we are not loaded
|
|
182
|
-
// typically this is true after an unloadRecord call
|
|
183
|
-
|
|
184
|
-
// if we are not empty, not new && we have a fulfilled request then we are loaded
|
|
185
|
-
// we should consider allowing for something to be loaded that is simply "not empty".
|
|
186
|
-
// which is how RecordState currently handles this case; however, RecordState is buggy
|
|
187
|
-
// in that it does not account for unloading.
|
|
188
|
-
return !this.isEmpty;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
dematerializeRecord() {
|
|
192
|
-
this._isDematerializing = true;
|
|
193
|
-
|
|
194
|
-
// TODO IGOR add a test that fails when this is missing, something that involves canceling a destroy
|
|
195
|
-
// and the destroy not happening, and then later on trying to destroy
|
|
196
|
-
this._doNotDestroy = false;
|
|
197
|
-
// this has to occur before the internal model is removed
|
|
198
|
-
// for legacy compat.
|
|
199
|
-
const { identifier } = this;
|
|
200
|
-
this.store._instanceCache.removeRecord(identifier);
|
|
201
|
-
|
|
202
|
-
// move to an empty never-loaded state
|
|
203
|
-
// ensure any record notifications happen prior to us
|
|
204
|
-
// unseting the record but after we've triggered
|
|
205
|
-
// destroy
|
|
206
|
-
this.store._backburner.join(() => {
|
|
207
|
-
this._recordData.unloadRecord();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
this.hasRecord = false; // this must occur after relationship removal
|
|
211
|
-
this.error = null;
|
|
212
|
-
this.store.recordArrayManager.recordDidChange(this.identifier);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
deleteRecord() {
|
|
216
|
-
run(() => {
|
|
217
|
-
const backburner = this.store._backburner;
|
|
218
|
-
backburner.run(() => {
|
|
219
|
-
if (this._recordData.setIsDeleted) {
|
|
220
|
-
this._recordData.setIsDeleted(true);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (this.isNew()) {
|
|
224
|
-
// destroyRecord follows up deleteRecord with save(). This prevents an unecessary save for a new record
|
|
225
|
-
this._deletedRecordWasNew = true;
|
|
226
|
-
this.unloadRecord();
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/*
|
|
233
|
-
Unload the record for this internal model. This will cause the record to be
|
|
234
|
-
destroyed and freed up for garbage collection. It will also do a check
|
|
235
|
-
for cleaning up internal models.
|
|
236
|
-
|
|
237
|
-
This check is performed by first computing the set of related internal
|
|
238
|
-
models. If all records in this set are unloaded, then the entire set is
|
|
239
|
-
destroyed. Otherwise, nothing in the set is destroyed.
|
|
240
|
-
|
|
241
|
-
This means that this internal model will be freed up for garbage collection
|
|
242
|
-
once all models that refer to it via some relationship are also unloaded.
|
|
243
|
-
*/
|
|
244
|
-
unloadRecord() {
|
|
245
|
-
if (this.isDestroyed) {
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
if (DEBUG) {
|
|
249
|
-
const requests = this.store.getRequestStateService().getPendingRequestsForRecord(this.identifier);
|
|
250
|
-
if (
|
|
251
|
-
requests.some((req) => {
|
|
252
|
-
return req.type === 'mutation';
|
|
253
|
-
})
|
|
254
|
-
) {
|
|
255
|
-
assert('You can only unload a record which is not inFlight. `' + this + '`');
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
this.dematerializeRecord();
|
|
259
|
-
if (this._scheduledDestroy === null) {
|
|
260
|
-
this._scheduledDestroy = emberBackburner.schedule('destroy', this, '_checkForOrphanedInternalModels');
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
hasScheduledDestroy() {
|
|
265
|
-
return !!this._scheduledDestroy;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
cancelDestroy() {
|
|
269
|
-
assert(
|
|
270
|
-
`You cannot cancel the destruction of an InternalModel once it has already been destroyed`,
|
|
271
|
-
!this.isDestroyed
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
this._doNotDestroy = true;
|
|
275
|
-
this._isDematerializing = false;
|
|
276
|
-
cancel(this._scheduledDestroy);
|
|
277
|
-
this._scheduledDestroy = null;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// typically, we prefer to async destroy this lets us batch cleanup work.
|
|
281
|
-
// Unfortunately, some scenarios where that is not possible. Such as:
|
|
282
|
-
//
|
|
283
|
-
// ```js
|
|
284
|
-
// const record = store.findRecord(‘record’, 1);
|
|
285
|
-
// record.unloadRecord();
|
|
286
|
-
// store.createRecord(‘record’, 1);
|
|
287
|
-
// ```
|
|
288
|
-
//
|
|
289
|
-
// In those scenarios, we make that model's cleanup work, sync.
|
|
290
|
-
//
|
|
291
|
-
destroySync() {
|
|
292
|
-
if (this._isDematerializing) {
|
|
293
|
-
this.cancelDestroy();
|
|
294
|
-
}
|
|
295
|
-
this._checkForOrphanedInternalModels();
|
|
296
|
-
if (this.isDestroyed || this.isDestroying) {
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// just in-case we are not one of the orphaned, we should still
|
|
301
|
-
// still destroy ourselves
|
|
302
|
-
this.destroy();
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
_checkForOrphanedInternalModels() {
|
|
306
|
-
this._isDematerializing = false;
|
|
307
|
-
this._scheduledDestroy = null;
|
|
308
|
-
if (this.isDestroyed) {
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
destroyFromRecordData() {
|
|
314
|
-
if (this._doNotDestroy) {
|
|
315
|
-
this._doNotDestroy = false;
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
this.destroy();
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
destroy() {
|
|
322
|
-
let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' });
|
|
323
|
-
assert(
|
|
324
|
-
'Cannot destroy an internalModel while its record is materialized',
|
|
325
|
-
!record || record.isDestroyed || record.isDestroying
|
|
326
|
-
);
|
|
327
|
-
this.isDestroying = true;
|
|
328
|
-
|
|
329
|
-
internalModelFactoryFor(this.store).remove(this);
|
|
330
|
-
this._isDestroyed = true;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
setupData(data) {
|
|
334
|
-
if (this.isNew()) {
|
|
335
|
-
this.store._notificationManager.notify(this.identifier, 'identity');
|
|
336
|
-
}
|
|
337
|
-
this._recordData.pushData(data, this.hasRecord);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
notifyAttributes(keys: string[]): void {
|
|
341
|
-
if (this.hasRecord) {
|
|
342
|
-
let manager = this.store._notificationManager;
|
|
343
|
-
let { identifier } = this;
|
|
344
|
-
|
|
345
|
-
if (!keys || !keys.length) {
|
|
346
|
-
manager.notify(identifier, 'attributes');
|
|
347
|
-
} else {
|
|
348
|
-
for (let i = 0; i < keys.length; i++) {
|
|
349
|
-
manager.notify(identifier, 'attributes', keys[i]);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
get isDestroyed(): boolean {
|
|
356
|
-
return this._isDestroyed;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
hasChangedAttributes(): boolean {
|
|
360
|
-
if (!this.hasRecordData) {
|
|
361
|
-
// no need to calculate changed attributes when calling `findRecord`
|
|
362
|
-
return false;
|
|
363
|
-
}
|
|
364
|
-
return this._recordData.hasChangedAttributes();
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
changedAttributes(): ChangedAttributesHash {
|
|
368
|
-
if (!this.hasRecordData) {
|
|
369
|
-
// no need to calculate changed attributes when calling `findRecord`
|
|
370
|
-
return {};
|
|
371
|
-
}
|
|
372
|
-
return this._recordData.changedAttributes();
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
adapterWillCommit(): void {
|
|
376
|
-
this._recordData.willCommit();
|
|
377
|
-
let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' });
|
|
378
|
-
if (record && isDSModel(record)) {
|
|
379
|
-
record.errors.clear();
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
notifyHasManyChange(key: string) {
|
|
384
|
-
if (this.hasRecord) {
|
|
385
|
-
this.store._notificationManager.notify(this.identifier, 'relationships', key);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
notifyBelongsToChange(key: string) {
|
|
390
|
-
if (this.hasRecord) {
|
|
391
|
-
this.store._notificationManager.notify(this.identifier, 'relationships', key);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
notifyPropertyChange(key: string) {
|
|
396
|
-
if (this.hasRecord) {
|
|
397
|
-
// TODO this should likely *mostly* be the `attributes` bucket
|
|
398
|
-
// but it seems for local mutations we rely on computed updating
|
|
399
|
-
// iteself when set. As we design our own thing we may need to change
|
|
400
|
-
// that.
|
|
401
|
-
this.store._notificationManager.notify(this.identifier, 'property', key);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
notifyStateChange(key?: string) {
|
|
406
|
-
if (this.hasRecord) {
|
|
407
|
-
this.store._notificationManager.notify(this.identifier, 'state');
|
|
408
|
-
}
|
|
409
|
-
if (!key || key === 'isDeletionCommitted') {
|
|
410
|
-
this.store.recordArrayManager.recordDidChange(this.identifier);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
rollbackAttributes() {
|
|
415
|
-
this.store._backburner.join(() => {
|
|
416
|
-
let dirtyKeys = this._recordData.rollbackAttributes();
|
|
417
|
-
|
|
418
|
-
let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' });
|
|
419
|
-
if (record && isDSModel(record)) {
|
|
420
|
-
record.errors.clear();
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (this.hasRecord && dirtyKeys && dirtyKeys.length > 0) {
|
|
424
|
-
this.notifyAttributes(dirtyKeys);
|
|
425
|
-
}
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
removeFromInverseRelationships() {
|
|
430
|
-
if (this.hasRecordData) {
|
|
431
|
-
this.store._backburner.join(() => {
|
|
432
|
-
this._recordData.removeFromInverseRelationships();
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/*
|
|
438
|
-
When a find request is triggered on the store, the user can optionally pass in
|
|
439
|
-
attributes and relationships to be preloaded. These are meant to behave as if they
|
|
440
|
-
came back from the server, except the user obtained them out of band and is informing
|
|
441
|
-
the store of their existence. The most common use case is for supporting client side
|
|
442
|
-
nested URLs, such as `/posts/1/comments/2` so the user can do
|
|
443
|
-
`store.findRecord('comment', 2, { preload: { post: 1 } })` without having to fetch the post.
|
|
444
|
-
|
|
445
|
-
Preloaded data can be attributes and relationships passed in either as IDs or as actual
|
|
446
|
-
models.
|
|
447
|
-
*/
|
|
448
|
-
preloadData(preload) {
|
|
449
|
-
let jsonPayload: JsonApiResource = {};
|
|
450
|
-
//TODO(Igor) consider the polymorphic case
|
|
451
|
-
Object.keys(preload).forEach((key) => {
|
|
452
|
-
let preloadValue = preload[key];
|
|
453
|
-
let relationshipMeta = this.modelClass.metaForProperty(key);
|
|
454
|
-
if (relationshipMeta.isRelationship) {
|
|
455
|
-
if (!jsonPayload.relationships) {
|
|
456
|
-
jsonPayload.relationships = {};
|
|
457
|
-
}
|
|
458
|
-
jsonPayload.relationships[key] = this._preloadRelationship(key, preloadValue);
|
|
459
|
-
} else {
|
|
460
|
-
if (!jsonPayload.attributes) {
|
|
461
|
-
jsonPayload.attributes = {};
|
|
462
|
-
}
|
|
463
|
-
jsonPayload.attributes[key] = preloadValue;
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
|
-
this._recordData.pushData(jsonPayload);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
_preloadRelationship(key, preloadValue) {
|
|
470
|
-
let relationshipMeta = this.modelClass.metaForProperty(key);
|
|
471
|
-
let modelClass = relationshipMeta.type;
|
|
472
|
-
let data;
|
|
473
|
-
if (relationshipMeta.kind === 'hasMany') {
|
|
474
|
-
assert('You need to pass in an array to set a hasMany property on a record', Array.isArray(preloadValue));
|
|
475
|
-
data = preloadValue.map((value) => this._convertPreloadRelationshipToJSON(value, modelClass));
|
|
476
|
-
} else {
|
|
477
|
-
data = this._convertPreloadRelationshipToJSON(preloadValue, modelClass);
|
|
478
|
-
}
|
|
479
|
-
return { data };
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
_convertPreloadRelationshipToJSON(value, modelClass) {
|
|
483
|
-
if (typeof value === 'string' || typeof value === 'number') {
|
|
484
|
-
return { type: modelClass, id: value };
|
|
485
|
-
}
|
|
486
|
-
let internalModel;
|
|
487
|
-
if (value._internalModel) {
|
|
488
|
-
internalModel = value._internalModel;
|
|
489
|
-
} else {
|
|
490
|
-
internalModel = value;
|
|
491
|
-
}
|
|
492
|
-
// TODO IGOR DAVID assert if no id is present
|
|
493
|
-
return { type: internalModel.modelName, id: internalModel.id };
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/*
|
|
497
|
-
* calling `InstanceCache.setRecordId` is necessary to update
|
|
498
|
-
* the cache index for this record if we have changed.
|
|
499
|
-
*
|
|
500
|
-
* However, since the store is not aware of whether the update
|
|
501
|
-
* is from us (via user set) or from a push of new data
|
|
502
|
-
* it will also call us so that we can notify and update state.
|
|
503
|
-
*
|
|
504
|
-
* When it does so it calls with `fromCache` so that we can
|
|
505
|
-
* short-circuit instead of cycling back.
|
|
506
|
-
*
|
|
507
|
-
* This differs from the short-circuit in the `_isUpdatingId`
|
|
508
|
-
* case in that the the cache can originate the call to setId,
|
|
509
|
-
* so on first entry we will still need to do our own update.
|
|
510
|
-
*/
|
|
511
|
-
setId(id: string | null, fromCache: boolean = false) {
|
|
512
|
-
if (this._isUpdatingId === true) {
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
this._isUpdatingId = true;
|
|
516
|
-
let didChange = id !== this._id;
|
|
517
|
-
this._id = id;
|
|
518
|
-
|
|
519
|
-
if (didChange && id !== null) {
|
|
520
|
-
if (!fromCache) {
|
|
521
|
-
this.store._instanceCache.setRecordId(this.modelName, id, this.clientId);
|
|
522
|
-
}
|
|
523
|
-
// internal set of ID to get it to RecordData from DS.Model
|
|
524
|
-
// if we are within create we may not have a recordData yet.
|
|
525
|
-
if (this.hasRecordData && this._recordData.__setId) {
|
|
526
|
-
this._recordData.__setId(id);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (didChange && this.hasRecord) {
|
|
531
|
-
this.store._notificationManager.notify(this.identifier, 'identity');
|
|
532
|
-
}
|
|
533
|
-
this._isUpdatingId = false;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
didError() {}
|
|
537
|
-
|
|
538
|
-
/*
|
|
539
|
-
If the adapter did not return a hash in response to a commit,
|
|
540
|
-
merge the changed attributes and relationships into the existing
|
|
541
|
-
saved data.
|
|
542
|
-
*/
|
|
543
|
-
adapterDidCommit(data) {
|
|
544
|
-
this._recordData.didCommit(data);
|
|
545
|
-
this.store.recordArrayManager.recordDidChange(this.identifier);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
hasErrors(): boolean {
|
|
549
|
-
// TODO add assertion forcing consuming RecordData's to implement getErrors
|
|
550
|
-
if (this._recordData.getErrors) {
|
|
551
|
-
return this._recordData.getErrors(this.identifier).length > 0;
|
|
552
|
-
} else {
|
|
553
|
-
let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' });
|
|
554
|
-
// we can't have errors if we never tried loading
|
|
555
|
-
if (!record) {
|
|
556
|
-
return false;
|
|
557
|
-
}
|
|
558
|
-
let errors = (record as DSModel).errors;
|
|
559
|
-
return errors.length > 0;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// FOR USE DURING COMMIT PROCESS
|
|
564
|
-
adapterDidInvalidate(parsedErrors, error?) {
|
|
565
|
-
// TODO @runspired this should be handled by RecordState
|
|
566
|
-
// and errors should be dirtied but lazily fetch if at
|
|
567
|
-
// all possible. We should only notify errors here.
|
|
568
|
-
let attribute;
|
|
569
|
-
if (error && parsedErrors) {
|
|
570
|
-
// TODO add assertion forcing consuming RecordData's to implement getErrors
|
|
571
|
-
if (!this._recordData.getErrors) {
|
|
572
|
-
let record = this.store._instanceCache.getRecord(this.identifier) as DSModel;
|
|
573
|
-
let errors = record.errors;
|
|
574
|
-
for (attribute in parsedErrors) {
|
|
575
|
-
if (Object.prototype.hasOwnProperty.call(parsedErrors, attribute)) {
|
|
576
|
-
errors.add(attribute, parsedErrors[attribute]);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
let jsonApiErrors: JsonApiValidationError[] = errorsHashToArray(parsedErrors);
|
|
582
|
-
if (jsonApiErrors.length === 0) {
|
|
583
|
-
jsonApiErrors = [{ title: 'Invalid Error', detail: '', source: { pointer: '/data' } }];
|
|
584
|
-
}
|
|
585
|
-
this._recordData.commitWasRejected(this.identifier, jsonApiErrors);
|
|
586
|
-
} else {
|
|
587
|
-
this._recordData.commitWasRejected(this.identifier);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
notifyErrorsChange() {
|
|
592
|
-
this.store._notificationManager.notify(this.identifier, 'errors');
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
adapterDidError() {
|
|
596
|
-
this._recordData.commitWasRejected();
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
toString() {
|
|
600
|
-
return `<${this.modelName}:${this.id}>`;
|
|
601
|
-
}
|
|
602
|
-
}
|