@ember-data/model 4.7.0-beta.0 → 4.7.1
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 +9 -24
- package/addon/-private/belongs-to.js +87 -25
- package/addon/-private/deprecated-promise-proxy.ts +54 -0
- package/addon/-private/errors.ts +6 -6
- package/addon/-private/has-many.js +84 -22
- package/addon/-private/legacy-data-fetch.js +35 -13
- package/addon/-private/legacy-data-utils.ts +1 -1
- package/addon/-private/legacy-relationships-support.ts +279 -218
- package/addon/-private/many-array.ts +216 -242
- package/addon/-private/model-for-mixin.ts +2 -4
- package/addon/-private/model.js +565 -172
- package/addon/-private/notify-changes.ts +2 -2
- package/addon/-private/promise-belongs-to.ts +1 -1
- package/addon/-private/promise-many-array.ts +204 -124
- package/addon/-private/promise-proxy-base.js +4 -0
- package/addon/-private/record-state.ts +63 -46
- package/addon/-private/references/belongs-to.ts +65 -38
- package/addon/-private/references/has-many.ts +86 -55
- package/addon/-private/relationship-meta.ts +8 -26
- package/index.js +3 -0
- package/package.json +13 -10
|
@@ -1,36 +1,31 @@
|
|
|
1
|
-
import { assert } from '@ember/debug';
|
|
1
|
+
import { assert, deprecate } from '@ember/debug';
|
|
2
2
|
import { DEBUG } from '@glimmer/env';
|
|
3
3
|
|
|
4
4
|
import { importSync } from '@embroider/macros';
|
|
5
5
|
import { all, resolve } from 'rsvp';
|
|
6
6
|
|
|
7
7
|
import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra';
|
|
8
|
-
import
|
|
9
|
-
BelongsToRelationship,
|
|
10
|
-
ManyRelationship,
|
|
11
|
-
RecordData as DefaultRecordData,
|
|
12
|
-
} from '@ember-data/record-data/-private';
|
|
8
|
+
import { DEPRECATE_PROMISE_PROXIES } from '@ember-data/private-build-infra/deprecations';
|
|
13
9
|
import type { UpgradedMeta } from '@ember-data/record-data/-private/graph/-edge-definition';
|
|
14
|
-
import type {
|
|
10
|
+
import type { LocalRelationshipOperation } from '@ember-data/record-data/-private/graph/-operations';
|
|
11
|
+
import type { ImplicitRelationship } from '@ember-data/record-data/-private/graph/index';
|
|
12
|
+
import type BelongsToRelationship from '@ember-data/record-data/-private/relationships/state/belongs-to';
|
|
13
|
+
import type ManyRelationship from '@ember-data/record-data/-private/relationships/state/has-many';
|
|
15
14
|
import type Store from '@ember-data/store';
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
18
|
-
import type { IdentifierCache } from '@ember-data/store/-private/identifier-cache';
|
|
15
|
+
import { fastPush, isStableIdentifier, recordIdentifierFor, SOURCE, storeFor } from '@ember-data/store/-private';
|
|
16
|
+
import type { NonSingletonRecordDataManager } from '@ember-data/store/-private/managers/record-data-manager';
|
|
19
17
|
import type { DSModel } from '@ember-data/types/q/ds-model';
|
|
20
|
-
import
|
|
18
|
+
import { CollectionResourceRelationship, SingleResourceRelationship } from '@ember-data/types/q/ember-data-json-api';
|
|
21
19
|
import type { StableRecordIdentifier } from '@ember-data/types/q/identifier';
|
|
22
20
|
import type { RecordData } from '@ember-data/types/q/record-data';
|
|
23
21
|
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
22
|
import type { RecordInstance } from '@ember-data/types/q/record-instance';
|
|
26
|
-
import type { DefaultSingleResourceRelationship } from '@ember-data/types/q/relationship-record-data';
|
|
27
23
|
import type { FindOptions } from '@ember-data/types/q/store';
|
|
28
24
|
import type { Dict } from '@ember-data/types/q/utils';
|
|
29
25
|
|
|
30
26
|
import { _findBelongsTo, _findHasMany } from './legacy-data-fetch';
|
|
31
27
|
import { assertIdentifierHasId } from './legacy-data-utils';
|
|
32
|
-
import
|
|
33
|
-
import ManyArray from './many-array';
|
|
28
|
+
import RelatedCollection from './many-array';
|
|
34
29
|
import type { BelongsToProxyCreateArgs, BelongsToProxyMeta } from './promise-belongs-to';
|
|
35
30
|
import PromiseBelongsTo from './promise-belongs-to';
|
|
36
31
|
import type { HasManyProxyCreateArgs } from './promise-many-array';
|
|
@@ -38,17 +33,16 @@ import PromiseManyArray from './promise-many-array';
|
|
|
38
33
|
import BelongsToReference from './references/belongs-to';
|
|
39
34
|
import HasManyReference from './references/has-many';
|
|
40
35
|
|
|
41
|
-
type ManyArrayFactory = { create(args: ManyArrayCreateArgs): ManyArray };
|
|
42
36
|
type PromiseBelongsToFactory = { create(args: BelongsToProxyCreateArgs): PromiseBelongsTo };
|
|
43
37
|
|
|
44
38
|
export class LegacySupport {
|
|
45
39
|
declare record: DSModel;
|
|
46
40
|
declare store: Store;
|
|
47
|
-
declare recordData:
|
|
41
|
+
declare recordData: RecordData;
|
|
48
42
|
declare references: Dict<BelongsToReference | HasManyReference>;
|
|
49
43
|
declare identifier: StableRecordIdentifier;
|
|
50
|
-
declare _manyArrayCache: Dict<
|
|
51
|
-
declare _relationshipPromisesCache: Dict<Promise<
|
|
44
|
+
declare _manyArrayCache: Dict<RelatedCollection>;
|
|
45
|
+
declare _relationshipPromisesCache: Dict<Promise<RelatedCollection | RecordInstance>>;
|
|
52
46
|
declare _relationshipProxyCache: Dict<PromiseManyArray | PromiseBelongsTo>;
|
|
53
47
|
|
|
54
48
|
declare isDestroying: boolean;
|
|
@@ -58,26 +52,52 @@ export class LegacySupport {
|
|
|
58
52
|
this.record = record;
|
|
59
53
|
this.store = storeFor(record)!;
|
|
60
54
|
this.identifier = recordIdentifierFor(record);
|
|
61
|
-
this.recordData = this.store._instanceCache.getRecordData(this.identifier)
|
|
55
|
+
this.recordData = this.store._instanceCache.getRecordData(this.identifier);
|
|
62
56
|
|
|
63
|
-
this._manyArrayCache = Object.create(null) as Dict<
|
|
64
|
-
this._relationshipPromisesCache = Object.create(null) as Dict<Promise<
|
|
57
|
+
this._manyArrayCache = Object.create(null) as Dict<RelatedCollection>;
|
|
58
|
+
this._relationshipPromisesCache = Object.create(null) as Dict<Promise<RelatedCollection | RecordInstance>>;
|
|
65
59
|
this._relationshipProxyCache = Object.create(null) as Dict<PromiseManyArray | PromiseBelongsTo>;
|
|
66
60
|
this.references = Object.create(null) as Dict<BelongsToReference>;
|
|
67
61
|
}
|
|
68
62
|
|
|
63
|
+
_syncArray(array: RelatedCollection) {
|
|
64
|
+
// It’s possible the parent side of the relationship may have been destroyed by this point
|
|
65
|
+
if (this.isDestroyed || this.isDestroying) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const currentState = array[SOURCE];
|
|
69
|
+
const identifier = this.identifier;
|
|
70
|
+
|
|
71
|
+
let [identifiers, jsonApi] = this._getCurrentState(identifier, array.key);
|
|
72
|
+
|
|
73
|
+
if (jsonApi.meta) {
|
|
74
|
+
array.meta = jsonApi.meta;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (jsonApi.links) {
|
|
78
|
+
array.links = jsonApi.links;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
currentState.length = 0;
|
|
82
|
+
fastPush(currentState, identifiers);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
updateCache(operation: LocalRelationshipOperation): void {
|
|
86
|
+
this.recordData.update(operation);
|
|
87
|
+
}
|
|
88
|
+
|
|
69
89
|
_findBelongsTo(
|
|
70
90
|
key: string,
|
|
71
|
-
resource:
|
|
72
|
-
|
|
91
|
+
resource: SingleResourceRelationship,
|
|
92
|
+
relationship: BelongsToRelationship,
|
|
73
93
|
options?: FindOptions
|
|
74
94
|
): Promise<RecordInstance | null> {
|
|
75
95
|
// TODO @runspired follow up if parent isNew then we should not be attempting load here
|
|
76
96
|
// TODO @runspired follow up on whether this should be in the relationship requests cache
|
|
77
|
-
return this._findBelongsToByJsonApiResource(resource, this.identifier,
|
|
97
|
+
return this._findBelongsToByJsonApiResource(resource, this.identifier, relationship, options).then(
|
|
78
98
|
(identifier: StableRecordIdentifier | null) =>
|
|
79
|
-
handleCompletedRelationshipRequest(this, key,
|
|
80
|
-
(e: Error) => handleCompletedRelationshipRequest(this, key,
|
|
99
|
+
handleCompletedRelationshipRequest(this, key, relationship, identifier),
|
|
100
|
+
(e: Error) => handleCompletedRelationshipRequest(this, key, relationship, null, e)
|
|
81
101
|
);
|
|
82
102
|
}
|
|
83
103
|
|
|
@@ -87,15 +107,16 @@ export class LegacySupport {
|
|
|
87
107
|
return loadingPromise;
|
|
88
108
|
}
|
|
89
109
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
let
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
const graphFor = (
|
|
111
|
+
importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private')
|
|
112
|
+
).graphFor;
|
|
113
|
+
const relationship = graphFor(this.store).get(this.identifier, key);
|
|
114
|
+
assert(`Expected ${key} to be a belongs-to relationship`, isBelongsTo(relationship));
|
|
115
|
+
|
|
116
|
+
let resource = this.recordData.getRelationship(this.identifier, key) as SingleResourceRelationship;
|
|
117
|
+
relationship.state.hasFailedLoadAttempt = false;
|
|
118
|
+
relationship.state.shouldForceReload = true;
|
|
119
|
+
let promise = this._findBelongsTo(key, resource, relationship, options);
|
|
99
120
|
if (this._relationshipProxyCache[key]) {
|
|
100
121
|
return this._updatePromiseProxyFor('belongsTo', key, { promise });
|
|
101
122
|
}
|
|
@@ -104,32 +125,36 @@ export class LegacySupport {
|
|
|
104
125
|
|
|
105
126
|
getBelongsTo(key: string, options?: FindOptions): PromiseBelongsTo | RecordInstance | null {
|
|
106
127
|
const { identifier, recordData } = this;
|
|
107
|
-
let resource = recordData.
|
|
108
|
-
let relatedIdentifier =
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
let resource = recordData.getRelationship(this.identifier, key) as SingleResourceRelationship;
|
|
129
|
+
let relatedIdentifier = resource && resource.data ? resource.data : null;
|
|
130
|
+
assert(`Expected a stable identifier`, !relatedIdentifier || isStableIdentifier(relatedIdentifier));
|
|
131
|
+
|
|
132
|
+
const store = this.store;
|
|
133
|
+
const graphFor = (
|
|
134
|
+
importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private')
|
|
135
|
+
).graphFor;
|
|
136
|
+
const relationship = graphFor(store).get(this.identifier, key);
|
|
137
|
+
assert(`Expected ${key} to be a belongs-to relationship`, isBelongsTo(relationship));
|
|
138
|
+
|
|
139
|
+
let isAsync = relationship.definition.isAsync;
|
|
116
140
|
let _belongsToState: BelongsToProxyMeta = {
|
|
117
141
|
key,
|
|
118
142
|
store,
|
|
119
143
|
legacySupport: this,
|
|
120
|
-
modelName:
|
|
144
|
+
modelName: relationship.definition.type,
|
|
121
145
|
};
|
|
122
146
|
|
|
123
147
|
if (isAsync) {
|
|
124
|
-
if (
|
|
148
|
+
if (relationship.state.hasFailedLoadAttempt) {
|
|
125
149
|
return this._relationshipProxyCache[key] as PromiseBelongsTo;
|
|
126
150
|
}
|
|
127
151
|
|
|
128
|
-
let promise = this._findBelongsTo(key, resource,
|
|
152
|
+
let promise = this._findBelongsTo(key, resource, relationship, options);
|
|
153
|
+
const isLoaded = relatedIdentifier && store._instanceCache.recordIsLoaded(relatedIdentifier);
|
|
129
154
|
|
|
130
155
|
return this._updatePromiseProxyFor('belongsTo', key, {
|
|
131
156
|
promise,
|
|
132
|
-
content:
|
|
157
|
+
content: isLoaded ? store._instanceCache.getRecord(relatedIdentifier!) : null,
|
|
133
158
|
_belongsToState,
|
|
134
159
|
});
|
|
135
160
|
} else {
|
|
@@ -140,8 +165,8 @@ export class LegacySupport {
|
|
|
140
165
|
assert(
|
|
141
166
|
`You looked up the '${key}' relationship on a '${identifier.type}' with id ${
|
|
142
167
|
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 ||
|
|
168
|
+
} 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(<type>, { async: true, inverse: <inverse> })\`)`,
|
|
169
|
+
toReturn === null || store._instanceCache.recordIsLoaded(relatedIdentifier, true)
|
|
145
170
|
);
|
|
146
171
|
return toReturn;
|
|
147
172
|
}
|
|
@@ -149,52 +174,100 @@ export class LegacySupport {
|
|
|
149
174
|
}
|
|
150
175
|
|
|
151
176
|
setDirtyBelongsTo(key: string, value: RecordInstance | null) {
|
|
152
|
-
return this.recordData.
|
|
177
|
+
return this.recordData.update(
|
|
178
|
+
{
|
|
179
|
+
op: 'replaceRelatedRecord',
|
|
180
|
+
record: this.identifier,
|
|
181
|
+
field: key,
|
|
182
|
+
value: extractIdentifierFromRecord(value),
|
|
183
|
+
},
|
|
184
|
+
// @ts-expect-error
|
|
185
|
+
true
|
|
186
|
+
);
|
|
153
187
|
}
|
|
154
188
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
189
|
+
_getCurrentState(
|
|
190
|
+
identifier: StableRecordIdentifier,
|
|
191
|
+
field: string
|
|
192
|
+
): [StableRecordIdentifier[], CollectionResourceRelationship] {
|
|
193
|
+
let jsonApi = (this.recordData as NonSingletonRecordDataManager).getRelationship(
|
|
194
|
+
identifier,
|
|
195
|
+
field,
|
|
196
|
+
true
|
|
197
|
+
) as CollectionResourceRelationship;
|
|
198
|
+
const cache = this.store._instanceCache;
|
|
199
|
+
let identifiers: StableRecordIdentifier[] = [];
|
|
200
|
+
if (jsonApi.data) {
|
|
201
|
+
for (let i = 0; i < jsonApi.data.length; i++) {
|
|
202
|
+
const identifier = jsonApi.data[i];
|
|
203
|
+
assert(`Expected a stable identifier`, isStableIdentifier(identifier));
|
|
204
|
+
if (cache.recordIsLoaded(identifier, true)) {
|
|
205
|
+
identifiers.push(identifier);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
163
208
|
}
|
|
164
209
|
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
}
|
|
210
|
+
return [identifiers, jsonApi];
|
|
211
|
+
}
|
|
179
212
|
|
|
180
|
-
|
|
213
|
+
getManyArray(key: string, definition?: UpgradedMeta): RelatedCollection {
|
|
214
|
+
if (HAS_RECORD_DATA_PACKAGE) {
|
|
215
|
+
let manyArray: RelatedCollection | undefined = this._manyArrayCache[key];
|
|
216
|
+
if (!definition) {
|
|
217
|
+
const graphFor = (
|
|
218
|
+
importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private')
|
|
219
|
+
).graphFor;
|
|
220
|
+
definition = graphFor(this.store).get(this.identifier, key).definition;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!manyArray) {
|
|
224
|
+
const [identifiers, doc] = this._getCurrentState(this.identifier, key);
|
|
225
|
+
|
|
226
|
+
manyArray = new RelatedCollection({
|
|
227
|
+
store: this.store,
|
|
228
|
+
type: definition.type,
|
|
229
|
+
identifier: this.identifier,
|
|
230
|
+
recordData: this.recordData,
|
|
231
|
+
identifiers,
|
|
232
|
+
key,
|
|
233
|
+
meta: doc.meta || null,
|
|
234
|
+
links: doc.links || null,
|
|
235
|
+
isPolymorphic: definition.isPolymorphic,
|
|
236
|
+
isAsync: definition.isAsync,
|
|
237
|
+
_inverseIsAsync: definition.inverseIsAsync,
|
|
238
|
+
manager: this,
|
|
239
|
+
isLoaded: !definition.isAsync,
|
|
240
|
+
allowMutation: true,
|
|
241
|
+
});
|
|
242
|
+
this._manyArrayCache[key] = manyArray;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return manyArray;
|
|
246
|
+
}
|
|
247
|
+
assert('hasMany only works with the @ember-data/record-data package');
|
|
181
248
|
}
|
|
182
249
|
|
|
183
250
|
fetchAsyncHasMany(
|
|
184
251
|
key: string,
|
|
185
252
|
relationship: ManyRelationship,
|
|
186
|
-
manyArray:
|
|
253
|
+
manyArray: RelatedCollection,
|
|
187
254
|
options?: FindOptions
|
|
188
|
-
): Promise<
|
|
255
|
+
): Promise<RelatedCollection> {
|
|
189
256
|
if (HAS_RECORD_DATA_PACKAGE) {
|
|
190
|
-
let loadingPromise = this._relationshipPromisesCache[key] as Promise<
|
|
257
|
+
let loadingPromise = this._relationshipPromisesCache[key] as Promise<RelatedCollection> | undefined;
|
|
191
258
|
if (loadingPromise) {
|
|
192
259
|
return loadingPromise;
|
|
193
260
|
}
|
|
194
261
|
|
|
195
|
-
const jsonApi = this.recordData.
|
|
262
|
+
const jsonApi = this.recordData.getRelationship(this.identifier, key) as CollectionResourceRelationship;
|
|
263
|
+
const promise = this._findHasManyByJsonApiResource(jsonApi, this.identifier, relationship, options);
|
|
264
|
+
|
|
265
|
+
if (!promise) {
|
|
266
|
+
manyArray.isLoaded = true;
|
|
267
|
+
return resolve(manyArray);
|
|
268
|
+
}
|
|
196
269
|
|
|
197
|
-
loadingPromise =
|
|
270
|
+
loadingPromise = promise.then(
|
|
198
271
|
() => handleCompletedRelationshipRequest(this, key, relationship, manyArray),
|
|
199
272
|
(e: Error) => handleCompletedRelationshipRequest(this, key, relationship, manyArray, e)
|
|
200
273
|
);
|
|
@@ -230,7 +303,7 @@ export class LegacySupport {
|
|
|
230
303
|
assert(`hasMany only works with the @ember-data/record-data package`);
|
|
231
304
|
}
|
|
232
305
|
|
|
233
|
-
getHasMany(key: string, options?: FindOptions): PromiseManyArray |
|
|
306
|
+
getHasMany(key: string, options?: FindOptions): PromiseManyArray | RelatedCollection {
|
|
234
307
|
if (HAS_RECORD_DATA_PACKAGE) {
|
|
235
308
|
const graphFor = (
|
|
236
309
|
importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private')
|
|
@@ -251,7 +324,7 @@ export class LegacySupport {
|
|
|
251
324
|
assert(
|
|
252
325
|
`You looked up the '${key}' relationship on a '${this.identifier.type}' with id ${
|
|
253
326
|
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 })')`,
|
|
327
|
+
} 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(<type>, { async: true, inverse: <inverse> })')`,
|
|
255
328
|
!anyUnloaded(this.store, relationship)
|
|
256
329
|
);
|
|
257
330
|
|
|
@@ -261,11 +334,6 @@ export class LegacySupport {
|
|
|
261
334
|
assert(`hasMany only works with the @ember-data/record-data package`);
|
|
262
335
|
}
|
|
263
336
|
|
|
264
|
-
setDirtyHasMany(key: string, records: RecordInstance[]) {
|
|
265
|
-
assertRecordsPassedToHasMany(records);
|
|
266
|
-
return this.recordData.setDirtyHasMany(key, extractRecordDatasFromRecords(records));
|
|
267
|
-
}
|
|
268
|
-
|
|
269
337
|
_updatePromiseProxyFor(kind: 'hasMany', key: string, args: HasManyProxyCreateArgs): PromiseManyArray;
|
|
270
338
|
_updatePromiseProxyFor(kind: 'belongsTo', key: string, args: BelongsToProxyCreateArgs): PromiseBelongsTo;
|
|
271
339
|
_updatePromiseProxyFor(
|
|
@@ -318,7 +386,8 @@ export class LegacySupport {
|
|
|
318
386
|
const graphFor = (
|
|
319
387
|
importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private')
|
|
320
388
|
).graphFor;
|
|
321
|
-
const
|
|
389
|
+
const graph = graphFor(this.store);
|
|
390
|
+
const relationship = graph.get(this.identifier, name);
|
|
322
391
|
|
|
323
392
|
if (DEBUG && kind) {
|
|
324
393
|
let modelName = this.identifier.type;
|
|
@@ -332,9 +401,15 @@ export class LegacySupport {
|
|
|
332
401
|
let relationshipKind = relationship.definition.kind;
|
|
333
402
|
|
|
334
403
|
if (relationshipKind === 'belongsTo') {
|
|
335
|
-
reference = new BelongsToReference(
|
|
404
|
+
reference = new BelongsToReference(
|
|
405
|
+
this.store,
|
|
406
|
+
graph,
|
|
407
|
+
this.identifier,
|
|
408
|
+
relationship as BelongsToRelationship,
|
|
409
|
+
name
|
|
410
|
+
);
|
|
336
411
|
} else if (relationshipKind === 'hasMany') {
|
|
337
|
-
reference = new HasManyReference(this.store, this.identifier, relationship as ManyRelationship, name);
|
|
412
|
+
reference = new HasManyReference(this.store, graph, this.identifier, relationship as ManyRelationship, name);
|
|
338
413
|
}
|
|
339
414
|
|
|
340
415
|
this.references[name] = reference;
|
|
@@ -344,22 +419,20 @@ export class LegacySupport {
|
|
|
344
419
|
}
|
|
345
420
|
|
|
346
421
|
_findHasManyByJsonApiResource(
|
|
347
|
-
resource,
|
|
422
|
+
resource: CollectionResourceRelationship,
|
|
348
423
|
parentIdentifier: StableRecordIdentifier,
|
|
349
424
|
relationship: ManyRelationship,
|
|
350
425
|
options: FindOptions = {}
|
|
351
|
-
): Promise<void | unknown[]> {
|
|
426
|
+
): Promise<void | unknown[]> | void {
|
|
352
427
|
if (HAS_RECORD_DATA_PACKAGE) {
|
|
353
428
|
if (!resource) {
|
|
354
|
-
return
|
|
429
|
+
return;
|
|
355
430
|
}
|
|
356
431
|
const { definition, state } = relationship;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
let { isStale, hasDematerializedInverse, hasReceivedData, isEmpty, shouldForceReload } = state;
|
|
432
|
+
const adapter = this.store.adapterFor(definition.type);
|
|
433
|
+
const { isStale, hasDematerializedInverse, hasReceivedData, isEmpty, shouldForceReload } = state;
|
|
360
434
|
const allInverseRecordsAreLoaded = areAllInverseRecordsLoaded(this.store, resource);
|
|
361
|
-
|
|
362
|
-
let shouldFindViaLink =
|
|
435
|
+
const shouldFindViaLink =
|
|
363
436
|
resource.links &&
|
|
364
437
|
resource.links.related &&
|
|
365
438
|
(typeof adapter.findHasMany === 'function' || typeof resource.data === 'undefined') &&
|
|
@@ -369,9 +442,9 @@ export class LegacySupport {
|
|
|
369
442
|
if (shouldFindViaLink) {
|
|
370
443
|
// findHasMany, although not public, does not need to care about our upgrade relationship definitions
|
|
371
444
|
// and can stick with the public definition API for now.
|
|
372
|
-
const relationshipMeta = this.store
|
|
373
|
-
|
|
374
|
-
|
|
445
|
+
const relationshipMeta = this.store
|
|
446
|
+
.getSchemaDefinitionService()
|
|
447
|
+
.relationshipsDefinitionFor({ type: definition.inverseType })[definition.key];
|
|
375
448
|
let adapter = this.store.adapterFor(parentIdentifier.type);
|
|
376
449
|
|
|
377
450
|
/*
|
|
@@ -394,20 +467,28 @@ export class LegacySupport {
|
|
|
394
467
|
typeof adapter.findHasMany === 'function'
|
|
395
468
|
);
|
|
396
469
|
|
|
397
|
-
return _findHasMany(adapter, this.store, parentIdentifier, resource.links
|
|
470
|
+
return _findHasMany(adapter, this.store, parentIdentifier, resource.links!.related, relationshipMeta, options);
|
|
398
471
|
}
|
|
399
472
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
let hasLocalPartialData =
|
|
473
|
+
const preferLocalCache = hasReceivedData && !isEmpty;
|
|
474
|
+
const hasLocalPartialData =
|
|
403
475
|
hasDematerializedInverse || (isEmpty && Array.isArray(resource.data) && resource.data.length > 0);
|
|
404
476
|
|
|
405
477
|
// fetch using data, pulling from local cache if possible
|
|
406
478
|
if (!shouldForceReload && !isStale && (preferLocalCache || hasLocalPartialData)) {
|
|
479
|
+
if (allInverseRecordsAreLoaded) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
assert(`Expected collection to be an array`, Array.isArray(resource.data));
|
|
483
|
+
if (allInverseRecordsAreLoaded) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
407
486
|
let finds = new Array(resource.data.length);
|
|
487
|
+
let cache = this.store._instanceCache;
|
|
408
488
|
for (let i = 0; i < resource.data.length; i++) {
|
|
409
|
-
|
|
410
|
-
|
|
489
|
+
const identifier = resource.data[i];
|
|
490
|
+
assert(`expected a stable identifier`, isStableIdentifier(identifier));
|
|
491
|
+
finds[i] = cache._fetchDataIfNeededForIdentifier(identifier, options);
|
|
411
492
|
}
|
|
412
493
|
|
|
413
494
|
return all(finds);
|
|
@@ -417,7 +498,9 @@ export class LegacySupport {
|
|
|
417
498
|
|
|
418
499
|
// fetch by data
|
|
419
500
|
if (hasData || hasLocalPartialData) {
|
|
420
|
-
|
|
501
|
+
const identifiers = resource.data;
|
|
502
|
+
assert(`Expected collection to be an array`, Array.isArray(identifiers));
|
|
503
|
+
assert(`Expected stable identifiers`, identifiers.every(isStableIdentifier));
|
|
421
504
|
let fetches = new Array(identifiers.length);
|
|
422
505
|
const manager = this.store._fetchManager;
|
|
423
506
|
|
|
@@ -432,49 +515,51 @@ export class LegacySupport {
|
|
|
432
515
|
|
|
433
516
|
// we were explicitly told we have no data and no links.
|
|
434
517
|
// TODO if the relationshipIsStale, should we hit the adapter anyway?
|
|
435
|
-
return
|
|
518
|
+
return;
|
|
436
519
|
}
|
|
437
520
|
assert(`hasMany only works with the @ember-data/record-data package`);
|
|
438
521
|
}
|
|
439
522
|
|
|
440
523
|
_findBelongsToByJsonApiResource(
|
|
441
|
-
resource,
|
|
524
|
+
resource: SingleResourceRelationship,
|
|
442
525
|
parentIdentifier: StableRecordIdentifier,
|
|
443
|
-
|
|
526
|
+
relationship: BelongsToRelationship,
|
|
444
527
|
options: FindOptions = {}
|
|
445
528
|
): Promise<StableRecordIdentifier | null> {
|
|
446
529
|
if (!resource) {
|
|
447
530
|
return resolve(null);
|
|
448
531
|
}
|
|
449
532
|
|
|
450
|
-
const
|
|
533
|
+
const identifier = resource.data ? resource.data : null;
|
|
534
|
+
assert(`Expected a stable identifier`, !identifier || isStableIdentifier(identifier));
|
|
451
535
|
|
|
452
|
-
let { isStale, hasDematerializedInverse, hasReceivedData, isEmpty, shouldForceReload } =
|
|
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));
|
|
536
|
+
let { isStale, hasDematerializedInverse, hasReceivedData, isEmpty, shouldForceReload } = relationship.state;
|
|
460
537
|
|
|
461
|
-
if
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
return pendingRequest;
|
|
466
|
-
}
|
|
538
|
+
// short circuit if we are already loading
|
|
539
|
+
let pendingRequest = identifier && this.store._fetchManager.getPendingFetch(identifier, options);
|
|
540
|
+
if (pendingRequest) {
|
|
541
|
+
return pendingRequest;
|
|
467
542
|
}
|
|
468
543
|
|
|
544
|
+
const allInverseRecordsAreLoaded = areAllInverseRecordsLoaded(this.store, resource);
|
|
545
|
+
const shouldFindViaLink =
|
|
546
|
+
resource.links?.related &&
|
|
547
|
+
(shouldForceReload || hasDematerializedInverse || isStale || (!allInverseRecordsAreLoaded && !isEmpty));
|
|
548
|
+
|
|
469
549
|
// fetch via link
|
|
470
550
|
if (shouldFindViaLink) {
|
|
471
|
-
|
|
551
|
+
const relationshipMeta = this.store.getSchemaDefinitionService().relationshipsDefinitionFor(this.identifier)[
|
|
552
|
+
relationship.definition.key
|
|
553
|
+
];
|
|
554
|
+
assert(`Attempted to access a belongsTo relationship but no definition exists for it`, relationshipMeta);
|
|
555
|
+
|
|
556
|
+
return _findBelongsTo(this.store, parentIdentifier, resource.links!.related, relationshipMeta, options);
|
|
472
557
|
}
|
|
473
558
|
|
|
474
559
|
let preferLocalCache = hasReceivedData && allInverseRecordsAreLoaded && !isEmpty;
|
|
475
560
|
let hasLocalPartialData = hasDematerializedInverse || (isEmpty && resource.data);
|
|
476
561
|
// null is explicit empty, undefined is "we don't know anything"
|
|
477
|
-
|
|
562
|
+
const localDataIsEmpty = resource.data === undefined || resource.data === null;
|
|
478
563
|
|
|
479
564
|
// fetch using data, pulling from local cache if possible
|
|
480
565
|
if (!shouldForceReload && !isStale && (preferLocalCache || hasLocalPartialData)) {
|
|
@@ -485,22 +570,21 @@ export class LegacySupport {
|
|
|
485
570
|
return resolve(null);
|
|
486
571
|
}
|
|
487
572
|
|
|
488
|
-
if (!
|
|
489
|
-
assert(`No
|
|
573
|
+
if (!identifier) {
|
|
574
|
+
assert(`No Information found for ${resource.data!.lid}`, identifier);
|
|
490
575
|
}
|
|
491
576
|
|
|
492
|
-
return this.store._instanceCache._fetchDataIfNeededForIdentifier(
|
|
577
|
+
return this.store._instanceCache._fetchDataIfNeededForIdentifier(identifier, options);
|
|
493
578
|
}
|
|
494
579
|
|
|
495
|
-
let resourceIsLocal = !localDataIsEmpty && resource.data
|
|
580
|
+
let resourceIsLocal = !localDataIsEmpty && resource.data!.id === null;
|
|
496
581
|
|
|
497
|
-
if (
|
|
498
|
-
return resolve(
|
|
582
|
+
if (identifier && resourceIsLocal) {
|
|
583
|
+
return resolve(identifier);
|
|
499
584
|
}
|
|
500
585
|
|
|
501
586
|
// fetch by data
|
|
502
|
-
if (
|
|
503
|
-
let identifier = internalModel.identifier;
|
|
587
|
+
if (identifier && !localDataIsEmpty) {
|
|
504
588
|
assertIdentifierHasId(identifier);
|
|
505
589
|
|
|
506
590
|
return this.store._fetchManager.scheduleFetch(identifier, options);
|
|
@@ -512,32 +596,28 @@ export class LegacySupport {
|
|
|
512
596
|
}
|
|
513
597
|
|
|
514
598
|
destroy() {
|
|
515
|
-
assert(
|
|
516
|
-
'Cannot destroy an internalModel while its record is materialized',
|
|
517
|
-
!this.record || this.record.isDestroyed || this.record.isDestroying
|
|
518
|
-
);
|
|
519
599
|
this.isDestroying = true;
|
|
520
600
|
|
|
521
|
-
|
|
601
|
+
let cache: Dict<{ destroy(): void }> = this._manyArrayCache;
|
|
602
|
+
this._manyArrayCache = Object.create(null);
|
|
522
603
|
Object.keys(cache).forEach((key) => {
|
|
523
604
|
cache[key]!.destroy();
|
|
524
|
-
delete cache[key];
|
|
525
605
|
});
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
606
|
+
|
|
607
|
+
cache = this._relationshipProxyCache;
|
|
608
|
+
this._relationshipProxyCache = Object.create(null);
|
|
609
|
+
Object.keys(cache).forEach((key) => {
|
|
610
|
+
const proxy = cache[key]!;
|
|
529
611
|
if (proxy.destroy) {
|
|
530
612
|
proxy.destroy();
|
|
531
613
|
}
|
|
532
|
-
delete this._relationshipProxyCache[key];
|
|
533
614
|
});
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
615
|
+
|
|
616
|
+
cache = this.references;
|
|
617
|
+
this.references = Object.create(null);
|
|
618
|
+
Object.keys(cache).forEach((key) => {
|
|
619
|
+
cache[key]!.destroy();
|
|
620
|
+
});
|
|
541
621
|
this.isDestroyed = true;
|
|
542
622
|
}
|
|
543
623
|
}
|
|
@@ -552,8 +632,8 @@ function handleCompletedRelationshipRequest(
|
|
|
552
632
|
recordExt: LegacySupport,
|
|
553
633
|
key: string,
|
|
554
634
|
relationship: ManyRelationship,
|
|
555
|
-
value:
|
|
556
|
-
):
|
|
635
|
+
value: RelatedCollection
|
|
636
|
+
): RelatedCollection;
|
|
557
637
|
function handleCompletedRelationshipRequest(
|
|
558
638
|
recordExt: LegacySupport,
|
|
559
639
|
key: string,
|
|
@@ -565,16 +645,16 @@ function handleCompletedRelationshipRequest(
|
|
|
565
645
|
recordExt: LegacySupport,
|
|
566
646
|
key: string,
|
|
567
647
|
relationship: ManyRelationship,
|
|
568
|
-
value:
|
|
648
|
+
value: RelatedCollection,
|
|
569
649
|
error: Error
|
|
570
650
|
): never;
|
|
571
651
|
function handleCompletedRelationshipRequest(
|
|
572
652
|
recordExt: LegacySupport,
|
|
573
653
|
key: string,
|
|
574
654
|
relationship: BelongsToRelationship | ManyRelationship,
|
|
575
|
-
value:
|
|
655
|
+
value: RelatedCollection | StableRecordIdentifier | null,
|
|
576
656
|
error?: Error
|
|
577
|
-
):
|
|
657
|
+
): RelatedCollection | RecordInstance | null {
|
|
578
658
|
delete recordExt._relationshipPromisesCache[key];
|
|
579
659
|
relationship.state.shouldForceReload = false;
|
|
580
660
|
const isHasMany = relationship.definition.kind === 'hasMany';
|
|
@@ -582,7 +662,7 @@ function handleCompletedRelationshipRequest(
|
|
|
582
662
|
if (isHasMany) {
|
|
583
663
|
// we don't notify the record property here to avoid refetch
|
|
584
664
|
// only the many array
|
|
585
|
-
(value as
|
|
665
|
+
(value as RelatedCollection).notify();
|
|
586
666
|
}
|
|
587
667
|
|
|
588
668
|
if (error) {
|
|
@@ -606,7 +686,7 @@ function handleCompletedRelationshipRequest(
|
|
|
606
686
|
}
|
|
607
687
|
|
|
608
688
|
if (isHasMany) {
|
|
609
|
-
(value as
|
|
689
|
+
(value as RelatedCollection).isLoaded = true;
|
|
610
690
|
}
|
|
611
691
|
|
|
612
692
|
relationship.state.hasFailedLoadAttempt = false;
|
|
@@ -614,43 +694,40 @@ function handleCompletedRelationshipRequest(
|
|
|
614
694
|
relationship.state.isStale = false;
|
|
615
695
|
|
|
616
696
|
return isHasMany || !value
|
|
617
|
-
? (value as
|
|
697
|
+
? (value as RelatedCollection | null)
|
|
618
698
|
: recordExt.store.peekRecord(value as StableRecordIdentifier);
|
|
619
699
|
}
|
|
620
700
|
|
|
621
|
-
|
|
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 };
|
|
701
|
+
type PromiseProxyRecord = { then(): void; content: RecordInstance | null | undefined };
|
|
638
702
|
|
|
639
|
-
function
|
|
703
|
+
function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | RecordInstance | null) {
|
|
640
704
|
if (!recordOrPromiseRecord) {
|
|
641
705
|
return null;
|
|
642
706
|
}
|
|
643
707
|
|
|
644
|
-
if (isPromiseRecord(recordOrPromiseRecord)) {
|
|
645
|
-
let content = recordOrPromiseRecord.
|
|
708
|
+
if (DEPRECATE_PROMISE_PROXIES && isPromiseRecord(recordOrPromiseRecord)) {
|
|
709
|
+
let content = recordOrPromiseRecord.content;
|
|
646
710
|
assert(
|
|
647
711
|
'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
712
|
content !== undefined
|
|
649
713
|
);
|
|
650
|
-
|
|
714
|
+
deprecate(
|
|
715
|
+
`You passed in a PromiseProxy to a Relationship API that now expects a resolved value. await the value before setting it.`,
|
|
716
|
+
false,
|
|
717
|
+
{
|
|
718
|
+
id: 'ember-data:deprecate-promise-proxies',
|
|
719
|
+
until: '5.0',
|
|
720
|
+
since: {
|
|
721
|
+
enabled: '4.8',
|
|
722
|
+
available: '4.8',
|
|
723
|
+
},
|
|
724
|
+
for: 'ember-data',
|
|
725
|
+
}
|
|
726
|
+
);
|
|
727
|
+
return content ? recordIdentifierFor(content) : null;
|
|
651
728
|
}
|
|
652
729
|
|
|
653
|
-
return
|
|
730
|
+
return recordIdentifierFor(recordOrPromiseRecord);
|
|
654
731
|
}
|
|
655
732
|
|
|
656
733
|
function isPromiseRecord(record: PromiseProxyRecord | RecordInstance): record is PromiseProxyRecord {
|
|
@@ -658,52 +735,36 @@ function isPromiseRecord(record: PromiseProxyRecord | RecordInstance): record is
|
|
|
658
735
|
}
|
|
659
736
|
|
|
660
737
|
function anyUnloaded(store: Store, relationship: ManyRelationship) {
|
|
661
|
-
let state = relationship.
|
|
738
|
+
let state = relationship.localState;
|
|
739
|
+
const cache = store._instanceCache;
|
|
662
740
|
const unloaded = state.find((s) => {
|
|
663
|
-
let
|
|
664
|
-
return
|
|
741
|
+
let isLoaded = cache.recordIsLoaded(s, true);
|
|
742
|
+
return !isLoaded;
|
|
665
743
|
});
|
|
666
744
|
|
|
667
745
|
return unloaded || false;
|
|
668
746
|
}
|
|
669
747
|
|
|
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
748
|
function areAllInverseRecordsLoaded(store: Store, resource: JsonApiRelationship): boolean {
|
|
681
|
-
const
|
|
749
|
+
const instanceCache = store._instanceCache;
|
|
750
|
+
const identifiers = resource.data;
|
|
682
751
|
|
|
683
|
-
if (Array.isArray(
|
|
752
|
+
if (Array.isArray(identifiers)) {
|
|
753
|
+
assert(`Expected stable identifiers`, identifiers.every(isStableIdentifier));
|
|
684
754
|
// treat as collection
|
|
685
755
|
// check for unloaded records
|
|
686
|
-
|
|
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
|
-
}
|
|
756
|
+
return identifiers.every((identifier: StableRecordIdentifier) => instanceCache.recordIsLoaded(identifier));
|
|
699
757
|
}
|
|
758
|
+
|
|
759
|
+
// treat as single resource
|
|
760
|
+
if (!identifiers) return true;
|
|
761
|
+
|
|
762
|
+
assert(`Expected stable identifiers`, isStableIdentifier(identifiers));
|
|
763
|
+
return instanceCache.recordIsLoaded(identifiers);
|
|
700
764
|
}
|
|
701
765
|
|
|
702
|
-
function
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
): InternalModel {
|
|
707
|
-
const identifier = cache.getOrCreateRecordIdentifier(resource);
|
|
708
|
-
return store._instanceCache._internalModelForResource(identifier);
|
|
766
|
+
function isBelongsTo(
|
|
767
|
+
relationship: BelongsToRelationship | ImplicitRelationship | ManyRelationship
|
|
768
|
+
): relationship is BelongsToRelationship {
|
|
769
|
+
return relationship.definition.kind === 'belongsTo';
|
|
709
770
|
}
|