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
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { assert } from '@ember/debug';
|
|
2
|
+
import { getOwner } from '@ember/application';
|
|
3
|
+
import JSONAPISerializer from '@ember-data/serializer/json-api';
|
|
4
|
+
import RESTSerializer from '@ember-data/serializer/rest';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Helper function to implement fragment transform lookup.
|
|
8
|
+
* Used by FragmentSerializer, FragmentRESTSerializer, and FragmentJSONAPISerializer.
|
|
9
|
+
*
|
|
10
|
+
* @param {Serializer} serializer - The serializer instance
|
|
11
|
+
* @param {String} attributeType - The attribute type to get the transform for
|
|
12
|
+
* @param {Function} superTransformFor - The parent class's transformFor method
|
|
13
|
+
* @return {Transform}
|
|
14
|
+
* @private
|
|
15
|
+
*/
|
|
16
|
+
export function fragmentTransformFor(
|
|
17
|
+
serializer,
|
|
18
|
+
attributeType,
|
|
19
|
+
superTransformFor,
|
|
20
|
+
) {
|
|
21
|
+
if (attributeType.indexOf('-mf-') !== 0) {
|
|
22
|
+
return superTransformFor.call(serializer, attributeType);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const owner = getOwner(serializer);
|
|
26
|
+
const containerKey = `transform:${attributeType}`;
|
|
27
|
+
|
|
28
|
+
if (!owner.hasRegistration(containerKey)) {
|
|
29
|
+
const match = attributeType.match(
|
|
30
|
+
/^-mf-(fragment|fragment-array|array)(?:\$([^$]+))?(?:\$(.+))?$/,
|
|
31
|
+
);
|
|
32
|
+
assert(
|
|
33
|
+
`Failed parsing ember-data-model-fragments attribute type ${attributeType}`,
|
|
34
|
+
match != null,
|
|
35
|
+
);
|
|
36
|
+
const transformName = match[1];
|
|
37
|
+
const type = match[2];
|
|
38
|
+
const polymorphicTypeProp = match[3];
|
|
39
|
+
let transformClass = owner.factoryFor(`transform:${transformName}`);
|
|
40
|
+
transformClass = transformClass && transformClass.class;
|
|
41
|
+
transformClass = transformClass.extend({
|
|
42
|
+
type,
|
|
43
|
+
polymorphicTypeProp,
|
|
44
|
+
store: serializer.store,
|
|
45
|
+
});
|
|
46
|
+
owner.register(containerKey, transformClass);
|
|
47
|
+
}
|
|
48
|
+
return owner.lookup(containerKey);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Helper function to implement fragment-aware applyTransforms.
|
|
53
|
+
* Used by FragmentSerializer, FragmentRESTSerializer, and FragmentJSONAPISerializer.
|
|
54
|
+
*
|
|
55
|
+
* @param {Serializer} serializer - The serializer instance
|
|
56
|
+
* @param {Class} typeClass - The model class
|
|
57
|
+
* @param {Object} data - The data to apply transforms to
|
|
58
|
+
* @return {Object} The transformed data
|
|
59
|
+
* @private
|
|
60
|
+
*/
|
|
61
|
+
export function fragmentApplyTransforms(serializer, typeClass, data) {
|
|
62
|
+
const attributes = typeClass.attributes;
|
|
63
|
+
|
|
64
|
+
// Handle regular @attr transforms
|
|
65
|
+
typeClass.eachTransformedAttribute((key, attrType) => {
|
|
66
|
+
if (data[key] === undefined) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const transform = serializer.transformFor(attrType);
|
|
71
|
+
const transformMeta = attributes.get(key);
|
|
72
|
+
data[key] = transform.deserialize(data[key], transformMeta.options, data);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Also handle @array computed properties with transforms
|
|
76
|
+
// These are not in eachTransformedAttribute because they're computed properties,
|
|
77
|
+
// not regular @attr attributes
|
|
78
|
+
typeClass.eachComputedProperty((key, meta) => {
|
|
79
|
+
if (data[key] === undefined) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Only handle @array attributes that have a transform type (arrayTransform)
|
|
84
|
+
// meta.arrayTransform is the child transform type (e.g., 'string')
|
|
85
|
+
// meta.type is the full transform type string (e.g., '-mf-array$string')
|
|
86
|
+
if (meta?.isFragment && meta.kind === 'array' && meta.arrayTransform) {
|
|
87
|
+
// Use the full transform type string from meta.type
|
|
88
|
+
const transform = serializer.transformFor(meta.type);
|
|
89
|
+
data[key] = transform.deserialize(data[key], meta.options, data);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return data;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isFragmentAttribute(meta) {
|
|
97
|
+
return (
|
|
98
|
+
meta &&
|
|
99
|
+
meta.isFragment &&
|
|
100
|
+
(meta.kind === 'fragment' ||
|
|
101
|
+
meta.kind === 'fragment-array' ||
|
|
102
|
+
meta.kind === 'array')
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function serializedAttributesHash(serializer, snapshot, payload) {
|
|
107
|
+
// JSON:API: fragment attributes belong inside payload.data.attributes.
|
|
108
|
+
// If the model has no regular @attr fields, JSONAPISerializer#serialize
|
|
109
|
+
// omits `attributes` entirely, so we create it here. We dispatch by
|
|
110
|
+
// serializer type rather than payload shape, because a model could
|
|
111
|
+
// legitimately declare an @attr named `data` under RESTSerializer /
|
|
112
|
+
// JSONSerializer and we must not clobber it.
|
|
113
|
+
if (serializer instanceof JSONAPISerializer) {
|
|
114
|
+
if (payload?.data && typeof payload.data === 'object') {
|
|
115
|
+
payload.data.attributes ??= {};
|
|
116
|
+
return payload.data.attributes;
|
|
117
|
+
}
|
|
118
|
+
return payload;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// RESTSerializer: payload is keyed by the model's payload key, e.g.
|
|
122
|
+
// `{ person: { ...attrs } }`.
|
|
123
|
+
if (serializer instanceof RESTSerializer) {
|
|
124
|
+
const rootKey = serializer.payloadKeyFromModelName?.(snapshot.modelName);
|
|
125
|
+
if (rootKey && payload?.[rootKey] && typeof payload[rootKey] === 'object') {
|
|
126
|
+
return payload[rootKey];
|
|
127
|
+
}
|
|
128
|
+
return payload;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// JSONSerializer (default): flat hash keyed by attribute names.
|
|
132
|
+
return payload;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Helper function to serialize computed fragment attributes that newer
|
|
137
|
+
* ember-data versions no longer include via eachAttribute.
|
|
138
|
+
*
|
|
139
|
+
* @param {Serializer} serializer
|
|
140
|
+
* @param {Snapshot} snapshot
|
|
141
|
+
* @param {Object} payload
|
|
142
|
+
* @return {Object}
|
|
143
|
+
* @private
|
|
144
|
+
*/
|
|
145
|
+
export function fragmentSerialize(serializer, snapshot, payload) {
|
|
146
|
+
const attributes = serializedAttributesHash(serializer, snapshot, payload);
|
|
147
|
+
const modelClass = serializer.store.modelFor(snapshot.modelName);
|
|
148
|
+
|
|
149
|
+
modelClass.eachComputedProperty((key, meta) => {
|
|
150
|
+
if (!isFragmentAttribute(meta)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const value = snapshot.attr(key);
|
|
155
|
+
|
|
156
|
+
if (value === undefined) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const attributeKey = serializer.keyForAttribute(key, 'serialize');
|
|
161
|
+
const transform = serializer.transformFor(meta.type);
|
|
162
|
+
|
|
163
|
+
attributes[attributeKey] = transform.serialize(
|
|
164
|
+
value,
|
|
165
|
+
meta.options,
|
|
166
|
+
snapshot,
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return payload;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Helper function to extract attributes including fragment attributes.
|
|
175
|
+
* The default extractAttributes only iterates modelClass.eachAttribute which
|
|
176
|
+
* doesn't include fragment attributes (they're computed properties).
|
|
177
|
+
*
|
|
178
|
+
* Used by FragmentSerializer and FragmentRESTSerializer.
|
|
179
|
+
*
|
|
180
|
+
* @param {Serializer} serializer - The serializer instance
|
|
181
|
+
* @param {Class} modelClass - The model class
|
|
182
|
+
* @param {Object} resourceHash - The raw resource data from the server
|
|
183
|
+
* @param {Function} superExtractAttributes - The parent's extractAttributes method
|
|
184
|
+
* @return {Object} The extracted attributes
|
|
185
|
+
* @private
|
|
186
|
+
*/
|
|
187
|
+
export function fragmentExtractAttributes(
|
|
188
|
+
serializer,
|
|
189
|
+
modelClass,
|
|
190
|
+
resourceHash,
|
|
191
|
+
superExtractAttributes,
|
|
192
|
+
) {
|
|
193
|
+
// First, call parent to get regular attributes
|
|
194
|
+
const attributes = superExtractAttributes.call(
|
|
195
|
+
serializer,
|
|
196
|
+
modelClass,
|
|
197
|
+
resourceHash,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// Then, add fragment attributes
|
|
201
|
+
modelClass.eachComputedProperty((key, meta) => {
|
|
202
|
+
if (isFragmentAttribute(meta)) {
|
|
203
|
+
const attributeKey = serializer.keyForAttribute(key, 'deserialize');
|
|
204
|
+
if (resourceHash[attributeKey] !== undefined) {
|
|
205
|
+
attributes[key] = resourceHash[attributeKey];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return attributes;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Helper function to extract attributes including fragment attributes for JSON:API.
|
|
215
|
+
* JSON:API serializers have attributes nested under resourceHash.attributes.
|
|
216
|
+
*
|
|
217
|
+
* Used by FragmentJSONAPISerializer.
|
|
218
|
+
*
|
|
219
|
+
* @param {Serializer} serializer - The serializer instance
|
|
220
|
+
* @param {Class} modelClass - The model class
|
|
221
|
+
* @param {Object} resourceHash - The raw resource data from the server
|
|
222
|
+
* @param {Function} superExtractAttributes - The parent's extractAttributes method
|
|
223
|
+
* @return {Object} The extracted attributes
|
|
224
|
+
* @private
|
|
225
|
+
*/
|
|
226
|
+
export function fragmentExtractAttributesJSONAPI(
|
|
227
|
+
serializer,
|
|
228
|
+
modelClass,
|
|
229
|
+
resourceHash,
|
|
230
|
+
superExtractAttributes,
|
|
231
|
+
) {
|
|
232
|
+
// First, call parent to get regular attributes
|
|
233
|
+
const attributes = superExtractAttributes.call(
|
|
234
|
+
serializer,
|
|
235
|
+
modelClass,
|
|
236
|
+
resourceHash,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// For JSON:API serializers, attributes are nested under resourceHash.attributes
|
|
240
|
+
const attrHash = resourceHash.attributes || resourceHash;
|
|
241
|
+
|
|
242
|
+
// Then, add fragment attributes
|
|
243
|
+
modelClass.eachComputedProperty((key, meta) => {
|
|
244
|
+
if (isFragmentAttribute(meta)) {
|
|
245
|
+
const attributeKey = serializer.keyForAttribute(key, 'deserialize');
|
|
246
|
+
if (attrHash[attributeKey] !== undefined) {
|
|
247
|
+
attributes[key] = attrHash[attributeKey];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return attributes;
|
|
253
|
+
}
|
package/addon/store.js
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { assert } from '@ember/debug';
|
|
2
|
+
import { getOwner } from '@ember/application';
|
|
3
|
+
import Store from 'ember-data/store';
|
|
4
|
+
import {
|
|
5
|
+
macroCondition,
|
|
6
|
+
dependencySatisfies,
|
|
7
|
+
importSync,
|
|
8
|
+
} from '@embroider/macros';
|
|
9
|
+
import FragmentCache from './cache/fragment-cache';
|
|
10
|
+
import { default as Fragment } from './fragment';
|
|
11
|
+
import { installCacheManagerCompat } from './util/fragment-cache';
|
|
12
|
+
|
|
13
|
+
// Import side-effects to ensure monkey-patches are applied
|
|
14
|
+
// These must be imported before any store instances are created
|
|
15
|
+
import './ext'; // Applies Snapshot monkey-patch for fragment serialization
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
FragmentStore is the base store class for ember-data-model-fragments.
|
|
19
|
+
|
|
20
|
+
To use this addon, you must create an application store service that extends FragmentStore:
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
// app/services/store.js
|
|
24
|
+
import FragmentStore from 'ember-data-model-fragments/store';
|
|
25
|
+
|
|
26
|
+
export default class extends FragmentStore {}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Your application serializer should also extend one of the fragment-aware serializers:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
// app/serializers/application.js
|
|
33
|
+
import FragmentSerializer from 'ember-data-model-fragments/serializer';
|
|
34
|
+
|
|
35
|
+
export default class extends FragmentSerializer {}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
@class FragmentStore
|
|
39
|
+
@extends Store
|
|
40
|
+
@public
|
|
41
|
+
*/
|
|
42
|
+
export default class FragmentStore extends Store {
|
|
43
|
+
get cache() {
|
|
44
|
+
return installCacheManagerCompat(this, super.cache);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Override createCache to return our FragmentCache
|
|
49
|
+
* This is the V2 Cache hook introduced in ember-data 4.7+
|
|
50
|
+
*
|
|
51
|
+
* @method createCache
|
|
52
|
+
* @param {Object} storeWrapper
|
|
53
|
+
* @return {FragmentCache}
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
createCache(storeWrapper) {
|
|
57
|
+
return new FragmentCache(storeWrapper);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Override createSchemaService to provide fragment-aware schema for ember-data 4.13+
|
|
62
|
+
*
|
|
63
|
+
* In ember-data 4.13, a new schema service architecture was introduced that only
|
|
64
|
+
* recognizes attributes with `kind: 'attribute'`. Fragment attributes use different
|
|
65
|
+
* kinds ('fragment', 'fragment-array', 'array'), so they need special handling.
|
|
66
|
+
*
|
|
67
|
+
* This method is only called in ember-data 4.13+. For ember-data 4.12, this method
|
|
68
|
+
* doesn't exist in the Store class, so it's never invoked.
|
|
69
|
+
*
|
|
70
|
+
* The `macroCondition` ensures build-time optimization:
|
|
71
|
+
* - For 4.12 builds: This code is completely removed (tree-shaking)
|
|
72
|
+
* - For 4.13 builds: Only the FragmentSchemaService path remains
|
|
73
|
+
*
|
|
74
|
+
* @method createSchemaService
|
|
75
|
+
* @return {FragmentSchemaService|undefined}
|
|
76
|
+
* @public
|
|
77
|
+
*/
|
|
78
|
+
createSchemaService() {
|
|
79
|
+
if (macroCondition(dependencySatisfies('ember-data', '>=4.13.0-alpha.0'))) {
|
|
80
|
+
const { buildSchema } = importSync('@ember-data/model/hooks');
|
|
81
|
+
|
|
82
|
+
const FragmentSchemaService = importSync('./schema-service').default;
|
|
83
|
+
|
|
84
|
+
return new FragmentSchemaService(this, buildSchema(this));
|
|
85
|
+
}
|
|
86
|
+
// For ember-data 4.12, this method is never called (doesn't exist in Store base class)
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Override teardownRecord to handle fragments in a disconnected state.
|
|
92
|
+
* In ember-data 4.12+, fragments can end up disconnected during unload,
|
|
93
|
+
* and the default teardownRecord fails when trying to destroy them.
|
|
94
|
+
*
|
|
95
|
+
* @method teardownRecord
|
|
96
|
+
* @param {Model} record
|
|
97
|
+
* @public
|
|
98
|
+
*/
|
|
99
|
+
teardownRecord(record) {
|
|
100
|
+
// Check if record is a fragment (by checking if it has no id or by model type)
|
|
101
|
+
// We need to handle the case where the fragment's store is disconnected
|
|
102
|
+
if (record.isDestroyed || record.isDestroying) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
record.destroy();
|
|
107
|
+
} catch (e) {
|
|
108
|
+
// If the error is about disconnected state, just let it go
|
|
109
|
+
// The fragment will be cleaned up by ember's garbage collection
|
|
110
|
+
if (
|
|
111
|
+
e?.message?.includes?.('disconnected state') ||
|
|
112
|
+
e?.message?.includes?.('cannot utilize the store')
|
|
113
|
+
) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
throw e;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
Create a new fragment that does not yet have an owner record.
|
|
122
|
+
The properties passed to this method are set on the newly created
|
|
123
|
+
fragment.
|
|
124
|
+
|
|
125
|
+
To create a new instance of the `name` fragment:
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
store.createFragment('name', {
|
|
129
|
+
first: 'Alex',
|
|
130
|
+
last: 'Routé'
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
@method createFragment
|
|
135
|
+
@param {String} modelName - The type of fragment to create
|
|
136
|
+
@param {Object} props - A hash of properties to set on the newly created fragment
|
|
137
|
+
@return {Fragment} fragment
|
|
138
|
+
@public
|
|
139
|
+
*/
|
|
140
|
+
createFragment(modelName, props) {
|
|
141
|
+
assert(
|
|
142
|
+
`The '${modelName}' model must be a subclass of MF.Fragment`,
|
|
143
|
+
this.isFragment(modelName),
|
|
144
|
+
);
|
|
145
|
+
// Create a new identifier for the fragment
|
|
146
|
+
const identifier = this.identifierCache.createIdentifierForNewRecord({
|
|
147
|
+
type: modelName,
|
|
148
|
+
});
|
|
149
|
+
// Signal to cache that this is a new record
|
|
150
|
+
this.cache.clientDidCreate(identifier, props || {});
|
|
151
|
+
if (macroCondition(dependencySatisfies('ember-data', '>=5.8.0'))) {
|
|
152
|
+
const record = this._instanceCache.getRecord(identifier);
|
|
153
|
+
|
|
154
|
+
if (props) {
|
|
155
|
+
const definitions =
|
|
156
|
+
this.getSchemaDefinitionService().fields(identifier);
|
|
157
|
+
|
|
158
|
+
for (const [key, value] of Object.entries(props)) {
|
|
159
|
+
if (!definitions.has(key)) {
|
|
160
|
+
record.set(key, value);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return record;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Get the record instance
|
|
169
|
+
return this._instanceCache.getRecord(identifier, props);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
Override `serializerFor` so fragment models never fall back to
|
|
174
|
+
`serializer:application`.
|
|
175
|
+
|
|
176
|
+
Why: a typical app's `serializer:application` is a REST or JSON:API
|
|
177
|
+
serializer that does not know how to normalize a raw fragment hash. In
|
|
178
|
+
particular, a JSON:API application serializer would trip the assert in
|
|
179
|
+
`FragmentTransform.deserializeSingle` and break fragment deserialization
|
|
180
|
+
on ember-data 4.12 (and any other path that runs the fragment transform
|
|
181
|
+
pipeline).
|
|
182
|
+
|
|
183
|
+
Resolution order for fragment model names:
|
|
184
|
+
1. `serializer:{modelName}` (if the app registered a per-fragment serializer)
|
|
185
|
+
2. `serializer:-fragment` (consumer-overridable global fragment serializer)
|
|
186
|
+
3. `serializer:-mf-fragment` (lazily-registered default `FragmentSerializer`,
|
|
187
|
+
which extends `JSONSerializer` and is what the fragment pipeline expects)
|
|
188
|
+
|
|
189
|
+
Non-fragment lookups defer to the parent `serializerFor`, preserving normal
|
|
190
|
+
app behavior (including `serializer:application` fallback).
|
|
191
|
+
|
|
192
|
+
Implementation note: in ember-data 5.x, `serializerFor` is defined on the
|
|
193
|
+
parent `Store` as a class-field arrow function, which is assigned per
|
|
194
|
+
instance during the parent constructor and would shadow any prototype-level
|
|
195
|
+
override declared on this subclass. To handle both shapes (class field on
|
|
196
|
+
5.x, prototype method on 4.12) we install the override in the constructor,
|
|
197
|
+
which runs after the parent constructor and therefore wins in either case.
|
|
198
|
+
The original `serializerFor` is captured and used for non-fragment lookups.
|
|
199
|
+
|
|
200
|
+
This restores the pre-4.13 behavior that was lost when the previous
|
|
201
|
+
`Store.reopen({ serializerFor })` from `addon/ext.js` was removed.
|
|
202
|
+
|
|
203
|
+
@method serializerFor
|
|
204
|
+
@param {String} modelName
|
|
205
|
+
@return {Serializer}
|
|
206
|
+
@public
|
|
207
|
+
*/
|
|
208
|
+
constructor(...args) {
|
|
209
|
+
super(...args);
|
|
210
|
+
|
|
211
|
+
const parentSerializerFor =
|
|
212
|
+
typeof this.serializerFor === 'function'
|
|
213
|
+
? this.serializerFor.bind(this)
|
|
214
|
+
: null;
|
|
215
|
+
|
|
216
|
+
this.serializerFor = (modelName) => {
|
|
217
|
+
if (typeof modelName === 'string' && this._isFragmentSafe(modelName)) {
|
|
218
|
+
return this._fragmentSerializerFor(modelName);
|
|
219
|
+
}
|
|
220
|
+
if (parentSerializerFor) {
|
|
221
|
+
return parentSerializerFor(modelName);
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
Resolve a serializer for a fragment model name.
|
|
229
|
+
|
|
230
|
+
@private
|
|
231
|
+
*/
|
|
232
|
+
_fragmentSerializerFor(modelName) {
|
|
233
|
+
const owner = getOwner(this);
|
|
234
|
+
|
|
235
|
+
// 1. Per-fragment-type serializer (e.g. app/serializers/name.js).
|
|
236
|
+
// `owner.lookup('serializer:<name>')` returns `undefined` when no
|
|
237
|
+
// registration / app file exists, so a non-undefined result means a
|
|
238
|
+
// consumer actually provided one.
|
|
239
|
+
const perTypeKey = `serializer:${modelName}`;
|
|
240
|
+
let serializer = owner.lookup(perTypeKey);
|
|
241
|
+
if (serializer !== undefined) {
|
|
242
|
+
return serializer;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 2. Consumer-provided global fragment serializer.
|
|
246
|
+
serializer = owner.lookup('serializer:-fragment');
|
|
247
|
+
if (serializer !== undefined) {
|
|
248
|
+
return serializer;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 3. Lazily register and return our default FragmentSerializer.
|
|
252
|
+
// This is JSON-based (not REST/JSON:API), which is what the fragment
|
|
253
|
+
// pipeline expects.
|
|
254
|
+
const FALLBACK_KEY = 'serializer:-mf-fragment';
|
|
255
|
+
if (!owner.hasRegistration(FALLBACK_KEY)) {
|
|
256
|
+
const FragmentSerializer = importSync('./serializers/fragment').default;
|
|
257
|
+
owner.register(FALLBACK_KEY, FragmentSerializer);
|
|
258
|
+
}
|
|
259
|
+
return owner.lookup(FALLBACK_KEY);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
Like `isFragment`, but never throws for unknown model names. `serializerFor`
|
|
264
|
+
is called with synthetic names (e.g. `-default`, `application`, transform
|
|
265
|
+
types, etc.) so we can't let `modelFor` blow up.
|
|
266
|
+
|
|
267
|
+
@private
|
|
268
|
+
*/
|
|
269
|
+
_isFragmentSafe(modelName) {
|
|
270
|
+
if (
|
|
271
|
+
!modelName ||
|
|
272
|
+
modelName === 'application' ||
|
|
273
|
+
modelName === '-default' ||
|
|
274
|
+
modelName.charAt(0) === '-'
|
|
275
|
+
) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
return this.isFragment(modelName);
|
|
280
|
+
} catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
Returns true if the modelName is a fragment, false if not
|
|
287
|
+
|
|
288
|
+
@method isFragment
|
|
289
|
+
@param {String} modelName - The modelName to check if a fragment
|
|
290
|
+
@return {Boolean}
|
|
291
|
+
@public
|
|
292
|
+
*/
|
|
293
|
+
isFragment(modelName) {
|
|
294
|
+
if (modelName === 'application' || modelName === '-default') {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const type = this.modelFor(modelName);
|
|
299
|
+
return Fragment.detect(type);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { assert } from '@ember/debug';
|
|
2
1
|
import Transform from '@ember-data/serializer/transform';
|
|
3
|
-
import JSONAPISerializer from '@ember-data/serializer/json-api';
|
|
4
2
|
import { service } from '@ember/service';
|
|
5
3
|
import { recordIdentifierFor } from '@ember-data/store';
|
|
6
4
|
|
|
@@ -80,13 +78,11 @@ const FragmentTransform = Transform.extend({
|
|
|
80
78
|
deserializeSingle(data, options, parentData) {
|
|
81
79
|
const store = this.store;
|
|
82
80
|
const modelName = this.modelNameFor(data, options, parentData);
|
|
81
|
+
// `FragmentStore#serializerFor` guarantees a JSON-based serializer
|
|
82
|
+
// (FragmentSerializer / JSONSerializer) for fragment model names, so we
|
|
83
|
+
// do not need to defend against REST/JSON:API serializers here.
|
|
83
84
|
const serializer = store.serializerFor(modelName);
|
|
84
85
|
|
|
85
|
-
assert(
|
|
86
|
-
'The `JSONAPISerializer` is not suitable for model fragments, please use `JSONSerializer`',
|
|
87
|
-
!(serializer instanceof JSONAPISerializer),
|
|
88
|
-
);
|
|
89
|
-
|
|
90
86
|
const typeClass = store.modelFor(modelName);
|
|
91
87
|
const serialized = serializer.normalize(typeClass, data);
|
|
92
88
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const CACHE_METHODS = [
|
|
2
|
+
'createFragmentRecordData',
|
|
3
|
+
'getFragment',
|
|
4
|
+
'hasFragment',
|
|
5
|
+
'setDirtyFragment',
|
|
6
|
+
'isFragmentDirty',
|
|
7
|
+
'getFragmentOwner',
|
|
8
|
+
'setFragmentOwner',
|
|
9
|
+
'newFragmentIdentifierForKey',
|
|
10
|
+
'getFragmentArrayCache',
|
|
11
|
+
'setFragmentArrayCache',
|
|
12
|
+
'rollbackFragment',
|
|
13
|
+
'hasChangedFragments',
|
|
14
|
+
'changedFragments',
|
|
15
|
+
'getFragmentCanonicalState',
|
|
16
|
+
'getFragmentCurrentState',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function installCacheManagerCompat(store, rawCache = store.cache) {
|
|
20
|
+
const cacheManager = rawCache;
|
|
21
|
+
const cache = cacheManager?.___cache;
|
|
22
|
+
|
|
23
|
+
if (!cacheManager || !cache || cacheManager.__mfCompatInstalled) {
|
|
24
|
+
return cache || cacheManager;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Object.defineProperty(cacheManager, '__mfCompatInstalled', {
|
|
28
|
+
value: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
Object.defineProperty(cacheManager, '__innerCache', {
|
|
33
|
+
get() {
|
|
34
|
+
return cache.__innerCache;
|
|
35
|
+
},
|
|
36
|
+
configurable: true,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
CACHE_METHODS.forEach((methodName) => {
|
|
40
|
+
if (typeof cacheManager[methodName] === 'function') {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Object.defineProperty(cacheManager, methodName, {
|
|
45
|
+
value(...args) {
|
|
46
|
+
return cache[methodName](...args);
|
|
47
|
+
},
|
|
48
|
+
configurable: true,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return cache;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { installCacheManagerCompat };
|
|
56
|
+
|
|
57
|
+
export default function fragmentCacheFor(store) {
|
|
58
|
+
return installCacheManagerCompat(store);
|
|
59
|
+
}
|