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/addon/ext.js CHANGED
@@ -1,151 +1,11 @@
1
- import { assert } from '@ember/debug';
2
- import Store from '@ember-data/store';
3
1
  import Model from '@ember-data/model';
4
- // eslint-disable-next-line ember/use-ember-data-rfc-395-imports
5
- import { Snapshot } from 'ember-data/-private';
6
- import { dasherize } from '@ember/string';
7
- import JSONSerializer from '@ember-data/serializer/json';
8
- import FragmentCache from './cache/fragment-cache';
9
- import { default as Fragment } from './fragment';
10
- import { isPresent } from '@ember/utils';
11
- import { getOwner } from '@ember/application';
12
-
13
- function serializerForFragment(owner, normalizedModelName) {
14
- let serializer = owner.lookup(`serializer:${normalizedModelName}`);
15
-
16
- if (serializer !== undefined) {
17
- return serializer;
18
- }
19
-
20
- // no serializer found for the specific model, fallback and check for application serializer
21
- serializer = owner.lookup('serializer:-fragment');
22
- if (serializer !== undefined) {
23
- return serializer;
24
- }
25
-
26
- // final fallback, no model specific serializer, no application serializer, no
27
- // `serializer` property on store: use json-api serializer
28
- serializer = owner.lookup('serializer:-default');
29
-
30
- return serializer;
31
- }
2
+ import { dependencySatisfies, macroCondition } from '@embroider/macros';
3
+ import { Snapshot } from '@ember-data/legacy-compat/-private';
4
+ import fragmentCacheFor from './util/fragment-cache';
32
5
  /**
33
6
  @module ember-data-model-fragments
34
7
  */
35
8
 
