@ember-data/model 4.5.0-beta.0 → 4.5.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/-private/attr.js +14 -5
- package/addon/-private/belongs-to.js +9 -5
- package/addon/-private/{system/diff-array.ts → diff-array.ts} +0 -0
- package/addon/-private/errors.ts +7 -69
- package/addon/-private/has-many.js +7 -6
- package/addon/-private/index.ts +6 -5
- package/addon/-private/legacy-data-fetch.js +370 -0
- package/addon/-private/legacy-data-utils.ts +92 -0
- package/addon/-private/legacy-relationships-support.ts +709 -0
- package/addon/-private/{system/many-array.ts → many-array.ts} +38 -33
- package/addon/-private/{system/model-for-mixin.ts → model-for-mixin.ts} +3 -3
- package/addon/-private/model.js +104 -50
- package/addon/-private/notify-changes.ts +21 -13
- package/addon/-private/{system/promise-belongs-to.ts → promise-belongs-to.ts} +9 -8
- package/addon/-private/{system/promise-many-array.ts → promise-many-array.ts} +3 -3
- package/addon/-private/record-state.ts +31 -21
- package/addon/-private/references/belongs-to.ts +585 -0
- package/addon/-private/references/has-many.ts +621 -0
- package/addon/-private/{system/relationships/relationship-meta.ts → relationship-meta.ts} +5 -5
- package/blueprints/model-test/index.js +2 -0
- package/blueprints/model-test/mocha-rfc-232-files/__root__/__path__/__test__.js +1 -1
- package/blueprints/model-test/qunit-files/__root__/__path__/__test__.js +1 -1
- package/index.js +4 -0
- package/package.json +24 -22
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import { assert } from '@ember/debug';
|
|
2
|
+
import { DEBUG } from '@glimmer/env';
|
|
3
|
+
|
|
4
|
+
import { importSync } from '@embroider/macros';
|
|
5
|
+
import { all, resolve } from 'rsvp';
|
|
6
|
+
|
|
7
|
+
import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra';
|
|
8
|
+
import type {
|
|
9
|
+
BelongsToRelationship,
|
|
10
|
+
ManyRelationship,
|
|
11
|
+
RecordData as DefaultRecordData,
|
|
12
|
+
} from '@ember-data/record-data/-private';
|
|
13
|
+
import type { UpgradedMeta } from '@ember-data/record-data/-private/graph/-edge-definition';
|
|
14
|
+
import type { RelationshipState } from '@ember-data/record-data/-private/graph/-state';
|
|
15
|
+
import type Store from '@ember-data/store';
|
|
16
|
+
import type { InternalModel } from '@ember-data/store/-private';
|
|
17
|
+
import { recordDataFor, recordIdentifierFor, storeFor } from '@ember-data/store/-private';
|
|
18
|
+
import type { IdentifierCache } from '@ember-data/store/-private/identifier-cache';
|
|
19
|
+
import type { DSModel } from '@ember-data/types/q/ds-model';
|
|
20
|
+
import type { ResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api';
|
|
21
|
+
import type { StableRecordIdentifier } from '@ember-data/types/q/identifier';
|
|
22
|
+
import type { RecordData } from '@ember-data/types/q/record-data';
|
|
23
|
+
import type { JsonApiRelationship } from '@ember-data/types/q/record-data-json-api';
|
|
24
|
+
import type { RelationshipSchema } from '@ember-data/types/q/record-data-schemas';
|
|
25
|
+
import type { RecordInstance } from '@ember-data/types/q/record-instance';
|
|
26
|
+
import type { DefaultSingleResourceRelationship } from '@ember-data/types/q/relationship-record-data';
|
|
27
|
+
import type { FindOptions } from '@ember-data/types/q/store';
|
|
28
|
+
import type { Dict } from '@ember-data/types/q/utils';
|
|
29
|
+
|
|
30
|
+
import { _findBelongsTo, _findHasMany } from './legacy-data-fetch';
|
|
31
|
+
import { assertIdentifierHasId } from './legacy-data-utils';
|
|
32
|
+
import type { ManyArrayCreateArgs } from './many-array';
|
|
33
|
+
import ManyArray from './many-array';
|
|
34
|
+
import type { BelongsToProxyCreateArgs, BelongsToProxyMeta } from './promise-belongs-to';
|
|
35
|
+
import PromiseBelongsTo from './promise-belongs-to';
|
|
36
|
+
import type { HasManyProxyCreateArgs } from './promise-many-array';
|
|
37
|
+
import PromiseManyArray from './promise-many-array';
|
|
38
|
+
import BelongsToReference from './references/belongs-to';
|
|
39
|
+
import HasManyReference from './references/has-many';
|
|
40
|
+
|
|
41
|
+
type ManyArrayFactory = { create(args: ManyArrayCreateArgs): ManyArray };
|
|
42
|
+
type PromiseBelongsToFactory = { create(args: BelongsToProxyCreateArgs): PromiseBelongsTo };
|
|
43
|
+
|
|
44
|
+
export class LegacySupport {
|
|
45
|
+
declare record: DSModel;
|
|
46
|
+
declare store: Store;
|
|
47
|
+
declare recordData: DefaultRecordData;
|
|
48
|
+
declare references: Dict<BelongsToReference | HasManyReference>;
|
|
49
|
+
declare identifier: StableRecordIdentifier;
|
|
50
|
+
declare _manyArrayCache: Dict<ManyArray>;
|
|
51
|
+
declare _relationshipPromisesCache: Dict<Promise<ManyArray | RecordInstance>>;
|
|
52
|
+
declare _relationshipProxyCache: Dict<PromiseManyArray | PromiseBelongsTo>;
|
|
53
|
+
|
|
54
|
+
declare isDestroying: boolean;
|
|
55
|
+
declare isDestroyed: boolean;
|
|
56
|
+
|
|
57
|
+
constructor(record: DSModel) {
|
|
58
|
+
this.record = record;
|
|
59
|
+
this.store = storeFor(record)!;
|
|
60
|
+
this.identifier = recordIdentifierFor(record);
|
|
61
|
+
this.recordData = this.store._instanceCache.getRecordData(this.identifier) as DefaultRecordData;
|
|
62
|
+
|
|
63
|
+
this._manyArrayCache = Object.create(null) as Dict<ManyArray>;
|
|
64
|
+
this._relationshipPromisesCache = Object.create(null) as Dict<Promise<ManyArray | RecordInstance>>;
|
|
65
|
+
this._relationshipProxyCache = Object.create(null) as Dict<PromiseManyArray | PromiseBelongsTo>;
|
|
66
|
+
this.references = Object.create(null) as Dict<BelongsToReference>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_findBelongsTo(
|
|
70
|
+
key: string,
|
|
71
|
+
resource: DefaultSingleResourceRelationship,
|
|
72
|
+
relationshipMeta: RelationshipSchema,
|
|
73
|
+
options?: FindOptions
|
|
74
|
+
): Promise<RecordInstance | null> {
|
|
75
|
+
// TODO @runspired follow up if parent isNew then we should not be attempting load here
|
|
76
|
+
// TODO @runspired follow up on whether this should be in the relationship requests cache
|
|
77
|
+
return this._findBelongsToByJsonApiResource(resource, this.identifier, relationshipMeta, options).then(
|
|
78
|
+
(identifier: StableRecordIdentifier | null) =>
|
|
79
|
+
handleCompletedRelationshipRequest(this, key, resource._relationship, identifier),
|
|
80
|
+
(e: Error) => handleCompletedRelationshipRequest(this, key, resource._relationship, null, e)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
reloadBelongsTo(key: string, options?: FindOptions): Promise<RecordInstance | null> {
|
|
85
|
+
let loadingPromise = this._relationshipPromisesCache[key] as Promise<RecordInstance | null> | undefined;
|
|
86
|
+
if (loadingPromise) {
|
|
87
|
+
return loadingPromise;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let resource = this.recordData.getBelongsTo(key);
|
|
91
|
+
// TODO move this to a public api
|
|
92
|
+
if (resource._relationship) {
|
|
93
|
+
resource._relationship.state.hasFailedLoadAttempt = false;
|
|
94
|
+
resource._relationship.state.shouldForceReload = true;
|
|
95
|
+
}
|
|
96
|
+
let relationshipMeta = this.store.getSchemaDefinitionService().relationshipsDefinitionFor(this.identifier)[key];
|
|
97
|
+
assert(`Attempted to reload a belongsTo relationship but no definition exists for it`, relationshipMeta);
|
|
98
|
+
let promise = this._findBelongsTo(key, resource, relationshipMeta, options);
|
|
99
|
+
if (this._relationshipProxyCache[key]) {
|
|
100
|
+
return this._updatePromiseProxyFor('belongsTo', key, { promise });
|
|
101
|
+
}
|
|
102
|
+
return promise;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getBelongsTo(key: string, options?: FindOptions): PromiseBelongsTo | RecordInstance | null {
|
|
106
|
+
const { identifier, recordData } = this;
|
|
107
|
+
let resource = recordData.getBelongsTo(key);
|
|
108
|
+
let relatedIdentifier =
|
|
109
|
+
resource && resource.data ? this.store.identifierCache.getOrCreateRecordIdentifier(resource.data) : null;
|
|
110
|
+
let relationshipMeta = this.store.getSchemaDefinitionService().relationshipsDefinitionFor(identifier)[key];
|
|
111
|
+
assert(`Attempted to access a belongsTo relationship but no definition exists for it`, relationshipMeta);
|
|
112
|
+
|
|
113
|
+
let store = this.store;
|
|
114
|
+
let async = relationshipMeta.options.async;
|
|
115
|
+
let isAsync = typeof async === 'undefined' ? true : async;
|
|
116
|
+
let _belongsToState: BelongsToProxyMeta = {
|
|
117
|
+
key,
|
|
118
|
+
store,
|
|
119
|
+
legacySupport: this,
|
|
120
|
+
modelName: relationshipMeta.type,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (isAsync) {
|
|
124
|
+
if (resource._relationship.state.hasFailedLoadAttempt) {
|
|
125
|
+
return this._relationshipProxyCache[key] as PromiseBelongsTo;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let promise = this._findBelongsTo(key, resource, relationshipMeta, options);
|
|
129
|
+
|
|
130
|
+
return this._updatePromiseProxyFor('belongsTo', key, {
|
|
131
|
+
promise,
|
|
132
|
+
content: relatedIdentifier ? store._instanceCache.getRecord(relatedIdentifier) : null,
|
|
133
|
+
_belongsToState,
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
if (relatedIdentifier === null) {
|
|
137
|
+
return null;
|
|
138
|
+
} else {
|
|
139
|
+
let toReturn = store._instanceCache.getRecord(relatedIdentifier);
|
|
140
|
+
assert(
|
|
141
|
+
`You looked up the '${key}' relationship on a '${identifier.type}' with id ${
|
|
142
|
+
identifier.id || 'null'
|
|
143
|
+
} but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (\`belongsTo({ async: true })\`)`,
|
|
144
|
+
toReturn === null || !store._instanceCache.getInternalModel(relatedIdentifier).isEmpty
|
|
145
|
+
);
|
|
146
|
+
return toReturn;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
setDirtyBelongsTo(key: string, value: RecordInstance | null) {
|
|
152
|
+
return this.recordData.setDirtyBelongsTo(key, extractRecordDataFromRecord(value));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getManyArray(key: string, definition?: UpgradedMeta): ManyArray {
|
|
156
|
+
assert('hasMany only works with the @ember-data/record-data package', HAS_RECORD_DATA_PACKAGE);
|
|
157
|
+
let manyArray: ManyArray | undefined = this._manyArrayCache[key];
|
|
158
|
+
if (!definition) {
|
|
159
|
+
const graphFor = (
|
|
160
|
+
importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private')
|
|
161
|
+
).graphFor;
|
|
162
|
+
definition = graphFor(this.store).get(this.identifier, key).definition;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!manyArray) {
|
|
166
|
+
manyArray = (ManyArray as unknown as ManyArrayFactory).create({
|
|
167
|
+
store: this.store,
|
|
168
|
+
type: this.store.modelFor(definition.type),
|
|
169
|
+
recordData: this.recordData,
|
|
170
|
+
key,
|
|
171
|
+
isPolymorphic: definition.isPolymorphic,
|
|
172
|
+
isAsync: definition.isAsync,
|
|
173
|
+
_inverseIsAsync: definition.inverseIsAsync,
|
|
174
|
+
legacySupport: this,
|
|
175
|
+
isLoaded: !definition.isAsync,
|
|
176
|
+
});
|
|
177
|
+
this._manyArrayCache[key] = manyArray;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return manyArray;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fetchAsyncHasMany(
|
|
184
|
+
key: string,
|
|
185
|
+
relationship: ManyRelationship,
|
|
186
|
+
manyArray: ManyArray,
|
|
187
|
+
options?: FindOptions
|
|
188
|
+
): Promise<ManyArray> {
|
|
189
|
+
if (HAS_RECORD_DATA_PACKAGE) {
|
|
190
|
+
let loadingPromise = this._relationshipPromisesCache[key] as Promise<ManyArray> | undefined;
|
|
191
|
+
if (loadingPromise) {
|
|
192
|
+
return loadingPromise;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const jsonApi = this.recordData.getHasMany(key);
|
|
196
|
+
|
|
197
|
+
loadingPromise = this._findHasManyByJsonApiResource(jsonApi, this.identifier, relationship, options).then(
|
|
198
|
+
() => handleCompletedRelationshipRequest(this, key, relationship, manyArray),
|
|
199
|
+
(e: Error) => handleCompletedRelationshipRequest(this, key, relationship, manyArray, e)
|
|
200
|
+
);
|
|
201
|
+
this._relationshipPromisesCache[key] = loadingPromise;
|
|
202
|
+
return loadingPromise;
|
|
203
|
+
}
|
|
204
|
+
assert('hasMany only works with the @ember-data/record-data package');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
reloadHasMany(key: string, options?: FindOptions) {
|
|
208
|
+
if (HAS_RECORD_DATA_PACKAGE) {
|
|
209
|
+
let loadingPromise = this._relationshipPromisesCache[key];
|
|
210
|
+
if (loadingPromise) {
|
|
211
|
+
return loadingPromise;
|
|
212
|
+
}
|
|
213
|
+
const graphFor = (
|
|
214
|
+
importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private')
|
|
215
|
+
).graphFor;
|
|
216
|
+
const relationship = graphFor(this.store).get(this.identifier, key) as ManyRelationship;
|
|
217
|
+
const { definition, state } = relationship;
|
|
218
|
+
|
|
219
|
+
state.hasFailedLoadAttempt = false;
|
|
220
|
+
state.shouldForceReload = true;
|
|
221
|
+
let manyArray = this.getManyArray(key, definition);
|
|
222
|
+
let promise = this.fetchAsyncHasMany(key, relationship, manyArray, options);
|
|
223
|
+
|
|
224
|
+
if (this._relationshipProxyCache[key]) {
|
|
225
|
+
return this._updatePromiseProxyFor('hasMany', key, { promise });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return promise;
|
|
229
|
+
}
|
|
230
|
+
assert(`hasMany only works with the @ember-data/record-data package`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
getHasMany(key: string, options?: FindOptions): PromiseManyArray | ManyArray {
|
|
234
|
+
if (HAS_RECORD_DATA_PACKAGE) {
|
|
235
|
+
const graphFor = (
|
|
236
|
+
importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private')
|
|
237
|
+
).graphFor;
|
|
238
|
+
const relationship = graphFor(this.store).get(this.identifier, key) as ManyRelationship;
|
|
239
|
+
const { definition, state } = relationship;
|
|
240
|
+
let manyArray = this.getManyArray(key, definition);
|
|
241
|
+
|
|
242
|
+
if (definition.isAsync) {
|
|
243
|
+
if (state.hasFailedLoadAttempt) {
|
|
244
|
+
return this._relationshipProxyCache[key] as PromiseManyArray;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let promise = this.fetchAsyncHasMany(key, relationship, manyArray, options);
|
|
248
|
+
|
|
249
|
+
return this._updatePromiseProxyFor('hasMany', key, { promise, content: manyArray });
|
|
250
|
+
} else {
|
|
251
|
+
assert(
|
|
252
|
+
`You looked up the '${key}' relationship on a '${this.identifier.type}' with id ${
|
|
253
|
+
this.identifier.id || 'null'
|
|
254
|
+
} but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async ('hasMany({ async: true })')`,
|
|
255
|
+
!anyUnloaded(this.store, relationship)
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
return manyArray;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
assert(`hasMany only works with the @ember-data/record-data package`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
setDirtyHasMany(key: string, records: RecordInstance[]) {
|
|
265
|
+
assertRecordsPassedToHasMany(records);
|
|
266
|
+
return this.recordData.setDirtyHasMany(key, extractRecordDatasFromRecords(records));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
_updatePromiseProxyFor(kind: 'hasMany', key: string, args: HasManyProxyCreateArgs): PromiseManyArray;
|
|
270
|
+
_updatePromiseProxyFor(kind: 'belongsTo', key: string, args: BelongsToProxyCreateArgs): PromiseBelongsTo;
|
|
271
|
+
_updatePromiseProxyFor(
|
|
272
|
+
kind: 'belongsTo',
|
|
273
|
+
key: string,
|
|
274
|
+
args: { promise: Promise<RecordInstance | null> }
|
|
275
|
+
): PromiseBelongsTo;
|
|
276
|
+
_updatePromiseProxyFor(
|
|
277
|
+
kind: 'hasMany' | 'belongsTo',
|
|
278
|
+
key: string,
|
|
279
|
+
args: BelongsToProxyCreateArgs | HasManyProxyCreateArgs | { promise: Promise<RecordInstance | null> }
|
|
280
|
+
): PromiseBelongsTo | PromiseManyArray {
|
|
281
|
+
let promiseProxy = this._relationshipProxyCache[key];
|
|
282
|
+
if (kind === 'hasMany') {
|
|
283
|
+
const { promise, content } = args as HasManyProxyCreateArgs;
|
|
284
|
+
if (promiseProxy) {
|
|
285
|
+
assert(`Expected a PromiseManyArray`, '_update' in promiseProxy);
|
|
286
|
+
promiseProxy._update(promise, content);
|
|
287
|
+
} else {
|
|
288
|
+
promiseProxy = this._relationshipProxyCache[key] = new PromiseManyArray(promise, content);
|
|
289
|
+
}
|
|
290
|
+
return promiseProxy;
|
|
291
|
+
}
|
|
292
|
+
if (promiseProxy) {
|
|
293
|
+
const { promise, content } = args as BelongsToProxyCreateArgs;
|
|
294
|
+
assert(`Expected a PromiseBelongsTo`, '_belongsToState' in promiseProxy);
|
|
295
|
+
|
|
296
|
+
if (content !== undefined) {
|
|
297
|
+
promiseProxy.set('content', content);
|
|
298
|
+
}
|
|
299
|
+
void promiseProxy.set('promise', promise);
|
|
300
|
+
} else {
|
|
301
|
+
promiseProxy = (PromiseBelongsTo as unknown as PromiseBelongsToFactory).create(args as BelongsToProxyCreateArgs);
|
|
302
|
+
this._relationshipProxyCache[key] = promiseProxy;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return promiseProxy;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
referenceFor(kind: string | null, name: string) {
|
|
309
|
+
let reference = this.references[name];
|
|
310
|
+
|
|
311
|
+
if (!reference) {
|
|
312
|
+
if (!HAS_RECORD_DATA_PACKAGE) {
|
|
313
|
+
// TODO @runspired while this feels odd, it is not a regression in capability because we do
|
|
314
|
+
// not today support references pulling from RecordDatas other than our own
|
|
315
|
+
// because of the intimate API access involved. This is something we will need to redesign.
|
|
316
|
+
assert(`snapshot.belongsTo only supported for @ember-data/record-data`);
|
|
317
|
+
}
|
|
318
|
+
const graphFor = (
|
|
319
|
+
importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private')
|
|
320
|
+
).graphFor;
|
|
321
|
+
const relationship = graphFor(this.store).get(this.identifier, name);
|
|
322
|
+
|
|
323
|
+
if (DEBUG && kind) {
|
|
324
|
+
let modelName = this.identifier.type;
|
|
325
|
+
let actualRelationshipKind = relationship.definition.kind;
|
|
326
|
+
assert(
|
|
327
|
+
`You tried to get the '${name}' relationship on a '${modelName}' via record.${kind}('${name}'), but the relationship is of kind '${actualRelationshipKind}'. Use record.${actualRelationshipKind}('${name}') instead.`,
|
|
328
|
+
actualRelationshipKind === kind
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let relationshipKind = relationship.definition.kind;
|
|
333
|
+
|
|
334
|
+
if (relationshipKind === 'belongsTo') {
|
|
335
|
+
reference = new BelongsToReference(this.store, this.identifier, relationship as BelongsToRelationship, name);
|
|
336
|
+
} else if (relationshipKind === 'hasMany') {
|
|
337
|
+
reference = new HasManyReference(this.store, this.identifier, relationship as ManyRelationship, name);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this.references[name] = reference;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return reference;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
_findHasManyByJsonApiResource(
|
|
347
|
+
resource,
|
|
348
|
+
parentIdentifier: StableRecordIdentifier,
|
|
349
|
+
relationship: ManyRelationship,
|
|
350
|
+
options: FindOptions = {}
|
|
351
|
+
): Promise<void | unknown[]> {
|
|
352
|
+
if (HAS_RECORD_DATA_PACKAGE) {
|
|
353
|
+
if (!resource) {
|
|
354
|
+
return resolve();
|
|
355
|
+
}
|
|
356
|
+
const { definition, state } = relationship;
|
|
357
|
+
let adapter = this.store.adapterFor(definition.type);
|
|
358
|
+
|
|
359
|
+
let { isStale, hasDematerializedInverse, hasReceivedData, isEmpty, shouldForceReload } = state;
|
|
360
|
+
const allInverseRecordsAreLoaded = areAllInverseRecordsLoaded(this.store, resource);
|
|
361
|
+
|
|
362
|
+
let shouldFindViaLink =
|
|
363
|
+
resource.links &&
|
|
364
|
+
resource.links.related &&
|
|
365
|
+
(typeof adapter.findHasMany === 'function' || typeof resource.data === 'undefined') &&
|
|
366
|
+
(shouldForceReload || hasDematerializedInverse || isStale || (!allInverseRecordsAreLoaded && !isEmpty));
|
|
367
|
+
|
|
368
|
+
// fetch via link
|
|
369
|
+
if (shouldFindViaLink) {
|
|
370
|
+
// findHasMany, although not public, does not need to care about our upgrade relationship definitions
|
|
371
|
+
// and can stick with the public definition API for now.
|
|
372
|
+
const relationshipMeta = this.store._instanceCache._storeWrapper.relationshipsDefinitionFor(
|
|
373
|
+
definition.inverseType
|
|
374
|
+
)[definition.key];
|
|
375
|
+
let adapter = this.store.adapterFor(parentIdentifier.type);
|
|
376
|
+
|
|
377
|
+
/*
|
|
378
|
+
If a relationship was originally populated by the adapter as a link
|
|
379
|
+
(as opposed to a list of IDs), this method is called when the
|
|
380
|
+
relationship is fetched.
|
|
381
|
+
|
|
382
|
+
The link (which is usually a URL) is passed through unchanged, so the
|
|
383
|
+
adapter can make whatever request it wants.
|
|
384
|
+
|
|
385
|
+
The usual use-case is for the server to register a URL as a link, and
|
|
386
|
+
then use that URL in the future to make a request for the relationship.
|
|
387
|
+
*/
|
|
388
|
+
assert(
|
|
389
|
+
`You tried to load a hasMany relationship but you have no adapter (for ${parentIdentifier.type})`,
|
|
390
|
+
adapter
|
|
391
|
+
);
|
|
392
|
+
assert(
|
|
393
|
+
`You tried to load a hasMany relationship from a specified 'link' in the original payload but your adapter does not implement 'findHasMany'`,
|
|
394
|
+
typeof adapter.findHasMany === 'function'
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
return _findHasMany(adapter, this.store, parentIdentifier, resource.links.related, relationshipMeta, options);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
let preferLocalCache = hasReceivedData && !isEmpty;
|
|
401
|
+
|
|
402
|
+
let hasLocalPartialData =
|
|
403
|
+
hasDematerializedInverse || (isEmpty && Array.isArray(resource.data) && resource.data.length > 0);
|
|
404
|
+
|
|
405
|
+
// fetch using data, pulling from local cache if possible
|
|
406
|
+
if (!shouldForceReload && !isStale && (preferLocalCache || hasLocalPartialData)) {
|
|
407
|
+
let finds = new Array(resource.data.length);
|
|
408
|
+
for (let i = 0; i < resource.data.length; i++) {
|
|
409
|
+
let identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resource.data[i]);
|
|
410
|
+
finds[i] = this.store._instanceCache._fetchDataIfNeededForIdentifier(identifier, options);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return all(finds);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
let hasData = hasReceivedData && !isEmpty;
|
|
417
|
+
|
|
418
|
+
// fetch by data
|
|
419
|
+
if (hasData || hasLocalPartialData) {
|
|
420
|
+
let identifiers = resource.data.map((json) => this.store.identifierCache.getOrCreateRecordIdentifier(json));
|
|
421
|
+
let fetches = new Array(identifiers.length);
|
|
422
|
+
const manager = this.store._fetchManager;
|
|
423
|
+
|
|
424
|
+
for (let i = 0; i < identifiers.length; i++) {
|
|
425
|
+
let identifier = identifiers[i];
|
|
426
|
+
assertIdentifierHasId(identifier);
|
|
427
|
+
fetches[i] = manager.scheduleFetch(identifier, options);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return all(fetches);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// we were explicitly told we have no data and no links.
|
|
434
|
+
// TODO if the relationshipIsStale, should we hit the adapter anyway?
|
|
435
|
+
return resolve();
|
|
436
|
+
}
|
|
437
|
+
assert(`hasMany only works with the @ember-data/record-data package`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
_findBelongsToByJsonApiResource(
|
|
441
|
+
resource,
|
|
442
|
+
parentIdentifier: StableRecordIdentifier,
|
|
443
|
+
relationshipMeta,
|
|
444
|
+
options: FindOptions = {}
|
|
445
|
+
): Promise<StableRecordIdentifier | null> {
|
|
446
|
+
if (!resource) {
|
|
447
|
+
return resolve(null);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const internalModel = resource.data ? this.store._instanceCache._internalModelForResource(resource.data) : null;
|
|
451
|
+
|
|
452
|
+
let { isStale, hasDematerializedInverse, hasReceivedData, isEmpty, shouldForceReload } = resource._relationship
|
|
453
|
+
.state as RelationshipState;
|
|
454
|
+
const allInverseRecordsAreLoaded = areAllInverseRecordsLoaded(this.store, resource);
|
|
455
|
+
|
|
456
|
+
let shouldFindViaLink =
|
|
457
|
+
resource.links &&
|
|
458
|
+
resource.links.related &&
|
|
459
|
+
(shouldForceReload || hasDematerializedInverse || isStale || (!allInverseRecordsAreLoaded && !isEmpty));
|
|
460
|
+
|
|
461
|
+
if (internalModel) {
|
|
462
|
+
// short circuit if we are already loading
|
|
463
|
+
let pendingRequest = this.store._fetchManager.getPendingFetch(internalModel.identifier, options);
|
|
464
|
+
if (pendingRequest) {
|
|
465
|
+
return pendingRequest;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// fetch via link
|
|
470
|
+
if (shouldFindViaLink) {
|
|
471
|
+
return _findBelongsTo(this.store, parentIdentifier, resource.links.related, relationshipMeta, options);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let preferLocalCache = hasReceivedData && allInverseRecordsAreLoaded && !isEmpty;
|
|
475
|
+
let hasLocalPartialData = hasDematerializedInverse || (isEmpty && resource.data);
|
|
476
|
+
// null is explicit empty, undefined is "we don't know anything"
|
|
477
|
+
let localDataIsEmpty = resource.data === undefined || resource.data === null;
|
|
478
|
+
|
|
479
|
+
// fetch using data, pulling from local cache if possible
|
|
480
|
+
if (!shouldForceReload && !isStale && (preferLocalCache || hasLocalPartialData)) {
|
|
481
|
+
/*
|
|
482
|
+
We have canonical data, but our local state is empty
|
|
483
|
+
*/
|
|
484
|
+
if (localDataIsEmpty) {
|
|
485
|
+
return resolve(null);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!internalModel) {
|
|
489
|
+
assert(`No InternalModel found for ${resource.lid}`, internalModel);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return this.store._instanceCache._fetchDataIfNeededForIdentifier(internalModel.identifier, options);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
let resourceIsLocal = !localDataIsEmpty && resource.data.id === null;
|
|
496
|
+
|
|
497
|
+
if (internalModel && resourceIsLocal) {
|
|
498
|
+
return resolve(internalModel.identifier);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// fetch by data
|
|
502
|
+
if (internalModel && !localDataIsEmpty) {
|
|
503
|
+
let identifier = internalModel.identifier;
|
|
504
|
+
assertIdentifierHasId(identifier);
|
|
505
|
+
|
|
506
|
+
return this.store._fetchManager.scheduleFetch(identifier, options);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// we were explicitly told we have no data and no links.
|
|
510
|
+
// TODO if the relationshipIsStale, should we hit the adapter anyway?
|
|
511
|
+
return resolve(null);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
destroy() {
|
|
515
|
+
assert(
|
|
516
|
+
'Cannot destroy an internalModel while its record is materialized',
|
|
517
|
+
!this.record || this.record.isDestroyed || this.record.isDestroying
|
|
518
|
+
);
|
|
519
|
+
this.isDestroying = true;
|
|
520
|
+
|
|
521
|
+
const cache = this._manyArrayCache;
|
|
522
|
+
Object.keys(cache).forEach((key) => {
|
|
523
|
+
cache[key]!.destroy();
|
|
524
|
+
delete cache[key];
|
|
525
|
+
});
|
|
526
|
+
const keys = Object.keys(this._relationshipProxyCache);
|
|
527
|
+
keys.forEach((key) => {
|
|
528
|
+
const proxy = this._relationshipProxyCache[key]!;
|
|
529
|
+
if (proxy.destroy) {
|
|
530
|
+
proxy.destroy();
|
|
531
|
+
}
|
|
532
|
+
delete this._relationshipProxyCache[key];
|
|
533
|
+
});
|
|
534
|
+
if (this.references) {
|
|
535
|
+
const refs = this.references;
|
|
536
|
+
Object.keys(refs).forEach((key) => {
|
|
537
|
+
refs[key]!.destroy();
|
|
538
|
+
delete refs[key];
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
this.isDestroyed = true;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function handleCompletedRelationshipRequest(
|
|
546
|
+
recordExt: LegacySupport,
|
|
547
|
+
key: string,
|
|
548
|
+
relationship: BelongsToRelationship,
|
|
549
|
+
value: StableRecordIdentifier | null
|
|
550
|
+
): RecordInstance | null;
|
|
551
|
+
function handleCompletedRelationshipRequest(
|
|
552
|
+
recordExt: LegacySupport,
|
|
553
|
+
key: string,
|
|
554
|
+
relationship: ManyRelationship,
|
|
555
|
+
value: ManyArray
|
|
556
|
+
): ManyArray;
|
|
557
|
+
function handleCompletedRelationshipRequest(
|
|
558
|
+
recordExt: LegacySupport,
|
|
559
|
+
key: string,
|
|
560
|
+
relationship: BelongsToRelationship,
|
|
561
|
+
value: null,
|
|
562
|
+
error: Error
|
|
563
|
+
): never;
|
|
564
|
+
function handleCompletedRelationshipRequest(
|
|
565
|
+
recordExt: LegacySupport,
|
|
566
|
+
key: string,
|
|
567
|
+
relationship: ManyRelationship,
|
|
568
|
+
value: ManyArray,
|
|
569
|
+
error: Error
|
|
570
|
+
): never;
|
|
571
|
+
function handleCompletedRelationshipRequest(
|
|
572
|
+
recordExt: LegacySupport,
|
|
573
|
+
key: string,
|
|
574
|
+
relationship: BelongsToRelationship | ManyRelationship,
|
|
575
|
+
value: ManyArray | StableRecordIdentifier | null,
|
|
576
|
+
error?: Error
|
|
577
|
+
): ManyArray | RecordInstance | null {
|
|
578
|
+
delete recordExt._relationshipPromisesCache[key];
|
|
579
|
+
relationship.state.shouldForceReload = false;
|
|
580
|
+
const isHasMany = relationship.definition.kind === 'hasMany';
|
|
581
|
+
|
|
582
|
+
if (isHasMany) {
|
|
583
|
+
// we don't notify the record property here to avoid refetch
|
|
584
|
+
// only the many array
|
|
585
|
+
(value as ManyArray).notify();
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (error) {
|
|
589
|
+
relationship.state.hasFailedLoadAttempt = true;
|
|
590
|
+
let proxy = recordExt._relationshipProxyCache[key];
|
|
591
|
+
// belongsTo relationships are sometimes unloaded
|
|
592
|
+
// when a load fails, in this case we need
|
|
593
|
+
// to make sure that we aren't proxying
|
|
594
|
+
// to destroyed content
|
|
595
|
+
// for the sync belongsTo reload case there will be no proxy
|
|
596
|
+
// for the async reload case there will be no proxy if the ui
|
|
597
|
+
// has never been accessed
|
|
598
|
+
if (proxy && !isHasMany) {
|
|
599
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
600
|
+
if (proxy.content && proxy.content.isDestroying) {
|
|
601
|
+
(proxy as PromiseBelongsTo).set('content', null);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
throw error;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (isHasMany) {
|
|
609
|
+
(value as ManyArray).set('isLoaded', true);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
relationship.state.hasFailedLoadAttempt = false;
|
|
613
|
+
// only set to not stale if no error is thrown
|
|
614
|
+
relationship.state.isStale = false;
|
|
615
|
+
|
|
616
|
+
return isHasMany || !value
|
|
617
|
+
? (value as ManyArray | null)
|
|
618
|
+
: recordExt.store.peekRecord(value as StableRecordIdentifier);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function assertRecordsPassedToHasMany(records: RecordInstance[]) {
|
|
622
|
+
assert(`You must pass an array of records to set a hasMany relationship`, Array.isArray(records));
|
|
623
|
+
assert(
|
|
624
|
+
`All elements of a hasMany relationship must be instances of Model, you passed ${records
|
|
625
|
+
.map((r) => `${typeof r}`)
|
|
626
|
+
.join(', ')}`,
|
|
627
|
+
(function () {
|
|
628
|
+
return records.every((record) => Object.prototype.hasOwnProperty.call(record, '_internalModel') === true);
|
|
629
|
+
})()
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function extractRecordDatasFromRecords(records: RecordInstance[]): RecordData[] {
|
|
634
|
+
return records.map(extractRecordDataFromRecord) as RecordData[];
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
type PromiseProxyRecord = { then(): void; get(str: 'content'): RecordInstance | null | undefined };
|
|
638
|
+
|
|
639
|
+
function extractRecordDataFromRecord(recordOrPromiseRecord: PromiseProxyRecord | RecordInstance | null) {
|
|
640
|
+
if (!recordOrPromiseRecord) {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (isPromiseRecord(recordOrPromiseRecord)) {
|
|
645
|
+
let content = recordOrPromiseRecord.get && recordOrPromiseRecord.get('content');
|
|
646
|
+
assert(
|
|
647
|
+
'You passed in a promise that did not originate from an EmberData relationship. You can only pass promises that come from a belongsTo or hasMany relationship to the get call.',
|
|
648
|
+
content !== undefined
|
|
649
|
+
);
|
|
650
|
+
return content ? recordDataFor(content) : null;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return recordDataFor(recordOrPromiseRecord);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function isPromiseRecord(record: PromiseProxyRecord | RecordInstance): record is PromiseProxyRecord {
|
|
657
|
+
return !!record.then;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function anyUnloaded(store: Store, relationship: ManyRelationship) {
|
|
661
|
+
let state = relationship.currentState;
|
|
662
|
+
const unloaded = state.find((s) => {
|
|
663
|
+
let im = store._instanceCache.getInternalModel(s);
|
|
664
|
+
return im._isDematerializing || !im.isLoaded;
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
return unloaded || false;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Flag indicating whether all inverse records are available
|
|
672
|
+
*
|
|
673
|
+
* true if the inverse exists and is loaded (not empty)
|
|
674
|
+
* true if there is no inverse
|
|
675
|
+
* false if the inverse exists and is not loaded (empty)
|
|
676
|
+
*
|
|
677
|
+
* @internal
|
|
678
|
+
* @return {boolean}
|
|
679
|
+
*/
|
|
680
|
+
function areAllInverseRecordsLoaded(store: Store, resource: JsonApiRelationship): boolean {
|
|
681
|
+
const cache = store.identifierCache;
|
|
682
|
+
|
|
683
|
+
if (Array.isArray(resource.data)) {
|
|
684
|
+
// treat as collection
|
|
685
|
+
// check for unloaded records
|
|
686
|
+
let hasEmptyRecords = resource.data.reduce((hasEmptyModel, resourceIdentifier) => {
|
|
687
|
+
return hasEmptyModel || internalModelForRelatedResource(store, cache, resourceIdentifier).isEmpty;
|
|
688
|
+
}, false);
|
|
689
|
+
|
|
690
|
+
return !hasEmptyRecords;
|
|
691
|
+
} else {
|
|
692
|
+
// treat as single resource
|
|
693
|
+
if (!resource.data) {
|
|
694
|
+
return true;
|
|
695
|
+
} else {
|
|
696
|
+
const internalModel = internalModelForRelatedResource(store, cache, resource.data);
|
|
697
|
+
return !internalModel.isEmpty;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function internalModelForRelatedResource(
|
|
703
|
+
store: Store,
|
|
704
|
+
cache: IdentifierCache,
|
|
705
|
+
resource: ResourceIdentifierObject
|
|
706
|
+
): InternalModel {
|
|
707
|
+
const identifier = cache.getOrCreateRecordIdentifier(resource);
|
|
708
|
+
return store._instanceCache._internalModelForResource(identifier);
|
|
709
|
+
}
|