@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
|
@@ -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
|
+
}
|