@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.
Files changed (45) hide show
  1. package/addon/-debug/index.js +35 -13
  2. package/addon/-private/{identifier-cache.ts → caches/identifier-cache.ts} +148 -73
  3. package/addon/-private/caches/instance-cache.ts +690 -0
  4. package/addon/-private/{record-data-for.ts → caches/record-data-for.ts} +2 -7
  5. package/addon/-private/index.ts +44 -24
  6. package/addon/-private/{model → legacy-model-support}/record-reference.ts +15 -13
  7. package/addon/-private/{schema-definition-service.ts → legacy-model-support/schema-definition-service.ts} +13 -9
  8. package/addon/-private/{model → legacy-model-support}/shim-model-class.ts +18 -11
  9. package/addon/-private/managers/record-array-manager.ts +377 -0
  10. package/addon/-private/managers/record-data-manager.ts +845 -0
  11. package/addon/-private/managers/record-data-store-wrapper.ts +421 -0
  12. package/addon/-private/managers/record-notification-manager.ts +109 -0
  13. package/addon/-private/network/fetch-manager.ts +567 -0
  14. package/addon/-private/{finders.js → network/finders.js} +14 -17
  15. package/addon/-private/{request-cache.ts → network/request-cache.ts} +21 -18
  16. package/addon/-private/{snapshot-record-array.ts → network/snapshot-record-array.ts} +14 -31
  17. package/addon/-private/{snapshot.ts → network/snapshot.ts} +40 -49
  18. package/addon/-private/{promise-proxies.ts → proxies/promise-proxies.ts} +76 -15
  19. package/addon/-private/{promise-proxy-base.js → proxies/promise-proxy-base.js} +0 -0
  20. package/addon/-private/record-arrays/identifier-array.ts +924 -0
  21. package/addon/-private/{core-store.ts → store-service.ts} +574 -215
  22. package/addon/-private/{coerce-id.ts → utils/coerce-id.ts} +1 -1
  23. package/addon/-private/{common.js → utils/common.js} +1 -2
  24. package/addon/-private/utils/construct-resource.ts +2 -2
  25. package/addon/-private/{identifer-debug-consts.ts → utils/identifer-debug-consts.ts} +0 -0
  26. package/addon/-private/utils/is-non-empty-string.ts +1 -1
  27. package/addon/-private/{normalize-model-name.ts → utils/normalize-model-name.ts} +1 -3
  28. package/addon/-private/utils/promise-record.ts +5 -6
  29. package/addon/-private/{serializer-response.ts → utils/serializer-response.ts} +2 -2
  30. package/addon/-private/utils/uuid-polyfill.ts +73 -0
  31. package/package.json +12 -8
  32. package/addon/-private/backburner.js +0 -25
  33. package/addon/-private/errors-utils.js +0 -146
  34. package/addon/-private/fetch-manager.ts +0 -597
  35. package/addon/-private/identity-map.ts +0 -54
  36. package/addon/-private/instance-cache.ts +0 -387
  37. package/addon/-private/internal-model-factory.ts +0 -359
  38. package/addon/-private/internal-model-map.ts +0 -121
  39. package/addon/-private/model/internal-model.ts +0 -602
  40. package/addon/-private/record-array-manager.ts +0 -444
  41. package/addon/-private/record-arrays/adapter-populated-record-array.ts +0 -130
  42. package/addon/-private/record-arrays/record-array.ts +0 -318
  43. package/addon/-private/record-data-store-wrapper.ts +0 -243
  44. package/addon/-private/record-notification-manager.ts +0 -73
  45. package/addon/-private/weak-cache.ts +0 -125