36
- /**
37
- @class Store
38
- @namespace DS
39
- */
40
- Store.reopen({
41
- /**
42
- * Override createCache to return our FragmentCache
43
- * This is the V2 Cache hook introduced in ember-data 4.7+
44
- */
45
- createCache(storeWrapper) {
46
- return new FragmentCache(storeWrapper);
47
- },
48
-
49
- /**
50
- * Override teardownRecord to handle fragments in a disconnected state.
51
- * In ember-data 4.12+, fragments can end up disconnected during unload,
52
- * and the default teardownRecord fails when trying to destroy them.
53
- */
54
- teardownRecord(record) {
55
- // Check if record is a fragment (by checking if it has no id or by model type)
56
- // We need to handle the case where the fragment's store is disconnected
57
- if (record.isDestroyed || record.isDestroying) {
58
- return;
59
- }
60
- try {
61
- record.destroy();
62
- } catch (e) {
63
- // If the error is about disconnected state, just let it go
64
- // The fragment will be cleaned up by ember's garbage collection
65
- if (
66
- e?.message?.includes?.('disconnected state') ||
67
- e?.message?.includes?.('cannot utilize the store')
68
- ) {
69
- return;
70
- }
71
- throw e;
72
- }
73
- },
74
-
75
- /**
76
- Create a new fragment that does not yet have an owner record.
77
- The properties passed to this method are set on the newly created
78
- fragment.
79
-
80
- To create a new instance of the `name` fragment:
81
-
82
- ```js
83
- store.createFragment('name', {
84
- first: 'Alex',
85
- last: 'Routé'
86
- });
87
- ```
88
-
89
- @method createFragment
90
- @param {String} type
91
- @param {Object} properties a hash of properties to set on the
92
- newly created fragment.
93
- @return {MF.Fragment} fragment
94
- */
95
- createFragment(modelName, props) {
96
- assert(
97
- `The '${modelName}' model must be a subclass of MF.Fragment`,
98
- this.isFragment(modelName),
99
- );
100
- // Create a new identifier for the fragment
101
- const identifier = this.identifierCache.createIdentifierForNewRecord({
102
- type: modelName,
103
- });
104
- // Signal to cache that this is a new record
105
- this.cache.clientDidCreate(identifier, props || {});
106
- // Get the record instance
107
- return this._instanceCache.getRecord(identifier, props);
108
- },
109
-
110
- /**
111
- Returns true if the modelName is a fragment, false if not
112
-
113
- @method isFragment
114
- @private
115
- @param {String} the modelName to check if a fragment
116
- @return {boolean}
117
- */
118
- isFragment(modelName) {
119
- if (modelName === 'application' || modelName === '-default') {
120
- return false;
121
- }
122
-
123
- const type = this.modelFor(modelName);
124
- return Fragment.detect(type);
125
- },
126
-
127
- serializerFor(modelName) {
128
- // this assertion is cargo-culted from ember-data TODO: update comment
129
- assert(
130
- "You need to pass a model name to the store's serializerFor method",
131
- isPresent(modelName),
132
- );
133
- assert(
134
- `Passing classes to store.serializerFor has been removed. Please pass a dasherized string instead of ${modelName}`,
135
- typeof modelName === 'string',
136
- );
137
-
138
- const owner = getOwner(this);
139
- const normalizedModelName = dasherize(modelName);
140
-
141
- if (this.isFragment(normalizedModelName)) {
142
- return serializerForFragment(owner, normalizedModelName);
143
- } else {
144
- return this._super(...arguments);
145
- }
146
- },
147
- });
148
-
149
9
  /**
150
10
  Override `Snapshot._attributes` to snapshot fragment attributes before they are
151
11
  passed to the `DS.Model#serialize`.
@@ -157,89 +17,65 @@ const oldSnapshotAttributes = Object.getOwnPropertyDescriptor(
157
17
  '_attributes',
158
18
  );
159
19
 
20
+ // Symbol to store our converted attributes cache
21
+ const FRAGMENT_ATTRS = Symbol('fragmentAttrs');
22
+
23
+ function convertSnapshotValue(value) {
24
+ if (value && typeof value._createSnapshot === 'function') {
25
+ return value._createSnapshot();
26
+ }
27
+
28
+ if (Array.isArray(value)) {
29
+ return value.map((item) => convertSnapshotValue(item));
30
+ }
31
+
32
+ return value;
33
+ }
34
+
35
+ function isFragmentDefinition(definition) {
36
+ return definition?.isFragment || definition?.options?.isFragment;
37
+ }
38
+
160
39
  Object.defineProperty(Snapshot.prototype, '_attributes', {
161
40
  get() {
162
- const attrs = oldSnapshotAttributes.get.call(this);
163
- Object.keys(attrs).forEach((key) => {
164
- const attr = attrs[key];
165
- // If the attribute has a `_createSnapshot` method, invoke it before the
166
- // snapshot gets passed to the serializer
167
- if (attr && typeof attr._createSnapshot === 'function') {
168
- attrs[key] = attr._createSnapshot();
169
- }
170
- // Handle arrays of fragments (fragment arrays)
171
- else if (Array.isArray(attr)) {
172
- attrs[key] = attr.map((item) => {
173
- if (item && typeof item._createSnapshot === 'function') {
174
- return item._createSnapshot();
175
- }
176
- return item;
177
- });
178
- }
41
+ // Return cached converted attrs if available
42
+ if (this[FRAGMENT_ATTRS]) {
43
+ return this[FRAGMENT_ATTRS];
44
+ }
45
+
46
+ const cachedAttrs = oldSnapshotAttributes.get.call(this);
47
+
48
+ // Create a new object to avoid modifying the cached __attributes in place
49
+ // This is needed because ember-data caches __attributes and reuses it
50
+ const attrs = Object.create(null);
51
+
52
+ Object.keys(cachedAttrs).forEach((key) => {
53
+ attrs[key] = convertSnapshotValue(cachedAttrs[key]);
179
54
  });
180
- return attrs;
181
- },
182
- });
183
55
 
184
- /**
185
- @class JSONSerializer
186
- @namespace DS
187
- */
188
- JSONSerializer.reopen({
189
- /**
190
- Enables fragment properties to have custom transforms based on the fragment
191
- type, so that deserialization does not have to happen on the fly
192
-
193
- @method transformFor
194
- @private
195
- */
196
- transformFor(attributeType) {
197
- if (attributeType.indexOf('-mf-') !== 0) {
198
- return this._super(...arguments);
199
- }
56
+ if (macroCondition(dependencySatisfies('ember-data', '>=5.8.0'))) {
57
+ const schema = this._store.getSchemaDefinitionService?.();
200
58
 
201
- const owner = getOwner(this);
202
- const containerKey = `transform:${attributeType}`;
203
-
204
- if (!owner.hasRegistration(containerKey)) {
205
- const match = attributeType.match(
206
- /^-mf-(fragment|fragment-array|array)(?:\$([^$]+))?(?:\$(.+))?$/,
207
- );
208
- assert(
209
- `Failed parsing ember-data-model-fragments attribute type ${attributeType}`,
210
- match != null,
211
- );
212
- const transformName = match[1];
213
- const type = match[2];
214
- const polymorphicTypeProp = match[3];
215
- let transformClass = owner.factoryFor(`transform:${transformName}`);
216
- transformClass = transformClass && transformClass.class;
217
- transformClass = transformClass.extend({
218
- type,
219
- polymorphicTypeProp,
220
- store: this.store,
221
- });
222
- owner.register(containerKey, transformClass);
223
- }
224
- return owner.lookup(containerKey);
225
- },
59
+ if (schema) {
60
+ const definitions = schema.attributesDefinitionFor(this.identifier);
226
61
 
227
- // We need to override this to handle polymorphic with a typeKey function
228
- applyTransforms(typeClass, data) {
229
- const attributes = typeClass.attributes;
62
+ Object.entries(definitions).forEach(([key, definition]) => {
63
+ if (key in attrs || !isFragmentDefinition(definition)) {
64
+ return;
65
+ }
230
66
 
231
- typeClass.eachTransformedAttribute((key, typeClass) => {
232
- if (data[key] === undefined) {
233
- return;
67
+ attrs[key] = convertSnapshotValue(
68
+ fragmentCacheFor(this._store).getAttr(this.identifier, key),
69
+ );
70
+ });
234
71
  }
72
+ }
235
73
 
236
- const transform = this.transformFor(typeClass);
237
- const transformMeta = attributes.get(key);
238
- data[key] = transform.deserialize(data[key], transformMeta.options, data);
239
- });
74
+ // Cache the converted attrs
75
+ this[FRAGMENT_ATTRS] = attrs;
240
76
 
241
- return data;
77
+ return attrs;
242
78
  },
243
79
  });
244
80
 
245
- export { Store, Model, JSONSerializer };
81
+ export { Model };
package/addon/fragment.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { get, computed } from '@ember/object';
2
- import Ember from 'ember';
3
2
  import { isDestroying, isDestroyed } from '@ember/destroyable';
3
+ import { Comparable } from '@ember/-internals/runtime';
4
4
  // DS.Model gets munged to add fragment support, which must be included first
5
5
  import { Model } from './ext';
6
6
  import { copy } from './util/copy';
7
+ import fragmentCacheFor from './util/fragment-cache';
7
8
  import { recordIdentifierFor } from '@ember-data/store';
8
9
 
9
10
  /**
@@ -16,7 +17,7 @@ import { recordIdentifierFor } from '@ember-data/store';
16
17
  */
17
18
  export function fragmentRecordDataFor(fragment) {
18
19
  const identifier = recordIdentifierFor(fragment);
19
- return fragment.store.cache.createFragmentRecordData(identifier);
20
+ return fragmentCacheFor(fragment.store).createFragmentRecordData(identifier);
20
21
  }
21
22
 
22
23
  /**
@@ -31,14 +32,17 @@ export function fragmentRecordDataFor(fragment) {
31
32
  Example:
32
33
 
33
34
  ```javascript
34
- App.Person = DS.Model.extend({
35
- name: MF.fragment('name')
36
- });
35
+ import Model from '@ember-data/model';
36
+ import MF from 'ember-data-model-fragments';
37
37
 
38
- App.Name = MF.Fragment.extend({
39
- first : DS.attr('string'),
40
- last : DS.attr('string')
41
- });
38
+ class Person extends Model {
39
+ @MF.fragment('name') name;
40
+ }
41
+
42
+ class Name extends MF.Fragment {
43
+ @attr('string') first;
44
+ @attr('string') last;
45
+ }
42
46
  ```
43
47
 
44
48
  With JSON response:
@@ -54,37 +58,39 @@ export function fragmentRecordDataFor(fragment) {
54
58
  ```
55
59
 
56
60
  ```javascript
57
- let person = store.getbyid('person', '1');
58
- let name = person.get('name');
61
+ let person = store.peekRecord('person', '1');
62
+ let name = person.name;
59
63
 
60
- person.get('hasDirtyAttributes'); // false
61
- name.get('hasDirtyAttributes'); // false
62
- name.get('first'); // 'Robert'
64
+ person.hasDirtyAttributes; // false
65
+ name.hasDirtyAttributes; // false
66
+ name.first; // 'Robert'
63
67
 
64
- name.set('first', 'The Animal');
65
- name.get('hasDirtyAttributes'); // true
66
- person.get('hasDirtyAttributes'); // true
68
+ name.first = 'The Animal';
69
+ name.hasDirtyAttributes; // true
70
+ person.hasDirtyAttributes; // true
67
71
 
68
72
  person.rollbackAttributes();
69
- name.get('first'); // 'Robert'
70
- person.get('hasDirtyAttributes'); // false
71
- person.get('hasDirtyAttributes'); // false
73
+ name.first; // 'Robert'
74
+ person.hasDirtyAttributes; // false
72
75
  ```
73
76
 
74
77
  @class Fragment
75
78
  @namespace MF
76
- @extends CoreModel
77
- @uses Ember.Comparable
78
- @uses Copyable
79
+ @extends Model
80
+ @uses Comparable
81
+ @public
79
82
  */
80
- const Fragment = Model.extend(Ember.Comparable, {
83
+ // Note: We use Model.extend() with Comparable mixin for now
84
+ // as mixins are being phased out but still work in ember-data 4.12+
85
+ const Fragment = Model.extend(Comparable, {
81
86
  /**
82
87
  Compare two fragments by identity to allow `FragmentArray` to diff arrays.
83
88
 
84
89
  @method compare
85
- @param a {MF.Fragment} the first fragment to compare
86
- @param b {MF.Fragment} the second fragment to compare
87
- @return {Integer} the result of the comparison
90
+ @param {Fragment} f1 - The first fragment to compare
91
+ @param {Fragment} f2 - The second fragment to compare
92
+ @return {Integer} The result of the comparison (0 if equal, 1 if not)
93
+ @public
88
94
  */
89
95
  compare(f1, f2) {
90
96
  return f1 === f2 ? 0 : 1;
@@ -96,37 +102,81 @@ const Fragment = Model.extend(Ember.Comparable, {
96
102
  to other records safely.
97
103
 
98
104
  @method copy
99
- @return {MF.Fragment} the newly created fragment
105
+ @return {Fragment} The newly created fragment
106
+ @public
100
107
  */
101
108
  copy() {
102
109
  const type = this.constructor;
103
110
  const props = Object.create(null);
104
111
  const modelName = type.modelName || this._internalModel.modelName;
112
+ const identifier = recordIdentifierFor(this);
105
113
 
106
- // Look up model via store to avoid schema access deprecation in ember-data 4.12+
107
- const modelClass = this.store.modelFor(modelName);
114
+ // Use schema service to get all attributes including fragment attributes
115
+ // eachAttribute only iterates standard @attr() properties, not fragment properties
116
+ const schemaService = this.store
117
+ .getSchemaDefinitionService()
118
+ .attributesDefinitionFor(identifier);
108
119
 
109
120
  // Loop over each attribute and copy individually to ensure nested fragments
110
- // are also copied
111
- modelClass.eachAttribute((name) => {
112
- props[name] = copy(get(this, name));
113
- });
121
+ // are also copied. For fragment attributes, we need to serialize to raw data
122
+ // since createFragment expects raw data, not fragment instances.
123
+ for (const name of Object.keys(schemaService)) {
124
+ const value = get(this, name);
125
+ const definition = schemaService[name];
126
+ const isFragmentAttr =
127
+ definition?.isFragment || definition?.options?.isFragment;
128
+
129
+ if (isFragmentAttr) {
130
+ // For fragment attributes, serialize to get raw data that can be used to create new fragments
131
+ if (value === null || value === undefined) {
132
+ props[name] = value;
133
+ } else if (typeof value.serialize === 'function') {
134
+ // Single fragment - serialize it
135
+ props[name] = value.serialize();
136
+ } else if (
137
+ Array.isArray(value) ||
138
+ typeof value.toArray === 'function'
139
+ ) {
140
+ // Fragment array or array - serialize each element
141
+ const arr =
142
+ typeof value.toArray === 'function' ? value.toArray() : value;
143
+ props[name] = arr.map((item) =>
144
+ typeof item.serialize === 'function' ? item.serialize() : item,
145
+ );
146
+ } else {
147
+ // Fallback - use as-is
148
+ props[name] = value;
149
+ }
150
+ } else {
151
+ // Regular attribute - just copy the value
152
+ props[name] = copy(value);
153
+ }
154
+ }
114
155
 
115
156
  return this.store.createFragment(modelName, props);
116
157
  },
117
158
 
159
+ /**
160
+ @method toStringExtension
161
+ @return {String}
162
+ @public
163
+ */
118
164
  toStringExtension() {
119
165
  if (isDestroying(this) || isDestroyed(this)) {
120
166
  return '';
121
167
  }
122
168
  const identifier = recordIdentifierFor(this);
123
- const owner = this.store.cache.getFragmentOwner(identifier);
169
+ const owner = fragmentCacheFor(this.store).getFragmentOwner(identifier);
124
170
  return owner ? `owner(${owner.ownerIdentifier?.id})` : '';
125
171
  },
126
172
 
127
173
  /**
128
174
  Override toString to include the toStringExtension output.
129
175
  ember-data 4.12+ doesn't call toStringExtension in Model.toString().
176
+
177
+ @method toString
178
+ @return {String}
179
+ @public
130
180
  */
131
181
  toString() {
132
182
  if (isDestroying(this) || isDestroyed(this)) {
@@ -137,18 +187,32 @@ const Fragment = Model.extend(Ember.Comparable, {
137
187
  const extensionStr = extension ? `:${extension}` : '';
138
188
  return `<${identifier.type}:${identifier.id}${extensionStr}>`;
139
189
  },
140
- }).reopenClass({
141
- fragmentOwnerProperties: computed(function () {
142
- const props = [];
190
+ });
143
191
 
144
- this.eachComputedProperty((name, meta) => {
145
- if (meta.isFragmentOwner) {
146
- props.push(name);
147
- }
148
- });
192
+ // Add static property using native class syntax approach
193
+ // This replaces reopenClass which is deprecated
194
+ Object.defineProperty(Fragment, 'fragmentOwnerProperties', {
195
+ get() {
196
+ return computed(function () {
197
+ const props = [];
198
+
199
+ this.eachComputedProperty((name, meta) => {
200
+ if (meta.isFragmentOwner) {
201
+ props.push(name);
202
+ }
203
+ });
204
+
205
+ return props;
206
+ }).readOnly();
207
+ },
208
+ configurable: true,
209
+ });
149
210
 
150
- return props;
151
- }).readOnly(),
211
+ Object.defineProperty(Fragment, 'toString', {
212
+ value() {
213
+ return `model:${this.modelName || 'fragment'}`;
214
+ },
215
+ configurable: true,
152
216
  });
153
217
 
154
218
  /**
@@ -178,7 +242,7 @@ export function setFragmentOwner(fragment, ownerRecordDataOrIdentifier, key) {
178
242
  const fragmentIdentifier = recordIdentifierFor(fragment);
179
243
  const ownerIdentifier =
180
244
  ownerRecordDataOrIdentifier.identifier || ownerRecordDataOrIdentifier;
181
- fragment.store.cache.setFragmentOwner(
245
+ fragmentCacheFor(fragment.store).setFragmentOwner(
182
246
  fragmentIdentifier,
183
247
  ownerIdentifier,
184
248
  key,
@@ -187,7 +251,17 @@ export function setFragmentOwner(fragment, ownerRecordDataOrIdentifier, key) {
187
251
  // Notify any observers of `fragmentOwner` properties
188
252
  // Look up model via store to avoid schema access deprecation in ember-data 4.12+
189
253
  const modelClass = fragment.store.modelFor(fragment.constructor.modelName);
190
- modelClass.fragmentOwnerProperties.forEach((name) => {
254
+
255
+ // Get the fragment owner properties array
256
+ // In 4.13+, we need to iterate computed properties directly since static property access may not work
257
+ const ownerProps = [];
258
+ modelClass.eachComputedProperty((name, meta) => {
259
+ if (meta.isFragmentOwner) {
260
+ ownerProps.push(name);
261
+ }
262
+ });
263
+
264
+ ownerProps.forEach((name) => {
191
265
  fragment.notifyPropertyChange(name);
192
266
  });
193
267
 
@@ -207,7 +281,7 @@ export function isFragment(obj) {
207
281
  Object.defineProperty(Fragment.prototype, 'hasDirtyAttributes', {
208
282
  get() {
209
283
  const identifier = recordIdentifierFor(this);
210
- return this.store.cache.hasChangedAttrs(identifier);
284
+ return fragmentCacheFor(this.store).hasChangedAttrs(identifier);
211
285
  },
212
286
  configurable: true,
213
287
  });
package/addon/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import Namespace from '@ember/application/namespace';
2
- import Ember from 'ember';
2
+ import { importSync } from '@embroider/macros';
3
3
  import VERSION from './version';
4
4
  import Fragment from './fragment';
5
5
  import FragmentArray from './array/fragment';
@@ -7,6 +7,11 @@ import FragmentTransform from './transforms/fragment';
7
7
  import FragmentArrayTransform from './transforms/fragment-array';
8
8
  import ArrayTransform from './transforms/array';
9
9
  import { fragment, fragmentArray, array, fragmentOwner } from './attributes';
10
+ import FragmentStore from './store';
11
+ import FragmentSerializer, {
12
+ FragmentRESTSerializer,
13
+ FragmentJSONAPISerializer,
14
+ } from './serializer';
10
15
 
11
16
  /**
12
17
  Ember Data Model Fragments
@@ -21,14 +26,20 @@ const MF = Namespace.create({
21
26
  FragmentTransform: FragmentTransform,
22
27
  FragmentArrayTransform: FragmentArrayTransform,
23
28
  ArrayTransform: ArrayTransform,
29
+ FragmentStore: FragmentStore,
30
+ FragmentSerializer: FragmentSerializer,
31
+ FragmentRESTSerializer: FragmentRESTSerializer,
32
+ FragmentJSONAPISerializer: FragmentJSONAPISerializer,
24
33
  fragment: fragment,
25
34
  fragmentArray: fragmentArray,
26
35
  array: array,
27
36
  fragmentOwner: fragmentOwner,
28
37
  });
29
38
 
30
- if (Ember.libraries) {
31
- Ember.libraries.register('Model Fragments', MF.VERSION);
32
- }
39
+ Object.defineProperty(MF, 'FragmentSchemaService', {
40
+ get() {
41
+ return importSync('./schema-service').default;
42
+ },
43
+ });
33
44
 
34
45
  export default MF;