ember-data-model-fragments 7.0.2 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/addon/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,8 +1,10 @@
1
1
  import { get, computed } from '@ember/object';
2
- import Ember from 'ember';
2
+ import { isDestroying, isDestroyed } from '@ember/destroyable';
3
+ import { Comparable } from '@ember/-internals/runtime';
3
4
  // DS.Model gets munged to add fragment support, which must be included first
4
5
  import { Model } from './ext';
5
6
  import { copy } from './util/copy';
7
+ import fragmentCacheFor from './util/fragment-cache';
6
8
  import { recordIdentifierFor } from '@ember-data/store';
7
9
 
8
10
  /**
@@ -15,7 +17,7 @@ import { recordIdentifierFor } from '@ember-data/store';
15
17
  */
16
18
  export function fragmentRecordDataFor(fragment) {
17
19
  const identifier = recordIdentifierFor(fragment);
18
- return fragment.store.cache.createFragmentRecordData(identifier);
20
+ return fragmentCacheFor(fragment.store).createFragmentRecordData(identifier);
19
21
  }
20
22
 
21
23
  /**
@@ -30,14 +32,17 @@ export function fragmentRecordDataFor(fragment) {
30
32
  Example:
31
33
 
32
34
  ```javascript
33
- App.Person = DS.Model.extend({
34
- name: MF.fragment('name')
35
- });
35
+ import Model from '@ember-data/model';
36
+ import MF from 'ember-data-model-fragments';
36
37
 
37
- App.Name = MF.Fragment.extend({
38
- first : DS.attr('string'),
39
- last : DS.attr('string')
40
- });
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
+ }
41
46
  ```
42
47
 
43
48
  With JSON response:
@@ -53,37 +58,39 @@ export function fragmentRecordDataFor(fragment) {
53
58
  ```
54
59
 
55
60
  ```javascript
56
- let person = store.getbyid('person', '1');
57
- let name = person.get('name');
61
+ let person = store.peekRecord('person', '1');
62
+ let name = person.name;
58
63
 
59
- person.get('hasDirtyAttributes'); // false
60
- name.get('hasDirtyAttributes'); // false
61
- name.get('first'); // 'Robert'
64
+ person.hasDirtyAttributes; // false
65
+ name.hasDirtyAttributes; // false
66
+ name.first; // 'Robert'
62
67
 
63
- name.set('first', 'The Animal');
64
- name.get('hasDirtyAttributes'); // true
65
- person.get('hasDirtyAttributes'); // true
68
+ name.first = 'The Animal';
69
+ name.hasDirtyAttributes; // true
70
+ person.hasDirtyAttributes; // true
66
71
 
67
72
  person.rollbackAttributes();
68
- name.get('first'); // 'Robert'
69
- person.get('hasDirtyAttributes'); // false
70
- person.get('hasDirtyAttributes'); // false
73
+ name.first; // 'Robert'
74
+ person.hasDirtyAttributes; // false
71
75
  ```
72
76
 
73
77
  @class Fragment
74
78
  @namespace MF
75
- @extends CoreModel
76
- @uses Ember.Comparable
77
- @uses Copyable
79
+ @extends Model
80
+ @uses Comparable
81
+ @public
78
82
  */
