ember-data-model-fragments 7.0.3 → 8.0.2
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/.release-plan.json +3 -3
- package/CHANGELOG.md +45 -0
- package/README.md +95 -31
- package/addon/array/fragment.js +19 -0
- package/addon/array/stateful.js +9 -45
- package/addon/attributes/array.js +55 -46
- package/addon/attributes/fragment-array.js +58 -49
- package/addon/attributes/fragment-owner.js +2 -1
- package/addon/attributes/fragment.js +61 -49
- package/addon/cache/fragment-cache.js +323 -24
- package/addon/cache/fragment-record-data-proxy.js +3 -1
- package/addon/cache/fragment-state-manager.js +283 -70
- package/addon/ext.js +52 -216
- package/addon/fragment.js +122 -48
- package/addon/index.js +15 -4
- package/addon/schema-service.js +190 -0
- package/addon/serializer.js +21 -0
- package/addon/serializers/fragment.js +85 -0
- package/addon/serializers/json-api.js +85 -0
- package/addon/serializers/rest.js +83 -0
- package/addon/serializers/utils.js +253 -0
- package/addon/store.js +301 -0
- package/addon/transforms/fragment.js +3 -7
- package/addon/util/fragment-cache.js +59 -0
- package/package.json +55 -49
- package/addon/record-data.js +0 -1131
- package/app/initializers/model-fragments.js +0 -11
- package/record-data.d.ts +0 -4
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
isFragment,
|
|
8
8
|
setFragmentOwner,
|
|
9
9
|
} from '../fragment';
|
|
10
|
+
import fragmentCacheFor from '../util/fragment-cache';
|
|
10
11
|
import metaTypeFor from '../util/meta-type-for';
|
|
11
12
|
import isInstanceOfType from '../util/instance-of-type';
|
|
12
13
|
|
|
@@ -60,57 +61,68 @@ export default function fragment(type, options) {
|
|
|
60
61
|
options,
|
|
61
62
|
};
|
|
62
63
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
cache.setDirtyFragment(identifier, key, null);
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
if (isFragment(value)) {
|
|
64
|
+
// Use computed with a dependency on hasDirtyAttributes which changes on rollback
|
|
65
|
+
// This ensures the computed property is re-evaluated when dirty state changes
|
|
66
|
+
const cp = computed(
|
|
67
|
+
'currentState',
|
|
68
|
+
'hasDirtyAttributes',
|
|
69
|
+
'isDestroyed',
|
|
70
|
+
'isDestroying',
|
|
71
|
+
'store.{_instanceCache,cache}',
|
|
72
|
+
{
|
|
73
|
+
get(key) {
|
|
74
|
+
if (this.isDestroying || this.isDestroyed) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const identifier = recordIdentifierFor(this);
|
|
78
|
+
const cache = fragmentCacheFor(this.store);
|
|
79
|
+
const fragmentIdentifier = cache.getFragment(identifier, key);
|
|
80
|
+
if (fragmentIdentifier === null) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
// Get the fragment record from the identifier
|
|
84
|
+
return this.store._instanceCache.getRecord(fragmentIdentifier);
|
|
85
|
+
},
|
|
86
|
+
set(key, value) {
|
|
90
87
|
assert(
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
'You must pass a fragment or null to set a fragment',
|
|
89
|
+
value === null || isFragment(value) || typeOf(value) === 'object',
|
|
93
90
|
);
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
91
|
+
const identifier = recordIdentifierFor(this);
|
|
92
|
+
const cache = fragmentCacheFor(this.store);
|
|
93
|
+
if (value === null) {
|
|
94
|
+
cache.setDirtyFragment(identifier, key, null);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
if (isFragment(value)) {
|
|
98
|
+
assert(
|
|
99
|
+
`You can only set '${type}' fragments to this property`,
|
|
100
|
+
isInstanceOfType(this.store.modelFor(type), value),
|
|
101
|
+
);
|
|
102
|
+
const fragmentIdentifier = recordIdentifierFor(value);
|
|
103
|
+
setFragmentOwner(value, identifier, key);
|
|
104
|
+
cache.setDirtyFragment(identifier, key, fragmentIdentifier);
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
// Value is a plain object - update existing fragment or create new one
|
|
108
|
+
const fragmentIdentifier = cache.getFragment(identifier, key);
|
|
109
|
+
const actualType = getActualFragmentType(type, options, value, this);
|
|
110
|
+
if (fragmentIdentifier?.type !== actualType) {
|
|
111
|
+
// Create a new fragment
|
|
112
|
+
const fragment = this.store.createFragment(actualType, value);
|
|
113
|
+
const newFragmentIdentifier = recordIdentifierFor(fragment);
|
|
114
|
+
setFragmentOwner(fragment, identifier, key);
|
|
115
|
+
cache.setDirtyFragment(identifier, key, newFragmentIdentifier);
|
|
116
|
+
return fragment;
|
|
117
|
+
}
|
|
118
|
+
// Update existing fragment
|
|
119
|
+
const fragment =
|
|
120
|
+
this.store._instanceCache.getRecord(fragmentIdentifier);
|
|
121
|
+
fragment.setProperties(value);
|
|
108
122
|
return fragment;
|
|
109
|
-
}
|
|
110
|
-
// Update existing fragment
|
|
111
|
-
const fragment = this.store._instanceCache.getRecord(fragmentIdentifier);
|
|
112
|
-
fragment.setProperties(value);
|
|
113
|
-
return fragment;
|
|
123
|
+
},
|
|
114
124
|
},
|
|
115
|
-
|
|
125
|
+
).meta(meta);
|
|
126
|
+
|
|
127
|
+
return cp;
|
|
116
128
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assert } from '@ember/debug';
|
|
1
2
|
import JSONAPICache from '@ember-data/json-api';
|
|
2
3
|
import FragmentStateManager from './fragment-state-manager';
|
|
3
4
|
import FragmentRecordDataProxy from './fragment-record-data-proxy';
|
|
@@ -17,12 +18,40 @@ export default class FragmentCache {
|
|
|
17
18
|
this.__innerCache = new JSONAPICache(storeWrapper);
|
|
18
19
|
this.__fragmentState = new FragmentStateManager(storeWrapper);
|
|
19
20
|
this.__recordDataProxies = new Map();
|
|
21
|
+
this.__storeValidated = false;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
get store() {
|
|
23
25
|
return this.__storeWrapper._store;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Validates that the store service extends FragmentStore
|
|
30
|
+
* This validation is lazy - only checked when fragment functionality is first needed
|
|
31
|
+
*/
|
|
32
|
+
_validateStore() {
|
|
33
|
+
if (this.__storeValidated) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.__storeValidated = true;
|
|
37
|
+
|
|
38
|
+
const store = this.store;
|
|
39
|
+
|
|
40
|
+
// Check if the store has the required fragment methods
|
|
41
|
+
const hasFragmentMethods =
|
|
42
|
+
typeof store.createFragment === 'function' &&
|
|
43
|
+
typeof store.isFragment === 'function';
|
|
44
|
+
|
|
45
|
+
assert(
|
|
46
|
+
`ember-data-model-fragments requires your store service to extend FragmentStore.\n\n` +
|
|
47
|
+
`Create app/services/store.js with the following:\n\n` +
|
|
48
|
+
`import FragmentStore from 'ember-data-model-fragments/store';\n` +
|
|
49
|
+
`export default class extends FragmentStore {}\n\n` +
|
|
50
|
+
`See the ember-data-model-fragments documentation for more information.`,
|
|
51
|
+
hasFragmentMethods,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
26
55
|
/**
|
|
27
56
|
* Get or create a FragmentRecordDataProxy for the given identifier.
|
|
28
57
|
* This provides backwards compatibility with code expecting per-resource RecordData API.
|
|
@@ -41,6 +70,7 @@ export default class FragmentCache {
|
|
|
41
70
|
// ==================
|
|
42
71
|
|
|
43
72
|
getFragment(identifier, key) {
|
|
73
|
+
this._validateStore();
|
|
44
74
|
return this.__fragmentState.getFragment(identifier, key);
|
|
45
75
|
}
|
|
46
76
|
|
|
@@ -109,10 +139,136 @@ export default class FragmentCache {
|
|
|
109
139
|
// ==================
|
|
110
140
|
|
|
111
141
|
/**
|
|
112
|
-
* Cache the response to a request
|
|
142
|
+
* Cache the response to a request.
|
|
143
|
+
*
|
|
144
|
+
* In ember-data 4.13+, this is the primary entry point for caching data.
|
|
145
|
+
* We intercept to extract fragment attributes before passing to the inner cache.
|
|
146
|
+
*
|
|
147
|
+
* The document structure is:
|
|
148
|
+
* {
|
|
149
|
+
* request: {...},
|
|
150
|
+
* response: {...},
|
|
151
|
+
* content: {
|
|
152
|
+
* data: { type, id, attributes, relationships } | [...],
|
|
153
|
+
* included: [...],
|
|
154
|
+
* meta: {...}
|
|
155
|
+
* }
|
|
156
|
+
* }
|
|
113
157
|
*/
|
|
114
158
|
put(doc) {
|
|
115
|
-
|
|
159
|
+
// Normalize id to string to ensure consistent comparison
|
|
160
|
+
if (doc?.content?.data) {
|
|
161
|
+
if (Array.isArray(doc.content.data)) {
|
|
162
|
+
doc.content.data.forEach((resource) => {
|
|
163
|
+
if (resource.id != null && typeof resource.id !== 'string') {
|
|
164
|
+
resource.id = String(resource.id);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
} else if (
|
|
168
|
+
doc.content.data.id != null &&
|
|
169
|
+
typeof doc.content.data.id !== 'string'
|
|
170
|
+
) {
|
|
171
|
+
doc.content.data.id = String(doc.content.data.id);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// NEW APPROACH: Store fragment data separately, push to inner cache first, then process fragments
|
|
176
|
+
// This is needed because polymorphic fragment type resolution may require accessing owner attributes,
|
|
177
|
+
// which requires the owner record to exist in the cache first.
|
|
178
|
+
|
|
179
|
+
// Step 1: Extract fragment data from resources WITHOUT creating fragment identifiers
|
|
180
|
+
const fragmentDataByIdentifier = new Map();
|
|
181
|
+
if (doc && doc.content && doc.content.data) {
|
|
182
|
+
this._collectFragmentsFromDocument(doc.content, fragmentDataByIdentifier);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Step 2: Push to inner cache (this creates the owner records)
|
|
186
|
+
const result = this.__innerCache.put(doc);
|
|
187
|
+
|
|
188
|
+
// Step 3: Now that owner records exist, push fragment data to create fragment identifiers
|
|
189
|
+
for (const [identifier, data] of fragmentDataByIdentifier) {
|
|
190
|
+
this.__fragmentState.pushFragmentData(identifier, data, false);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Collect fragment attributes from a JSON:API document WITHOUT creating fragment identifiers.
|
|
198
|
+
* This just stores the raw fragment data and removes fragment attributes from resources.
|
|
199
|
+
* Fragment identifiers will be created later after owner records are in the cache.
|
|
200
|
+
*
|
|
201
|
+
* @private
|
|
202
|
+
*/
|
|
203
|
+
_collectFragmentsFromDocument(jsonApiDoc, fragmentDataByIdentifier) {
|
|
204
|
+
const { data, included } = jsonApiDoc;
|
|
205
|
+
|
|
206
|
+
// Handle single resource
|
|
207
|
+
if (data && !Array.isArray(data)) {
|
|
208
|
+
this._collectFragmentsFromResource(data, fragmentDataByIdentifier);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Handle array of resources
|
|
212
|
+
if (Array.isArray(data)) {
|
|
213
|
+
for (const resource of data) {
|
|
214
|
+
this._collectFragmentsFromResource(resource, fragmentDataByIdentifier);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Handle included resources
|
|
219
|
+
if (included) {
|
|
220
|
+
for (const resource of included) {
|
|
221
|
+
this._collectFragmentsFromResource(resource, fragmentDataByIdentifier);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Collect fragment attributes from a single resource WITHOUT creating fragment identifiers.
|
|
228
|
+
*
|
|
229
|
+
* @private
|
|
230
|
+
*/
|
|
231
|
+
_collectFragmentsFromResource(resource, fragmentDataByIdentifier) {
|
|
232
|
+
if (!resource || !resource.attributes || !resource.type) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Get or create identifier for this resource
|
|
237
|
+
const identifier =
|
|
238
|
+
this.__storeWrapper.identifierCache.getOrCreateRecordIdentifier({
|
|
239
|
+
type: resource.type,
|
|
240
|
+
id: resource.id,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const definitions = this.__storeWrapper
|
|
244
|
+
.getSchemaDefinitionService()
|
|
245
|
+
.attributesDefinitionFor(identifier);
|
|
246
|
+
|
|
247
|
+
const fragmentData = {};
|
|
248
|
+
const fragmentKeys = [];
|
|
249
|
+
|
|
250
|
+
for (const [key, definition] of Object.entries(definitions)) {
|
|
251
|
+
const isFragment =
|
|
252
|
+
definition.isFragment || definition.options?.isFragment;
|
|
253
|
+
|
|
254
|
+
if (isFragment && resource.attributes[key] !== undefined) {
|
|
255
|
+
fragmentData[key] = resource.attributes[key];
|
|
256
|
+
fragmentKeys.push(key);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Store fragment data for later processing (AFTER inner cache put)
|
|
261
|
+
if (fragmentKeys.length > 0) {
|
|
262
|
+
fragmentDataByIdentifier.set(identifier, { attributes: fragmentData });
|
|
263
|
+
|
|
264
|
+
// Clone attributes and remove fragment keys to avoid mutating original data
|
|
265
|
+
// This is necessary because resource.attributes may reference user-provided data
|
|
266
|
+
const cleanedAttributes = { ...resource.attributes };
|
|
267
|
+
for (const key of fragmentKeys) {
|
|
268
|
+
delete cleanedAttributes[key];
|
|
269
|
+
}
|
|
270
|
+
resource.attributes = cleanedAttributes;
|
|
271
|
+
}
|
|
116
272
|
}
|
|
117
273
|
|
|
118
274
|
/**
|
|
@@ -136,6 +292,10 @@ export default class FragmentCache {
|
|
|
136
292
|
return this.__innerCache.peek(identifier);
|
|
137
293
|
}
|
|
138
294
|
|
|
295
|
+
peekRemoteState(identifier) {
|
|
296
|
+
return this.__innerCache.peekRemoteState?.(identifier);
|
|
297
|
+
}
|
|
298
|
+
|
|
139
299
|
/**
|
|
140
300
|
* Peek the Cache for existing request data
|
|
141
301
|
*/
|
|
@@ -146,8 +306,13 @@ export default class FragmentCache {
|
|
|
146
306
|
/**
|
|
147
307
|
* Push resource data from a remote source into the cache.
|
|
148
308
|
* Intercepts to handle fragment attributes.
|
|
309
|
+
*
|
|
310
|
+
* In ember-data 4.13+, the signature is:
|
|
311
|
+
* upsert(identifier, resource, hasRecord) where resource is the JSON:API resource object
|
|
312
|
+
* In ember-data 4.12, the signature was:
|
|
313
|
+
* upsert(identifier, data, calculateChanges) where data = { attributes: {...} }
|
|
149
314
|
*/
|
|
150
|
-
upsert(identifier, data,
|
|
315
|
+
upsert(identifier, data, hasRecordOrCalculateChanges) {
|
|
151
316
|
// Normalize the id to string to match identifier.id (which is always coerced to string)
|
|
152
317
|
// This ensures cached.id matches identifier.id type when didCommit compares them
|
|
153
318
|
if (data.id != null && typeof data.id !== 'string') {
|
|
@@ -164,7 +329,9 @@ export default class FragmentCache {
|
|
|
164
329
|
.attributesDefinitionFor(identifier);
|
|
165
330
|
|
|
166
331
|
for (const [key, definition] of Object.entries(definitions)) {
|
|
167
|
-
|
|
332
|
+
const isFragment =
|
|
333
|
+
definition.isFragment || definition.options?.isFragment;
|
|
334
|
+
if (isFragment && data.attributes[key] !== undefined) {
|
|
168
335
|
fragmentData[key] = data.attributes[key];
|
|
169
336
|
fragmentAttributeKeys.push(key);
|
|
170
337
|
}
|
|
@@ -187,7 +354,7 @@ export default class FragmentCache {
|
|
|
187
354
|
const changedKeys = this.__innerCache.upsert(
|
|
188
355
|
identifier,
|
|
189
356
|
data,
|
|
190
|
-
|
|
357
|
+
hasRecordOrCalculateChanges,
|
|
191
358
|
);
|
|
192
359
|
|
|
193
360
|
// Handle fragment attributes
|
|
@@ -195,9 +362,9 @@ export default class FragmentCache {
|
|
|
195
362
|
const changedFragmentKeys = this.__fragmentState.pushFragmentData(
|
|
196
363
|
identifier,
|
|
197
364
|
{ attributes: fragmentData },
|
|
198
|
-
|
|
365
|
+
hasRecordOrCalculateChanges,
|
|
199
366
|
);
|
|
200
|
-
if (
|
|
367
|
+
if (hasRecordOrCalculateChanges && changedFragmentKeys?.length) {
|
|
201
368
|
return [...(changedKeys || []), ...changedFragmentKeys];
|
|
202
369
|
}
|
|
203
370
|
}
|
|
@@ -209,6 +376,91 @@ export default class FragmentCache {
|
|
|
209
376
|
* Signal to the cache that a new record has been instantiated on the client
|
|
210
377
|
*/
|
|
211
378
|
clientDidCreate(identifier, options) {
|
|
379
|
+
// Extract fragment attributes from options before passing to inner cache
|
|
380
|
+
if (options) {
|
|
381
|
+
const definitions = this.__storeWrapper
|
|
382
|
+
.getSchemaDefinitionService()
|
|
383
|
+
.attributesDefinitionFor(identifier);
|
|
384
|
+
|
|
385
|
+
// Raw fragment data (plain objects / arrays of plain objects) flows
|
|
386
|
+
// through pushFragmentData -> behavior.pushData which only accepts raw
|
|
387
|
+
// canonical data. Fragment-instance values must be adopted directly
|
|
388
|
+
// (mirroring behavior.getDefaultValue's handling of fragment defaults)
|
|
389
|
+
// so that consumers can pass `store.createFragment(...)` results into
|
|
390
|
+
// `store.createRecord(type, { fragmentKey: fragment })` without hitting
|
|
391
|
+
// "Fragment canonical value must be an object or null".
|
|
392
|
+
const fragmentData = {};
|
|
393
|
+
const fragmentInstanceData = {};
|
|
394
|
+
const regularOptions = {};
|
|
395
|
+
let hasFragmentData = false;
|
|
396
|
+
let hasFragmentInstanceData = false;
|
|
397
|
+
|
|
398
|
+
for (const [key, value] of Object.entries(options)) {
|
|
399
|
+
const definition = definitions[key];
|
|
400
|
+
const isFragmentAttr =
|
|
401
|
+
definition?.isFragment || definition?.options?.isFragment;
|
|
402
|
+
|
|
403
|
+
if (isFragmentAttr && value !== undefined) {
|
|
404
|
+
if (this.__fragmentState.valueContainsFragmentInstance(value)) {
|
|
405
|
+
fragmentInstanceData[key] = value;
|
|
406
|
+
hasFragmentInstanceData = true;
|
|
407
|
+
} else {
|
|
408
|
+
fragmentData[key] = value;
|
|
409
|
+
hasFragmentData = true;
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
regularOptions[key] = value;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// IMPORTANT: Create the inner cache entry first, so the owner's attributes
|
|
417
|
+
// are available when fragment typeKey functions try to access them
|
|
418
|
+
const result = this.__innerCache.clientDidCreate(
|
|
419
|
+
identifier,
|
|
420
|
+
regularOptions,
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
// Adopt fragment-instance values directly into the canonical fragment
|
|
424
|
+
// data map. This sets up ownership on each adopted fragment and avoids
|
|
425
|
+
// the raw-object-only pushData assertion path.
|
|
426
|
+
if (hasFragmentInstanceData) {
|
|
427
|
+
for (const [key, value] of Object.entries(fragmentInstanceData)) {
|
|
428
|
+
const adopted = this.__fragmentState.adoptFragmentForKey(
|
|
429
|
+
identifier,
|
|
430
|
+
key,
|
|
431
|
+
value,
|
|
432
|
+
);
|
|
433
|
+
if (adopted !== undefined) {
|
|
434
|
+
this.__fragmentState.setCanonicalFragmentValue(
|
|
435
|
+
identifier,
|
|
436
|
+
key,
|
|
437
|
+
adopted,
|
|
438
|
+
);
|
|
439
|
+
} else {
|
|
440
|
+
// Defensive fallback: shouldn't happen because
|
|
441
|
+
// _valueContainsFragmentInstance only flagged values that
|
|
442
|
+
// adoptFragmentForKey can handle, but if it ever does, route
|
|
443
|
+
// through the raw-object path as a last resort.
|
|
444
|
+
fragmentData[key] = value;
|
|
445
|
+
hasFragmentData = true;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Push remaining raw fragment data (plain objects) through the normal
|
|
451
|
+
// canonical-data path.
|
|
452
|
+
if (hasFragmentData) {
|
|
453
|
+
this.__fragmentState.pushFragmentData(
|
|
454
|
+
identifier,
|
|
455
|
+
{ attributes: fragmentData },
|
|
456
|
+
false,
|
|
457
|
+
false,
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
|
|
212
464
|
return this.__innerCache.clientDidCreate(identifier, options);
|
|
213
465
|
}
|
|
214
466
|
|
|
@@ -263,7 +515,9 @@ export default class FragmentCache {
|
|
|
263
515
|
fragmentData = { attributes: {} };
|
|
264
516
|
|
|
265
517
|
for (const [key, definition] of Object.entries(definitions)) {
|
|
266
|
-
|
|
518
|
+
const isFragment =
|
|
519
|
+
definition.isFragment || definition.options?.isFragment;
|
|
520
|
+
if (isFragment && attributes[key] !== undefined) {
|
|
267
521
|
fragmentData.attributes[key] = attributes[key];
|
|
268
522
|
}
|
|
269
523
|
}
|
|
@@ -315,7 +569,13 @@ export default class FragmentCache {
|
|
|
315
569
|
.attributesDefinitionFor(identifier);
|
|
316
570
|
const definition = definitions[attr];
|
|
317
571
|
|
|
318
|
-
|
|
572
|
+
// Check for fragment attribute - support both metadata formats:
|
|
573
|
+
// - Direct: definition.isFragment (ember-data 4.12 or original metadata)
|
|
574
|
+
// - Transformed: definition.options?.isFragment (from FragmentSchemaService in 4.13)
|
|
575
|
+
const isFragmentAttr =
|
|
576
|
+
definition?.isFragment || definition?.options?.isFragment;
|
|
577
|
+
|
|
578
|
+
if (isFragmentAttr) {
|
|
319
579
|
// Fragment attributes are handled by fragment state manager
|
|
320
580
|
// getFragment returns identifier(s), we need to convert to Fragment instance(s)
|
|
321
581
|
const fragmentValue = this.__fragmentState.getFragment(identifier, attr);
|
|
@@ -324,29 +584,45 @@ export default class FragmentCache {
|
|
|
324
584
|
return fragmentValue;
|
|
325
585
|
}
|
|
326
586
|
|
|
587
|
+
// Get the fragment kind from the appropriate location:
|
|
588
|
+
// - Direct: definition.kind (original metadata)
|
|
589
|
+
// - Transformed: definition.options?.fragmentKind (from FragmentSchemaService)
|
|
590
|
+
const fragmentKind = definition.options?.fragmentKind || definition.kind;
|
|
591
|
+
|
|
327
592
|
// For single fragments, convert identifier to Fragment instance
|
|
328
|
-
if (
|
|
593
|
+
if (fragmentKind === 'fragment') {
|
|
329
594
|
if (fragmentValue.lid) {
|
|
330
595
|
// It's an identifier, get the record instance
|
|
331
|
-
|
|
596
|
+
const record = this.store._instanceCache.getRecord(fragmentValue);
|
|
597
|
+
return record;
|
|
332
598
|
}
|
|
333
599
|
return fragmentValue;
|
|
334
600
|
}
|
|
335
601
|
|
|
336
|
-
// For fragment arrays
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
602
|
+
// For fragment arrays and primitive arrays, return the wrapper object
|
|
603
|
+
// This is needed for Snapshot._attributes which expects objects with _createSnapshot
|
|
604
|
+
if (fragmentKind === 'fragment-array' || fragmentKind === 'array') {
|
|
605
|
+
// Get the cached wrapper if it exists
|
|
606
|
+
let arrayWrapper = this.getFragmentArrayCache(identifier, attr);
|
|
607
|
+
if (arrayWrapper) {
|
|
608
|
+
return arrayWrapper;
|
|
609
|
+
}
|
|
610
|
+
// If no wrapper exists yet, convert identifiers to Fragment instances
|
|
611
|
+
// so ext.js patch can call _createSnapshot on each fragment
|
|
612
|
+
if (fragmentKind === 'fragment-array' && Array.isArray(fragmentValue)) {
|
|
613
|
+
const fragments = fragmentValue.map((item) => {
|
|
614
|
+
if (item?.lid) {
|
|
615
|
+
return this.store._instanceCache.getRecord(item);
|
|
616
|
+
}
|
|
617
|
+
return item;
|
|
618
|
+
});
|
|
619
|
+
return fragments;
|
|
620
|
+
}
|
|
621
|
+
// For primitive arrays, return as-is
|
|
622
|
+
return fragmentValue;
|
|
347
623
|
}
|
|
348
624
|
|
|
349
|
-
// For
|
|
625
|
+
// For single fragment type that fell through, return as-is
|
|
350
626
|
return fragmentValue;
|
|
351
627
|
}
|
|
352
628
|
|
|
@@ -362,7 +638,10 @@ export default class FragmentCache {
|
|
|
362
638
|
.attributesDefinitionFor(identifier);
|
|
363
639
|
const definition = definitions[attr];
|
|
364
640
|
|
|
365
|
-
|
|
641
|
+
const isFragmentAttr =
|
|
642
|
+
definition?.isFragment || definition?.options?.isFragment;
|
|
643
|
+
|
|
644
|
+
if (isFragmentAttr) {
|
|
366
645
|
return this.__fragmentState.setDirtyFragment(identifier, attr, value);
|
|
367
646
|
}
|
|
368
647
|
|
|
@@ -431,6 +710,22 @@ export default class FragmentCache {
|
|
|
431
710
|
return this.__innerCache.getRelationship(identifier, field);
|
|
432
711
|
}
|
|
433
712
|
|
|
713
|
+
getRemoteRelationship(identifier, field) {
|
|
714
|
+
return this.__innerCache.getRemoteRelationship?.(identifier, field);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
changedRelationships(identifier) {
|
|
718
|
+
return this.__innerCache.changedRelationships?.(identifier);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
hasChangedRelationships(identifier) {
|
|
722
|
+
return this.__innerCache.hasChangedRelationships?.(identifier) || false;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
rollbackRelationships(identifier) {
|
|
726
|
+
return this.__innerCache.rollbackRelationships?.(identifier);
|
|
727
|
+
}
|
|
728
|
+
|
|
434
729
|
/**
|
|
435
730
|
* Update the cache state for the given resource to be marked as locally deleted
|
|
436
731
|
*/
|
|
@@ -445,6 +740,10 @@ export default class FragmentCache {
|
|
|
445
740
|
return this.__innerCache.getErrors(identifier);
|
|
446
741
|
}
|
|
447
742
|
|
|
743
|
+
getRemoteAttr(identifier, attr) {
|
|
744
|
+
return this.__innerCache.getRemoteAttr?.(identifier, attr);
|
|
745
|
+
}
|
|
746
|
+
|
|
448
747
|
/**
|
|
449
748
|
* Query the cache for whether a given resource has any available data
|
|
450
749
|
*/
|
|
@@ -125,7 +125,9 @@ export default class FragmentRecordDataProxy {
|
|
|
125
125
|
.getSchemaDefinitionService()
|
|
126
126
|
.attributesDefinitionFor(this.identifier);
|
|
127
127
|
for (const [key, definition] of Object.entries(definitions)) {
|
|
128
|
-
|
|
128
|
+
const isFragmentAttr =
|
|
129
|
+
definition.isFragment || definition.options?.isFragment;
|
|
130
|
+
if (!isFragmentAttr) {
|
|
129
131
|
regularState[key] = this.__cache.getAttr(this.identifier, key);
|
|
130
132
|
}
|
|
131
133
|
}
|