diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f520fa2..abfcaa78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,7 @@ jobs: - ember-beta - ember-canary - ember-data-4.12 + - ember-data-4.13 - embroider-safe - embroider-optimized diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..f55a7cf6 --- /dev/null +++ b/.npmrc @@ -0,0 +1,24 @@ +#################### +# super strict mode +#################### +auto-install-peers=false +# Relaxed for ember-try compatibility - ember-try swaps dependency versions +# which naturally creates peer dependency mismatches during testing +strict-peer-dependents=false +resolve-peers-from-workspace-root=false + +################ +# Optimizations +################ +# Less strict, but required for tooling to not barf on duplicate peer trees. +# (many libraries declare the same peers, which resolve to the same +# versions) +dedupe-peer-dependents=true +public-hoist-pattern[]=ember-source + +################ +# Compatibility +################ +# highest is what everyone is used to, but +# not ensuring folks are actually compatible with declared ranges. +resolution-mode=highest diff --git a/README.md b/README.md index 6dd0fbb3..43e5b40a 100644 --- a/README.md +++ b/README.md @@ -21,20 +21,6 @@ Use the following table to decide which version of this project to use with your | >= v3.28.x < v4.7.x | v6.0.x | 14+ | | v4.12.x | v7.x | 18+ | -### Upgrading to ember-data 4.12 - -When using ember-data 4.12, you must add the following entry to your app's deprecation workflow to allow the app to boot. This addon uses `reopenClass` internally, which ember-data 4.12 deprecates: - -```javascript -// config/deprecation-workflow.js -self.deprecationWorkflow = self.deprecationWorkflow || {}; -self.deprecationWorkflow.config = { - workflow: [ - { handler: "silence", matchId: "ember-data:deprecate-model-reopenclass" }, - ], -}; -``` - ## Installation To install as an Ember CLI addon: @@ -51,6 +37,36 @@ $ ember generate fragment foo someAttr:string anotherAttr:boolean Which will create the module `app/models/foo.js` which exports a `Fragment` class with the given attributes. +## Setup + +This addon requires you to extend the provided `FragmentStore` and a fragment-aware serializer in your application. + +### Store + +Create or update your application's store service to extend `FragmentStore`: + +```javascript +// app/services/store.js + +import FragmentStore from "ember-data-model-fragments/store"; + +export default class Store extends FragmentStore {} +``` + +### Serializer + +Create or update your application serializer to extend one of the fragment-aware serializers: + +```javascript +// app/serializers/application.js + +import FragmentSerializer from "ember-data-model-fragments/serializer"; + +export default class ApplicationSerializer extends FragmentSerializer {} +``` + +See the [Serialization](#serialization) section for more options if you're using `RESTSerializer` or `JSONAPISerializer`. + ## Example ```javascript @@ -297,31 +313,36 @@ export default class NameSerializer extends JSONSerializer { } ``` -Since fragment deserialization uses the value of a single attribute in the parent model, the `normalizeResponse` method of the serializer is never used. And since the attribute value is not a full-fledged [JSON API](http://jsonapi.org/) response, `JSONAPISerializer` cannot be used with fragments. Because of this, auto-generated fragment serializers **do not use the application serializer** and instead use `JSONSerializer`. +Since fragment deserialization uses the value of a single attribute in the parent model, the `normalizeResponse` method of the serializer is never used. And since the attribute value is not a full-fledged [JSON API](http://jsonapi.org/) response, `JSONAPISerializer` cannot be used directly with fragments. -If common logic must be added to auto-generated fragment serializers, apps can register a custom `serializer:-fragment` with the application in an initializer. +Your application serializer should extend one of the fragment-aware serializers provided by this addon: ```javascript -// app/serializers/fragment.js +// app/serializers/application.js -import JSONSerializer from "@ember-data/serializer/json"; +import FragmentSerializer from "ember-data-model-fragments/serializer"; -export default class FragmentSerializer extends JSONSerializer {} +export default class ApplicationSerializer extends FragmentSerializer {} ``` +If you're using `RESTSerializer`, use `FragmentRESTSerializer` instead: + ```javascript -// app/initializers/fragment-serializer.js +// app/serializers/application.js -import FragmentSerializer from "../serializers/fragment"; +import { FragmentRESTSerializer } from "ember-data-model-fragments/serializer"; -export function initialize(application) { - application.register("serializer:-fragment", FragmentSerializer); -} +export default class ApplicationSerializer extends FragmentRESTSerializer {} +``` + +If you're using `JSONAPISerializer`, use `FragmentJSONAPISerializer`: -export default { - name: "fragment-serializer", - initialize: initialize, -}; +```javascript +// app/serializers/application.js + +import { FragmentJSONAPISerializer } from "ember-data-model-fragments/serializer"; + +export default class ApplicationSerializer extends FragmentJSONAPISerializer {} ``` If custom serialization of the owner record is needed, fragment [snapshots](http://emberjs.com/api/data/classes/DS.Snapshot.html) can be accessed using the [`Snapshot#attr`](http://emberjs.com/api/data/classes/DS.Snapshot.html#method_attr) method. Note that this differs from how relationships are accessed on snapshots (using `belongsTo`/`hasMany` methods): @@ -330,9 +351,9 @@ If custom serialization of the owner record is needed, fragment [snapshots](http // apps/serializers/person.js // Fragment snapshots are accessed using `snapshot.attr()` -import JSONSerializer from "@ember-data/serializer/json"; +import FragmentSerializer from "ember-data-model-fragments/serializer"; -export default JSONSerializer.extend({ +export default class PersonSerializer extends FragmentSerializer { serialize(snapshot, options) { const json = super.serialize(...arguments); @@ -355,8 +376,8 @@ export default JSONSerializer.extend({ json.title_count = titlesSnapshot.length; return json; - }, -}); + } +} ``` ## Nesting diff --git a/addon/array/stateful.js b/addon/array/stateful.js index 64407077..904f141b 100644 --- a/addon/array/stateful.js +++ b/addon/array/stateful.js @@ -2,24 +2,12 @@ import EmberObject, { get } from '@ember/object'; import { isArray } from '@ember/array'; import MutableArray from '@ember/array/mutable'; import { assert } from '@ember/debug'; -import { diffArray } from '@ember-data/model/-private'; import { copy } from '../util/copy'; -import { gte } from 'ember-compatibility-helpers'; /** @module ember-data-model-fragments */ -/** - * Whether the current version of ember supports array observers. - * Array observers were deprecated in ember 3.26 and removed in 4.0. - * @see https://deprecations.emberjs.com/v3.x#toc_array-observers - * @see https://github.com/emberjs/ember.js/pull/19833 - * @type {boolean} - * @private - */ -export const HAS_ARRAY_OBSERVERS = !gte('4.0.0'); - /** A state-aware array that is tied to an attribute of a `DS.Model` instance. @@ -88,14 +76,10 @@ const StatefulArray = EmberObject.extend(MutableArray, { notify() { this._isDirty = true; - if (HAS_ARRAY_OBSERVERS && this.hasArrayObservers && !this._hasNotified) { - this.retrieveLatest(); - } else { - this._hasNotified = true; - this.notifyPropertyChange('[]'); - this.notifyPropertyChange('firstObject'); - this.notifyPropertyChange('lastObject'); - } + this._hasNotified = true; + this.notifyPropertyChange('[]'); + this.notifyPropertyChange('firstObject'); + this.notifyPropertyChange('lastObject'); }, get length() { @@ -181,30 +165,9 @@ const StatefulArray = EmberObject.extend(MutableArray, { this._isDirty = false; this._isUpdating = true; - if (HAS_ARRAY_OBSERVERS && this.hasArrayObservers && !this._hasNotified) { - // diff to find changes - const diff = diffArray(this.currentState, currentState); - // it's null if no change found - if (diff.firstChangeIndex !== null) { - // we found a change - this.arrayContentWillChange( - diff.firstChangeIndex, - diff.removedCount, - diff.addedCount, - ); - this._length = currentState.length; - this.currentState = currentState; - this.arrayContentDidChange( - diff.firstChangeIndex, - diff.removedCount, - diff.addedCount, - ); - } - } else { - this._hasNotified = false; - this._length = currentState.length; - this.currentState = currentState; - } + this._hasNotified = false; + this._length = currentState.length; + this.currentState = currentState; this._isUpdating = false; }, diff --git a/addon/attributes/array.js b/addon/attributes/array.js index 7a9d6848..f86b55bd 100644 --- a/addon/attributes/array.js +++ b/addon/attributes/array.js @@ -52,8 +52,9 @@ export default function array(type, options) { options, }; - // eslint-disable-next-line ember/require-computed-property-dependencies - return computed({ + // Use computed with a dependency on hasDirtyAttributes which changes on rollback + // This ensures the computed property is re-evaluated when dirty state changes + return computed('currentState', 'hasDirtyAttributes', 'store.cache', { get(key) { const identifier = recordIdentifierFor(this); const cache = this.store.cache; diff --git a/addon/attributes/fragment-array.js b/addon/attributes/fragment-array.js index c24d3dc4..3a3d878d 100644 --- a/addon/attributes/fragment-array.js +++ b/addon/attributes/fragment-array.js @@ -61,8 +61,9 @@ export default function fragmentArray(type, options) { options, }; - // eslint-disable-next-line ember/require-computed-property-dependencies - return computed({ + // Use computed with a dependency on hasDirtyAttributes which changes on rollback + // This ensures the computed property is re-evaluated when dirty state changes + return computed('currentState', 'hasDirtyAttributes', 'store.cache', { get(key) { const identifier = recordIdentifierFor(this); const cache = this.store.cache; diff --git a/addon/attributes/fragment.js b/addon/attributes/fragment.js index 109451e5..c41c03c0 100644 --- a/addon/attributes/fragment.js +++ b/addon/attributes/fragment.js @@ -60,53 +60,63 @@ export default function fragment(type, options) { options, }; - return computed('store.{_instanceCache,cache}', { - get(key) { - const identifier = recordIdentifierFor(this); - const cache = this.store.cache; - const fragmentIdentifier = cache.getFragment(identifier, key); - if (fragmentIdentifier === null) { - return null; - } - // Get the fragment record from the identifier - return this.store._instanceCache.getRecord(fragmentIdentifier); - }, - set(key, value) { - assert( - 'You must pass a fragment or null to set a fragment', - value === null || isFragment(value) || typeOf(value) === 'object', - ); - const identifier = recordIdentifierFor(this); - const cache = this.store.cache; - if (value === null) { - cache.setDirtyFragment(identifier, key, null); - return null; - } - if (isFragment(value)) { + // Use computed with a dependency on hasDirtyAttributes which changes on rollback + // This ensures the computed property is re-evaluated when dirty state changes + const cp = computed( + 'currentState', + 'hasDirtyAttributes', + 'store.{_instanceCache,cache}', + { + get(key) { + const identifier = recordIdentifierFor(this); + const cache = this.store.cache; + const fragmentIdentifier = cache.getFragment(identifier, key); + if (fragmentIdentifier === null) { + return null; + } + // Get the fragment record from the identifier + return this.store._instanceCache.getRecord(fragmentIdentifier); + }, + set(key, value) { assert( - `You can only set '${type}' fragments to this property`, - isInstanceOfType(this.store.modelFor(type), value), + 'You must pass a fragment or null to set a fragment', + value === null || isFragment(value) || typeOf(value) === 'object', ); - const fragmentIdentifier = recordIdentifierFor(value); - setFragmentOwner(value, identifier, key); - cache.setDirtyFragment(identifier, key, fragmentIdentifier); - return value; - } - // Value is a plain object - update existing fragment or create new one - const fragmentIdentifier = cache.getFragment(identifier, key); - const actualType = getActualFragmentType(type, options, value, this); - if (fragmentIdentifier?.type !== actualType) { - // Create a new fragment - const fragment = this.store.createFragment(actualType, value); - const newFragmentIdentifier = recordIdentifierFor(fragment); - setFragmentOwner(fragment, identifier, key); - cache.setDirtyFragment(identifier, key, newFragmentIdentifier); + const identifier = recordIdentifierFor(this); + const cache = this.store.cache; + if (value === null) { + cache.setDirtyFragment(identifier, key, null); + return null; + } + if (isFragment(value)) { + assert( + `You can only set '${type}' fragments to this property`, + isInstanceOfType(this.store.modelFor(type), value), + ); + const fragmentIdentifier = recordIdentifierFor(value); + setFragmentOwner(value, identifier, key); + cache.setDirtyFragment(identifier, key, fragmentIdentifier); + return value; + } + // Value is a plain object - update existing fragment or create new one + const fragmentIdentifier = cache.getFragment(identifier, key); + const actualType = getActualFragmentType(type, options, value, this); + if (fragmentIdentifier?.type !== actualType) { + // Create a new fragment + const fragment = this.store.createFragment(actualType, value); + const newFragmentIdentifier = recordIdentifierFor(fragment); + setFragmentOwner(fragment, identifier, key); + cache.setDirtyFragment(identifier, key, newFragmentIdentifier); + return fragment; + } + // Update existing fragment + const fragment = + this.store._instanceCache.getRecord(fragmentIdentifier); + fragment.setProperties(value); return fragment; - } - // Update existing fragment - const fragment = this.store._instanceCache.getRecord(fragmentIdentifier); - fragment.setProperties(value); - return fragment; + }, }, - }).meta(meta); + ).meta(meta); + + return cp; } diff --git a/addon/cache/fragment-cache.js b/addon/cache/fragment-cache.js index 0db54977..84a9d0e1 100644 --- a/addon/cache/fragment-cache.js +++ b/addon/cache/fragment-cache.js @@ -1,3 +1,4 @@ +import { assert } from '@ember/debug'; import JSONAPICache from '@ember-data/json-api'; import FragmentStateManager from './fragment-state-manager'; import FragmentRecordDataProxy from './fragment-record-data-proxy'; @@ -17,12 +18,40 @@ export default class FragmentCache { this.__innerCache = new JSONAPICache(storeWrapper); this.__fragmentState = new FragmentStateManager(storeWrapper); this.__recordDataProxies = new Map(); + this.__storeValidated = false; } get store() { return this.__storeWrapper._store; } + /** + * Validates that the store service extends FragmentStore + * This validation is lazy - only checked when fragment functionality is first needed + */ + _validateStore() { + if (this.__storeValidated) { + return; + } + this.__storeValidated = true; + + const store = this.store; + + // Check if the store has the required fragment methods + const hasFragmentMethods = + typeof store.createFragment === 'function' && + typeof store.isFragment === 'function'; + + assert( + `ember-data-model-fragments requires your store service to extend FragmentStore.\n\n` + + `Create app/services/store.js with the following:\n\n` + + `import FragmentStore from 'ember-data-model-fragments/store';\n` + + `export default class extends FragmentStore {}\n\n` + + `See the ember-data-model-fragments documentation for more information.`, + hasFragmentMethods, + ); + } + /** * Get or create a FragmentRecordDataProxy for the given identifier. * This provides backwards compatibility with code expecting per-resource RecordData API. @@ -41,6 +70,7 @@ export default class FragmentCache { // ================== getFragment(identifier, key) { + this._validateStore(); return this.__fragmentState.getFragment(identifier, key); } @@ -109,10 +139,136 @@ export default class FragmentCache { // ================== /** - * Cache the response to a request + * Cache the response to a request. + * + * In ember-data 4.13+, this is the primary entry point for caching data. + * We intercept to extract fragment attributes before passing to the inner cache. + * + * The document structure is: + * { + * request: {...}, + * response: {...}, + * content: { + * data: { type, id, attributes, relationships } | [...], + * included: [...], + * meta: {...} + * } + * } */ put(doc) { - return this.__innerCache.put(doc); + // Normalize id to string to ensure consistent comparison + if (doc?.content?.data) { + if (Array.isArray(doc.content.data)) { + doc.content.data.forEach((resource) => { + if (resource.id != null && typeof resource.id !== 'string') { + resource.id = String(resource.id); + } + }); + } else if ( + doc.content.data.id != null && + typeof doc.content.data.id !== 'string' + ) { + doc.content.data.id = String(doc.content.data.id); + } + } + + // NEW APPROACH: Store fragment data separately, push to inner cache first, then process fragments + // This is needed because polymorphic fragment type resolution may require accessing owner attributes, + // which requires the owner record to exist in the cache first. + + // Step 1: Extract fragment data from resources WITHOUT creating fragment identifiers + const fragmentDataByIdentifier = new Map(); + if (doc && doc.content && doc.content.data) { + this._collectFragmentsFromDocument(doc.content, fragmentDataByIdentifier); + } + + // Step 2: Push to inner cache (this creates the owner records) + const result = this.__innerCache.put(doc); + + // Step 3: Now that owner records exist, push fragment data to create fragment identifiers + for (const [identifier, data] of fragmentDataByIdentifier) { + this.__fragmentState.pushFragmentData(identifier, data, false); + } + + return result; + } + + /** + * Collect fragment attributes from a JSON:API document WITHOUT creating fragment identifiers. + * This just stores the raw fragment data and removes fragment attributes from resources. + * Fragment identifiers will be created later after owner records are in the cache. + * + * @private + */ + _collectFragmentsFromDocument(jsonApiDoc, fragmentDataByIdentifier) { + const { data, included } = jsonApiDoc; + + // Handle single resource + if (data && !Array.isArray(data)) { + this._collectFragmentsFromResource(data, fragmentDataByIdentifier); + } + + // Handle array of resources + if (Array.isArray(data)) { + for (const resource of data) { + this._collectFragmentsFromResource(resource, fragmentDataByIdentifier); + } + } + + // Handle included resources + if (included) { + for (const resource of included) { + this._collectFragmentsFromResource(resource, fragmentDataByIdentifier); + } + } + } + + /** + * Collect fragment attributes from a single resource WITHOUT creating fragment identifiers. + * + * @private + */ + _collectFragmentsFromResource(resource, fragmentDataByIdentifier) { + if (!resource || !resource.attributes || !resource.type) { + return; + } + + // Get or create identifier for this resource + const identifier = + this.__storeWrapper.identifierCache.getOrCreateRecordIdentifier({ + type: resource.type, + id: resource.id, + }); + + const definitions = this.__storeWrapper + .getSchemaDefinitionService() + .attributesDefinitionFor(identifier); + + const fragmentData = {}; + const fragmentKeys = []; + + for (const [key, definition] of Object.entries(definitions)) { + const isFragment = + definition.isFragment || definition.options?.isFragment; + + if (isFragment && resource.attributes[key] !== undefined) { + fragmentData[key] = resource.attributes[key]; + fragmentKeys.push(key); + } + } + + // Store fragment data for later processing (AFTER inner cache put) + if (fragmentKeys.length > 0) { + fragmentDataByIdentifier.set(identifier, { attributes: fragmentData }); + + // Clone attributes and remove fragment keys to avoid mutating original data + // This is necessary because resource.attributes may reference user-provided data + const cleanedAttributes = { ...resource.attributes }; + for (const key of fragmentKeys) { + delete cleanedAttributes[key]; + } + resource.attributes = cleanedAttributes; + } } /** @@ -146,8 +302,13 @@ export default class FragmentCache { /** * Push resource data from a remote source into the cache. * Intercepts to handle fragment attributes. + * + * In ember-data 4.13+, the signature is: + * upsert(identifier, resource, hasRecord) where resource is the JSON:API resource object + * In ember-data 4.12, the signature was: + * upsert(identifier, data, calculateChanges) where data = { attributes: {...} } */ - upsert(identifier, data, calculateChanges) { + upsert(identifier, data, hasRecordOrCalculateChanges) { // Normalize the id to string to match identifier.id (which is always coerced to string) // This ensures cached.id matches identifier.id type when didCommit compares them if (data.id != null && typeof data.id !== 'string') { @@ -164,7 +325,9 @@ export default class FragmentCache { .attributesDefinitionFor(identifier); for (const [key, definition] of Object.entries(definitions)) { - if (definition.isFragment && data.attributes[key] !== undefined) { + const isFragment = + definition.isFragment || definition.options?.isFragment; + if (isFragment && data.attributes[key] !== undefined) { fragmentData[key] = data.attributes[key]; fragmentAttributeKeys.push(key); } @@ -187,7 +350,7 @@ export default class FragmentCache { const changedKeys = this.__innerCache.upsert( identifier, data, - calculateChanges, + hasRecordOrCalculateChanges, ); // Handle fragment attributes @@ -195,9 +358,9 @@ export default class FragmentCache { const changedFragmentKeys = this.__fragmentState.pushFragmentData( identifier, { attributes: fragmentData }, - calculateChanges, + hasRecordOrCalculateChanges, ); - if (calculateChanges && changedFragmentKeys?.length) { + if (hasRecordOrCalculateChanges && changedFragmentKeys?.length) { return [...(changedKeys || []), ...changedFragmentKeys]; } } @@ -209,6 +372,48 @@ export default class FragmentCache { * Signal to the cache that a new record has been instantiated on the client */ clientDidCreate(identifier, options) { + // Extract fragment attributes from options before passing to inner cache + if (options) { + const definitions = this.__storeWrapper + .getSchemaDefinitionService() + .attributesDefinitionFor(identifier); + + const fragmentData = {}; + const regularOptions = {}; + let hasFragmentData = false; + + for (const [key, value] of Object.entries(options)) { + const definition = definitions[key]; + const isFragmentAttr = + definition?.isFragment || definition?.options?.isFragment; + + if (isFragmentAttr && value !== undefined) { + fragmentData[key] = value; + hasFragmentData = true; + } else { + regularOptions[key] = value; + } + } + + // IMPORTANT: Create the inner cache entry first, so the owner's attributes + // are available when fragment typeKey functions try to access them + const result = this.__innerCache.clientDidCreate( + identifier, + regularOptions, + ); + + // Push fragment data to our fragment state manager AFTER the cache entry exists + if (hasFragmentData) { + this.__fragmentState.pushFragmentData( + identifier, + { attributes: fragmentData }, + false, + ); + } + + return result; + } + return this.__innerCache.clientDidCreate(identifier, options); } @@ -263,7 +468,9 @@ export default class FragmentCache { fragmentData = { attributes: {} }; for (const [key, definition] of Object.entries(definitions)) { - if (definition.isFragment && attributes[key] !== undefined) { + const isFragment = + definition.isFragment || definition.options?.isFragment; + if (isFragment && attributes[key] !== undefined) { fragmentData.attributes[key] = attributes[key]; } } @@ -315,7 +522,13 @@ export default class FragmentCache { .attributesDefinitionFor(identifier); const definition = definitions[attr]; - if (definition?.isFragment) { + // Check for fragment attribute - support both metadata formats: + // - Direct: definition.isFragment (ember-data 4.12 or original metadata) + // - Transformed: definition.options?.isFragment (from FragmentSchemaService in 4.13) + const isFragmentAttr = + definition?.isFragment || definition?.options?.isFragment; + + if (isFragmentAttr) { // Fragment attributes are handled by fragment state manager // getFragment returns identifier(s), we need to convert to Fragment instance(s) const fragmentValue = this.__fragmentState.getFragment(identifier, attr); @@ -324,29 +537,45 @@ export default class FragmentCache { return fragmentValue; } + // Get the fragment kind from the appropriate location: + // - Direct: definition.kind (original metadata) + // - Transformed: definition.options?.fragmentKind (from FragmentSchemaService) + const fragmentKind = definition.options?.fragmentKind || definition.kind; + // For single fragments, convert identifier to Fragment instance - if (definition.kind === 'fragment') { + if (fragmentKind === 'fragment') { if (fragmentValue.lid) { // It's an identifier, get the record instance - return this.store._instanceCache.getRecord(fragmentValue); + const record = this.store._instanceCache.getRecord(fragmentValue); + return record; } return fragmentValue; } - // For fragment arrays, convert array of identifiers to Fragment instances - if ( - definition.kind === 'fragment-array' && - Array.isArray(fragmentValue) - ) { - return fragmentValue.map((item) => { - if (item?.lid) { - return this.store._instanceCache.getRecord(item); - } - return item; - }); + // For fragment arrays and primitive arrays, return the wrapper object + // This is needed for Snapshot._attributes which expects objects with _createSnapshot + if (fragmentKind === 'fragment-array' || fragmentKind === 'array') { + // Get the cached wrapper if it exists + let arrayWrapper = this.getFragmentArrayCache(identifier, attr); + if (arrayWrapper) { + return arrayWrapper; + } + // If no wrapper exists yet, convert identifiers to Fragment instances + // so ext.js patch can call _createSnapshot on each fragment + if (fragmentKind === 'fragment-array' && Array.isArray(fragmentValue)) { + const fragments = fragmentValue.map((item) => { + if (item?.lid) { + return this.store._instanceCache.getRecord(item); + } + return item; + }); + return fragments; + } + // For primitive arrays, return as-is + return fragmentValue; } - // For primitive arrays, return as-is + // For single fragment type that fell through, return as-is return fragmentValue; } @@ -362,7 +591,10 @@ export default class FragmentCache { .attributesDefinitionFor(identifier); const definition = definitions[attr]; - if (definition?.isFragment) { + const isFragmentAttr = + definition?.isFragment || definition?.options?.isFragment; + + if (isFragmentAttr) { return this.__fragmentState.setDirtyFragment(identifier, attr, value); } diff --git a/addon/cache/fragment-record-data-proxy.js b/addon/cache/fragment-record-data-proxy.js index 1856c243..93a7a673 100644 --- a/addon/cache/fragment-record-data-proxy.js +++ b/addon/cache/fragment-record-data-proxy.js @@ -125,7 +125,9 @@ export default class FragmentRecordDataProxy { .getSchemaDefinitionService() .attributesDefinitionFor(this.identifier); for (const [key, definition] of Object.entries(definitions)) { - if (!definition.isFragment) { + const isFragmentAttr = + definition.isFragment || definition.options?.isFragment; + if (!isFragmentAttr) { regularState[key] = this.__cache.getAttr(this.identifier, key); } } diff --git a/addon/cache/fragment-state-manager.js b/addon/cache/fragment-state-manager.js index 676ae8fb..6febf46e 100644 --- a/addon/cache/fragment-state-manager.js +++ b/addon/cache/fragment-state-manager.js @@ -1,11 +1,41 @@ import { assert } from '@ember/debug'; import { typeOf } from '@ember/utils'; import { isArray } from '@ember/array'; -import { diffArray } from '@ember-data/model/-private'; import { recordIdentifierFor } from '@ember-data/store'; import { getActualFragmentType, isFragment } from '../fragment'; import isInstanceOfType from '../util/instance-of-type'; +/** + * Simple array diff implementation. + * Returns an object with `firstChangeIndex` indicating where the arrays first differ. + * Returns `{ firstChangeIndex: null }` if arrays are identical. + * + * @param {Array} oldArray + * @param {Array} newArray + * @returns {Object} Object with firstChangeIndex property + */ +function diffArray(oldArray, newArray) { + if (oldArray === newArray) { + return { firstChangeIndex: null }; + } + + if (!oldArray || !newArray) { + return { firstChangeIndex: 0 }; + } + + if (oldArray.length !== newArray.length) { + return { firstChangeIndex: Math.min(oldArray.length, newArray.length) }; + } + + for (let i = 0; i < oldArray.length; i++) { + if (oldArray[i] !== newArray[i]) { + return { firstChangeIndex: i }; + } + } + + return { firstChangeIndex: null }; +} + /** * Behavior for single fragment attributes */ @@ -441,36 +471,53 @@ export default class FragmentStateManager { _getBehaviors(identifier) { let behaviors = this.__behaviors.get(identifier.lid); - if (!behaviors) { - behaviors = Object.create(null); - const definitions = this.__storeWrapper - .getSchemaDefinitionService() - .attributesDefinitionFor(identifier); - for (const [key, definition] of Object.entries(definitions)) { - if (!definition.isFragment) { - continue; - } - switch (definition.kind) { - case 'fragment-array': - behaviors[key] = new FragmentArrayBehavior( - this, - identifier, - definition, - ); - break; - case 'fragment': - behaviors[key] = new FragmentBehavior(this, identifier, definition); - break; - case 'array': - behaviors[key] = new ArrayBehavior(this, identifier, definition); - break; - default: - assert(`Unsupported fragment type: ${definition.kind}`); - break; - } + if (behaviors) { + return behaviors; + } + + behaviors = Object.create(null); + + const definitions = this.__storeWrapper + .getSchemaDefinitionService() + .attributesDefinitionFor(identifier); + + for (const [key, definition] of Object.entries(definitions)) { + // Support both metadata formats: + // - ember-data 4.12: definition.isFragment (direct property on meta) + // - ember-data 4.13: definition.options.isFragment (transformed by FragmentSchemaService) + const isFragmentAttr = + definition.isFragment || definition.options?.isFragment; + + if (!isFragmentAttr) { + continue; + } + + // Get the fragment kind from the appropriate location: + // - ember-data 4.12: definition.kind (original location) + // - ember-data 4.13: definition.options.fragmentKind (preserved by FragmentSchemaService) + const fragmentKind = definition.options?.fragmentKind || definition.kind; + + switch (fragmentKind) { + case 'fragment-array': + behaviors[key] = new FragmentArrayBehavior( + this, + identifier, + definition, + ); + break; + case 'fragment': + behaviors[key] = new FragmentBehavior(this, identifier, definition); + break; + case 'array': + behaviors[key] = new ArrayBehavior(this, identifier, definition); + break; + default: + assert(`Unsupported fragment type: ${fragmentKind}`); + break; } - this.__behaviors.set(identifier.lid, behaviors); } + + this.__behaviors.set(identifier.lid, behaviors); return behaviors; } @@ -691,7 +738,9 @@ export default class FragmentStateManager { // Get canonical values for all attributes for (const [key, definition] of Object.entries(definitions)) { - if (definition.isFragment) { + const isFragmentAttr = + definition.isFragment || definition.options?.isFragment; + if (isFragmentAttr) { // Fragment attributes - use behavior's canonicalState const behaviors = this._getBehaviors(identifier); const behavior = behaviors[key]; @@ -723,7 +772,9 @@ export default class FragmentStateManager { // Get current values for all attributes for (const [key, definition] of Object.entries(definitions)) { - if (definition.isFragment) { + const isFragmentAttr = + definition.isFragment || definition.options?.isFragment; + if (isFragmentAttr) { // Fragment attributes - use behavior's currentState const behaviors = this._getBehaviors(identifier); const behavior = behaviors[key]; @@ -823,15 +874,16 @@ export default class FragmentStateManager { newCanonicalFragments[key] = behavior.pushData(current, canonical); }); - if (calculateChange) { - changedFragmentKeys = this._changedFragmentKeys( - identifier, - newCanonicalFragments, - ); - } + // Always calculate which keys changed so we can notify observers + changedFragmentKeys = this._changedFragmentKeys( + identifier, + newCanonicalFragments, + ); Object.assign(fragmentData, newCanonicalFragments); - changedFragmentKeys?.forEach((key) => { + + // Always notify for changed keys so computed properties are invalidated + changedFragmentKeys.forEach((key) => { // Notify the storeWrapper that the fragment attribute changed this.__storeWrapper.notifyChange(identifier, 'attributes', key); const arrayCache = this.__fragmentArrayCache.get(identifier.lid); @@ -839,7 +891,7 @@ export default class FragmentStateManager { }); } - return changedFragmentKeys || []; + return calculateChange ? changedFragmentKeys || [] : []; } willCommitFragments(identifier) { @@ -968,6 +1020,16 @@ export default class FragmentStateManager { } unloadFragments(identifier) { + // Guard against store being destroyed during teardown + if (this.store.isDestroying || this.store.isDestroyed) { + // Just clear our internal data structures + this.__fragments.delete(identifier.lid); + this.__inFlightFragments.delete(identifier.lid); + this.__fragmentData.delete(identifier.lid); + this.__fragmentArrayCache.delete(identifier.lid); + return; + } + const behaviors = this._getBehaviors(identifier); const fragments = this.__fragments.get(identifier.lid) || {}; const inFlight = this.__inFlightFragments.get(identifier.lid) || {}; @@ -1059,6 +1121,28 @@ export default class FragmentStateManager { _notifyStateChange(identifier, key) { this.__storeWrapper.notifyChange(identifier, 'attributes', key); + // Also notify the record instance directly to invalidate computed property caches + if (key) { + try { + const record = this.store._instanceCache.getRecord(identifier); + if (record && typeof record.notifyPropertyChange === 'function') { + record.notifyPropertyChange(key); + // Also clear any cached value by directly removing from Ember's cache + // This is needed because computed setters cache their return value + // and notifyPropertyChange alone may not clear it in all Ember versions + const meta = record.constructor.metaForProperty?.(key); + if (meta) { + // Force the computed property to re-evaluate by clearing its cache entry + const cache = record['__ember_meta__']?.peekCache?.(key); + if (cache !== undefined) { + record['__ember_meta__']?.deleteFromCache?.(key); + } + } + } + } catch { + // Record may not be instantiated yet + } + } } // Fragment lifecycle methods using cache API directly @@ -1086,7 +1170,9 @@ export default class FragmentStateManager { const inFlightValues = {}; for (const [key, definition] of Object.entries(definitions)) { - if (!definition.isFragment) { + const isFragmentAttr = + definition.isFragment || definition.options?.isFragment; + if (!isFragmentAttr) { // getAttr returns dirty value if exists, else canonical inFlightValues[key] = innerCache.getAttr(identifier, key); } @@ -1127,7 +1213,9 @@ export default class FragmentStateManager { // Get current values (may include new dirty changes made during in-flight) const currentValues = {}; for (const [key, definition] of Object.entries(definitions)) { - if (!definition.isFragment) { + const isFragmentAttr = + definition.isFragment || definition.options?.isFragment; + if (!isFragmentAttr) { currentValues[key] = innerCache.getAttr(identifier, key); } } @@ -1140,7 +1228,9 @@ export default class FragmentStateManager { const newDirtyAttrs = {}; for (const [key, definition] of Object.entries(definitions)) { - if (definition.isFragment) continue; + const isFragmentAttr = + definition.isFragment || definition.options?.isFragment; + if (isFragmentAttr) continue; // Determine the new canonical value const canonicalValue = diff --git a/addon/ext.js b/addon/ext.js index 08e6d388..641450e6 100644 --- a/addon/ext.js +++ b/addon/ext.js @@ -1,151 +1,9 @@ -import { assert } from '@ember/debug'; -import Store from '@ember-data/store'; import Model from '@ember-data/model'; -// eslint-disable-next-line ember/use-ember-data-rfc-395-imports -import { Snapshot } from 'ember-data/-private'; -import { dasherize } from '@ember/string'; -import JSONSerializer from '@ember-data/serializer/json'; -import FragmentCache from './cache/fragment-cache'; -import { default as Fragment } from './fragment'; -import { isPresent } from '@ember/utils'; -import { getOwner } from '@ember/application'; - -function serializerForFragment(owner, normalizedModelName) { - let serializer = owner.lookup(`serializer:${normalizedModelName}`); - - if (serializer !== undefined) { - return serializer; - } - - // no serializer found for the specific model, fallback and check for application serializer - serializer = owner.lookup('serializer:-fragment'); - if (serializer !== undefined) { - return serializer; - } - - // final fallback, no model specific serializer, no application serializer, no - // `serializer` property on store: use json-api serializer - serializer = owner.lookup('serializer:-default'); - - return serializer; -} +import { Snapshot } from '@ember-data/legacy-compat/-private'; /** @module ember-data-model-fragments */ -/** - @class Store - @namespace DS -*/ -Store.reopen({ - /** - * Override createCache to return our FragmentCache - * This is the V2 Cache hook introduced in ember-data 4.7+ - */ - createCache(storeWrapper) { - return new FragmentCache(storeWrapper); - }, - - /** - * Override teardownRecord to handle fragments in a disconnected state. - * In ember-data 4.12+, fragments can end up disconnected during unload, - * and the default teardownRecord fails when trying to destroy them. - */ - teardownRecord(record) { - // Check if record is a fragment (by checking if it has no id or by model type) - // We need to handle the case where the fragment's store is disconnected - if (record.isDestroyed || record.isDestroying) { - return; - } - try { - record.destroy(); - } catch (e) { - // If the error is about disconnected state, just let it go - // The fragment will be cleaned up by ember's garbage collection - if ( - e?.message?.includes?.('disconnected state') || - e?.message?.includes?.('cannot utilize the store') - ) { - return; - } - throw e; - } - }, - - /** - Create a new fragment that does not yet have an owner record. - The properties passed to this method are set on the newly created - fragment. - - To create a new instance of the `name` fragment: - - ```js - store.createFragment('name', { - first: 'Alex', - last: 'Routé' - }); - ``` - - @method createFragment - @param {String} type - @param {Object} properties a hash of properties to set on the - newly created fragment. - @return {MF.Fragment} fragment - */ - createFragment(modelName, props) { - assert( - `The '${modelName}' model must be a subclass of MF.Fragment`, - this.isFragment(modelName), - ); - // Create a new identifier for the fragment - const identifier = this.identifierCache.createIdentifierForNewRecord({ - type: modelName, - }); - // Signal to cache that this is a new record - this.cache.clientDidCreate(identifier, props || {}); - // Get the record instance - return this._instanceCache.getRecord(identifier, props); - }, - - /** - Returns true if the modelName is a fragment, false if not - - @method isFragment - @private - @param {String} the modelName to check if a fragment - @return {boolean} - */ - isFragment(modelName) { - if (modelName === 'application' || modelName === '-default') { - return false; - } - - const type = this.modelFor(modelName); - return Fragment.detect(type); - }, - - serializerFor(modelName) { - // this assertion is cargo-culted from ember-data TODO: update comment - assert( - "You need to pass a model name to the store's serializerFor method", - isPresent(modelName), - ); - assert( - `Passing classes to store.serializerFor has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string', - ); - - const owner = getOwner(this); - const normalizedModelName = dasherize(modelName); - - if (this.isFragment(normalizedModelName)) { - return serializerForFragment(owner, normalizedModelName); - } else { - return this._super(...arguments); - } - }, -}); - /** Override `Snapshot._attributes` to snapshot fragment attributes before they are passed to the `DS.Model#serialize`. @@ -157,89 +15,47 @@ const oldSnapshotAttributes = Object.getOwnPropertyDescriptor( '_attributes', ); +// Symbol to store our converted attributes cache +const FRAGMENT_ATTRS = Symbol('fragmentAttrs'); + Object.defineProperty(Snapshot.prototype, '_attributes', { get() { - const attrs = oldSnapshotAttributes.get.call(this); - Object.keys(attrs).forEach((key) => { - const attr = attrs[key]; + // Return cached converted attrs if available + if (this[FRAGMENT_ATTRS]) { + return this[FRAGMENT_ATTRS]; + } + + const cachedAttrs = oldSnapshotAttributes.get.call(this); + + // Create a new object to avoid modifying the cached __attributes in place + // This is needed because ember-data caches __attributes and reuses it + const attrs = Object.create(null); + + Object.keys(cachedAttrs).forEach((key) => { + const attr = cachedAttrs[key]; + // If the attribute has a `_createSnapshot` method, invoke it before the // snapshot gets passed to the serializer if (attr && typeof attr._createSnapshot === 'function') { attrs[key] = attr._createSnapshot(); - } - // Handle arrays of fragments (fragment arrays) - else if (Array.isArray(attr)) { + } else if (Array.isArray(attr)) { + // Handle arrays of fragments (fragment arrays) attrs[key] = attr.map((item) => { if (item && typeof item._createSnapshot === 'function') { return item._createSnapshot(); } return item; }); + } else { + attrs[key] = attr; } }); - return attrs; - }, -}); -/** - @class JSONSerializer - @namespace DS -*/ -JSONSerializer.reopen({ - /** - Enables fragment properties to have custom transforms based on the fragment - type, so that deserialization does not have to happen on the fly - - @method transformFor - @private - */ - transformFor(attributeType) { - if (attributeType.indexOf('-mf-') !== 0) { - return this._super(...arguments); - } - - const owner = getOwner(this); - const containerKey = `transform:${attributeType}`; - - if (!owner.hasRegistration(containerKey)) { - const match = attributeType.match( - /^-mf-(fragment|fragment-array|array)(?:\$([^$]+))?(?:\$(.+))?$/, - ); - assert( - `Failed parsing ember-data-model-fragments attribute type ${attributeType}`, - match != null, - ); - const transformName = match[1]; - const type = match[2]; - const polymorphicTypeProp = match[3]; - let transformClass = owner.factoryFor(`transform:${transformName}`); - transformClass = transformClass && transformClass.class; - transformClass = transformClass.extend({ - type, - polymorphicTypeProp, - store: this.store, - }); - owner.register(containerKey, transformClass); - } - return owner.lookup(containerKey); - }, - - // We need to override this to handle polymorphic with a typeKey function - applyTransforms(typeClass, data) { - const attributes = typeClass.attributes; - - typeClass.eachTransformedAttribute((key, typeClass) => { - if (data[key] === undefined) { - return; - } + // Cache the converted attrs + this[FRAGMENT_ATTRS] = attrs; - const transform = this.transformFor(typeClass); - const transformMeta = attributes.get(key); - data[key] = transform.deserialize(data[key], transformMeta.options, data); - }); - - return data; + return attrs; }, }); -export { Store, Model, JSONSerializer }; +export { Model }; diff --git a/addon/fragment.js b/addon/fragment.js index 8b1ed584..73f775e6 100644 --- a/addon/fragment.js +++ b/addon/fragment.js @@ -30,14 +30,17 @@ export function fragmentRecordDataFor(fragment) { Example: ```javascript - App.Person = DS.Model.extend({ - name: MF.fragment('name') - }); + import Model from '@ember-data/model'; + import MF from 'ember-data-model-fragments'; + + class Person extends Model { + @MF.fragment('name') name; + } - App.Name = MF.Fragment.extend({ - first : DS.attr('string'), - last : DS.attr('string') - }); + class Name extends MF.Fragment { + @attr('string') first; + @attr('string') last; + } ``` With JSON response: @@ -54,36 +57,39 @@ export function fragmentRecordDataFor(fragment) { ```javascript let person = store.getbyid('person', '1'); - let name = person.get('name'); + let name = person.name; - person.get('hasDirtyAttributes'); // false - name.get('hasDirtyAttributes'); // false - name.get('first'); // 'Robert' + person.hasDirtyAttributes; // false + name.hasDirtyAttributes; // false + name.first; // 'Robert' - name.set('first', 'The Animal'); - name.get('hasDirtyAttributes'); // true - person.get('hasDirtyAttributes'); // true + name.first = 'The Animal'; + name.hasDirtyAttributes; // true + person.hasDirtyAttributes; // true person.rollbackAttributes(); - name.get('first'); // 'Robert' - person.get('hasDirtyAttributes'); // false - person.get('hasDirtyAttributes'); // false + name.first; // 'Robert' + person.hasDirtyAttributes; // false + person.hasDirtyAttributes; // false ``` @class Fragment @namespace MF - @extends CoreModel + @extends Model @uses Ember.Comparable - @uses Copyable + @public */ +// Note: We use Model.extend() with Ember.Comparable mixin for now +// as mixins are being phased out but still work in ember-data 4.12+ const Fragment = Model.extend(Ember.Comparable, { /** Compare two fragments by identity to allow `FragmentArray` to diff arrays. @method compare - @param a {MF.Fragment} the first fragment to compare - @param b {MF.Fragment} the second fragment to compare - @return {Integer} the result of the comparison + @param {Fragment} f1 - The first fragment to compare + @param {Fragment} f2 - The second fragment to compare + @return {Integer} The result of the comparison (0 if equal, 1 if not) + @public */ compare(f1, f2) { return f1 === f2 ? 0 : 1; @@ -95,25 +101,65 @@ const Fragment = Model.extend(Ember.Comparable, { to other records safely. @method copy - @return {MF.Fragment} the newly created fragment + @return {Fragment} The newly created fragment + @public */ copy() { const type = this.constructor; const props = Object.create(null); const modelName = type.modelName || this._internalModel.modelName; + const identifier = recordIdentifierFor(this); - // Look up model via store to avoid schema access deprecation in ember-data 4.12+ - const modelClass = this.store.modelFor(modelName); + // Use schema service to get all attributes including fragment attributes + // eachAttribute only iterates standard @attr() properties, not fragment properties + const schemaService = this.store + .getSchemaDefinitionService() + .attributesDefinitionFor(identifier); // Loop over each attribute and copy individually to ensure nested fragments - // are also copied - modelClass.eachAttribute((name) => { - props[name] = copy(get(this, name)); - }); + // are also copied. For fragment attributes, we need to serialize to raw data + // since createFragment expects raw data, not fragment instances. + for (const name of Object.keys(schemaService)) { + const value = get(this, name); + const definition = schemaService[name]; + const isFragmentAttr = + definition?.isFragment || definition?.options?.isFragment; + + if (isFragmentAttr) { + // For fragment attributes, serialize to get raw data that can be used to create new fragments + if (value === null || value === undefined) { + props[name] = value; + } else if (typeof value.serialize === 'function') { + // Single fragment - serialize it + props[name] = value.serialize(); + } else if ( + Array.isArray(value) || + typeof value.toArray === 'function' + ) { + // Fragment array or array - serialize each element + const arr = + typeof value.toArray === 'function' ? value.toArray() : value; + props[name] = arr.map((item) => + typeof item.serialize === 'function' ? item.serialize() : item, + ); + } else { + // Fallback - use as-is + props[name] = value; + } + } else { + // Regular attribute - just copy the value + props[name] = copy(value); + } + } return this.store.createFragment(modelName, props); }, + /** + @method toStringExtension + @return {String} + @public + */ toStringExtension() { const identifier = recordIdentifierFor(this); const owner = this.store.cache.getFragmentOwner(identifier); @@ -123,6 +169,10 @@ const Fragment = Model.extend(Ember.Comparable, { /** Override toString to include the toStringExtension output. ember-data 4.12+ doesn't call toStringExtension in Model.toString(). + + @method toString + @return {String} + @public */ toString() { const identifier = recordIdentifierFor(this); @@ -130,18 +180,25 @@ const Fragment = Model.extend(Ember.Comparable, { const extensionStr = extension ? `:${extension}` : ''; return `<${identifier.type}:${identifier.id}${extensionStr}>`; }, -}).reopenClass({ - fragmentOwnerProperties: computed(function () { - const props = []; +}); - this.eachComputedProperty((name, meta) => { - if (meta.isFragmentOwner) { - props.push(name); - } - }); +// Add static property using native class syntax approach +// This replaces reopenClass which is deprecated +Object.defineProperty(Fragment, 'fragmentOwnerProperties', { + get() { + return computed(function () { + const props = []; + + this.eachComputedProperty((name, meta) => { + if (meta.isFragmentOwner) { + props.push(name); + } + }); - return props; - }).readOnly(), + return props; + }).readOnly(); + }, + configurable: true, }); /** @@ -180,7 +237,17 @@ export function setFragmentOwner(fragment, ownerRecordDataOrIdentifier, key) { // Notify any observers of `fragmentOwner` properties // Look up model via store to avoid schema access deprecation in ember-data 4.12+ const modelClass = fragment.store.modelFor(fragment.constructor.modelName); - modelClass.fragmentOwnerProperties.forEach((name) => { + + // Get the fragment owner properties array + // In 4.13+, we need to iterate computed properties directly since static property access may not work + const ownerProps = []; + modelClass.eachComputedProperty((name, meta) => { + if (meta.isFragmentOwner) { + ownerProps.push(name); + } + }); + + ownerProps.forEach((name) => { fragment.notifyPropertyChange(name); }); diff --git a/addon/index.js b/addon/index.js index 075a97dc..3d1754de 100644 --- a/addon/index.js +++ b/addon/index.js @@ -1,5 +1,4 @@ import Namespace from '@ember/application/namespace'; -import Ember from 'ember'; import VERSION from './version'; import Fragment from './fragment'; import FragmentArray from './array/fragment'; @@ -7,6 +6,12 @@ import FragmentTransform from './transforms/fragment'; import FragmentArrayTransform from './transforms/fragment-array'; import ArrayTransform from './transforms/array'; import { fragment, fragmentArray, array, fragmentOwner } from './attributes'; +import FragmentStore from './store'; +import FragmentSerializer, { + FragmentRESTSerializer, + FragmentJSONAPISerializer, +} from './serializer'; +import FragmentSchemaService from './schema-service'; /** Ember Data Model Fragments @@ -21,14 +26,15 @@ const MF = Namespace.create({ FragmentTransform: FragmentTransform, FragmentArrayTransform: FragmentArrayTransform, ArrayTransform: ArrayTransform, + FragmentStore: FragmentStore, + FragmentSerializer: FragmentSerializer, + FragmentRESTSerializer: FragmentRESTSerializer, + FragmentJSONAPISerializer: FragmentJSONAPISerializer, + FragmentSchemaService: FragmentSchemaService, fragment: fragment, fragmentArray: fragmentArray, array: array, fragmentOwner: fragmentOwner, }); -if (Ember.libraries) { - Ember.libraries.register('Model Fragments', MF.VERSION); -} - export default MF; diff --git a/addon/record-data.js b/addon/record-data.js deleted file mode 100644 index 4670d29c..00000000 --- a/addon/record-data.js +++ /dev/null @@ -1,1131 +0,0 @@ -// eslint-disable-next-line ember/use-ember-data-rfc-395-imports -import { RecordData } from 'ember-data/-private'; -import { diffArray } from '@ember-data/model/-private'; -import { recordDataFor } from '@ember-data/store/-private'; -import { assert } from '@ember/debug'; -import { typeOf } from '@ember/utils'; -import { isArray } from '@ember/array'; -import { getActualFragmentType, isFragment } from './fragment'; -import isInstanceOfType from './util/instance-of-type'; -import { gte } from 'ember-compatibility-helpers'; - -class FragmentBehavior { - constructor(recordData, definition) { - this.recordData = recordData; - this.definition = definition; - } - - getDefaultValue(key) { - const { options } = this.definition; - if (options.defaultValue === undefined) { - return null; - } - let defaultValue; - if (typeof options.defaultValue === 'function') { - const record = this.recordData._fragmentGetRecord(); - defaultValue = options.defaultValue.call(null, record, options, key); - } else { - defaultValue = options.defaultValue; - } - assert( - "The fragment's default value must be an object or null", - defaultValue === null || - typeOf(defaultValue) === 'object' || - isFragment(defaultValue), - ); - if (defaultValue === null) { - return null; - } - if (isFragment(defaultValue)) { - assert( - `The fragment's default value must be a '${this.definition.modelName}' fragment`, - isInstanceOfType( - defaultValue.store.modelFor(this.definition.modelName), - defaultValue, - ), - ); - const recordData = recordDataFor(defaultValue); - recordData.setFragmentOwner(this.recordData, key); - return recordData; - } - return this.recordData._newFragmentRecordData( - this.definition, - defaultValue, - ); - } - - pushData(fragment, canonical) { - assert( - 'Fragment value must be a RecordData', - fragment === null || fragment instanceof RecordData, - ); - assert( - 'Fragment canonical value must be an object or null', - canonical === null || typeOf(canonical) === 'object', - ); - - if (canonical === null) { - // server replaced fragment with null - return null; - } - - if (fragment) { - // merge the fragment with the new data from the server - fragment._fragmentPushData({ attributes: canonical }); - return fragment; - } - return this.recordData._newFragmentRecordData(this.definition, canonical); - } - - willCommit(fragment) { - assert( - 'Fragment value must be a RecordData', - fragment instanceof RecordData, - ); - fragment._fragmentWillCommit(); - } - - didCommit(fragment, canonical) { - assert( - 'Fragment value must be a RecordData', - fragment === null || fragment instanceof RecordData, - ); - assert( - 'Fragment canonical value must be an object', - canonical == null || typeOf(canonical) === 'object', - ); - - if (canonical == null) { - fragment?._fragmentDidCommit(null); - - if (canonical === null) { - // server replaced fragment with null - return null; - } - - // server confirmed in-flight fragment - return fragment; - } - - if (fragment) { - // merge the fragment with the new data from the server - fragment._fragmentDidCommit({ attributes: canonical }); - return fragment; - } - - return this.recordData._newFragmentRecordData(this.definition, canonical); - } - - commitWasRejected(fragment) { - assert( - 'Fragment value must be a RecordData', - fragment instanceof RecordData, - ); - fragment._fragmentCommitWasRejected(); - } - - rollback(fragment) { - assert( - 'Fragment value must be a RecordData', - fragment instanceof RecordData, - ); - fragment._fragmentRollbackAttributes(); - } - - unload(fragment) { - assert( - 'Fragment value must be a RecordData', - fragment instanceof RecordData, - ); - fragment._fragmentUnloadRecord(); - } - - isDirty(value, originalValue) { - assert( - 'Fragment value must be a RecordData', - value === null || value instanceof RecordData, - ); - assert( - 'Fragment original value must be a RecordData', - originalValue === null || originalValue instanceof RecordData, - ); - return ( - value !== originalValue || - (value !== null && value.hasChangedAttributes()) - ); - } - - currentState(fragment) { - assert( - 'Fragment value must be a RecordData', - fragment === null || fragment instanceof RecordData, - ); - return fragment === null ? null : fragment.getCurrentState(); - } - - canonicalState(fragment) { - assert( - 'Fragment value must be a RecordData', - fragment === null || fragment instanceof RecordData, - ); - return fragment === null ? null : fragment.getCanonicalState(); - } -} - -class FragmentArrayBehavior { - constructor(recordData, definition) { - this.recordData = recordData; - this.definition = definition; - } - - getDefaultValue(key) { - const { options } = this.definition; - if (options.defaultValue === undefined) { - return []; - } - let defaultValue; - if (typeof options.defaultValue === 'function') { - const record = this.recordData._fragmentGetRecord(); - defaultValue = options.defaultValue.call(null, record, options, key); - } else { - defaultValue = options.defaultValue; - } - assert( - "The fragment array's default value must be an array of fragments or null", - defaultValue === null || - (isArray(defaultValue) && - defaultValue.every((v) => typeOf(v) === 'object' || isFragment(v))), - ); - if (defaultValue === null) { - return null; - } - return defaultValue.map((item) => { - if (isFragment(item)) { - assert( - `The fragment array's default value can only include '${this.definition.modelName}' fragments`, - isInstanceOfType( - item.store.modelFor(this.definition.modelName), - item, - ), - ); - const recordData = recordDataFor(defaultValue); - recordData.setFragmentOwner(this.recordData, key); - return recordData; - } - return this.recordData._newFragmentRecordData(this.definition, item); - }); - } - - pushData(fragmentArray, canonical) { - assert( - 'Fragment array value must be an array of RecordData', - fragmentArray === null || - (isArray(fragmentArray) && - fragmentArray.every((rd) => rd instanceof RecordData)), - ); - assert( - 'Fragment array canonical value must be an array of objects', - canonical === null || - (isArray(canonical) && canonical.every((v) => typeOf(v) === 'object')), - ); - - if (canonical === null) { - // push replaced fragment array with null - return null; - } - - // merge the fragment array with the pushed data - return canonical.map((attributes, i) => { - const fragment = fragmentArray?.[i]; - if (fragment) { - fragment._fragmentPushData({ attributes }); - return fragment; - } else { - return this.recordData._newFragmentRecordData( - this.definition, - attributes, - ); - } - }); - } - - willCommit(fragmentArray) { - assert( - 'Fragment array value must be an array of RecordData', - isArray(fragmentArray) && - fragmentArray.every((rd) => rd instanceof RecordData), - ); - fragmentArray.forEach((fragment) => fragment._fragmentWillCommit()); - } - - didCommit(fragmentArray, canonical) { - assert( - 'Fragment array value must be an array of RecordData', - fragmentArray === null || - (isArray(fragmentArray) && - fragmentArray.every((rd) => rd instanceof RecordData)), - ); - assert( - 'Fragment array canonical value must be an array of objects', - canonical == null || - (isArray(canonical) && canonical.every((v) => typeOf(v) === 'object')), - ); - - if (canonical == null) { - fragmentArray?.forEach((fragment) => fragment._fragmentDidCommit(null)); - if (canonical === null) { - // server replaced fragment array with null - return null; - } - // server confirmed in-flight fragments - return fragmentArray; - } - - // merge the fragment array with the new data from the server - const result = canonical.map((attributes, i) => { - const fragment = fragmentArray?.[i]; - if (fragment) { - fragment._fragmentDidCommit({ attributes }); - return fragment; - } else { - return this.recordData._newFragmentRecordData( - this.definition, - attributes, - ); - } - }); - - // cleanup the remaining fragments - for (let i = canonical.length; i < fragmentArray?.length; ++i) { - fragmentArray[i]._fragmentDidCommit(null); - } - return result; - } - - commitWasRejected(fragmentArray) { - assert( - 'Fragment array value must be an array of RecordData', - isArray(fragmentArray) && - fragmentArray.every((rd) => rd instanceof RecordData), - ); - fragmentArray.forEach((fragment) => fragment._fragmentCommitWasRejected()); - } - - rollback(fragmentArray) { - assert( - 'Fragment array value must be an array of RecordData', - isArray(fragmentArray) && - fragmentArray.every((rd) => rd instanceof RecordData), - ); - fragmentArray.forEach((fragment) => fragment._fragmentRollbackAttributes()); - } - - unload(fragmentArray) { - assert( - 'Fragment array value must be an array of RecordData', - isArray(fragmentArray) && - fragmentArray.every((rd) => rd instanceof RecordData), - ); - fragmentArray.forEach((fragment) => fragment._fragmentUnloadRecord()); - } - - isDirty(value, originalValue) { - assert( - 'Fragment array value must be an array of RecordData', - value === null || - (isArray(value) && value.every((rd) => rd instanceof RecordData)), - ); - assert( - 'Fragment array original value must be an array of RecordData', - originalValue === null || - (isArray(originalValue) && - originalValue.every((rd) => rd instanceof RecordData)), - ); - return ( - !isArrayEqual(value, originalValue) || - (value !== null && value.some((rd) => rd.hasChangedAttributes())) - ); - } - - currentState(fragmentArray) { - assert( - 'Fragment array value must be an array of RecordData', - fragmentArray === null || - (isArray(fragmentArray) && - fragmentArray.every((rd) => rd instanceof RecordData)), - ); - return fragmentArray === null - ? null - : fragmentArray.map((fragment) => fragment.getCurrentState()); - } - - canonicalState(fragmentArray) { - assert( - 'Fragment array value must be an array of RecordData', - fragmentArray === null || - (isArray(fragmentArray) && - fragmentArray.every((rd) => rd instanceof RecordData)), - ); - return fragmentArray === null - ? null - : fragmentArray.map((fragment) => fragment.getCanonicalState()); - } -} - -class ArrayBehavior { - constructor(recordData, definition) { - this.recordData = recordData; - this.definition = definition; - } - - getDefaultValue(key) { - const { options } = this.definition; - if (options.defaultValue === undefined) { - return []; - } - let defaultValue; - if (typeof options.defaultValue === 'function') { - const record = this.recordData._fragmentGetRecord(); - defaultValue = options.defaultValue.call(null, record, options, key); - } else { - defaultValue = options.defaultValue; - } - assert( - "The array's default value must be an array or null", - defaultValue === null || isArray(defaultValue), - ); - if (defaultValue === null) { - return null; - } - return defaultValue.slice(); - } - - pushData(array, canonical) { - assert('Array value must be an array', array === null || isArray(array)); - assert( - 'Array canonical value must be an array', - canonical === null || isArray(canonical), - ); - if (canonical === null) { - return null; - } - return canonical.slice(); - } - - willCommit(array) { - assert('Array value must be an array', isArray(array)); - // nothing to do - } - - didCommit(array, canonical) { - assert('Array value must be an array', array === null || isArray(array)); - assert( - 'Array canonical value must be an array', - canonical === null || canonical === undefined || isArray(canonical), - ); - if (canonical === null) { - // server replaced array with null - return null; - } - if (canonical === undefined) { - // server confirmed in-flight array - return array; - } - // server returned new canonical data - return canonical.slice(); - } - - commitWasRejected(array) { - assert('Array value must be an array', isArray(array)); - // nothing to do - } - - rollback(array) { - assert('Array value must be an array', isArray(array)); - // nothing to do - } - - unload(array) { - assert('Array value must be an array', isArray(array)); - // nothing to do - } - - isDirty(value, originalValue) { - assert('Array value must be an array', value === null || isArray(value)); - assert( - 'Array original value must be an array', - originalValue === null || isArray(originalValue), - ); - return !isArrayEqual(value, originalValue); - } - - currentState(array) { - assert('Array value must be an array', array === null || isArray(array)); - return array === null ? null : array.slice(); - } - - canonicalState(array) { - assert('Array value must be an array', array === null || isArray(array)); - return array === null ? null : array.slice(); - } -} - -export default class FragmentRecordData extends RecordData { - constructor(identifier, storeWrapper) { - super(identifier, storeWrapper); - - const behavior = Object.create(null); - const definitions = this.storeWrapper.attributesDefinitionFor( - this.modelName, - ); - for (const [key, definition] of Object.entries(definitions)) { - if (!definition.isFragment) { - continue; - } - switch (definition.kind) { - case 'fragment-array': - behavior[key] = new FragmentArrayBehavior(this, definition); - break; - case 'fragment': - behavior[key] = new FragmentBehavior(this, definition); - break; - case 'array': - behavior[key] = new ArrayBehavior(this, definition); - break; - default: - assert(`Unsupported fragment type: ${definition.kind}`); - break; - } - } - this._fragmentBehavior = behavior; - } - - _getFragmentDefault(key) { - const behavior = this._fragmentBehavior[key]; - assert( - `Attribute '${key}' for model '${this.modelName}' must be a fragment`, - behavior != null, - ); - assert( - 'Fragment default value was already initialized', - !this.hasFragment(key), - ); - const defaultValue = behavior.getDefaultValue(key); - this._fragmentData[key] = defaultValue; - return defaultValue; - } - - getFragment(key) { - if (key in this._fragments) { - return this._fragments[key]; - } else if (key in this._inFlightFragments) { - return this._inFlightFragments[key]; - } else if (key in this._fragmentData) { - return this._fragmentData[key]; - } else { - return this._getFragmentDefault(key); - } - } - - hasFragment(key) { - return ( - key in this._fragments || - key in this._inFlightFragments || - key in this._fragmentData - ); - } - - setDirtyFragment(key, value) { - const behavior = this._fragmentBehavior[key]; - assert( - `Attribute '${key}' for model '${this.modelName}' must be a fragment`, - behavior != null, - ); - let originalValue; - if (key in this._inFlightFragments) { - originalValue = this._inFlightFragments[key]; - } else if (key in this._fragmentData) { - originalValue = this._fragmentData[key]; - } else { - originalValue = this._getFragmentDefault(key); - } - const isDirty = behavior.isDirty(value, originalValue); - const oldDirty = this.isFragmentDirty(key); - if (isDirty !== oldDirty) { - this.notifyStateChange(key); - } - if (isDirty) { - this._fragments[key] = value; - this.fragmentDidDirty(); - } else { - delete this._fragments[key]; - this.fragmentDidReset(); - } - // this._fragmentArrayCache[key]?.notify(); - } - - setDirtyAttribute(key, value) { - assert( - `Attribute '${key}' for model '${this.modelName}' must not be a fragment`, - this._fragmentBehavior[key] == null, - ); - const oldDirty = this.isAttrDirty(key); - super.setDirtyAttribute(key, value); - - const isDirty = this.isAttrDirty(key); - if (isDirty !== oldDirty) { - this.notifyStateChange(key); - } - if (isDirty) { - this.fragmentDidDirty(); - } else { - this.fragmentDidReset(); - } - } - - getFragmentOwner() { - return this._fragmentOwner?.recordData; - } - - setFragmentOwner(recordData, key) { - assert( - 'Fragment owner must be a RecordData', - recordData instanceof RecordData, - ); - assert( - 'Fragment owner key must be a fragment', - recordData._fragmentBehavior[key] != null, - ); - assert( - 'Fragments can only belong to one owner, try copying instead', - !this._fragmentOwner || this._fragmentOwner.recordData === recordData, - ); - this._fragmentOwner = { recordData, key }; - } - - _newFragmentRecordDataForKey(key, attributes) { - const behavior = this._fragmentBehavior[key]; - assert( - `Attribute '${key}' for model '${this.modelName}' must be a fragment`, - behavior != null, - ); - return this._newFragmentRecordData(behavior.definition, attributes); - } - - _newFragmentRecordData(definition, attributes) { - const type = getActualFragmentType( - definition.modelName, - definition.options, - attributes, - this._fragmentGetRecord(), - ); - const recordData = this.storeWrapper.recordDataFor(type); - recordData.setFragmentOwner(this, definition.name); - recordData._fragmentPushData({ attributes }); - return recordData; - } - - hasChangedAttributes() { - return super.hasChangedAttributes() || this.hasChangedFragments(); - } - - hasChangedFragments() { - return Object.keys(this._fragmentsOrInFlight).length > 0; - } - - isFragmentDirty(key) { - return this.__fragments?.[key] !== undefined; - } - - getCanonicalState() { - const result = Object.assign({}, this._data); - for (const [key, behavior] of Object.entries(this._fragmentBehavior)) { - const value = - key in this._fragmentData - ? this._fragmentData[key] - : this._getFragmentDefault(key); - result[key] = behavior.canonicalState(value); - } - return result; - } - - getCurrentState() { - const result = Object.assign( - {}, - this._data, - this._inFlightAttributes, - this._attributes, - ); - for (const [key, behavior] of Object.entries(this._fragmentBehavior)) { - result[key] = behavior.currentState(this.getFragment(key)); - } - return result; - } - - /* - Returns an object, whose keys are changed properties, and value is an - [oldProp, newProp] array. - - @method changedAttributes - @private - */ - changedAttributes() { - const result = super.changedAttributes(); - if (this.hasChangedFragments()) { - Object.assign(result, this.changedFragments()); - } - return result; - } - - changedFragments() { - const diffData = Object.create(null); - for (const [key, newFragment] of Object.entries( - this._fragmentsOrInFlight, - )) { - const behavior = this._fragmentBehavior[key]; - const oldFragment = this._fragmentData[key]; - diffData[key] = [ - behavior.canonicalState(oldFragment), - behavior.currentState(newFragment), - ]; - } - return diffData; - } - - _changedFragmentKeys(updates) { - const changedKeys = []; - const original = Object.assign( - {}, - this._fragmentData, - this._inFlightFragments, - ); - for (const key of Object.keys(updates)) { - if (this._fragments[key]) { - continue; - } - const eitherIsNull = original[key] === null || updates[key] === null; - if ( - eitherIsNull || - diffArray(original[key], updates[key]).firstChangeIndex !== null - ) { - changedKeys.push(key); - } - } - return changedKeys; - } - - pushData(data, calculateChange) { - let changedFragmentKeys; - - const subFragmentsToProcess = []; - if (data.attributes) { - // copy so that we don't mutate the caller's data - const attributes = Object.assign({}, data.attributes); - data = Object.assign({}, data, { attributes }); - - for (const [key, behavior] of Object.entries(this._fragmentBehavior)) { - const canonical = data.attributes[key]; - if (canonical === undefined) { - continue; - } - // strip fragments from the attributes so the super call does not process them - delete data.attributes[key]; - - subFragmentsToProcess.push({ key, behavior, canonical, attributes }); - } - } - - // Wee need first the attributes to be setup before the fragment, to be able to access them (for polymorphic fragments for example) - const changedAttributeKeys = super.pushData(data, calculateChange); - - if (data.attributes) { - const newCanonicalFragments = {}; - - subFragmentsToProcess.forEach(({ key, behavior, canonical }) => { - const current = - key in this._fragmentData - ? this._fragmentData[key] - : this._getFragmentDefault(key); - newCanonicalFragments[key] = behavior.pushData(current, canonical); - }); - - if (calculateChange) { - changedFragmentKeys = this._changedFragmentKeys(newCanonicalFragments); - } - - Object.assign(this._fragmentData, newCanonicalFragments); - // update fragment arrays - changedFragmentKeys?.forEach((key) => { - this._fragmentArrayCache[key]?.notify(); - }); - } - - const changedKeys = mergeArrays(changedAttributeKeys, changedFragmentKeys); - if (gte('ember-data', '4.5.0') && changedKeys?.length > 0) { - internalModelFor(this).notifyAttributes(changedKeys); - } - // on ember-data 2.8 - 4.4, InternalModel.setupData will notify - return changedKeys || []; - } - - willCommit() { - for (const [key, behavior] of Object.entries(this._fragmentBehavior)) { - const data = this.getFragment(key); - if (data) { - behavior.willCommit(data); - } - } - this._inFlightFragments = this._fragments; - this._fragments = null; - // this.notifyStateChange(); - super.willCommit(); - } - - /** - * Checks if the fragments which are considered as changed are still - * different to the state which is acknowledged by the server. - * - * This method is needed when data for the internal model is pushed and the - * pushed data might acknowledge dirty attributes as confirmed. - */ - _updateChangedFragments() { - for (const key of Object.keys(this._fragments)) { - const value = this._fragments[key]; - const originalValue = this._fragmentData[key]; - const behavior = this._fragmentBehavior[key]; - const isDirty = behavior.isDirty(value, originalValue); - if (!isDirty) { - delete this._fragments[key]; - } - } - } - - didCommit(data) { - if (data?.attributes) { - // copy so that we don't mutate the caller's data - const attributes = Object.assign({}, data.attributes); - data = Object.assign({}, data, { attributes }); - } - - const newCanonicalFragments = {}; - for (const [key, behavior] of Object.entries(this._fragmentBehavior)) { - let canonical; - if (data?.attributes) { - // strip fragments from the attributes so the super call does not process them - canonical = data.attributes[key]; - delete data.attributes[key]; - } - const fragment = - key in this._inFlightFragments - ? this._inFlightFragments[key] - : this._fragmentData[key]; - newCanonicalFragments[key] = behavior.didCommit(fragment, canonical); - } - - const changedFragmentKeys = this._changedFragmentKeys( - newCanonicalFragments, - ); - Object.assign(this._fragmentData, newCanonicalFragments); - this._inFlightFragments = null; - - this._updateChangedFragments(); - - const changedAttributeKeys = super.didCommit(data); - - // update fragment arrays - changedFragmentKeys.forEach((key) => { - this._fragmentArrayCache[key]?.notify(); - }); - - const changedKeys = mergeArrays(changedAttributeKeys, changedFragmentKeys); - if (gte('ember-data', '4.5.0') && changedKeys?.length > 0) { - internalModelFor(this).notifyAttributes(changedKeys); - } - // on ember-data 2.8 - 4.4, InternalModel.adapterDidCommit will notify - return changedKeys; - } - - commitWasRejected(identifier, errors) { - for (const [key, behavior] of Object.entries(this._fragmentBehavior)) { - const fragment = - key in this._inFlightFragments - ? this._inFlightFragments[key] - : this._fragmentData[key]; - if (fragment == null) { - continue; - } - behavior.commitWasRejected(fragment); - } - Object.assign(this._fragments, this._inFlightFragments); - this._inFlightFragments = null; - super.commitWasRejected(identifier, errors); - } - - rollbackAttributes() { - let dirtyFragmentKeys; - if (this.hasChangedFragments()) { - dirtyFragmentKeys = Object.keys(this._fragments); - dirtyFragmentKeys.forEach((key) => { - this.rollbackFragment(key); - }); - this._fragments = null; - } - const dirtyAttributeKeys = super.rollbackAttributes(); - this.notifyStateChange(); - this.fragmentDidReset(); - return mergeArrays(dirtyAttributeKeys, dirtyFragmentKeys); - } - - rollbackFragment(key) { - const behavior = this._fragmentBehavior[key]; - assert( - `Attribute '${key}' for model '${this.modelName}' must be a fragment`, - behavior != null, - ); - if (!this.isFragmentDirty(key)) { - return; - } - delete this._fragments[key]; - const fragment = this._fragmentData[key]; - if (fragment == null) { - return; - } - behavior.rollback(fragment); - this._fragmentArrayCache[key]?.notify(); - - if (!this.hasChangedAttributes()) { - this.notifyStateChange(key); - this.fragmentDidReset(); - } - } - - reset() { - super.reset(); - this.__fragments = null; - this.__inFlightFragments = null; - this.__fragmentData = null; - this.__fragmentArrayCache = null; - this._fragmentOwner = null; - } - - unloadRecord() { - for (const [key, behavior] of Object.entries(this._fragmentBehavior)) { - const fragment = this._fragments[key]; - if (fragment != null) { - behavior.unload(fragment); - } - const inFlight = this._inFlightFragments[key]; - if (inFlight != null) { - behavior.unload(inFlight); - } - const fragmentData = this._fragmentData[key]; - if (fragmentData != null) { - behavior.unload(fragmentData); - } - this._fragmentArrayCache[key]?.destroy(); - } - super.unloadRecord(); - } - - /** - * When a fragment becomes dirty, update the dirty state of the fragment's owner - */ - fragmentDidDirty() { - assert('Fragment is not dirty', this.hasChangedAttributes()); - if (!this._fragmentOwner) { - return; - } - const { recordData: owner, key } = this._fragmentOwner; - if (owner.isFragmentDirty(key)) { - // fragment owner is already dirty - return; - } - assert( - `Fragment '${key}' in owner '${owner.modelName}' has not been initialized`, - key in owner._fragmentData, - ); - - owner._fragments[key] = owner._fragmentData[key]; - owner.notifyStateChange(key); - owner.fragmentDidDirty(); - } - - /** - * When a fragment becomes clean, update the fragment owner - */ - fragmentDidReset() { - if (!this._fragmentOwner) { - return; - } - const { recordData: owner, key } = this._fragmentOwner; - if (!owner.isFragmentDirty(key)) { - // fragment owner is already clean - return; - } - - const behavior = owner._fragmentBehavior[key]; - const value = owner.getFragment(key); - const originalValue = owner._fragmentData[key]; - const isDirty = behavior.isDirty(value, originalValue); - - if (isDirty) { - // fragment is still dirty - return; - } - - delete owner._fragments[key]; - owner.notifyStateChange(key); - owner.fragmentDidReset(); - } - - notifyStateChange(key) { - if (key && gte('ember-data', '4.5.0')) { - this.storeWrapper.notifyPropertyChange( - this.modelName, - this.id, - this.clientId, - key, - ); - } else { - this.storeWrapper.notifyStateChange( - this.modelName, - this.id, - this.clientId, - key, - ); - } - } - - /* - * Ensure that any changes to the fragment record-data also update the InternalModel's - * state machine and fire property change notifications on the Record - */ - - _fragmentGetRecord(properties) { - if (gte('ember-data', '4.5.0')) { - return this.storeWrapper._store._instanceCache.getRecord( - this.identifier, - properties, - ); - } - return internalModelFor(this).getRecord(properties); - } - _fragmentPushData(data) { - internalModelFor(this).setupData(data); - } - _fragmentWillCommit() { - internalModelFor(this).adapterWillCommit(); - } - _fragmentDidCommit(data) { - internalModelFor(this).adapterDidCommit(data); - } - _fragmentRollbackAttributes() { - internalModelFor(this).rollbackAttributes(); - } - _fragmentCommitWasRejected() { - internalModelFor(this).adapterDidInvalidate(); - } - _fragmentUnloadRecord() { - internalModelFor(this).unloadRecord(); - } - - /** - * The current dirty state - */ - get _fragments() { - if (this.__fragments === null) { - this.__fragments = Object.create(null); - } - return this.__fragments; - } - - set _fragments(v) { - this.__fragments = v; - } - - /** - * The current saved state - */ - get _fragmentData() { - if (this.__fragmentData === null) { - this.__fragmentData = Object.create(null); - } - return this.__fragmentData; - } - - set _fragmentData(v) { - this.__fragmentData = v; - } - - /** - * The fragments which are currently being saved to the backend - */ - get _inFlightFragments() { - if (this.__inFlightFragments === null) { - this.__inFlightFragments = Object.create(null); - } - return this.__inFlightFragments; - } - - set _inFlightFragments(v) { - this.__inFlightFragments = v; - } - - get _fragmentsOrInFlight() { - return this.__inFlightFragments && - Object.keys(this.__inFlightFragments).length > 0 - ? this.__inFlightFragments - : this.__fragments || {}; - } - - /** - * Fragment array instances - * - * This likely belongs in InternalModel, since ember-data caches its has-many - * arrays in `InternalModel#_manyArrayCache`. But we can't extend InternalModel. - */ - get _fragmentArrayCache() { - if (this.__fragmentArrayCache === null) { - this.__fragmentArrayCache = Object.create(null); - } - return this.__fragmentArrayCache; - } -} - -function internalModelFor(recordData) { - const store = recordData.storeWrapper._store; - if (gte('ember-data', '4.5.0')) { - return store._instanceCache._internalModelForResource( - recordData.identifier, - ); - } - return store._internalModelForResource(recordData.identifier); -} - -function isArrayEqual(a, b) { - if (a === b) { - return true; - } - if (a === null || b === null) { - return false; - } - if (a.length !== b.length) { - return false; - } - for (let i = 0; i < a.length; ++i) { - if (a[i] !== b[i]) { - return false; - } - } - return true; -} - -function mergeArrays(a, b) { - if (b == null) { - return a; - } - if (a == null) { - return b; - } - return [...a, ...b]; -} diff --git a/addon/schema-service.js b/addon/schema-service.js new file mode 100644 index 00000000..d1eb0711 --- /dev/null +++ b/addon/schema-service.js @@ -0,0 +1,119 @@ +import { + macroCondition, + dependencySatisfies, + importSync, +} from '@embroider/macros'; + +/** + * FragmentSchemaService extends ModelSchemaProvider to add support for fragment attributes. + * + * In ember-data 4.13+, the schema service only recognizes attributes with `kind: 'attribute'`. + * Fragment attributes use `kind: 'fragment'`, `kind: 'fragment-array'`, or `kind: 'array'`, + * so they need to be transformed to be included in the schema. + * + * This class overrides `_loadModelSchema()` to: + * 1. Call the parent implementation to load standard attributes and relationships + * 2. Scan the model for fragment attributes + * 3. Transform fragment metadata to be recognized by the schema service + * 4. Add transformed fragments to the schema + * + * NOTE: This class only exists in ember-data 4.13+. For 4.12, this module exports null. + * + * @class FragmentSchemaService + * @extends ModelSchemaProvider + * @public + */ +let FragmentSchemaService = null; + +if (macroCondition(dependencySatisfies('ember-data', '>=4.13.0-alpha.0'))) { + const { ModelSchemaProvider } = importSync('@ember-data/model'); + + FragmentSchemaService = class FragmentSchemaService extends ( + ModelSchemaProvider + ) { + /** + * Override _loadModelSchema to include fragment attributes in the schema. + * + * The parent implementation only includes attributes where `meta.kind === 'attribute'`. + * We need to also include fragment attributes and transform them to be compatible. + * + * @method _loadModelSchema + * @param {String} type - The model type name + * @return {Object} internalSchema - The schema with attributes, relationships, and fields + * @private + */ + _loadModelSchema(type) { + // Call parent implementation to get standard schema (attributes + relationships) + const internalSchema = super._loadModelSchema(type); + + // Get the model class to scan for fragment attributes + const modelClass = this.store.modelFor(type); + + // Scan computed properties for fragment attributes + modelClass.eachComputedProperty((name, meta) => { + if (this._isFragmentAttribute(meta)) { + // Transform fragment metadata to be recognized as an attribute + // while preserving fragment-specific information + const transformedMeta = { + name, + key: name, // ember-data expects this + kind: 'attribute', // Change to 'attribute' so schema service recognizes it + // Keep the original type for transform lookup + type: meta.type, + options: { + ...meta.options, + isFragment: true, // Mark as fragment in options + fragmentKind: meta.kind, // Preserve original kind + modelName: meta.modelName, + }, + isAttribute: true, // Required by ember-data + isFragment: true, // Preserve for our code + modelName: meta.modelName, // Preserve model name at top level + }; + + // Add to all three schema structures: + // 1. attributes object (used by attributesDefinitionFor - legacy API) + internalSchema.attributes[name] = transformedMeta; + + // 2. fields Map (used by fields() - new API) + internalSchema.fields.set(name, transformedMeta); + + // 3. schema.fields array (used by resource() - new API) + // Note: We'll rebuild this array after the loop to avoid duplicates + } + }); + + // Rebuild schema.fields array from the fields Map + // This ensures it includes all attributes, relationships, AND fragments + internalSchema.schema.fields = Array.from(internalSchema.fields.values()); + + return internalSchema; + } + + /** + * Check if a computed property metadata object represents a fragment attribute. + * + * Fragment attributes have: + * - isFragment: true + * - kind: 'fragment', 'fragment-array', or 'array' + * + * @method _isFragmentAttribute + * @param {Object} meta - The computed property metadata + * @return {Boolean} + * @private + */ + _isFragmentAttribute(meta) { + return ( + typeof meta === 'object' && + meta !== null && + 'kind' in meta && + meta.isFragment === true && + (meta.kind === 'fragment' || + meta.kind === 'fragment-array' || + meta.kind === 'array') + ); + } + }; +} + +export default FragmentSchemaService; diff --git a/addon/serializer.js b/addon/serializer.js new file mode 100644 index 00000000..71f6bae4 --- /dev/null +++ b/addon/serializer.js @@ -0,0 +1,21 @@ +/** + * Re-exports for ember-data-model-fragments serializers. + * + * Usage: + * + * ```js + * // For JSONSerializer (default) + * import FragmentSerializer from 'ember-data-model-fragments/serializer'; + * + * // For RESTSerializer + * import { FragmentRESTSerializer } from 'ember-data-model-fragments/serializer'; + * + * // For JSONAPISerializer + * import { FragmentJSONAPISerializer } from 'ember-data-model-fragments/serializer'; + * ``` + */ + +export { default } from './serializers/fragment'; +export { default as FragmentSerializer } from './serializers/fragment'; +export { default as FragmentRESTSerializer } from './serializers/rest'; +export { default as FragmentJSONAPISerializer } from './serializers/json-api'; diff --git a/addon/serializers/fragment.js b/addon/serializers/fragment.js new file mode 100644 index 00000000..aa5432b7 --- /dev/null +++ b/addon/serializers/fragment.js @@ -0,0 +1,76 @@ +import JSONSerializer from '@ember-data/serializer/json'; +import { + fragmentTransformFor, + fragmentApplyTransforms, + fragmentExtractAttributes, +} from './utils'; + +/** + FragmentSerializer is the base serializer class for ember-data-model-fragments. + Extends JSONSerializer. + + To use fragment serialization properly, your serializers should extend FragmentSerializer: + + ```js + // app/serializers/application.js + import FragmentSerializer from 'ember-data-model-fragments/serializer'; + + export default class ApplicationSerializer extends FragmentSerializer {} + ``` + + @class FragmentSerializer + @extends JSONSerializer + @public +*/ +export default class FragmentSerializer extends JSONSerializer { + /** + Enables fragment properties to have custom transforms based on the fragment + type, so that deserialization does not have to happen on the fly + + @method transformFor + @param {String} attributeType - The attribute type to get the transform for + @return {Transform} + @public + */ + transformFor(attributeType) { + return fragmentTransformFor( + this, + attributeType, + JSONSerializer.prototype.transformFor, + ); + } + + /** + Override applyTransforms to handle polymorphic fragments with a typeKey function + + @method applyTransforms + @param {Class} typeClass - The model class + @param {Object} data - The data to apply transforms to + @return {Object} The transformed data + @public + */ + applyTransforms(typeClass, data) { + return fragmentApplyTransforms(this, typeClass, data); + } + + /** + Override extractAttributes to include fragment attributes. + The default implementation only iterates modelClass.eachAttribute which + doesn't include fragment attributes (they're computed properties with + isFragment: true metadata). + + @method extractAttributes + @param {Class} modelClass - The model class + @param {Object} resourceHash - The raw resource data from the server + @return {Object} The extracted attributes + @public + */ + extractAttributes(modelClass, resourceHash) { + return fragmentExtractAttributes( + this, + modelClass, + resourceHash, + JSONSerializer.prototype.extractAttributes, + ); + } +} diff --git a/addon/serializers/json-api.js b/addon/serializers/json-api.js new file mode 100644 index 00000000..28068bf9 --- /dev/null +++ b/addon/serializers/json-api.js @@ -0,0 +1,76 @@ +import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { + fragmentTransformFor, + fragmentApplyTransforms, + fragmentExtractAttributesJSONAPI, +} from './utils'; + +/** + FragmentJSONAPISerializer is the base serializer class for ember-data-model-fragments + when using JSONAPISerializer. + + ```js + // app/serializers/application.js + import { FragmentJSONAPISerializer } from 'ember-data-model-fragments/serializer'; + + export default class ApplicationSerializer extends FragmentJSONAPISerializer {} + ``` + + @class FragmentJSONAPISerializer + @extends JSONAPISerializer + @public +*/ +export default class FragmentJSONAPISerializer extends JSONAPISerializer { + /** + Enables fragment properties to have custom transforms based on the fragment + type, so that deserialization does not have to happen on the fly + + @method transformFor + @param {String} attributeType - The attribute type to get the transform for + @return {Transform} + @public + */ + transformFor(attributeType) { + return fragmentTransformFor( + this, + attributeType, + JSONAPISerializer.prototype.transformFor, + ); + } + + /** + Override applyTransforms to handle polymorphic fragments with a typeKey function + + @method applyTransforms + @param {Class} typeClass - The model class + @param {Object} data - The data to apply transforms to + @return {Object} The transformed data + @public + */ + applyTransforms(typeClass, data) { + return fragmentApplyTransforms(this, typeClass, data); + } + + /** + Override extractAttributes to include fragment attributes. + The default implementation only iterates modelClass.eachAttribute which + doesn't include fragment attributes (they're computed properties with + isFragment: true metadata). + + For JSON:API, attributes are nested under resourceHash.attributes. + + @method extractAttributes + @param {Class} modelClass - The model class + @param {Object} resourceHash - The raw resource data from the server + @return {Object} The extracted attributes + @public + */ + extractAttributes(modelClass, resourceHash) { + return fragmentExtractAttributesJSONAPI( + this, + modelClass, + resourceHash, + JSONAPISerializer.prototype.extractAttributes, + ); + } +} diff --git a/addon/serializers/rest.js b/addon/serializers/rest.js new file mode 100644 index 00000000..b0c17dba --- /dev/null +++ b/addon/serializers/rest.js @@ -0,0 +1,74 @@ +import RESTSerializer from '@ember-data/serializer/rest'; +import { + fragmentTransformFor, + fragmentApplyTransforms, + fragmentExtractAttributes, +} from './utils'; + +/** + FragmentRESTSerializer is the base serializer class for ember-data-model-fragments + when using RESTSerializer. + + ```js + // app/serializers/application.js + import { FragmentRESTSerializer } from 'ember-data-model-fragments/serializer'; + + export default class ApplicationSerializer extends FragmentRESTSerializer {} + ``` + + @class FragmentRESTSerializer + @extends RESTSerializer + @public +*/ +export default class FragmentRESTSerializer extends RESTSerializer { + /** + Enables fragment properties to have custom transforms based on the fragment + type, so that deserialization does not have to happen on the fly + + @method transformFor + @param {String} attributeType - The attribute type to get the transform for + @return {Transform} + @public + */ + transformFor(attributeType) { + return fragmentTransformFor( + this, + attributeType, + RESTSerializer.prototype.transformFor, + ); + } + + /** + Override applyTransforms to handle polymorphic fragments with a typeKey function + + @method applyTransforms + @param {Class} typeClass - The model class + @param {Object} data - The data to apply transforms to + @return {Object} The transformed data + @public + */ + applyTransforms(typeClass, data) { + return fragmentApplyTransforms(this, typeClass, data); + } + + /** + Override extractAttributes to include fragment attributes. + The default implementation only iterates modelClass.eachAttribute which + doesn't include fragment attributes (they're computed properties with + isFragment: true metadata). + + @method extractAttributes + @param {Class} modelClass - The model class + @param {Object} resourceHash - The raw resource data from the server + @return {Object} The extracted attributes + @public + */ + extractAttributes(modelClass, resourceHash) { + return fragmentExtractAttributes( + this, + modelClass, + resourceHash, + RESTSerializer.prototype.extractAttributes, + ); + } +} diff --git a/addon/serializers/utils.js b/addon/serializers/utils.js new file mode 100644 index 00000000..183f84e0 --- /dev/null +++ b/addon/serializers/utils.js @@ -0,0 +1,186 @@ +import { assert } from '@ember/debug'; +import { getOwner } from '@ember/application'; + +/** + * Helper function to implement fragment transform lookup. + * Used by FragmentSerializer, FragmentRESTSerializer, and FragmentJSONAPISerializer. + * + * @param {Serializer} serializer - The serializer instance + * @param {String} attributeType - The attribute type to get the transform for + * @param {Function} superTransformFor - The parent class's transformFor method + * @return {Transform} + * @private + */ +export function fragmentTransformFor( + serializer, + attributeType, + superTransformFor, +) { + if (attributeType.indexOf('-mf-') !== 0) { + return superTransformFor.call(serializer, attributeType); + } + + const owner = getOwner(serializer); + const containerKey = `transform:${attributeType}`; + + if (!owner.hasRegistration(containerKey)) { + const match = attributeType.match( + /^-mf-(fragment|fragment-array|array)(?:\$([^$]+))?(?:\$(.+))?$/, + ); + assert( + `Failed parsing ember-data-model-fragments attribute type ${attributeType}`, + match != null, + ); + const transformName = match[1]; + const type = match[2]; + const polymorphicTypeProp = match[3]; + let transformClass = owner.factoryFor(`transform:${transformName}`); + transformClass = transformClass && transformClass.class; + transformClass = transformClass.extend({ + type, + polymorphicTypeProp, + store: serializer.store, + }); + owner.register(containerKey, transformClass); + } + return owner.lookup(containerKey); +} + +/** + * Helper function to implement fragment-aware applyTransforms. + * Used by FragmentSerializer, FragmentRESTSerializer, and FragmentJSONAPISerializer. + * + * @param {Serializer} serializer - The serializer instance + * @param {Class} typeClass - The model class + * @param {Object} data - The data to apply transforms to + * @return {Object} The transformed data + * @private + */ +export function fragmentApplyTransforms(serializer, typeClass, data) { + const attributes = typeClass.attributes; + + // Handle regular @attr transforms + typeClass.eachTransformedAttribute((key, attrType) => { + if (data[key] === undefined) { + return; + } + + const transform = serializer.transformFor(attrType); + const transformMeta = attributes.get(key); + data[key] = transform.deserialize(data[key], transformMeta.options, data); + }); + + // Also handle @array computed properties with transforms + // These are not in eachTransformedAttribute because they're computed properties, + // not regular @attr attributes + typeClass.eachComputedProperty((key, meta) => { + if (data[key] === undefined) { + return; + } + + // Only handle @array attributes that have a transform type (arrayTransform) + // meta.arrayTransform is the child transform type (e.g., 'string') + // meta.type is the full transform type string (e.g., '-mf-array$string') + if (meta?.isFragment && meta.kind === 'array' && meta.arrayTransform) { + // Use the full transform type string from meta.type + const transform = serializer.transformFor(meta.type); + data[key] = transform.deserialize(data[key], meta.options, data); + } + }); + + return data; +} + +/** + * Helper function to extract attributes including fragment attributes. + * The default extractAttributes only iterates modelClass.eachAttribute which + * doesn't include fragment attributes (they're computed properties). + * + * Used by FragmentSerializer and FragmentRESTSerializer. + * + * @param {Serializer} serializer - The serializer instance + * @param {Class} modelClass - The model class + * @param {Object} resourceHash - The raw resource data from the server + * @param {Function} superExtractAttributes - The parent's extractAttributes method + * @return {Object} The extracted attributes + * @private + */ +export function fragmentExtractAttributes( + serializer, + modelClass, + resourceHash, + superExtractAttributes, +) { + // First, call parent to get regular attributes + const attributes = superExtractAttributes.call( + serializer, + modelClass, + resourceHash, + ); + + // Then, add fragment attributes + modelClass.eachComputedProperty((key, meta) => { + if ( + meta && + meta.isFragment && + (meta.kind === 'fragment' || + meta.kind === 'fragment-array' || + meta.kind === 'array') + ) { + const attributeKey = serializer.keyForAttribute(key, 'deserialize'); + if (resourceHash[attributeKey] !== undefined) { + attributes[key] = resourceHash[attributeKey]; + } + } + }); + + return attributes; +} + +/** + * Helper function to extract attributes including fragment attributes for JSON:API. + * JSON:API serializers have attributes nested under resourceHash.attributes. + * + * Used by FragmentJSONAPISerializer. + * + * @param {Serializer} serializer - The serializer instance + * @param {Class} modelClass - The model class + * @param {Object} resourceHash - The raw resource data from the server + * @param {Function} superExtractAttributes - The parent's extractAttributes method + * @return {Object} The extracted attributes + * @private + */ +export function fragmentExtractAttributesJSONAPI( + serializer, + modelClass, + resourceHash, + superExtractAttributes, +) { + // First, call parent to get regular attributes + const attributes = superExtractAttributes.call( + serializer, + modelClass, + resourceHash, + ); + + // For JSON:API serializers, attributes are nested under resourceHash.attributes + const attrHash = resourceHash.attributes || resourceHash; + + // Then, add fragment attributes + modelClass.eachComputedProperty((key, meta) => { + if ( + meta && + meta.isFragment && + (meta.kind === 'fragment' || + meta.kind === 'fragment-array' || + meta.kind === 'array') + ) { + const attributeKey = serializer.keyForAttribute(key, 'deserialize'); + if (attrHash[attributeKey] !== undefined) { + attributes[key] = attrHash[attributeKey]; + } + } + }); + + return attributes; +} diff --git a/addon/store.js b/addon/store.js new file mode 100644 index 00000000..6a8a762b --- /dev/null +++ b/addon/store.js @@ -0,0 +1,162 @@ +import { assert } from '@ember/debug'; +import Store from 'ember-data/store'; +import { + macroCondition, + dependencySatisfies, + importSync, +} from '@embroider/macros'; +import FragmentCache from './cache/fragment-cache'; +import { default as Fragment } from './fragment'; + +// Import side-effects to ensure monkey-patches are applied +// These must be imported before any store instances are created +import './ext'; // Applies Snapshot monkey-patch for fragment serialization + +/** + FragmentStore is the base store class for ember-data-model-fragments. + + To use this addon, you must create an application store service that extends FragmentStore: + + ```js + // app/services/store.js + import FragmentStore from 'ember-data-model-fragments/store'; + + export default class extends FragmentStore {} + ``` + + Your application serializer should also extend one of the fragment-aware serializers: + + ```js + // app/serializers/application.js + import FragmentSerializer from 'ember-data-model-fragments/serializer'; + + export default class extends FragmentSerializer {} + ``` + + @class FragmentStore + @extends Store + @public +*/ +export default class FragmentStore extends Store { + /** + * Override createCache to return our FragmentCache + * This is the V2 Cache hook introduced in ember-data 4.7+ + * + * @method createCache + * @param {Object} storeWrapper + * @return {FragmentCache} + * @public + */ + createCache(storeWrapper) { + return new FragmentCache(storeWrapper); + } + + /** + * Override createSchemaService to provide fragment-aware schema for ember-data 4.13+ + * + * In ember-data 4.13, a new schema service architecture was introduced that only + * recognizes attributes with `kind: 'attribute'`. Fragment attributes use different + * kinds ('fragment', 'fragment-array', 'array'), so they need special handling. + * + * This method is only called in ember-data 4.13+. For ember-data 4.12, this method + * doesn't exist in the Store class, so it's never invoked. + * + * The `macroCondition` ensures build-time optimization: + * - For 4.12 builds: This code is completely removed (tree-shaking) + * - For 4.13 builds: Only the FragmentSchemaService path remains + * + * @method createSchemaService + * @return {FragmentSchemaService|undefined} + * @public + */ + createSchemaService() { + if (macroCondition(dependencySatisfies('ember-data', '>=4.13.0-alpha.0'))) { + const FragmentSchemaService = importSync('./schema-service').default; + return new FragmentSchemaService(this); + } + // For ember-data 4.12, this method is never called (doesn't exist in Store base class) + return undefined; + } + + /** + * Override teardownRecord to handle fragments in a disconnected state. + * In ember-data 4.12+, fragments can end up disconnected during unload, + * and the default teardownRecord fails when trying to destroy them. + * + * @method teardownRecord + * @param {Model} record + * @public + */ + teardownRecord(record) { + // Check if record is a fragment (by checking if it has no id or by model type) + // We need to handle the case where the fragment's store is disconnected + if (record.isDestroyed || record.isDestroying) { + return; + } + try { + record.destroy(); + } catch (e) { + // If the error is about disconnected state, just let it go + // The fragment will be cleaned up by ember's garbage collection + if ( + e?.message?.includes?.('disconnected state') || + e?.message?.includes?.('cannot utilize the store') + ) { + return; + } + throw e; + } + } + + /** + Create a new fragment that does not yet have an owner record. + The properties passed to this method are set on the newly created + fragment. + + To create a new instance of the `name` fragment: + + ```js + store.createFragment('name', { + first: 'Alex', + last: 'Routé' + }); + ``` + + @method createFragment + @param {String} modelName - The type of fragment to create + @param {Object} props - A hash of properties to set on the newly created fragment + @return {Fragment} fragment + @public + */ + createFragment(modelName, props) { + assert( + `The '${modelName}' model must be a subclass of MF.Fragment`, + this.isFragment(modelName), + ); + // Create a new identifier for the fragment + const identifier = this.identifierCache.createIdentifierForNewRecord({ + type: modelName, + }); + // Signal to cache that this is a new record + this.cache.clientDidCreate(identifier, props || {}); + // Get the record instance + return this._instanceCache.getRecord(identifier, props); + } + + /** + Returns true if the modelName is a fragment, false if not + + @method isFragment + @param {String} modelName - The modelName to check if a fragment + @return {Boolean} + @public + */ + isFragment(modelName) { + if (modelName === 'application' || modelName === '-default') { + return false; + } + + const type = this.modelFor(modelName); + return Fragment.detect(type); + } +} diff --git a/app/initializers/model-fragments.js b/app/initializers/model-fragments.js deleted file mode 100644 index 465ade1b..00000000 --- a/app/initializers/model-fragments.js +++ /dev/null @@ -1,11 +0,0 @@ -// Import the full module to ensure monkey-patches are applied before any store -// instances are created. Sad face for side-effects :( -import 'ember-data-model-fragments'; -import 'ember-data-model-fragments/ext'; - -export default { - name: 'fragmentTransform', - after: 'ember-data', - - initialize() {}, -}; diff --git a/package.json b/package.json index 291c5c7e..4a6a6e59 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "broccoli-merge-trees": "^3.0.0", "calculate-cache-key-for-tree": "^1.1.0", "ember-cli-babel": "^8.2.0", - "ember-compatibility-helpers": "^1.2.7", "ember-template-imports": "^4.3.0", "git-repo-info": "^2.1.1", "npm-git-info": "^1.0.3" @@ -55,7 +54,10 @@ "@babel/eslint-parser": "^7.27.1", "@babel/plugin-proposal-decorators": "^7.27.1", "@babel/preset-env": "^7.27.1", - "@ember-data/json-api": "~4.12.0", + "@ember-data/json-api": "~4.13.0-alpha.9", + "@ember-data/model": "~4.13.0-alpha.9", + "@ember-data/serializer": "~4.13.0-alpha.9", + "@ember-data/store": "~4.13.0-alpha.9", "@ember/optional-features": "^2.2.0", "@ember/string": "^3.1.1", "@ember/test-helpers": "^5.2.1", @@ -78,7 +80,8 @@ "ember-cli-htmlbars": "^6.3.0", "ember-cli-inject-live-reload": "^2.1.0", "ember-cli-terser": "^4.0.2", - "ember-data": "~4.12.0", + "ember-data": "~4.13.0-alpha.9", + "ember-inflector": "^6.0.0", "ember-load-initializers": "^3.0.1", "ember-page-title": "^9.0.1", "ember-qunit": "^9.0.2", @@ -106,9 +109,17 @@ }, "peerDependencies": { "@ember-data/json-api": ">= 4.12.0", + "@ember-data/model": ">= 4.12.0", + "@ember-data/serializer": ">= 4.12.0", + "@ember-data/store": ">= 4.12.0", "ember-data": ">= 4.12.0", "ember-source": ">= 5.0.0" }, + "pnpm": { + "onlyBuiltDependencies": [ + "core-js" + ] + }, "engines": { "node": ">= 18" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17531483..5a8fc0fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,7 +1,7 @@ lockfileVersion: '9.0' settings: - autoInstallPeers: true + autoInstallPeers: false excludeLinksFromLockfile: false overrides: @@ -27,9 +27,6 @@ importers: ember-cli-babel: specifier: ^8.2.0 version: 8.2.0(@babel/core@7.27.4) - ember-compatibility-helpers: - specifier: ^1.2.7 - version: 1.2.7(@babel/core@7.27.4) ember-template-imports: specifier: ^4.3.0 version: 4.3.0 @@ -50,8 +47,17 @@ importers: specifier: ^7.27.1 version: 7.27.2(@babel/core@7.27.4)(supports-color@8.1.1) '@ember-data/json-api': - specifier: ~4.12.0 - version: 4.12.8(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) + specifier: ~4.13.0-alpha.9 + version: 4.13.0-alpha.9(@ember-data/graph@4.13.0-alpha.9(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) + '@ember-data/model': + specifier: ~4.13.0-alpha.9 + version: 4.13.0-alpha.9(28d7247ae793456ad31c1824cc98f4f3) + '@ember-data/serializer': + specifier: ~4.13.0-alpha.9 + version: 4.13.0-alpha.9(@ember-data/legacy-compat@4.13.0-alpha.9(c7ad85e53b9587cb1a558efa86bed54f))(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) + '@ember-data/store': + specifier: ~4.13.0-alpha.9 + version: 4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) '@ember/optional-features': specifier: ^2.2.0 version: 2.2.0 @@ -119,8 +125,11 @@ importers: specifier: ^4.0.2 version: 4.0.2 ember-data: - specifier: ~4.12.0 - version: 4.12.8(@babel/core@7.27.4)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9))(webpack@5.99.9) + specifier: ~4.13.0-alpha.9 + version: 4.13.0-alpha.9(@ember/string@3.1.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.4))(@ember/test-waiters@3.1.0)(ember-inflector@6.0.0(@babel/core@7.27.4))(qunit@2.24.1) + ember-inflector: + specifier: ^6.0.0 + version: 6.0.0(@babel/core@7.27.4) ember-load-initializers: specifier: ^3.0.1 version: 3.0.1(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)) @@ -156,7 +165,7 @@ importers: version: 12.5.0(@babel/core@7.27.4)(eslint@9.28.0) eslint-plugin-n: specifier: ^17.17.0 - version: 17.19.0(eslint@9.28.0)(typescript@5.8.3) + version: 17.19.0(eslint@9.28.0) eslint-plugin-qunit: specifier: ^8.1.2 version: 8.1.2(eslint@9.28.0) @@ -186,10 +195,10 @@ importers: version: 0.17.4 stylelint: specifier: ^16.19.1 - version: 16.20.0(typescript@5.8.3) + version: 16.20.0 stylelint-config-standard: specifier: ^36.0.1 - version: 36.0.1(stylelint@16.20.0(typescript@5.8.3)) + version: 36.0.1(stylelint@16.20.0) webpack: specifier: ^5.0.0 version: 5.99.9 @@ -817,110 +826,119 @@ packages: '@dual-bundle/import-meta-resolve@4.1.0': resolution: {integrity: sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==} - '@ember-data/adapter@4.12.8': - resolution: {integrity: sha512-HIwLGUkAXPbOfCw/vt1Xi5a3/J/sV4tT0LVsB/HPo+m0h/ztSmrfCQVRJCzZUP3ACeOL+eGeMQt4zyz8RfZazw==} - engines: {node: 16.* || >= 18.*} - peerDependencies: - '@ember-data/store': 4.12.8 - '@ember/string': ^3.0.1 - ember-inflector: ^4.0.2 - - '@ember-data/debug@4.12.8': - resolution: {integrity: sha512-dA2VXsO8OPddZ723oQxLbjQVoWMpVuqhskBgaf8kRNmJI9ru8AxhR6KWJaF2LMeJ3VhI5ujo1rNfOC2Y1t/chw==} - engines: {node: 16.* || >= 18.*} - peerDependencies: - '@ember-data/store': 4.12.8 - '@ember/string': ^3.0.1 - - '@ember-data/graph@4.12.8': - resolution: {integrity: sha512-Nm297TOVsOvIqnzRPclW3YL+ILgpz00Rc5Z5KNk1Je3RP8+02uA7Sh39p5WG9YQr6rz3+xY5jd1VbmIoLOQiaA==} - engines: {node: 16.* || >= 18.*} - peerDependencies: - '@ember-data/store': 4.12.8 - - '@ember-data/json-api@4.12.8': - resolution: {integrity: sha512-A5ann76wOeRXeRPOG8wrWQn4BK+yb7T1l6Ybm1eSgkFQeNVvVc/eM6ejcRospQInSRZnOJZCPHYd+wggZgpXGA==} - engines: {node: 16.* || >= 18.*} - peerDependencies: - '@ember-data/graph': 4.12.8 - '@ember-data/store': 4.12.8 - - '@ember-data/legacy-compat@4.12.8': - resolution: {integrity: sha512-sMC+QWdA+oMFtGH1UvwK2UU/iua29s298SSftRP9M84JAqr7t8AWfZd73m1CWe9aboyYKe1KXOCfPUsgrSICCg==} - engines: {node: 16.* || >= 18} - peerDependencies: - '@ember-data/graph': 4.12.8 - '@ember-data/json-api': 4.12.8 - '@ember/string': ^3.0.1 + '@ember-data/adapter@4.13.0-alpha.9': + resolution: {integrity: sha512-AcA1wPg0gKoVUTFAtPSMwhWBQtcZm2qixb/FZfhAqmLoZX6kcWrOKzE/leSXjkc0JHnRP7Ra+U2SWrEbhAtCSA==} + engines: {node: '>= 18.20.7'} + peerDependencies: + '@ember-data/legacy-compat': 4.13.0-alpha.9 + '@ember-data/request-utils': 4.13.0-alpha.9 + '@ember-data/store': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 + + '@ember-data/debug@4.13.0-alpha.9': + resolution: {integrity: sha512-f4Z7LiKfCCwB6yUczEOKOfa2XEZgakKQogwMZq+JCpONbVfGNvKFgCpn1Jx9eBnv3R31yyA9feqi1om1zk03Lg==} + engines: {node: '>= 18.20.7'} + peerDependencies: + '@ember-data/model': 4.13.0-alpha.9 + '@ember-data/request-utils': 4.13.0-alpha.9 + '@ember-data/store': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 + + '@ember-data/graph@4.13.0-alpha.9': + resolution: {integrity: sha512-gkMEm6SDqa7QzGafIPQBg8e/fOgSHO6mknd8XRT/+Q6fSYC/gFjmsJ0TH8JmN1Y7jCRX2Jzm9w3v8CbU04JPvw==} + engines: {node: '>= 18.20.7'} + peerDependencies: + '@ember-data/store': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 + + '@ember-data/json-api@4.13.0-alpha.9': + resolution: {integrity: sha512-+DJryvQaBT2AUmfwwLhR3+muBmltBYStaIGtf5IcM5UAhrr8EqsWJ8GDS8IT5arz7/cmTLLQLtD2jl0KVHwPmA==} + engines: {node: '>= 18.20.7'} + peerDependencies: + '@ember-data/graph': 4.13.0-alpha.9 + '@ember-data/request-utils': 4.13.0-alpha.9 + '@ember-data/store': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 + + '@ember-data/legacy-compat@4.13.0-alpha.9': + resolution: {integrity: sha512-AW8TGBSAClD20WPG1rNUDfIGfG9cQV43gY+SW1s+JGs1BE53oaK7z1gfeS5zkujvp3lcVD3h7lkGfUq06wMS5A==} + engines: {node: '>= 18.20.7'} + peerDependencies: + '@ember-data/graph': 4.13.0-alpha.9 + '@ember-data/json-api': 4.13.0-alpha.9 + '@ember-data/request': 4.13.0-alpha.9 + '@ember-data/request-utils': 4.13.0-alpha.9 + '@ember-data/store': 4.13.0-alpha.9 + '@ember/test-waiters': ^3.1.0 || >= 4.0.0 + '@warp-drive/core-types': 4.13.0-alpha.9 peerDependenciesMeta: '@ember-data/graph': optional: true '@ember-data/json-api': optional: true - '@ember-data/model@4.12.8': - resolution: {integrity: sha512-rJQVri/mrZIdwmonVqbHVsCI+xLvW5CClnlXLiHCBDpoq/klXJ6u5FMglH64GAEpjuIfWKiygdOvMGiaYFJt+A==} - engines: {node: 16.* || >= 18.*} - peerDependencies: - '@ember-data/debug': 4.12.8 - '@ember-data/graph': 4.12.8 - '@ember-data/json-api': 4.12.8 - '@ember-data/legacy-compat': 4.12.8 - '@ember-data/store': 4.12.8 - '@ember-data/tracking': 4.12.8 - '@ember/string': ^3.0.1 - ember-inflector: ^4.0.2 + '@ember-data/model@4.13.0-alpha.9': + resolution: {integrity: sha512-VryeVeXtWWGh0jw3GAtnI+PHDVOPZNuquIJOIMfudXlefpNNOcCsi+05TDXIUFO6r5BB/S7Wym5pR06/+oxvrQ==} + engines: {node: '>= 18.20.7'} + peerDependencies: + '@ember-data/graph': 4.13.0-alpha.9 + '@ember-data/json-api': 4.13.0-alpha.9 + '@ember-data/legacy-compat': 4.13.0-alpha.9 + '@ember-data/request-utils': 4.13.0-alpha.9 + '@ember-data/store': 4.13.0-alpha.9 + '@ember-data/tracking': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 peerDependenciesMeta: - '@ember-data/debug': - optional: true '@ember-data/graph': optional: true '@ember-data/json-api': optional: true - '@ember-data/private-build-infra@4.12.8': - resolution: {integrity: sha512-acOT5m5Bnq78IYcCjRoP9Loh65XNODFor+nThvH4IDmfaxNfKfr8Qheu4f23r5oPOXmHbcDBWRjsjs2dkaKTAw==} - engines: {node: 16.* || >= 18.*} + '@ember-data/request-utils@4.13.0-alpha.9': + resolution: {integrity: sha512-p3Dyjz3RrkGlTkmeUx4pB3Wy5NzkahfbwxTz0tihaeauJ6mcm5jssS9Q3286daXzcufZxVlA4xZFIMhgxloXGQ==} + engines: {node: '>= 18.20.7'} + peerDependencies: + '@ember/string': ^3.1.1 || ^4.0.0 + '@warp-drive/core-types': 4.13.0-alpha.9 + ember-inflector: ^4.0.2 || ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + '@ember/string': + optional: true + ember-inflector: + optional: true - '@ember-data/request@4.12.8': - resolution: {integrity: sha512-aTn+Cd5b901MGhLKRJdd/+xXrkp1GAmJEn55F8W2ojYk82rt2ZbO/Ppe2DWhTRMujj6vKclYhWJt0NNafnUobQ==} - engines: {node: 16.* || >= 18} + '@ember-data/request@4.13.0-alpha.9': + resolution: {integrity: sha512-tz2UdNHqAx+pBVB4yKLz0LLaDxALaDzsFQfdaIPtgjBh/haXG//kN0B5Ys2YOUAmQ/4ZBFXAyygF51ZDCJTwPw==} + engines: {node: '>= 18.20.7'} + peerDependencies: + '@warp-drive/core-types': 4.13.0-alpha.9 '@ember-data/rfc395-data@0.0.4': resolution: {integrity: sha512-tGRdvgC9/QMQSuSuJV45xoyhI0Pzjm7A9o/MVVA3HakXIImJbbzx/k/6dO9CUEQXIyS2y0fW6C1XaYOG7rY0FQ==} - '@ember-data/serializer@4.12.8': - resolution: {integrity: sha512-XKjSnq8jR1C8sFCZmdd1cTfV5THt1ykYDcDNo80pLoZaIosYtt1QVIVLq0puTjNXO/B8GyQl8DN2p/AS9fwbaw==} - engines: {node: 16.* || >= 18.*} - peerDependencies: - '@ember-data/store': 4.12.8 - '@ember/string': ^3.0.1 - ember-inflector: ^4.0.2 - - '@ember-data/store@4.12.8': - resolution: {integrity: sha512-pI+c/ZtRO5T02JcQ+yvUQsRZIIw/+fVUUnxa6mHiiNkjOJZaK8/2resdskSgV3SFGI82icanV7Ve5LJj9EzscA==} - engines: {node: 16.* || >= 18.*} - peerDependencies: - '@ember-data/graph': 4.12.8 - '@ember-data/json-api': 4.12.8 - '@ember-data/legacy-compat': 4.12.8 - '@ember-data/model': 4.12.8 - '@ember-data/tracking': 4.12.8 - '@ember/string': ^3.0.1 - '@glimmer/tracking': ^1.1.2 - peerDependenciesMeta: - '@ember-data/graph': - optional: true - '@ember-data/json-api': - optional: true - '@ember-data/legacy-compat': - optional: true - '@ember-data/model': - optional: true + '@ember-data/serializer@4.13.0-alpha.9': + resolution: {integrity: sha512-bM7QRQOSVLkLcuyCppMyDW5W09xgZD5Y9GSKucE4TYy/+NQ7Ajn1cabTDdlDhc3FXu932FWk5xyshcEL0Hpa1g==} + engines: {node: '>= 18.20.7'} + peerDependencies: + '@ember-data/legacy-compat': 4.13.0-alpha.9 + '@ember-data/request-utils': 4.13.0-alpha.9 + '@ember-data/store': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 - '@ember-data/tracking@4.12.8': - resolution: {integrity: sha512-CczHOsEbInbVg4WF2UQhV89gCnSfH+8ZR1WinPFQ8PaY6e1KSlPULuTXhC03NhAo8GaJzHlvc3KfATt5qgBplg==} - engines: {node: 16.* || >= 18} + '@ember-data/store@4.13.0-alpha.9': + resolution: {integrity: sha512-jZIyXJlwxPBvyHpfSQHEwAJeTPNf+P2McVFdVUi1PqtgTe915kWTIOnbUnPBaGLbHbQsunavUMu+w801G5dkkw==} + engines: {node: '>= 18.20.7'} + peerDependencies: + '@ember-data/request': 4.13.0-alpha.9 + '@ember-data/request-utils': 4.13.0-alpha.9 + '@ember-data/tracking': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 + + '@ember-data/tracking@4.13.0-alpha.9': + resolution: {integrity: sha512-OT/ruoVY7qRGWnAq1t5igHilw3sovHaxqKGT/Rxv3FWpKS9AK0P2rVzbmSp2IvjKFKfnx/WiM/wuN92DZP5vwg==} + engines: {node: '>= 18.20.7'} + peerDependencies: + '@warp-drive/core-types': 4.13.0-alpha.9 '@ember/edition-utils@1.2.0': resolution: {integrity: sha512-VmVq/8saCaPdesQmftPqbFtxJWrzxNGSQ+e8x8LLe3Hjm36pJ04Q8LeORGZkAeOhldoUX9seLGmSaHeXkIqoog==} @@ -1384,10 +1402,6 @@ packages: '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - '@types/broccoli-plugin@3.0.4': - resolution: {integrity: sha512-VfG0WydDHFr6MGj75U16bKxOnrl8uP9bXvq7VD+NuvnAq5/22cQDrf8o7BnzBJQt+Xm9jkPt1hh2EHVWluGYIA==} - deprecated: This is a stub types definition. broccoli-plugin provides its own type definitions, so you do not need this installed. - '@types/chai-as-promised@7.1.8': resolution: {integrity: sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==} @@ -1515,6 +1529,14 @@ packages: resolution: {integrity: sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@warp-drive/build-config@4.13.0-alpha.9': + resolution: {integrity: sha512-QxUX2lc6CCbN5xcCIQJXFVkT7meJ1GBD/BXQusMN6TL3sqL4NApGWrfhNa8vgJomB8eJmc22AYoMCSKTIphnGg==} + engines: {node: '>= 18.20.7'} + + '@warp-drive/core-types@4.13.0-alpha.9': + resolution: {integrity: sha512-SmXf01VPAuo4GDx0NcWWVZEAKow7FzyXv2NCCaSdWtPtPFNtbGbk6dWVMoK65wTmDEX1XJShI5Lw+3tcMFmzfA==} + engines: {node: '>= 18.20.7'} + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -1605,11 +1627,6 @@ packages: ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true ajv-keywords@3.5.2: resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} @@ -1799,10 +1816,6 @@ packages: resolution: {integrity: sha512-CtWYYHU/MgK88rxMrLfkD356dApswtR/kWZ/c6JifG1m10e7tBBrs/366dFzWMAoqYmG5/JSh+94tUSpIwh+ag==} engines: {node: '>= 12.*'} - babel-import-util@1.4.1: - resolution: {integrity: sha512-TNdiTQdPhXlx02pzG//UyVPSKE7SNWjY0n4So/ZnjQpWwaM5LvWBLkWa1JKll5u06HNscHD91XZPuwrMg1kadQ==} - engines: {node: '>= 12.*'} - babel-import-util@2.1.1: resolution: {integrity: sha512-3qBQWRjzP9NreSH/YrOEU1Lj5F60+pWSLP0kIdCWxjFHH7pX2YPHIxQ67el4gnMNfYoDxSDGcT0zpVlZ+gVtQA==} engines: {node: '>= 12.*'} @@ -1825,12 +1838,6 @@ packages: '@babel/core': ^7.12.0 webpack: '>=5' - babel-plugin-debug-macros@0.2.0: - resolution: {integrity: sha512-Wpmw4TbhR3Eq2t3W51eBAQSdKlr+uAyF0GI4GtPfMCD12Y4cIdpKC9l0RjNTH/P9isFypSqqewMPm7//fnZlNA==} - engines: {node: '>=4'} - peerDependencies: - '@babel/core': ^7.0.0-beta.42 - babel-plugin-debug-macros@0.3.4: resolution: {integrity: sha512-wfel/vb3pXfwIDZUrkoDrn5FHmlWI96PCJ3UCDv2a86poJ3EQrnArNW5KfHSVJ9IOgxHbo748cQt7sDU+0KCEw==} engines: {node: '>=6'} @@ -1853,10 +1860,6 @@ packages: resolution: {integrity: sha512-n+ktQ3JeyWrpRutSyPn2PsHeH+A94SVm+iUoogzf9VUqpP47FfWem24gpQXhn+p6+x5/BpuFJXMLXWt7ZoYAKA==} engines: {node: '>= 12.*'} - babel-plugin-filter-imports@4.0.0: - resolution: {integrity: sha512-jDLlxI8QnfKd7PtieH6pl4tZJzymzfCDCPGdTq/grgbiYAikwDPp/oL0IlFJn0HQjLpcLkyYhPKkUVneRESw5w==} - engines: {node: '>=8'} - babel-plugin-htmlbars-inline-precompile@5.3.1: resolution: {integrity: sha512-QWjjFgSKtSRIcsBhJmEwS2laIdrA6na8HAlc/pEAhjHgQsah/gMiBFRZvbQTy//hWxR4BMwV7/Mya7q5H8uHeA==} engines: {node: 10.* || >= 12.*} @@ -1889,9 +1892,6 @@ packages: babel-remove-types@1.0.1: resolution: {integrity: sha512-au+oEGwCCxqb8R0x8EwccTVtWCP4lFkNpHV5skNZnNCwvar3DBBkmGZbx2B1A3RaCHVLQrxF6qv6rR/ZDRPW+A==} - babel6-plugin-strip-class-callcheck@6.0.0: - resolution: {integrity: sha512-biNFJ7JAK4+9BwswDGL0dmYpvXHvswOFR/iKg3Q/f+pNxPEa5bWZkLHI1fW4spPytkHGMe7f/XtYyhzml9hiWg==} - babylon@6.18.0: resolution: {integrity: sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==} hasBin: true @@ -2068,10 +2068,6 @@ packages: resolution: {integrity: sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg==} engines: {node: 10.* || >= 12.*} - broccoli-rollup@5.0.0: - resolution: {integrity: sha512-QdMuXHwsdz/LOS8zu4HP91Sfi4ofimrOXoYP/lrPdRh7lJYD87Lfq4WzzUhGHsxMfzANIEvl/7qVHKD3cFJ4tA==} - engines: {node: '>=12.0'} - broccoli-slow-trees@3.1.0: resolution: {integrity: sha512-FRI7mRTk2wjIDrdNJd6znS7Kmmne4VkAkl8Ix1R/VoePFMD0g0tEl671xswzFqaRjpT9Qu+CC4hdXDLDJBuzMw==} @@ -2822,16 +2818,6 @@ packages: resolution: {integrity: sha512-bcBFDYVTFHyqyq8BNvsj6UO3pE6Uqou/cNmee0WaqBgZ+1nQqFz0UE26usrtnFAT+YaFZSkqF2H36QW84k0/cg==} engines: {node: 12.* || 14.* || >= 16} - ember-cache-primitive-polyfill@1.0.1: - resolution: {integrity: sha512-hSPcvIKarA8wad2/b6jDd/eU+OtKmi6uP+iYQbzi5TQpjsqV6b4QdRqrLk7ClSRRKBAtdTuutx+m+X+WlEd2lw==} - engines: {node: 10.* || >= 12} - - ember-cached-decorator-polyfill@1.0.2: - resolution: {integrity: sha512-hUX6OYTKltAPAu8vsVZK02BfMTV0OUXrPqvRahYPhgS7D0I6joLjlskd7mhqJMcaXLywqceIy8/s+x8bxF8bpQ==} - engines: {node: 14.* || >= 16} - peerDependencies: - ember-source: ^3.13.0 || ^4.0.0 || >= 5.0.0 - ember-cli-babel-plugin-helpers@1.1.1: resolution: {integrity: sha512-sKvOiPNHr5F/60NLd7SFzMpYPte/nnGkq/tMIfXejfKHIhaiIkYFqX8Z9UFTKWLLn+V7NOaby6niNPZUdvKCRw==} engines: {node: 6.* || 8.* || >= 10.*} @@ -2916,15 +2902,20 @@ packages: engines: {node: '>= 18'} hasBin: true - ember-compatibility-helpers@1.2.7: - resolution: {integrity: sha512-BtkjulweiXo9c3yVWrtexw2dTmBrvavD/xixNC6TKOBdrixUwU+6nuOO9dufDWsMxoid7MvtmDpzc9+mE8PdaA==} - engines: {node: 10.* || >= 12.*} - - ember-data@4.12.8: - resolution: {integrity: sha512-fK9mp+chqXGWYx6lal/azBKP4AtW8E6u3xUUWet6henO2zPN4S5lRs6iBfaynPkmhW5DK5bvaxNmFvSzmPOghw==} - engines: {node: 16.* || >= 18.*} + ember-data@4.13.0-alpha.9: + resolution: {integrity: sha512-3lAg1z3uvsGjui8om4Y9mQ+q3+YYxwZN/0BpCQSxSqrKPbWe3tMs3BOp93r/+Aeq55AJfflsfvWjl0KyouRVoA==} + engines: {node: '>= 18.20.7'} peerDependencies: - '@ember/string': ^3.0.1 + '@ember/test-helpers': ^3.3.0 || ^4.0.4 || ^5.1.0 + '@ember/test-waiters': ^3.1.0 || ^4.0.0 + qunit: ^2.18.0 + peerDependenciesMeta: + '@ember/test-helpers': + optional: true + '@ember/test-waiters': + optional: true + qunit: + optional: true ember-eslint-parser@0.5.9: resolution: {integrity: sha512-IW4/3cEiFp49M2LiKyzi7VcT1egogOe8UxQ9eUKTooenC7Q4qNhzTD6rOZ8j51m8iJC+8hCzjbNCa3K4CN0Hhg==} @@ -2936,11 +2927,8 @@ packages: '@typescript-eslint/parser': optional: true - ember-inflector@4.0.3: - resolution: {integrity: sha512-E+NnmzybMRWn1JyEfDxY7arjOTJLIcGjcXnUxizgjD4TlvO1s3O65blZt+Xq2C2AFSPeqHLC6PXd6XHYM8BxdQ==} - engines: {node: 14.* || 16.* || >= 18} - peerDependencies: - ember-source: ^3.16.0 || ^4.0.0 || ^5.0.0 + ember-inflector@6.0.0: + resolution: {integrity: sha512-g6trqBhQHRwlq9bBmoyxhAl0tD0/CaTKK0xWPUgi3BfxFOgGG1bbiwAx+tjyiAkLzDqU+ihyjtT+sd41y6K1hA==} ember-load-initializers@3.0.1: resolution: {integrity: sha512-qV3vxJKw5+7TVDdtdLPy8PhVsh58MlK8jwzqh5xeOwJPNP7o0+BlhvwoIlLYTPzGaHdfjEIFCgVSyMRGd74E1g==} @@ -3230,9 +3218,6 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} - estree-walker@0.6.1: - resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} - esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -3563,11 +3548,6 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -3942,6 +3922,10 @@ packages: resolution: {integrity: sha512-wzkZHqpb4eGrOKBl34xy3umnYHx8Si5R1U4fwmdxLo5gdH6mEK8gclckTj/qWqy4Je0bsDYe/qazZYuO7xe3XQ==} engines: {node: '>=14.0.0'} + inflection@3.0.2: + resolution: {integrity: sha512-+Bg3+kg+J6JUWn8J6bzFmOWkTQ6L/NHfDRSYU+EVvuKHDxUDHAXgqixHfVlzuBQaPOTac8hn43aPhMNk6rMe3g==} + engines: {node: '>=18.0.0'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -4794,9 +4778,6 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-modules-path@1.0.2: - resolution: {integrity: sha512-6Gbjq+d7uhkO7epaKi5DNgUJn7H0gEyA4Jg0Mo1uQOi3Rk50G83LtmhhFyw0LxnAFhtlspkiiw52ISP13qzcBg==} - node-notifier@10.0.1: resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==} @@ -5535,14 +5516,6 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rollup-pluginutils@2.8.2: - resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} - - rollup@2.79.2: - resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==} - engines: {node: '>=10.0.0'} - hasBin: true - route-recognizer@0.3.4: resolution: {integrity: sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g==} @@ -6234,11 +6207,6 @@ packages: typescript-memoize@1.1.1: resolution: {integrity: sha512-GQ90TcKpIH4XxYTI2F98yEQYZgjNMOGPpOgdjIBhaLaWji5HPWlRnZ4AeA1hfBxtY7bCGDJsqDDHk/KaHOl5bA==} - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} - engines: {node: '>=14.17'} - hasBin: true - uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -7388,175 +7356,150 @@ snapshots: '@dual-bundle/import-meta-resolve@4.1.0': {} - '@ember-data/adapter@4.12.8(@ember-data/store@4.12.8)(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)))': + '@ember-data/adapter@4.13.0-alpha.9(@ember-data/legacy-compat@4.13.0-alpha.9(c7ad85e53b9587cb1a558efa86bed54f))(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9)': dependencies: - '@ember-data/private-build-infra': 4.12.8 - '@ember-data/store': 4.12.8(@babel/core@7.27.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)) - '@ember/string': 3.1.1 + '@ember-data/legacy-compat': 4.13.0-alpha.9(c7ad85e53b9587cb1a558efa86bed54f) + '@ember-data/request-utils': 4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)) + '@ember-data/store': 4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) + '@ember/edition-utils': 1.2.0 '@embroider/macros': 1.19.6 - ember-cli-babel: 7.26.11 + '@warp-drive/build-config': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 + ember-cli-path-utils: 1.0.0 + ember-cli-string-utils: 1.1.0 ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)) transitivePeerDependencies: - '@glint/template' - supports-color - '@ember-data/debug@4.12.8(@ember-data/store@4.12.8)(@ember/string@3.1.1)(webpack@5.99.9)': + '@ember-data/debug@4.13.0-alpha.9(@ember-data/model@4.13.0-alpha.9(28d7247ae793456ad31c1824cc98f4f3))(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9)': dependencies: - '@ember-data/private-build-infra': 4.12.8 - '@ember-data/store': 4.12.8(@babel/core@7.27.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)) + '@ember-data/model': 4.13.0-alpha.9(28d7247ae793456ad31c1824cc98f4f3) + '@ember-data/request-utils': 4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)) + '@ember-data/store': 4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) '@ember/edition-utils': 1.2.0 - '@ember/string': 3.1.1 '@embroider/macros': 1.19.6 - ember-auto-import: 2.10.0(webpack@5.99.9) - ember-cli-babel: 7.26.11 + '@warp-drive/build-config': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 transitivePeerDependencies: - '@glint/template' - supports-color - - webpack - '@ember-data/graph@4.12.8(@ember-data/store@4.12.8)': + '@ember-data/graph@4.13.0-alpha.9(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9)': dependencies: - '@ember-data/private-build-infra': 4.12.8 - '@ember-data/store': 4.12.8(@babel/core@7.27.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)) - '@ember/edition-utils': 1.2.0 + '@ember-data/store': 4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) '@embroider/macros': 1.19.6 - ember-cli-babel: 7.26.11 + '@warp-drive/build-config': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 transitivePeerDependencies: - '@glint/template' - supports-color - '@ember-data/json-api@4.12.8(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8)': - dependencies: - '@ember-data/graph': 4.12.8(@ember-data/store@4.12.8) - '@ember-data/private-build-infra': 4.12.8 - '@ember-data/store': 4.12.8(@babel/core@7.27.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)) - '@ember/edition-utils': 1.2.0 + ? '@ember-data/json-api@4.13.0-alpha.9(@ember-data/graph@4.13.0-alpha.9(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9)' + : dependencies: + '@ember-data/graph': 4.13.0-alpha.9(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) + '@ember-data/request-utils': 4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)) + '@ember-data/store': 4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) '@embroider/macros': 1.19.6 - ember-cli-babel: 7.26.11 + '@warp-drive/build-config': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 transitivePeerDependencies: - '@glint/template' - supports-color - '@ember-data/legacy-compat@4.12.8(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember/string@3.1.1)': + '@ember-data/legacy-compat@4.13.0-alpha.9(c7ad85e53b9587cb1a558efa86bed54f)': dependencies: - '@ember-data/private-build-infra': 4.12.8 - '@ember/string': 3.1.1 + '@ember-data/request': 4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9) + '@ember-data/request-utils': 4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)) + '@ember-data/store': 4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) + '@ember/test-waiters': 3.1.0 '@embroider/macros': 1.19.6 - ember-cli-babel: 7.26.11 + '@warp-drive/build-config': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 optionalDependencies: - '@ember-data/graph': 4.12.8(@ember-data/store@4.12.8) - '@ember-data/json-api': 4.12.8(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) + '@ember-data/graph': 4.13.0-alpha.9(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) + '@ember-data/json-api': 4.13.0-alpha.9(@ember-data/graph@4.13.0-alpha.9(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) transitivePeerDependencies: - '@glint/template' - supports-color - '@ember-data/model@4.12.8(@babel/core@7.27.4)(@ember-data/debug@4.12.8)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)))(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9))': + '@ember-data/model@4.13.0-alpha.9(28d7247ae793456ad31c1824cc98f4f3)': dependencies: - '@ember-data/legacy-compat': 4.12.8(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember/string@3.1.1) - '@ember-data/private-build-infra': 4.12.8 - '@ember-data/store': 4.12.8(@babel/core@7.27.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)) - '@ember-data/tracking': 4.12.8 + '@ember-data/legacy-compat': 4.13.0-alpha.9(c7ad85e53b9587cb1a558efa86bed54f) + '@ember-data/request-utils': 4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)) + '@ember-data/store': 4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) + '@ember-data/tracking': 4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9) '@ember/edition-utils': 1.2.0 - '@ember/string': 3.1.1 '@embroider/macros': 1.19.6 - ember-cached-decorator-polyfill: 1.0.2(@babel/core@7.27.4)(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)) - ember-cli-babel: 7.26.11 + '@warp-drive/build-config': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 ember-cli-string-utils: 1.1.0 ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)) - inflection: 2.0.1 + inflection: 3.0.2 optionalDependencies: - '@ember-data/debug': 4.12.8(@ember-data/store@4.12.8)(@ember/string@3.1.1)(webpack@5.99.9) - '@ember-data/graph': 4.12.8(@ember-data/store@4.12.8) - '@ember-data/json-api': 4.12.8(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) + '@ember-data/graph': 4.13.0-alpha.9(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) + '@ember-data/json-api': 4.13.0-alpha.9(@ember-data/graph@4.13.0-alpha.9(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) transitivePeerDependencies: - - '@babel/core' - '@glint/template' - - ember-source - supports-color - '@ember-data/private-build-infra@4.12.8': + '@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4))': dependencies: - '@babel/core': 7.27.4(supports-color@8.1.1) - '@babel/plugin-transform-block-scoping': 7.27.5(@babel/core@7.27.4) - '@babel/runtime': 7.27.6 - '@ember/edition-utils': 1.2.0 '@embroider/macros': 1.19.6 - babel-import-util: 1.4.1 - babel-plugin-debug-macros: 0.3.4(@babel/core@7.27.4) - babel-plugin-filter-imports: 4.0.0 - babel6-plugin-strip-class-callcheck: 6.0.0 - broccoli-debug: 0.6.5 - broccoli-file-creator: 2.1.1 - broccoli-funnel: 3.0.8 - broccoli-merge-trees: 4.2.0 - broccoli-rollup: 5.0.0 - calculate-cache-key-for-tree: 2.0.0 - chalk: 4.1.2 - ember-cli-babel: 7.26.11 - ember-cli-path-utils: 1.0.0 - ember-cli-string-utils: 1.1.0 - ember-cli-version-checker: 5.1.2 - git-repo-info: 2.1.1 - glob: 9.3.5 - npm-git-info: 1.0.3 - semver: 7.7.2 - silent-error: 1.1.1 + '@warp-drive/build-config': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 + optionalDependencies: + '@ember/string': 3.1.1 + ember-inflector: 6.0.0(@babel/core@7.27.4) transitivePeerDependencies: - '@glint/template' - supports-color - '@ember-data/request@4.12.8': + '@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9)': dependencies: - '@ember-data/private-build-infra': 4.12.8 '@ember/test-waiters': 3.1.0 '@embroider/macros': 1.19.6 - ember-cli-babel: 7.26.11 + '@warp-drive/build-config': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 transitivePeerDependencies: - '@glint/template' - supports-color '@ember-data/rfc395-data@0.0.4': {} - '@ember-data/serializer@4.12.8(@ember-data/store@4.12.8)(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)))': + '@ember-data/serializer@4.13.0-alpha.9(@ember-data/legacy-compat@4.13.0-alpha.9(c7ad85e53b9587cb1a558efa86bed54f))(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9)': dependencies: - '@ember-data/private-build-infra': 4.12.8 - '@ember-data/store': 4.12.8(@babel/core@7.27.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)) - '@ember/string': 3.1.1 + '@ember-data/legacy-compat': 4.13.0-alpha.9(c7ad85e53b9587cb1a558efa86bed54f) + '@ember-data/request-utils': 4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)) + '@ember-data/store': 4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) + '@ember/edition-utils': 1.2.0 '@embroider/macros': 1.19.6 - ember-cli-babel: 7.26.11 + '@warp-drive/build-config': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 + ember-cli-path-utils: 1.0.0 + ember-cli-string-utils: 1.1.0 ember-cli-test-info: 1.0.0 - ember-inflector: 4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)) transitivePeerDependencies: - '@glint/template' - supports-color - '@ember-data/store@4.12.8(@babel/core@7.27.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9))': + '@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9)': dependencies: - '@ember-data/private-build-infra': 4.12.8 - '@ember-data/tracking': 4.12.8 - '@ember/string': 3.1.1 + '@ember-data/request': 4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9) + '@ember-data/request-utils': 4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)) + '@ember-data/tracking': 4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9) '@embroider/macros': 1.19.6 - '@glimmer/tracking': 1.1.2 - ember-cached-decorator-polyfill: 1.0.2(@babel/core@7.27.4)(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)) - ember-cli-babel: 7.26.11 - optionalDependencies: - '@ember-data/graph': 4.12.8(@ember-data/store@4.12.8) - '@ember-data/json-api': 4.12.8(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) - '@ember-data/legacy-compat': 4.12.8(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember/string@3.1.1) - '@ember-data/model': 4.12.8(@babel/core@7.27.4)(@ember-data/debug@4.12.8)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)))(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)) + '@warp-drive/build-config': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 transitivePeerDependencies: - - '@babel/core' - '@glint/template' - - ember-source - supports-color - '@ember-data/tracking@4.12.8': + '@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9)': dependencies: - '@ember-data/private-build-infra': 4.12.8 '@embroider/macros': 1.19.6 - ember-cli-babel: 7.26.11 + '@warp-drive/build-config': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 transitivePeerDependencies: - '@glint/template' - supports-color @@ -7604,7 +7547,7 @@ snapshots: '@embroider/addon-shim@1.8.7': dependencies: - '@embroider/shared-internals': 2.9.0 + '@embroider/shared-internals': 2.9.2(supports-color@8.1.1) broccoli-funnel: 3.0.8 semver: 7.7.2 transitivePeerDependencies: @@ -8322,12 +8265,6 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 22.15.30 - '@types/broccoli-plugin@3.0.4': - dependencies: - broccoli-plugin: 4.0.7 - transitivePeerDependencies: - - supports-color - '@types/chai-as-promised@7.1.8': dependencies: '@types/chai': 4.3.20 @@ -8438,12 +8375,11 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/project-service@8.34.0(typescript@5.8.3)': + '@typescript-eslint/project-service@8.34.0': dependencies: - '@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.34.0 '@typescript-eslint/types': 8.34.0 debug: 4.4.1(supports-color@8.1.1) - typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -8452,16 +8388,14 @@ snapshots: '@typescript-eslint/types': 8.34.0 '@typescript-eslint/visitor-keys': 8.34.0 - '@typescript-eslint/tsconfig-utils@8.34.0(typescript@5.8.3)': - dependencies: - typescript: 5.8.3 + '@typescript-eslint/tsconfig-utils@8.34.0': {} '@typescript-eslint/types@8.34.0': {} - '@typescript-eslint/typescript-estree@8.34.0(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.34.0': dependencies: - '@typescript-eslint/project-service': 8.34.0(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.34.0(typescript@5.8.3) + '@typescript-eslint/project-service': 8.34.0 + '@typescript-eslint/tsconfig-utils': 8.34.0 '@typescript-eslint/types': 8.34.0 '@typescript-eslint/visitor-keys': 8.34.0 debug: 4.4.1(supports-color@8.1.1) @@ -8469,19 +8403,17 @@ snapshots: is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 + ts-api-utils: 2.1.0 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.34.0(eslint@9.28.0)(typescript@5.8.3)': + '@typescript-eslint/utils@8.34.0(eslint@9.28.0)': dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0) '@typescript-eslint/scope-manager': 8.34.0 '@typescript-eslint/types': 8.34.0 - '@typescript-eslint/typescript-estree': 8.34.0(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.34.0 eslint: 9.28.0 - typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -8490,6 +8422,25 @@ snapshots: '@typescript-eslint/types': 8.34.0 eslint-visitor-keys: 4.2.1 + '@warp-drive/build-config@4.13.0-alpha.9': + dependencies: + '@embroider/addon-shim': 1.8.7 + '@embroider/macros': 1.19.6 + babel-import-util: 2.1.1 + broccoli-funnel: 3.0.8 + semver: 7.7.2 + transitivePeerDependencies: + - '@glint/template' + - supports-color + + '@warp-drive/core-types@4.13.0-alpha.9': + dependencies: + '@embroider/macros': 1.19.6 + '@warp-drive/build-config': 4.13.0-alpha.9 + transitivePeerDependencies: + - '@glint/template' + - supports-color + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -8602,8 +8553,8 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - ajv-formats@2.1.1(ajv@8.17.1): - optionalDependencies: + ajv-formats@2.1.1: + dependencies: ajv: 8.17.1 ajv-keywords@3.5.2(ajv@6.12.6): @@ -8783,8 +8734,6 @@ snapshots: babel-import-util@0.2.0: {} - babel-import-util@1.4.1: {} - babel-import-util@2.1.1: {} babel-import-util@3.0.1: {} @@ -8805,11 +8754,6 @@ snapshots: schema-utils: 4.3.2 webpack: 5.99.9 - babel-plugin-debug-macros@0.2.0(@babel/core@7.27.4): - dependencies: - '@babel/core': 7.27.4(supports-color@8.1.1) - semver: 5.7.2 - babel-plugin-debug-macros@0.3.4(@babel/core@7.27.4): dependencies: '@babel/core': 7.27.4(supports-color@8.1.1) @@ -8833,11 +8777,6 @@ snapshots: '@glimmer/syntax': 0.94.9 babel-import-util: 3.0.1 - babel-plugin-filter-imports@4.0.0: - dependencies: - '@babel/types': 7.27.6 - lodash: 4.17.21 - babel-plugin-htmlbars-inline-precompile@5.3.1: dependencies: babel-plugin-ember-modules-api-polyfill: 3.5.0 @@ -8897,8 +8836,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel6-plugin-strip-class-callcheck@6.0.0: {} - babylon@6.18.0: {} backbone@1.6.1: @@ -9272,20 +9209,6 @@ snapshots: transitivePeerDependencies: - supports-color - broccoli-rollup@5.0.0: - dependencies: - '@types/broccoli-plugin': 3.0.4 - broccoli-plugin: 4.0.7 - fs-tree-diff: 2.0.1 - heimdalljs: 0.2.6 - node-modules-path: 1.0.2 - rollup: 2.79.2 - rollup-pluginutils: 2.8.2 - symlink-or-copy: 1.3.1 - walk-sync: 2.2.0 - transitivePeerDependencies: - - supports-color - broccoli-slow-trees@3.1.0: dependencies: heimdalljs: 0.2.6 @@ -9729,14 +9652,12 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 - cosmiconfig@9.0.0(typescript@5.8.3): + cosmiconfig@9.0.0: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 js-yaml: 4.1.0 parse-json: 5.2.0 - optionalDependencies: - typescript: 5.8.3 cross-spawn@6.0.6: dependencies: @@ -9983,30 +9904,6 @@ snapshots: - supports-color - webpack - ember-cache-primitive-polyfill@1.0.1(@babel/core@7.27.4): - dependencies: - ember-cli-babel: 7.26.11 - ember-cli-version-checker: 5.1.2 - ember-compatibility-helpers: 1.2.7(@babel/core@7.27.4) - silent-error: 1.1.1 - transitivePeerDependencies: - - '@babel/core' - - supports-color - - ember-cached-decorator-polyfill@1.0.2(@babel/core@7.27.4)(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)): - dependencies: - '@embroider/macros': 1.19.6 - '@glimmer/tracking': 1.1.2 - babel-import-util: 1.4.1 - ember-cache-primitive-polyfill: 1.0.1(@babel/core@7.27.4) - ember-cli-babel: 7.26.11 - ember-cli-babel-plugin-helpers: 1.1.1 - ember-source: 5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9) - transitivePeerDependencies: - - '@babel/core' - - '@glint/template' - - supports-color - ember-cli-babel-plugin-helpers@1.1.1: {} ember-cli-babel@7.26.11: @@ -10326,45 +10223,32 @@ snapshots: - walrus - whiskers - ember-compatibility-helpers@1.2.7(@babel/core@7.27.4): - dependencies: - babel-plugin-debug-macros: 0.2.0(@babel/core@7.27.4) - ember-cli-version-checker: 5.1.2 - find-up: 5.0.0 - fs-extra: 9.1.0 - semver: 5.7.2 - transitivePeerDependencies: - - '@babel/core' - - supports-color - - ember-data@4.12.8(@babel/core@7.27.4)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9))(webpack@5.99.9): - dependencies: - '@ember-data/adapter': 4.12.8(@ember-data/store@4.12.8)(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9))) - '@ember-data/debug': 4.12.8(@ember-data/store@4.12.8)(@ember/string@3.1.1)(webpack@5.99.9) - '@ember-data/graph': 4.12.8(@ember-data/store@4.12.8) - '@ember-data/json-api': 4.12.8(@ember-data/graph@4.12.8)(@ember-data/store@4.12.8) - '@ember-data/legacy-compat': 4.12.8(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember/string@3.1.1) - '@ember-data/model': 4.12.8(@babel/core@7.27.4)(@ember-data/debug@4.12.8)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/store@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)))(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)) - '@ember-data/private-build-infra': 4.12.8 - '@ember-data/request': 4.12.8 - '@ember-data/serializer': 4.12.8(@ember-data/store@4.12.8)(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9))) - '@ember-data/store': 4.12.8(@babel/core@7.27.4)(@ember-data/graph@4.12.8)(@ember-data/json-api@4.12.8)(@ember-data/legacy-compat@4.12.8)(@ember-data/model@4.12.8)(@ember-data/tracking@4.12.8)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)) - '@ember-data/tracking': 4.12.8 + ember-data@4.13.0-alpha.9(@ember/string@3.1.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.4))(@ember/test-waiters@3.1.0)(ember-inflector@6.0.0(@babel/core@7.27.4))(qunit@2.24.1): + dependencies: + '@ember-data/adapter': 4.13.0-alpha.9(@ember-data/legacy-compat@4.13.0-alpha.9(c7ad85e53b9587cb1a558efa86bed54f))(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) + '@ember-data/debug': 4.13.0-alpha.9(@ember-data/model@4.13.0-alpha.9(28d7247ae793456ad31c1824cc98f4f3))(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) + '@ember-data/graph': 4.13.0-alpha.9(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) + '@ember-data/json-api': 4.13.0-alpha.9(@ember-data/graph@4.13.0-alpha.9(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) + '@ember-data/legacy-compat': 4.13.0-alpha.9(c7ad85e53b9587cb1a558efa86bed54f) + '@ember-data/model': 4.13.0-alpha.9(28d7247ae793456ad31c1824cc98f4f3) + '@ember-data/request': 4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9) + '@ember-data/request-utils': 4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)) + '@ember-data/serializer': 4.13.0-alpha.9(@ember-data/legacy-compat@4.13.0-alpha.9(c7ad85e53b9587cb1a558efa86bed54f))(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/store@4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) + '@ember-data/store': 4.13.0-alpha.9(@ember-data/request-utils@4.13.0-alpha.9(@ember/string@3.1.1)(@warp-drive/core-types@4.13.0-alpha.9)(ember-inflector@6.0.0(@babel/core@7.27.4)))(@ember-data/request@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@ember-data/tracking@4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9))(@warp-drive/core-types@4.13.0-alpha.9) + '@ember-data/tracking': 4.13.0-alpha.9(@warp-drive/core-types@4.13.0-alpha.9) '@ember/edition-utils': 1.2.0 - '@ember/string': 3.1.1 '@embroider/macros': 1.19.6 - '@glimmer/env': 0.1.7 - broccoli-merge-trees: 4.2.0 - ember-auto-import: 2.10.0(webpack@5.99.9) - ember-cli-babel: 7.26.11 - ember-inflector: 4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)) + '@warp-drive/build-config': 4.13.0-alpha.9 + '@warp-drive/core-types': 4.13.0-alpha.9 + optionalDependencies: + '@ember/test-helpers': 5.2.2(@babel/core@7.27.4) + '@ember/test-waiters': 3.1.0 + qunit: 2.24.1 transitivePeerDependencies: - - '@babel/core' - - '@glimmer/tracking' + - '@ember/string' - '@glint/template' - - ember-source + - ember-inflector - supports-color - - webpack ember-eslint-parser@0.5.9(@babel/core@7.27.4)(eslint@9.28.0): dependencies: @@ -10379,11 +10263,12 @@ snapshots: transitivePeerDependencies: - eslint - ember-inflector@4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)): + ember-inflector@6.0.0(@babel/core@7.27.4): dependencies: - ember-cli-babel: 7.26.11 - ember-source: 5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9) + '@embroider/addon-shim': 1.8.7 + decorator-transforms: 2.3.0(@babel/core@7.27.4) transitivePeerDependencies: + - '@babel/core' - supports-color ember-load-initializers@3.0.1(ember-source@5.12.0(@glimmer/component@2.0.0)(rsvp@4.8.5)(webpack@5.99.9)): @@ -10755,10 +10640,10 @@ snapshots: eslint: 9.28.0 eslint-compat-utils: 0.5.1(eslint@9.28.0) - eslint-plugin-n@17.19.0(eslint@9.28.0)(typescript@5.8.3): + eslint-plugin-n@17.19.0(eslint@9.28.0): dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@9.28.0) - '@typescript-eslint/utils': 8.34.0(eslint@9.28.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.34.0(eslint@9.28.0) enhanced-resolve: 5.18.1 eslint: 9.28.0 eslint-plugin-es-x: 7.8.0(eslint@9.28.0) @@ -10767,7 +10652,7 @@ snapshots: ignore: 5.3.2 minimatch: 9.0.5 semver: 7.7.2 - ts-declaration-location: 1.0.7(typescript@5.8.3) + ts-declaration-location: 1.0.7 transitivePeerDependencies: - supports-color - typescript @@ -10869,8 +10754,6 @@ snapshots: estraverse@5.3.0: {} - estree-walker@0.6.1: {} - esutils@2.0.3: {} etag@1.8.1: {} @@ -11368,9 +11251,6 @@ snapshots: fs.realpath@1.0.0: {} - fsevents@2.3.3: - optional: true - function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -11814,6 +11694,8 @@ snapshots: inflection@2.0.1: {} + inflection@3.0.2: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -12680,8 +12562,6 @@ snapshots: node-int64@0.4.0: {} - node-modules-path@1.0.2: {} - node-notifier@10.0.1: dependencies: growly: 1.3.0 @@ -13404,14 +13284,6 @@ snapshots: dependencies: glob: 7.2.3 - rollup-pluginutils@2.8.2: - dependencies: - estree-walker: 0.6.1 - - rollup@2.79.2: - optionalDependencies: - fsevents: 2.3.3 - route-recognizer@0.3.4: {} router_js@8.0.6(route-recognizer@0.3.4)(rsvp@4.8.5): @@ -13525,7 +13397,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) + ajv-formats: 2.1.1 ajv-keywords: 5.1.0(ajv@8.17.1) semver@5.7.2: {} @@ -13921,16 +13793,16 @@ snapshots: styled_string@0.0.1: {} - stylelint-config-recommended@14.0.1(stylelint@16.20.0(typescript@5.8.3)): + stylelint-config-recommended@14.0.1(stylelint@16.20.0): dependencies: - stylelint: 16.20.0(typescript@5.8.3) + stylelint: 16.20.0 - stylelint-config-standard@36.0.1(stylelint@16.20.0(typescript@5.8.3)): + stylelint-config-standard@36.0.1(stylelint@16.20.0): dependencies: - stylelint: 16.20.0(typescript@5.8.3) - stylelint-config-recommended: 14.0.1(stylelint@16.20.0(typescript@5.8.3)) + stylelint: 16.20.0 + stylelint-config-recommended: 14.0.1(stylelint@16.20.0) - stylelint@16.20.0(typescript@5.8.3): + stylelint@16.20.0: dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 @@ -13939,7 +13811,7 @@ snapshots: '@dual-bundle/import-meta-resolve': 4.1.0 balanced-match: 2.0.0 colord: 2.9.3 - cosmiconfig: 9.0.0(typescript@5.8.3) + cosmiconfig: 9.0.0 css-functions-list: 3.2.3 css-tree: 3.1.0 debug: 4.4.1(supports-color@8.1.1) @@ -14276,14 +14148,11 @@ snapshots: transitivePeerDependencies: - supports-color - ts-api-utils@2.1.0(typescript@5.8.3): - dependencies: - typescript: 5.8.3 + ts-api-utils@2.1.0: {} - ts-declaration-location@1.0.7(typescript@5.8.3): + ts-declaration-location@1.0.7: dependencies: picomatch: 4.0.2 - typescript: 5.8.3 tslib@1.14.1: {} @@ -14343,8 +14212,6 @@ snapshots: typescript-memoize@1.1.1: {} - typescript@5.8.3: {} - uc.micro@2.1.0: {} uglify-js@3.19.3: diff --git a/record-data.d.ts b/record-data.d.ts deleted file mode 100644 index 0b455055..00000000 --- a/record-data.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -// eslint-disable-next-line ember/use-ember-data-rfc-395-imports -import { RecordData } from 'ember-data/-private'; - -export default class FragmentRecordData extends RecordData {} diff --git a/tests/dummy/app/models/zoo.js b/tests/dummy/app/models/zoo.js index df2c8b31..6e03a0b7 100644 --- a/tests/dummy/app/models/zoo.js +++ b/tests/dummy/app/models/zoo.js @@ -6,5 +6,5 @@ export default class Zoo extends Model { @attr('string') city; @fragment('animal', { polymorphic: true, typeKey: '$type' }) star; @fragmentArray('animal', { polymorphic: true, typeKey: '$type' }) animals; - @belongsTo('person') manager; + @belongsTo('person', { async: true, inverse: null }) manager; } diff --git a/tests/dummy/app/serializers/application.js b/tests/dummy/app/serializers/application.js index ea2b25ea..861e3456 100644 --- a/tests/dummy/app/serializers/application.js +++ b/tests/dummy/app/serializers/application.js @@ -1,3 +1,3 @@ -import RESTSerializer from '@ember-data/serializer/rest'; +import { FragmentRESTSerializer } from 'ember-data-model-fragments/serializer'; -export default class ApplicationSerializer extends RESTSerializer {} +export default class ApplicationSerializer extends FragmentRESTSerializer {} diff --git a/tests/dummy/app/serializers/component.js b/tests/dummy/app/serializers/component.js index 40797a43..b8ccd293 100644 --- a/tests/dummy/app/serializers/component.js +++ b/tests/dummy/app/serializers/component.js @@ -1,6 +1,6 @@ -import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { FragmentJSONAPISerializer } from 'ember-data-model-fragments/serializer'; -export default class extends JSONAPISerializer { +export default class extends FragmentJSONAPISerializer { serialize(snapshot, ...args) { const data = super.serialize(snapshot, ...args); const { record } = snapshot; diff --git a/tests/dummy/app/services/store.js b/tests/dummy/app/services/store.js new file mode 100644 index 00000000..3c3893dc --- /dev/null +++ b/tests/dummy/app/services/store.js @@ -0,0 +1,3 @@ +import FragmentStore from 'ember-data-model-fragments/store'; + +export default class Store extends FragmentStore {} diff --git a/tests/dummy/config/ember-try.js b/tests/dummy/config/ember-try.js index ed0c722b..d4067a8d 100644 --- a/tests/dummy/config/ember-try.js +++ b/tests/dummy/config/ember-try.js @@ -53,6 +53,23 @@ module.exports = async function () { devDependencies: { 'ember-data': '~4.12.0', '@ember-data/json-api': '~4.12.0', + '@ember-data/model': '~4.12.0', + '@ember-data/serializer': '~4.12.0', + '@ember-data/store': '~4.12.0', + // ember-data 4.12 has peer deps on ember-inflector 4.x + 'ember-inflector': '^4.0.3', + }, + }, + }, + { + name: 'ember-data-4.13', + npm: { + devDependencies: { + 'ember-data': '~4.13.0-alpha.9', + '@ember-data/json-api': '~4.13.0-alpha.9', + '@ember-data/model': '~4.13.0-alpha.9', + '@ember-data/serializer': '~4.13.0-alpha.9', + '@ember-data/store': '~4.13.0-alpha.9', }, }, }, diff --git a/tests/integration/save-test.js b/tests/integration/save-test.js index f29079ec..8f7f6edd 100644 --- a/tests/integration/save-test.js +++ b/tests/integration/save-test.js @@ -1,19 +1,13 @@ /* eslint-disable ember/no-observers */ import Model, { attr } from '@ember-data/model'; -import { - fragment, - fragmentArray, - array, -} from 'ember-data-model-fragments/attributes'; +import { array } from 'ember-data-model-fragments/attributes'; import EmberObject, { observer } from '@ember/object'; import { addObserver } from '@ember/object/observers'; import ObjectProxy from '@ember/object/proxy'; import { copy } from 'ember-data-model-fragments/util/copy'; -import MF from 'ember-data-model-fragments'; import { module, test, skip } from 'qunit'; import { setupApplicationTest } from '../helpers'; import Pretender from 'pretender'; -import { gte } from 'ember-compatibility-helpers'; let store, owner, server; @@ -874,95 +868,6 @@ module('integration - Persistence', function (hooks) { ); }); - if (!gte('ember-data', '4.5.0')) { - // InternalModel.currentState was removed in ember-data 4.5 - - test('fragments with default values are rolled back to uncommitted state after failed save', async function (assert) { - class Address extends MF.Fragment { - @attr('string') line1; - @attr('string') line2; - } - - owner.register('model:address', Address); - - class PersonWithDefaults extends Model { - @fragment('address', { defaultValue: {} }) address; - @fragmentArray('address', { defaultValue: [{}] }) addresses; - } - - owner.register('model:person', PersonWithDefaults); - - const person = store.createRecord('person'); - const address = person.address; - const addresses = person.addresses; - - assert.equal( - address._internalModel.currentState.stateName, - 'root.loaded.updated.uncommitted', - 'fragment state before save', - ); - assert.equal( - addresses.firstObject._internalModel.currentState.stateName, - 'root.loaded.updated.uncommitted', - 'fragment array state before save', - ); - - server.post('/people', () => { - const response = { - errors: [{ code: 'custom-error-code' }], - }; - return [ - 400, - { 'Content-Type': 'application/json' }, - JSON.stringify(response), - ]; - }); - - const savePromise = person.save(); - - assert.equal( - address._internalModel.currentState.stateName, - 'root.loaded.updated.inFlight', - 'fragment state during save', - ); - assert.equal( - addresses.firstObject._internalModel.currentState.stateName, - 'root.loaded.updated.inFlight', - 'fragment array state during save', - ); - - await assert.rejects( - savePromise, - (ex) => ex.errors[0].code === 'custom-error-code', - ); - - assert.equal( - address._internalModel.currentState.stateName, - 'root.loaded.updated.uncommitted', - 'fragment state after save', - ); - assert.equal( - addresses.firstObject._internalModel.currentState.stateName, - 'root.loaded.updated.uncommitted', - 'fragment array state after save', - ); - - // unload will fail if the record is in-flight - person.unloadRecord(); - - assert.equal( - address._internalModel.currentState.stateName, - 'root.empty', - 'fragment state after unload', - ); - assert.equal( - addresses.firstObject._internalModel.currentState.stateName, - 'root.empty', - 'fragment array state after unload', - ); - }); - } - test('setting an array does not error on save', async function (assert) { assert.expect(0); class Army extends Model { diff --git a/tests/unit/fragment-test.js b/tests/unit/fragment-test.js index 0c25359e..31208139 100644 --- a/tests/unit/fragment-test.js +++ b/tests/unit/fragment-test.js @@ -5,7 +5,6 @@ import { setupApplicationTest } from '../helpers'; import Pretender from 'pretender'; import Lion from 'dummy/models/lion'; import Elephant from 'dummy/models/elephant'; -import { gte } from 'ember-compatibility-helpers'; let store; @@ -272,7 +271,7 @@ module('unit - `MF.Fragment`', function (hooks) { assert.ok(fragment.isDestroying, 'the fragment is being destroyed'); }); - test('fragments unloaded/reload w/ relationship', function (assert) { + test('fragments unloaded/reload w/ relationship', async function (assert) { // Related to: https://github.com/lytics/ember-data-model-fragments/issues/261 function isUnloaded(recordOrFragment) { @@ -319,15 +318,11 @@ module('unit - `MF.Fragment`', function (hooks) { let zoo = pushZoo(); // Prime the relationship and fragment - zoo.manager; + const manager = await zoo.manager; zoo.star; assert.equal(person.title, 'Zoo Manager', 'Person has the right title'); - assert.equal( - zoo.manager.content, - person, - 'Manager relationship was correctly loaded', - ); + assert.equal(manager, person, 'Manager relationship was correctly loaded'); assert.equal( zoo.star.name, 'Sabu', @@ -354,8 +349,9 @@ module('unit - `MF.Fragment`', function (hooks) { // Make sure the reloaded record is new and has the right data assert.notOk(isUnloaded(zoo), 'Zoo was unloaded'); assert.notOk(isUnloaded(zoo.star), 'Fragment is now unloaded'); + const reloadedManager = await zoo.manager; assert.equal( - zoo.manager.content, + reloadedManager, person, 'Manager relationship was correctly loaded', ); @@ -372,34 +368,6 @@ module('unit - `MF.Fragment`', function (hooks) { assert.ok(zoo.star !== origZoo.star, 'Fragments were not reused'); }); - if (!gte('ember-data', '4.4.0')) { - // lifecycle events were deprecated in ember-data 3.12 and removed in 4.4 - // https://deprecations.emberjs.com/ember-data/v3.x/#toc_record-lifecycle-event-methods - // https://github.com/emberjs/data/pull/7970 - - test('fragments call ready callback when they are created', function (assert) { - const name = store.createFragment('name'); - assert.ok( - name.readyWasCalled, - 'when making fragment directly with store.createFragment', - ); - - const person = store.createRecord('person', { - name: { first: 'dan' }, - names: [{ first: 'eric' }], - }); - - assert.ok( - person.name.readyWasCalled, - 'when creating model that has fragment', - ); - assert.ok( - person.names.isEvery('readyWasCalled'), - 'when creating model that has fragmentArray', - ); - }); - } - test('can be created with null', async function (assert) { const person = store.push({ data: { diff --git a/tests/unit/serialize-test.js b/tests/unit/serialize-test.js index ab743b56..a9986d34 100644 --- a/tests/unit/serialize-test.js +++ b/tests/unit/serialize-test.js @@ -2,6 +2,7 @@ import { isEmpty } from '@ember/utils'; import { module, test } from 'qunit'; import { setupApplicationTest } from '../helpers'; import JSONSerializer from '@ember-data/serializer/json'; +import FragmentSerializer from 'ember-data-model-fragments/serializer'; import Person from 'dummy/models/person'; import { fragmentArray, array } from 'ember-data-model-fragments/attributes'; import Pretender from 'pretender'; @@ -49,31 +50,26 @@ module('unit - Serialization', function (hooks) { test('fragment properties are snapshotted as normal attributes on the owner record snapshot', async function (assert) { assert.expect(7); - const person = { - name: { - first: 'Catelyn', - last: 'Stark', - }, - houses: [ - { - name: 'Tully', - region: 'Riverlands', - exiled: true, - }, - { - name: 'Stark', - region: 'North', - exiled: true, - }, - ], - children: ['Robb', 'Sansa', 'Arya', 'Brandon', 'Rickon'], - }; + // Store expected values before pushing - store.push may mutate the source object + const expectedName = { first: 'Catelyn', last: 'Stark' }; + const expectedHouses = [ + { name: 'Tully', region: 'Riverlands', exiled: true }, + { name: 'Stark', region: 'North', exiled: true }, + ]; + const expectedChildren = ['Robb', 'Sansa', 'Arya', 'Brandon', 'Rickon']; store.push({ data: { type: 'person', id: 1, - attributes: person, + attributes: { + name: { first: 'Catelyn', last: 'Stark' }, + houses: [ + { name: 'Tully', region: 'Riverlands', exiled: true }, + { name: 'Stark', region: 'North', exiled: true }, + ], + children: ['Robb', 'Sansa', 'Arya', 'Brandon', 'Rickon'], + }, }, }); @@ -87,7 +83,7 @@ module('unit - Serialization', function (hooks) { assert.equal( name.attr('first'), - person.name.first, + expectedName.first, 'fragment attributes are snapshoted correctly', ); @@ -102,7 +98,7 @@ module('unit - Serialization', function (hooks) { ); assert.equal( houses[0].attr('name'), - person.houses[0].name, + expectedHouses[0].name, 'fragment array attributes are snapshotted correctly', ); @@ -110,7 +106,7 @@ module('unit - Serialization', function (hooks) { assert.ok(Array.isArray(children), 'array attribute is an array'); assert.deepEqual( children, - person.children, + expectedChildren, 'array attribute is snapshotted correctly', ); } @@ -181,7 +177,8 @@ module('unit - Serialization', function (hooks) { }, }); - owner.register('serializer:name', JSONSerializer); + // Use FragmentSerializer to ensure proper fragment transform handling + owner.register('serializer:name', FragmentSerializer); const person = await store.findRecord('person', 1); const serialized = person.serialize(); diff --git a/tests/unit/store-test.js b/tests/unit/store-test.js index 4734f0f5..ab024355 100644 --- a/tests/unit/store-test.js +++ b/tests/unit/store-test.js @@ -2,9 +2,7 @@ import { schedule } from '@ember/runloop'; import { module, test } from 'qunit'; import { setupApplicationTest } from '../helpers'; import Name from 'dummy/models/name'; -import JSONAPISerializer from '@ember-data/serializer/json-api'; import JSONSerializer from '@ember-data/serializer/json'; -import { gte } from 'ember-compatibility-helpers'; let store, owner; @@ -39,46 +37,16 @@ module('unit - `DS.Store`', function (hooks) { assert.notOk(store.isFragment('person', 'a model should return false')); }); - test('the default fragment serializer does not use the application serializer', function (assert) { - class ApplicationSerializer extends JSONAPISerializer {} - owner.register('serializer:application', ApplicationSerializer); + test('fragments use the application serializer as fallback', function (assert) { + // The dummy app's application serializer is FragmentRESTSerializer + // Fragments without specific serializers should use the application serializer + const fragmentSerializer = store.serializerFor('name'); + const applicationSerializer = store.serializerFor('application'); - assert.ok( - !(store.serializerFor('name') instanceof ApplicationSerializer), - 'fragment serializer fallback is not `JSONAPISerializer`', - ); - assert.ok( - store.serializerFor('name') instanceof JSONSerializer, - 'fragment serializer fallback is correct', - ); - }); - - if (!gte('ember-data', '4.4.0')) { - // default adapter was deprecated in ember-data 3.15 and removed in 4.4 - // https://deprecations.emberjs.com/ember-data/v3.x/#toc_ember-data-default-adapter - // https://github.com/emberjs/data/pull/7861 - - test("the default fragment serializer does not use the adapter's `defaultSerializer`", function (assert) { - store.set('defaultAdapter.defaultSerializer', '-json-api'); - - assert.ok( - !(store.serializerFor('name') instanceof JSONAPISerializer), - 'fragment serializer fallback is not `JSONAPISerializer`', - ); - assert.ok( - store.serializerFor('name') instanceof JSONSerializer, - 'fragment serializer fallback is correct', - ); - }); - } - - test('the default fragment serializer is `serializer:-fragment` if registered', function (assert) { - class FragmentSerializer extends JSONSerializer {} - owner.register('serializer:-fragment', FragmentSerializer); - - assert.ok( - store.serializerFor('name') instanceof FragmentSerializer, - 'fragment serializer fallback is correct', + assert.strictEqual( + fragmentSerializer, + applicationSerializer, + 'fragment serializer falls back to application serializer', ); });