79
- 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, {
80
86
  /**
81
87
  Compare two fragments by identity to allow `FragmentArray` to diff arrays.
82
88
 
83
89
  @method compare
84
- @param a {MF.Fragment} the first fragment to compare
85
- @param b {MF.Fragment} the second fragment to compare
86
- @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
87
94
  */
88
95
  compare(f1, f2) {
89
96
  return f1 === f2 ? 0 : 1;
@@ -95,53 +102,117 @@ const Fragment = Model.extend(Ember.Comparable, {
95
102
  to other records safely.
96
103
 
97
104
  @method copy
98
- @return {MF.Fragment} the newly created fragment
105
+ @return {Fragment} The newly created fragment
106
+ @public
99
107
  */
100
108
  copy() {
101
109
  const type = this.constructor;
102
110
  const props = Object.create(null);
103
111
  const modelName = type.modelName || this._internalModel.modelName;
112
+ const identifier = recordIdentifierFor(this);
104
113
 
105
- // Look up model via store to avoid schema access deprecation in ember-data 4.12+
106
- 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);
107
119
 
108
120
  // Loop over each attribute and copy individually to ensure nested fragments
109
- // are also copied
110
- modelClass.eachAttribute((name) => {
111
- props[name] = copy(get(this, name));
112
- });
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
+ }
113
155
 
114
156
  return this.store.createFragment(modelName, props);
115
157
  },
116
158
 
159
+ /**
160
+ @method toStringExtension
161
+ @return {String}
162
+ @public
163
+ */
117
164
  toStringExtension() {
165
+ if (isDestroying(this) || isDestroyed(this)) {
166
+ return '';
167
+ }
118
168
  const identifier = recordIdentifierFor(this);
119
- const owner = this.store.cache.getFragmentOwner(identifier);
169
+ const owner = fragmentCacheFor(this.store).getFragmentOwner(identifier);
120
170
  return owner ? `owner(${owner.ownerIdentifier?.id})` : '';
121
171
  },
122
172
 
123
173
  /**
124
174
  Override toString to include the toStringExtension output.
125
175
  ember-data 4.12+ doesn't call toStringExtension in Model.toString().
176
+
177
+ @method toString
178
+ @return {String}
179
+ @public
126
180
  */
127
181
  toString() {
182
+ if (isDestroying(this) || isDestroyed(this)) {
183
+ return `<fragment(destroyed)>`;
184
+ }
128
185
  const identifier = recordIdentifierFor(this);
129
186
  const extension = this.toStringExtension();
130
187
  const extensionStr = extension ? `:${extension}` : '';
131
188
  return `<${identifier.type}:${identifier.id}${extensionStr}>`;
132
189
  },
133
- }).reopenClass({
134
- fragmentOwnerProperties: computed(function () {
135
- const props = [];
190
+ });
136
191
 
137
- this.eachComputedProperty((name, meta) => {
138
- if (meta.isFragmentOwner) {
139
- props.push(name);
140
- }
141
- });
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 = [];
142
198
 
143
- return props;
144
- }).readOnly(),
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
+ });
210
+
211
+ Object.defineProperty(Fragment, 'toString', {
212
+ value() {
213
+ return `model:${this.modelName || 'fragment'}`;
214
+ },
215
+ configurable: true,
145
216
  });
146
217
 
147
218
  /**
@@ -171,7 +242,7 @@ export function setFragmentOwner(fragment, ownerRecordDataOrIdentifier, key) {
171
242
  const fragmentIdentifier = recordIdentifierFor(fragment);
172
243
  const ownerIdentifier =
173
244
  ownerRecordDataOrIdentifier.identifier || ownerRecordDataOrIdentifier;
174
- fragment.store.cache.setFragmentOwner(
245
+ fragmentCacheFor(fragment.store).setFragmentOwner(
175
246
  fragmentIdentifier,
176
247
  ownerIdentifier,
177
248
  key,
@@ -180,7 +251,17 @@ export function setFragmentOwner(fragment, ownerRecordDataOrIdentifier, key) {
180
251
  // Notify any observers of `fragmentOwner` properties
181
252
  // Look up model via store to avoid schema access deprecation in ember-data 4.12+
182
253
  const modelClass = fragment.store.modelFor(fragment.constructor.modelName);
183
- 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) => {
184
265
  fragment.notifyPropertyChange(name);
185
266
  });
186
267
 
@@ -200,7 +281,7 @@ export function isFragment(obj) {
200
281
  Object.defineProperty(Fragment.prototype, 'hasDirtyAttributes', {
201
282
  get() {
202
283
  const identifier = recordIdentifierFor(this);
203
- return this.store.cache.hasChangedAttrs(identifier);
284
+ return fragmentCacheFor(this.store).hasChangedAttrs(identifier);
204
285
  },
205
286
  configurable: true,
206
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;