diff --git a/packages/-ember-data/addon/store.ts b/packages/-ember-data/addon/store.ts index 320780c8a6c..bf7296a7d70 100644 --- a/packages/-ember-data/addon/store.ts +++ b/packages/-ember-data/addon/store.ts @@ -20,6 +20,7 @@ import BaseStore, { CacheHandler } from '@ember-data/store'; import type { Cache } from '@warp-drive/core-types/cache'; import type { CacheCapabilitiesManager } from '@ember-data/store/-types/q/cache-store-wrapper'; import type { ModelSchema } from '@ember-data/store/-types/q/ds-model'; +import { TypeFromInstance } from '@warp-drive/core-types/record'; function hasRequestManager(store: BaseStore): boolean { return 'requestManager' in store; @@ -55,6 +56,8 @@ export default class Store extends BaseStore { teardownRecord.call(this, record); } + override modelFor(type: TypeFromInstance): ModelSchema; + override modelFor(type: string): ModelSchema; override modelFor(type: string): ModelSchema { return modelFor.call(this, type) || super.modelFor(type); } diff --git a/packages/core-types/src/identifier.ts b/packages/core-types/src/identifier.ts index 58c8cb4f983..605c5ef1d07 100644 --- a/packages/core-types/src/identifier.ts +++ b/packages/core-types/src/identifier.ts @@ -17,14 +17,14 @@ export interface Identifier { clientId?: string; } -export interface ExistingRecordIdentifier extends Identifier { +export interface ExistingRecordIdentifier extends Identifier { id: string; - type: string; + type: T; } -export interface NewRecordIdentifier extends Identifier { +export interface NewRecordIdentifier extends Identifier { id: string | null; - type: string; + type: T; } export type StableDocumentIdentifier = { @@ -42,7 +42,7 @@ export type StableDocumentIdentifier = { * * @internal */ -export type RecordIdentifier = ExistingRecordIdentifier | NewRecordIdentifier; +export type RecordIdentifier = ExistingRecordIdentifier | NewRecordIdentifier; /** * Used when an Identifier is known to be the stable version @@ -63,9 +63,9 @@ export interface StableIdentifier extends Identifier { * * @internal */ -export interface StableExistingRecordIdentifier extends StableIdentifier { +export interface StableExistingRecordIdentifier extends StableIdentifier { id: string; - type: string; + type: T; [DEBUG_CLIENT_ORIGINATED]?: boolean; [CACHE_OWNER]: number | undefined; [DEBUG_STALE_CACHE_OWNER]?: number | undefined; @@ -82,9 +82,9 @@ export interface StableExistingRecordIdentifier extends StableIdentifier { * * @internal */ -export interface StableNewRecordIdentifier extends StableIdentifier { +export interface StableNewRecordIdentifier extends StableIdentifier { id: string | null; - type: string; + type: T; [DEBUG_CLIENT_ORIGINATED]?: boolean; [CACHE_OWNER]: number | undefined; [DEBUG_STALE_CACHE_OWNER]?: number | undefined; @@ -120,4 +120,6 @@ export interface StableNewRecordIdentifier extends StableIdentifier { * @property {string | null} id * @public */ -export type StableRecordIdentifier = StableExistingRecordIdentifier | StableNewRecordIdentifier; +export type StableRecordIdentifier = + | StableExistingRecordIdentifier + | StableNewRecordIdentifier; diff --git a/packages/core-types/src/record.ts b/packages/core-types/src/record.ts new file mode 100644 index 00000000000..18eeb5a2396 --- /dev/null +++ b/packages/core-types/src/record.ts @@ -0,0 +1,51 @@ +/* + * @module @warp-drive/core-types + */ +import type { ResourceType } from './symbols'; + +/** + * Records may be anything, They don't even + * have to be objects. + * + * Whatever they are, if they have a ResourceType + * property, that property will be used by EmberData + * and WarpDrive to provide better type safety and + * intellisense. + * + * @class TypedRecordInstance + * @typedoc + */ +export interface TypedRecordInstance { + /** + * The type of the resource. + * + * This is an optional feature that can be used by + * record implementations to provide a typescript + * hint for the type of the resource. + * + * When used, EmberData and WarpDrive APIs can + * take advantage of this to provide better type + * safety and intellisense. + * + * @property {ResourceType} [ResourceType] + * @type {string} + * @typedoc + */ + [ResourceType]: string; +} + +/** + * A type utility that extracts the ResourceType if available, + * otherwise it returns never. + * + * @typedoc + */ +export type TypeFromInstance = T extends TypedRecordInstance ? T[typeof ResourceType] : never; + +/** + * A type utility that extracts the ResourceType if available, + * otherwise it returns string + * + * @typedoc + */ +export type TypeFromInstanceOrString = T extends TypedRecordInstance ? T[typeof ResourceType] : string; diff --git a/packages/core-types/src/spec/raw.ts b/packages/core-types/src/spec/raw.ts index 412dbe746ae..baa93c4acae 100644 --- a/packages/core-types/src/spec/raw.ts +++ b/packages/core-types/src/spec/raw.ts @@ -27,9 +27,9 @@ export interface PaginationLinks extends Links { * [JSON:API Spec](https://jsonapi.org/format/#document-resource-identifier-objects) * @internal */ -export interface ExistingResourceIdentifierObject { +export interface ExistingResourceIdentifierObject { id: string; - type: string; + type: T; /** * While not officially part of the `JSON:API` spec, @@ -65,7 +65,7 @@ export interface ExistingResourceIdentifierObject { * * @internal */ -export interface NewResourceIdentifierObject { +export interface NewResourceIdentifierObject { /** * Resources newly created on the client _may_ * not have an `id` available to them prior @@ -76,7 +76,7 @@ export interface NewResourceIdentifierObject { * @internal */ id: string | null; - type: string; + type: T; /** * Resources newly created on the client _will always_ @@ -90,10 +90,10 @@ export interface ResourceIdentifier { lid: string; } -export type ResourceIdentifierObject = +export type ResourceIdentifierObject = | ResourceIdentifier - | ExistingResourceIdentifierObject - | NewResourceIdentifierObject; + | ExistingResourceIdentifierObject + | NewResourceIdentifierObject; // TODO disallow NewResource, make narrowable export interface SingleResourceRelationship { @@ -112,7 +112,7 @@ export interface CollectionResourceRelationship { * Contains the data for an existing resource in JSON:API format * @internal */ -export interface ExistingResourceObject extends ExistingResourceIdentifierObject { +export interface ExistingResourceObject extends ExistingResourceIdentifierObject { meta?: Meta; attributes?: ObjectValue; relationships?: Record; @@ -132,12 +132,12 @@ export interface EmptyResourceDocument extends Document { data: null; } -export interface SingleResourceDocument extends Document { - data: ExistingResourceObject; +export interface SingleResourceDocument extends Document { + data: ExistingResourceObject; } -export interface CollectionResourceDocument extends Document { - data: ExistingResourceObject[]; +export interface CollectionResourceDocument extends Document { + data: ExistingResourceObject[]; } /** @@ -148,4 +148,7 @@ export interface CollectionResourceDocument extends Document { * * @internal */ -export type JsonApiDocument = EmptyResourceDocument | SingleResourceDocument | CollectionResourceDocument; +export type JsonApiDocument = + | EmptyResourceDocument + | SingleResourceDocument + | CollectionResourceDocument; diff --git a/packages/core-types/src/symbols.ts b/packages/core-types/src/symbols.ts index b724a1c47c1..2c8e45e98c7 100644 --- a/packages/core-types/src/symbols.ts +++ b/packages/core-types/src/symbols.ts @@ -1 +1,20 @@ +/* + * @module @warp-drive/core-types + */ export const RecordStore = Symbol('Store'); + +/** + * Symbol for the type of a resource. + * + * This is an optional feature that can be used by + * record implementations to provide a typescript + * hint for the type of the resource. + * + * When used, EmberData and WarpDrive APIs can + * take advantage of this to provide better type + * safety and intellisense. + * + * @type {Symbol} + * @typedoc + */ +export const ResourceType = Symbol('$type'); diff --git a/packages/legacy-compat/src/legacy-network-handler/fetch-manager.ts b/packages/legacy-compat/src/legacy-network-handler/fetch-manager.ts index 64f7e82bb81..71ac21275d7 100644 --- a/packages/legacy-compat/src/legacy-network-handler/fetch-manager.ts +++ b/packages/legacy-compat/src/legacy-network-handler/fetch-manager.ts @@ -13,8 +13,9 @@ import type { InstanceCache } from '@ember-data/store/-private/caches/instance-c import type RequestStateService from '@ember-data/store/-private/network/request-cache'; import type { FindRecordQuery, Request, SaveRecordMutation } from '@ember-data/store/-private/network/request-cache'; import type { ModelSchema } from '@ember-data/store/-types/q/ds-model'; -import type { FindOptions } from '@ember-data/store/-types/q/store'; +import type { FindRecordOptions } from '@ember-data/store/-types/q/store'; import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; import type { CollectionResourceDocument, SingleResourceDocument } from '@warp-drive/core-types/spec/raw'; import { upgradeStore } from '../-private'; @@ -32,14 +33,14 @@ type SerializerWithParseErrors = MinimumSerializerInterface & { export const SaveOp: unique symbol = Symbol('SaveOp'); -export type FetchMutationOptions = FindOptions & { [SaveOp]: 'createRecord' | 'deleteRecord' | 'updateRecord' }; +export type FetchMutationOptions = FindRecordOptions & { [SaveOp]: 'createRecord' | 'deleteRecord' | 'updateRecord' }; interface PendingFetchItem { identifier: StableExistingRecordIdentifier; queryRequest: Request; // eslint-disable-next-line @typescript-eslint/no-explicit-any resolver: Deferred; - options: FindOptions; + options: FindRecordOptions; trace?: unknown; promise: Promise; } @@ -68,7 +69,9 @@ export default class FetchManager { this.isDestroyed = false; } - createSnapshot(identifier: StableRecordIdentifier, options: FindOptions = {}): Snapshot { + createSnapshot(identifier: StableRecordIdentifier>, options?: FindRecordOptions): Snapshot; + createSnapshot(identifier: StableRecordIdentifier, options?: FindRecordOptions): Snapshot; + createSnapshot(identifier: StableRecordIdentifier, options: FindRecordOptions = {}): Snapshot { return new Snapshot(options, identifier, this._store); } @@ -112,7 +115,7 @@ export default class FetchManager { scheduleFetch( identifier: StableExistingRecordIdentifier, - options: FindOptions, + options: FindRecordOptions, request: StoreRequestInfo ): Promise { const query: FindRecordQuery = { @@ -222,7 +225,7 @@ export default class FetchManager { return promise; } - getPendingFetch(identifier: StableExistingRecordIdentifier, options: FindOptions) { + getPendingFetch(identifier: StableExistingRecordIdentifier, options: FindRecordOptions) { const pendingFetches = this._pendingFetch.get(identifier.type)?.get(identifier); // We already have a pending fetch for this @@ -246,7 +249,7 @@ export default class FetchManager { fetchDataIfNeededForIdentifier( identifier: StableExistingRecordIdentifier, - options: FindOptions = {}, + options: FindRecordOptions = {}, request: StoreRequestInfo ): Promise { // pre-loading will change the isEmpty value @@ -338,7 +341,7 @@ function optionsSatisfies(current: object | undefined, existing: object | undefi } // this function helps resolve whether we have a pending request that we should use instead -function isSameRequest(options: FindOptions = {}, existingOptions: FindOptions = {}) { +function isSameRequest(options: FindRecordOptions = {}, existingOptions: FindRecordOptions = {}) { return ( optionsSatisfies(options.adapterOptions, existingOptions.adapterOptions) && includesSatisfies(options.include, existingOptions.include) diff --git a/packages/legacy-compat/src/legacy-network-handler/snapshot-record-array.ts b/packages/legacy-compat/src/legacy-network-handler/snapshot-record-array.ts index ecfdede8847..0e83e61b6dc 100644 --- a/packages/legacy-compat/src/legacy-network-handler/snapshot-record-array.ts +++ b/packages/legacy-compat/src/legacy-network-handler/snapshot-record-array.ts @@ -5,7 +5,7 @@ import type Store from '@ember-data/store'; import { SOURCE } from '@ember-data/store/-private'; import type IdentifierArray from '@ember-data/store/-private/record-arrays/identifier-array'; import type { ModelSchema } from '@ember-data/store/-types/q/ds-model'; -import type { FindOptions } from '@ember-data/store/-types/q/store'; +import type { FindAllOptions } from '@ember-data/store/-types/q/store'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import { upgradeStore } from '../-private'; @@ -13,7 +13,7 @@ import type Snapshot from './snapshot'; /** SnapshotRecordArray is not directly instantiable. Instances are provided to consuming application's - adapters for certain requests. + adapters for certain `findAll` requests. @class SnapshotRecordArray @public @@ -25,7 +25,7 @@ export default class SnapshotRecordArray { declare __store: Store; declare adapterOptions?: Record; - declare include?: string; + declare include?: string | string[]; /** SnapshotRecordArray is not directly instantiable. @@ -39,7 +39,7 @@ export default class SnapshotRecordArray { @param {string} type @param options */ - constructor(store: Store, type: string, options: FindOptions = {}) { + constructor(store: Store, type: string, options: FindAllOptions = {}) { this.__store = store; /** An array of snapshots diff --git a/packages/legacy-compat/src/legacy-network-handler/snapshot.ts b/packages/legacy-compat/src/legacy-network-handler/snapshot.ts index 79ac2bd88b4..1fe05fba2f4 100644 --- a/packages/legacy-compat/src/legacy-network-handler/snapshot.ts +++ b/packages/legacy-compat/src/legacy-network-handler/snapshot.ts @@ -10,12 +10,12 @@ import type { CollectionEdge } from '@ember-data/graph/-private/edges/collection import type { ResourceEdge } from '@ember-data/graph/-private/edges/resource'; import { HAS_JSON_API_PACKAGE } from '@ember-data/packages'; import type Store from '@ember-data/store'; -import type { RecordInstance } from '@ember-data/store/-types/q/record-instance'; -import type { FindOptions } from '@ember-data/store/-types/q/store'; +import type { FindRecordOptions } from '@ember-data/store/-types/q/store'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { ChangedAttributesHash } from '@warp-drive/core-types/cache'; import type { CollectionRelationship } from '@warp-drive/core-types/cache/relationship'; import type { Value } from '@warp-drive/core-types/json/raw'; +import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; import type { AttributeSchema, RelationshipSchema } from '@warp-drive/core-types/schema'; import { upgradeStore } from '../-private'; @@ -34,18 +34,18 @@ type RecordId = string | null; @class Snapshot @public */ -export default class Snapshot implements Snapshot { - declare __attributes: Record | null; +export default class Snapshot { + declare __attributes: Record | null; declare _belongsToRelationships: Record; declare _belongsToIds: Record; declare _hasManyRelationships: Record; declare _hasManyIds: Record; declare _changedAttributes: ChangedAttributesHash; - declare identifier: StableRecordIdentifier; - declare modelName: string; + declare identifier: StableRecordIdentifier : string>; + declare modelName: R extends TypedRecordInstance ? TypeFromInstance : string; declare id: string | null; - declare include?: unknown; + declare include?: string | string[]; declare adapterOptions?: Record; declare _store: Store; @@ -57,7 +57,11 @@ export default class Snapshot implements Snapshot { * @param identifier * @param _store */ - constructor(options: FindOptions, identifier: StableRecordIdentifier, store: Store) { + constructor( + options: FindRecordOptions, + identifier: StableRecordIdentifier : string>, + store: Store + ) { this._store = store; this.__attributes = null; @@ -151,8 +155,8 @@ export default class Snapshot implements Snapshot { @type {Model} @public */ - get record(): RecordInstance | null { - const record = this._store.peekRecord(this.identifier); + get record(): R | null { + const record = this._store.peekRecord(this.identifier); assert( `Record ${this.identifier.type} ${this.identifier.id} (${this.identifier.lid}) is not yet loaded and thus cannot be accessed from the Snapshot during serialization`, record !== null @@ -160,7 +164,7 @@ export default class Snapshot implements Snapshot { return record; } - get _attributes(): Record { + get _attributes(): Record { if (this.__attributes !== null) { return this.__attributes; } @@ -199,7 +203,7 @@ export default class Snapshot implements Snapshot { @return {Object} The attribute value or undefined @public */ - attr(keyName: string): unknown { + attr(keyName: keyof R & string): unknown { if (keyName in this._attributes) { return this._attributes[keyName]; } @@ -220,7 +224,7 @@ export default class Snapshot implements Snapshot { @return {Object} All attributes of the current snapshot @public */ - attributes(): Record { + attributes(): Record { return { ...this._attributes }; } diff --git a/packages/model/src/-private/hooks.ts b/packages/model/src/-private/hooks.ts index 0fbfed67c4b..0a1b299bcbb 100644 --- a/packages/model/src/-private/hooks.ts +++ b/packages/model/src/-private/hooks.ts @@ -4,6 +4,7 @@ import { assert } from '@ember/debug'; import { setCacheFor, setRecordIdentifier, type Store, StoreMap } from '@ember-data/store/-private'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { Cache } from '@warp-drive/core-types/cache'; +import type { TypeFromInstance, TypeFromInstanceOrString } from '@warp-drive/core-types/record'; import type { ModelStore } from './model'; import type Model from './model'; @@ -50,7 +51,9 @@ export function teardownRecord(record: Model): void { record.destroy(); } -export function modelFor(this: Store, modelName: string): typeof Model | void { +export function modelFor(type: TypeFromInstance): typeof Model | void; +export function modelFor(type: string): typeof Model | void; +export function modelFor(this: Store, modelName: TypeFromInstanceOrString): typeof Model | void { assert( `Attempted to call store.modelFor(), but the store instance has already been destroyed.`, !this.isDestroyed && !this.isDestroying diff --git a/packages/model/src/-private/legacy-relationships-support.ts b/packages/model/src/-private/legacy-relationships-support.ts index 0e27e3f3879..131c5f1eaf0 100644 --- a/packages/model/src/-private/legacy-relationships-support.ts +++ b/packages/model/src/-private/legacy-relationships-support.ts @@ -20,8 +20,8 @@ import { } from '@ember-data/store/-private'; import type { Cache } from '@ember-data/store/-types/q/cache'; import type { JsonApiRelationship } from '@ember-data/store/-types/q/record-data-json-api'; -import type { RecordInstance } from '@ember-data/store/-types/q/record-instance'; -import type { FindOptions } from '@ember-data/store/-types/q/store'; +import type { OpaqueRecordInstance } from '@ember-data/store/-types/q/record-instance'; +import type { BaseFinderOptions } from '@ember-data/store/-types/q/store'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { CollectionRelationship } from '@warp-drive/core-types/cache/relationship'; import type { LocalRelationshipOperation } from '@warp-drive/core-types/graph'; @@ -63,7 +63,7 @@ export class LegacySupport { declare references: Record; declare identifier: StableRecordIdentifier; declare _manyArrayCache: Record; - declare _relationshipPromisesCache: Record>; + declare _relationshipPromisesCache: Record>; declare _relationshipProxyCache: Record; declare _pending: Record | undefined>; @@ -86,7 +86,7 @@ export class LegacySupport { this._manyArrayCache = Object.create(null) as Record; this._relationshipPromisesCache = Object.create(null) as Record< string, - Promise + Promise >; this._relationshipProxyCache = Object.create(null) as Record; this._pending = Object.create(null) as Record>; @@ -123,8 +123,8 @@ export class LegacySupport { key: string, resource: SingleResourceRelationship, relationship: ResourceEdge, - options?: FindOptions - ): Promise { + options?: BaseFinderOptions + ): Promise { // TODO @runspired follow up if parent isNew then we should not be attempting load here // TODO @runspired follow up on whether this should be in the relationship requests cache return this._findBelongsToByJsonApiResource(resource, this.identifier, relationship, options).then( @@ -134,8 +134,8 @@ export class LegacySupport { ); } - reloadBelongsTo(key: string, options?: FindOptions): Promise { - const loadingPromise = this._relationshipPromisesCache[key] as Promise | undefined; + reloadBelongsTo(key: string, options?: BaseFinderOptions): Promise { + const loadingPromise = this._relationshipPromisesCache[key] as Promise | undefined; if (loadingPromise) { return loadingPromise; } @@ -154,7 +154,7 @@ export class LegacySupport { return promise; } - getBelongsTo(key: string, options?: FindOptions): PromiseBelongsTo | RecordInstance | null { + getBelongsTo(key: string, options?: BaseFinderOptions): PromiseBelongsTo | OpaqueRecordInstance | null { const { identifier, cache } = this; const resource = cache.getRelationship(this.identifier, key) as SingleResourceRelationship; const relatedIdentifier = resource && resource.data ? resource.data : null; @@ -201,7 +201,7 @@ export class LegacySupport { } } - setDirtyBelongsTo(key: string, value: RecordInstance | null) { + setDirtyBelongsTo(key: string, value: OpaqueRecordInstance | null) { return this.cache.mutate( { op: 'replaceRelatedRecord', @@ -272,7 +272,7 @@ export class LegacySupport { key: string, relationship: CollectionEdge, manyArray: RelatedCollection, - options?: FindOptions + options?: BaseFinderOptions ): Promise { if (HAS_JSON_API_PACKAGE) { let loadingPromise = this._relationshipPromisesCache[key] as Promise | undefined; @@ -298,7 +298,7 @@ export class LegacySupport { assert('hasMany only works with the @ember-data/json-api package'); } - reloadHasMany(key: string, options?: FindOptions) { + reloadHasMany(key: string, options?: BaseFinderOptions) { if (HAS_JSON_API_PACKAGE) { const loadingPromise = this._relationshipPromisesCache[key]; if (loadingPromise) { @@ -321,7 +321,7 @@ export class LegacySupport { assert(`hasMany only works with the @ember-data/json-api package`); } - getHasMany(key: string, options?: FindOptions): PromiseManyArray | RelatedCollection { + getHasMany(key: string, options?: BaseFinderOptions): PromiseManyArray | RelatedCollection { if (HAS_JSON_API_PACKAGE) { const relationship = this.graph.get(this.identifier, key) as CollectionEdge; const { definition, state } = relationship; @@ -354,12 +354,12 @@ export class LegacySupport { _updatePromiseProxyFor( kind: 'belongsTo', key: string, - args: { promise: Promise } + args: { promise: Promise } ): PromiseBelongsTo; _updatePromiseProxyFor( kind: 'hasMany' | 'belongsTo', key: string, - args: BelongsToProxyCreateArgs | HasManyProxyCreateArgs | { promise: Promise } + args: BelongsToProxyCreateArgs | HasManyProxyCreateArgs | { promise: Promise } ): PromiseBelongsTo | PromiseManyArray { let promiseProxy = this._relationshipProxyCache[key]; if (kind === 'hasMany') { @@ -430,7 +430,7 @@ export class LegacySupport { resource: CollectionResourceRelationship, parentIdentifier: StableRecordIdentifier, relationship: CollectionEdge, - options: FindOptions = {} + options: BaseFinderOptions = {} ): Promise | void { if (HAS_JSON_API_PACKAGE) { if (!resource) { @@ -508,7 +508,7 @@ export class LegacySupport { resource: SingleResourceRelationship, parentIdentifier: StableRecordIdentifier, relationship: ResourceEdge, - options: FindOptions = {} + options: BaseFinderOptions = {} ): Promise { if (!resource) { return Promise.resolve(null); @@ -634,7 +634,7 @@ function handleCompletedRelationshipRequest( key: string, relationship: ResourceEdge, value: StableRecordIdentifier | null -): RecordInstance | null; +): OpaqueRecordInstance | null; function handleCompletedRelationshipRequest( recordExt: LegacySupport, key: string, @@ -661,7 +661,7 @@ function handleCompletedRelationshipRequest( relationship: ResourceEdge | CollectionEdge, value: RelatedCollection | StableRecordIdentifier | null, error?: Error -): RelatedCollection | RecordInstance | null { +): RelatedCollection | OpaqueRecordInstance | null { delete recordExt._relationshipPromisesCache[key]; relationship.state.shouldForceReload = false; const isHasMany = relationship.definition.kind === 'hasMany'; @@ -708,9 +708,9 @@ function handleCompletedRelationshipRequest( : recordExt.store.peekRecord(value as StableRecordIdentifier); } -type PromiseProxyRecord = { then(): void; content: RecordInstance | null | undefined }; +type PromiseProxyRecord = { then(): void; content: OpaqueRecordInstance | null | undefined }; -function extractIdentifierFromRecord(record: PromiseProxyRecord | RecordInstance | null) { +function extractIdentifierFromRecord(record: PromiseProxyRecord | OpaqueRecordInstance | null) { if (!record) { return null; } diff --git a/packages/model/src/-private/many-array.ts b/packages/model/src/-private/many-array.ts index 198715f1ce1..93a9d522569 100644 --- a/packages/model/src/-private/many-array.ts +++ b/packages/model/src/-private/many-array.ts @@ -18,8 +18,8 @@ import type { IdentifierArrayCreateOptions } from '@ember-data/store/-private/re import type { CreateRecordProperties } from '@ember-data/store/-private/store-service'; import type { Cache } from '@ember-data/store/-types/q/cache'; import type { ModelSchema } from '@ember-data/store/-types/q/ds-model'; -import type { RecordInstance } from '@ember-data/store/-types/q/record-instance'; -import type { FindOptions } from '@ember-data/store/-types/q/store'; +import type { OpaqueRecordInstance } from '@ember-data/store/-types/q/record-instance'; +import type { BaseFinderOptions } from '@ember-data/store/-types/q/store'; import type { Signal } from '@ember-data/tracking/-private'; import { addToTransaction } from '@ember-data/tracking/-private'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -199,7 +199,7 @@ export default class RelatedCollection extends RecordArray { if (DEPRECATE_MANY_ARRAY_DUPLICATES) { // dedupe const seen = new Set(target); - const unique = new Set(); + const unique = new Set(); args.forEach((item) => { const identifier = recordIdentifierFor(item); @@ -210,7 +210,7 @@ export default class RelatedCollection extends RecordArray { }); const newArgs = Array.from(unique); - const result = Reflect.apply(target[prop], receiver, newArgs) as RecordInstance[]; + const result = Reflect.apply(target[prop], receiver, newArgs) as OpaqueRecordInstance[]; if (newArgs.length) { mutateAddToRelatedRecords(this, { value: extractIdentifiersFromRecords(newArgs) }, _SIGNAL); @@ -219,7 +219,7 @@ export default class RelatedCollection extends RecordArray { } // else, no dedupe, error on duplicates - const result = Reflect.apply(target[prop], receiver, args) as RecordInstance[]; + const result = Reflect.apply(target[prop], receiver, args) as OpaqueRecordInstance[]; if (newValues.length) { mutateAddToRelatedRecords(this, { value: newValues }, _SIGNAL); } @@ -229,7 +229,7 @@ export default class RelatedCollection extends RecordArray { case 'pop': { const result: unknown = Reflect.apply(target[prop], receiver, args); if (result) { - mutateRemoveFromRelatedRecords(this, { value: recordIdentifierFor(result as RecordInstance) }, _SIGNAL); + mutateRemoveFromRelatedRecords(this, { value: recordIdentifierFor(result as OpaqueRecordInstance) }, _SIGNAL); } return result; } @@ -247,7 +247,7 @@ export default class RelatedCollection extends RecordArray { if (DEPRECATE_MANY_ARRAY_DUPLICATES) { // dedupe const seen = new Set(target); - const unique = new Set(); + const unique = new Set(); args.forEach((item) => { const identifier = recordIdentifierFor(item); @@ -267,7 +267,7 @@ export default class RelatedCollection extends RecordArray { } // else, no dedupe, error on duplicates - const result = Reflect.apply(target[prop], receiver, args) as RecordInstance[]; + const result = Reflect.apply(target[prop], receiver, args) as OpaqueRecordInstance[]; if (newValues.length) { mutateAddToRelatedRecords(this, { value: newValues, index: 0 }, _SIGNAL); } @@ -280,7 +280,7 @@ export default class RelatedCollection extends RecordArray { if (result) { mutateRemoveFromRelatedRecords( this, - { value: recordIdentifierFor(result as RecordInstance), index: 0 }, + { value: recordIdentifierFor(result as OpaqueRecordInstance), index: 0 }, _SIGNAL ); } @@ -289,12 +289,12 @@ export default class RelatedCollection extends RecordArray { case 'sort': { const result: unknown = Reflect.apply(target[prop], receiver, args); - mutateSortRelatedRecords(this, (result as RecordInstance[]).map(recordIdentifierFor), _SIGNAL); + mutateSortRelatedRecords(this, (result as OpaqueRecordInstance[]).map(recordIdentifierFor), _SIGNAL); return result; } case 'splice': { - const [start, deleteCount, ...adds] = args as [number, number, ...RecordInstance[]]; + const [start, deleteCount, ...adds] = args as [number, number, ...OpaqueRecordInstance[]]; // detect a full replace if (start === 0 && deleteCount === this[SOURCE].length) { @@ -313,14 +313,14 @@ export default class RelatedCollection extends RecordArray { const unique = Array.from(current); const newArgs = ([start, deleteCount] as unknown[]).concat(unique); - const result = Reflect.apply(target[prop], receiver, newArgs) as RecordInstance[]; + const result = Reflect.apply(target[prop], receiver, newArgs) as OpaqueRecordInstance[]; mutateReplaceRelatedRecords(this, extractIdentifiersFromRecords(unique), _SIGNAL); return result; } // else, no dedupe, error on duplicates - const result = Reflect.apply(target[prop], receiver, args) as RecordInstance[]; + const result = Reflect.apply(target[prop], receiver, args) as OpaqueRecordInstance[]; mutateReplaceRelatedRecords(this, newValues, _SIGNAL); return result; } @@ -339,7 +339,7 @@ export default class RelatedCollection extends RecordArray { currentState.splice(start, deleteCount); const seen = new Set(currentState); - const unique: RecordInstance[] = []; + const unique: OpaqueRecordInstance[] = []; adds.forEach((item) => { const identifier = recordIdentifierFor(item); if (!seen.has(identifier)) { @@ -349,7 +349,7 @@ export default class RelatedCollection extends RecordArray { }); const newArgs = [start, deleteCount, ...unique]; - const result = Reflect.apply(target[prop], receiver, newArgs) as RecordInstance[]; + const result = Reflect.apply(target[prop], receiver, newArgs) as OpaqueRecordInstance[]; if (deleteCount > 0) { mutateRemoveFromRelatedRecords(this, { value: result.map(recordIdentifierFor), index: start }, _SIGNAL); @@ -363,7 +363,7 @@ export default class RelatedCollection extends RecordArray { } // else, no dedupe, error on duplicates - const result = Reflect.apply(target[prop], receiver, args) as RecordInstance[]; + const result = Reflect.apply(target[prop], receiver, args) as OpaqueRecordInstance[]; if (deleteCount > 0) { mutateRemoveFromRelatedRecords(this, { value: result.map(recordIdentifierFor), index: start }, _SIGNAL); } @@ -406,7 +406,7 @@ export default class RelatedCollection extends RecordArray { @method reload @public */ - reload(options?: FindOptions) { + reload(options?: BaseFinderOptions) { // TODO this is odd, we don't ask the store for anything else like this? return this._manager.reloadHasMany(this.key, options); } @@ -438,7 +438,7 @@ export default class RelatedCollection extends RecordArray { @param {Object} hash @return {Model} record */ - createRecord(hash: CreateRecordProperties): RecordInstance { + createRecord(hash: CreateRecordProperties): OpaqueRecordInstance { const { store } = this; assert(`Expected modelName to be set`, this.modelName); const record = store.createRecord(this.modelName, hash); @@ -459,9 +459,9 @@ RelatedCollection.prototype._inverseIsAsync = false; RelatedCollection.prototype.key = ''; RelatedCollection.prototype.DEPRECATED_CLASS_NAME = 'ManyArray'; -type PromiseProxyRecord = { then(): void; content: RecordInstance | null | undefined }; +type PromiseProxyRecord = { then(): void; content: OpaqueRecordInstance | null | undefined }; -function assertRecordPassedToHasMany(record: RecordInstance | PromiseProxyRecord) { +function assertRecordPassedToHasMany(record: OpaqueRecordInstance | PromiseProxyRecord) { assert( `All elements of a hasMany relationship must be instances of Model, you passed $${typeof record}`, (function () { @@ -475,11 +475,11 @@ function assertRecordPassedToHasMany(record: RecordInstance | PromiseProxyRecord ); } -function extractIdentifiersFromRecords(records: RecordInstance[]): StableRecordIdentifier[] { +function extractIdentifiersFromRecords(records: OpaqueRecordInstance[]): StableRecordIdentifier[] { return records.map(extractIdentifierFromRecord); } -function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | RecordInstance) { +function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | OpaqueRecordInstance) { assertRecordPassedToHasMany(recordOrPromiseRecord); return recordIdentifierFor(recordOrPromiseRecord); } diff --git a/packages/model/src/-private/model-methods.ts b/packages/model/src/-private/model-methods.ts index 381d3e9abad..0eaa3ad387b 100644 --- a/packages/model/src/-private/model-methods.ts +++ b/packages/model/src/-private/model-methods.ts @@ -2,10 +2,12 @@ import { assert } from '@ember/debug'; import { importSync } from '@embroider/macros'; +import type { Snapshot } from '@ember-data/legacy-compat/-private'; import { upgradeStore } from '@ember-data/legacy-compat/-private'; import type Store from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store'; import { peekCache } from '@ember-data/store/-private'; +import type { ChangedAttributesHash } from '@warp-drive/core-types/cache'; import { RecordStore } from '@warp-drive/core-types/symbols'; import type Errors from './errors'; @@ -56,7 +58,7 @@ export function hasMany(this: MinimalLegacyRecord, prop: string) { return lookupLegacySupport(this).referenceFor('hasMany', prop); } -export function reload(this: MinimalLegacyRecord, options: Record = {}) { +export function reload(this: T, options: Record = {}): Promise { options.isReloading = true; options.reload = true; @@ -80,16 +82,16 @@ export function reload(this: MinimalLegacyRecord, options: Record(this: T): ChangedAttributesHash { return peekCache(this).changedAttrs(recordIdentifierFor(this)); } -export function serialize(this: MinimalLegacyRecord, options?: Record) { +export function serialize(this: T, options?: Record): unknown { upgradeStore(this[RecordStore]); return this[RecordStore].serializeRecord(this, options); } -export function deleteRecord(this: MinimalLegacyRecord) { +export function deleteRecord(this: T): void { // ensure we've populated currentState prior to deleting a new record if (this.currentState) { this[RecordStore].deleteRecord(this); @@ -103,13 +105,13 @@ export function save(this: T, options?: Record; + promise = this[RecordStore].saveRecord(this, options); } return promise; } -export function destroyRecord(this: MinimalLegacyRecord, options?: Record) { +export function destroyRecord(this: T, options?: Record): Promise { const { isNew } = this.currentState; this.deleteRecord(); if (isNew) { @@ -121,7 +123,7 @@ export function destroyRecord(this: MinimalLegacyRecord, options?: Record(this: T): Snapshot { const store = this[RecordStore]; upgradeStore(store); @@ -132,5 +134,6 @@ export function createSnapshot(this: MinimalLegacyRecord) { store._fetchManager = new FetchManager(store); } - return store._fetchManager.createSnapshot(recordIdentifierFor(this)); + // @ts-expect-error Typescript isn't able to curry narrowed args that are divorced from each other. + return store._fetchManager.createSnapshot(recordIdentifierFor(this)); } diff --git a/packages/model/src/-private/promise-belongs-to.ts b/packages/model/src/-private/promise-belongs-to.ts index 67a2673d96d..524222310e2 100644 --- a/packages/model/src/-private/promise-belongs-to.ts +++ b/packages/model/src/-private/promise-belongs-to.ts @@ -4,7 +4,7 @@ import type PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; import type ObjectProxy from '@ember/object/proxy'; import type Store from '@ember-data/store'; -import type { RecordInstance } from '@ember-data/store/-types/q/record-instance'; +import type { OpaqueRecordInstance } from '@ember-data/store/-types/q/record-instance'; import { cached } from '@ember-data/tracking'; import type { LegacySupport } from './legacy-relationships-support'; @@ -18,8 +18,8 @@ export interface BelongsToProxyMeta { modelName: string; } export interface BelongsToProxyCreateArgs { - promise: Promise; - content?: RecordInstance | null; + promise: Promise; + content?: OpaqueRecordInstance | null; _belongsToState: BelongsToProxyMeta; } @@ -30,7 +30,8 @@ interface PromiseObjectType extends PromiseProxyMixin, ObjectProxy< // eslint-disable-next-line @typescript-eslint/no-unused-vars declare class PromiseObjectType {} -const Extended: PromiseObjectType = PromiseObject as unknown as PromiseObjectType; +const Extended: PromiseObjectType = + PromiseObject as unknown as PromiseObjectType; /** @module @ember-data/model @@ -45,7 +46,7 @@ const Extended: PromiseObjectType = PromiseObject as unknown as @extends PromiseObject @private */ -class PromiseBelongsTo extends Extended { +class PromiseBelongsTo extends Extended { declare _belongsToState: BelongsToProxyMeta; @cached diff --git a/packages/model/src/-private/promise-many-array.ts b/packages/model/src/-private/promise-many-array.ts index a75ccbd1862..eb4b738ae3d 100644 --- a/packages/model/src/-private/promise-many-array.ts +++ b/packages/model/src/-private/promise-many-array.ts @@ -1,7 +1,7 @@ import { assert } from '@ember/debug'; import { DEPRECATE_COMPUTED_CHAINS } from '@ember-data/deprecations'; -import type { FindOptions } from '@ember-data/store/-types/q/store'; +import type { BaseFinderOptions } from '@ember-data/store/-types/q/store'; import { compat } from '@ember-data/tracking'; import { defineSignal } from '@ember-data/tracking/-private'; @@ -82,7 +82,7 @@ export default class PromiseManyArray { * @param options * @return */ - reload(options: FindOptions) { + reload(options: Omit) { assert('You are trying to reload an async manyArray before it has been created', this.content); void this.content.reload(options); return this; diff --git a/packages/model/src/-private/references/belongs-to.ts b/packages/model/src/-private/references/belongs-to.ts index 8a647635e07..ce60f4dca25 100644 --- a/packages/model/src/-private/references/belongs-to.ts +++ b/packages/model/src/-private/references/belongs-to.ts @@ -3,7 +3,7 @@ import type { ResourceEdge } from '@ember-data/graph/-private/edges/resource'; import type { Graph } from '@ember-data/graph/-private/graph'; import type Store from '@ember-data/store'; import type { NotificationType } from '@ember-data/store/-private/managers/notification-manager'; -import type { RecordInstance } from '@ember-data/store/-types/q/record-instance'; +import type { OpaqueRecordInstance } from '@ember-data/store/-types/q/record-instance'; import { cached, compat } from '@ember-data/tracking'; import { defineSignal } from '@ember-data/tracking/-private'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -467,9 +467,9 @@ export default class BelongsToReference { @public @param {Object} doc a JSONAPI document object describing the new value of this relationship. @param {Boolean} [skipFetch] if `true`, do not attempt to fetch unloaded records - @return {Promise} + @return {Promise} */ - async push(doc: SingleResourceDocument, skipFetch?: boolean): Promise { + async push(doc: SingleResourceDocument, skipFetch?: boolean): Promise { const { store } = this; const isResourceData = doc.data && isMaybeResource(doc.data); const added = isResourceData @@ -559,7 +559,7 @@ export default class BelongsToReference { @public @return {Model} the record in this relationship */ - value(): RecordInstance | null { + value(): OpaqueRecordInstance | null { const resource = this._resource(); return resource && resource.data ? this.store.peekRecord(resource.data) : null; } @@ -626,7 +626,7 @@ export default class BelongsToReference { @param {Object} options the options to pass in. @return {Promise} a promise that resolves with the record in this belongs-to relationship. */ - async load(options?: Record): Promise { + async load(options?: Record): Promise { const support: LegacySupport = (LEGACY_SUPPORT as Map).get( this.___identifier )!; @@ -636,7 +636,7 @@ export default class BelongsToReference { ? support.reloadBelongsTo(this.key, options).then(() => this.value()) : // we cast to fix the return type since typescript and eslint don't understand async functions // properly - (support.getBelongsTo(this.key, options) as Promise); + (support.getBelongsTo(this.key, options) as Promise); } /** diff --git a/packages/model/src/-private/references/has-many.ts b/packages/model/src/-private/references/has-many.ts index 9796c1b2a1d..512304c4626 100644 --- a/packages/model/src/-private/references/has-many.ts +++ b/packages/model/src/-private/references/has-many.ts @@ -5,7 +5,7 @@ import type { CollectionEdge } from '@ember-data/graph/-private/edges/collection import type { Graph } from '@ember-data/graph/-private/graph'; import type Store from '@ember-data/store'; import type { NotificationType } from '@ember-data/store/-private/managers/notification-manager'; -import type { FindOptions } from '@ember-data/store/-types/q/store'; +import type { BaseFinderOptions } from '@ember-data/store/-types/q/store'; import { cached, compat } from '@ember-data/tracking'; import { defineSignal } from '@ember-data/tracking/-private'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -672,7 +672,7 @@ export default class HasManyReference { @return {Promise} a promise that resolves with the ManyArray in this has-many relationship. */ - async load(options?: FindOptions): Promise { + async load(options?: BaseFinderOptions): Promise { const support: LegacySupport = (LEGACY_SUPPORT as Map).get( this.___identifier )!; @@ -735,7 +735,7 @@ export default class HasManyReference { @param {Object} options the options to pass in. @return {Promise} a promise that resolves with the ManyArray in this has-many relationship. */ - reload(options?: FindOptions) { + reload(options?: BaseFinderOptions) { const support: LegacySupport = (LEGACY_SUPPORT as Map).get( this.___identifier )!; diff --git a/packages/schema-record/src/-base-fields.ts b/packages/schema-record/src/-base-fields.ts index d01d7649b1d..0e6b8047b03 100644 --- a/packages/schema-record/src/-base-fields.ts +++ b/packages/schema-record/src/-base-fields.ts @@ -1,7 +1,7 @@ import { assert } from '@ember/debug'; import { recordIdentifierFor } from '@ember-data/store'; -import type { RecordInstance } from '@ember-data/store/-types/q/record-instance'; +import type { OpaqueRecordInstance } from '@ember-data/store/-types/q/record-instance'; import type { FieldSchema } from '@ember-data/store/-types/q/schema-service'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; @@ -29,7 +29,7 @@ export const SchemaRecordFields: FieldSchema[] = [ }, ]; -const _constructor: Derivation = function (record) { +const _constructor: Derivation = function (record) { let state = Support.get(record as WeakKey); if (!state) { state = {}; diff --git a/packages/store/src/-private/cache-handler.ts b/packages/store/src/-private/cache-handler.ts index 815b34e3fdc..c3fe0c6274a 100644 --- a/packages/store/src/-private/cache-handler.ts +++ b/packages/store/src/-private/cache-handler.ts @@ -24,7 +24,7 @@ import type { import type { ApiError } from '@warp-drive/core-types/spec/error'; import type { ResourceIdentifierObject } from '@warp-drive/core-types/spec/raw'; -import type { RecordInstance } from '../-types/q/record-instance'; +import type { OpaqueRecordInstance } from '../-types/q/record-instance'; import { Document } from './document'; import type Store from './store-service'; @@ -200,7 +200,7 @@ function maybeUpdateUiObjects( doc: document as CollectionResourceDataDocument, }); recordArrayManager._keyedArrays.set(identifier.lid, managed); - const doc = new Document(store, identifier); + const doc = new Document(store, identifier); doc.data = managed; doc.meta = document.meta; doc.links = document.links; @@ -223,13 +223,13 @@ function maybeUpdateUiObjects( return document as T; } const data = document.data ? store.peekRecord(document.data) : null; - let doc: Document | undefined; + let doc: Document | undefined; if (identifier) { doc = store._documentCache.get(identifier); } if (!doc) { - doc = new Document(store, identifier); + doc = new Document(store, identifier); doc.data = data; copyDocumentProperties(doc, document); diff --git a/packages/store/src/-private/caches/cache-utils.ts b/packages/store/src/-private/caches/cache-utils.ts index 52647659de8..11a3b2ca5e4 100644 --- a/packages/store/src/-private/caches/cache-utils.ts +++ b/packages/store/src/-private/caches/cache-utils.ts @@ -3,16 +3,16 @@ import { assert } from '@ember/debug'; import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier'; import type { Cache } from '../../-types/q/cache'; -import type { RecordInstance } from '../../-types/q/record-instance'; +import type { OpaqueRecordInstance } from '../../-types/q/record-instance'; /* * Returns the Cache instance associated with a given * Model or Identifier */ -export const CacheForIdentifierCache = new Map(); +export const CacheForIdentifierCache = new Map(); -export function setCacheFor(identifier: StableRecordIdentifier | RecordInstance, cache: Cache): void { +export function setCacheFor(identifier: StableRecordIdentifier | OpaqueRecordInstance, cache: Cache): void { assert( `Illegal set of identifier`, !CacheForIdentifierCache.has(identifier) || CacheForIdentifierCache.get(identifier) === cache @@ -20,13 +20,13 @@ export function setCacheFor(identifier: StableRecordIdentifier | RecordInstance, CacheForIdentifierCache.set(identifier, cache); } -export function removeRecordDataFor(identifier: StableRecordIdentifier | RecordInstance): void { +export function removeRecordDataFor(identifier: StableRecordIdentifier | OpaqueRecordInstance): 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 { +export default function peekCache(instance: OpaqueRecordInstance): Cache; +export default function peekCache(instance: StableRecordIdentifier | OpaqueRecordInstance): Cache | null { if (CacheForIdentifierCache.has(instance as StableRecordIdentifier)) { return CacheForIdentifierCache.get(instance as StableRecordIdentifier) as Cache; } diff --git a/packages/store/src/-private/caches/instance-cache.ts b/packages/store/src/-private/caches/instance-cache.ts index 50cfc59d39f..90910c1c973 100644 --- a/packages/store/src/-private/caches/instance-cache.ts +++ b/packages/store/src/-private/caches/instance-cache.ts @@ -4,12 +4,13 @@ import { LOG_INSTANCE_CACHE } from '@ember-data/debugging'; import { DEBUG } from '@ember-data/env'; import type { RecordIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; import type { Value } from '@warp-drive/core-types/json/raw'; +import type { TypedRecordInstance, TypeFromInstance, TypeFromInstanceOrString } from '@warp-drive/core-types/record'; import type { RelationshipSchema } from '@warp-drive/core-types/schema'; import type { ExistingResourceIdentifierObject, NewResourceIdentifierObject } from '@warp-drive/core-types/spec/raw'; import type { Cache } from '../../-types/q/cache'; import type { JsonApiRelationship, JsonApiResource } from '../../-types/q/record-data-json-api'; -import type { RecordInstance } from '../../-types/q/record-instance'; +import type { OpaqueRecordInstance } from '../../-types/q/record-instance'; import RecordReference from '../legacy-model-support/record-reference'; import { CacheCapabilitiesManager } from '../managers/cache-capabilities-manager'; import type { CacheManager } from '../managers/cache-manager'; @@ -24,7 +25,7 @@ type Destroyable = { destroy(): void; }; -function isDestroyable(record: RecordInstance): record is Destroyable { +function isDestroyable(record: OpaqueRecordInstance): record is Destroyable { return Boolean(record && typeof record === 'object' && typeof (record as Destroyable).destroy === 'function'); } @@ -32,9 +33,9 @@ function isDestroyable(record: RecordInstance): record is Destroyable { @module @ember-data/store */ -const RecordCache = new Map(); +const RecordCache = new Map(); -export function peekRecordIdentifier(record: RecordInstance): StableRecordIdentifier | undefined { +export function peekRecordIdentifier(record: OpaqueRecordInstance): StableRecordIdentifier | undefined { return RecordCache.get(record); } @@ -57,12 +58,16 @@ export function peekRecordIdentifier(record: RecordInstance): StableRecordIdenti @param {Object} record a record instance previously obstained from the store. @return {StableRecordIdentifier} */ -export function recordIdentifierFor(record: RecordInstance): StableRecordIdentifier { +export function recordIdentifierFor( + record: T +): StableRecordIdentifier>; +export function recordIdentifierFor(record: OpaqueRecordInstance): StableRecordIdentifier; +export function recordIdentifierFor(record: T): StableRecordIdentifier> { assert(`${String(record)} is not a record instantiated by @ember-data/store`, RecordCache.has(record)); - return RecordCache.get(record)!; + return RecordCache.get(record)! as StableRecordIdentifier>; } -export function setRecordIdentifier(record: RecordInstance, identifier: StableRecordIdentifier): void { +export function setRecordIdentifier(record: OpaqueRecordInstance, identifier: StableRecordIdentifier): void { if (DEBUG) { if (RecordCache.has(record) && RecordCache.get(record) !== identifier) { throw new Error(`${String(record)} was already assigned an identifier`); @@ -80,9 +85,9 @@ export function setRecordIdentifier(record: RecordInstance, identifier: StableRe RecordCache.set(record, identifier); } -export const StoreMap = new Map(); +export const StoreMap = new Map(); -export function storeFor(record: RecordInstance): Store | undefined { +export function storeFor(record: OpaqueRecordInstance): Store | undefined { const store = StoreMap.get(record); assert( @@ -93,7 +98,7 @@ export function storeFor(record: RecordInstance): Store | undefined { } type Caches = { - record: Map; + record: Map; reference: WeakMap; }; @@ -105,7 +110,7 @@ export class InstanceCache { declare __cacheManager: CacheManager; __instances: Caches = { - record: new Map(), + record: new Map(), reference: new WeakMap(), }; @@ -175,11 +180,11 @@ export class InstanceCache { } ); } - peek(identifier: StableRecordIdentifier): Cache | RecordInstance | undefined { + peek(identifier: StableRecordIdentifier): Cache | OpaqueRecordInstance | undefined { return this.__instances.record.get(identifier); } - getRecord(identifier: StableRecordIdentifier, properties?: CreateRecordProperties): RecordInstance { + getRecord(identifier: StableRecordIdentifier, properties?: CreateRecordProperties): OpaqueRecordInstance { let record = this.__instances.record.get(identifier); if (!record) { @@ -404,7 +409,7 @@ export function resourceIsFullyDeleted(instanceCache: InstanceCache, identifier: Preloaded data can be attributes and relationships passed in either as IDs or as actual models. */ -type PreloadRelationshipValue = RecordInstance | string; +type PreloadRelationshipValue = OpaqueRecordInstance | string; export function preloadData(store: Store, identifier: StableRecordIdentifier, preload: Record) { const jsonPayload: JsonApiResource = {}; //TODO(Igor) consider the polymorphic case @@ -451,7 +456,7 @@ function preloadRelationship( findRecord('user', '1', { preload: { friends: [record] }}); */ function _convertPreloadRelationshipToJSON( - value: RecordInstance | string, + value: OpaqueRecordInstance | string, type: string ): ExistingResourceIdentifierObject | NewResourceIdentifierObject { if (typeof value === 'string' || typeof value === 'number') { diff --git a/packages/store/src/-private/legacy-model-support/record-reference.ts b/packages/store/src/-private/legacy-model-support/record-reference.ts index 40485d4dc87..2f6d93755b9 100644 --- a/packages/store/src/-private/legacy-model-support/record-reference.ts +++ b/packages/store/src/-private/legacy-model-support/record-reference.ts @@ -7,7 +7,7 @@ import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier'; */ import type { SingleResourceDocument } from '@warp-drive/core-types/spec/raw'; -import type { RecordInstance } from '../../-types/q/record-instance'; +import type { OpaqueRecordInstance } from '../../-types/q/record-instance'; import type { NotificationType } from '../managers/notification-manager'; import type Store from '../store-service'; @@ -156,7 +156,7 @@ export default class RecordReference { @param objectOrPromise a JSON:API ResourceDocument or a promise resolving to one @return a promise for the value (record or relationship) */ - push(objectOrPromise: SingleResourceDocument | Promise): Promise { + push(objectOrPromise: SingleResourceDocument | Promise): Promise { // TODO @deprecate pushing unresolved payloads return Promise.resolve(objectOrPromise).then((data) => { return this.store.push(data); @@ -180,7 +180,7 @@ export default class RecordReference { @public @return {Model} the record for this RecordReference */ - value(): RecordInstance | null { + value(): OpaqueRecordInstance | null { return this.store.peekRecord(this.___identifier); } diff --git a/packages/store/src/-private/legacy-model-support/shim-model-class.ts b/packages/store/src/-private/legacy-model-support/shim-model-class.ts index 113d5509429..648ca3b114a 100644 --- a/packages/store/src/-private/legacy-model-support/shim-model-class.ts +++ b/packages/store/src/-private/legacy-model-support/shim-model-class.ts @@ -1,25 +1,27 @@ +import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; import type { AttributeSchema, RelationshipSchema } from '@warp-drive/core-types/schema'; -import type { ModelSchema } from '../../-types/q/ds-model'; +import type { KeyOrString, ModelSchema } from '../../-types/q/ds-model'; import type Store from '../store-service'; -type GenericRecord = Record; - // if modelFor turns out to be a bottleneck we should replace with a Map // and clear it during store teardown. -const AvailableShims = new WeakMap>>(); +const AvailableShims = new WeakMap>(); -export function getShimClass(store: Store, modelName: string): ShimModelClass { +export function getShimClass( + store: Store, + modelName: T extends TypedRecordInstance ? TypeFromInstance : string +): ShimModelClass { let shims = AvailableShims.get(store); if (!shims) { - shims = Object.create(null) as Record>; + shims = Object.create(null) as Record; AvailableShims.set(store, shims); } let shim = shims[modelName]; if (shim === undefined) { - shim = shims[modelName] = new ShimModelClass(store, modelName); + shim = shims[modelName] = new ShimModelClass(store, modelName); } return shim; @@ -36,45 +38,45 @@ function mapFromHash(hash: Record): Map { } // Mimics the static apis of @ember-data/model -export default class ShimModelClass implements ModelSchema { +export default class ShimModelClass implements ModelSchema { declare __store: Store; - declare modelName: string; - constructor(store: Store, modelName: string) { + declare modelName: T extends TypedRecordInstance ? TypeFromInstance : string; + constructor(store: Store, modelName: T extends TypedRecordInstance ? TypeFromInstance : string) { this.__store = store; this.modelName = modelName; } - get fields(): Map { + get fields(): Map, 'attribute' | 'belongsTo' | 'hasMany'> { const attrs = this.__store.getSchemaDefinitionService().attributesDefinitionFor({ type: this.modelName }); const relationships = this.__store .getSchemaDefinitionService() .relationshipsDefinitionFor({ type: this.modelName }); - const fields = new Map(); - Object.keys(attrs).forEach((key) => fields.set(key as keyof T & string, 'attribute')); - Object.keys(relationships).forEach((key) => fields.set(key as keyof T & string, relationships[key]!.kind)); + const fields = new Map, 'attribute' | 'belongsTo' | 'hasMany'>(); + Object.keys(attrs).forEach((key) => fields.set(key as KeyOrString, 'attribute')); + Object.keys(relationships).forEach((key) => fields.set(key as KeyOrString, relationships[key]!.kind)); return fields; } - get attributes(): Map { + get attributes(): Map, AttributeSchema> { const attrs = this.__store.getSchemaDefinitionService().attributesDefinitionFor({ type: this.modelName }); return mapFromHash(attrs as Record); } - get relationshipsByName(): Map { + get relationshipsByName(): Map, RelationshipSchema> { const relationships = this.__store .getSchemaDefinitionService() .relationshipsDefinitionFor({ type: this.modelName }); return mapFromHash(relationships as Record); } - eachAttribute(callback: (key: K, attribute: AttributeSchema) => void, binding?: T) { + eachAttribute>(callback: (key: K, attribute: AttributeSchema) => void, binding?: T) { const attrDefs = this.__store.getSchemaDefinitionService().attributesDefinitionFor({ type: this.modelName }); Object.keys(attrDefs).forEach((key) => { callback.call(binding, key as K, attrDefs[key]); }); } - eachRelationship( + eachRelationship>( callback: (key: K, relationship: RelationshipSchema) => void, binding?: T ) { @@ -86,7 +88,7 @@ export default class ShimModelClass implements ModelSchema }); } - eachTransformedAttribute(callback: (key: K, type: string | null) => void, binding?: T) { + eachTransformedAttribute>(callback: (key: K, type: string | null) => void, binding?: T) { const attrDefs = this.__store.getSchemaDefinitionService().attributesDefinitionFor({ type: this.modelName }); Object.keys(attrDefs).forEach((key) => { if (attrDefs[key]!.type) { diff --git a/packages/store/src/-private/network/request-cache.ts b/packages/store/src/-private/network/request-cache.ts index 9a5f1954b68..43129b17b93 100644 --- a/packages/store/src/-private/network/request-cache.ts +++ b/packages/store/src/-private/network/request-cache.ts @@ -6,7 +6,7 @@ import { assert } from '@ember/debug'; import { DEBUG } from '@ember-data/env'; import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier'; -import type { FindOptions } from '../../-types/q/store'; +import type { FindRecordOptions } from '../../-types/q/store'; import type Store from '../store-service'; const Touching: unique symbol = Symbol('touching'); @@ -15,7 +15,7 @@ const EMPTY_ARR: RequestState[] = DEBUG ? (Object.freeze([]) as unknown as Reque export interface Operation { op: string; - options: FindOptions | undefined; + options: FindRecordOptions | undefined; recordIdentifier: StableRecordIdentifier; } diff --git a/packages/store/src/-private/record-arrays/identifier-array.ts b/packages/store/src/-private/record-arrays/identifier-array.ts index 2642aafd8de..95d825544a0 100644 --- a/packages/store/src/-private/record-arrays/identifier-array.ts +++ b/packages/store/src/-private/record-arrays/identifier-array.ts @@ -13,10 +13,11 @@ import { subscribe, } from '@ember-data/tracking/-private'; import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; import type { ImmutableRequestInfo } from '@warp-drive/core-types/request'; import type { Links, PaginationLinks } from '@warp-drive/core-types/spec/raw'; -import type { RecordInstance } from '../../-types/q/record-instance'; +import type { OpaqueRecordInstance } from '../../-types/q/record-instance'; import { isStableIdentifier } from '../caches/identifier-cache'; import { recordIdentifierFor } from '../caches/instance-cache'; import type RecordArrayManager from '../managers/record-array-manager'; @@ -55,7 +56,7 @@ function isArrayGetter(prop: KeyType): prop is keyof Array { function isArraySetter(prop: KeyType): prop is keyof Array { return ARRAY_SETTER_METHODS.has(prop); } -function isSelfProp(self: T, prop: KeyType): prop is keyof T { +function isSelfProp(self: T, prop: KeyType): prop is Exclude { return prop in self; } @@ -86,9 +87,9 @@ declare global { } } -export type IdentifierArrayCreateOptions = { +export type IdentifierArrayCreateOptions = { identifiers: StableRecordIdentifier[]; - type?: string; + type?: T extends TypedRecordInstance ? TypeFromInstance : string; store: Store; allowMutation: boolean; manager: RecordArrayManager; @@ -100,12 +101,12 @@ interface PrivateState { links: Links | PaginationLinks | null; meta: Record | null; } -type ForEachCB = (record: RecordInstance, index: number, context: typeof Proxy) => void; -function safeForEach( - instance: typeof Proxy, +type ForEachCB = (record: T, index: number, context: typeof Proxy) => void; +function safeForEach( + instance: typeof Proxy, arr: StableRecordIdentifier[], store: Store, - callback: ForEachCB, + callback: ForEachCB, target: unknown ) { if (target === undefined) { @@ -122,7 +123,7 @@ function safeForEach( const length = arr.length; // we need to access length to ensure we are consumed for (let index = 0; index < length; index++) { - callback.call(target, store._instanceCache.getRecord(arr[index]), index, instance); + callback.call(target, store._instanceCache.getRecord(arr[index]) as T, index, instance); } return instance; @@ -140,16 +141,16 @@ function safeForEach( @class RecordArray @public */ -interface IdentifierArray extends Omit, '[]'> { +interface IdentifierArray extends Omit, '[]'> { [MUTATE]?( target: StableRecordIdentifier[], - receiver: typeof Proxy, + receiver: typeof Proxy, prop: string, args: unknown[], _SIGNAL: Signal ): unknown; } -class IdentifierArray { +class IdentifierArray { declare DEPRECATED_CLASS_NAME: string; /** The flag to signal a `RecordArray` is currently loading data. @@ -168,7 +169,7 @@ class IdentifierArray { isLoaded = true; isDestroying = false; isDestroyed = false; - _updatingPromise: Promise | null = null; + _updatingPromise: Promise> | null = null; [IS_COLLECTION] = true; declare [ARRAY_SIGNAL]: Signal; @@ -179,7 +180,7 @@ class IdentifierArray { declare links: Links | PaginationLinks | null; declare meta: Record | null; - declare modelName?: string; + declare modelName?: T extends TypedRecordInstance ? TypeFromInstance : string; /** The store that created this record array. @@ -208,7 +209,7 @@ class IdentifierArray { this[SOURCE].length = value; } - constructor(options: IdentifierArrayCreateOptions) { + constructor(options: IdentifierArrayCreateOptions) { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; this.modelName = options.type; @@ -229,8 +230,8 @@ class IdentifierArray { // we track all mutations within the call // and forward them as one - const proxy = new Proxy(this[SOURCE], { - get>( + const proxy = new Proxy(this[SOURCE], { + get>( target: StableRecordIdentifier[], prop: keyof R, receiver: R @@ -262,7 +263,7 @@ class IdentifierArray { fn = function () { subscribe(_SIGNAL); transaction = true; - const result = safeForEach(receiver, target, store, arguments[0] as ForEachCB, arguments[1]); + const result = safeForEach(receiver, target, store, arguments[0] as ForEachCB, arguments[1]); transaction = false; return result; }; @@ -342,7 +343,7 @@ class IdentifierArray { target: StableRecordIdentifier[], prop: KeyType, value: unknown, - receiver: typeof Proxy + receiver: typeof Proxy ): boolean { if (prop === 'length') { if (!transaction && value === 0) { @@ -380,6 +381,7 @@ class IdentifierArray { target[index] = identifier; return true; } else if (isSelfProp(self, prop)) { + // @ts-expect-error not all properties are indeces and we can't safely cast self[prop] = value; return true; } @@ -431,7 +433,7 @@ class IdentifierArray { getPrototypeOf() { return IdentifierArray.prototype; }, - }) as IdentifierArray; + }) as IdentifierArray; createArrayTags(proxy, _SIGNAL); @@ -460,7 +462,7 @@ class IdentifierArray { @method update @public */ - update(): Promise { + update(): Promise> { if (this.isUpdating) { return this._updatingPromise!; } @@ -485,9 +487,13 @@ class IdentifierArray { Update this RecordArray and return a promise which resolves once the update is finished. */ - _update(): Promise { + _update(): Promise> { assert(`_update cannot be used with this array`, this.modelName); - return this.store.findAll(this.modelName, { reload: true }); + // @ts-expect-error typescript is unable to handle the complexity of + // T = unknown, modelName = string + // T extends TypedRecordInstance, modelName = TypeFromInstance + // both being valid options to pass through here. + return this.store.findAll(this.modelName, { reload: true }); } // TODO deprecate @@ -538,7 +544,7 @@ export type CollectionCreateOptions = IdentifierArrayCreateOptions & { isLoaded: boolean; }; -export class Collection extends IdentifierArray { +export class Collection extends IdentifierArray { query: ImmutableRequestInfo | Record | null = null; constructor(options: CollectionCreateOptions) { @@ -547,13 +553,17 @@ export class Collection extends IdentifierArray { this.isLoaded = options.isLoaded || false; } - _update(): Promise { + _update(): Promise> { const { store, query } = this; // TODO save options from initial request? assert(`update cannot be used with this array`, this.modelName); assert(`update cannot be used with no query`, query); - const promise = store.query(this.modelName, query as Record, { _recordArray: this }); + // @ts-expect-error typescript is unable to handle the complexity of + // T = unknown, modelName = string + // T extends TypedRecordInstance, modelName = TypeFromInstance + // both being valid options to pass through here. + const promise = store.query(this.modelName, query as Record, { _recordArray: this }); return promise; } @@ -570,9 +580,9 @@ Collection.prototype.query = null; // Ensure instanceof works correctly // Object.setPrototypeOf(IdentifierArray.prototype, Array.prototype); -type PromiseProxyRecord = { then(): void; content: RecordInstance | null | undefined }; +type PromiseProxyRecord = { then(): void; content: OpaqueRecordInstance | null | undefined }; -function assertRecordPassedToHasMany(record: RecordInstance | PromiseProxyRecord) { +function assertRecordPassedToHasMany(record: OpaqueRecordInstance | PromiseProxyRecord) { assert( `All elements of a hasMany relationship must be instances of Model, you passed $${typeof record}`, (function () { @@ -586,7 +596,7 @@ function assertRecordPassedToHasMany(record: RecordInstance | PromiseProxyRecord ); } -function extractIdentifierFromRecord(record: PromiseProxyRecord | RecordInstance | null) { +function extractIdentifierFromRecord(record: PromiseProxyRecord | OpaqueRecordInstance | null) { if (!record) { return null; } diff --git a/packages/store/src/-private/store-service.ts b/packages/store/src/-private/store-service.ts index b26adaa64a2..22f84c33620 100644 --- a/packages/store/src/-private/store-service.ts +++ b/packages/store/src/-private/store-service.ts @@ -15,6 +15,7 @@ import type { StableExistingRecordIdentifier, StableRecordIdentifier, } from '@warp-drive/core-types/identifier'; +import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; import { EnableHydration, SkipCache } from '@warp-drive/core-types/request'; import type { ResourceDocument } from '@warp-drive/core-types/spec/document'; import type { @@ -24,13 +25,14 @@ import type { ResourceIdentifierObject, SingleResourceDocument, } from '@warp-drive/core-types/spec/raw'; +import type { ResourceType } from '@warp-drive/core-types/symbols'; import type { Cache, CacheV1 } from '../-types/q/cache'; import type { CacheCapabilitiesManager } from '../-types/q/cache-store-wrapper'; import type { ModelSchema } from '../-types/q/ds-model'; -import type { RecordInstance } from '../-types/q/record-instance'; +import type { OpaqueRecordInstance } from '../-types/q/record-instance'; import type { SchemaService } from '../-types/q/schema-service'; -import type { FindOptions, QueryOptions } from '../-types/q/store'; +import type { FindAllOptions, FindRecordOptions, QueryOptions } from '../-types/q/store'; import type { LifetimesService, StoreRequestContext, StoreRequestInput } from './cache-handler'; import { IdentifierCache } from './caches/identifier-cache'; import { @@ -64,10 +66,26 @@ type CompatStore = Store & { }; function upgradeStore(store: Store): asserts store is CompatStore {} -export interface CreateRecordProperties { - id?: string | null; - [key: string]: unknown; -} +type MaybeHasId = { id?: string | null }; +/** + * Currently only records that extend object can be created via + * store.createRecord. This is a limitation of the current API, + * but can be worked around by creating a new identifier, running + * the cache.clientDidCreate method, and then peeking the record + * for the identifier. + * + * To assign primary key to a record during creation, only `id` will + * work correctly for `store.createRecord`, other primary key may be + * handled by updating the record after creation or using the flow + * described above. + * + * TODO: These are limitations we want to (and can) address. If you + * have need of lifting these limitations, please open an issue. + * + * @typedoc + */ +export type CreateRecordProperties = MaybeHasId & Record> = + T extends TypedRecordInstance ? Partial> : MaybeHasId & Record; /** * A Store coordinates interaction between your application, a [Cache](https://api.emberjs.com/ember-data/release/classes/%3CInterface%3E%20Cache), @@ -95,9 +113,12 @@ interface Store { createCache(storeWrapper: CacheCapabilitiesManager): Cache; - instantiateRecord(identifier: StableRecordIdentifier, createRecordArgs: { [key: string]: unknown }): RecordInstance; + instantiateRecord( + identifier: StableRecordIdentifier, + createRecordArgs: { [key: string]: unknown } + ): OpaqueRecordInstance; - teardownRecord(record: RecordInstance): void; + teardownRecord(record: OpaqueRecordInstance): void; } class Store extends EmberObject { @@ -205,7 +226,10 @@ class Store extends EmberObject { declare _graph?: Graph; declare _requestCache: RequestStateService; declare _instanceCache: InstanceCache; - declare _documentCache: Map>; + declare _documentCache: Map< + StableDocumentIdentifier, + Document + >; declare _cbs: { coalesce?: () => void; sync?: () => void; notify?: () => void } | null; declare _forceShim: boolean; @@ -607,7 +631,7 @@ class Store extends EmberObject { } /** - Returns the schema for a particular `modelName`. + Returns the schema for a particular resource type (modelName). When used with Model from @ember-data/model the return is the model class, but this is not guaranteed. @@ -625,12 +649,13 @@ class Store extends EmberObject { @method modelFor @public - @param {String} type + @param {string} type @return {ModelSchema} */ // TODO @deprecate in favor of schema APIs, requires adapter/serializer overhaul or replacement - - modelFor(type: string): ModelSchema { + modelFor(type: TypeFromInstance): ModelSchema; + modelFor(type: string): ModelSchema; + modelFor(type: T extends TypedRecordInstance ? TypeFromInstance : string): ModelSchema { if (DEBUG) { assertDestroyedStoreOnly(this, 'modelFor'); } @@ -640,7 +665,7 @@ class Store extends EmberObject { this.getSchemaDefinitionService().doesTypeExist(type) ); - return getShimClass(this, type); + return getShimClass(this, type); } /** @@ -667,19 +692,21 @@ class Store extends EmberObject { @method createRecord @public - @param {String} modelName + @param {String} type the name of the resource @param {Object} inputProperties a hash of properties to set on the newly created record. @return {Model} record */ - createRecord(modelName: string, inputProperties: CreateRecordProperties): RecordInstance { + createRecord(type: TypeFromInstance, inputProperties: CreateRecordProperties): T; + createRecord(type: string, inputProperties: CreateRecordProperties): OpaqueRecordInstance; + createRecord(type: string, inputProperties: CreateRecordProperties): OpaqueRecordInstance { if (DEBUG) { assertDestroyingStore(this, 'createRecord'); } - assert(`You need to pass a model name to the store's createRecord method`, modelName); + assert(`You need to pass a model name to the store's createRecord method`, type); assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${type}`, + typeof type === 'string' ); // This is wrapped in a `run.join` so that in test environments users do not need to manually wrap @@ -687,30 +714,31 @@ class Store extends EmberObject { // of record-arrays via ember's run loop, not our own. // // to remove this, we would need to move to a new `async` API. - let record!: RecordInstance; + let record!: OpaqueRecordInstance; this._join(() => { - const normalizedModelName = normalizeModelName(modelName); + const normalizedModelName = normalizeModelName(type); const properties = { ...inputProperties }; // If the passed properties do not include a primary key, // give the adapter an opportunity to generate one. Typically, // client-side ID generators will use something like uuid.js // to avoid conflicts. + let id: string | null = null; if (properties.id === null || properties.id === undefined) { upgradeStore(this); - const adapter = this.adapterFor?.(modelName, true); + const adapter = this.adapterFor?.(normalizedModelName, true); if (adapter && adapter.generateIdForRecord) { - properties.id = adapter.generateIdForRecord(this, modelName, properties); + id = properties.id = coerceId(adapter.generateIdForRecord(this, normalizedModelName, properties)); } else { - properties.id = null; + id = properties.id = null; } + } else { + id = properties.id = coerceId(properties.id); } - // Coerce ID to a string - properties.id = coerceId(properties.id); - const resource = { type: normalizedModelName, id: properties.id }; + const resource = { type: normalizedModelName, id }; if (resource.id) { const identifier = this.identifierCache.peekRecordIdentifier(resource as ResourceIdentifierObject); @@ -747,9 +775,9 @@ class Store extends EmberObject { @method deleteRecord @public - @param {Model} record + @param {unknown} record */ - deleteRecord(record: RecordInstance): void { + deleteRecord(record: T): void { if (DEBUG) { assertDestroyingStore(this, 'deleteRecord'); } @@ -782,7 +810,7 @@ class Store extends EmberObject { @public @param {Model} record */ - unloadRecord(record: RecordInstance): void { + unloadRecord(record: T): void { if (DEBUG) { assertDestroyingStore(this, 'unloadRecord'); } @@ -1158,18 +1186,20 @@ class Store extends EmberObject { @since 1.13.0 @method findRecord @public - @param {String|object} modelName - either a string representing the modelName or a ResourceIdentifier object containing both the type (a string) and the id (a string) for the record or an lid (a string) of an existing record + @param {String|object} type - either a string representing the name of the resource or a ResourceIdentifier object containing both the type (a string) and the id (a string) for the record or an lid (a string) of an existing record @param {(String|Integer|Object)} id - optional object with options for the request only if the first param is a ResourceIdentifier, else the string id of the record to be retrieved @param {Object} [options] - if the first param is a string this will be the optional options for the request. See examples for available options. @return {Promise} promise */ - findRecord(resource: string, id: string | number, options?: FindOptions): Promise; - findRecord(resource: ResourceIdentifierObject, id?: FindOptions): Promise; + findRecord(resource: TypeFromInstance, id: string | number, options?: FindRecordOptions): Promise; + findRecord(resource: string, id: string | number, options?: FindRecordOptions): Promise; + findRecord(resource: ResourceIdentifierObject>, id?: FindRecordOptions): Promise; + findRecord(resource: ResourceIdentifierObject, id?: FindRecordOptions): Promise; findRecord( resource: string | ResourceIdentifierObject, - id?: string | number | FindOptions, - options?: FindOptions - ): Promise { + id?: string | number | FindRecordOptions, + options?: FindRecordOptions + ): Promise { if (DEBUG) { assertDestroyingStore(this, 'findRecord'); } @@ -1179,7 +1209,7 @@ class Store extends EmberObject { resource ); if (isMaybeIdentifier(resource)) { - options = id as FindOptions | undefined; + options = id as FindRecordOptions | undefined; } else { assert( `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${resource}`, @@ -1205,7 +1235,7 @@ class Store extends EmberObject { }); } - const promise = this.request({ + const promise = this.request({ op: 'findRecord', data: { record: identifier, @@ -1330,9 +1360,9 @@ class Store extends EmberObject { @param {String|Integer} id - optional only if the first param is a ResourceIdentifier, else the string id of the record to be retrieved. @return {Model|null} record */ - peekRecord(identifier: string, id: string | number): T | null; - peekRecord(identifier: ResourceIdentifierObject): T | null; - peekRecord(identifier: ResourceIdentifierObject | string, id?: string | number): T | null { + peekRecord(identifier: string, id: string | number): T | null; + peekRecord(identifier: ResourceIdentifierObject): T | null; + peekRecord(identifier: ResourceIdentifierObject | string, id?: string | number): T | null { if (arguments.length === 1 && isMaybeIdentifier(identifier)) { const stableIdentifier = this.identifierCache.peekRecordIdentifier(identifier); const isLoaded = stableIdentifier && this._instanceCache.recordIsLoaded(stableIdentifier); @@ -1408,32 +1438,30 @@ class Store extends EmberObject { @since 1.13.0 @method query @public - @param {String} modelName - @param {any} query an opaque query to be used by the adapter + @param {String} type the name of the resource + @param {object} query a query to be used by the adapter @param {Object} options optional, may include `adapterOptions` hash which will be passed to adapter.query @return {Promise} promise */ - query( - modelName: string, - query: Record, - options: { [key: string]: unknown; adapterOptions?: Record } - ): Promise { + query(type: TypeFromInstance, query: Record, options?: QueryOptions): Promise>; + query(type: string, query: Record, options?: QueryOptions): Promise; + query(type: string, query: Record, options: QueryOptions = {}): Promise { if (DEBUG) { assertDestroyingStore(this, 'query'); } - assert(`You need to pass a model name to the store's query method`, modelName); + assert(`You need to pass a model name to the store's query method`, type); assert(`You need to pass a query hash to the store's query method`, query); assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${type}`, + typeof type === 'string' ); const promise = this.request({ op: 'query', data: { - type: normalizeModelName(modelName), + type: normalizeModelName(type), query, - options: options || {}, + options: options, }, cacheOptions: { [SkipCache as symbol]: true }, }); @@ -1543,7 +1571,7 @@ class Store extends EmberObject { modelName: string, query: Record, options?: QueryOptions - ): Promise { + ): Promise { if (DEBUG) { assertDestroyingStore(this, 'queryRecord'); } @@ -1554,7 +1582,7 @@ class Store extends EmberObject { typeof modelName === 'string' ); - const promise = this.request({ + const promise = this.request({ op: 'queryRecord', data: { type: normalizeModelName(modelName), @@ -1751,24 +1779,26 @@ class Store extends EmberObject { @since 1.13.0 @method findAll @public - @param {String} modelName - @param {Object} options + @param {string} type the name of the resource + @param {object} options @return {Promise} promise */ - findAll(modelName: string, options: { reload?: boolean; backgroundReload?: boolean } = {}): Promise { + findAll(type: TypeFromInstance, options?: FindAllOptions): Promise>; + findAll(type: string, options?: FindAllOptions): Promise; + findAll(type: TypeFromInstance | string, options: FindAllOptions = {}): Promise> { if (DEBUG) { assertDestroyingStore(this, 'findAll'); } - assert(`You need to pass a model name to the store's findAll method`, modelName); + assert(`You need to pass a model name to the store's findAll method`, type); assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${type}`, + typeof type === 'string' ); - const promise = this.request({ + const promise = this.request>({ op: 'findAll', data: { - type: normalizeModelName(modelName), + type: normalizeModelName(type), options: options || {}, }, cacheOptions: { [SkipCache as symbol]: true }, @@ -1799,21 +1829,22 @@ class Store extends EmberObject { @since 1.13.0 @method peekAll @public - @param {String} modelName + @param {string} type the name of the resource @return {RecordArray} */ - peekAll(modelName: string): IdentifierArray { + peekAll(type: TypeFromInstance): IdentifierArray; + peekAll(type: string): IdentifierArray; + peekAll(type: string): IdentifierArray { if (DEBUG) { assertDestroyingStore(this, 'peekAll'); } - assert(`You need to pass a model name to the store's peekAll method`, modelName); + assert(`You need to pass a model name to the store's peekAll method`, type); assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, - typeof modelName === 'string' + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${type}`, + typeof type === 'string' ); - const type = normalizeModelName(modelName); - return this.recordArrayManager.liveArrayFor(type); + return this.recordArrayManager.liveArrayFor(normalizeModelName(type)); } /** @@ -1828,22 +1859,22 @@ class Store extends EmberObject { ``` @method unloadAll + @param {string} type the name of the resource @public - @param {String} modelName */ - unloadAll(modelName?: string) { + unloadAll(type: TypeFromInstance): void; + unloadAll(type?: string): void; + unloadAll(type?: string) { if (DEBUG) { assertDestroyedStoreOnly(this, 'unloadAll'); } assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${String( - modelName - )}`, - !modelName || typeof modelName === 'string' + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${String(type)}`, + !type || typeof type === 'string' ); this._join(() => { - if (modelName === undefined) { + if (type === undefined) { // destroy the graph before unloadAll // since then we avoid churning relationships // during unload @@ -1852,8 +1883,7 @@ class Store extends EmberObject { this.recordArrayManager.clear(); this._instanceCache.clear(); } else { - const normalizedModelName = normalizeModelName(modelName); - this._instanceCache.clear(normalizedModelName); + this._instanceCache.clear(normalizeModelName(type)); } }); } @@ -2010,9 +2040,11 @@ class Store extends EmberObject { updated. */ push(data: EmptyResourceDocument): null; - push(data: SingleResourceDocument): RecordInstance; - push(data: CollectionResourceDocument): RecordInstance[]; - push(data: JsonApiDocument): RecordInstance | RecordInstance[] | null { + push(data: SingleResourceDocument>): T; + push(data: SingleResourceDocument): OpaqueRecordInstance; + push(data: CollectionResourceDocument>): T[]; + push(data: CollectionResourceDocument): OpaqueRecordInstance[]; + push(data: JsonApiDocument): OpaqueRecordInstance | OpaqueRecordInstance[] | null { if (DEBUG) { assertDestroyingStore(this, 'push'); } @@ -2072,13 +2104,15 @@ class Store extends EmberObject { /** * Trigger a save for a Record. * + * Returns a promise resolving with the same record when the save is complete. + * * @method saveRecord * @public - * @param {RecordInstance} record + * @param {unknown} record * @param options - * @return {Promise} + * @return {Promise} */ - saveRecord(record: RecordInstance, options: Record = {}): Promise { + saveRecord(record: T, options: Record = {}): Promise { if (DEBUG) { assertDestroyingStore(this, 'saveRecord'); } @@ -2124,7 +2158,7 @@ class Store extends EmberObject { // we lie here on the type because legacy doesn't have enough context cache.willCommit(identifier, { request } as unknown as StoreRequestContext); - return this.request(request).then((document) => document.content); + return this.request(request).then((document) => document.content); } /** @@ -2246,9 +2280,9 @@ function normalizeProperties( if (def !== undefined) { if (def.kind === 'hasMany') { if (DEBUG) { - assertRecordsPassedToHasMany(properties[prop] as RecordInstance[]); + assertRecordsPassedToHasMany(properties[prop] as OpaqueRecordInstance[]); } - relationshipValue = extractIdentifiersFromRecords(properties[prop] as RecordInstance[]); + relationshipValue = extractIdentifiersFromRecords(properties[prop] as OpaqueRecordInstance[]); } else { relationshipValue = extractIdentifierFromRecord(properties[prop]); } @@ -2261,7 +2295,7 @@ function normalizeProperties( return properties; } -function assertRecordsPassedToHasMany(records: RecordInstance[]) { +function assertRecordsPassedToHasMany(records: OpaqueRecordInstance[]) { assert(`You must pass an array of records to set a hasMany relationship`, Array.isArray(records)); assert( `All elements of a hasMany relationship must be instances of Model, you passed ${records @@ -2280,13 +2314,13 @@ function assertRecordsPassedToHasMany(records: RecordInstance[]) { ); } -function extractIdentifiersFromRecords(records: RecordInstance[]): StableRecordIdentifier[] { +function extractIdentifiersFromRecords(records: OpaqueRecordInstance[]): StableRecordIdentifier[] { return records.map((record) => extractIdentifierFromRecord(record)) as StableRecordIdentifier[]; } -type PromiseProxyRecord = { then(): void; content: RecordInstance | null | undefined }; +type PromiseProxyRecord = { then(): void; content: OpaqueRecordInstance | null | undefined }; -function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | RecordInstance | null) { +function extractIdentifierFromRecord(recordOrPromiseRecord: PromiseProxyRecord | OpaqueRecordInstance | null) { if (!recordOrPromiseRecord) { return null; } diff --git a/packages/store/src/-private/utils/coerce-id.ts b/packages/store/src/-private/utils/coerce-id.ts index d124bcc996d..8d41509aa0c 100644 --- a/packages/store/src/-private/utils/coerce-id.ts +++ b/packages/store/src/-private/utils/coerce-id.ts @@ -14,7 +14,7 @@ import { DEPRECATE_NON_STRICT_ID } from '@ember-data/deprecations'; // corresponding record, we will not know if it is a string or a number. type Coercable = string | number | boolean | null | undefined | symbol; -function coerceId(id: Coercable): string | null { +function coerceId(id: unknown): string | null { if (DEPRECATE_NON_STRICT_ID) { let normalized: string | null; if (id === null || id === undefined || id === '') { diff --git a/packages/store/src/-types/q/ds-model.ts b/packages/store/src/-types/q/ds-model.ts index ca2405c024c..343c38e3573 100644 --- a/packages/store/src/-types/q/ds-model.ts +++ b/packages/store/src/-types/q/ds-model.ts @@ -1,20 +1,22 @@ +import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; import type { AttributeSchema, RelationshipSchema } from '@warp-drive/core-types/schema'; -type GenericRecord = Record; -export interface ModelSchema { - modelName: string; - fields: Map; - attributes: Map; - relationshipsByName: Map; - eachAttribute( +export type KeyOrString = keyof T & string extends never ? string : keyof T & string; + +export interface ModelSchema { + modelName: T extends TypedRecordInstance ? TypeFromInstance : string; + fields: Map, 'attribute' | 'belongsTo' | 'hasMany'>; + attributes: Map, AttributeSchema>; + relationshipsByName: Map, RelationshipSchema>; + eachAttribute>( callback: (this: ModelSchema, key: K, attribute: AttributeSchema) => void, binding?: T ): void; - eachRelationship( + eachRelationship>( callback: (this: ModelSchema, key: K, relationship: RelationshipSchema) => void, binding?: T ): void; - eachTransformedAttribute( + eachTransformedAttribute>( callback: (this: ModelSchema, key: K, type: string | null) => void, binding?: T ): void; diff --git a/packages/store/src/-types/q/record-instance.ts b/packages/store/src/-types/q/record-instance.ts index 159391d910a..6e8d04219bb 100644 --- a/packages/store/src/-types/q/record-instance.ts +++ b/packages/store/src/-types/q/record-instance.ts @@ -2,15 +2,26 @@ @module @ember-data/store */ -/* - A `Record` is the result of the store instantiating a class to present data for a resource to the UI. +/** + In EmberData, a "record instance" is a class instance used to present the data + for a single resource, transforming the resource's cached raw data into a form + that is useful for the application. - Historically in `ember-data` this meant that it was the result of calling `ModelFactory.create()` to - gain instance to a class built upon `@ember-data/model`. However, as we go forward into a future in which - model instances (aka `Records`) are completely user supplied and opaque to the internals, we need a type - through which to communicate what is valid. + Since every application's needs are different, EmberData does not assume to know + what the shape of the record instance should be. Instead, it provides a way to + define the record instance's via the `instantiateRecord` hook on the store. - The type belows allows for anything extending object. -*/ + Thus for most purposes the `RecordInstance` type is "opaque" to EmberData, and + should be treated as "unknown" by the library. -export type RecordInstance = unknown; + Wherever possible, if typing an API that is consumer facing, instead of using + OpaqueRecordInstance, we should prefer to use a generic and check if the generic + extends `TypedRecordInstance`. This allows consumers to define their own record + instance types and not only have their types flow through EmberData APIs, but + also allows EmberData to provide typechecking and intellisense for the record + based on a special symbol prsent on record instances that implement the + `TypedRecordInstance` interface. + + @typedoc +*/ +export type OpaqueRecordInstance = unknown; diff --git a/packages/store/src/-types/q/schema-service.ts b/packages/store/src/-types/q/schema-service.ts index 34dc5d6687e..6a7b47e61a2 100644 --- a/packages/store/src/-types/q/schema-service.ts +++ b/packages/store/src/-types/q/schema-service.ts @@ -24,18 +24,23 @@ export interface FieldSchema { } /** - * A SchemaDefinitionService implementation provides the ability - * to query for various information about a resource in an abstract manner. + * The SchemaService provides the ability to query for information about the structure + * of any resource type. * - * How an implementation determines this information is left up to the implementation, - * this means that schema information could be lazily populated, derived-on-demand, - * or progressively enhanced during the course of an application's runtime. + * Applications can provide any implementation of the SchemaService they please so long + * as it conforms to this interface. * - * The implementation provided to work with `@ember-data/model` makes use of the - * static schema properties on those classes to respond to these queries; however, - * that is not a necessary approach. For instance, Schema information could be sideloaded - * or pre-flighted for API calls, resulting in no need to bundle and ship potentially - * large and expensive JSON or JS schemas to pull information from. + * The design of the service means that schema information could be lazily populated, + * derived-on-demand, or progressively enhanced during the course of an application's runtime. + * The primary requirement is merely that any information the service needs to correctly + * respond to an inquest is available by the time it is asked. + * + * The `@ember-data/model` package provides an implementation of this service which + * makes use of your model classes as the source of information to respond to queries + * about resource schema. While this is useful, this may not be ideal for your application. + * For instance, Schema information could be sideloaded or pre-flighted for API calls, + * resulting in no need to bundle and ship potentially large and expensive JSON + * or large Javascript based Models to pull information from. * * To register a custom schema implementation, extend the store service or * lookup and register the schema service first thing on app-boot. Example below @@ -48,13 +53,13 @@ export interface FieldSchema { * export default class extends Store { * constructor(...args) { * super(...args); - * this.registerSchemaDefinitionService(new CustomSchemas()); + * this.registerSchema(new CustomSchemas()); * } * } * ``` * - * At runtime, both the `Store` and the `StoreWrapper` provide - * access to this service via the `getSchemaDefinitionService()` method. + * At runtime, both the `Store` and the `CacheCapabilitiesManager` provide + * access to this service via the `schema` property. * * ```ts * export default class extends Component { @@ -62,15 +67,21 @@ export interface FieldSchema { * * get attributes() { * return this.store - * .getSchemaDefinitionService() + * .schema * .attributesDefinitionFor(this.args.dataType); * } * } * ``` * - * This is not a class and cannot be instantiated. + * Note: there can only be one schema service registered at a time. + * If you register a new schema service, the old one will be replaced. + * + * If you would like to inherit from another schema service, you can do so by + * using typical class inheritance patterns OR by accessing the existing + * schema service at runtime before replacing it with your own, and then + * having your own delegate to it when needed. * - * @class SchemaService + * @class SchemaService * @public */ export interface SchemaService { diff --git a/packages/store/src/-types/q/store.ts b/packages/store/src/-types/q/store.ts index 4ce5cf309ff..ee99e8e12b7 100644 --- a/packages/store/src/-types/q/store.ts +++ b/packages/store/src/-types/q/store.ts @@ -1,13 +1,17 @@ import type { Value } from '@warp-drive/core-types/json/raw'; -export interface FindOptions { +export interface BaseFinderOptions { reload?: boolean; backgroundReload?: boolean; - include?: string; + include?: string | string[]; adapterOptions?: Record; +} +export interface FindRecordOptions extends BaseFinderOptions { preload?: Record; } -export interface QueryOptions { - adapterOptions?: Record; -} +export type QueryOptions = { + [K in string | 'adapterOptions']?: K extends 'adapterOptions' ? Record : unknown; +}; + +export type FindAllOptions = BaseFinderOptions; diff --git a/tests/builders/app/services/store.ts b/tests/builders/app/services/store.ts index 615242ee715..51b80238721 100644 --- a/tests/builders/app/services/store.ts +++ b/tests/builders/app/services/store.ts @@ -9,6 +9,7 @@ import type { CacheCapabilitiesManager } from '@ember-data/store/-types/q/cache- import type { ModelSchema } from '@ember-data/store/-types/q/ds-model'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { Cache } from '@warp-drive/core-types/cache'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; export default class Store extends DataStore { constructor(args: unknown) { @@ -36,7 +37,9 @@ export default class Store extends DataStore { return teardownRecord.call(this, record); } + override modelFor(type: TypeFromInstance): ModelSchema; + override modelFor(type: string): ModelSchema; override modelFor(type: string): ModelSchema { - return modelFor.call(this, type)!; + return modelFor.call(this, type) || super.modelFor(type); } } diff --git a/tests/docs/fixtures/expected.js b/tests/docs/fixtures/expected.js index 62e4c5fee32..f2755d8610d 100644 --- a/tests/docs/fixtures/expected.js +++ b/tests/docs/fixtures/expected.js @@ -521,9 +521,9 @@ module.exports = { '(public) @ember-data/store RecordReference#reload', '(public) @ember-data/store RecordReference#remoteType', '(public) @ember-data/store RecordReference#value', - '(public) @ember-data/store SchemaService#attributesDefinitionFor', - '(public) @ember-data/store SchemaService#doesTypeExist', - '(public) @ember-data/store SchemaService#relationshipsDefinitionFor', + '(public) @ember-data/store SchemaService#attributesDefinitionFor', + '(public) @ember-data/store SchemaService#doesTypeExist', + '(public) @ember-data/store SchemaService#relationshipsDefinitionFor', '(public) @ember-data/store Snapshot#adapterOptions', '(public) @ember-data/store Snapshot#attr', '(public) @ember-data/store Snapshot#attributes', diff --git a/tests/ember-data__adapter/app/services/store.ts b/tests/ember-data__adapter/app/services/store.ts index 3a52705a78e..3b285906e10 100644 --- a/tests/ember-data__adapter/app/services/store.ts +++ b/tests/ember-data__adapter/app/services/store.ts @@ -16,6 +16,7 @@ import BaseStore, { CacheHandler } from '@ember-data/store'; import type { CacheCapabilitiesManager } from '@ember-data/store/-types/q/cache-store-wrapper'; import type { ModelSchema } from '@ember-data/store/-types/q/ds-model'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; export default class Store extends BaseStore { constructor(args: unknown) { @@ -38,7 +39,9 @@ export default class Store extends BaseStore { teardownRecord.call(this, record); } - override modelFor(type: string): ModelSchema> { + override modelFor(type: TypeFromInstance): ModelSchema; + override modelFor(type: string): ModelSchema; + override modelFor(type: string): ModelSchema { return modelFor.call(this, type) || super.modelFor(type); } diff --git a/tests/ember-data__graph/app/services/store.ts b/tests/ember-data__graph/app/services/store.ts index 3a52705a78e..3b285906e10 100644 --- a/tests/ember-data__graph/app/services/store.ts +++ b/tests/ember-data__graph/app/services/store.ts @@ -16,6 +16,7 @@ import BaseStore, { CacheHandler } from '@ember-data/store'; import type { CacheCapabilitiesManager } from '@ember-data/store/-types/q/cache-store-wrapper'; import type { ModelSchema } from '@ember-data/store/-types/q/ds-model'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; export default class Store extends BaseStore { constructor(args: unknown) { @@ -38,7 +39,9 @@ export default class Store extends BaseStore { teardownRecord.call(this, record); } - override modelFor(type: string): ModelSchema> { + override modelFor(type: TypeFromInstance): ModelSchema; + override modelFor(type: string): ModelSchema; + override modelFor(type: string): ModelSchema { return modelFor.call(this, type) || super.modelFor(type); } diff --git a/tests/ember-data__graph/tests/integration/graph/edge-removal/helpers.ts b/tests/ember-data__graph/tests/integration/graph/edge-removal/helpers.ts index 3d46db9265e..9ea6d254ac7 100644 --- a/tests/ember-data__graph/tests/integration/graph/edge-removal/helpers.ts +++ b/tests/ember-data__graph/tests/integration/graph/edge-removal/helpers.ts @@ -3,7 +3,7 @@ import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import { recordIdentifierFor } from '@ember-data/store'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; -import type { CollectionResourceDocument, SingleResourceDocument } from '@warp-drive/core-types/spec/raw'; +import type { CollectionResourceDocument } from '@warp-drive/core-types/spec/raw'; import type { Diagnostic } from '@warp-drive/diagnostic'; import type { Context, UserRecord } from './setup'; @@ -109,7 +109,7 @@ export async function setInitialState(context: Context, config: TestConfig, asse let chris: UserRecord, john: UserRecord, johnIdentifier: StableRecordIdentifier; if (!config.useCreate) { - const data: CollectionResourceDocument = { + const data: CollectionResourceDocument<'user'> = { data: [ { type: 'user', @@ -126,17 +126,17 @@ export async function setInitialState(context: Context, config: TestConfig, asse ], }; - [chris, john] = store.push(data); + [chris, john] = store.push(data); johnIdentifier = identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); } else { - chris = store.push({ + chris = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Chris' }, }, - } as SingleResourceDocument); - john = store.createRecord('user', { name: 'John', bestFriends: isMany ? [chris] : chris }) as UserRecord; + }); + john = store.createRecord('user', { name: 'John', bestFriends: isMany ? [chris] : chris }); johnIdentifier = recordIdentifierFor(john); } diff --git a/tests/ember-data__graph/tests/integration/graph/edge-removal/setup.ts b/tests/ember-data__graph/tests/integration/graph/edge-removal/setup.ts index abf1492d1c7..838ebf74614 100644 --- a/tests/ember-data__graph/tests/integration/graph/edge-removal/setup.ts +++ b/tests/ember-data__graph/tests/integration/graph/edge-removal/setup.ts @@ -8,15 +8,9 @@ import type { Graph, GraphEdge } from '@ember-data/graph/-private/graph'; import type Model from '@ember-data/model'; import type Store from '@ember-data/store'; import type { ModelSchema } from '@ember-data/store/-types/q/ds-model'; -import type { RecordInstance } from '@ember-data/store/-types/q/record-instance'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { CollectionRelationship } from '@warp-drive/core-types/cache/relationship'; -import type { - CollectionResourceDocument, - EmptyResourceDocument, - JsonApiDocument, - SingleResourceDocument, -} from '@warp-drive/core-types/spec/raw'; +import type { ResourceType } from '@warp-drive/core-types/symbols'; import type { EmberHooks } from '@warp-drive/diagnostic'; import { setupTest } from '@warp-drive/diagnostic/ember'; @@ -134,30 +128,24 @@ class Serializer { } } -export interface UserRecord extends Model { +export type UserRecord = Model & { name?: string; bestFriend?: UserRecord; bestFriends?: UserRecord[]; -} + [ResourceType]: 'user'; +}; export interface Context extends TestContext { - store: TestStore; + store: Store; graph: AbstractGraph; } -interface TestStore extends Store { - push(data: EmptyResourceDocument): null; - push(data: SingleResourceDocument): T; - push(data: CollectionResourceDocument): T[]; - push(data: JsonApiDocument): T | T[] | null; -} - export function setupGraphTest(hooks: EmberHooks) { setupTest(hooks); hooks.beforeEach(function (this: Context) { this.owner.register('adapter:application', Adapter); this.owner.register('serializer:application', Serializer); - this.store = this.owner.lookup('service:store') as TestStore; + this.store = this.owner.lookup('service:store') as Store; this.graph = graphForTest(this.store); }); } diff --git a/tests/ember-data__graph/tests/integration/graph/edge-test.ts b/tests/ember-data__graph/tests/integration/graph/edge-test.ts index d028cb76b13..a9e48a54283 100644 --- a/tests/ember-data__graph/tests/integration/graph/edge-test.ts +++ b/tests/ember-data__graph/tests/integration/graph/edge-test.ts @@ -6,6 +6,7 @@ import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import type Store from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store'; import { peekCache } from '@ember-data/store/-private'; +import { ResourceType } from '@warp-drive/core-types/symbols'; import { module, test } from '@warp-drive/diagnostic'; import { setupTest } from '@warp-drive/diagnostic/ember'; @@ -36,6 +37,7 @@ module('Integration | Graph | Edges', function (hooks) { class User extends Model { @attr declare name: string; @belongsTo('user', { async: false, inverse: 'bestFriend' }) declare bestFriend: User; + [ResourceType] = 'user' as const; } owner.register('model:user', User); @@ -57,7 +59,7 @@ module('Integration | Graph | Edges', function (hooks) { 'We still have no record data instance after accessing a named relationship' ); - store.push({ + store.push({ data: { type: 'user', id: '2', @@ -76,13 +78,13 @@ module('Integration | Graph | Edges', function (hooks) { assert.deepEqual(state.remote, [identifier2], 'Our initial canonical state is correct'); assert.deepEqual(state.local, [identifier2], 'Our initial current state is correct'); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Chris' }, }, - }) as User; + }); assert.equal( peekCache(identifier)?.getAttr(identifier, 'name'), diff --git a/tests/ember-data__graph/tests/integration/graph/polymorphism/implicit-keys-test.ts b/tests/ember-data__graph/tests/integration/graph/polymorphism/implicit-keys-test.ts index 54b55aa8c4c..40c0118e687 100644 --- a/tests/ember-data__graph/tests/integration/graph/polymorphism/implicit-keys-test.ts +++ b/tests/ember-data__graph/tests/integration/graph/polymorphism/implicit-keys-test.ts @@ -4,6 +4,8 @@ import { graphFor } from '@ember-data/graph/-private'; import Model, { attr, belongsTo } from '@ember-data/model'; import type Store from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store'; +import type { CollectionResourceDocument } from '@warp-drive/core-types/spec/raw'; +import { ResourceType } from '@warp-drive/core-types/symbols'; import { module, test } from '@warp-drive/diagnostic'; import { setupTest } from '@warp-drive/diagnostic/ember'; @@ -15,13 +17,16 @@ module('Integration | Graph | Implicit Keys', function (hooks) { class User extends Model { @attr declare name: string; @belongsTo('organization', { async: false, inverse: null }) declare organization: Organization; + [ResourceType] = 'user' as const; } class Product extends Model { @attr declare name: string; @belongsTo('organization', { async: false, inverse: null }) declare organization: Organization; + [ResourceType] = 'product' as const; } class Organization extends Model { @attr declare name: string; + [ResourceType] = 'organization' as const; } owner.register('model:user', User); owner.register('model:product', Product); @@ -29,10 +34,10 @@ module('Integration | Graph | Implicit Keys', function (hooks) { const store = owner.lookup('service:store') as unknown as Store; const graph = graphFor(store); - let user, product, organization; + let user!: User, product!: Product, organization!: Organization; await assert.expectNoAssertion(() => { - [user, product, organization] = store.push({ + const data: CollectionResourceDocument<'user' | 'product' | 'organization'> = { data: [ { type: 'user', @@ -56,7 +61,8 @@ module('Integration | Graph | Implicit Keys', function (hooks) { attributes: { name: 'Ember.js' }, }, ], - }); + }; + [user, product, organization] = store.push(data) as [User, Product, Organization]; }); const userIdentifier = recordIdentifierFor(user); diff --git a/tests/ember-data__serializer/app/services/store.ts b/tests/ember-data__serializer/app/services/store.ts index 3a52705a78e..3b285906e10 100644 --- a/tests/ember-data__serializer/app/services/store.ts +++ b/tests/ember-data__serializer/app/services/store.ts @@ -16,6 +16,7 @@ import BaseStore, { CacheHandler } from '@ember-data/store'; import type { CacheCapabilitiesManager } from '@ember-data/store/-types/q/cache-store-wrapper'; import type { ModelSchema } from '@ember-data/store/-types/q/ds-model'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; export default class Store extends BaseStore { constructor(args: unknown) { @@ -38,7 +39,9 @@ export default class Store extends BaseStore { teardownRecord.call(this, record); } - override modelFor(type: string): ModelSchema> { + override modelFor(type: TypeFromInstance): ModelSchema; + override modelFor(type: string): ModelSchema; + override modelFor(type: string): ModelSchema { return modelFor.call(this, type) || super.modelFor(type); } diff --git a/tests/main/tests/integration/cache-handler/store-package-setup-test.ts b/tests/main/tests/integration/cache-handler/store-package-setup-test.ts index 15fcd3ef368..5c984c14f2b 100644 --- a/tests/main/tests/integration/cache-handler/store-package-setup-test.ts +++ b/tests/main/tests/integration/cache-handler/store-package-setup-test.ts @@ -17,7 +17,7 @@ import type { NotificationType } from '@ember-data/store/-private/managers/notif import type { Collection } from '@ember-data/store/-private/record-arrays/identifier-array'; import type { CacheCapabilitiesManager } from '@ember-data/store/-types/q/cache-store-wrapper'; import type { JsonApiResource } from '@ember-data/store/-types/q/record-data-json-api'; -import type { RecordInstance } from '@ember-data/store/-types/q/record-instance'; +import type { OpaqueRecordInstance } from '@ember-data/store/-types/q/record-instance'; import type { FieldSchema } from '@ember-data/store/-types/q/schema-service'; import type { StableDocumentIdentifier, @@ -121,7 +121,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { const { owner } = this; const store = owner.lookup('service:store') as unknown as TestStore; - const userDocument = await store.request>({ + const userDocument = await store.request>({ url: '/assets/users/1.json', }); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); @@ -185,7 +185,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ]); store.requestManager.useCache(CacheHandler); - const userDocument = await store.request>({ + const userDocument = await store.request>({ url: '/assets/users/1.json', }); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); @@ -213,7 +213,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { 'we get access to the document meta' ); - const userDocument2 = await store.request>({ + const userDocument2 = await store.request>({ url: '/assets/users/1.json', }); const data2 = userDocument2.content.data; @@ -325,7 +325,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { owner.register('service:request', RequestManagerService); const store = owner.lookup('service:store') as unknown as TestStore; - const userDocument = await store.request>({ + const userDocument = await store.request>({ op: 'random-op', url: '/assets/users/1.json', }); @@ -609,7 +609,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ]); store.requestManager.useCache(CacheHandler); - const userDocument = await store.request>({ + const userDocument = await store.request>({ url: '/assets/users/1.json', }); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); @@ -638,7 +638,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ' we get access to the document meta' ); - const userDocument2 = await store.request>({ + const userDocument2 = await store.request>({ url: '/assets/users/1.json', cacheOptions: { backgroundReload: true }, }); @@ -750,7 +750,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ]); store.requestManager.useCache(CacheHandler); - const userDocument = await store.request>({ + const userDocument = await store.request>({ url: '/assets/users/1.json', }); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); @@ -1029,7 +1029,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ]); store.requestManager.useCache(CacheHandler); - const userDocument = await store.request>({ + const userDocument = await store.request>({ url: '/assets/users/list.json', }); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); @@ -1060,7 +1060,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { 'we get access to the document meta' ); - const userDocument2 = await store.request>({ + const userDocument2 = await store.request>({ url: '/assets/users/list.json', }); const data2 = userDocument2.content.data!; @@ -1143,7 +1143,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ]); store.requestManager.useCache(CacheHandler); - const userDocument = await store.request>({ + const userDocument = await store.request>({ url: '/assets/users/list.json', }); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); @@ -1174,7 +1174,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { 'we get access to the document meta' ); - const userDocument2 = await store.request>({ + const userDocument2 = await store.request>({ url: '/assets/users/list.json', cacheOptions: { backgroundReload: true }, }); @@ -1292,7 +1292,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { store.requestManager.useCache(CacheHandler); // Initial Fetch with Hydration - const userDocument = await store.request>({ + const userDocument = await store.request>({ url: '/assets/users/list.json', }); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); @@ -2001,7 +2001,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { const { owner } = this; const store = owner.lookup('service:store') as unknown as TestStore; - const request = store.request>({ + const request = store.request>({ url: '/assets/users/1.json', }); const userDocument = await request; @@ -2090,7 +2090,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ]); store.requestManager.useCache(CacheHandler); - const userDocument = await store.request>({ + const userDocument = await store.request>({ url: '/assets/users/1.json', }); const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); @@ -2119,7 +2119,7 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { ' we get access to the document meta' ); - const request2 = store.request>({ + const request2 = store.request>({ url: '/assets/users/1.json', cacheOptions: { backgroundReload: true }, }); diff --git a/tests/main/tests/integration/identifiers/lid-reflection-test.ts b/tests/main/tests/integration/identifiers/lid-reflection-test.ts index e63252d462c..5c211c2bd68 100644 --- a/tests/main/tests/integration/identifiers/lid-reflection-test.ts +++ b/tests/main/tests/integration/identifiers/lid-reflection-test.ts @@ -14,20 +14,20 @@ import { recordIdentifierFor } from '@ember-data/store'; module('Integration | Identifiers - lid reflection', function (hooks: NestedHooks) { setupTest(hooks); + class User extends Model { + @attr declare name: string; + @attr declare age: number; + } + hooks.beforeEach(function () { const { owner } = this; - class User extends Model { - @attr declare name: string; - @attr declare age: number; - } - owner.register('model:user', User); }); test(`We can access the lid when serializing a record`, function (assert: Assert) { class TestSerializer extends EmberObject { - serialize(snapshot: Snapshot) { + serialize(snapshot: Snapshot) { // TODO should snapshots have direct access to the identifier? const identifier = recordIdentifierFor(snapshot.record); return { 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 ef2e8fd5dcd..1da8146e40d 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 @@ -19,7 +19,7 @@ module('unit/model - Custom Class Model', function (hooks: NestedHooks) { // these types aren't correct but we don't have a registry to help // make them correct yet save(): Promise { - return this.store.saveRecord(this) as Promise; + return this.store.saveRecord(this); } } diff --git a/tests/recommended-json-api/app/services/store.ts b/tests/recommended-json-api/app/services/store.ts index 8e137da4dae..b25e7b063d3 100644 --- a/tests/recommended-json-api/app/services/store.ts +++ b/tests/recommended-json-api/app/services/store.ts @@ -7,8 +7,10 @@ import Fetch from '@ember-data/request/fetch'; import { LifetimesService } from '@ember-data/request-utils'; import DataStore, { CacheHandler } from '@ember-data/store'; import type { CacheCapabilitiesManager } from '@ember-data/store/-types/q/cache-store-wrapper'; +import type { ModelSchema } from '@ember-data/store/-types/q/ds-model'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { Cache } from '@warp-drive/core-types/cache'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; import CONFIG from '../config/environment'; @@ -39,8 +41,9 @@ export default class Store extends DataStore { return teardownRecord.call(this, record); } - // @ts-expect-error Not sure what the fix is here - override modelFor(type: string) { - return modelFor.call(this, type); + override modelFor(type: TypeFromInstance): ModelSchema; + override modelFor(type: string): ModelSchema; + override modelFor(type: string): ModelSchema { + return modelFor.call(this, type) as ModelSchema; } } diff --git a/tests/warp-drive__schema-record/tests/-utils/reactive-context.ts b/tests/warp-drive__schema-record/tests/-utils/reactive-context.ts index a59c0a625ba..449733a450f 100644 --- a/tests/warp-drive__schema-record/tests/-utils/reactive-context.ts +++ b/tests/warp-drive__schema-record/tests/-utils/reactive-context.ts @@ -4,11 +4,15 @@ import Component from '@glimmer/component'; import { hbs } from 'ember-cli-htmlbars'; -import type { RecordInstance } from '@ember-data/store/-types/q/record-instance'; +import type { OpaqueRecordInstance } from '@ember-data/store/-types/q/record-instance'; import type { FieldSchema } from '@ember-data/store/-types/q/schema-service'; import type { ResourceRelationship } from '@warp-drive/core-types/cache/relationship'; -export async function reactiveContext(this: TestContext, record: T, fields: FieldSchema[]) { +export async function reactiveContext( + this: TestContext, + record: T, + fields: FieldSchema[] +) { const _fields: string[] = []; fields.forEach((field) => { _fields.push(field.name + 'Count'); diff --git a/tests/warp-drive__schema-record/tests/legacy/mode-test.ts b/tests/warp-drive__schema-record/tests/legacy/mode-test.ts index 938acf60053..924332a0971 100644 --- a/tests/warp-drive__schema-record/tests/legacy/mode-test.ts +++ b/tests/warp-drive__schema-record/tests/legacy/mode-test.ts @@ -13,6 +13,7 @@ import { import RequestManager from '@ember-data/request'; import type Store from '@ember-data/store'; import { CacheHandler } from '@ember-data/store'; +import type { ResourceType } from '@warp-drive/core-types/symbols'; import { Editable, Legacy } from '@warp-drive/schema-record/record'; import { registerDerivations, SchemaService, withFields } from '@warp-drive/schema-record/schema'; @@ -45,13 +46,14 @@ interface User { isDestroyed: boolean; errors: Errors; unloadRecord(): void; - _createSnapshot(): Snapshot; + _createSnapshot(): Snapshot; serialize(): Record; save(): Promise; changedAttributes(): Record; rollbackAttributes(): void; reload(): Promise; destroyRecord(): Promise; + [ResourceType]: 'user'; } module('Legacy Mode', function (hooks) { @@ -74,13 +76,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); assert.strictEqual(record.id, '1', 'id is accessible'); assert.strictEqual(record.name, 'Rey Pupatine', 'name is accessible'); @@ -112,13 +114,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); assert.false(record[Legacy], 'record is in legacy mode'); @@ -152,13 +154,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); assert.true(record[Legacy], 'record is in legacy mode'); assert.strictEqual( @@ -187,13 +189,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); assert.false(record[Legacy], 'record is NOT in legacy mode'); assert.strictEqual(record.$type, 'user', '$type is accessible'); @@ -218,13 +220,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); assert.true(record[Legacy], 'record is in legacy mode'); @@ -263,7 +265,7 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', @@ -290,7 +292,7 @@ module('Legacy Mode', function (hooks) { }, }, ], - }) as User; + }); assert.true(record[Legacy], 'record is in legacy mode'); @@ -323,13 +325,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); try { const errors = record.errors; @@ -359,13 +361,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); try { const currentState = record.currentState; @@ -397,13 +399,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); try { record.unloadRecord(); @@ -432,13 +434,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); record.deleteRecord(); assert.true(record.isDeleted, 'state flag is updated'); @@ -462,13 +464,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); const snapshot = record._createSnapshot(); assert.ok(snapshot, 'snapshot is created'); @@ -494,13 +496,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); assert.strictEqual(record.dirtyType, '', 'dirtyType is correct'); assert.strictEqual(record.adapterError, null, 'adapterError is correct'); @@ -532,13 +534,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); assert.false(record.isDestroying, 'isDestroying is correct'); assert.false(record.isDestroyed, 'isDestroyed is correct'); @@ -586,13 +588,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); const serialized = record.serialize(); @@ -653,13 +655,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); assert.strictEqual(record.name, 'Rey Pupatine', 'name is initialized'); @@ -686,13 +688,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); record.name = 'Rey Skybarker'; assert.true(record.hasDirtyAttributes, 'hasDirtyAttributes is correct'); @@ -755,13 +757,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); record.name = 'Rey Skybarker'; assert.true(record.hasDirtyAttributes, 'hasDirtyAttributes is correct'); @@ -820,13 +822,13 @@ module('Legacy Mode', function (hooks) { ]), }); - const record = store.push({ + const record = store.push({ data: { type: 'user', id: '1', attributes: { name: 'Rey Pupatine' }, }, - }) as User; + }); const promise = record.destroyRecord();