@@ -0,0 +1,690 @@
1
+ import { assert, deprecate, warn } from '@ember/debug';
2
+ import { DEBUG } from '@glimmer/env';
3
+
4
+ import { importSync } from '@embroider/macros';
5
+ import { resolve } from 'rsvp';
6
+
7
+ import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra';
8
+ import { LOG_INSTANCE_CACHE } from '@ember-data/private-build-infra/debugging';
9
+ import { DEPRECATE_V1_RECORD_DATA, DEPRECATE_V1CACHE_STORE_APIS } from '@ember-data/private-build-infra/deprecations';
10
+ import type { Graph } from '@ember-data/record-data/-private/graph/graph';
11
+ import type { peekGraph } from '@ember-data/record-data/-private/graph/index';
12
+ import type {
13
+ ExistingResourceIdentifierObject,
14
+ ExistingResourceObject,
15
+ NewResourceIdentifierObject,
16
+ } from '@ember-data/types/q/ember-data-json-api';
17
+ import type {
18
+ RecordIdentifier,
19
+ StableExistingRecordIdentifier,
20
+ StableRecordIdentifier,
21
+ } from '@ember-data/types/q/identifier';
22
+ import type { RecordData } from '@ember-data/types/q/record-data';
23
+ import { JsonApiRelationship, JsonApiResource } from '@ember-data/types/q/record-data-json-api';
24
+ import { RelationshipSchema } from '@ember-data/types/q/record-data-schemas';
25
+ import type { RecordDataStoreWrapper as StoreWrapper } from '@ember-data/types/q/record-data-store-wrapper';
26
+ import type { RecordInstance } from '@ember-data/types/q/record-instance';
27
+ import type { FindOptions } from '@ember-data/types/q/store';
28
+ import { Dict } from '@ember-data/types/q/utils';
29
+
30
+ import RecordReference from '../legacy-model-support/record-reference';
31
+ import { NonSingletonRecordDataManager, SingletonRecordDataManager } from '../managers/record-data-manager';
32
+ import { RecordDataStoreWrapper } from '../managers/record-data-store-wrapper';
33
+ import Snapshot from '../network/snapshot';
34
+ import type { CreateRecordProperties } from '../store-service';
35
+ import type Store from '../store-service';
36
+ import { assertIdentifierHasId } from '../store-service';
37
+ import coerceId, { ensureStringId } from '../utils/coerce-id';
38
+ import constructResource from '../utils/construct-resource';
39
+ import normalizeModelName from '../utils/normalize-model-name';
40
+ import { removeRecordDataFor, setRecordDataFor } from './record-data-for';
41
+
42
+ let _peekGraph: peekGraph;
43
+ if (HAS_RECORD_DATA_PACKAGE) {
44
+ let __peekGraph: peekGraph;
45
+ _peekGraph = (wrapper: Store | StoreWrapper): Graph | undefined => {
46
+ let a = (importSync('@ember-data/record-data/-private') as { peekGraph: peekGraph }).peekGraph;
47
+ __peekGraph = __peekGraph || a;
48
+ return __peekGraph(wrapper);
49
+ };
50
+ }
51
+
52
+ /**
53
+ @module @ember-data/store
54
+ */
55
+
56
+ const RecordCache = new Map<RecordInstance, StableRecordIdentifier>();
57
+
58
+ export function peekRecordIdentifier(record: RecordInstance): StableRecordIdentifier | undefined {
59
+ return RecordCache.get(record);
60
+ }
61
+
62
+ /**
63
+ Retrieves the unique referentially-stable [RecordIdentifier](/ember-data/release/classes/StableRecordIdentifier)
64
+ assigned to the given record instance.
65
+ ```js
66
+ import { recordIdentifierFor } from "@ember-data/store";
67
+ // ... gain access to a record, for instance with peekRecord or findRecord
68
+ const record = store.peekRecord("user", "1");
69
+ // get the identifier for the record (see docs for StableRecordIdentifier)
70
+ const identifier = recordIdentifierFor(record);
71
+ // access the identifier's properties.
72
+ const { id, type, lid } = identifier;
73
+ ```
74
+ @method recordIdentifierFor
75
+ @public
76
+ @static
77
+ @for @ember-data/store
78
+ @param {Object} record a record instance previously obstained from the store.
79
+ @returns {StableRecordIdentifier}
80
+ */
81
+ export function recordIdentifierFor(record: RecordInstance): StableRecordIdentifier {
82
+ assert(`${String(record)} is not a record instantiated by @ember-data/store`, RecordCache.has(record));
83
+ return RecordCache.get(record)!;
84
+ }
85
+
86
+ export function setRecordIdentifier(record: RecordInstance, identifier: StableRecordIdentifier): void {
87
+ if (DEBUG && RecordCache.has(record) && RecordCache.get(record) !== identifier) {
88
+ throw new Error(`${String(record)} was already assigned an identifier`);
89
+ }
90
+
91
+ /*
92
+ It would be nice to do a reverse check here that an identifier has not
93
+ previously been assigned a record; however, unload + rematerialization
94
+ prevents us from having a great way of doing so when CustomRecordClasses
95
+ don't necessarily give us access to a `isDestroyed` for dematerialized
96
+ instance.
97
+ */
98
+
99
+ RecordCache.set(record, identifier);
100
+ }
101
+
102
+ export const StoreMap = new Map<RecordInstance, Store>();
103
+
104
+ export function storeFor(record: RecordInstance): Store | undefined {
105
+ const store = StoreMap.get(record);
106
+
107
+ assert(
108
+ `A record in a disconnected state cannot utilize the store. This typically means the record has been destroyed, most commonly by unloading it.`,
109
+ store
110
+ );
111
+ return store;
112
+ }
113
+
114
+ type Caches = {
115
+ record: Map<StableRecordIdentifier, RecordInstance>;
116
+ recordData: Map<StableRecordIdentifier, RecordData>;
117
+ reference: WeakMap<StableRecordIdentifier, RecordReference>;
118
+ };
119
+
120
+ export class InstanceCache {
121
+ declare store: Store;
122
+ declare _storeWrapper: RecordDataStoreWrapper;
123
+ declare __recordDataFor: (resource: RecordIdentifier) => RecordData;
124
+
125
+ declare __cacheManager: NonSingletonRecordDataManager;
126
+ __instances: Caches = {
127
+ record: new Map<StableRecordIdentifier, RecordInstance>(),
128
+ recordData: new Map<StableRecordIdentifier, RecordData>(),
129
+ reference: new WeakMap<StableRecordIdentifier, RecordReference>(),
130
+ };
131
+
132
+ constructor(store: Store) {
133
+ this.store = store;
134
+
135
+ this._storeWrapper = new RecordDataStoreWrapper(this.store);
136
+ this.__recordDataFor = (resource: RecordIdentifier) => {
137
+ // TODO enforce strict
138
+ const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resource);
139
+ return this.getRecordData(identifier);
140
+ };
141
+
142
+ store.identifierCache.__configureMerge(
143
+ (identifier: StableRecordIdentifier, matchedIdentifier: StableRecordIdentifier, resourceData) => {
144
+ let keptIdentifier = identifier;
145
+ if (identifier.id !== matchedIdentifier.id) {
146
+ keptIdentifier = 'id' in resourceData && identifier.id === resourceData.id ? identifier : matchedIdentifier;
147
+ } else if (identifier.type !== matchedIdentifier.type) {
148
+ keptIdentifier =
149
+ 'type' in resourceData && identifier.type === resourceData.type ? identifier : matchedIdentifier;
150
+ }
151
+ let staleIdentifier = identifier === keptIdentifier ? matchedIdentifier : identifier;
152
+
153
+ // check for duplicate entities
154
+ let keptHasRecord = this.__instances.record.has(keptIdentifier);
155
+ let staleHasRecord = this.__instances.record.has(staleIdentifier);
156
+ let keptRecordData = this.__instances.recordData.get(keptIdentifier) || null;
157
+ let staleRecordData = this.__instances.recordData.get(staleIdentifier) || null;
158
+
159
+ // we cannot merge entities when both have records
160
+ // (this may not be strictly true, we could probably swap the recordData the record points at)
161
+ if (keptHasRecord && staleHasRecord) {
162
+ // TODO we probably don't need to throw these errors anymore
163
+ // we can probably just "swap" what data source the abandoned
164
+ // record points at so long as
165
+ // it itself is not retained by the store in any way.
166
+ if ('id' in resourceData) {
167
+ throw new Error(
168
+ `Failed to update the 'id' for the RecordIdentifier '${identifier.type}:${String(identifier.id)} (${
169
+ identifier.lid
170
+ })' to '${String(resourceData.id)}', because that id is already in use by '${
171
+ matchedIdentifier.type
172
+ }:${String(matchedIdentifier.id)} (${matchedIdentifier.lid})'`
173
+ );
174
+ }
175
+
176
+ assert(
177
+ `Failed to update the RecordIdentifier '${identifier.type}:${String(identifier.id)} (${
178
+ identifier.lid
179
+ })' to merge with the detected duplicate identifier '${matchedIdentifier.type}:${String(
180
+ matchedIdentifier.id
181
+ )} (${String(matchedIdentifier.lid)})'`
182
+ );
183
+ }
184
+
185
+ let recordData = keptRecordData || staleRecordData;
186
+
187
+ if (recordData) {
188
+ recordData.sync({
189
+ op: 'mergeIdentifiers',
190
+ record: staleIdentifier,
191
+ value: keptIdentifier,
192
+ });
193
+ } else if (HAS_RECORD_DATA_PACKAGE) {
194
+ // TODO notify cache always, this requires it always being a singleton
195
+ // and not ever specific to one record-data
196
+ this.store.__private_singleton_recordData?.sync({
197
+ op: 'mergeIdentifiers',
198
+ record: staleIdentifier,
199
+ value: keptIdentifier,
200
+ });
201
+ }
202
+
203
+ if (staleRecordData === null) {
204
+ return keptIdentifier;
205
+ }
206
+
207
+ /*
208
+ TODO @runspired consider adding this to make polymorphism even nicer
209
+ if (HAS_RECORD_DATA_PACKAGE) {
210
+ if (identifier.type !== matchedIdentifier.type) {
211
+ const graphFor = importSync('@ember-data/record-data/-private').graphFor;
212
+ graphFor(this).registerPolymorphicType(identifier.type, matchedIdentifier.type);
213
+ }
214
+ }
215
+ */
216
+
217
+ this.unloadRecord(staleIdentifier);
218
+ return keptIdentifier;
219
+ }
220
+ );
221
+ }
222
+ peek({ identifier, bucket }: { identifier: StableRecordIdentifier; bucket: 'record' }): RecordInstance | undefined;
223
+ peek({ identifier, bucket }: { identifier: StableRecordIdentifier; bucket: 'recordData' }): RecordData | undefined;
224
+ peek({
225
+ identifier,
226
+ bucket,
227
+ }: {
228
+ identifier: StableRecordIdentifier;
229
+ bucket: 'record' | 'recordData';
230
+ }): RecordData | RecordInstance | undefined {
231
+ return this.__instances[bucket]?.get(identifier);
232
+ }
233
+
234
+ getRecord(identifier: StableRecordIdentifier, properties?: CreateRecordProperties): RecordInstance {
235
+ let record = this.__instances.record.get(identifier);
236
+
237
+ if (!record) {
238
+ const recordData = this.getRecordData(identifier);
239
+
240
+ record = this.store.instantiateRecord(
241
+ identifier,
242
+ properties || {},
243
+ this.__recordDataFor,
244
+ this.store._notificationManager
245
+ );
246
+ setRecordIdentifier(record, identifier);
247
+ setRecordDataFor(record, recordData);
248
+ StoreMap.set(record, this.store);
249
+ this.__instances.record.set(identifier, record);
250
+
251
+ if (LOG_INSTANCE_CACHE) {
252
+ // eslint-disable-next-line no-console
253
+ console.log(`InstanceCache: created Record for ${String(identifier)}`, properties);
254
+ }
255
+ }
256
+
257
+ return record;
258
+ }
259
+
260
+ getRecordData(identifier: StableRecordIdentifier): RecordData {
261
+ let recordData = this.__instances.recordData.get(identifier);
262
+
263
+ if (!recordData) {
264
+ if (DEPRECATE_V1CACHE_STORE_APIS && this.store.createRecordDataFor.length > 2) {
265
+ deprecate(
266
+ `Store.createRecordDataFor(<type>, <id>, <lid>, <storeWrapper>) has been deprecated in favor of Store.createRecordDataFor(<identifier>, <storeWrapper>)`,
267
+ false,
268
+ {
269
+ id: 'ember-data:deprecate-v1cache-store-apis',
270
+ for: 'ember-data',
271
+ until: '5.0',
272
+ since: { enabled: '4.8', available: '4.8' },
273
+ }
274
+ );
275
+ let recordDataInstance = this.store.createRecordDataFor(
276
+ identifier.type,
277
+ identifier.id,
278
+ // @ts-expect-error
279
+ identifier.lid,
280
+ this._storeWrapper
281
+ );
282
+ if (DEPRECATE_V1_RECORD_DATA) {
283
+ recordData = new NonSingletonRecordDataManager(this.store, recordDataInstance, identifier);
284
+ } else {
285
+ recordData = this.__cacheManager =
286
+ this.__cacheManager || new NonSingletonRecordDataManager(this.store, recordDataInstance, identifier);
287
+ }
288
+ } else {
289
+ let recordDataInstance = this.store.createRecordDataFor(identifier, this._storeWrapper);
290
+ if (DEPRECATE_V1_RECORD_DATA) {
291
+ recordData = new NonSingletonRecordDataManager(this.store, recordDataInstance, identifier);
292
+ } else {
293
+ if (DEBUG) {
294
+ recordData = this.__cacheManager = this.__cacheManager || new SingletonRecordDataManager();
295
+ (recordData as SingletonRecordDataManager)._addRecordData(identifier, recordDataInstance as RecordData);
296
+ } else {
297
+ recordData = recordDataInstance as RecordData;
298
+ }
299
+ }
300
+ }
301
+ setRecordDataFor(identifier, recordData);
302
+
303
+ this.__instances.recordData.set(identifier, recordData);
304
+ if (LOG_INSTANCE_CACHE) {
305
+ // eslint-disable-next-line no-console
306
+ console.log(`InstanceCache: created RecordData for ${String(identifier)}`);
307
+ }
308
+ }
309
+
310
+ return recordData;
311
+ }
312
+
313
+ getReference(identifier: StableRecordIdentifier) {
314
+ let cache = this.__instances.reference;
315
+ let reference = cache.get(identifier);
316
+
317
+ if (!reference) {
318
+ reference = new RecordReference(this.store, identifier);
319
+ cache.set(identifier, reference);
320
+ }
321
+ return reference;
322
+ }
323
+
324
+ recordIsLoaded(identifier: StableRecordIdentifier, filterDeleted: boolean = false) {
325
+ const recordData = this.__instances.recordData.get(identifier);
326
+ if (!recordData) {
327
+ return false;
328
+ }
329
+ const isNew = recordData.isNew(identifier);
330
+ const isEmpty = recordData.isEmpty(identifier);
331
+
332
+ // if we are new we must consider ourselves loaded
333
+ if (isNew) {
334
+ return !recordData.isDeleted(identifier);
335
+ }
336
+ // even if we have a past request, if we are now empty we are not loaded
337
+ // typically this is true after an unloadRecord call
338
+
339
+ // if we are not empty, not new && we have a fulfilled request then we are loaded
340
+ // we should consider allowing for something to be loaded that is simply "not empty".
341
+ // which is how RecordState currently handles this case; however, RecordState is buggy
342
+ // in that it does not account for unloading.
343
+ return filterDeleted && recordData.isDeletionCommitted(identifier) ? false : !isEmpty;
344
+
345
+ /*
346
+ const req = this.store.getRequestStateService();
347
+ const fulfilled = req.getLastRequestForRecord(identifier);
348
+ const isLocallyLoaded = !isEmpty;
349
+ const isLoading =
350
+ !isLocallyLoaded &&
351
+ fulfilled === null &&
352
+ req.getPendingRequestsForRecord(identifier).some((req) => req.type === 'query');
353
+
354
+ if (isEmpty || (filterDeleted && recordData.isDeletionCommitted(identifier)) || isLoading) {
355
+ return false;
356
+ }
357
+
358
+ return true;
359
+ */
360
+ }
361
+
362
+ createSnapshot(identifier: StableRecordIdentifier, options: FindOptions = {}): Snapshot {
363
+ return new Snapshot(options, identifier, this.store);
364
+ }
365
+
366
+ disconnect(identifier: StableRecordIdentifier) {
367
+ const record = this.__instances.record.get(identifier);
368
+ assert(
369
+ 'Cannot destroy record while it is still materialized',
370
+ !record || record.isDestroyed || record.isDestroying
371
+ );
372
+
373
+ if (HAS_RECORD_DATA_PACKAGE) {
374
+ let graph = _peekGraph(this.store);
375
+ if (graph) {
376
+ graph.remove(identifier);
377
+ }
378
+ }
379
+
380
+ this.store.identifierCache.forgetRecordIdentifier(identifier);
381
+ if (LOG_INSTANCE_CACHE) {
382
+ // eslint-disable-next-line no-console
383
+ console.log(`InstanceCache: disconnected ${String(identifier)}`);
384
+ }
385
+ }
386
+
387
+ unloadRecord(identifier: StableRecordIdentifier) {
388
+ if (DEBUG) {
389
+ const requests = this.store.getRequestStateService().getPendingRequestsForRecord(identifier);
390
+ if (
391
+ requests.some((req) => {
392
+ return req.type === 'mutation';
393
+ })
394
+ ) {
395
+ assert(`You can only unload a record which is not inFlight. '${String(identifier)}'`);
396
+ }
397
+ }
398
+ if (LOG_INSTANCE_CACHE) {
399
+ // eslint-disable-next-line no-console
400
+ console.groupCollapsed(`InstanceCache: unloading record for ${String(identifier)}`);
401
+ }
402
+
403
+ // TODO is this join still necessary?
404
+ this.store._join(() => {
405
+ const record = this.__instances.record.get(identifier);
406
+ const recordData = this.__instances.recordData.get(identifier);
407
+
408
+ if (record) {
409
+ this.store.teardownRecord(record);
410
+ this.__instances.record.delete(identifier);
411
+ StoreMap.delete(record);
412
+ RecordCache.delete(record);
413
+ removeRecordDataFor(record);
414
+
415
+ if (LOG_INSTANCE_CACHE) {
416
+ // eslint-disable-next-line no-console
417
+ console.log(`InstanceCache: destroyed record for ${String(identifier)}`);
418
+ }
419
+ }
420
+
421
+ if (recordData) {
422
+ recordData.unloadRecord(identifier);
423
+ this.__instances.recordData.delete(identifier);
424
+ removeRecordDataFor(identifier);
425
+ } else {
426
+ this.disconnect(identifier);
427
+ }
428
+
429
+ this.store._fetchManager.clearEntries(identifier);
430
+ this.store.recordArrayManager.identifierRemoved(identifier);
431
+ if (LOG_INSTANCE_CACHE) {
432
+ // eslint-disable-next-line no-console
433
+ console.log(`InstanceCache: unloaded RecordData for ${String(identifier)}`);
434
+ // eslint-disable-next-line no-console
435
+ console.groupEnd();
436
+ }
437
+ });
438
+ }
439
+
440
+ clear(type?: string) {
441
+ const typeCache = this.store.identifierCache._cache.types;
442
+ if (type === undefined) {
443
+ this.__instances.recordData.forEach((value, identifier) => {
444
+ this.unloadRecord(identifier);
445
+ });
446
+ } else {
447
+ let identifiers = typeCache[type]?.lid;
448
+ const rds = this.__instances.recordData;
449
+ if (identifiers) {
450
+ identifiers.forEach((identifier) => {
451
+ if (rds.has(identifier)) {
452
+ this.unloadRecord(identifier);
453
+ }
454
+ // TODO we don't remove the identifier, should we?
455
+ });
456
+ }
457
+ }
458
+ }
459
+
460
+ // TODO this should move into the network layer
461
+ _fetchDataIfNeededForIdentifier(
462
+ identifier: StableRecordIdentifier,
463
+ options: FindOptions = {}
464
+ ): Promise<StableRecordIdentifier> {
465
+ // pre-loading will change the isEmpty value
466
+ const isEmpty = _isEmpty(this, identifier);
467
+ const isLoading = _isLoading(this, identifier);
468
+
469
+ if (options.preload) {
470
+ this.store._join(() => {
471
+ preloadData(this.store, identifier, options.preload!);
472
+ });
473
+ }
474
+
475
+ let promise: Promise<StableRecordIdentifier>;
476
+ if (isEmpty) {
477
+ assertIdentifierHasId(identifier);
478
+
479
+ promise = this.store._fetchManager.scheduleFetch(identifier, options);
480
+ } else if (isLoading) {
481
+ promise = this.store._fetchManager.getPendingFetch(identifier, options)!;
482
+ assert(`Expected to find a pending request for a record in the loading state, but found none`, promise);
483
+ } else {
484
+ promise = resolve(identifier);
485
+ }
486
+
487
+ return promise;
488
+ }
489
+
490
+ // TODO this should move into something coordinating operations
491
+ setRecordId(identifier: StableRecordIdentifier, id: string) {
492
+ const { type, lid } = identifier;
493
+ let oldId = identifier.id;
494
+
495
+ // ID absolutely can't be missing if the oldID is empty (missing Id in response for a new record)
496
+ assert(
497
+ `'${type}' was saved to the server, but the response does not have an id and your record does not either.`,
498
+ !(id === null && oldId === null)
499
+ );
500
+
501
+ // ID absolutely can't be different than oldID if oldID is not null
502
+ // TODO this assertion and restriction may not strictly be needed in the identifiers world
503
+ assert(
504
+ `Cannot update the id for '${type}:${lid}' from '${String(oldId)}' to '${id}'.`,
505
+ !(oldId !== null && id !== oldId)
506
+ );
507
+
508
+ // ID can be null if oldID is not null (altered ID in response for a record)
509
+ // however, this is more than likely a developer error.
510
+ if (oldId !== null && id === null) {
511
+ warn(
512
+ `Your ${type} record was saved to the server, but the response does not have an id.`,
513
+ !(oldId !== null && id === null)
514
+ );
515
+ return;
516
+ }
517
+
518
+ if (LOG_INSTANCE_CACHE) {
519
+ // eslint-disable-next-line no-console
520
+ console.log(`InstanceCache: updating id to '${id}' for record ${String(identifier)}`);
521
+ }
522
+
523
+ let existingIdentifier = this.store.identifierCache.peekRecordIdentifier({ type, id });
524
+ assert(
525
+ `'${type}' was saved to the server, but the response returned the new id '${id}', which has already been used with another record.'`,
526
+ !existingIdentifier || existingIdentifier === identifier
527
+ );
528
+
529
+ if (identifier.id === null) {
530
+ // TODO potentially this needs to handle merged result
531
+ this.store.identifierCache.updateRecordIdentifier(identifier, { type, id });
532
+ }
533
+
534
+ // TODO update recordData if needed ?
535
+ // TODO handle consequences of identifier merge for notifications
536
+ this.store._notificationManager.notify(identifier, 'identity');
537
+ }
538
+
539
+ // TODO this should move into something coordinating operations
540
+ loadData(data: ExistingResourceObject): StableExistingRecordIdentifier {
541
+ let modelName = data.type;
542
+ assert(
543
+ `You must include an 'id' for ${modelName} in an object passed to 'push'`,
544
+ data.id !== null && data.id !== undefined && data.id !== ''
545
+ );
546
+ assert(
547
+ `You tried to push data with a type '${modelName}' but no model could be found with that name.`,
548
+ this.store.getSchemaDefinitionService().doesTypeExist(modelName)
549
+ );
550
+
551
+ const resource = constructResource(normalizeModelName(data.type), ensureStringId(data.id), coerceId(data.lid));
552
+ let identifier = this.store.identifierCache.peekRecordIdentifier(resource);
553
+ let isUpdate = false;
554
+
555
+ // store.push will be from empty
556
+ // findRecord will be from root.loading
557
+ // this cannot be loading state if we do not already have an identifier
558
+ // all else will be updates
559
+ if (identifier) {
560
+ const isLoading = _isLoading(this, identifier) || !this.recordIsLoaded(identifier);
561
+ isUpdate = !_isEmpty(this, identifier) && !isLoading;
562
+
563
+ // exclude store.push (root.empty) case
564
+ if (isUpdate || isLoading) {
565
+ identifier = this.store.identifierCache.updateRecordIdentifier(identifier, data);
566
+ }
567
+ } else {
568
+ identifier = this.store.identifierCache.getOrCreateRecordIdentifier(data);
569
+ }
570
+
571
+ const recordData = this.getRecordData(identifier);
572
+ if (recordData.isNew(identifier)) {
573
+ this.store._notificationManager.notify(identifier, 'identity');
574
+ }
575
+
576
+ const hasRecord = this.__instances.record.has(identifier);
577
+ recordData.pushData(identifier, data, hasRecord);
578
+
579
+ if (!isUpdate) {
580
+ this.store.recordArrayManager.identifierAdded(identifier);
581
+ }
582
+
583
+ return identifier as StableExistingRecordIdentifier;
584
+ }
585
+ }
586
+
587
+ function _recordDataIsFullDeleted(identifier: StableRecordIdentifier, recordData: RecordData): boolean {
588
+ return (
589
+ recordData.isDeletionCommitted(identifier) || (recordData.isNew(identifier) && recordData.isDeleted(identifier))
590
+ );
591
+ }
592
+
593
+ export function recordDataIsFullyDeleted(cache: InstanceCache, identifier: StableRecordIdentifier): boolean {
594
+ let recordData = cache.__instances.recordData.get(identifier);
595
+ return !recordData || _recordDataIsFullDeleted(identifier, recordData);
596
+ }
597
+
598
+ /*
599
+ When a find request is triggered on the store, the user can optionally pass in
600
+ attributes and relationships to be preloaded. These are meant to behave as if they
601
+ came back from the server, except the user obtained them out of band and is informing
602
+ the store of their existence. The most common use case is for supporting client side
603
+ nested URLs, such as `/posts/1/comments/2` so the user can do
604
+ `store.findRecord('comment', 2, { preload: { post: 1 } })` without having to fetch the post.
605
+
606
+ Preloaded data can be attributes and relationships passed in either as IDs or as actual
607
+ models.
608
+ */
609
+ type PreloadRelationshipValue = RecordInstance | string;
610
+ function preloadData(store: Store, identifier: StableRecordIdentifier, preload: Dict<unknown>) {
611
+ let jsonPayload: JsonApiResource = {};
612
+ //TODO(Igor) consider the polymorphic case
613
+ const schemas = store.getSchemaDefinitionService();
614
+ const relationships = schemas.relationshipsDefinitionFor(identifier);
615
+ Object.keys(preload).forEach((key) => {
616
+ let preloadValue = preload[key];
617
+
618
+ let relationshipMeta = relationships[key];
619
+ if (relationshipMeta) {
620
+ if (!jsonPayload.relationships) {
621
+ jsonPayload.relationships = {};
622
+ }
623
+ jsonPayload.relationships[key] = preloadRelationship(
624
+ relationshipMeta,
625
+ preloadValue as PreloadRelationshipValue | null | Array<PreloadRelationshipValue>
626
+ );
627
+ } else {
628
+ if (!jsonPayload.attributes) {
629
+ jsonPayload.attributes = {};
630
+ }
631
+ jsonPayload.attributes[key] = preloadValue;
632
+ }
633
+ });
634
+ store._instanceCache.getRecordData(identifier).pushData(identifier, jsonPayload);
635
+ }
636
+
637
+ function preloadRelationship(
638
+ schema: RelationshipSchema,
639
+ preloadValue: PreloadRelationshipValue | null | Array<PreloadRelationshipValue>
640
+ ): JsonApiRelationship {
641
+ const relatedType = schema.type;
642
+
643
+ if (schema.kind === 'hasMany') {
644
+ assert('You need to pass in an array to set a hasMany property on a record', Array.isArray(preloadValue));
645
+ return { data: preloadValue.map((value) => _convertPreloadRelationshipToJSON(value, relatedType)) };
646
+ }
647
+
648
+ assert('You should not pass in an array to set a belongsTo property on a record', !Array.isArray(preloadValue));
649
+ return { data: preloadValue ? _convertPreloadRelationshipToJSON(preloadValue, relatedType) : null };
650
+ }
651
+
652
+ /*
653
+ findRecord('user', '1', { preload: { friends: ['1'] }});
654
+ findRecord('user', '1', { preload: { friends: [record] }});
655
+ */
656
+ function _convertPreloadRelationshipToJSON(
657
+ value: RecordInstance | string,
658
+ type: string
659
+ ): ExistingResourceIdentifierObject | NewResourceIdentifierObject {
660
+ if (typeof value === 'string' || typeof value === 'number') {
661
+ return { type, id: value };
662
+ }
663
+ // TODO if not a record instance assert it's an identifier
664
+ // and allow identifiers to be used
665
+ return recordIdentifierFor(value);
666
+ }
667
+
668
+ function _isEmpty(cache: InstanceCache, identifier: StableRecordIdentifier): boolean {
669
+ const recordData = cache.__instances.recordData.get(identifier);
670
+ if (!recordData) {
671
+ return true;
672
+ }
673
+ const isNew = recordData.isNew(identifier);
674
+ const isDeleted = recordData.isDeleted(identifier);
675
+ const isEmpty = recordData.isEmpty(identifier);
676
+
677
+ return (!isNew || isDeleted) && isEmpty;
678
+ }
679
+
680
+ function _isLoading(cache: InstanceCache, identifier: StableRecordIdentifier): boolean {
681
+ const req = cache.store.getRequestStateService();
682
+ // const fulfilled = req.getLastRequestForRecord(identifier);
683
+ const isLoaded = cache.recordIsLoaded(identifier);
684
+
685
+ return (
686
+ !isLoaded &&
687
+ // fulfilled === null &&
688
+ req.getPendingRequestsForRecord(identifier).some((req) => req.type === 'query')
689
+ );
690
+ }