diff --git a/ember-data-types/q/ds-model.ts b/ember-data-types/q/ds-model.ts index 680f2e28ba8..77aa290f6b6 100644 --- a/ember-data-types/q/ds-model.ts +++ b/ember-data-types/q/ds-model.ts @@ -16,6 +16,8 @@ export interface DSModel extends EmberObject { eachRelationship(callback: (this: T, key: string, meta: RelationshipSchema) => void, binding?: T): void; eachAttribute(callback: (this: T, key: string, meta: AttributeSchema) => void, binding?: T): void; invalidErrorsChanged(errors: JsonApiValidationError[]): void; + rollbackAttributes(): void; + changedAttributes(): Record; [key: string]: unknown; isDeleted: boolean; deleteRecord(): void; diff --git a/packages/json-api/src/-private/cache.ts b/packages/json-api/src/-private/cache.ts index bc626cce410..7b7e4350f19 100644 --- a/packages/json-api/src/-private/cache.ts +++ b/packages/json-api/src/-private/cache.ts @@ -87,22 +87,29 @@ export default class SingletonCache implements Cache { } /** - * Private method used when the store's `createRecordDataFor` hook is called - * to populate an entry for the identifier into the singleton. + * Private method used to populate an entry for the identifier * - * @method createCache + * @method _createCache * @private * @param identifier */ - createCache(identifier: StableRecordIdentifier): void { - this.__cache.set(identifier, makeCache()); + _createCache(identifier: StableRecordIdentifier): CachedResource { + assert(`Expected no resource data to yet exist in the cache`, !this.__cache.has(identifier)); + const cache = makeCache(); + this.__cache.set(identifier, cache); + return cache; } - __peek(identifier: StableRecordIdentifier, allowDestroyed = false): CachedResource { + __safePeek(identifier: StableRecordIdentifier, allowDestroyed: boolean): CachedResource | undefined { let resource = this.__cache.get(identifier); if (!resource && allowDestroyed) { resource = this.__destroyedCache.get(identifier); } + return resource; + } + + __peek(identifier: StableRecordIdentifier, allowDestroyed: boolean): CachedResource { + let resource = this.__safePeek(identifier, allowDestroyed); assert( `Expected Cache to have a resource entry for the identifier ${String(identifier)} but none was found`, resource @@ -116,9 +123,9 @@ export default class SingletonCache implements Cache { calculateChanges?: boolean | undefined ): void | string[] { let changedKeys: string[] | undefined; - const peeked = this.__peek(identifier); + const peeked = this.__safePeek(identifier, false); const existed = !!peeked; - const cached = peeked || this.createCache(identifier); + const cached = peeked || this._createCache(identifier); const isLoading = _isLoading(peeked, this.__storeWrapper, identifier) || !recordIsLoaded(peeked); let isUpdate = !_isEmpty(peeked) && !isLoading; @@ -136,6 +143,7 @@ export default class SingletonCache implements Cache { if (cached.isNew) { cached.isNew = false; + this.__storeWrapper.notifyChange(identifier, 'identity'); this.__storeWrapper.notifyChange(identifier, 'state'); } @@ -211,7 +219,7 @@ export default class SingletonCache implements Cache { console.log(`EmberData | Mutation - clientDidCreate ${identifier.lid}`, options); } } - const cached = this.__peek(identifier); + const cached = this._createCache(identifier); cached.isNew = true; let createOptions = {}; @@ -273,12 +281,12 @@ export default class SingletonCache implements Cache { return createOptions; } willCommit(identifier: StableRecordIdentifier): void { - const cached = this.__peek(identifier); + const cached = this.__peek(identifier, false); cached.inflightAttrs = cached.localAttrs; cached.localAttrs = null; } didCommit(identifier: StableRecordIdentifier, data: JsonApiResource | null): void { - const cached = this.__peek(identifier); + const cached = this.__peek(identifier, false); if (cached.isDeleted) { graphFor(this.__storeWrapper).push({ op: 'deleteRecord', @@ -337,7 +345,7 @@ export default class SingletonCache implements Cache { } commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiValidationError[] | undefined): void { - const cached = this.__peek(identifier); + const cached = this.__peek(identifier, false); if (cached.inflightAttrs) { let keys = Object.keys(cached.inflightAttrs); if (keys.length > 0) { @@ -357,9 +365,17 @@ export default class SingletonCache implements Cache { } unloadRecord(identifier: StableRecordIdentifier): void { + // TODO this is necessary because + // we maintain memebership inside InstanceCache + // for peekAll, so even though we haven't created + // any data we think this exists. + // TODO can we eliminate that membership now? + if (!this.__cache.has(identifier)) { + return; + } const removeFromRecordArray = !this.isDeletionCommitted(identifier); let removed = false; - const cached = this.__peek(identifier); + const cached = this.__peek(identifier, false); const storeWrapper = this.__storeWrapper; peekGraph(storeWrapper)?.unload(identifier); @@ -422,7 +438,7 @@ export default class SingletonCache implements Cache { } } setAttr(identifier: StableRecordIdentifier, attr: string, value: unknown): void { - const cached = this.__peek(identifier); + const cached = this.__peek(identifier, false); const existing = cached.inflightAttrs && attr in cached.inflightAttrs ? cached.inflightAttrs[attr] @@ -443,7 +459,7 @@ export default class SingletonCache implements Cache { } changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash { // TODO freeze in dev - return this.__peek(identifier).changes || Object.create(null); + return this.__peek(identifier, false).changes || Object.create(null); } hasChangedAttrs(identifier: StableRecordIdentifier): boolean { const cached = this.__peek(identifier, true); @@ -454,7 +470,7 @@ export default class SingletonCache implements Cache { ); } rollbackAttrs(identifier: StableRecordIdentifier): string[] { - const cached = this.__peek(identifier); + const cached = this.__peek(identifier, false); let dirtyKeys: string[] | undefined; cached.isDeleted = false; @@ -498,7 +514,7 @@ export default class SingletonCache implements Cache { } setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void { - const cached = this.__peek(identifier); + const cached = this.__peek(identifier, false); cached.isDeleted = isDeleted; if (cached.isNew) { // TODO can we delete this since we will do this in unload? @@ -514,17 +530,20 @@ export default class SingletonCache implements Cache { return this.__peek(identifier, true).errors || []; } isEmpty(identifier: StableRecordIdentifier): boolean { - const cached = this.__peek(identifier, true); - return cached.remoteAttrs === null && cached.inflightAttrs === null && cached.localAttrs === null; + const cached = this.__safePeek(identifier, true); + return cached ? cached.remoteAttrs === null && cached.inflightAttrs === null && cached.localAttrs === null : true; } isNew(identifier: StableRecordIdentifier): boolean { - return this.__peek(identifier, true).isNew; + // TODO can we assert here? + return this.__safePeek(identifier, true)?.isNew || false; } isDeleted(identifier: StableRecordIdentifier): boolean { - return this.__peek(identifier, true).isDeleted; + // TODO can we assert here? + return this.__safePeek(identifier, true)?.isDeleted || false; } isDeletionCommitted(identifier: StableRecordIdentifier): boolean { - return this.__peek(identifier, true).isDeletionCommitted; + // TODO can we assert here? + return this.__safePeek(identifier, true)?.isDeletionCommitted || false; } } @@ -654,7 +673,7 @@ function recordIsLoaded(cached: CachedResource | undefined, filterDeleted: boole } function _isLoading( - peeked: CachedResource, + peeked: CachedResource | undefined, storeWrapper: CacheStoreWrapper, identifier: StableRecordIdentifier ): boolean { diff --git a/packages/private-build-infra/addon/current-deprecations.ts b/packages/private-build-infra/addon/current-deprecations.ts index 4932b05d419..ed90ff96b05 100644 --- a/packages/private-build-infra/addon/current-deprecations.ts +++ b/packages/private-build-infra/addon/current-deprecations.ts @@ -61,4 +61,6 @@ export default { DEPRECATE_ARRAY_LIKE: '4.7', DEPRECATE_COMPUTED_CHAINS: '5.0', DEPRECATE_NON_EXPLICIT_POLYMORPHISM: '4.7', + DEPRECATE_INSTANTIATE_RECORD_ARGS: '4.12', + DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK: '4.12', }; diff --git a/packages/private-build-infra/addon/deprecations.ts b/packages/private-build-infra/addon/deprecations.ts index 8e528641fef..9a5f1978d71 100644 --- a/packages/private-build-infra/addon/deprecations.ts +++ b/packages/private-build-infra/addon/deprecations.ts @@ -30,3 +30,5 @@ export const DEPRECATE_PROMISE_PROXIES = deprecationState('DEPRECATE_PROMISE_PRO export const DEPRECATE_ARRAY_LIKE = deprecationState('DEPRECATE_ARRAY_LIKE'); export const DEPRECATE_COMPUTED_CHAINS = deprecationState('DEPRECATE_COMPUTED_CHAINS'); export const DEPRECATE_NON_EXPLICIT_POLYMORPHISM = deprecationState('DEPRECATE_NON_EXPLICIT_POLYMORPHISM'); +export const DEPRECATE_INSTANTIATE_RECORD_ARGS = deprecationState('DEPRECATE_INSTANTIATE_RECORD_ARGS'); +export const DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK = deprecationState('DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK'); diff --git a/packages/store/src/-private/caches/cache-utils.ts b/packages/store/src/-private/caches/cache-utils.ts new file mode 100644 index 00000000000..75ef8dde718 --- /dev/null +++ b/packages/store/src/-private/caches/cache-utils.ts @@ -0,0 +1,34 @@ +import { assert } from '@ember/debug'; + +import type { Cache } from '@ember-data/types/q/cache'; +import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { RecordInstance } from '@ember-data/types/q/record-instance'; + +/* + * Returns the Cache instance associated with a given + * Model or Identifier + */ + +export const CacheForIdentifierCache = new Map(); + +export function setCacheFor(identifier: StableRecordIdentifier | RecordInstance, recordData: Cache): void { + assert( + `Illegal set of identifier`, + !CacheForIdentifierCache.has(identifier) || CacheForIdentifierCache.get(identifier) === recordData + ); + CacheForIdentifierCache.set(identifier, recordData); +} + +export function removeRecordDataFor(identifier: StableRecordIdentifier | RecordInstance): void { + CacheForIdentifierCache.delete(identifier); +} + +export default function peekCache(instance: StableRecordIdentifier): Cache | null; +export default function peekCache(instance: RecordInstance): Cache; +export default function peekCache(instance: StableRecordIdentifier | RecordInstance): Cache | null { + if (CacheForIdentifierCache.has(instance as StableRecordIdentifier)) { + return CacheForIdentifierCache.get(instance as StableRecordIdentifier) as Cache; + } + + return null; +} diff --git a/packages/store/src/-private/caches/instance-cache.ts b/packages/store/src/-private/caches/instance-cache.ts index 4ba9f907123..283d6f240e6 100644 --- a/packages/store/src/-private/caches/instance-cache.ts +++ b/packages/store/src/-private/caches/instance-cache.ts @@ -8,8 +8,13 @@ import type { Graph } from '@ember-data/graph/-private/graph/graph'; import type { peekGraph } from '@ember-data/graph/-private/graph/index'; import { HAS_GRAPH_PACKAGE, HAS_JSON_API_PACKAGE } from '@ember-data/private-build-infra'; import { LOG_INSTANCE_CACHE } from '@ember-data/private-build-infra/debugging'; -import { DEPRECATE_V1_RECORD_DATA, DEPRECATE_V1CACHE_STORE_APIS } from '@ember-data/private-build-infra/deprecations'; -import type { Cache } from '@ember-data/types/q/cache'; +import { + DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK, + DEPRECATE_INSTANTIATE_RECORD_ARGS, + DEPRECATE_V1_RECORD_DATA, + DEPRECATE_V1CACHE_STORE_APIS, +} from '@ember-data/private-build-infra/deprecations'; +import type { Cache, CacheV1 } from '@ember-data/types/q/cache'; import type { CacheStoreWrapper as StoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; import type { ExistingResourceIdentifierObject, @@ -28,7 +33,7 @@ import type { FindOptions } from '@ember-data/types/q/store'; import type { Dict } from '@ember-data/types/q/utils'; import RecordReference from '../legacy-model-support/record-reference'; -import { NonSingletonCacheManager, SingletonCacheManager } from '../managers/cache-manager'; +import { NonSingletonCacheManager } from '../managers/cache-manager'; import { CacheStoreWrapper } from '../managers/cache-store-wrapper'; import Snapshot from '../network/snapshot'; import type { CreateRecordProperties } from '../store-service'; @@ -37,7 +42,7 @@ import coerceId, { ensureStringId } from '../utils/coerce-id'; import constructResource from '../utils/construct-resource'; import { assertIdentifierHasId } from '../utils/identifier-has-id'; import normalizeModelName from '../utils/normalize-model-name'; -import { RecordDataForIdentifierCache, removeRecordDataFor, setRecordDataFor } from './record-data-for'; +import { CacheForIdentifierCache, removeRecordDataFor, setCacheFor } from './cache-utils'; let _peekGraph: peekGraph; if (HAS_GRAPH_PACKAGE) { @@ -121,6 +126,7 @@ type Caches = { export class InstanceCache { declare store: Store; + declare cache: Cache; declare _storeWrapper: CacheStoreWrapper; declare __recordDataFor: (resource: RecordIdentifier) => Cache; @@ -135,11 +141,14 @@ export class InstanceCache { this.store = store; this._storeWrapper = new CacheStoreWrapper(this.store); - this.__recordDataFor = (resource: RecordIdentifier) => { - // TODO enforce strict - const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resource); - return this.getRecordData(identifier); - }; + + if (DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK) { + this.__recordDataFor = (resource: RecordIdentifier) => { + // TODO enforce strict + const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resource); + return this.getRecordData(identifier); + }; + } store.identifierCache.__configureMerge( (identifier: StableRecordIdentifier, matchedIdentifier: StableRecordIdentifier, resourceData) => { @@ -192,10 +201,14 @@ export class InstanceCache { record: staleIdentifier, value: keptIdentifier, }); + } else if (!DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK) { + this.store.cache.patch({ + op: 'mergeIdentifiers', + record: staleIdentifier, + value: keptIdentifier, + }); } else if (HAS_JSON_API_PACKAGE) { - // TODO notify cache always, this requires it always being a singleton - // and not ever specific to one record-data - this.store.__private_singleton_recordData?.patch({ + this.store.cache.patch({ op: 'mergeIdentifiers', record: staleIdentifier, value: keptIdentifier, @@ -241,16 +254,34 @@ export class InstanceCache { `Cannot create a new record instance while the store is being destroyed`, !this.store.isDestroying && !this.store.isDestroyed ); - const recordData = this.getRecordData(identifier); + const recordData = DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK ? this.getRecordData(identifier) : this.store.cache; + + if (DEPRECATE_INSTANTIATE_RECORD_ARGS) { + if (this.store.instantiateRecord.length > 2) { + deprecate( + `Expected store.instantiateRecord to have an arity of 2. recordDataFor and notificationManager args have been deprecated.`, + false, + { + for: '@ember-data/store', + id: 'ember-data:deprecate-instantiate-record-args', + since: { available: '4.12', enabled: '4.12' }, + until: '5.0', + } + ); + } + record = this.store.instantiateRecord( + identifier, + properties || {}, + // @ts-expect-error + this.__recordDataFor, + this.store.notifications + ); + } else { + record = this.store.instantiateRecord(identifier, properties || {}); + } - record = this.store.instantiateRecord( - identifier, - properties || {}, - this.__recordDataFor, - this.store.notifications - ); setRecordIdentifier(record, identifier); - setRecordDataFor(record, recordData); + setCacheFor(record, recordData); StoreMap.set(record, this.store); this.__instances.record.set(identifier, record); @@ -267,7 +298,7 @@ export class InstanceCache { let recordData = this.__instances.recordData.get(identifier); if (DEPRECATE_V1CACHE_STORE_APIS) { - if (!recordData && this.store.createRecordDataFor.length > 2) { + if (!recordData && this.store.createRecordDataFor && this.store.createRecordDataFor.length > 2) { deprecate( `Store.createRecordDataFor(, , , ) has been deprecated in favor of Store.createRecordDataFor(, )`, false, @@ -295,19 +326,25 @@ export class InstanceCache { } if (!recordData) { - let recordDataInstance = this.store.createRecordDataFor(identifier, this._storeWrapper); - if (DEPRECATE_V1_RECORD_DATA) { - recordData = new NonSingletonCacheManager(this.store, recordDataInstance, identifier); + let recordDataInstance: Cache | CacheV1; + if (DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK) { + recordDataInstance = this.store.createRecordDataFor + ? this.store.createRecordDataFor(identifier, this._storeWrapper) + : this.store.cache; } else { - if (DEBUG) { - recordData = this.__cacheManager = this.__cacheManager || new SingletonCacheManager(); - (recordData as SingletonCacheManager)._addRecordData(identifier, recordDataInstance as Cache); + recordDataInstance = this.store.cache; + } + if (DEPRECATE_V1_RECORD_DATA) { + if (recordDataInstance.version !== '2') { + recordData = new NonSingletonCacheManager(this.store, recordDataInstance, identifier); } else { - recordData = recordDataInstance as Cache; + recordData = recordDataInstance; } + } else { + recordData = recordDataInstance as Cache; } - setRecordDataFor(identifier, recordData); + setCacheFor(identifier, recordData); this.__instances.recordData.set(identifier, recordData); if (LOG_INSTANCE_CACHE) { @@ -387,6 +424,9 @@ export class InstanceCache { } this.store.identifierCache.forgetRecordIdentifier(identifier); + this.__instances.recordData.delete(identifier); + removeRecordDataFor(identifier); + this.store._fetchManager.clearEntries(identifier); if (LOG_INSTANCE_CACHE) { // eslint-disable-next-line no-console console.log(`InstanceCache: disconnected ${String(identifier)}`); @@ -575,10 +615,6 @@ export class InstanceCache { } const recordData = this.getRecordData(identifier); - if (recordData.isNew(identifier)) { - this.store.notifications.notify(identifier, 'identity'); - } - const hasRecord = this.__instances.record.has(identifier); recordData.upsert(identifier, data, hasRecord); @@ -694,5 +730,5 @@ function _isLoading(cache: InstanceCache, identifier: StableRecordIdentifier): b export function _clearCaches() { RecordCache.clear(); StoreMap.clear(); - RecordDataForIdentifierCache.clear(); + CacheForIdentifierCache.clear(); } diff --git a/packages/store/src/-private/caches/record-data-for.ts b/packages/store/src/-private/caches/record-data-for.ts deleted file mode 100644 index e9375d6d92d..00000000000 --- a/packages/store/src/-private/caches/record-data-for.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { assert } from '@ember/debug'; - -import type { Cache } from '@ember-data/types/q/cache'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; -import type { RecordInstance } from '@ember-data/types/q/record-instance'; - -/* - * Returns the RecordData instance associated with a given - * Model or Identifier - */ - -export const RecordDataForIdentifierCache = new Map(); - -export function setRecordDataFor(identifier: StableRecordIdentifier | RecordInstance, recordData: Cache): void { - assert( - `Illegal set of identifier`, - !RecordDataForIdentifierCache.has(identifier) || RecordDataForIdentifierCache.get(identifier) === recordData - ); - RecordDataForIdentifierCache.set(identifier, recordData); -} - -export function removeRecordDataFor(identifier: StableRecordIdentifier | RecordInstance): void { - RecordDataForIdentifierCache.delete(identifier); -} - -export default function recordDataFor(instance: StableRecordIdentifier): Cache | null; -export default function recordDataFor(instance: RecordInstance): Cache; -export default function recordDataFor(instance: StableRecordIdentifier | RecordInstance): Cache | null { - if (RecordDataForIdentifierCache.has(instance as StableRecordIdentifier)) { - return RecordDataForIdentifierCache.get(instance as StableRecordIdentifier) as Cache; - } - - return null; -} diff --git a/packages/store/src/-private/index.ts b/packages/store/src/-private/index.ts index 738a0b93f9f..a1d16206235 100644 --- a/packages/store/src/-private/index.ts +++ b/packages/store/src/-private/index.ts @@ -58,4 +58,4 @@ export { default as SnapshotRecordArray } from './network/snapshot-record-array' // leaked for private use / test use, should investigate removing export { _clearCaches } from './caches/instance-cache'; -export { default as recordDataFor, removeRecordDataFor } from './caches/record-data-for'; +export { default as recordDataFor, removeRecordDataFor } from './caches/cache-utils'; diff --git a/packages/store/src/-private/managers/cache-manager.ts b/packages/store/src/-private/managers/cache-manager.ts index e7a16092360..672576ad56e 100644 --- a/packages/store/src/-private/managers/cache-manager.ts +++ b/packages/store/src/-private/managers/cache-manager.ts @@ -1,4 +1,4 @@ -import { assert, deprecate } from '@ember/debug'; +import { deprecate } from '@ember/debug'; import type { LocalRelationshipOperation } from '@ember-data/graph/-private/graph/-operations'; import type { Cache, CacheV1, ChangedAttributesHash, MergeOperation } from '@ember-data/types/q/cache'; @@ -753,109 +753,100 @@ export class NonSingletonCacheManager implements Cache { export class SingletonCacheManager implements Cache { version: '2' = '2'; - #recordDatas: Map; + #cache: Cache; - constructor() { - this.#recordDatas = new Map(); - } - - _addRecordData(identifier: StableRecordIdentifier, recordData: Cache) { - this.#recordDatas.set(identifier, recordData); - } - - #recordData(identifier: StableRecordIdentifier): Cache { - assert(`No RecordData Yet Exists!`, this.#recordDatas.has(identifier)); - return this.#recordDatas.get(identifier)!; + constructor(cache: Cache) { + this.#cache = cache; } // Cache // ===== upsert(identifier: StableRecordIdentifier, data: JsonApiResource, hasRecord?: boolean): void | string[] { - return this.#recordData(identifier).upsert(identifier, data, hasRecord); + return this.#cache.upsert(identifier, data, hasRecord); } patch(op: MergeOperation): void { - this.#recordData(op.record).patch(op); + this.#cache.patch(op); } clientDidCreate(identifier: StableRecordIdentifier, options?: Dict): Dict { - return this.#recordData(identifier).clientDidCreate(identifier, options); + return this.#cache.clientDidCreate(identifier, options); } willCommit(identifier: StableRecordIdentifier): void { - this.#recordData(identifier).willCommit(identifier); + this.#cache.willCommit(identifier); } didCommit(identifier: StableRecordIdentifier, data: JsonApiResource | null): void { - this.#recordData(identifier).didCommit(identifier, data); + this.#cache.didCommit(identifier, data); } commitWasRejected(identifier: StableRecordIdentifier, errors?: JsonApiValidationError[]): void { - this.#recordData(identifier).commitWasRejected(identifier, errors); + this.#cache.commitWasRejected(identifier, errors); } unloadRecord(identifier: StableRecordIdentifier): void { - this.#recordData(identifier).unloadRecord(identifier); + this.#cache.unloadRecord(identifier); } // Attrs // ===== getAttr(identifier: StableRecordIdentifier, propertyName: string): unknown { - return this.#recordData(identifier).getAttr(identifier, propertyName); + return this.#cache.getAttr(identifier, propertyName); } setAttr(identifier: StableRecordIdentifier, propertyName: string, value: unknown): void { - this.#recordData(identifier).setAttr(identifier, propertyName, value); + this.#cache.setAttr(identifier, propertyName, value); } changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash { - return this.#recordData(identifier).changedAttrs(identifier); + return this.#cache.changedAttrs(identifier); } hasChangedAttrs(identifier: StableRecordIdentifier): boolean { - return this.#recordData(identifier).hasChangedAttrs(identifier); + return this.#cache.hasChangedAttrs(identifier); } rollbackAttrs(identifier: StableRecordIdentifier): string[] { - return this.#recordData(identifier).rollbackAttrs(identifier); + return this.#cache.rollbackAttrs(identifier); } getRelationship( identifier: StableRecordIdentifier, propertyName: string ): SingleResourceRelationship | CollectionResourceRelationship { - return this.#recordData(identifier).getRelationship(identifier, propertyName); + return this.#cache.getRelationship(identifier, propertyName); } update(operation: LocalRelationshipOperation): void { - this.#recordData(operation.record).update(operation); + this.#cache.update(operation); } // State // ============= setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void { - this.#recordData(identifier).setIsDeleted(identifier, isDeleted); + this.#cache.setIsDeleted(identifier, isDeleted); } getErrors(identifier: StableRecordIdentifier): JsonApiValidationError[] { - return this.#recordData(identifier).getErrors(identifier); + return this.#cache.getErrors(identifier); } isEmpty(identifier: StableRecordIdentifier): boolean { - return this.#recordData(identifier).isEmpty(identifier); + return this.#cache.isEmpty(identifier); } isNew(identifier: StableRecordIdentifier): boolean { - return this.#recordData(identifier).isNew(identifier); + return this.#cache.isNew(identifier); } isDeleted(identifier: StableRecordIdentifier): boolean { - return this.#recordData(identifier).isDeleted(identifier); + return this.#cache.isDeleted(identifier); } isDeletionCommitted(identifier: StableRecordIdentifier): boolean { - return this.#recordData(identifier).isDeletionCommitted(identifier); + return this.#cache.isDeletionCommitted(identifier); } } diff --git a/packages/store/src/-private/managers/cache-store-wrapper.ts b/packages/store/src/-private/managers/cache-store-wrapper.ts index e4db4a6a13a..e1a5184051d 100644 --- a/packages/store/src/-private/managers/cache-store-wrapper.ts +++ b/packages/store/src/-private/managers/cache-store-wrapper.ts @@ -1,6 +1,9 @@ import { assert, deprecate } from '@ember/debug'; -import { DEPRECATE_V1CACHE_STORE_APIS } from '@ember-data/private-build-infra/deprecations'; +import { + DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK, + DEPRECATE_V1CACHE_STORE_APIS, +} from '@ember-data/private-build-infra/deprecations'; import type { Cache } from '@ember-data/types/q/cache'; import type { LegacyCacheStoreWrapper, @@ -241,13 +244,18 @@ class LegacyWrapper implements LegacyCacheStoreWrapper { identifier = type; } - const recordData = this._store._instanceCache.getRecordData(identifier); + const cache = DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK + ? this._store._instanceCache.getRecordData(identifier) + : this._store.cache; - if (!id && !lid) { - recordData.clientDidCreate(identifier); + if (DEPRECATE_V1CACHE_STORE_APIS) { + if (!id && !lid && typeof type === 'string') { + cache.clientDidCreate(identifier); + this._store.recordArrayManager.identifierAdded(identifier); + } } - return recordData; + return cache; } setRecordId(type: string | StableRecordIdentifier, id: string, lid?: string) { @@ -404,9 +412,23 @@ class V2CacheStoreWrapper implements StoreWrapper { } recordDataFor(identifier: StableRecordIdentifier): Cache { + if (DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK) { + deprecate( + `StoreWrapper.recordDataFor is deprecated. With Singleton Cache, this method is no longer needed as the caller is its own cache reference.`, + false, + { + for: '@ember-data/store', + id: 'ember-data:deprecate-record-data-for', + since: { available: '4.10', enabled: '4.10' }, + until: '5.0', + } + ); + } assert(`Expected a stable identifier`, isStableIdentifier(identifier)); - return this._store._instanceCache.getRecordData(identifier); + return DEPRECATE_CREATE_RECORD_DATA_FOR_HOOK + ? this._store._instanceCache.getRecordData(identifier) + : (void 0 as unknown as Cache); } setRecordId(identifier: StableRecordIdentifier, id: string) { diff --git a/packages/store/src/-private/store-service.ts b/packages/store/src/-private/store-service.ts index 11085c58192..4a3ec4c728e 100644 --- a/packages/store/src/-private/store-service.ts +++ b/packages/store/src/-private/store-service.ts @@ -19,7 +19,6 @@ import { DEPRECATE_JSON_API_FALLBACK, DEPRECATE_PROMISE_PROXIES, DEPRECATE_STORE_FIND, - DEPRECATE_V1CACHE_STORE_APIS, } from '@ember-data/private-build-infra/deprecations'; import type { Cache, CacheV1 } from '@ember-data/types/q/cache'; import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; @@ -40,6 +39,7 @@ import type { SchemaDefinitionService } from '@ember-data/types/q/schema-definit import type { FindOptions } from '@ember-data/types/q/store'; import type { Dict } from '@ember-data/types/q/utils'; +import peekCache, { setCacheFor } from './caches/cache-utils'; import { IdentifierCache } from './caches/identifier-cache'; import { InstanceCache, @@ -51,12 +51,11 @@ import { storeFor, StoreMap, } from './caches/instance-cache'; -import recordDataFor, { setRecordDataFor } from './caches/record-data-for'; import RecordReference from './legacy-model-support/record-reference'; import { DSModelSchemaDefinitionService, getModelFactory } from './legacy-model-support/schema-definition-service'; import type ShimModelClass from './legacy-model-support/shim-model-class'; import { getShimClass } from './legacy-model-support/shim-model-class'; -import type { NonSingletonCacheManager } from './managers/cache-manager'; +import { NonSingletonCacheManager, SingletonCacheManager } from './managers/cache-manager'; import NotificationManager from './managers/notification-manager'; import RecordArrayManager from './managers/record-array-manager'; import FetchManager, { SaveOp } from './network/fetch-manager'; @@ -167,9 +166,11 @@ export interface CreateRecordProperties { @extends Ember.Service */ -class Store { - __private_singleton_recordData!: Cache; +interface Store { + createRecordDataFor?(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper): Cache | CacheV1; +} +class Store { declare recordArrayManager: RecordArrayManager; /** @@ -338,16 +339,14 @@ class Store { * @method instantiateRecord (hook) * @param identifier * @param createRecordArgs - * @param recordDataFor - * @param notificationManager + * @param recordDataFor deprecated use this.cache + * @param notificationManager deprecated use this.notifications * @returns A record instance * @public */ instantiateRecord( identifier: StableRecordIdentifier, - createRecordArgs: { [key: string]: unknown }, - recordDataFor: (identifier: StableRecordIdentifier) => Cache, - notificationManager: NotificationManager + createRecordArgs: { [key: string]: unknown } ): DSModel | RecordInstance { if (HAS_MODEL_PACKAGE) { let modelName = identifier.type; @@ -2398,55 +2397,60 @@ class Store { /** * Instantiation hook allowing applications or addons to configure the store - * to utilize a custom RecordData implementation. + * to utilize a custom Cache implementation. * - * @method createRecordDataFor (hook) + * This hook should not be called directly by consuming applications or libraries. + * Use `Store.cache` to access the Cache instance. + * + * @method createCache (hook) * @public - * @param identifier * @param storeWrapper + * @returns {Cache} */ - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper): Cache | CacheV1 { + createCache(storeWrapper: CacheStoreWrapper): Cache { if (HAS_JSON_API_PACKAGE) { - // we can't greedily use require as this causes - // a cycle we can't easily fix (or clearly pin point) at present. - // - // it can be reproduced in partner tests by running - // node ./scripts/packages-for-commit.js && pnpm test-external:ember-observer if (_Cache === undefined) { _Cache = (importSync('@ember-data/json-api/-private') as typeof import('@ember-data/json-api/-private')).Cache; } - if (DEPRECATE_V1CACHE_STORE_APIS) { - if (arguments.length === 4) { - deprecate( - `Store.createRecordDataFor(, , , ) has been deprecated in favor of Store.createRecordDataFor(, )`, - false, - { - id: 'ember-data:deprecate-v1cache-store-apis', - for: 'ember-data', - until: '5.0', - since: { enabled: '4.7', available: '4.7' }, - } - ); - identifier = this.identifierCache.getOrCreateRecordIdentifier({ - type: arguments[0], - id: arguments[1], - lid: arguments[2], - }); - storeWrapper = arguments[3]; - } - } - - this.__private_singleton_recordData = this.__private_singleton_recordData || new _Cache(storeWrapper); - ( - this.__private_singleton_recordData as Cache & { createCache(identifier: StableRecordIdentifier): void } - ).createCache(identifier); - return this.__private_singleton_recordData; + return new _Cache(storeWrapper); } - assert(`Expected store.createRecordDataFor to be implemented but it wasn't`); + assert(`Expected store.createCache to be implemented but it wasn't`); + } + + /** + * Returns the cache instance associated to this Store, instantiates the Cache + * if necessary via `Store.createCache` + * + * @property {Cache} cache + * @public + */ + get cache(): Cache { + let { cache } = this._instanceCache; + if (!cache) { + cache = this._instanceCache.cache = this.createCache(this._instanceCache._storeWrapper); + if (DEBUG) { + cache = new SingletonCacheManager(cache); + } + } + return cache; } + /** + * [DEPRECATED] use Store.createCache + * + * Instantiation hook allowing applications or addons to configure the store + * to utilize a custom RecordData implementation. + * + * @method createRecordDataFor (hook) + * @deprecated + * @public + * @param identifier + * @param storeWrapper + * @returns {Cache} + */ + /** `normalize` converts a json payload into the normalized form that [push](../methods/push?anchor=push) expects. @@ -2858,7 +2862,7 @@ function extractIdentifierFromRecord( if (!recordOrPromiseRecord) { return null; } - const extract = isForV1 ? recordDataFor : recordIdentifierFor; + const extract = isForV1 ? peekCache : recordIdentifierFor; if (DEPRECATE_PROMISE_PROXIES) { if (isPromiseRecord(recordOrPromiseRecord)) { @@ -2894,5 +2898,5 @@ function isPromiseRecord(record: PromiseProxyRecord | RecordInstance): record is function secretInit(record: RecordInstance, recordData: Cache, identifier: StableRecordIdentifier, store: Store): void { setRecordIdentifier(record, identifier); StoreMap.set(record, store); - setRecordDataFor(record, recordData); + setCacheFor(record, recordData); } diff --git a/tests/adapter-encapsulation/app/services/store.js b/tests/adapter-encapsulation/app/services/store.js index 5b4733b1744..5614e02ca3e 100644 --- a/tests/adapter-encapsulation/app/services/store.js +++ b/tests/adapter-encapsulation/app/services/store.js @@ -2,9 +2,7 @@ import { Cache } from '@ember-data/json-api/-private'; import Store from '@ember-data/store'; export default class DefaultStore extends Store { - createRecordDataFor(identifier, storeWrapper) { - this.__private_singleton_recordData = this.__private_singleton_recordData || new Cache(storeWrapper); - this.__private_singleton_recordData.createCache(identifier); - return this.__private_singleton_recordData; + createCache(storeWrapper) { + return new Cache(storeWrapper); } } diff --git a/tests/docs/fixtures/expected.js b/tests/docs/fixtures/expected.js index f5cd817d7a9..ea4561ada8b 100644 --- a/tests/docs/fixtures/expected.js +++ b/tests/docs/fixtures/expected.js @@ -46,7 +46,7 @@ module.exports = { '(private) @ember-data/model Model#_notifyProperties', '(private) @ember-data/model Model#create', '(private) @ember-data/model Model#currentState', - '(private) @ember-data/json-api Cache#createCache', + '(private) @ember-data/json-api Cache#_createCache', '(private) @ember-data/serializer/json JSONSerializer#_canSerialize', '(private) @ember-data/serializer/json JSONSerializer#_getMappedKey', '(private) @ember-data/serializer/json JSONSerializer#_mustSerialize', @@ -408,6 +408,8 @@ module.exports = { '(public) @ember-data/store Store#unloadAll', '(public) @ember-data/store Store#unloadRecord', '(public) @ember-data/store Store#saveRecord', + '(public) @ember-data/store Store#cache', + '(public) @ember-data/store Store#createCache (hook)', '(public) @ember-data/store Store#createRecordDataFor (hook)', '(public) @ember-data/store Store#instantiateRecord (hook)', '(public) @ember-data/store Store#teardownRecord (hook)', diff --git a/tests/main/tests/integration/record-data/record-data-errors-test.ts b/tests/main/tests/integration/record-data/record-data-errors-test.ts index 33f02efc32d..eae0996ba5c 100644 --- a/tests/main/tests/integration/record-data/record-data-errors-test.ts +++ b/tests/main/tests/integration/record-data/record-data-errors-test.ts @@ -121,8 +121,9 @@ if (!DEPRECATE_V1_RECORD_DATA) { } } class TestStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { - return new LifecycleRecordData(); + createCache(wrapper: CacheStoreWrapper) { + // @ts-expect-error + return new LifecycleRecordData(wrapper) as Cache; } } class TestAdapter extends EmberObject { @@ -181,8 +182,9 @@ if (!DEPRECATE_V1_RECORD_DATA) { } } class TestStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { - return new LifecycleRecordData(); + createCache(wrapper: CacheStoreWrapper) { + // @ts-expect-error + return new LifecycleRecordData(wrapper) as Cache; } } class TestAdapter extends EmberObject { @@ -231,9 +233,10 @@ if (!DEPRECATE_V1_RECORD_DATA) { } class TestStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, sw: CacheStoreWrapper): Cache { - storeWrapper = sw; - return new LifecycleRecordData(); + createCache(wrapper: CacheStoreWrapper) { + storeWrapper = wrapper; + // @ts-expect-error + return new LifecycleRecordData(wrapper) as Cache; } } @@ -405,6 +408,10 @@ if (!DEPRECATE_V1_RECORD_DATA) { createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { return new TestRecordData(); } + createCache(wrapper: CacheStoreWrapper) { + // @ts-expect-error + return new TestRecordData(wrapper) as Cache; + } } hooks.beforeEach(function () { @@ -440,6 +447,10 @@ if (!DEPRECATE_V1_RECORD_DATA) { createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { return new LifecycleRecordData(); } + createCache(wrapper: CacheStoreWrapper) { + // @ts-expect-error + return new LifecycleRecordData(wrapper) as Cache; + } } let TestAdapter = EmberObject.extend({ @@ -500,6 +511,10 @@ if (!DEPRECATE_V1_RECORD_DATA) { createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { return new LifecycleRecordData(); } + createCache(wrapper: CacheStoreWrapper) { + // @ts-expect-error + return new LifecycleRecordData(wrapper) as Cache; + } } let TestAdapter = EmberObject.extend({ @@ -561,6 +576,10 @@ if (!DEPRECATE_V1_RECORD_DATA) { createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { return new LifecycleRecordData(wrapper); } + createCache(wrapper: CacheStoreWrapper) { + // @ts-expect-error + return new LifecycleRecordData(wrapper) as Cache; + } } owner.register('service:store', TestStore); diff --git a/tests/main/tests/integration/record-data/record-data-state-test.ts b/tests/main/tests/integration/record-data/record-data-state-test.ts index 8ffcdf54a7f..5439f4eae77 100644 --- a/tests/main/tests/integration/record-data/record-data-state-test.ts +++ b/tests/main/tests/integration/record-data/record-data-state-test.ts @@ -275,6 +275,10 @@ module('integration/record-data - Record Data State', function (hooks) { createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { return new LifecycleRecordData(wrapper, identifier); } + createCache(wrapper: CacheStoreWrapper) { + // @ts-expect-error + return new LifecycleRecordData(wrapper) as Cache; + } } let TestAdapter = EmberObject.extend({ @@ -371,6 +375,10 @@ module('integration/record-data - Record Data State', function (hooks) { createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { return new LifecycleRecordData(wrapper, identifier); } + createCache(wrapper: CacheStoreWrapper) { + // @ts-expect-error + return new LifecycleRecordData(wrapper) as Cache; + } } owner.register('service:store', TestStore); diff --git a/tests/main/tests/integration/record-data/record-data-test.ts b/tests/main/tests/integration/record-data/record-data-test.ts index a1e29cb173b..f82d4e9cfdd 100644 --- a/tests/main/tests/integration/record-data/record-data-test.ts +++ b/tests/main/tests/integration/record-data/record-data-test.ts @@ -13,6 +13,7 @@ import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store from '@ember-data/store'; import type { Cache, ChangedAttributesHash, MergeOperation } from '@ember-data/types/q/cache'; import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; +import { DSModel } from '@ember-data/types/q/ds-model'; import type { CollectionResourceRelationship, SingleResourceRelationship, @@ -197,48 +198,12 @@ class CustomStore extends Store { } } -let houseHash, davidHash, runspiredHash, igorHash; - module('integration/record-data - Custom RecordData Implementations', function (hooks) { setupTest(hooks); - let store; - hooks.beforeEach(function () { let { owner } = this; - houseHash = { - type: 'house', - id: '1', - attributes: { - name: 'Moomin', - }, - }; - - davidHash = { - type: 'person', - id: '1', - attributes: { - name: 'David', - }, - }; - - runspiredHash = { - type: 'person', - id: '2', - attributes: { - name: 'Runspired', - }, - }; - - igorHash = { - type: 'person', - id: '3', - attributes: { - name: 'Igor', - }, - }; - owner.register('model:person', Person); owner.register('model:house', House); owner.unregister('service:store'); @@ -248,8 +213,8 @@ module('integration/record-data - Custom RecordData Implementations', function ( }); test('A RecordData implementation that has the required spec methods should not error out', async function (assert) { - let { owner } = this; - store = owner.lookup('service:store'); + const { owner } = this; + const store: Store = owner.lookup('service:store') as Store; store.push({ data: [ @@ -371,6 +336,10 @@ module('integration/record-data - Custom RecordData Implementations', function ( createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { return new LifecycleRecordData(storeWrapper, identifier); } + createCache(storeWrapper: CacheStoreWrapper) { + // @ts-expect-error + return new LifecycleRecordData(storeWrapper) as Cache; + } } let TestAdapter = EmberObject.extend({ @@ -391,14 +360,14 @@ module('integration/record-data - Custom RecordData Implementations', function ( owner.register('service:store', TestStore); owner.register('adapter:application', TestAdapter, { singleton: false }); - store = owner.lookup('service:store'); + const store = owner.lookup('service:store') as Store; store.push({ data: [personHash], }); assert.strictEqual(calledUpsert, 1, 'Called upsert'); - let person = store.peekRecord('person', '1'); + let person = store.peekRecord('person', '1') as DSModel; person.save(); assert.strictEqual(calledWillCommit, 1, 'Called willCommit'); @@ -430,7 +399,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( calledRollbackAttributes = 0; calledDidCommit = 0; - let clientPerson = store.createRecord('person', { id: '2' }); + let clientPerson: DSModel = store.createRecord('person', { id: '2' }) as DSModel; assert.strictEqual(calledClientDidCreate, 1, 'Called clientDidCreate'); clientPerson.save(); @@ -513,17 +482,22 @@ module('integration/record-data - Custom RecordData Implementations', function ( createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { return new AttributeRecordData(storeWrapper, identifier); } + + createCache(storeWrapper: CacheStoreWrapper) { + // @ts-expect-error + return new AttributeRecordData(storeWrapper) as Cache; + } } owner.register('service:store', TestStore); - store = owner.lookup('service:store'); + const store = owner.lookup('service:store') as Store; store.push({ data: [personHash], }); - let person = store.peekRecord('person', '1'); + let person = store.peekRecord('person', '1') as DSModel; assert.strictEqual(person.name, 'new attribute'); assert.strictEqual(calledGet, 1, 'called getAttr for initial get'); person.set('name', 'new value'); @@ -542,394 +516,4 @@ module('integration/record-data - Custom RecordData Implementations', function ( assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 1 }); } }); - - test('Record Data controls belongsTo notifications', async function (assert) { - assert.expect(7); - - let { owner } = this; - let belongsToReturnValue; - - class RelationshipRecordData extends TestRecordData { - getBelongsTo(key: string) { - assert.strictEqual(key, 'landlord', 'Passed correct key to getBelongsTo'); - return belongsToReturnValue; - } - - getRelationship(identifier: StableRecordIdentifier, key: string) { - assert.strictEqual(key, 'landlord', 'Passed correct key to getBelongsTo'); - return belongsToReturnValue; - } - - // Use correct interface once imports have been fix - setDirtyBelongsTo(key: string, recordData: any) { - assert.strictEqual(key, 'landlord', 'Passed correct key to setBelongsTo'); - assert.strictEqual(recordData.getResourceIdentifier().id, '2', 'Passed correct RD to setBelongsTo'); - } - - update(operation: LocalRelationshipOperation) { - assert.strictEqual(operation.op, 'replaceRelatedRecord', 'Passed correct op to update'); - assert.strictEqual(operation.field, 'landlord', 'Passed correct key to update'); - assert.strictEqual( - operation.value ? (operation.value as StableRecordIdentifier).id : null, - '2', - 'Passed correct Identifier to update' - ); - } - } - - class TestStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { - if (identifier.type === 'house') { - return new RelationshipRecordData(storeWrapper, identifier); - } else { - return super.createRecordDataFor(identifier, storeWrapper); - } - } - } - - owner.register('service:store', TestStore); - - store = owner.lookup('service:store'); - belongsToReturnValue = { data: store.identifierCache.getOrCreateRecordIdentifier({ id: '1', type: 'person' }) }; - - store.push({ - data: [davidHash, runspiredHash], - }); - - store.push({ - data: [houseHash], - }); - - let house = store.peekRecord('house', '1'); - let runspired = store.peekRecord('person', '2'); - assert.strictEqual(house.landlord.name, 'David', 'belongsTo get correctly looked up'); - - house.set('landlord', runspired); - assert.strictEqual(house.landlord.name, 'David', 'belongsTo does not change if RD did not notify'); - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 1 }); - } - }); - - test('Record Data custom belongsTo', async function (assert) { - assert.expect(DEPRECATE_V1_RECORD_DATA ? 5 : 4); - let { owner } = this; - - let belongsToReturnValue; - - let RelationshipRecordData; - if (!DEPRECATE_V1_RECORD_DATA) { - RelationshipRecordData = class extends TestRecordData { - getRelationship(identifier: StableRecordIdentifier, key: string) { - assert.strictEqual(key, 'landlord', 'Passed correct key to getBelongsTo'); - return belongsToReturnValue; - } - - update(this: V2TestRecordData, operation: LocalRelationshipOperation) { - belongsToReturnValue = { - data: store.identifierCache.getOrCreateRecordIdentifier({ id: '3', type: 'person' }), - }; - this._storeWrapper.notifyChange(this._identifier, 'relationships', 'landlord'); - } - }; - } else { - RelationshipRecordData = class extends TestRecordData { - getBelongsTo(key: string) { - assert.strictEqual(key, 'landlord', 'Passed correct key to getBelongsTo'); - return belongsToReturnValue; - } - - setDirtyBelongsTo(this: V1TestRecordData, key: string, recordData: this | null) { - belongsToReturnValue = { - data: store.identifierCache.getOrCreateRecordIdentifier({ id: '3', type: 'person' }), - }; - this._storeWrapper.notifyChange(this._identifier, 'relationships', 'landlord'); - } - }; - } - class TestStore extends Store { - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { - if (identifier.type === 'house') { - return new RelationshipRecordData(storeWrapper, identifier); - } else { - return super.createRecordDataFor(identifier, storeWrapper); - } - } - } - - owner.register('service:store', TestStore); - - store = owner.lookup('service:store'); - belongsToReturnValue = { data: store.identifierCache.getOrCreateRecordIdentifier({ id: '1', type: 'person' }) }; - - store.push({ - data: [davidHash, runspiredHash, igorHash], - }); - - store.push({ - data: [houseHash], - }); - - let house = store.peekRecord('house', '1'); - assert.strictEqual(house.landlord.name, 'David', 'belongsTo get correctly looked up'); - - let runspired = store.peekRecord('person', '2'); - house.set('landlord', runspired); - - // This is intentionally !== runspired to test the custom RD implementation - assert.strictEqual(house.landlord.name, 'Igor', 'RecordData sets the custom belongsTo value'); - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 1 }); - } - }); - - test('Record Data controls hasMany notifications', async function (assert) { - assert.expect(DEPRECATE_V1_RECORD_DATA ? 12 : 11); - - let { owner } = this; - - let hasManyReturnValue; - let notifier, houseIdentifier; - class RelationshipRecordData extends TestRecordData { - getHasMany(key: string) { - return hasManyReturnValue; - } - getRelationship() { - return hasManyReturnValue; - } - addToHasMany(key: string, recordDatas: any[], idx?: number) { - if (!DEPRECATE_V1_RECORD_DATA) { - throw new Error('should not have called addToHasMany in v2'); - } else { - assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); - assert.strictEqual(recordDatas[0].getResourceIdentifier().id, '2', 'Passed correct RD to addToHasMany'); - } - } - removeFromHasMany(key: string, recordDatas: any[]) { - if (!DEPRECATE_V1_RECORD_DATA) { - throw new Error('should not have called removeFromHasMany in v2'); - } else { - assert.strictEqual(key, 'tenants', 'Passed correct key to removeFromHasMany'); - assert.strictEqual(recordDatas[0].getResourceIdentifier().id, '1', 'Passed correct RD to removeFromHasMany'); - } - } - setDirtyHasMany(key: string, recordDatas: any[]) { - assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); - assert.strictEqual(recordDatas[0].getResourceIdentifier().id, '3', 'Passed correct RD to addToHasMany'); - } - update(operation: LocalRelationshipOperation) { - if (operation.op === 'addToRelatedRecords') { - assert.strictEqual(operation.field, 'tenants'); - assert.deepEqual( - (operation.value as StableRecordIdentifier[]).map((r) => r.id), - ['2'] - ); - } else if (operation.op === 'removeFromRelatedRecords') { - assert.strictEqual(operation.field, 'tenants'); - assert.deepEqual( - (operation.value as StableRecordIdentifier[]).map((r) => r.id), - ['1'] - ); - } else if (operation.op === 'replaceRelatedRecords') { - assert.strictEqual(operation.field, 'tenants', 'Passed correct key to addToHasMany'); - assert.strictEqual(operation.value[0].id, '3', 'Passed correct RD to addToHasMany'); - } else { - throw new Error(`operation ${operation.op} not implemented in test yet`); - } - } - } - - class TestStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { - if (identifier.type === 'house') { - notifier = storeWrapper; - houseIdentifier = identifier; - return new RelationshipRecordData(storeWrapper, identifier); - } else { - return super.createRecordDataFor(identifier, storeWrapper); - } - } - } - - owner.register('service:store', TestStore); - - store = owner.lookup('service:store'); - hasManyReturnValue = { data: [store.identifierCache.getOrCreateRecordIdentifier({ id: '1', type: 'person' })] }; - - store.push({ - data: [davidHash, runspiredHash, igorHash], - }); - - store.push({ - data: [houseHash], - }); - - let house = store.peekRecord('house', '1'); - let people = house.tenants; - let david = store.peekRecord('person', '1'); - let runspired = store.peekRecord('person', '2'); - let igor = store.peekRecord('person', '3'); - - assert.deepEqual(people.slice(), [david], 'initial lookup is correct'); - - people.push(runspired); - assert.deepEqual(people.slice(), [david, runspired], 'push applies the change locally'); - - people.splice(people.indexOf(david), 1); - assert.deepEqual(people.slice(), [runspired], 'splice applies the change locally'); - - house.tenants = [igor]; - assert.deepEqual(people.slice(), [igor], 'replace applies the change locally'); - - notifier.notifyChange(houseIdentifier, 'relationships', 'tenants'); - await settled(); - assert.deepEqual(people.slice(), [david], 'final lookup is correct once notified'); - - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 1 }); - } - }); - - test('Record Data supports custom hasMany handling', async function (assert) { - assert.expect(DEPRECATE_V1_RECORD_DATA ? 11 : 10); - let { owner } = this; - - let hasManyReturnValue; - class RelationshipRecordData extends TestRecordData { - getHasMany(key: string) { - return hasManyReturnValue; - } - getRelationship() { - return hasManyReturnValue; - } - - addToHasMany(this: V1TestRecordData, key: string, recordDatas: any[], idx?: number) { - if (!DEPRECATE_V1_RECORD_DATA) { - throw new Error('should not have called addToHasMany in v2'); - } else { - assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); - assert.strictEqual(recordDatas[0].getResourceIdentifier().id, '2', 'Passed correct RD to addToHasMany'); - } - - hasManyReturnValue = { - data: [ - store.identifierCache.getOrCreateRecordIdentifier({ id: '3', type: 'person' }), - store.identifierCache.getOrCreateRecordIdentifier({ id: '2', type: 'person' }), - ], - }; - this._storeWrapper.notifyChange(this._identifier, 'relationships', 'tenants'); - } - - removeFromHasMany(this: V1TestRecordData, key: string, recordDatas: any[]) { - if (!DEPRECATE_V1_RECORD_DATA) { - throw new Error('should not have called removeFromHasMany in v2'); - } else { - assert.strictEqual(key, 'tenants', 'Passed correct key to removeFromHasMany'); - assert.strictEqual(recordDatas[0].getResourceIdentifier().id, '2', 'Passed correct RD to removeFromHasMany'); - } - hasManyReturnValue = { data: [store.identifierCache.getOrCreateRecordIdentifier({ id: '1', type: 'person' })] }; - this._storeWrapper.notifyChange(this._identifier, 'relationships', 'tenants'); - } - - setDirtyHasMany(this: V1TestRecordData, key: string, recordDatas: any[]) { - assert.strictEqual(key, 'tenants', 'Passed correct key to addToHasMany'); - assert.strictEqual(recordDatas[0].getResourceIdentifier().id, '3', 'Passed correct RD to addToHasMany'); - hasManyReturnValue = { - data: [ - store.identifierCache.getOrCreateRecordIdentifier({ id: '1', type: 'person' }), - store.identifierCache.getOrCreateRecordIdentifier({ id: '2', type: 'person' }), - ], - }; - this._storeWrapper.notifyChange(this._identifier, 'relationships', 'tenants'); - } - - update(this: V2TestRecordData, operation: LocalRelationshipOperation) { - if (operation.op === 'addToRelatedRecords') { - hasManyReturnValue = { - data: [ - store.identifierCache.getOrCreateRecordIdentifier({ id: '3', type: 'person' }), - store.identifierCache.getOrCreateRecordIdentifier({ id: '2', type: 'person' }), - ], - }; - assert.strictEqual(operation.field, 'tenants', 'correct field for addToRelatedRecords'); - assert.deepEqual( - (operation.value as StableRecordIdentifier[]).map((r) => r.id), - ['2'], - 'correct ids passed' - ); - } else if (operation.op === 'removeFromRelatedRecords') { - assert.strictEqual(operation.field, 'tenants', 'correct field for removeFromRelatedRecords'); - assert.deepEqual( - (operation.value as StableRecordIdentifier[]).map((r) => r.id), - ['2'], - 'correct ids passed' - ); - hasManyReturnValue = { - data: [store.identifierCache.getOrCreateRecordIdentifier({ id: '1', type: 'person' })], - }; - } else if (operation.op === 'replaceRelatedRecords') { - assert.strictEqual(operation.field, 'tenants', 'Passed correct key to addToHasMany'); - assert.strictEqual(operation.value[0].id, '3', 'Passed correct RD to addToHasMany'); - hasManyReturnValue = { - data: [ - store.identifierCache.getOrCreateRecordIdentifier({ id: '1', type: 'person' }), - store.identifierCache.getOrCreateRecordIdentifier({ id: '2', type: 'person' }), - ], - }; - } else { - throw new Error(`operation ${operation.op} not implemented in test yet`); - } - this._storeWrapper.notifyChange(this._identifier, 'relationships', 'tenants'); - } - } - - class TestStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { - if (identifier.type === 'house') { - return new RelationshipRecordData(storeWrapper, identifier); - } else { - return super.createRecordDataFor(identifier, storeWrapper); - } - } - } - - owner.register('service:store', TestStore); - - store = owner.lookup('service:store'); - hasManyReturnValue = { data: [store.identifierCache.getOrCreateRecordIdentifier({ id: '1', type: 'person' })] }; - - store.push({ - data: [davidHash, runspiredHash, igorHash], - }); - - store.push({ - data: [houseHash], - }); - - let house = store.peekRecord('house', '1'); - let people = house.tenants; - let david = store.peekRecord('person', '1'); - let runspired = store.peekRecord('person', '2'); - let igor = store.peekRecord('person', '3'); - - assert.deepEqual(people.slice(), [david], 'getHasMany correctly looked up'); - people.push(runspired); - - // This is intentionally !== [david, runspired] to test the custom RD implementation - assert.deepEqual(people.slice(), [igor, runspired], 'hasMany changes after notifying'); - - people.splice(people.indexOf(runspired), 1); - // This is intentionally !== [igor] to test the custom RD implementation - assert.deepEqual(people.slice(), [david], 'hasMany removal applies the change when notified'); - - house.set('tenants', [igor]); - // This is intentionally !== [igor] to test the custom RD implementation - assert.deepEqual(people.slice(), [david, runspired], 'setDirtyHasMany applies changes'); - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 1 }); - } - }); }); diff --git a/tests/main/tests/integration/record-data/store-wrapper-test.ts b/tests/main/tests/integration/record-data/store-wrapper-test.ts index a0e6768acaa..8b3ae886d92 100644 --- a/tests/main/tests/integration/record-data/store-wrapper-test.ts +++ b/tests/main/tests/integration/record-data/store-wrapper-test.ts @@ -1,11 +1,14 @@ +import { settled } from '@ember/test-helpers'; + import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import { DEPRECATE_V1_RECORD_DATA } from '@ember-data/private-build-infra/deprecations'; -import Store from '@ember-data/store'; +import Store, { recordIdentifierFor } from '@ember-data/store'; import { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; +import { DSModel } from '@ember-data/types/q/ds-model'; import { StableRecordIdentifier } from '@ember-data/types/q/identifier'; import publicProps from '@ember-data/unpublished-test-infra/test-support/public-props'; @@ -108,8 +111,6 @@ let houseHash, houseHash2; module('integration/store-wrapper - RecordData StoreWrapper tests', function (hooks) { setupTest(hooks); - let store; - hooks.beforeEach(function () { let { owner } = this; houseHash = { @@ -136,349 +137,291 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho }); test('Relationship definitions', async function (assert) { - assert.expect(DEPRECATE_V1_RECORD_DATA ? 4 : 3); - let { owner } = this; - - class RelationshipRD extends TestRecordData { - constructor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { - super(); - let houseAttrs = { - name: { - type: 'string', - isAttribute: true, - options: {}, - name: 'name', - }, - }; - - assert.deepEqual( - storeWrapper.getSchemaDefinitionService().attributesDefinitionFor({ type: 'house' }), - houseAttrs, - 'can lookup attribute definitions for self' - ); - - let carAttrs = { - make: { - type: 'string', - isAttribute: true, - options: {}, - name: 'make', - }, - }; - - assert.deepEqual( - storeWrapper.getSchemaDefinitionService().attributesDefinitionFor({ type: 'car' }), - carAttrs, - 'can lookup attribute definitions for other models' - ); - - let houseRelationships = { - landlord: { - key: 'landlord', - kind: 'belongsTo', - name: 'landlord', - type: 'person', - options: { async: false, inverse: null }, - }, - car: { - key: 'car', - kind: 'belongsTo', - name: 'car', - type: 'car', - options: { async: false, inverse: 'garage' }, - }, - tenants: { - key: 'tenants', - kind: 'hasMany', - name: 'tenants', - options: { async: false, inverse: null }, - type: 'person', - }, - }; - let schema = storeWrapper.getSchemaDefinitionService().relationshipsDefinitionFor({ type: 'house' }); - let result = publicProps(['key', 'kind', 'name', 'type', 'options'], schema); - - // Retrive only public values from the result - // This should go away once we put private things in symbols/weakmaps - assert.deepEqual(houseRelationships, result, 'can lookup relationship definitions'); - } - } + const { owner } = this; + let storeWrapper!: CacheStoreWrapper; class TestStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - if (identifier.type === 'house') { - return new RelationshipRD(identifier, wrapper); - } else { - return super.createRecordDataFor(identifier, wrapper); - } + createCache(wrapper: CacheStoreWrapper) { + storeWrapper = wrapper; + return super.createCache(wrapper); } } owner.register('service:store', TestStore); - store = owner.lookup('service:store'); + const store = owner.lookup('service:store') as Store; + store.cache; + + let houseAttrs = { + name: { + type: 'string', + isAttribute: true, + options: {}, + name: 'name', + }, + }; - store.push({ - data: [houseHash], - }); + assert.deepEqual( + storeWrapper.getSchemaDefinitionService().attributesDefinitionFor({ type: 'house' }), + houseAttrs, + 'can lookup attribute definitions for self' + ); - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 1 }); - } - }); + let carAttrs = { + make: { + type: 'string', + isAttribute: true, + options: {}, + name: 'make', + }, + }; - test('RecordDataFor', async function (assert) { - assert.expect(DEPRECATE_V1_RECORD_DATA ? 4 : 3); - let { owner } = this; + assert.deepEqual( + storeWrapper.getSchemaDefinitionService().attributesDefinitionFor({ type: 'car' }), + carAttrs, + 'can lookup attribute definitions for other models' + ); + + let houseRelationships = { + landlord: { + key: 'landlord', + kind: 'belongsTo', + name: 'landlord', + type: 'person', + options: { async: false, inverse: null }, + }, + car: { + key: 'car', + kind: 'belongsTo', + name: 'car', + type: 'car', + options: { async: false, inverse: 'garage' }, + }, + tenants: { + key: 'tenants', + kind: 'hasMany', + name: 'tenants', + options: { async: false, inverse: null }, + type: 'person', + }, + }; + let schema = storeWrapper.getSchemaDefinitionService().relationshipsDefinitionFor({ type: 'house' }); + let result = publicProps(['key', 'kind', 'name', 'type', 'options'], schema); - let count = 0; - class RecordDataForTest extends TestRecordData { - id: string; - - constructor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { - super(); - count++; - this.id = identifier.id!; - - if (count === 1) { - const identifier = storeWrapper.identifierCache.getOrCreateRecordIdentifier({ type: 'house', id: '2' }); - assert.strictEqual( - storeWrapper.recordDataFor(identifier).getAttr(identifier, 'name'), - 'ours name', - 'Can lookup another RecordData that has been loaded' - ); - const identifier2 = storeWrapper.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '1' }); - const recordData = storeWrapper.recordDataFor(identifier2); - const attrValue = recordData.getAttr(identifier2, 'name'); - assert.strictEqual(attrValue, 'Chris', 'Can lookup another RecordData which hasnt been loaded'); + // Retrive only public values from the result + // This should go away once we put private things in symbols/weakmaps + assert.deepEqual(houseRelationships, result, 'can lookup relationship definitions'); + }); + + if (DEPRECATE_V1_RECORD_DATA) { + test('RecordDataFor', async function (assert) { + assert.expect(DEPRECATE_V1_RECORD_DATA ? 4 : 3); + let { owner } = this; + + let count = 0; + class RecordDataForTest extends TestRecordData { + id: string; + + constructor(identifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { + super(); + count++; + this.id = identifier.id!; + + if (count === 1) { + const identifier = storeWrapper.identifierCache.getOrCreateRecordIdentifier({ type: 'house', id: '2' }); + assert.strictEqual( + storeWrapper.recordDataFor(identifier).getAttr(identifier, 'name'), + 'ours name', + 'Can lookup another RecordData that has been loaded' + ); + const identifier2 = storeWrapper.identifierCache.getOrCreateRecordIdentifier({ type: 'person', id: '1' }); + const recordData = storeWrapper.recordDataFor(identifier2); + const attrValue = recordData.getAttr(identifier2, 'name'); + assert.strictEqual(attrValue, 'Chris', 'Can lookup another RecordData which hasnt been loaded'); + } } - } - getAttr(identifier: StableRecordIdentifier, key: string): unknown { - return 'ours name'; + getAttr(identifier: StableRecordIdentifier, key: string): unknown { + return 'ours name'; + } } - } - class TestStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - if (identifier.type === 'house') { - return new RecordDataForTest(identifier, wrapper); - } else { - return super.createRecordDataFor(identifier, wrapper); + class TestStore extends Store { + // @ts-expect-error + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { + if (identifier.type === 'house') { + return new RecordDataForTest(identifier, wrapper); + } else { + return this.cache; + } } } - } - owner.register('service:store', TestStore); - store = owner.lookup('service:store'); + owner.register('service:store', TestStore); + const store = owner.lookup('service:store') as Store; - store.push({ - data: [{ id: '1', type: 'person', attributes: { name: 'Chris' } }, houseHash, houseHash2], - }); + store.push({ + data: [{ id: '1', type: 'person', attributes: { name: 'Chris' } }, houseHash, houseHash2], + }); - assert.strictEqual(count, 2, 'two TestRecordDatas have been created'); - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 2 }); - } - }); + assert.strictEqual(count, 2, 'two TestRecordDatas have been created'); + if (DEPRECATE_V1_RECORD_DATA) { + assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 2 }); + } + }); - test('recordDataFor - create new', async function (assert) { - assert.expect(DEPRECATE_V1_RECORD_DATA ? 4 : 3); - let { owner } = this; - let count = 0; - let recordData; - let newRecordData; - let firstIdentifier, secondIdentifier; - - class RecordDataForTest extends TestRecordData { - id: string; - _isNew: boolean = false; - - constructor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - super(); - count++; - this.id = identifier.id!; - - if (count === 1) { - const newIdentifier = wrapper.identifierCache.createIdentifierForNewRecord({ type: 'house' }); - recordData = wrapper.recordDataFor(newIdentifier); - firstIdentifier = newIdentifier; - recordData.clientDidCreate(newIdentifier); - } else if (count === 2) { - newRecordData = this; - secondIdentifier = identifier; + test('recordDataFor - create new', async function (assert) { + assert.expect(DEPRECATE_V1_RECORD_DATA ? 4 : 3); + let { owner } = this; + let count = 0; + let recordData; + let newRecordData; + let firstIdentifier, secondIdentifier; + + class RecordDataForTest extends TestRecordData { + id: string; + _isNew: boolean = false; + + constructor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { + super(); + count++; + this.id = identifier.id!; + + if (count === 1) { + const newIdentifier = wrapper.identifierCache.createIdentifierForNewRecord({ type: 'house' }); + recordData = wrapper.recordDataFor(newIdentifier); + firstIdentifier = newIdentifier; + recordData.clientDidCreate(newIdentifier); + } else if (count === 2) { + newRecordData = this; + secondIdentifier = identifier; + } } - } - clientDidCreate() { - this._isNew = true; - } + clientDidCreate() { + this._isNew = true; + } - isNew() { - return this._isNew; + isNew() { + return this._isNew; + } } - } - class TestStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - if (identifier.type === 'house') { - return new RecordDataForTest(identifier, wrapper); - } else { - return super.createRecordDataFor(identifier, wrapper); + class TestStore extends Store { + // @ts-expect-error + createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { + if (identifier.type === 'house') { + return new RecordDataForTest(identifier, wrapper); + } else { + return this.cache; + } } } - } - owner.register('service:store', TestStore); - store = owner.lookup('service:store'); + owner.register('service:store', TestStore); + const store = owner.lookup('service:store') as Store; - store.push({ - data: { - type: 'house', - id: '1', - attributes: { - bedrooms: 1, + store.push({ + data: { + type: 'house', + id: '1', + attributes: { + bedrooms: 1, + }, }, - }, - }); + }); - assert.ok(recordData.isNew(firstIdentifier), 'Our RecordData is new'); - assert.ok( - newRecordData.isNew(secondIdentifier), - 'The recordData for a RecordData created via Wrapper.recordDataFor(type) is in the "new" state' - ); + assert.ok(recordData.isNew(firstIdentifier), 'Our RecordData is new'); + assert.ok( + newRecordData.isNew(secondIdentifier), + 'The recordData for a RecordData created via Wrapper.recordDataFor(type) is in the "new" state' + ); - assert.strictEqual(count, 2, 'two TestRecordDatas have been created'); - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 2 }); - } - }); + assert.strictEqual(count, 2, 'two TestRecordDatas have been created'); + if (DEPRECATE_V1_RECORD_DATA) { + assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 2 }); + } + }); + } test('setRecordId', async function (assert) { - assert.expect(DEPRECATE_V1_RECORD_DATA ? 3 : 2); - let { owner } = this; - - class RecordDataForTest extends TestRecordData { - id: string; - - constructor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - super(); - wrapper.setRecordId(identifier, '17'); - this.id = '17'; - } - } + const { owner } = this; + let storeWrapper!: CacheStoreWrapper; class TestStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - if (identifier.type === 'house') { - return new RecordDataForTest(identifier, wrapper); - } else { - return super.createRecordDataFor(identifier, wrapper); - } + createCache(wrapper: CacheStoreWrapper) { + storeWrapper = wrapper; + return super.createCache(wrapper); } } owner.register('service:store', TestStore); - store = owner.lookup('service:store'); + const store = owner.lookup('service:store') as Store; - let house = store.createRecord('house'); + let house = store.createRecord('house', {}) as DSModel; + storeWrapper.setRecordId(recordIdentifierFor(house), '17'); assert.strictEqual(house.id, '17', 'setRecordId correctly set the id'); assert.strictEqual( store.peekRecord('house', '17'), house, 'can lookup the record from the identify map based on the new id' ); - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 1 }); - } }); test('hasRecord', async function (assert) { - assert.expect(DEPRECATE_V1_RECORD_DATA ? 5 : 4); - let { owner } = this; - - class RecordDataForTest extends TestRecordData { - constructor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - super(); - if (!identifier.id) { - const id1 = wrapper.identifierCache.getOrCreateRecordIdentifier({ type: 'house', id: '1' }); - const id2 = wrapper.identifierCache.getOrCreateRecordIdentifier({ type: 'house', id: '2' }); - assert.true(wrapper.hasRecord(id1), 'house 1 is in use'); - assert.false(wrapper.hasRecord(id2), 'house 2 is not in use'); - } else { - assert.ok(true, 'we created a recordData'); - } - } - } + const { owner } = this; + let storeWrapper!: CacheStoreWrapper; class TestStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - if (identifier.type === 'house') { - return new RecordDataForTest(identifier, wrapper); - } else { - return super.createRecordDataFor(identifier, wrapper); - } + createCache(wrapper: CacheStoreWrapper) { + storeWrapper = wrapper; + return super.createCache(wrapper); } } owner.register('service:store', TestStore); - store = owner.lookup('service:store'); + const store = owner.lookup('service:store') as Store; store.push({ data: [houseHash, houseHash2], }); - store.peekRecord('house', 1); + store.peekRecord('house', '1'); // TODO isRecordInUse returns true if record has never been instantiated, think through whether thats correct - let house2 = store.peekRecord('house', 2); + let house2 = store.peekRecord('house', '2') as DSModel; house2.unloadRecord(); - store.createRecord('house'); - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 3 }); - } + store.createRecord('house', {}); + const id1 = storeWrapper.identifierCache.getOrCreateRecordIdentifier({ type: 'house', id: '1' }); + const id2 = storeWrapper.identifierCache.getOrCreateRecordIdentifier({ type: 'house', id: '2' }); + assert.true(storeWrapper.hasRecord(id1), 'house 1 is in use'); + assert.false(storeWrapper.hasRecord(id2), 'house 2 is not in use'); }); test('disconnectRecord', async function (assert) { - assert.expect(DEPRECATE_V1_RECORD_DATA ? 2 : 1); - let { owner } = this; - let wrapper; - let identifier; - - class RecordDataForTest extends TestRecordData { - constructor(stableIdentifier: StableRecordIdentifier, storeWrapper: CacheStoreWrapper) { - super(); - wrapper = storeWrapper; - identifier = stableIdentifier; - } - } + const { owner } = this; + let storeWrapper!: CacheStoreWrapper; class TestStore extends Store { - // @ts-expect-error - createRecordDataFor(identifier: StableRecordIdentifier, wrapper: CacheStoreWrapper) { - if (identifier.type === 'house') { - return new RecordDataForTest(identifier, wrapper); - } else { - return super.createRecordDataFor(identifier, wrapper); - } + createCache(wrapper: CacheStoreWrapper) { + storeWrapper = wrapper; + return super.createCache(wrapper); } } owner.register('service:store', TestStore); - store = owner.lookup('service:store'); + const store = owner.lookup('service:store') as Store; - store.push({ - data: [], - included: [houseHash], + const identifier = store._push({ + data: { + type: 'house', + id: '1', + attributes: { + name: 'Moomin', + }, + }, }); - wrapper.disconnectRecord(identifier); + storeWrapper.disconnectRecord(identifier as StableRecordIdentifier); + await settled(); assert.strictEqual(store.peekRecord('house', '1'), null, 'record was removed from id map'); - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 1 }); - } }); }); diff --git a/tests/main/tests/integration/record-data/unloading-record-data-test.js b/tests/main/tests/integration/record-data/unloading-record-data-test.js index b254f922eb8..133f669ac9d 100644 --- a/tests/main/tests/integration/record-data/unloading-record-data-test.js +++ b/tests/main/tests/integration/record-data/unloading-record-data-test.js @@ -1,7 +1,6 @@ import { run } from '@ember/runloop'; import { module, test } from 'qunit'; -import { resolve } from 'rsvp'; import { setupTest } from 'ember-qunit'; @@ -172,208 +171,91 @@ module('RecordData Compatibility', function (hooks) { const CustomRecordData = DEPRECATE_V1_RECORD_DATA ? V1CustomRecordData : V2CustomRecordData; - test(`store.unloadRecord on a record with default RecordData with relationship to a record with custom RecordData does not error`, async function (assert) { - const originalCreateRecordDataFor = store.createRecordDataFor; - let customCalled = 0, - customCalledFor = [], - originalCalled = 0, - originalCalledFor = []; - store.createRecordDataFor = function provideCustomRecordData(identifier, storeWrapper) { - if (identifier.type === 'pet') { - customCalled++; - customCalledFor.push(identifier); - return new CustomRecordData(identifier, storeWrapper); - } else { - originalCalled++; - originalCalledFor.push(identifier); - return originalCreateRecordDataFor.call(this, identifier, storeWrapper); - } - }; + if (DEPRECATE_V1_RECORD_DATA) { + test(`store.unloadRecord on a record with default RecordData with relationship to a record with custom RecordData does not error`, async function (assert) { + let customCalled = 0, + customCalledFor = [], + originalCalled = 0, + originalCalledFor = []; + store.createRecordDataFor = function provideCustomRecordData(identifier, storeWrapper) { + if (identifier.type === 'pet') { + customCalled++; + customCalledFor.push(identifier); + return new CustomRecordData(identifier, storeWrapper); + } else { + originalCalled++; + originalCalledFor.push(identifier); + return this.cache; + } + }; - let chris = store.push({ - data: { - type: 'person', - id: '1', - attributes: { name: 'Chris' }, - relationships: { - pets: { - data: [ - { type: 'pet', id: '1' }, - { type: 'pet', id: '2' }, - ], - }, - }, - }, - included: [ - { - type: 'pet', + let chris = store.push({ + data: { + type: 'person', id: '1', - attributes: { name: 'Shen' }, + attributes: { name: 'Chris' }, relationships: { - owner: { data: { type: 'person', id: '1' } }, - }, - }, - { - type: 'pet', - id: '2', - attributes: { name: 'Prince' }, - relationships: { - owner: { data: { type: 'person', id: '1' } }, - }, - }, - ], - }); - let pets = chris.pets; - let shen = pets.at(0); - - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 2 }); - } - - assert.strictEqual(shen.name, 'Shen', 'We found Shen'); - assert.strictEqual(customCalled, 2, 'we used the custom record-data for pet'); - assert.deepEqual( - customCalledFor.map((i) => { - return { type: i.type, id: i.id }; - }), - [ - { id: '1', type: 'pet' }, - { id: '2', type: 'pet' }, - ], - 'we used the cutom record-data for the correct pets' - ); - assert.strictEqual(originalCalled, 1, 'we used the default record-data for person'); - assert.deepEqual( - originalCalledFor.map((i) => { - return { type: i.type, id: i.id }; - }), - [{ id: '1', type: 'person' }], - 'we used the default record-data for the correct person' - ); - - try { - run(() => chris.unloadRecord()); - assert.ok(true, 'expected `unloadRecord()` not to throw'); - } catch (e) { - assert.ok(false, 'expected `unloadRecord()` not to throw'); - } - }); - - test(`store.unloadRecord on a record with custom RecordData with relationship to a record with default RecordData does not error`, async function (assert) { - const originalCreateRecordDataFor = store.createModelDataFor; - store.createModelDataFor = function provideCustomRecordData(identifier, storeWrapper) { - if (identifier.type === 'pet') { - return new CustomRecordData(identifier, storeWrapper); - } else { - return originalCreateRecordDataFor.call(this, identifier, storeWrapper); - } - }; - - let chris = store.push({ - data: { - type: 'person', - id: '1', - attributes: { name: 'Chris' }, - relationships: { - pets: { - data: [ - { type: 'pet', id: '1' }, - { type: 'pet', id: '2' }, - ], + pets: { + data: [ + { type: 'pet', id: '1' }, + { type: 'pet', id: '2' }, + ], + }, }, }, - }, - included: [ - { - type: 'pet', - id: '1', - attributes: { name: 'Shen' }, - relationships: { - owner: { data: { type: 'person', id: '1' } }, + included: [ + { + type: 'pet', + id: '1', + attributes: { name: 'Shen' }, + relationships: { + owner: { data: { type: 'person', id: '1' } }, + }, }, - }, - { - type: 'pet', - id: '2', - attributes: { name: 'Prince' }, - relationships: { - owner: { data: { type: 'person', id: '1' } }, + { + type: 'pet', + id: '2', + attributes: { name: 'Prince' }, + relationships: { + owner: { data: { type: 'person', id: '1' } }, + }, }, - }, - ], - }); - let pets = chris.pets; - let shen = pets.at(0); - - assert.strictEqual(shen.name, 'Shen', 'We found Shen'); - - try { - run(() => shen.unloadRecord()); - assert.ok(true, 'expected `unloadRecord()` not to throw'); - } catch (e) { - assert.ok(false, 'expected `unloadRecord()` not to throw'); - } - }); + ], + }); + let pets = chris.pets; + let shen = pets.at(0); - test(`store.findRecord does not eagerly instantiate record data`, async function (assert) { - let recordDataInstances = 0; - class TestRecordData extends CustomRecordData { - constructor() { - super(...arguments); - ++recordDataInstances; + if (DEPRECATE_V1_RECORD_DATA) { + assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 2 }); } - } - - store.createRecordDataFor = function (identifier, storeWrapper) { - return new TestRecordData(identifier, storeWrapper); - }; - this.owner.register( - 'adapter:pet', - class TestAdapter { - static create() { - return new TestAdapter(...arguments); - } - findRecord() { - assert.strictEqual( - recordDataInstances, - 0, - 'no instance created from findRecord before adapter promise resolves' - ); - - return resolve({ - data: { - id: '1', - type: 'pet', - attributes: { - name: 'Loki', - }, - }, - }); - } + assert.strictEqual(shen.name, 'Shen', 'We found Shen'); + assert.strictEqual(customCalled, 2, 'we used the custom record-data for pet'); + assert.deepEqual( + customCalledFor.map((i) => { + return { type: i.type, id: i.id }; + }), + [ + { id: '1', type: 'pet' }, + { id: '2', type: 'pet' }, + ], + 'we used the cutom record-data for the correct pets' + ); + assert.strictEqual(originalCalled, 1, 'we used the default record-data for person'); + assert.deepEqual( + originalCalledFor.map((i) => { + return { type: i.type, id: i.id }; + }), + [{ id: '1', type: 'person' }], + 'we used the default record-data for the correct person' + ); + + try { + run(() => chris.unloadRecord()); + assert.ok(true, 'expected `unloadRecord()` not to throw'); + } catch (e) { + assert.ok(false, 'expected `unloadRecord()` not to throw'); } - ); - this.owner.register( - 'serializer:pet', - class TestSerializer { - static create() { - return new TestSerializer(...arguments); - } - - normalizeResponse(store, modelClass, payload) { - return payload; - } - } - ); - - assert.strictEqual(recordDataInstances, 0, 'initially no instances'); - - await store.findRecord('pet', '1'); - - if (DEPRECATE_V1_RECORD_DATA) { - assert.expectDeprecation({ id: 'ember-data:deprecate-v1-cache', count: 1 }); - } - - assert.strictEqual(recordDataInstances, 1, 'record data created after promise fulfills'); - }); + }); + } }); diff --git a/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts b/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts index 29b6322c4db..422d5c36bae 100644 --- a/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts +++ b/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts @@ -1,3 +1,5 @@ +import { TestContext } from '@ember/test-helpers'; + import { module, test } from 'qunit'; import RSVP from 'rsvp'; @@ -7,13 +9,12 @@ import JSONAPIAdapter from '@ember-data/adapter/json-api'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store from '@ember-data/store'; import type { Snapshot } from '@ember-data/store/-private'; -import type NotificationManager from '@ember-data/store/-private/managers/notification-manager'; import { Cache } from '@ember-data/types/q/cache'; -import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper'; import type { RecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type { AttributesSchema, RelationshipsSchema } from '@ember-data/types/q/record-data-schemas'; import type { RecordInstance } from '@ember-data/types/q/record-instance'; import type { SchemaDefinitionService } from '@ember-data/types/q/schema-definition-service'; +import { deprecatedTest } from '@ember-data/unpublished-test-infra/test-support/deprecated-test'; module('unit/model - Custom Class Model', function (hooks) { let store: Store; @@ -50,7 +51,7 @@ module('unit/model - Custom Class Model', function (hooks) { }, }); } - instantiateRecord(identifier, createOptions, recordDataFor, notificationManager) { + instantiateRecord(identifier, createOptions) { return new Person(this); } teardownRecord(record) {} @@ -75,21 +76,10 @@ module('unit/model - Custom Class Model', function (hooks) { assert.expect(7); let notificationCount = 0; let identifier; - let storeWrapper; class CreationStore extends CustomStore { - createRecordDataFor(identifier: StableRecordIdentifier, sw: CacheStoreWrapper) { - let rd = super.createRecordDataFor(identifier, sw); - storeWrapper = sw; - return rd; - } - instantiateRecord( - id: StableRecordIdentifier, - createRecordArgs, - recordDataFor, - notificationManager: NotificationManager - ): Object { + instantiateRecord(id: StableRecordIdentifier, createRecordArgs): Object { identifier = id; - notificationManager.subscribe(identifier, (passedId, key) => { + this.notifications.subscribe(identifier, (passedId, key) => { notificationCount++; assert.strictEqual(passedId, identifier, 'passed the identifier to the callback'); if (notificationCount === 1) { @@ -104,7 +94,8 @@ module('unit/model - Custom Class Model', function (hooks) { } } this.owner.register('service:store', CreationStore); - store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as Store; + const storeWrapper = store._instanceCache._storeWrapper; store.push({ data: { id: '1', type: 'person', attributes: { name: 'chris' } } }); // emulate this happening within a single push store._join(() => { @@ -121,7 +112,7 @@ module('unit/model - Custom Class Model', function (hooks) { assert.expect(5); let returnValue; class CreationStore extends CustomStore { - instantiateRecord(identifier, createRecordArgs, recordDataFor, notificationManager) { + instantiateRecord(identifier, createRecordArgs) { assert.strictEqual(identifier.type, 'person', 'Identifier type passed in correctly'); assert.deepEqual(createRecordArgs, { otherProp: 'unk' }, 'createRecordArg passed in'); returnValue = {}; @@ -138,41 +129,46 @@ module('unit/model - Custom Class Model', function (hooks) { assert.deepEqual(returnValue, person, 'record instantiating does not modify the returned value'); }); - test('recordData lookup', function (assert) { - assert.expect(1); - let rd; - class CreationStore extends Store { - instantiateRecord(identifier, createRecordArgs, recordDataFor, notificationManager) { - rd = recordDataFor(identifier); - assert.strictEqual(rd.getAttr(identifier, 'name'), 'chris', 'Can look up record data from recordDataFor'); - return {}; + deprecatedTest( + 'recordData lookup', + { id: 'ember-data:deprecate-instantiate-record-args', count: 1, until: '5.0' }, + function (this: TestContext, assert: Assert) { + assert.expect(1); + let rd; + class CreationStore extends Store { + // @ts-expect-error + instantiateRecord(identifier, createRecordArgs, recordDataFor, notificationManager) { + rd = recordDataFor(identifier); + assert.strictEqual(rd.getAttr(identifier, 'name'), 'chris', 'Can look up record data from recordDataFor'); + return {}; + } + teardownRecord(record) {} } - teardownRecord(record) {} - } - this.owner.register('service:store', CreationStore); - store = this.owner.lookup('service:store') as Store; - let schema: SchemaDefinitionService = { - attributesDefinitionFor({ type: string }): AttributesSchema { - return { - name: { - type: 'string', - options: {}, - name: 'name', - kind: 'attribute', - }, - }; - }, - relationshipsDefinitionFor({ type: string }): RelationshipsSchema { - return {}; - }, - doesTypeExist() { - return true; - }, - }; - store.registerSchemaDefinitionService(schema); + this.owner.register('service:store', CreationStore); + store = this.owner.lookup('service:store') as Store; + let schema: SchemaDefinitionService = { + attributesDefinitionFor({ type: string }): AttributesSchema { + return { + name: { + type: 'string', + options: {}, + name: 'name', + kind: 'attribute', + }, + }; + }, + relationshipsDefinitionFor({ type: string }): RelationshipsSchema { + return {}; + }, + doesTypeExist() { + return true; + }, + }; + store.registerSchemaDefinitionService(schema); - store.createRecord('person', { name: 'chris' }); - }); + store.createRecord('person', { name: 'chris' }); + } + ); test('attribute and relationship with custom schema definition', async function (assert) { assert.expect(18); @@ -232,7 +228,7 @@ module('unit/model - Custom Class Model', function (hooks) { }) ); class CustomStore extends Store { - instantiateRecord(identifier, createOptions, recordDataFor, notificationManager) { + instantiateRecord(identifier, createOptions) { return new Person(this); } teardownRecord(record) {} @@ -316,7 +312,6 @@ module('unit/model - Custom Class Model', function (hooks) { }); test('store.deleteRecord', async function (assert) { - let rd: Cache; let ident: StableRecordIdentifier; assert.expect(10); this.owner.register( @@ -331,13 +326,12 @@ module('unit/model - Custom Class Model', function (hooks) { ); const subscribedValues: string[] = []; class CreationStore extends CustomStore { - instantiateRecord(identifier, createRecordArgs, recordDataFor, notificationManager) { + instantiateRecord(identifier, createRecordArgs) { ident = identifier; - rd = recordDataFor(identifier); - assert.false(rd.isDeleted(identifier), 'we are not deleted when we start'); - notificationManager.subscribe(identifier, (passedId, key) => { + assert.false(this.cache.isDeleted(identifier), 'we are not deleted when we start'); + this.notifications.subscribe(identifier, (passedId, key) => { subscribedValues.push(key); - assert.true(recordDataFor(identifier).isDeleted(identifier), 'we have been marked as deleted'); + assert.true(this.cache.isDeleted(identifier), 'we have been marked as deleted'); }); return {}; } @@ -346,7 +340,8 @@ module('unit/model - Custom Class Model', function (hooks) { } } this.owner.register('service:store', CreationStore); - store = this.owner.lookup('service:store') as Store; + const store = this.owner.lookup('service:store') as Store; + const rd: Cache = store.cache; let person = store.push({ data: { type: 'person', id: '1', attributes: { name: 'chris' } } }); store.deleteRecord(person); assert.true(rd!.isDeleted(ident!), 'record has been marked as deleted'); @@ -369,7 +364,7 @@ module('unit/model - Custom Class Model', function (hooks) { }) ); class CustomStore extends Store { - instantiateRecord(identifier, createOptions, recordDataFor, notificationManager) { + instantiateRecord(identifier, createOptions) { return new Person(this); } teardownRecord(record) {} diff --git a/tests/serializer-encapsulation/app/services/store.js b/tests/serializer-encapsulation/app/services/store.js index 5b4733b1744..5614e02ca3e 100644 --- a/tests/serializer-encapsulation/app/services/store.js +++ b/tests/serializer-encapsulation/app/services/store.js @@ -2,9 +2,7 @@ import { Cache } from '@ember-data/json-api/-private'; import Store from '@ember-data/store'; export default class DefaultStore extends Store { - createRecordDataFor(identifier, storeWrapper) { - this.__private_singleton_recordData = this.__private_singleton_recordData || new Cache(storeWrapper); - this.__private_singleton_recordData.createCache(identifier); - return this.__private_singleton_recordData; + createCache(storeWrapper) { + return new Cache(storeWrapper); } } diff --git a/tsconfig.json b/tsconfig.json index d4fb8d23bf5..f124b063638 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -41,7 +41,7 @@ "packages/store/src/-private/legacy-model-support/schema-definition-service.ts", "packages/store/src/-private/network/request-cache.ts", "packages/store/src/-private/managers/notification-manager.ts", - "packages/store/src/-private/caches/record-data-for.ts", + "packages/store/src/-private/caches/cache-utils.ts", "packages/store/src/-private/utils/normalize-model-name.ts", "packages/store/src/-private/legacy-model-support/shim-model-class.ts", "packages/store/src/-private/network/fetch-manager.ts",