diff --git a/packages/legacy-compat/package.json b/packages/legacy-compat/package.json index 24bca8ac35a..34360b65bc8 100644 --- a/packages/legacy-compat/package.json +++ b/packages/legacy-compat/package.json @@ -51,7 +51,8 @@ }, "peerDependencies": { "@ember-data/graph": "workspace:4.12.7", - "@ember-data/json-api": "workspace:4.12.7" + "@ember-data/json-api": "workspace:4.12.7", + "@ember/string": "^3.1.1" }, "peerDependenciesMeta": { "@ember-data/graph": { @@ -74,6 +75,7 @@ "@babel/preset-typescript": "^7.21.4", "@babel/preset-env": "^7.21.4", "@babel/runtime": "^7.21.0", + "@ember/string": "^3.1.1", "@rollup/plugin-babel": "^6.0.3", "@rollup/plugin-node-resolve": "^15.0.1", "tslib": "^2.5.0", diff --git a/packages/legacy-compat/rollup.config.mjs b/packages/legacy-compat/rollup.config.mjs index 4b2cbb68fe7..e15a77f21c0 100644 --- a/packages/legacy-compat/rollup.config.mjs +++ b/packages/legacy-compat/rollup.config.mjs @@ -17,7 +17,7 @@ export default { plugins: [ // These are the modules that users should be able to import from your // addon. Anything not listed here may get optimized away. - addon.publicEntrypoints(['index.js', '-private.js']), + addon.publicEntrypoints(['index.js', 'builders.js', '-private.js']), nodeResolve({ extensions: ['.ts'] }), babel({ diff --git a/packages/legacy-compat/src/builders.ts b/packages/legacy-compat/src/builders.ts new file mode 100644 index 00000000000..5b42e847ce4 --- /dev/null +++ b/packages/legacy-compat/src/builders.ts @@ -0,0 +1,17 @@ +/** + Builders for migrating from `store` methods to `store.request`. + These builders enable you to migrate your codebase to using the correct syntax for `store.request` while temporarily preserving legacy behaviors. + This is useful for quickly upgrading an entire app to a unified syntax while a longer incremental migration is made to shift off of adapters and serializers. + To that end, these builders are deprecated and will be removed in a future version of Ember Data. + @module @ember-data/legacy-compat/builders + @main @ember-data/legacy-compat/builders + @deprecated +*/ + +export { findAllBuilder as findAll } from './builders/find-all'; + +export { findRecordBuilder as findRecord } from './builders/find-record'; + +export { queryBuilder as query, queryRecordBuilder as queryRecord } from './builders/query'; + +export { saveRecordBuilder as saveRecord } from './builders/save-record'; diff --git a/packages/legacy-compat/src/builders/find-all.ts b/packages/legacy-compat/src/builders/find-all.ts new file mode 100644 index 00000000000..9b08f5fca89 --- /dev/null +++ b/packages/legacy-compat/src/builders/find-all.ts @@ -0,0 +1,64 @@ +/** + * @module @ember-data/legacy-compat/builders + */ +import { assert } from '@ember/debug'; + +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 & { + op: 'findAll'; + data: { + type: T; + options: FindAllBuilderOptions; + }; + [RequestSignature]?: RT; +}; + +type FindAllBuilderOptions = FindAllOptions; + +/** + This function builds a request config to perform a `findAll` request for the given type. + When passed to `store.request`, this config will result in the same behavior as a `store.findAll` request. + Additionally, it takes the same options as `store.findAll`. + + All `@ember-data/legacy-compat` builders exist to enable you to migrate your codebase to using the correct syntax for `store.request` while temporarily preserving legacy behaviors. + This is useful for quickly upgrading an entire app to a unified syntax while a longer incremental migration is made to shift off of adapters and serializers. + To that end, these builders are deprecated and will be removed in a future version of Ember Data. + + @method findAll + @deprecated + @public + @static + @for @ember-data/legacy-compat/builders + @param {string} type the name of the resource + @param {object} query a query to be used by the adapter + @param {FindAllBuilderOptions} [options] optional, may include `adapterOptions` hash which will be passed to adapter.findAll + @return {FindAllRequestInput} request config +*/ +export function findAllBuilder( + type: TypeFromInstance, + options?: FindAllBuilderOptions +): 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}`, + typeof type === 'string' + ); + + return { + op: 'findAll', + data: { + type: normalizeModelName(type), + options: options || {}, + }, + cacheOptions: { [SkipCache as symbol]: true }, + }; +} diff --git a/packages/legacy-compat/src/builders/find-record.ts b/packages/legacy-compat/src/builders/find-record.ts new file mode 100644 index 00000000000..2328df21b87 --- /dev/null +++ b/packages/legacy-compat/src/builders/find-record.ts @@ -0,0 +1,115 @@ +/** + * @module @ember-data/legacy-compat/builders + */ +import { assert } from '@ember/debug'; + +import type { StoreRequestInput } from '@ember-data/store'; +import { constructResource, ensureStringId } from '@ember-data/store/-private'; +import type { BaseFinderOptions, FindRecordOptions } 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 { 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 & { + op: 'findRecord'; + data: { + record: ResourceIdentifierObject; + options: FindRecordBuilderOptions; + }; + [RequestSignature]?: RT; +}; + +type FindRecordBuilderOptions = Omit; + +/** + This function builds a request config to find the record for a given identifier or type and id combination. + When passed to `store.request`, this config will result in the same behavior as a `store.findRecord` request. + Additionally, it takes the same options as `store.findRecord`, with the exception of `preload` (which is unsupported). + + **Example 1** + + ```ts + import { findRecord } from '@ember-data/legacy-compat/builders'; + const { content: post } = await store.request(findRecord('post', '1')); + ``` + + **Example 2** + + `findRecord` can be called with a single identifier argument instead of the combination + of `type` (modelName) and `id` as separate arguments. You may recognize this combo as + the typical pairing from [JSON:API](https://jsonapi.org/format/#document-resource-object-identification) + + ```ts + import { findRecord } from '@ember-data/legacy-compat/builders'; + const { content: post } = await store.request(findRecord({ type: 'post', id })); + ``` + + All `@ember-data/legacy-compat` builders exist to enable you to migrate your codebase to using the correct syntax for `store.request` while temporarily preserving legacy behaviors. + This is useful for quickly upgrading an entire app to a unified syntax while a longer incremental migration is made to shift off of adapters and serializers. + To that end, these builders are deprecated and will be removed in a future version of Ember Data. + + @method findRecord + @deprecated + @public + @static + @for @ember-data/legacy-compat/builders + @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|number|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 {FindRecordBuilderOptions} [options] - if the first param is a string this will be the optional options for the request. See examples for available options. + @return {FindRecordRequestInput} request config +*/ +export function findRecordBuilder( + resource: TypeFromInstance, + id: string, + options?: FindRecordBuilderOptions +): FindRecordRequestInput, T>; +export function findRecordBuilder( + resource: string, + id: string, + options?: FindRecordBuilderOptions +): FindRecordRequestInput; +export function findRecordBuilder( + resource: ResourceIdentifierObject>, + options?: FindRecordBuilderOptions +): FindRecordRequestInput, T>; +export function findRecordBuilder( + resource: ResourceIdentifierObject, + options?: FindRecordBuilderOptions +): FindRecordRequestInput; +export function findRecordBuilder( + resource: string | ResourceIdentifierObject, + idOrOptions?: string | FindRecordBuilderOptions, + options?: FindRecordBuilderOptions +): FindRecordRequestInput { + assert( + `You need to pass a modelName or resource identifier as the first argument to the findRecord builder`, + resource + ); + if (isMaybeIdentifier(resource)) { + options = idOrOptions as BaseFinderOptions | undefined; + } else { + assert( + `You need to pass a modelName or resource identifier as the first argument to the findRecord builder (passed ${resource})`, + typeof resource === 'string' + ); + const type = normalizeModelName(resource); + const normalizedId = ensureStringId(idOrOptions as string | number); + resource = constructResource(type, normalizedId); + } + + options = options || {}; + + assert('findRecord builder does not support options.preload', !(options as FindRecordOptions).preload); + + return { + op: 'findRecord' as const, + data: { + record: resource, + options, + }, + cacheOptions: { [SkipCache as symbol]: true }, + }; +} diff --git a/packages/legacy-compat/src/builders/query.ts b/packages/legacy-compat/src/builders/query.ts new file mode 100644 index 00000000000..22fbb0c77fc --- /dev/null +++ b/packages/legacy-compat/src/builders/query.ts @@ -0,0 +1,138 @@ +/** + * @module @ember-data/legacy-compat/builders + */ +import { assert } from '@ember/debug'; + +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 & { + op: 'query'; + data: { + type: T; + query: Record; + options: QueryBuilderOptions; + }; + [RequestSignature]?: RT; +}; + +type QueryBuilderOptions = QueryOptions; + +/** + This function builds a request config for a given type and query object. + When passed to `store.request`, this config will result in the same behavior as a `store.query` request. + Additionally, it takes the same options as `store.query`. + + All `@ember-data/legacy-compat` builders exist to enable you to migrate your codebase to using the correct syntax for `store.request` while temporarily preserving legacy behaviors. + This is useful for quickly upgrading an entire app to a unified syntax while a longer incremental migration is made to shift off of adapters and serializers. + To that end, these builders are deprecated and will be removed in a future version of Ember Data. + + @method query + @deprecated + @public + @static + @for @ember-data/legacy-compat/builders + @param {string} type the name of the resource + @param {object} query a query to be used by the adapter + @param {QueryBuilderOptions} [options] optional, may include `adapterOptions` hash which will be passed to adapter.query + @return {QueryRequestInput} request config +*/ +export function queryBuilder( + type: TypeFromInstance, + query: Record, + options?: QueryBuilderOptions +): QueryRequestInput, T[]>; +export function queryBuilder( + type: string, + query: Record, + options?: QueryBuilderOptions +): QueryRequestInput; +export function queryBuilder( + type: string, + query: Record, + options: QueryBuilderOptions = {} +): 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( + `Model name passed to the query builder must be a dasherized string instead of ${type}`, + typeof type === 'string' + ); + + return { + op: 'query' as const, + data: { + type: normalizeModelName(type), + query, + options: options, + }, + cacheOptions: { [SkipCache as symbol]: true }, + }; +} + +type QueryRecordRequestInput = StoreRequestInput & { + op: 'queryRecord'; + data: { + type: T; + query: Record; + options: QueryBuilderOptions; + }; + [RequestSignature]?: RT; +}; + +/** + This function builds a request config for a given type and query object. + When passed to `store.request`, this config will result in the same behavior as a `store.queryRecord` request. + Additionally, it takes the same options as `store.queryRecord`. + + All `@ember-data/legacy-compat` builders exist to enable you to migrate your codebase to using the correct syntax for `store.request` while temporarily preserving legacy behaviors. + This is useful for quickly upgrading an entire app to a unified syntax while a longer incremental migration is made to shift off of adapters and serializers. + To that end, these builders are deprecated and will be removed in a future version of Ember Data. + + @method queryRecord + @deprecated + @public + @static + @for @ember-data/legacy-compat/builders + @param {string} type the name of the resource + @param {object} query a query to be used by the adapter + @param {QueryBuilderOptions} [options] optional, may include `adapterOptions` hash which will be passed to adapter.query + @return {QueryRecordRequestInput} request config +*/ +export function queryRecordBuilder( + type: TypeFromInstance, + query: Record, + options?: QueryBuilderOptions +): QueryRecordRequestInput, T | null>; +export function queryRecordBuilder( + type: string, + query: Record, + options?: QueryBuilderOptions +): QueryRecordRequestInput; +export function queryRecordBuilder( + type: string, + query: Record, + options?: QueryBuilderOptions +): 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( + `Model name passed to the queryRecord builder must be a dasherized string instead of ${type}`, + typeof type === 'string' + ); + + return { + op: 'queryRecord', + data: { + type: normalizeModelName(type), + query, + options: options || {}, + }, + cacheOptions: { [SkipCache as symbol]: true }, + }; +} diff --git a/packages/legacy-compat/src/builders/save-record.ts b/packages/legacy-compat/src/builders/save-record.ts new file mode 100644 index 00000000000..3a759037f23 --- /dev/null +++ b/packages/legacy-compat/src/builders/save-record.ts @@ -0,0 +1,95 @@ +/** + * @module @ember-data/legacy-compat/builders + */ +import { assert } from '@ember/debug'; + +import { recordIdentifierFor, storeFor, type StoreRequestInput } from '@ember-data/store'; +import type { InstanceCache } from '@ember-data/store/-private/caches/instance-cache'; +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 & { + op: 'createRecord' | 'deleteRecord' | 'updateRecord'; + data: { + record: StableRecordIdentifier; + options: SaveRecordBuilderOptions; + }; + records: [StableRecordIdentifier]; + [RequestSignature]?: RT; +}; + +type SaveRecordBuilderOptions = Record; + +function _resourceIsFullDeleted(identifier: StableRecordIdentifier, cache: Cache): boolean { + return cache.isDeletionCommitted(identifier) || (cache.isNew(identifier) && cache.isDeleted(identifier)); +} + +function resourceIsFullyDeleted(instanceCache: InstanceCache, identifier: StableRecordIdentifier): boolean { + const cache = instanceCache.cache; + return !cache || _resourceIsFullDeleted(identifier, cache); +} + +/** + This function builds a request config for saving the given record (e.g. creating, updating, or deleting the record). + When passed to `store.request`, this config will result in the same behavior as a legacy `store.saveRecord` request. + Additionally, it takes the same options as `store.saveRecord`. + + All `@ember-data/legacy-compat` builders exist to enable you to migrate your codebase to using the correct syntax for `store.request` while temporarily preserving legacy behaviors. + This is useful for quickly upgrading an entire app to a unified syntax while a longer incremental migration is made to shift off of adapters and serializers. + To that end, these builders are deprecated and will be removed in a future version of Ember Data. + + @method saveRecord + @deprecated + @public + @static + @for @ember-data/legacy-compat/builders + @param {object} record a record to save + @param {SaveRecordBuilderOptions} options optional, may include `adapterOptions` hash which will be passed to adapter.saveRecord + @return {SaveRecordRequestInput} request config +*/ +export function saveRecordBuilder( + record: T, + options: Record = {} +): SaveRecordRequestInput, T> { + const store = storeFor(record); + assert(`Unable to initiate save for a record in a disconnected state`, store); + const identifier = recordIdentifierFor(record); + + if (!identifier) { + // this commonly means we're disconnected + // but just in case we throw here to prevent bad things. + throw new Error(`Record Is Disconnected`); + } + assert( + `Cannot initiate a save request for an unloaded record: ${identifier.lid}`, + store._instanceCache.recordIsLoaded(identifier) + ); + if (resourceIsFullyDeleted(store._instanceCache, identifier)) { + throw new Error('cannot build saveRecord request for deleted record'); + } + + if (!options) { + options = {}; + } + let operation: 'createRecord' | 'deleteRecord' | 'updateRecord' = 'updateRecord'; + + const cache = store.cache; + if (cache.isNew(identifier)) { + operation = 'createRecord'; + } else if (cache.isDeleted(identifier)) { + operation = 'deleteRecord'; + } + + return { + op: operation, + data: { + options, + record: identifier, + }, + records: [identifier], + cacheOptions: { [SkipCache as symbol]: true }, + }; +} diff --git a/packages/legacy-compat/src/builders/utils.ts b/packages/legacy-compat/src/builders/utils.ts new file mode 100644 index 00000000000..d3b87f027d2 --- /dev/null +++ b/packages/legacy-compat/src/builders/utils.ts @@ -0,0 +1,40 @@ +import { deprecate } from '@ember/debug'; +import { dasherize } from '@ember/string'; + +import { DEPRECATE_NON_STRICT_TYPES } from '@warp-drive/build-config/deprecations'; +import type { ResourceIdentifierObject } from '@warp-drive/core-types/spec/raw'; + +export function isMaybeIdentifier( + maybeIdentifier: string | ResourceIdentifierObject +): maybeIdentifier is ResourceIdentifierObject { + return Boolean( + maybeIdentifier !== null && + typeof maybeIdentifier === 'object' && + (('id' in maybeIdentifier && 'type' in maybeIdentifier && maybeIdentifier.id && maybeIdentifier.type) || + maybeIdentifier.lid) + ); +} + +export function normalizeModelName(type: string): string { + if (DEPRECATE_NON_STRICT_TYPES) { + const result = dasherize(type); + + deprecate( + `The resource type '${type}' is not normalized. Update your application code to use '${result}' instead of '${type}'.`, + result === type, + { + id: 'ember-data:deprecate-non-strict-types', + until: '6.0', + for: 'ember-data', + since: { + available: '5.3', + enabled: '5.3', + }, + } + ); + + return result; + } + + return type; +} diff --git a/packages/legacy-compat/src/legacy-network-handler/legacy-network-handler.ts b/packages/legacy-compat/src/legacy-network-handler/legacy-network-handler.ts index fc4120a9c42..adcd65834d6 100644 --- a/packages/legacy-compat/src/legacy-network-handler/legacy-network-handler.ts +++ b/packages/legacy-compat/src/legacy-network-handler/legacy-network-handler.ts @@ -173,6 +173,8 @@ function saveRecord(context: StoreRequestContext): Promise { const { store, data, op: operation } = context.request; const { options, record: identifier } = data as { record: StableRecordIdentifier; options: Record }; + store.cache.willCommit(identifier, context); + const saveOptions = Object.assign( { [SaveOp]: operation as 'updateRecord' | 'deleteRecord' | 'createRecord' }, options diff --git a/packages/request/src/index.ts b/packages/request/src/index.ts index 93e966944d8..20aa9a72700 100644 --- a/packages/request/src/index.ts +++ b/packages/request/src/index.ts @@ -2,3 +2,4 @@ export { RequestManager as default } from './-private/manager'; export { createDeferred } from './-private/future'; export { setPromiseResult, getPromiseResult } from './-private/promise-cache'; export type { Awaitable } from './-private/promise-cache'; +export { SkipCache } from './-private/types'; diff --git a/packages/store/src/-private/index.ts b/packages/store/src/-private/index.ts index a59815669a8..1f886577a52 100644 --- a/packages/store/src/-private/index.ts +++ b/packages/store/src/-private/index.ts @@ -39,9 +39,11 @@ export function normalizeModelName(modelName: string) { assert(`normalizeModelName support has been removed`); } +export { default as constructResource } from './utils/construct-resource'; + // TODO this should be a deprecated helper but we have so much usage of it // to also eliminate -export { default as coerceId } from './utils/coerce-id'; +export { default as coerceId, ensureStringId } from './utils/coerce-id'; export { default as RecordArray, diff --git a/packages/store/src/-private/store-service.ts b/packages/store/src/-private/store-service.ts index d02029ca4a9..061ffaa1478 100644 --- a/packages/store/src/-private/store-service.ts +++ b/packages/store/src/-private/store-service.ts @@ -2418,9 +2418,6 @@ class Store extends EmberObject { cacheOptions: { [SkipCache as symbol]: true }, }; - // 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); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b23245c3b9e..30b3b708abe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -616,6 +616,9 @@ importers: '@babel/runtime': specifier: ^7.21.0 version: 7.21.0 + '@ember/string': + specifier: ^3.1.1 + version: 3.1.1 '@embroider/addon-dev': specifier: ^3.0.0 version: 3.0.0(rollup@3.20.2) @@ -1223,7 +1226,7 @@ importers: version: file:packages/json-api(@ember-data/graph@4.12.7)(@ember-data/store@4.12.7) '@ember-data/legacy-compat': specifier: workspace:4.12.7 - version: file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7) + version: file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7)(@ember/string@3.0.1) '@ember-data/model': specifier: workspace:4.12.7 version: file:packages/model(@babel/core@7.21.4)(@ember-data/debug@4.12.7)(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7)(@ember-data/legacy-compat@4.12.7)(@ember-data/store@4.12.7)(@ember-data/tracking@4.12.7)(@ember/string@3.0.1)(ember-inflector@4.0.2)(ember-source@4.12.0) @@ -1834,7 +1837,7 @@ importers: version: file:packages/json-api(@ember-data/graph@4.12.7)(@ember-data/store@4.12.7) '@ember-data/legacy-compat': specifier: workspace:4.12.7 - version: file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7) + version: file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7)(@ember/string@3.0.1) '@ember-data/model': specifier: workspace:4.12.7 version: file:packages/model(@babel/core@7.21.4)(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7)(@ember-data/legacy-compat@4.12.7)(@ember-data/store@4.12.7)(@ember-data/tracking@4.12.7)(@ember/string@3.0.1)(ember-inflector@4.0.2)(ember-source@4.12.0) @@ -2127,7 +2130,7 @@ importers: version: file:packages/debug(@ember-data/store@4.12.7)(@ember/string@3.0.1)(webpack@5.77.0) '@ember-data/legacy-compat': specifier: workspace:4.12.7 - version: file:packages/legacy-compat + version: file:packages/legacy-compat(@ember/string@3.0.1) '@ember-data/model': specifier: workspace:4.12.7 version: file:packages/model(@babel/core@7.21.4)(@ember-data/debug@4.12.7)(@ember-data/legacy-compat@4.12.7)(@ember-data/store@4.12.7)(@ember-data/tracking@4.12.7)(@ember/string@3.0.1)(ember-inflector@4.0.2)(ember-source@4.12.0) @@ -2762,7 +2765,7 @@ importers: version: file:packages/json-api(@ember-data/graph@4.12.7)(@ember-data/store@4.12.7) '@ember-data/legacy-compat': specifier: workspace:4.12.7 - version: file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7) + version: file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7)(@ember/string@3.0.1) '@ember-data/model': specifier: workspace:4.12.7 version: file:packages/model(@babel/core@7.21.4)(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7)(@ember-data/legacy-compat@4.12.7)(@ember-data/store@4.12.7)(@ember-data/tracking@4.12.7)(@ember/string@3.0.1)(ember-inflector@4.0.2)(ember-source@4.12.0) @@ -4237,6 +4240,15 @@ packages: transitivePeerDependencies: - supports-color + /@ember/string@3.1.1: + resolution: {integrity: sha512-UbXJ+k3QOrYN4SRPHgXCqYIJ+yWWUg1+vr0H4DhdQPTy8LJfyqwZ2tc5uqpSSnEXE+/1KopHBE5J8GDagAg5cg==} + engines: {node: 12.* || 14.* || >= 16} + dependencies: + ember-cli-babel: 7.26.11 + transitivePeerDependencies: + - supports-color + dev: true + /@ember/test-helpers@2.9.3(@babel/core@7.21.4)(ember-source@4.12.0): resolution: {integrity: sha512-ejVg4Dj+G/6zyLvQsYOvmGiOLU6AS94tY4ClaO1E2oVvjjtVJIRmVLFN61I+DuyBg9hS3cFoPjQRTZB9MRIbxQ==} engines: {node: 10.* || 12.* || 14.* || 15.* || >= 16.*} @@ -15955,7 +15967,7 @@ packages: '@ember-data/debug': file:packages/debug(@ember-data/store@4.12.7)(@ember/string@3.0.1)(webpack@5.77.0) '@ember-data/graph': file:packages/graph(@ember-data/store@4.12.7) '@ember-data/json-api': file:packages/json-api(@ember-data/graph@4.12.7)(@ember-data/store@4.12.7) - '@ember-data/legacy-compat': file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7) + '@ember-data/legacy-compat': file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7)(@ember/string@3.0.1) '@ember-data/model': file:packages/model(@babel/core@7.21.4)(@ember-data/debug@4.12.7)(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7)(@ember-data/legacy-compat@4.12.7)(@ember-data/store@4.12.7)(@ember-data/tracking@4.12.7)(@ember/string@3.0.1)(ember-inflector@4.0.2)(ember-source@4.12.0) '@ember-data/private-build-infra': file:packages/private-build-infra '@ember-data/request': file:packages/request @@ -16114,28 +16126,31 @@ packages: transitivePeerDependencies: - supports-color - file:packages/legacy-compat: + file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7): resolution: {directory: packages/legacy-compat, type: directory} + id: file:packages/legacy-compat name: '@ember-data/legacy-compat' version: 4.12.7 engines: {node: 16.* || >= 18} peerDependencies: '@ember-data/graph': workspace:4.12.7 '@ember-data/json-api': workspace:4.12.7 + '@ember/string': ^3.1.1 peerDependenciesMeta: '@ember-data/graph': optional: true '@ember-data/json-api': optional: true dependencies: + '@ember-data/graph': file:packages/graph(@ember-data/store@4.12.7) + '@ember-data/json-api': file:packages/json-api(@ember-data/graph@4.12.7)(@ember-data/store@4.12.7) '@ember-data/private-build-infra': file:packages/private-build-infra '@embroider/macros': 1.10.0 ember-cli-babel: 7.26.11 transitivePeerDependencies: - supports-color - dev: true - file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7): + file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7)(@ember/string@3.0.1): resolution: {directory: packages/legacy-compat, type: directory} id: file:packages/legacy-compat name: '@ember-data/legacy-compat' @@ -16144,6 +16159,7 @@ packages: peerDependencies: '@ember-data/graph': workspace:4.12.7 '@ember-data/json-api': workspace:4.12.7 + '@ember/string': ^3.1.1 peerDependenciesMeta: '@ember-data/graph': optional: true @@ -16153,10 +16169,35 @@ packages: '@ember-data/graph': file:packages/graph(@ember-data/store@4.12.7) '@ember-data/json-api': file:packages/json-api(@ember-data/graph@4.12.7)(@ember-data/store@4.12.7) '@ember-data/private-build-infra': file:packages/private-build-infra + '@ember/string': 3.0.1 + '@embroider/macros': 1.10.0 + ember-cli-babel: 7.26.11 + transitivePeerDependencies: + - supports-color + + file:packages/legacy-compat(@ember/string@3.0.1): + resolution: {directory: packages/legacy-compat, type: directory} + id: file:packages/legacy-compat + name: '@ember-data/legacy-compat' + version: 4.12.7 + engines: {node: 16.* || >= 18} + peerDependencies: + '@ember-data/graph': workspace:4.12.7 + '@ember-data/json-api': workspace:4.12.7 + '@ember/string': ^3.1.1 + peerDependenciesMeta: + '@ember-data/graph': + optional: true + '@ember-data/json-api': + optional: true + dependencies: + '@ember-data/private-build-infra': file:packages/private-build-infra + '@ember/string': 3.0.1 '@embroider/macros': 1.10.0 ember-cli-babel: 7.26.11 transitivePeerDependencies: - supports-color + dev: true file:packages/model: resolution: {directory: packages/model, type: directory} @@ -16220,7 +16261,7 @@ packages: '@ember-data/debug': file:packages/debug(@ember-data/store@4.12.7)(@ember/string@3.0.1)(webpack@5.77.0) '@ember-data/graph': file:packages/graph(@ember-data/store@4.12.7) '@ember-data/json-api': file:packages/json-api(@ember-data/graph@4.12.7)(@ember-data/store@4.12.7) - '@ember-data/legacy-compat': file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7) + '@ember-data/legacy-compat': file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7)(@ember/string@3.0.1) '@ember-data/private-build-infra': file:packages/private-build-infra '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7)(@ember-data/legacy-compat@4.12.7)(@ember-data/model@4.12.7)(@ember-data/tracking@4.12.7)(@ember/string@3.0.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0) '@ember-data/tracking': file:packages/tracking @@ -16305,7 +16346,7 @@ packages: optional: true dependencies: '@ember-data/debug': file:packages/debug(@ember-data/store@4.12.7)(@ember/string@3.0.1)(webpack@5.77.0) - '@ember-data/legacy-compat': file:packages/legacy-compat + '@ember-data/legacy-compat': file:packages/legacy-compat(@ember/string@3.0.1) '@ember-data/private-build-infra': file:packages/private-build-infra '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/legacy-compat@4.12.7)(@ember-data/model@4.12.7)(@ember-data/tracking@4.12.7)(@ember/string@3.0.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0) '@ember-data/tracking': file:packages/tracking @@ -16349,7 +16390,7 @@ packages: dependencies: '@ember-data/graph': file:packages/graph(@ember-data/store@4.12.7) '@ember-data/json-api': file:packages/json-api(@ember-data/graph@4.12.7)(@ember-data/store@4.12.7) - '@ember-data/legacy-compat': file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7) + '@ember-data/legacy-compat': file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7)(@ember/string@3.0.1) '@ember-data/private-build-infra': file:packages/private-build-infra '@ember-data/store': file:packages/store(@babel/core@7.21.4)(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7)(@ember-data/legacy-compat@4.12.7)(@ember-data/model@4.12.7)(@ember-data/tracking@4.12.7)(@ember/string@3.0.1)(@glimmer/tracking@1.1.2)(ember-source@4.12.0) '@ember-data/tracking': file:packages/tracking @@ -16543,7 +16584,7 @@ packages: dependencies: '@ember-data/graph': file:packages/graph(@ember-data/store@4.12.7) '@ember-data/json-api': file:packages/json-api(@ember-data/graph@4.12.7)(@ember-data/store@4.12.7) - '@ember-data/legacy-compat': file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7) + '@ember-data/legacy-compat': file:packages/legacy-compat(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7)(@ember/string@3.0.1) '@ember-data/model': file:packages/model(@babel/core@7.21.4)(@ember-data/debug@4.12.7)(@ember-data/graph@4.12.7)(@ember-data/json-api@4.12.7)(@ember-data/legacy-compat@4.12.7)(@ember-data/store@4.12.7)(@ember-data/tracking@4.12.7)(@ember/string@3.0.1)(ember-inflector@4.0.2)(ember-source@4.12.0) '@ember-data/private-build-infra': file:packages/private-build-infra '@ember-data/tracking': file:packages/tracking @@ -16659,7 +16700,7 @@ packages: '@ember-data/model': optional: true dependencies: - '@ember-data/legacy-compat': file:packages/legacy-compat + '@ember-data/legacy-compat': file:packages/legacy-compat(@ember/string@3.0.1) '@ember-data/model': file:packages/model(@babel/core@7.21.4)(@ember-data/debug@4.12.7)(@ember-data/legacy-compat@4.12.7)(@ember-data/store@4.12.7)(@ember-data/tracking@4.12.7)(@ember/string@3.0.1)(ember-inflector@4.0.2)(ember-source@4.12.0) '@ember-data/private-build-infra': file:packages/private-build-infra '@ember-data/tracking': file:packages/tracking diff --git a/tests/docs/fixtures/expected.js b/tests/docs/fixtures/expected.js index 20b1e3d7575..9c7e854868d 100644 --- a/tests/docs/fixtures/expected.js +++ b/tests/docs/fixtures/expected.js @@ -11,6 +11,7 @@ module.exports = { '@ember-data/graph', '@ember-data/json-api', '@ember-data/legacy-compat', + '@ember-data/legacy-compat/builders', '@ember-data/model', '@ember-data/request', '@ember-data/request/fetch', @@ -73,6 +74,11 @@ module.exports = { '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_STRING_ARG_SCHEMAS', '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_V1CACHE_STORE_APIS', '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_MANY_ARRAY_DUPLICATES_4_12', + '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#findAll', + '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#findRecord', + '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#query', + '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#queryRecord', + '(public) @ember-data/legacy-compat/builders @ember-data/legacy-compat/builders#saveRecord', '(private) @ember-data/legacy-compat SnapshotRecordArray#_recordArray', '(private) @ember-data/legacy-compat SnapshotRecordArray#_snapshots', '(private) @ember-data/legacy-compat SnapshotRecordArray#constructor', diff --git a/tests/graph/package.json b/tests/graph/package.json index 0a7595a74ad..048245da6d1 100644 --- a/tests/graph/package.json +++ b/tests/graph/package.json @@ -16,7 +16,7 @@ }, "scripts": { "build": "ember build", - "start": "ember test --port=0 --serve --no-launch", + "start": "ember test --test-port=0 --serve --no-launch", "test": "ember test --test-port=0" }, "dependenciesMeta": { diff --git a/tests/json-api/package.json b/tests/json-api/package.json index e87b974d5cf..c9830504914 100644 --- a/tests/json-api/package.json +++ b/tests/json-api/package.json @@ -16,7 +16,7 @@ }, "scripts": { "build": "ember build", - "start": "ember test --port=0 --serve --no-launch", + "start": "ember test --test-port=0 --serve --no-launch", "test": "ember test --test-port=0" }, "dependenciesMeta": { diff --git a/tests/main/tests/integration/legacy-compat/find-all-test.ts b/tests/main/tests/integration/legacy-compat/find-all-test.ts new file mode 100644 index 00000000000..ac1a417cea1 --- /dev/null +++ b/tests/main/tests/integration/legacy-compat/find-all-test.ts @@ -0,0 +1,91 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import type { CompatStore } from '@ember-data/legacy-compat'; +import { findAll } from '@ember-data/legacy-compat/builders'; +import Model, { attr } from '@ember-data/model'; +import { ResourceType } from '@warp-drive/core-types/symbols'; + +type FindAllBuilderOptions = Exclude[1], undefined>; + +class Post extends Model { + [ResourceType] = 'post' as const; + @attr declare name: string; +} + +module('Integration - legacy-compat/builders/findAll', function (hooks) { + setupTest(hooks); + + test('basic payload', async function (assert) { + this.owner.register('model:post', Post); + this.owner.register( + 'adapter:application', + class Adapter { + findAll() { + assert.step('adapter-findAll'); + return Promise.resolve({ + data: [ + { + id: '1', + type: 'post', + attributes: { + name: 'Krystan rules, you drool', + }, + }, + ], + }); + } + static create() { + return new this(); + } + } + ); + + const store = this.owner.lookup('service:store') as CompatStore; + const { content: results } = await store.request(findAll('post')); + + assert.strictEqual(results.length, 1, 'post was found'); + assert.strictEqual(results[0].id, '1', 'post has correct id'); + assert.strictEqual(results[0].name, 'Krystan rules, you drool', 'post has correct name'); + assert.verifySteps(['adapter-findAll'], 'adapter-findAll was called'); + }); + + test('findAll', function (assert) { + const result = findAll('post'); + assert.deepEqual( + result, + { + op: 'findAll', + data: { + type: 'post', + options: {}, + }, + cacheOptions: {}, + }, + `findAll works` + ); + }); + + test('findAll with options', function (assert) { + const options: Required = { + reload: true, + backgroundReload: false, + include: 'author,comments', + adapterOptions: {}, + }; + const result = findAll('post', options); + assert.deepEqual( + result, + { + op: 'findAll', + data: { + type: 'post', + options, + }, + cacheOptions: {}, + }, + `findAll works with options` + ); + }); +}); diff --git a/tests/main/tests/integration/legacy-compat/find-record-test.ts b/tests/main/tests/integration/legacy-compat/find-record-test.ts new file mode 100644 index 00000000000..57dc134862e --- /dev/null +++ b/tests/main/tests/integration/legacy-compat/find-record-test.ts @@ -0,0 +1,149 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import type { CompatStore } from '@ember-data/legacy-compat'; +import { findRecord } from '@ember-data/legacy-compat/builders'; +import Model, { attr } from '@ember-data/model'; +import type { FindRecordOptions } from '@ember-data/store/-types/q/store'; +import { ResourceType } from '@warp-drive/core-types/symbols'; + +type FindRecordBuilderOptions = Exclude[1], undefined>; + +class Post extends Model { + [ResourceType] = 'post' as const; + @attr declare name: string; +} + +module('Integration - legacy-compat/builders/findRecord', function (hooks) { + setupTest(hooks); + + test('basic payload', async function (assert) { + this.owner.register('model:post', Post); + this.owner.register( + 'adapter:application', + class Adapter { + findRecord() { + assert.step('adapter-findRecord'); + return Promise.resolve({ + data: { + id: '1', + type: 'post', + attributes: { + name: 'Krystan rules, you drool', + }, + }, + }); + } + static create() { + return new this(); + } + } + ); + + const store = this.owner.lookup('service:store') as CompatStore; + const { content: post } = await store.request(findRecord('post', '1')); + + assert.strictEqual(post.id, '1', 'post has correct id'); + assert.strictEqual(post.name, 'Krystan rules, you drool', 'post has correct name'); + assert.verifySteps(['adapter-findRecord'], 'adapter-findRecord was called'); + }); + + test('findRecord by type+id', function (assert) { + const result = findRecord('post', '1'); + assert.deepEqual( + result, + { + op: 'findRecord', + data: { + record: { type: 'post', id: '1' }, + options: {}, + }, + cacheOptions: {}, + }, + `findRecord works with type+id` + ); + }); + + test('findRecord by type+id with options', function (assert) { + const options: Required = { + reload: true, + backgroundReload: false, + include: 'author,comments', + adapterOptions: {}, + }; + const result = findRecord('post', '1', options); + assert.deepEqual( + result, + { + op: 'findRecord', + data: { + record: { type: 'post', id: '1' }, + options, + }, + cacheOptions: {}, + }, + `findRecord works with type+id and options` + ); + }); + + test('findRecord by type+id with invalid options', async function (assert) { + // Type hacks to ensure we're notified if we add new FindRecordOptions that aren't valid FindRecordBuilderOptions + const invalidOptions: Omit, keyof FindRecordBuilderOptions> = { + preload: {}, + }; + await assert.expectAssertion(() => { + // @ts-expect-error TS knows the options are invalid + findRecord('post', '1', invalidOptions); + }, 'Assertion Failed: findRecord builder does not support options.preload'); + }); + + test('findRecord by identifier', function (assert) { + const result = findRecord({ type: 'post', id: '1' }); + assert.deepEqual( + result, + { + op: 'findRecord', + data: { + record: { type: 'post', id: '1' }, + options: {}, + }, + cacheOptions: {}, + }, + `findRecord works with an identifier` + ); + }); + + test('findRecord by identifier with options', function (assert) { + const options: Required = { + reload: true, + backgroundReload: false, + include: 'author,comments', + adapterOptions: {}, + }; + const result = findRecord({ type: 'post', id: '1' }, options); + assert.deepEqual( + result, + { + op: 'findRecord', + data: { + record: { type: 'post', id: '1' }, + options, + }, + cacheOptions: {}, + }, + `findRecord works with an identifier and options` + ); + }); + + test('findRecord by identifier with invalid options', async function (assert) { + // Type hacks to ensure we're notified if we add new FindRecordOptions that aren't valid FindRecordBuilderOptions + const invalidOptions: Omit, keyof FindRecordBuilderOptions> = { + preload: {}, + }; + await assert.expectAssertion(() => { + // @ts-expect-error TS knows the options are invalid + findRecord({ type: 'post', id: '1' }, invalidOptions); + }, 'Assertion Failed: findRecord builder does not support options.preload'); + }); +}); diff --git a/tests/main/tests/integration/legacy-compat/query-test.ts b/tests/main/tests/integration/legacy-compat/query-test.ts new file mode 100644 index 00000000000..c5519990b25 --- /dev/null +++ b/tests/main/tests/integration/legacy-compat/query-test.ts @@ -0,0 +1,165 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import type { CompatStore } from '@ember-data/legacy-compat'; +import { query, queryRecord } from '@ember-data/legacy-compat/builders'; +import Model, { attr } from '@ember-data/model'; +import { ResourceType } from '@warp-drive/core-types/symbols'; + +type QueryBuilderOptions = Exclude[2], undefined>; +type QueryRecordBuilderOptions = Exclude[2], undefined>; + +class Post extends Model { + [ResourceType] = 'post' as const; + @attr declare name: string; +} + +module('Integration - legacy-compat/builders/query', function (hooks) { + setupTest(hooks); + + module('query', function () { + test('basic payload', async function (assert) { + this.owner.register('model:post', Post); + this.owner.register( + 'adapter:application', + class Adapter { + query() { + assert.step('adapter-query'); + return Promise.resolve({ + data: [ + { + id: '1', + type: 'post', + attributes: { + name: 'Krystan rules, you drool', + }, + }, + ], + }); + } + static create() { + return new this(); + } + } + ); + + const store = this.owner.lookup('service:store') as CompatStore; + const { content: results } = await store.request(query('post', { id: '1' })); + + assert.strictEqual(results.length, 1, 'post was found'); + assert.strictEqual(results[0].id, '1', 'post has correct id'); + assert.strictEqual(results[0].name, 'Krystan rules, you drool', 'post has correct name'); + assert.verifySteps(['adapter-query'], 'adapter-query was called'); + }); + + test('query', function (assert) { + const result = query('post', { id: '1' }); + assert.deepEqual( + result, + { + op: 'query', + data: { + type: 'post', + query: { id: '1' }, + options: {}, + }, + cacheOptions: {}, + }, + `query works` + ); + }); + + test('query with options', function (assert) { + const options: Required = { + whatever: true, + adapterOptions: {}, + }; + const result = query('post', { id: '1' }, options); + assert.deepEqual( + result, + { + op: 'query', + data: { + type: 'post', + query: { id: '1' }, + options, + }, + cacheOptions: {}, + }, + `query works with options` + ); + }); + }); + + module('queryRecord', function () { + test('basic payload', async function (assert) { + this.owner.register('model:post', Post); + this.owner.register( + 'adapter:application', + class Adapter { + queryRecord() { + assert.step('adapter-queryRecord'); + return Promise.resolve({ + data: { + id: '1', + type: 'post', + attributes: { + name: 'Krystan rules, you drool', + }, + }, + }); + } + static create() { + return new this(); + } + } + ); + + const store = this.owner.lookup('service:store') as CompatStore; + 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.verifySteps(['adapter-queryRecord'], 'adapter-queryRecord was called'); + }); + + test('queryRecord', function (assert) { + const result = queryRecord('post', { id: '1' }); + assert.deepEqual( + result, + { + op: 'queryRecord', + data: { + type: 'post', + query: { id: '1' }, + options: {}, + }, + cacheOptions: {}, + }, + `queryRecord works` + ); + }); + + test('queryRecord with options', function (assert) { + const options: Required = { + whatever: true, + adapterOptions: {}, + }; + const result = queryRecord('post', { id: '1' }, options); + assert.deepEqual( + result, + { + op: 'queryRecord', + data: { + type: 'post', + query: { id: '1' }, + options, + }, + cacheOptions: {}, + }, + `queryRecord works with options` + ); + }); + }); +}); diff --git a/tests/main/tests/integration/legacy-compat/save-record-test.ts b/tests/main/tests/integration/legacy-compat/save-record-test.ts new file mode 100644 index 00000000000..daef1d41d18 --- /dev/null +++ b/tests/main/tests/integration/legacy-compat/save-record-test.ts @@ -0,0 +1,295 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import type { CompatStore } from '@ember-data/legacy-compat'; +import { saveRecord } from '@ember-data/legacy-compat/builders'; +import Model, { attr } from '@ember-data/model'; +import { recordIdentifierFor } from '@ember-data/store'; +import { ResourceType } from '@warp-drive/core-types/symbols'; + +class Post extends Model { + [ResourceType] = 'post' as const; + @attr declare name: string; +} + +type SaveRecordBuilderOptions = Exclude[1], undefined>; + +module('Integration - legacy-compat/builders/saveRecord', function (hooks) { + setupTest(hooks); + + hooks.beforeEach(function () { + this.owner.register('model:post', Post); + }); + + module('createRecord', function () { + test('basic payload', async function (assert) { + this.owner.register( + 'adapter:application', + class Adapter { + createRecord() { + assert.step('adapter-createRecord'); + return Promise.resolve({ + data: { + id: '1', + type: 'post', + attributes: { + name: 'Krystan rules, you drool', + }, + }, + }); + } + static create() { + return new this(); + } + } + ); + + 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)); + + assert.strictEqual(savedPost.id, '1', 'post has correct id'); + assert.strictEqual(savedPost.name, 'Krystan rules, you drool', 'post has correct name'); + assert.verifySteps(['adapter-createRecord'], 'adapter-createRecord was called'); + }); + + test('saveRecord', function (assert) { + const store = this.owner.lookup('service:store') as CompatStore; + const newPost = store.createRecord('post', { name: 'Krystan rules, you drool' }); + const identifier = recordIdentifierFor(newPost); + const result = saveRecord(newPost); + assert.deepEqual( + result, + { + op: 'createRecord', + data: { + record: identifier, + options: {}, + }, + records: [identifier], + cacheOptions: {}, + }, + `saveRecord works` + ); + }); + + test('saveRecord with options', function (assert) { + const options: Required = { + whatever: true, + adapterOptions: {}, + }; + const store = this.owner.lookup('service:store') as CompatStore; + const newPost: Post = store.createRecord('post', { name: 'Krystan rules, you drool' }); + const identifier = recordIdentifierFor(newPost); + const result = saveRecord(newPost, options); + assert.deepEqual( + result, + { + op: 'createRecord', + data: { + record: identifier, + options: options, + }, + records: [identifier], + cacheOptions: {}, + }, + `saveRecord works` + ); + }); + }); + + module('deleteRecord', function () { + test('basic payload', async function (assert) { + this.owner.register( + 'adapter:application', + class Adapter { + deleteRecord() { + assert.step('adapter-deleteRecord'); + return Promise.resolve(); + } + static create() { + return new this(); + } + } + ); + + const store = this.owner.lookup('service:store') as CompatStore; + const existingPost = store.push({ + data: { + id: '1', + type: 'post', + attributes: { + name: 'Krystan rules, you drool', + }, + }, + }); + existingPost.deleteRecord(); + 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'); + assert.true(savedPost.isDeleted, 'post isDeleted'); + assert.verifySteps(['adapter-deleteRecord'], 'adapter-deleteRecord was called'); + }); + + test('saveRecord', function (assert) { + const store = this.owner.lookup('service:store') as CompatStore; + const existingPost: Post = store.push({ + data: { + id: '1', + type: 'post', + attributes: { + name: 'Krystan rules, you drool', + }, + }, + }); + existingPost.deleteRecord(); + const identifier = recordIdentifierFor(existingPost); + const result = saveRecord(existingPost); + assert.deepEqual( + result, + { + op: 'deleteRecord', + data: { + record: identifier, + options: {}, + }, + records: [identifier], + cacheOptions: {}, + }, + `saveRecord works` + ); + }); + + test('saveRecord with options', function (assert) { + const options: Required = { + whatever: true, + adapterOptions: {}, + }; + const store = this.owner.lookup('service:store') as CompatStore; + const existingPost: Post = store.push({ + data: { + id: '1', + type: 'post', + attributes: { + name: 'Krystan rules, you drool', + }, + }, + }); + existingPost.deleteRecord(); + const identifier = recordIdentifierFor(existingPost); + const result = saveRecord(existingPost, options); + assert.deepEqual( + result, + { + op: 'deleteRecord', + data: { + record: identifier, + options: options, + }, + records: [identifier], + cacheOptions: {}, + }, + `saveRecord works` + ); + }); + }); + + module('updateRecord', function () { + test('basic payload', async function (assert) { + this.owner.register( + 'adapter:application', + class Adapter { + updateRecord() { + assert.step('adapter-updateRecord'); + return Promise.resolve(); + } + static create() { + return new this(); + } + } + ); + + const store = this.owner.lookup('service:store') as CompatStore; + const existingPost = store.push({ + data: { + id: '1', + type: 'post', + attributes: { + name: 'Krystan rules, you drool', + }, + }, + }); + existingPost.name = 'Chris drools, Krystan rules'; + 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'); + assert.false(savedPost.isDeleted, 'post is not deleted'); + assert.verifySteps(['adapter-updateRecord'], 'adapter-updateRecord was called'); + }); + + test('saveRecord', function (assert) { + const store = this.owner.lookup('service:store') as CompatStore; + const existingPost: Post = store.push({ + data: { + id: '1', + type: 'post', + attributes: { + name: 'Krystan rules, you drool', + }, + }, + }); + existingPost.name = 'Chris drools, Krystan rules'; + const identifier = recordIdentifierFor(existingPost); + const result = saveRecord(existingPost); + assert.deepEqual( + result, + { + op: 'updateRecord', + data: { + record: identifier, + options: {}, + }, + records: [identifier], + cacheOptions: {}, + }, + `saveRecord works` + ); + }); + + test('saveRecord with options', function (assert) { + const options: Required = { + whatever: true, + adapterOptions: {}, + }; + const store = this.owner.lookup('service:store') as CompatStore; + const existingPost: Post = store.push({ + data: { + id: '1', + type: 'post', + attributes: { + name: 'Krystan rules, you drool', + }, + }, + }); + existingPost.name = 'Chris drools, Krystan rules'; + const identifier = recordIdentifierFor(existingPost); + const result = saveRecord(existingPost, options); + assert.deepEqual( + result, + { + op: 'updateRecord', + data: { + record: identifier, + options: options, + }, + records: [identifier], + cacheOptions: {}, + }, + `saveRecord works` + ); + }); + }); +}); diff --git a/tests/request/package.json b/tests/request/package.json index e0cc4b2e450..c94ee04cde7 100644 --- a/tests/request/package.json +++ b/tests/request/package.json @@ -15,7 +15,7 @@ }, "scripts": { "build": "ember build", - "start": "ember test --port=0 --serve --no-launch", + "start": "ember test --test-port=0 --serve --no-launch", "test": "ember test --test-port=0" }, "dependenciesMeta": {