diff --git a/guides/typescript/3-typing-models.md b/guides/typescript/3-typing-models.md index db92dbabea0..bab9247ed67 100644 --- a/guides/typescript/3-typing-models.md +++ b/guides/typescript/3-typing-models.md @@ -1,4 +1,4 @@ -# Typing Models +# Typing Models & Transforms ## ResourceType @@ -15,21 +15,58 @@ export default class User extends Model { } ``` +The benefit of the above is that the value of ResourceType is readable at runtime and thus easy to debug. +However, you can also choose to do this via types only: + +```ts +import Model, { attr } from '@ember-data/model'; +import type { ResourceType } from '@warp-drive/core-types/symbols'; + +export default class User extends Model { + @attr declare name: string; + + declare [ResourceType]: 'user'; +} +``` + +EmberData will never access ResourceType as an actual value, these brands are *purely* for type inference. + ## Transforms Transforms with a `TransformName` brand will have their type and options validated. Once we move to stage-3 decorators, the signature of the field would also be validated against the transform. +Example: Typing a Transform + +```ts +import type { TransformName } from '@warp-drive/core-types/symbols'; + +export default class BigIntTransform { + deserialize(serialized: string): BigInt | null { + return !serialized || serialized === '' ? null : BigInt(serialized + 'n'); + } + serialize(deserialized: BigInt | null): string | null { + return !deserialized ? null : String(deserialized); + } + + declare [TransformName]: 'big-int'; + + static create() { + return new this(); + } +} +``` + Example: Using Transforms ```ts import Model, { attr } from '@ember-data/model'; import type { StringTransform } from '@ember-data/serializer/transforms'; -import { ResourceType } from '@warp-drive/core-types/symbols'; +import type { ResourceType } from '@warp-drive/core-types/symbols'; export default class User extends Model { @attr('string') declare name: string; - [ResourceType] = 'user' as const; + declare [ResourceType]: 'user'; } ``` diff --git a/guides/typescript/4-typing-requests.md b/guides/typescript/4-typing-requests.md new file mode 100644 index 00000000000..a17f2430ce4 --- /dev/null +++ b/guides/typescript/4-typing-requests.md @@ -0,0 +1,65 @@ +# Typing Requests & Builders + +## How it works (but what not to do in the general case) + +`requestManager.request` and `store.request` each take a generic that can be used to set +the return type of the content of the associated request. + +```ts +const { content } = await store.request({ ... }); + +// here content will be typed as a User +``` + +> [!CAUTION] +> Note that this puts the burden on you to ensure the return type accurately portrays the result! + +In all cases, the response will be a `StructuredDocument` where `T` is the content type provided. + +This approach allows for a lot of flexibility in designing great sugar overtop of the request infrastructure, but again, limits the amount of safety provided and should be used with great caution. + +A better approach is to use builders and set the generic via inference. + +## Setting Content's Type from a Builder + +The signature for `request` will infer the generic for the content type from a special brand on the options passed to it. + +```ts +type MyRequest { + // ... + [RequestSignature]: Collection +} + +function buildMyRequest(...): MyRequest { /* ... */ } + +const { content } = await store.request( + buildMyRequest(...) +); + +// here content will be set to `Collection` +``` + +## Advanced Builders + +Because builders are just functions that produce a request options object, and because this object can be branded with +the type signature of the response, we can use this to create +advanced more-strongly-typed systems. + +For instance, imagine you had a query builder that validated +and linted the query against a backing schema, such as you might +get with GraphQL + +```ts +const { content } = await store.request( + gql`query withoutVariable { + continents { + code + name + countries { + name + capital + } + } + }` +); +``` diff --git a/guides/typescript/index.md b/guides/typescript/index.md index 3769b20a960..3fe92aa0673 100644 --- a/guides/typescript/index.md +++ b/guides/typescript/index.md @@ -17,10 +17,8 @@ the following two sections - [Using Types Packages](./1-configuration.md#using-types-packages) - Usage - [Why Brands](./2-why-brands.md) - - [Typing Models](./3-typing-models.md) - - Typing Transforms - - Typing Requests - - Typing Builders + - [Typing Models & Transforms](./3-typing-models.md) + - [Typing Requests & Builders](./4-typing-requests.md) - Typing Handlers - Using Store APIs diff --git a/package.json b/package.json index 1a0a840e83b..7bb19179100 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test": "pnpm turbo test --concurrency=1", "test:production": "pnpm turbo test:production --concurrency=1", "test:try-one": "pnpm --filter main-test-app run test:try-one", - "test:docs": "pnpm build:docs && pnpm run -r --workspace-concurrency=-1 --if-present test:docs", + "test:docs": "FORCE_COLOR=2 pnpm build:docs && pnpm run -r --workspace-concurrency=-1 --if-present test:docs", "test:blueprints": "pnpm run -r --workspace-concurrency=-1 --if-present test:blueprints", "test:fastboot": "pnpm run -r --workspace-concurrency=-1 --if-present test:fastboot", "test:embroider": "pnpm run -r ---workspace-concurrency=-1 --if-present test:embroider", diff --git a/packages/active-record/src/-private/builders/find-record.ts b/packages/active-record/src/-private/builders/find-record.ts index 1b8cdde27a7..9a34e7e4613 100644 --- a/packages/active-record/src/-private/builders/find-record.ts +++ b/packages/active-record/src/-private/builders/find-record.ts @@ -6,17 +6,17 @@ import { underscore } from '@ember/string'; import { pluralize } from 'ember-inflector'; import { buildBaseURL, buildQueryParams, type FindRecordUrlOptions } from '@ember-data/request-utils'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; import type { - ConstrainedRequestOptions, + FindRecordOptions, FindRecordRequestOptions, RemotelyAccessibleIdentifier, } from '@warp-drive/core-types/request'; +import type { SingleResourceDataDocument } from '@warp-drive/core-types/spec/document'; import { copyForwardUrlOptions, extractCacheOptions } from './-utils'; -type FindRecordOptions = ConstrainedRequestOptions & { - include?: string | string[]; -}; +export type FindRecordResultDocument = Omit, 'data'> & { data: T }; /** * Builds request options to fetch a single resource by a known id or identifier @@ -76,10 +76,19 @@ type FindRecordOptions = ConstrainedRequestOptions & { * @param identifier * @param options */ +export function findRecord( + identifier: RemotelyAccessibleIdentifier>, + options?: FindRecordOptions +): FindRecordRequestOptions>; export function findRecord( identifier: RemotelyAccessibleIdentifier, options?: FindRecordOptions ): FindRecordRequestOptions; +export function findRecord( + type: TypeFromInstance, + id: string, + options?: FindRecordOptions +): FindRecordRequestOptions>; export function findRecord(type: string, id: string, options?: FindRecordOptions): FindRecordRequestOptions; export function findRecord( arg1: string | RemotelyAccessibleIdentifier, diff --git a/packages/active-record/src/-private/builders/query.ts b/packages/active-record/src/-private/builders/query.ts index 6e7f1d55249..5770a5ab82c 100644 --- a/packages/active-record/src/-private/builders/query.ts +++ b/packages/active-record/src/-private/builders/query.ts @@ -7,7 +7,9 @@ import { pluralize } from 'ember-inflector'; import { buildBaseURL, buildQueryParams, type QueryUrlOptions } from '@ember-data/request-utils'; import type { QueryParamsSource } from '@warp-drive/core-types/params'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; import type { ConstrainedRequestOptions, QueryRequestOptions } from '@warp-drive/core-types/request'; +import type { CollectionResourceDataDocument } from '@warp-drive/core-types/spec/document'; import { copyForwardUrlOptions, extractCacheOptions } from './-utils'; @@ -61,6 +63,18 @@ import { copyForwardUrlOptions, extractCacheOptions } from './-utils'; * @param query * @param options */ +export function query( + type: TypeFromInstance, + // eslint-disable-next-line @typescript-eslint/no-shadow + query?: QueryParamsSource, + options?: ConstrainedRequestOptions +): QueryRequestOptions>; +export function query( + type: string, + // eslint-disable-next-line @typescript-eslint/no-shadow + query?: QueryParamsSource, + options?: ConstrainedRequestOptions +): QueryRequestOptions; export function query( type: string, // eslint-disable-next-line @typescript-eslint/no-shadow diff --git a/packages/active-record/src/-private/builders/save-record.ts b/packages/active-record/src/-private/builders/save-record.ts index 8baeecf5199..4f055c805b4 100644 --- a/packages/active-record/src/-private/builders/save-record.ts +++ b/packages/active-record/src/-private/builders/save-record.ts @@ -76,6 +76,8 @@ function isExisting(identifier: StableRecordIdentifier): identifier is StableExi * @param record * @param options */ +export function deleteRecord(record: T, options?: ConstrainedRequestOptions): DeleteRequestOptions; +export function deleteRecord(record: unknown, options?: ConstrainedRequestOptions): DeleteRequestOptions; export function deleteRecord(record: unknown, options: ConstrainedRequestOptions = {}): DeleteRequestOptions { const identifier = recordIdentifierFor(record); assert(`Expected to be given a record instance`, identifier); @@ -147,6 +149,8 @@ export function deleteRecord(record: unknown, options: ConstrainedRequestOptions * @param record * @param options */ +export function createRecord(record: T, options?: ConstrainedRequestOptions): CreateRequestOptions; +export function createRecord(record: unknown, options?: ConstrainedRequestOptions): CreateRequestOptions; export function createRecord(record: unknown, options: ConstrainedRequestOptions = {}): CreateRequestOptions { const identifier = recordIdentifierFor(record); assert(`Expected to be given a record instance`, identifier); @@ -220,6 +224,14 @@ export function createRecord(record: unknown, options: ConstrainedRequestOptions * @param record * @param options */ +export function updateRecord( + record: T, + options?: ConstrainedRequestOptions & { patch?: boolean } +): UpdateRequestOptions; +export function updateRecord( + record: unknown, + options?: ConstrainedRequestOptions & { patch?: boolean } +): UpdateRequestOptions; export function updateRecord( record: unknown, options: ConstrainedRequestOptions & { patch?: boolean } = {} diff --git a/packages/core-types/src/record.ts b/packages/core-types/src/record.ts index 18eeb5a2396..f52d4304242 100644 --- a/packages/core-types/src/record.ts +++ b/packages/core-types/src/record.ts @@ -49,3 +49,93 @@ export type TypeFromInstance = T extends TypedRecordInstance ? T[typeof Resou * @typedoc */ export type TypeFromInstanceOrString = T extends TypedRecordInstance ? T[typeof ResourceType] : string; + +type Unpacked = T extends (infer U)[] ? U : T; +type NONE = { __NONE: never }; + +type __InternalExtract< + T extends TypedRecordInstance, + V extends TypedRecordInstance, + IncludePrefix extends boolean, + Ignore, + Pre extends string, +> = + // if we extend T, we return the leaf value + V extends T + ? IncludePrefix extends false + ? V[typeof ResourceType] + : Pre + : // else if we are in Ignore we add the lead and exit + V extends Ignore + ? IncludePrefix extends false + ? V[typeof ResourceType] + : Pre + : // else add T to Ignore and recurse + ExtractUnion; + +type __ExtractIfRecord< + T extends TypedRecordInstance, + V, + IncludePrefix extends boolean, + Ignore, + Pre extends string, +> = V extends TypedRecordInstance ? __InternalExtract : never; + +type _ExtractUnion = { + // for each string key in the record, + [K in keyof T]: K extends string + ? // we recursively extract any values that resolve to a TypedRecordInstance + __ExtractIfRecord>, IncludePrefix, Ignore, Pre extends string ? `${Pre}.${K}` : K> + : never; + // then we return any value that is not 'never' +}[keyof T]; + +/** + * A Utility that extracts either resource types or resource paths from a TypedRecordInstance. + * + * Its limitations are mostly around its intentional non-recursiveness. It presumes that APIs which + * implement includes will not allow cyclical include paths, and will collapse includes by type. + * + * This follows closer to the JSON:API fields spec than to the includes spec in nature, but in + * practice it is so impracticle for an API to allow z-algo include paths that this is probably + * reasonable. + * + * We may need to revisit this in the future, opting to either make this restriction optional or + * to allow for other strategies. + * + * There's a 90% chance this particular implementation belongs being in the JSON:API package instead + * of core-types, but it's here for now. + * + * @typedoc + */ +type ExtractUnion< + T extends TypedRecordInstance, + IncludePrefix extends boolean = false, + Ignore = NONE, + Pre = NONE, +> = Exclude< + IncludePrefix extends true + ? // if we want to include prefix, we union with the prefix. Outer Exclude will filter any "NONE" types + _ExtractUnion | Pre + : // Else we just union the types. + _ExtractUnion | T[typeof ResourceType], + NONE +>; + +/** + * A utility that provides the union of all ResourceName for all potential + * includes for the given TypedRecordInstance. + * + * @typedoc + */ +export type ExtractSuggestedCacheTypes = ExtractUnion; // ToPaths, false>; + +/** + * A utility that provides the union type of all valid include paths for the given + * TypedRecordInstance. + * + * Cyclical paths are filtered out. + * + * @typedoc + */ +export type Includes = ExtractUnion; // ToPaths>; diff --git a/packages/core-types/src/record.type-test.ts b/packages/core-types/src/record.type-test.ts new file mode 100644 index 00000000000..906296a7eeb --- /dev/null +++ b/packages/core-types/src/record.type-test.ts @@ -0,0 +1,85 @@ +// test + +import type { ExtractSuggestedCacheTypes, Includes, TypedRecordInstance } from './record'; +import type { ResourceType } from './symbols'; + +type NoSelfReference = { + name: string; + related: MyThing; + [ResourceType]: 'no-self-reference'; +}; + +type MyThing = { + name: string; + relatedThing: MyThing; + relatedThings: MyThing[]; + otherThing: OtherThing; + otherThings: OtherThing[]; + [ResourceType]: 'thing'; +}; + +type OtherThing = { + name: string; + thirdThing: OtherThing; + deals: OtherThing[]; + original: MyThing; + deep: DeepThing; + [ResourceType]: 'other-thing'; +}; + +type DeepThing = { + name: string; + relatedThing: MyThing; + otherThing: OtherThing; + myThing: DeepThing; + [ResourceType]: 'deep-thing'; +}; + +function takesSuggestTypes(types: ExtractSuggestedCacheTypes[]) {} +takesSuggestTypes([ + 'thing', + 'other-thing', + 'deep-thing', + // @ts-expect-error not a valid type + 'not-a-thing', +]); + +takesSuggestTypes([ + // we should include our own type even when not self-referential + 'no-self-reference', + 'thing', + 'other-thing', + 'deep-thing', + // @ts-expect-error not a valid type + 'not-a-thing', +]); + +function takesIncludes(includes: Includes[]) {} +takesIncludes([ + // @ts-expect-error not a valid path since it doesn't exist + 'not', + 'relatedThing', + 'relatedThings', + 'otherThing', + 'otherThings', + // @ts-expect-error not a valid path since its an attribute + 'name', + 'otherThing.thirdThing', + 'otherThing.deals', + 'otherThing.original', + // @ts-expect-error should not include this since original was already processed above + 'otherThing.original.relatedThing', + 'otherThing.deep', + 'otherThing.deep.relatedThing', + 'otherThing.deep.otherThing', + 'otherThing.deep.myThing', + 'otherThings.thirdThing', + 'otherThings.deals', + 'otherThings.original', + 'otherThings.deep', + 'otherThings.deep.relatedThing', + // @ts-expect-error should not include this since original was already processed above + 'otherThings.deep.relatedThing.relatedThing', + 'otherThings.deep.otherThing', + 'otherThings.deep.myThing', +]); diff --git a/packages/core-types/src/request.ts b/packages/core-types/src/request.ts index 3786b3769fa..a0f79b0ef61 100644 --- a/packages/core-types/src/request.ts +++ b/packages/core-types/src/request.ts @@ -1,6 +1,8 @@ import type { StableRecordIdentifier } from './identifier'; import type { QueryParamsSerializationOptions } from './params'; +import type { ExtractSuggestedCacheTypes, Includes, TypedRecordInstance, TypeFromInstanceOrString } from './record'; import type { ResourceIdentifierObject } from './spec/raw'; +import type { RequestSignature } from './symbols'; type Store = unknown; @@ -16,7 +18,7 @@ export type HTTPMethod = 'GET' | 'OPTIONS' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' * * @typedoc */ -export type CacheOptions = { +export type CacheOptions = { /** * A key that uniquely identifies this request. If not present, the url wil be used * as the key for any GET request, while all other requests will not be cached. @@ -54,7 +56,7 @@ export type CacheOptions = { * * @typedoc */ - types?: string[]; + types?: T extends TypedRecordInstance ? ExtractSuggestedCacheTypes[] : string[]; /** * If true, the request will never be handled by the cache-manager and thus @@ -67,41 +69,45 @@ export type CacheOptions = { */ [SkipCache]?: true; }; -export type FindRecordRequestOptions = { +export type FindRecordRequestOptions = { url: string; method: 'GET'; headers: Headers; - cacheOptions: CacheOptions; + cacheOptions: CacheOptions; op: 'findRecord'; - records: [ResourceIdentifierObject]; + records: [ResourceIdentifierObject>]; + [RequestSignature]?: RT; }; -export type QueryRequestOptions = { +export type QueryRequestOptions = { url: string; method: 'GET'; headers: Headers; - cacheOptions: CacheOptions; + cacheOptions: CacheOptions; op: 'query'; + [RequestSignature]?: RT; }; -export type PostQueryRequestOptions = { +export type PostQueryRequestOptions = { url: string; method: 'POST' | 'QUERY'; headers: Headers; body: string; - cacheOptions: CacheOptions & { key: string }; + cacheOptions: CacheOptions & { key: string }; op: 'query'; + [RequestSignature]?: RT; }; -export type DeleteRequestOptions = { +export type DeleteRequestOptions = { url: string; method: 'DELETE'; headers: Headers; op: 'deleteRecord'; data: { - record: StableRecordIdentifier; + record: StableRecordIdentifier>; }; - records: [ResourceIdentifierObject]; + records: [ResourceIdentifierObject>]; + [RequestSignature]?: RT; }; type ImmutableRequest = Readonly & { @@ -109,35 +115,37 @@ type ImmutableRequest = Readonly & { readonly records: [StableRecordIdentifier]; }; -export type UpdateRequestOptions = { +export type UpdateRequestOptions = { url: string; method: 'PATCH' | 'PUT'; headers: Headers; op: 'updateRecord'; data: { - record: StableRecordIdentifier; + record: StableRecordIdentifier>; }; - records: [ResourceIdentifierObject]; + records: [ResourceIdentifierObject>]; + [RequestSignature]?: RT; }; -export type CreateRequestOptions = { +export type CreateRequestOptions = { url: string; method: 'POST'; headers: Headers; op: 'createRecord'; data: { - record: StableRecordIdentifier; + record: StableRecordIdentifier>; }; - records: [ResourceIdentifierObject]; + records: [ResourceIdentifierObject>]; + [RequestSignature]?: RT; }; export type ImmutableDeleteRequestOptions = ImmutableRequest; export type ImmutableUpdateRequestOptions = ImmutableRequest; export type ImmutableCreateRequestOptions = ImmutableRequest; -export type RemotelyAccessibleIdentifier = { +export type RemotelyAccessibleIdentifier = { id: string; - type: string; + type: T; lid?: string; }; @@ -150,8 +158,8 @@ export type ConstrainedRequestOptions = { urlParamsSettings?: QueryParamsSerializationOptions; }; -export type FindRecordOptions = ConstrainedRequestOptions & { - include?: string | string[]; +export type FindRecordOptions = ConstrainedRequestOptions & { + include?: T extends TypedRecordInstance ? Includes[] : string | string[]; }; export interface StructuredDataDocument { @@ -247,7 +255,7 @@ export type ImmutableHeaders = Headers & { clone?(): Headers; toJSON(): [string, * * @typedoc */ -export type RequestInfo = Request & { +export type RequestInfo = Request & { /** * If provided, used instead of the AbortController auto-configured for each request by the RequestManager * @@ -259,7 +267,7 @@ export type RequestInfo = Request & { * @see {@link CacheOptions} * @typedoc */ - cacheOptions?: CacheOptions; + cacheOptions?: CacheOptions; store?: Store; op?: string; @@ -298,7 +306,7 @@ export type RequestInfo = Request & { * * @typedoc */ -export type ImmutableRequestInfo = Readonly> & { +export type ImmutableRequestInfo = Readonly> & { readonly cacheOptions?: Readonly; readonly headers?: ImmutableHeaders; readonly data?: Readonly>; @@ -308,6 +316,7 @@ export type ImmutableRequestInfo = Readonly> & { * @typedoc */ readonly bodyUsed?: boolean; + [RequestSignature]?: T; }; export interface ResponseInfo { diff --git a/packages/core-types/src/symbols.ts b/packages/core-types/src/symbols.ts index 3738faff4f2..ca9a776b2cc 100644 --- a/packages/core-types/src/symbols.ts +++ b/packages/core-types/src/symbols.ts @@ -37,3 +37,12 @@ export const ResourceType = Symbol('$type'); * @typedoc */ export const TransformName = Symbol('$TransformName'); + +/** + * Symbol for use by builders to indicate the return type + * generic to use for store.request() + * + * @type {Symbol} + * @typedoc + */ +export const RequestSignature = Symbol('RequestSignature'); diff --git a/packages/ember/src/-private/request.gts b/packages/ember/src/-private/request.gts index 9e421ae7a4b..ff9d8af52af 100644 --- a/packages/ember/src/-private/request.gts +++ b/packages/ember/src/-private/request.gts @@ -22,7 +22,7 @@ if (macroCondition(moduleExists('ember-provide-consume-context'))) { interface RequestSignature { Args: { request?: Future; - query?: StoreRequestInput; + query?: StoreRequestInput; store?: Store; }; Blocks: { @@ -51,7 +51,7 @@ export class Request extends Component> { return request; } assert(`You must provide either @request or an @query arg with the component`, query); - return this.store.request(query); + return this.store.request(query!); } get store(): Store { diff --git a/packages/json-api/package.json b/packages/json-api/package.json index ad9d9d4d33c..9e20463b170 100644 --- a/packages/json-api/package.json +++ b/packages/json-api/package.json @@ -116,6 +116,7 @@ "@warp-drive/internal-config": "workspace:5.4.0-alpha.54", "ember-inflector": "^4.0.2", "ember-source": "~5.7.0", + "expect-type": "^0.19.0", "pnpm-sync-dependencies-meta-injected": "0.0.10", "rollup": "^4.14.1", "typescript": "^5.4.5", diff --git a/packages/json-api/src/-private/builders/find-record.ts b/packages/json-api/src/-private/builders/find-record.ts index 6f8c075c242..bc41237d163 100644 --- a/packages/json-api/src/-private/builders/find-record.ts +++ b/packages/json-api/src/-private/builders/find-record.ts @@ -4,11 +4,13 @@ import { pluralize } from 'ember-inflector'; import { buildBaseURL, buildQueryParams, type FindRecordUrlOptions } from '@ember-data/request-utils'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; import type { FindRecordOptions, FindRecordRequestOptions, RemotelyAccessibleIdentifier, } from '@warp-drive/core-types/request'; +import type { SingleResourceDataDocument } from '@warp-drive/core-types/spec/document'; import { ACCEPT_HEADER_VALUE, copyForwardUrlOptions, extractCacheOptions } from './-utils'; @@ -70,10 +72,22 @@ import { ACCEPT_HEADER_VALUE, copyForwardUrlOptions, extractCacheOptions } from * @param identifier * @param options */ + +export type FindRecordResultDocument = Omit, 'data'> & { data: T }; + +export function findRecord( + identifier: RemotelyAccessibleIdentifier>, + options?: FindRecordOptions +): FindRecordRequestOptions>; export function findRecord( identifier: RemotelyAccessibleIdentifier, options?: FindRecordOptions ): FindRecordRequestOptions; +export function findRecord( + type: TypeFromInstance, + id: string, + options?: FindRecordOptions +): FindRecordRequestOptions>; export function findRecord(type: string, id: string, options?: FindRecordOptions): FindRecordRequestOptions; export function findRecord( arg1: string | RemotelyAccessibleIdentifier, diff --git a/packages/json-api/src/-private/builders/find-record.type-test.ts b/packages/json-api/src/-private/builders/find-record.type-test.ts new file mode 100644 index 00000000000..878fb1d33b8 --- /dev/null +++ b/packages/json-api/src/-private/builders/find-record.type-test.ts @@ -0,0 +1,61 @@ +import { expectTypeOf } from 'expect-type'; + +import Store from '@ember-data/store'; +import { RequestSignature, type ResourceType } from '@warp-drive/core-types/symbols'; + +import type { FindRecordResultDocument } from './find-record'; +import { findRecord } from './find-record'; + +type MyThing = { + name: string; + relatedThing: MyThing; + relatedThings: MyThing[]; + otherThing: OtherThing; + otherThings: OtherThing[]; + [ResourceType]: 'thing'; +}; + +type OtherThing = { + name: string; + thirdThing: OtherThing; + deals: OtherThing[]; + original: MyThing; + deep: DeepThing; + [ResourceType]: 'other-thing'; +}; + +type DeepThing = { + name: string; + relatedThing: MyThing; + otherThing: OtherThing; + myThing: DeepThing; + [ResourceType]: 'deep-thing'; +}; + +const store = new Store(); +const query = findRecord('thing', '1'); + +expectTypeOf>(query[RequestSignature]!); + +const result = await store.request(findRecord('thing', '1')); +const result2 = await store.request( + findRecord('thing', '1', { + include: [ + // @ts-expect-error name is an attribute, not a relationship + 'name', + 'relatedThing', + // @ts-expect-error relatedThings does not have thirdThing + 'relatedThing.thirdThing', + 'relatedThings', + 'otherThing', + 'otherThing.thirdThing', + 'otherThings', + 'otherThings.deep.myThing', + // @ts-expect-error cyclic relationships are not allowed in includes + 'relatedThing.relatedThing', + ], + }) +); + +expectTypeOf(result.content); +expectTypeOf(result2.content.data); diff --git a/packages/json-api/src/-private/builders/query.ts b/packages/json-api/src/-private/builders/query.ts index bb5b0569675..93fdc177b48 100644 --- a/packages/json-api/src/-private/builders/query.ts +++ b/packages/json-api/src/-private/builders/query.ts @@ -5,12 +5,14 @@ import { pluralize } from 'ember-inflector'; import { buildBaseURL, buildQueryParams, type QueryUrlOptions } from '@ember-data/request-utils'; import type { QueryParamsSource } from '@warp-drive/core-types/params'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; import type { CacheOptions, ConstrainedRequestOptions, PostQueryRequestOptions, QueryRequestOptions, } from '@warp-drive/core-types/request'; +import type { CollectionResourceDataDocument } from '@warp-drive/core-types/spec/document'; import { ACCEPT_HEADER_VALUE, copyForwardUrlOptions, extractCacheOptions } from './-utils'; @@ -68,6 +70,18 @@ import { ACCEPT_HEADER_VALUE, copyForwardUrlOptions, extractCacheOptions } from * @param query * @param options */ +export function query( + type: TypeFromInstance, + // eslint-disable-next-line @typescript-eslint/no-shadow + query?: QueryParamsSource, + options?: ConstrainedRequestOptions +): QueryRequestOptions>; +export function query( + type: string, + // eslint-disable-next-line @typescript-eslint/no-shadow + query?: QueryParamsSource, + options?: ConstrainedRequestOptions +): QueryRequestOptions; export function query( type: string, // eslint-disable-next-line @typescript-eslint/no-shadow @@ -144,6 +158,18 @@ export function query( * @param query * @param options */ +export function postQuery( + type: TypeFromInstance, + // eslint-disable-next-line @typescript-eslint/no-shadow + query?: QueryParamsSource, + options?: ConstrainedRequestOptions +): PostQueryRequestOptions>; +export function postQuery( + type: string, + // eslint-disable-next-line @typescript-eslint/no-shadow + query?: QueryParamsSource, + options?: ConstrainedRequestOptions +): PostQueryRequestOptions; export function postQuery( type: string, // eslint-disable-next-line @typescript-eslint/no-shadow diff --git a/packages/json-api/src/-private/builders/save-record.ts b/packages/json-api/src/-private/builders/save-record.ts index 2d849702eab..45d6d630526 100644 --- a/packages/json-api/src/-private/builders/save-record.ts +++ b/packages/json-api/src/-private/builders/save-record.ts @@ -75,6 +75,8 @@ function isExisting(identifier: StableRecordIdentifier): identifier is StableExi * @param record * @param options */ +export function deleteRecord(record: T, options?: ConstrainedRequestOptions): DeleteRequestOptions; +export function deleteRecord(record: unknown, options?: ConstrainedRequestOptions): DeleteRequestOptions; export function deleteRecord(record: unknown, options: ConstrainedRequestOptions = {}): DeleteRequestOptions { const identifier = recordIdentifierFor(record); assert(`Expected to be given a record instance`, identifier); @@ -146,6 +148,8 @@ export function deleteRecord(record: unknown, options: ConstrainedRequestOptions * @param record * @param options */ +export function createRecord(record: T, options?: ConstrainedRequestOptions): CreateRequestOptions; +export function createRecord(record: unknown, options?: ConstrainedRequestOptions): CreateRequestOptions; export function createRecord(record: unknown, options: ConstrainedRequestOptions = {}): CreateRequestOptions { const identifier = recordIdentifierFor(record); assert(`Expected to be given a record instance`, identifier); @@ -219,6 +223,14 @@ export function createRecord(record: unknown, options: ConstrainedRequestOptions * @param record * @param options */ +export function updateRecord( + record: T, + options?: ConstrainedRequestOptions & { patch?: boolean } +): UpdateRequestOptions; +export function updateRecord( + record: unknown, + options?: ConstrainedRequestOptions & { patch?: boolean } +): UpdateRequestOptions; export function updateRecord( record: unknown, options: ConstrainedRequestOptions & { patch?: boolean } = {} diff --git a/packages/legacy-compat/src/builders/find-all.ts b/packages/legacy-compat/src/builders/find-all.ts index 39045c07a28..9b08f5fca89 100644 --- a/packages/legacy-compat/src/builders/find-all.ts +++ b/packages/legacy-compat/src/builders/find-all.ts @@ -7,15 +7,17 @@ import type { StoreRequestInput } from '@ember-data/store'; import type { FindAllOptions } from '@ember-data/store/-types/q/store'; import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; import { SkipCache } from '@warp-drive/core-types/request'; +import type { RequestSignature } from '@warp-drive/core-types/symbols'; import { normalizeModelName } from './utils'; -type FindAllRequestInput = StoreRequestInput & { +type FindAllRequestInput = StoreRequestInput & { op: 'findAll'; data: { type: T; options: FindAllBuilderOptions; }; + [RequestSignature]?: RT; }; type FindAllBuilderOptions = FindAllOptions; @@ -42,9 +44,9 @@ type FindAllBuilderOptions = FindAllOptions; export function findAllBuilder( type: TypeFromInstance, options?: FindAllBuilderOptions -): FindAllRequestInput>; -export function findAllBuilder(type: string, options?: FindAllBuilderOptions): FindAllRequestInput; -export function findAllBuilder(type: string, options: FindAllBuilderOptions = {}): FindAllRequestInput { +): FindAllRequestInput, T[]>; +export function findAllBuilder(type: string, options?: FindAllBuilderOptions): FindAllRequestInput; +export function findAllBuilder(type: string, options: FindAllBuilderOptions = {}): FindAllRequestInput { assert(`You need to pass a model name to the findAll builder`, type); assert( `Model name passed to the findAll builder must be a dasherized string instead of ${type}`, diff --git a/packages/legacy-compat/src/builders/find-record.ts b/packages/legacy-compat/src/builders/find-record.ts index d1b1514eb99..2328df21b87 100644 --- a/packages/legacy-compat/src/builders/find-record.ts +++ b/packages/legacy-compat/src/builders/find-record.ts @@ -9,15 +9,17 @@ import type { BaseFinderOptions, FindRecordOptions } from '@ember-data/store/-ty import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; import { SkipCache } from '@warp-drive/core-types/request'; import type { ResourceIdentifierObject } from '@warp-drive/core-types/spec/raw'; +import type { RequestSignature } from '@warp-drive/core-types/symbols'; import { isMaybeIdentifier, normalizeModelName } from './utils'; -type FindRecordRequestInput = StoreRequestInput & { +type FindRecordRequestInput = StoreRequestInput & { op: 'findRecord'; data: { record: ResourceIdentifierObject; options: FindRecordBuilderOptions; }; + [RequestSignature]?: RT; }; type FindRecordBuilderOptions = Omit; @@ -63,25 +65,25 @@ export function findRecordBuilder( resource: TypeFromInstance, id: string, options?: FindRecordBuilderOptions -): FindRecordRequestInput>; +): FindRecordRequestInput, T>; export function findRecordBuilder( resource: string, id: string, options?: FindRecordBuilderOptions -): FindRecordRequestInput; +): FindRecordRequestInput; export function findRecordBuilder( resource: ResourceIdentifierObject>, options?: FindRecordBuilderOptions -): FindRecordRequestInput>; +): FindRecordRequestInput, T>; export function findRecordBuilder( resource: ResourceIdentifierObject, options?: FindRecordBuilderOptions -): FindRecordRequestInput; +): FindRecordRequestInput; export function findRecordBuilder( resource: string | ResourceIdentifierObject, idOrOptions?: string | FindRecordBuilderOptions, options?: FindRecordBuilderOptions -): FindRecordRequestInput { +): FindRecordRequestInput { assert( `You need to pass a modelName or resource identifier as the first argument to the findRecord builder`, resource diff --git a/packages/legacy-compat/src/builders/query.ts b/packages/legacy-compat/src/builders/query.ts index 25794265e3a..22fbb0c77fc 100644 --- a/packages/legacy-compat/src/builders/query.ts +++ b/packages/legacy-compat/src/builders/query.ts @@ -7,16 +7,18 @@ import type { StoreRequestInput } from '@ember-data/store'; import type { QueryOptions } from '@ember-data/store/-types/q/store'; import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; import { SkipCache } from '@warp-drive/core-types/request'; +import type { RequestSignature } from '@warp-drive/core-types/symbols'; import { normalizeModelName } from './utils'; -type QueryRequestInput = StoreRequestInput & { +type QueryRequestInput = StoreRequestInput & { op: 'query'; data: { type: T; query: Record; options: QueryBuilderOptions; }; + [RequestSignature]?: RT; }; type QueryBuilderOptions = QueryOptions; @@ -44,17 +46,17 @@ export function queryBuilder( type: TypeFromInstance, query: Record, options?: QueryBuilderOptions -): QueryRequestInput>; +): QueryRequestInput, T[]>; export function queryBuilder( type: string, query: Record, options?: QueryBuilderOptions -): QueryRequestInput; +): QueryRequestInput; export function queryBuilder( type: string, query: Record, options: QueryBuilderOptions = {} -): QueryRequestInput { +): QueryRequestInput { assert(`You need to pass a model name to the query builder`, type); assert(`You need to pass a query hash to the query builder`, query); assert( @@ -73,13 +75,14 @@ export function queryBuilder( }; } -type QueryRecordRequestInput = StoreRequestInput & { +type QueryRecordRequestInput = StoreRequestInput & { op: 'queryRecord'; data: { type: T; query: Record; options: QueryBuilderOptions; }; + [RequestSignature]?: RT; }; /** @@ -105,17 +108,17 @@ export function queryRecordBuilder( type: TypeFromInstance, query: Record, options?: QueryBuilderOptions -): QueryRecordRequestInput>; +): QueryRecordRequestInput, T | null>; export function queryRecordBuilder( type: string, query: Record, options?: QueryBuilderOptions -): QueryRecordRequestInput; +): QueryRecordRequestInput; export function queryRecordBuilder( type: string, query: Record, options?: QueryBuilderOptions -): QueryRecordRequestInput { +): QueryRecordRequestInput { assert(`You need to pass a model name to the queryRecord builder`, type); assert(`You need to pass a query hash to the queryRecord builder`, query); assert( diff --git a/packages/legacy-compat/src/builders/save-record.ts b/packages/legacy-compat/src/builders/save-record.ts index 28850bc5e70..3a759037f23 100644 --- a/packages/legacy-compat/src/builders/save-record.ts +++ b/packages/legacy-compat/src/builders/save-record.ts @@ -9,14 +9,16 @@ import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { Cache } from '@warp-drive/core-types/cache'; import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; import { SkipCache } from '@warp-drive/core-types/request'; +import type { RequestSignature } from '@warp-drive/core-types/symbols'; -type SaveRecordRequestInput = StoreRequestInput & { +type SaveRecordRequestInput = StoreRequestInput & { op: 'createRecord' | 'deleteRecord' | 'updateRecord'; data: { record: StableRecordIdentifier; options: SaveRecordBuilderOptions; }; records: [StableRecordIdentifier]; + [RequestSignature]?: RT; }; type SaveRecordBuilderOptions = Record; @@ -51,10 +53,10 @@ function resourceIsFullyDeleted(instanceCache: InstanceCache, identifier: Stable export function saveRecordBuilder( record: T, options: Record = {} -): SaveRecordRequestInput> { +): SaveRecordRequestInput, T> { const store = storeFor(record); assert(`Unable to initiate save for a record in a disconnected state`, store); - const identifier = recordIdentifierFor(record); + const identifier = recordIdentifierFor(record); if (!identifier) { // this commonly means we're disconnected diff --git a/packages/rest/src/-private/builders/find-record.ts b/packages/rest/src/-private/builders/find-record.ts index 8a9b02a690e..c82c8820270 100644 --- a/packages/rest/src/-private/builders/find-record.ts +++ b/packages/rest/src/-private/builders/find-record.ts @@ -6,18 +6,16 @@ import { camelize } from '@ember/string'; import { pluralize } from 'ember-inflector'; import { buildBaseURL, buildQueryParams, type FindRecordUrlOptions } from '@ember-data/request-utils'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; import type { - ConstrainedRequestOptions, + FindRecordOptions, FindRecordRequestOptions, RemotelyAccessibleIdentifier, } from '@warp-drive/core-types/request'; +import type { SingleResourceDataDocument } from '@warp-drive/core-types/spec/document'; import { copyForwardUrlOptions, extractCacheOptions } from './-utils'; -type FindRecordOptions = ConstrainedRequestOptions & { - include?: string | string[]; -}; - /** * Builds request options to fetch a single resource by a known id or identifier * configured for the url and header expectations of most REST APIs. @@ -76,17 +74,29 @@ type FindRecordOptions = ConstrainedRequestOptions & { * @param identifier * @param options */ +export type FindRecordResultDocument = Omit, 'data'> & { data: T }; + +export function findRecord( + identifier: RemotelyAccessibleIdentifier>, + options?: FindRecordOptions +): FindRecordRequestOptions>; export function findRecord( identifier: RemotelyAccessibleIdentifier, options?: FindRecordOptions ): FindRecordRequestOptions; +export function findRecord( + type: TypeFromInstance, + id: string, + options?: FindRecordOptions +): FindRecordRequestOptions>; export function findRecord(type: string, id: string, options?: FindRecordOptions): FindRecordRequestOptions; -export function findRecord( - arg1: string | RemotelyAccessibleIdentifier, +export function findRecord( + arg1: TypeFromInstance | RemotelyAccessibleIdentifier>, arg2: string | FindRecordOptions | undefined, arg3?: FindRecordOptions -): FindRecordRequestOptions { - const identifier: RemotelyAccessibleIdentifier = typeof arg1 === 'string' ? { type: arg1, id: arg2 as string } : arg1; +): FindRecordRequestOptions> { + const identifier: RemotelyAccessibleIdentifier> = + typeof arg1 === 'string' ? { type: arg1, id: arg2 as string } : arg1; const options: FindRecordOptions = (typeof arg1 === 'string' ? arg3 : (arg2 as FindRecordOptions)) || {}; const cacheOptions = extractCacheOptions(options); const urlOptions: FindRecordUrlOptions = { diff --git a/packages/rest/src/-private/builders/query.ts b/packages/rest/src/-private/builders/query.ts index 75076025276..508cc807eeb 100644 --- a/packages/rest/src/-private/builders/query.ts +++ b/packages/rest/src/-private/builders/query.ts @@ -7,7 +7,9 @@ import { pluralize } from 'ember-inflector'; import { buildBaseURL, buildQueryParams, type QueryUrlOptions } from '@ember-data/request-utils'; import type { QueryParamsSource } from '@warp-drive/core-types/params'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; import type { ConstrainedRequestOptions, QueryRequestOptions } from '@warp-drive/core-types/request'; +import type { CollectionResourceDataDocument } from '@warp-drive/core-types/spec/document'; import { copyForwardUrlOptions, extractCacheOptions } from './-utils'; @@ -61,6 +63,18 @@ import { copyForwardUrlOptions, extractCacheOptions } from './-utils'; * @param query * @param options */ +export function query( + type: TypeFromInstance, + // eslint-disable-next-line @typescript-eslint/no-shadow + query?: QueryParamsSource, + options?: ConstrainedRequestOptions +): QueryRequestOptions>; +export function query( + type: string, + // eslint-disable-next-line @typescript-eslint/no-shadow + query?: QueryParamsSource, + options?: ConstrainedRequestOptions +): QueryRequestOptions; export function query( type: string, // eslint-disable-next-line @typescript-eslint/no-shadow diff --git a/packages/rest/src/-private/builders/save-record.ts b/packages/rest/src/-private/builders/save-record.ts index 4fb075a903b..f5f66bd59e5 100644 --- a/packages/rest/src/-private/builders/save-record.ts +++ b/packages/rest/src/-private/builders/save-record.ts @@ -76,6 +76,8 @@ function isExisting(identifier: StableRecordIdentifier): identifier is StableExi * @param record * @param options */ +export function deleteRecord(record: T, options?: ConstrainedRequestOptions): DeleteRequestOptions; +export function deleteRecord(record: unknown, options?: ConstrainedRequestOptions): DeleteRequestOptions; export function deleteRecord(record: unknown, options: ConstrainedRequestOptions = {}): DeleteRequestOptions { const identifier = recordIdentifierFor(record); assert(`Expected to be given a record instance`, identifier); @@ -147,6 +149,8 @@ export function deleteRecord(record: unknown, options: ConstrainedRequestOptions * @param record * @param options */ +export function createRecord(record: T, options?: ConstrainedRequestOptions): CreateRequestOptions; +export function createRecord(record: unknown, options?: ConstrainedRequestOptions): CreateRequestOptions; export function createRecord(record: unknown, options: ConstrainedRequestOptions = {}): CreateRequestOptions { const identifier = recordIdentifierFor(record); assert(`Expected to be given a record instance`, identifier); @@ -220,6 +224,14 @@ export function createRecord(record: unknown, options: ConstrainedRequestOptions * @param record * @param options */ +export function updateRecord( + record: T, + options?: ConstrainedRequestOptions & { patch?: boolean } +): UpdateRequestOptions; +export function updateRecord( + record: unknown, + options?: ConstrainedRequestOptions & { patch?: boolean } +): UpdateRequestOptions; export function updateRecord( record: unknown, options: ConstrainedRequestOptions & { patch?: boolean } = {} diff --git a/packages/schema-record/src/record.ts b/packages/schema-record/src/record.ts index f9bd008bfe4..a58678e0584 100644 --- a/packages/schema-record/src/record.ts +++ b/packages/schema-record/src/record.ts @@ -224,7 +224,7 @@ class ResourceRelationship { this[Parent] = parent; } - fetch(options?: StoreRequestInput): Future { + fetch(options?: StoreRequestInput): Future { const url = options?.url ?? getHref(this.links.related) ?? getHref(this.links.self) ?? null; if (!url) { diff --git a/packages/store/src/-private/cache-handler.ts b/packages/store/src/-private/cache-handler.ts index 1f6406d3f27..60a930d9b62 100644 --- a/packages/store/src/-private/cache-handler.ts +++ b/packages/store/src/-private/cache-handler.ts @@ -115,12 +115,12 @@ export interface LifetimesService { ): void; } -export type LooseStoreRequestInfo = Omit & { +export type LooseStoreRequestInfo = Omit, 'records' | 'headers'> & { records?: ResourceIdentifierObject[]; headers?: Headers; }; -export type StoreRequestInput = ImmutableRequestInfo | LooseStoreRequestInfo; +export type StoreRequestInput = ImmutableRequestInfo | LooseStoreRequestInfo; export interface StoreRequestContext extends RequestContext { request: ImmutableRequestInfo & { store: Store; [EnableHydration]?: boolean }; diff --git a/packages/store/src/-private/store-service.ts b/packages/store/src/-private/store-service.ts index 07155cfa5da..ab04530ef3e 100644 --- a/packages/store/src/-private/store-service.ts +++ b/packages/store/src/-private/store-service.ts @@ -416,7 +416,7 @@ class Store extends EmberObject { * @return {Future} * @public */ - request(requestConfig: StoreRequestInput): Future { + request(requestConfig: StoreRequestInput): Future { // we lazily set the cache handler when we issue the first request // because constructor doesn't allow for this to run after // the user has had the chance to set the prop. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ce3dcac4a8..40d7c6da2e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1131,6 +1131,9 @@ importers: ember-source: specifier: ~5.7.0 version: 5.7.0(@babel/core@7.24.4)(@glimmer/component@1.1.2)(@glint/template@1.4.0)(webpack@5.91.0) + expect-type: + specifier: ^0.19.0 + version: 0.19.0 pnpm-sync-dependencies-meta-injected: specifier: 0.0.10 version: 0.0.10 diff --git a/tests/example-json-api/app/routes/application.ts b/tests/example-json-api/app/routes/application.ts index 480a77d19ae..dcbc3d50fa3 100644 --- a/tests/example-json-api/app/routes/application.ts +++ b/tests/example-json-api/app/routes/application.ts @@ -23,7 +23,7 @@ export default class ApplicationRoute extends Route { const oldBooksPaginated = this.store.query('book', { page: 1, pageSize: 20 }); // Example of new usage (refactored, paginated) - const books = this.store.request>(query('book')); + const books = this.store.request(query('book')); const data = await Promise.all([genres, authors, books, oldBooks, oldBooksPaginated]); diff --git a/tests/main/tests/integration/legacy-compat/query-test.ts b/tests/main/tests/integration/legacy-compat/query-test.ts index 07837eed052..c5519990b25 100644 --- a/tests/main/tests/integration/legacy-compat/query-test.ts +++ b/tests/main/tests/integration/legacy-compat/query-test.ts @@ -117,10 +117,10 @@ module('Integration - legacy-compat/builders/query', function (hooks) { ); const store = this.owner.lookup('service:store') as CompatStore; - const { content: post } = await store.request(queryRecord('post', { id: '1' })); + const { content: post } = await store.request(queryRecord('post', { id: '1' })); - assert.strictEqual(post.id, '1', 'post has correct id'); - assert.strictEqual(post.name, 'Krystan rules, you drool', 'post has correct name'); + assert.strictEqual(post?.id, '1', 'post has correct id'); + assert.strictEqual(post?.name, 'Krystan rules, you drool', 'post has correct name'); assert.verifySteps(['adapter-queryRecord'], 'adapter-queryRecord was called'); }); diff --git a/tests/main/tests/integration/legacy-compat/save-record-test.ts b/tests/main/tests/integration/legacy-compat/save-record-test.ts index 0a7c5c3f077..daef1d41d18 100644 --- a/tests/main/tests/integration/legacy-compat/save-record-test.ts +++ b/tests/main/tests/integration/legacy-compat/save-record-test.ts @@ -47,7 +47,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { const store = this.owner.lookup('service:store') as CompatStore; const newPost: Post = store.createRecord('post', { name: 'Krystan rules, you drool' }); - const { content: savedPost } = await store.request(saveRecord(newPost)); + const { content: savedPost } = await store.request(saveRecord(newPost)); assert.strictEqual(savedPost.id, '1', 'post has correct id'); assert.strictEqual(savedPost.name, 'Krystan rules, you drool', 'post has correct name'); @@ -56,7 +56,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { test('saveRecord', function (assert) { const store = this.owner.lookup('service:store') as CompatStore; - const newPost: Post = store.createRecord('post', { name: 'Krystan rules, you drool' }); + const newPost = store.createRecord('post', { name: 'Krystan rules, you drool' }); const identifier = recordIdentifierFor(newPost); const result = saveRecord(newPost); assert.deepEqual( @@ -115,7 +115,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { ); const store = this.owner.lookup('service:store') as CompatStore; - const existingPost: Post = store.push({ + const existingPost = store.push({ data: { id: '1', type: 'post', @@ -125,7 +125,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { }, }); existingPost.deleteRecord(); - const { content: savedPost } = await store.request(saveRecord(existingPost)); + const { content: savedPost } = await store.request(saveRecord(existingPost)); assert.strictEqual(savedPost.id, '1', 'post has correct id'); assert.strictEqual(savedPost.name, 'Krystan rules, you drool', 'post has correct name'); @@ -212,7 +212,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { ); const store = this.owner.lookup('service:store') as CompatStore; - const existingPost: Post = store.push({ + const existingPost = store.push({ data: { id: '1', type: 'post', @@ -222,7 +222,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { }, }); existingPost.name = 'Chris drools, Krystan rules'; - const { content: savedPost } = await store.request(saveRecord(existingPost)); + const { content: savedPost } = await store.request(saveRecord(existingPost)); assert.strictEqual(savedPost.id, '1', 'post has correct id'); assert.strictEqual(savedPost.name, 'Chris drools, Krystan rules', 'post has correct name');