From bfca85b901f930b2c65dabb6d05ca661fe1fc7ae Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Thu, 4 Apr 2024 13:56:14 -0700 Subject: [PATCH 01/18] Add legacy-compat/builders findRecord --- packages/legacy-compat/.eslintrc.cjs | 2 +- packages/legacy-compat/package.json | 1 + packages/legacy-compat/rollup.config.mjs | 2 +- packages/legacy-compat/src/builders.ts | 2 + .../legacy-compat/src/builders/find-record.ts | 69 +++++++++ packages/legacy-compat/src/builders/utils.ts | 40 +++++ packages/store/src/-private.ts | 4 +- packages/store/src/-private/store-service.ts | 4 +- tests/main/package.json | 2 +- .../adapter/rest-adapter/-ajax-mocks.js | 2 +- .../legacy-compat/find-record-test.ts | 145 ++++++++++++++++++ 11 files changed, 266 insertions(+), 7 deletions(-) create mode 100644 packages/legacy-compat/src/builders.ts create mode 100644 packages/legacy-compat/src/builders/find-record.ts create mode 100644 packages/legacy-compat/src/builders/utils.ts create mode 100644 tests/main/tests/integration/legacy-compat/find-record-test.ts diff --git a/packages/legacy-compat/.eslintrc.cjs b/packages/legacy-compat/.eslintrc.cjs index b13a54eda67..9ab544c4853 100644 --- a/packages/legacy-compat/.eslintrc.cjs +++ b/packages/legacy-compat/.eslintrc.cjs @@ -16,7 +16,7 @@ const config = { base.rules(), imports.rules(), isolation.rules({ - allowedImports: ['@ember/debug', '@ember/application'], + allowedImports: ['@ember/debug', '@ember/string', '@ember/application'], }), {} ), diff --git a/packages/legacy-compat/package.json b/packages/legacy-compat/package.json index 8e9d5c5ef6c..b9a0f5c2f4a 100644 --- a/packages/legacy-compat/package.json +++ b/packages/legacy-compat/package.json @@ -94,6 +94,7 @@ "@ember-data/json-api": "workspace:5.4.0-alpha.52", "@ember-data/request": "workspace:5.4.0-alpha.52", "@ember-data/store": "workspace:5.4.0-alpha.52", + "@ember/string": "^3.1.1", "@warp-drive/core-types": "workspace:0.0.0-alpha.38" }, "peerDependenciesMeta": { diff --git a/packages/legacy-compat/rollup.config.mjs b/packages/legacy-compat/rollup.config.mjs index 53615e2d1d0..0caa661de7b 100644 --- a/packages/legacy-compat/rollup.config.mjs +++ b/packages/legacy-compat/rollup.config.mjs @@ -19,7 +19,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..510d33911d8 --- /dev/null +++ b/packages/legacy-compat/src/builders.ts @@ -0,0 +1,2 @@ +export { findRecord } from './builders/find-record'; +export type { FindRecordBuilderOptions, FindRecordRequestInput } from './builders/find-record'; 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..38cad4b9fcb --- /dev/null +++ b/packages/legacy-compat/src/builders/find-record.ts @@ -0,0 +1,69 @@ +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 { 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 { isMaybeIdentifier, normalizeModelName } from './utils'; + +export type FindRecordRequestInput = StoreRequestInput & { + op: 'findRecord'; + data: { + record: ResourceIdentifierObject; + options: FindRecordBuilderOptions; + }; +}; + +export type FindRecordBuilderOptions = Omit; + +export function findRecord( + resource: TypeFromInstance, + id: string, + options?: FindRecordBuilderOptions +): FindRecordRequestInput; +export function findRecord(resource: string, id: string, options?: FindRecordBuilderOptions): FindRecordRequestInput; +export function findRecord( + resource: ResourceIdentifierObject>, + options?: FindRecordBuilderOptions +): FindRecordRequestInput; +export function findRecord( + resource: ResourceIdentifierObject, + options?: FindRecordBuilderOptions +): FindRecordRequestInput; +export function findRecord( + resource: string | ResourceIdentifierObject, + idOrOptions?: string | BaseFinderOptions, + 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( + `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${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/utils.ts b/packages/legacy-compat/src/builders/utils.ts new file mode 100644 index 00000000000..9faff88442d --- /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 '@ember-data/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/store/src/-private.ts b/packages/store/src/-private.ts index 86afd997c40..9ebddf4afae 100644 --- a/packages/store/src/-private.ts +++ b/packages/store/src/-private.ts @@ -16,9 +16,11 @@ export { isStableIdentifier, } from './-private/caches/identifier-cache'; +export { default as constructResource } from './-private/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 './-private/utils/coerce-id'; +export { default as coerceId, ensureStringId } from './-private/utils/coerce-id'; export type { NativeProxy } from './-private/record-arrays/native-proxy-type-fix'; export { default as RecordArray, diff --git a/packages/store/src/-private/store-service.ts b/packages/store/src/-private/store-service.ts index 839d4f3ef17..487d9f9f324 100644 --- a/packages/store/src/-private/store-service.ts +++ b/packages/store/src/-private/store-service.ts @@ -1198,8 +1198,8 @@ class Store extends EmberObject { */ findRecord(resource: TypeFromInstance, id: string | number, options?: FindRecordOptions): Promise; findRecord(resource: string, id: string | number, options?: FindRecordOptions): Promise; - findRecord(resource: ResourceIdentifierObject>, id?: FindRecordOptions): Promise; - findRecord(resource: ResourceIdentifierObject, id?: FindRecordOptions): Promise; + findRecord(resource: ResourceIdentifierObject>, options?: FindRecordOptions): Promise; + findRecord(resource: ResourceIdentifierObject, options?: FindRecordOptions): Promise; findRecord( resource: string | ResourceIdentifierObject, id?: string | number | FindRecordOptions, diff --git a/tests/main/package.json b/tests/main/package.json index 0a26f0fedd6..5b44462d011 100644 --- a/tests/main/package.json +++ b/tests/main/package.json @@ -21,7 +21,7 @@ "test:try-one": "ember try:one", "holodeck:start-program": "holodeck start", "holodeck:end-program": "holodeck end", - "launch:tests": "ember test --port=0 --serve --no-launch", + "launch:tests": "ember test --test-port=0 --serve --no-launch", "start": "holodeck run launch:tests", "test": "holodeck run examine", "test:production": "holodeck run examine", diff --git a/tests/main/tests/integration/adapter/rest-adapter/-ajax-mocks.js b/tests/main/tests/integration/adapter/rest-adapter/-ajax-mocks.js index 98e13e94a8b..9f84c5ce8f6 100644 --- a/tests/main/tests/integration/adapter/rest-adapter/-ajax-mocks.js +++ b/tests/main/tests/integration/adapter/rest-adapter/-ajax-mocks.js @@ -2,7 +2,7 @@ import deepCopy from '@ember-data/unpublished-test-infra/test-support/deep-copy' /** * @description Helper function to mock the response of an adapter in order to - * Test is behaviour. + * test its behavior. * @param { adapter } RESTAdapter instance * @param { response } Response to return from the adapter * @return { ajaxCallback } Function that returns information about the last 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..dbc6cc47ecf --- /dev/null +++ b/tests/main/tests/integration/legacy-compat/find-record-test.ts @@ -0,0 +1,145 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import type { CompatStore } from '@ember-data/legacy-compat'; +import type { FindRecordBuilderOptions } from '@ember-data/legacy-compat/builders'; +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'; + +module('Integration - legacy-compat/builders/findRecord', function (hooks) { + setupTest(hooks); + + test('basic payload', async function (assert) { + class Post extends Model { + @attr declare name: string; + } + 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'); + }); +}); From ca1b6692788657c88956564689d3b962370858e4 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Thu, 4 Apr 2024 14:09:52 -0700 Subject: [PATCH 02/18] Add findRecord docs --- .../legacy-compat/src/builders/find-record.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/legacy-compat/src/builders/find-record.ts b/packages/legacy-compat/src/builders/find-record.ts index 38cad4b9fcb..f9a93946e6d 100644 --- a/packages/legacy-compat/src/builders/find-record.ts +++ b/packages/legacy-compat/src/builders/find-record.ts @@ -19,6 +19,51 @@ export type FindRecordRequestInput = StoreRequestInput & { export type FindRecordBuilderOptions = Omit; +/** + This function builds a request config 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** + + ```app/routes/post.js + import Route from '@ember/routing/route'; + import { findRecord } from '@ember-data/legacy-compat/builders'; + + export default class PostRoute extends Route { + model({ post_id }) { + return this.store.request(findRecord('post', post_id)); + } + } + ``` + + **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) + + ```app/routes/post.js + import Route from '@ember/routing/route'; + import { findRecord } from '@ember-data/legacy-compat/builders'; + + export default class PostRoute extends Route { + model({ post_id: id }) { + return this.store.request(findRecord({ type: 'post', id }).content; + } + } + ``` + + + + @since x.x.x + @method findRecord + @public + @param {String|object} type - either a string representing the name of the resource or a ResourceIdentifier object containing both the type (a string) and the id (a string) for the record or an lid (a string) of an existing record + @param {(String|Integer|Object)} id - optional object with options for the request only if the first param is a ResourceIdentifier, else the string id of the record to be retrieved + @param {Object} [options] - if the first param is a string this will be the optional options for the request. See examples for available options. + @return {FindRecordRequestInput} request config +*/ export function findRecord( resource: TypeFromInstance, id: string, From 87313deaead49e75c32844c665054a0ea1081df2 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Thu, 4 Apr 2024 15:20:43 -0700 Subject: [PATCH 03/18] Add legacy-compat/builders query --- packages/legacy-compat/src/builders.ts | 5 +- .../legacy-compat/src/builders/find-record.ts | 16 ++-- packages/legacy-compat/src/builders/query.ts | 48 ++++++++++ packages/store/src/-private/store-service.ts | 2 +- .../integration/legacy-compat/query-test.ts | 87 +++++++++++++++++++ 5 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 packages/legacy-compat/src/builders/query.ts create mode 100644 tests/main/tests/integration/legacy-compat/query-test.ts diff --git a/packages/legacy-compat/src/builders.ts b/packages/legacy-compat/src/builders.ts index 510d33911d8..8e3b4ffbf00 100644 --- a/packages/legacy-compat/src/builders.ts +++ b/packages/legacy-compat/src/builders.ts @@ -1,2 +1,5 @@ -export { findRecord } from './builders/find-record'; +export { findRecordBuilder as findRecord } from './builders/find-record'; export type { FindRecordBuilderOptions, FindRecordRequestInput } from './builders/find-record'; + +export { queryBuilder as query } from './builders/query'; +export type { QueryBuilderOptions, QueryRequestInput } from './builders/query'; diff --git a/packages/legacy-compat/src/builders/find-record.ts b/packages/legacy-compat/src/builders/find-record.ts index f9a93946e6d..515e2613c7c 100644 --- a/packages/legacy-compat/src/builders/find-record.ts +++ b/packages/legacy-compat/src/builders/find-record.ts @@ -64,21 +64,25 @@ export type FindRecordBuilderOptions = Omit; @param {Object} [options] - if the first param is a string this will be the optional options for the request. See examples for available options. @return {FindRecordRequestInput} request config */ -export function findRecord( +export function findRecordBuilder( resource: TypeFromInstance, id: string, options?: FindRecordBuilderOptions ): FindRecordRequestInput; -export function findRecord(resource: string, id: string, options?: FindRecordBuilderOptions): FindRecordRequestInput; -export function findRecord( +export function findRecordBuilder( + resource: string, + id: string, + options?: FindRecordBuilderOptions +): FindRecordRequestInput; +export function findRecordBuilder( resource: ResourceIdentifierObject>, options?: FindRecordBuilderOptions ): FindRecordRequestInput; -export function findRecord( +export function findRecordBuilder( resource: ResourceIdentifierObject, options?: FindRecordBuilderOptions ): FindRecordRequestInput; -export function findRecord( +export function findRecordBuilder( resource: string | ResourceIdentifierObject, idOrOptions?: string | BaseFinderOptions, options?: FindRecordBuilderOptions @@ -91,7 +95,7 @@ export function findRecord( options = idOrOptions as BaseFinderOptions | undefined; } else { assert( - `Passing classes to store methods has been removed. Please pass a dasherized string instead of ${resource}`, + `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); diff --git a/packages/legacy-compat/src/builders/query.ts b/packages/legacy-compat/src/builders/query.ts new file mode 100644 index 00000000000..956ac5bc894 --- /dev/null +++ b/packages/legacy-compat/src/builders/query.ts @@ -0,0 +1,48 @@ +import { assert } from '@ember/debug'; + +import type { StoreRequestInput } from '@ember-data/store'; +import type { QueryOptions } from '@ember-data/store/-types/q/store'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; +import { SkipCache } from '@warp-drive/core-types/request'; + +import { normalizeModelName } from './utils'; + +export type QueryRequestInput = StoreRequestInput & { + op: 'query'; + data: { + type: string; + query: Record; + options: QueryBuilderOptions; + }; +}; + +export type QueryBuilderOptions = QueryOptions; + +export function queryBuilder( + type: TypeFromInstance, + query: Record, + options?: QueryOptions +): QueryRequestInput; +export function queryBuilder(type: string, query: Record, options?: QueryOptions): QueryRequestInput; +export function queryBuilder( + type: string, + query: Record, + options: QueryOptions = {} +): 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 }, + }; +} diff --git a/packages/store/src/-private/store-service.ts b/packages/store/src/-private/store-service.ts index 487d9f9f324..c5fc01364ab 100644 --- a/packages/store/src/-private/store-service.ts +++ b/packages/store/src/-private/store-service.ts @@ -485,7 +485,7 @@ class Store extends EmberObject { * a resource. * * This hook can be used to select or instantiate any desired - * mechanism of presentating cache data to the ui for access + * mechanism of presenting cache data to the ui for access * mutation, and interaction. * * @method instantiateRecord (hook) 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..8265655cd1e --- /dev/null +++ b/tests/main/tests/integration/legacy-compat/query-test.ts @@ -0,0 +1,87 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import type { CompatStore } from '@ember-data/legacy-compat'; +import type { QueryBuilderOptions } from '@ember-data/legacy-compat/builders'; +import { query } from '@ember-data/legacy-compat/builders'; +import Model, { attr } from '@ember-data/model'; + +module('Integration - legacy-compat/builders/query', function (hooks) { + setupTest(hooks); + + test('basic payload', async function (assert) { + class Post extends Model { + @attr declare name: string; + } + this.owner.register('model:post', Post); + this.owner.register( + 'adapter:application', + class Adapter { + query(store, type, queryObject) { + 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` + ); + }); +}); From ecf9efa514da2ad95e94f821a7463cd072512230 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Thu, 4 Apr 2024 15:24:05 -0700 Subject: [PATCH 04/18] Add query docs --- packages/legacy-compat/src/builders/query.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/legacy-compat/src/builders/query.ts b/packages/legacy-compat/src/builders/query.ts index 956ac5bc894..b57a3b65f2a 100644 --- a/packages/legacy-compat/src/builders/query.ts +++ b/packages/legacy-compat/src/builders/query.ts @@ -18,6 +18,21 @@ export type QueryRequestInput = StoreRequestInput & { export 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`. + + + + @since x.x.x + @method query + @public + @param {String} type the name of the resource + @param {object} query a query to be used by the adapter + @param {Object} options optional, may include `adapterOptions` hash which will be passed to adapter.query + @return {QueryRequestInput} request config +*/ export function queryBuilder( type: TypeFromInstance, query: Record, From 9c2815aee7be5458441a9c457032380caa4f8115 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Thu, 4 Apr 2024 15:33:56 -0700 Subject: [PATCH 05/18] queryRecord --- packages/legacy-compat/src/builders.ts | 4 +- packages/legacy-compat/src/builders/query.ts | 47 ++++- .../integration/legacy-compat/query-test.ts | 198 ++++++++++++------ 3 files changed, 184 insertions(+), 65 deletions(-) diff --git a/packages/legacy-compat/src/builders.ts b/packages/legacy-compat/src/builders.ts index 8e3b4ffbf00..ca6c1b3dbbd 100644 --- a/packages/legacy-compat/src/builders.ts +++ b/packages/legacy-compat/src/builders.ts @@ -1,5 +1,5 @@ export { findRecordBuilder as findRecord } from './builders/find-record'; export type { FindRecordBuilderOptions, FindRecordRequestInput } from './builders/find-record'; -export { queryBuilder as query } from './builders/query'; -export type { QueryBuilderOptions, QueryRequestInput } from './builders/query'; +export { queryBuilder as query, queryRecordBuilder as queryRecord } from './builders/query'; +export type { QueryBuilderOptions, QueryRecordRequestInput, QueryRequestInput } from './builders/query'; diff --git a/packages/legacy-compat/src/builders/query.ts b/packages/legacy-compat/src/builders/query.ts index b57a3b65f2a..b83249d6969 100644 --- a/packages/legacy-compat/src/builders/query.ts +++ b/packages/legacy-compat/src/builders/query.ts @@ -23,8 +23,6 @@ export type QueryBuilderOptions = QueryOptions; 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`. - - @since x.x.x @method query @public @@ -61,3 +59,48 @@ export function queryBuilder( cacheOptions: { [SkipCache as symbol]: true }, }; } + +export type QueryRecordRequestInput = StoreRequestInput & { + op: 'queryRecord'; + data: { + type: string; + query: Record; + options: QueryBuilderOptions; + }; +}; + +/** + 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`. + + @since x.x.x + @method query + @public + @param {String} type the name of the resource + @param {object} query a query to be used by the adapter + @param {Object} options optional, may include `adapterOptions` hash which will be passed to adapter.query + @return {QueryRecordRequestInput} request config +*/ +export function queryRecordBuilder( + modelName: string, + query: Record, + options?: QueryOptions +): QueryRecordRequestInput { + assert(`You need to pass a model name to the queryRecord builder`, modelName); + 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 ${modelName}`, + typeof modelName === 'string' + ); + + return { + op: 'queryRecord', + data: { + type: normalizeModelName(modelName), + query, + options: options || {}, + }, + cacheOptions: { [SkipCache as symbol]: true }, + }; +} diff --git a/tests/main/tests/integration/legacy-compat/query-test.ts b/tests/main/tests/integration/legacy-compat/query-test.ts index 8265655cd1e..58ffd04457e 100644 --- a/tests/main/tests/integration/legacy-compat/query-test.ts +++ b/tests/main/tests/integration/legacy-compat/query-test.ts @@ -4,84 +4,160 @@ import { setupTest } from 'ember-qunit'; import type { CompatStore } from '@ember-data/legacy-compat'; import type { QueryBuilderOptions } from '@ember-data/legacy-compat/builders'; -import { query } from '@ember-data/legacy-compat/builders'; +import { query, queryRecord } from '@ember-data/legacy-compat/builders'; import Model, { attr } from '@ember-data/model'; module('Integration - legacy-compat/builders/query', function (hooks) { setupTest(hooks); - test('basic payload', async function (assert) { - class Post extends Model { - @attr declare name: string; - } - this.owner.register('model:post', Post); - this.owner.register( - 'adapter:application', - class Adapter { - query(store, type, queryObject) { - assert.step('adapter-query'); - return Promise.resolve({ - data: [ - { + module('query', function () { + test('basic payload', async function (assert) { + class Post extends Model { + @attr declare name: string; + } + 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) { + class Post extends Model { + @attr declare name: string; + } + 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(); + } } - static create() { - return new this(); - } - } - ); + ); - const store = this.owner.lookup('service:store') as CompatStore; - const { content: results } = await store.request(query('post', { id: '1' })); + const store = this.owner.lookup('service:store') as CompatStore; + const { content: post } = await store.request(queryRecord('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'); - }); + 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('query', function (assert) { - const result = query('post', { id: '1' }); - assert.deepEqual( - result, - { - op: 'query', - data: { - type: 'post', - query: { id: '1' }, - options: {}, + test('queryRecord', function (assert) { + const result = queryRecord('post', { id: '1' }); + assert.deepEqual( + result, + { + op: 'queryRecord', + data: { + type: 'post', + query: { id: '1' }, + options: {}, + }, + cacheOptions: {}, }, - cacheOptions: {}, - }, - `query works` - ); - }); + `queryRecord 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, + 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: {}, }, - cacheOptions: {}, - }, - `query works with options` - ); + `queryRecord works with options` + ); + }); }); }); From 07b082745c34b0a7b473b96b5bd5c543e9391498 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Thu, 4 Apr 2024 15:50:37 -0700 Subject: [PATCH 06/18] findAll --- packages/legacy-compat/src/builders.ts | 3 + .../legacy-compat/src/builders/find-all.ts | 53 ++++++++++++ .../legacy-compat/src/builders/find-record.ts | 2 +- packages/legacy-compat/src/builders/query.ts | 14 +-- .../legacy-compat/find-all-test.ts | 85 +++++++++++++++++++ 5 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 packages/legacy-compat/src/builders/find-all.ts create mode 100644 tests/main/tests/integration/legacy-compat/find-all-test.ts diff --git a/packages/legacy-compat/src/builders.ts b/packages/legacy-compat/src/builders.ts index ca6c1b3dbbd..bac3f3c5a7a 100644 --- a/packages/legacy-compat/src/builders.ts +++ b/packages/legacy-compat/src/builders.ts @@ -1,3 +1,6 @@ +export { findAllBuilder as findAll } from './builders/find-all'; +export type { FindAllBuilderOptions, FindAllRequestInput } from './builders/find-all'; + export { findRecordBuilder as findRecord } from './builders/find-record'; export type { FindRecordBuilderOptions, FindRecordRequestInput } from './builders/find-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..4127fc30ccd --- /dev/null +++ b/packages/legacy-compat/src/builders/find-all.ts @@ -0,0 +1,53 @@ +import { assert } from '@ember/debug'; + +import type { StoreRequestInput } from '@ember-data/store'; +import type { FindAllOptions } from '@ember-data/store/-types/q/store'; +import type { TypeFromInstance } from '@warp-drive/core-types/record'; +import { SkipCache } from '@warp-drive/core-types/request'; + +import { normalizeModelName } from './utils'; + +export type FindAllRequestInput = StoreRequestInput & { + op: 'findAll'; + data: { + type: string; + options: FindAllBuilderOptions; + }; +}; + +export type FindAllBuilderOptions = FindAllOptions; + +/** + This function builds a request config 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`. + + @since x.x.x + @method query + @public + @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.query + @return {FindAllRequestInput} request config +*/ +export function findAllBuilder(type: TypeFromInstance, options?: FindAllBuilderOptions): FindAllRequestInput; +export function findAllBuilder(type: string, options?: FindAllBuilderOptions): FindAllRequestInput; +export function findAllBuilder( + type: TypeFromInstance | 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 index 515e2613c7c..0a5c969c4b3 100644 --- a/packages/legacy-compat/src/builders/find-record.ts +++ b/packages/legacy-compat/src/builders/find-record.ts @@ -61,7 +61,7 @@ export type FindRecordBuilderOptions = Omit; @public @param {String|object} type - either a string representing the name of the resource or a ResourceIdentifier object containing both the type (a string) and the id (a string) for the record or an lid (a string) of an existing record @param {(String|Integer|Object)} id - optional object with options for the request only if the first param is a ResourceIdentifier, else the string id of the record to be retrieved - @param {Object} [options] - if the first param is a string this will be the optional options for the request. See examples for available options. + @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( diff --git a/packages/legacy-compat/src/builders/query.ts b/packages/legacy-compat/src/builders/query.ts index b83249d6969..a74a3f50425 100644 --- a/packages/legacy-compat/src/builders/query.ts +++ b/packages/legacy-compat/src/builders/query.ts @@ -28,19 +28,23 @@ export type QueryBuilderOptions = QueryOptions; @public @param {String} type the name of the resource @param {object} query a query to be used by the adapter - @param {Object} options optional, may include `adapterOptions` hash which will be passed to adapter.query + @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?: QueryOptions + options?: QueryBuilderOptions +): QueryRequestInput; +export function queryBuilder( + type: string, + query: Record, + options?: QueryBuilderOptions ): QueryRequestInput; -export function queryBuilder(type: string, query: Record, options?: QueryOptions): QueryRequestInput; export function queryBuilder( type: string, query: Record, - options: QueryOptions = {} + 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); @@ -85,7 +89,7 @@ export type QueryRecordRequestInput = StoreRequestInput & { export function queryRecordBuilder( modelName: string, query: Record, - options?: QueryOptions + options?: QueryBuilderOptions ): QueryRecordRequestInput { assert(`You need to pass a model name to the queryRecord builder`, modelName); assert(`You need to pass a query hash to the queryRecord builder`, query); 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..eb153284bae --- /dev/null +++ b/tests/main/tests/integration/legacy-compat/find-all-test.ts @@ -0,0 +1,85 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import type { CompatStore } from '@ember-data/legacy-compat'; +import type { QueryBuilderOptions } from '@ember-data/legacy-compat/builders'; +import { findAll } from '@ember-data/legacy-compat/builders'; +import Model, { attr } from '@ember-data/model'; + +module('Integration - legacy-compat/builders/findAll', function (hooks) { + setupTest(hooks); + + test('basic payload', async function (assert) { + class Post extends Model { + @attr declare name: string; + } + 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 = { + whatever: true, + adapterOptions: {}, + }; + const result = findAll('post', options); + assert.deepEqual( + result, + { + op: 'findAll', + data: { + type: 'post', + options, + }, + cacheOptions: {}, + }, + `findAll works with options` + ); + }); +}); From ac7af5ec15870f79ecd0aa0548809bf858672678 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Thu, 4 Apr 2024 16:19:15 -0700 Subject: [PATCH 07/18] saveRecord --- .../legacy-compat/src/builders/save-record.ts | 92 +++++++++++++++++++ .../legacy-network-handler.ts | 2 + packages/store/src/-private/store-service.ts | 5 +- packages/store/src/index.ts | 2 +- 4 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 packages/legacy-compat/src/builders/save-record.ts 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..74fdbc544ae --- /dev/null +++ b/packages/legacy-compat/src/builders/save-record.ts @@ -0,0 +1,92 @@ +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 { SkipCache } from '@warp-drive/core-types/request'; + +export type SaveRecordRequestInput = StoreRequestInput & { + op: 'createRecord' | 'deleteRecord' | 'updateRecord'; + data: { + record: StableRecordIdentifier; + options: SaveRecordBuilderOptions; + }; + records: [StableRecordIdentifier]; +}; + +export 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); +} + +/** + * FIXME: Docs + This function builds a request config 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`. + + @since x.x.x + @method query + @public + @param {String} type the name of the resource + @param {object} query a query to be used by the adapter + @param {SaveRecordBuilderOptions} options optional, may include `adapterOptions` hash which will be passed to adapter.query + @return {SaveRecordRequestInput} request config +*/ +export function saveRecordBuilder(record: T, options: Record = {}): SaveRecordRequestInput { + 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`); + } + // TODO we used to check if the record was destroyed here + 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 }, + }; +} + +/* + +TODO: +* [] test this +* [] make sure nothing fails bc of willCommit change +* [] cargo cult jsdoc setup from json-api/src/-private/builders/query.ts + +*/ 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 bd3b0805289..b0ded4a57ad 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 @@ -180,6 +180,8 @@ function saveRecord(context: StoreRequestContext): Promise { upgradeStore(store); + store.cache.willCommit(identifier, context); + const saveOptions = Object.assign( { [SaveOp]: operation as 'updateRecord' | 'deleteRecord' | 'createRecord' }, options diff --git a/packages/store/src/-private/store-service.ts b/packages/store/src/-private/store-service.ts index c5fc01364ab..d893d784c65 100644 --- a/packages/store/src/-private/store-service.ts +++ b/packages/store/src/-private/store-service.ts @@ -2121,7 +2121,7 @@ class Store extends EmberObject { if (DEBUG) { assertDestroyingStore(this, 'saveRecord'); } - assert(`Unable to initate save for a record in a disconnected state`, storeFor(record)); + assert(`Unable to initiate save for a record in a disconnected state`, storeFor(record)); const identifier = recordIdentifierFor(record); const cache = this.cache; @@ -2160,9 +2160,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/packages/store/src/index.ts b/packages/store/src/index.ts index e194e086eb2..e6711bdb98a 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -120,7 +120,7 @@ * * ### Presenting Data from the Cache * - * Now that we have a source and a cach for our data, we need to configure how + * Now that we have a source and a cache for our data, we need to configure how * the Store delivers that data back to our application. We do this via the hook * [instantiateRecord](https://api.emberjs.com/ember-data/release/classes/Store/methods/instantiateRecord%20(hook)?anchor=instantiateRecord%20(hook)), * which allows us to transform the data for a resource before handing it to the application. From 80890238b3f8bfd5cd2576a4f5adac88476ba29b Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Fri, 5 Apr 2024 11:09:35 -0700 Subject: [PATCH 08/18] saveRecord tests --- packages/legacy-compat/src/builders.ts | 3 + .../legacy-compat/src/builders/save-record.ts | 2 - .../legacy-compat/save-record-test.ts | 292 ++++++++++++++++++ 3 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 tests/main/tests/integration/legacy-compat/save-record-test.ts diff --git a/packages/legacy-compat/src/builders.ts b/packages/legacy-compat/src/builders.ts index bac3f3c5a7a..d52e599c292 100644 --- a/packages/legacy-compat/src/builders.ts +++ b/packages/legacy-compat/src/builders.ts @@ -6,3 +6,6 @@ export type { FindRecordBuilderOptions, FindRecordRequestInput } from './builder export { queryBuilder as query, queryRecordBuilder as queryRecord } from './builders/query'; export type { QueryBuilderOptions, QueryRecordRequestInput, QueryRequestInput } from './builders/query'; + +export { saveRecordBuilder as saveRecord } from './builders/save-record'; +export type { SaveRecordBuilderOptions, SaveRecordRequestInput } from './builders/save-record'; diff --git a/packages/legacy-compat/src/builders/save-record.ts b/packages/legacy-compat/src/builders/save-record.ts index 74fdbc544ae..b9ba3d345c9 100644 --- a/packages/legacy-compat/src/builders/save-record.ts +++ b/packages/legacy-compat/src/builders/save-record.ts @@ -85,8 +85,6 @@ export function saveRecordBuilder(record: T, options: Record /* TODO: -* [] test this -* [] make sure nothing fails bc of willCommit change * [] cargo cult jsdoc setup from json-api/src/-private/builders/query.ts */ 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..44311c02c80 --- /dev/null +++ b/tests/main/tests/integration/legacy-compat/save-record-test.ts @@ -0,0 +1,292 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import type { CompatStore } from '@ember-data/legacy-compat'; +import type { SaveRecordBuilderOptions } from '@ember-data/legacy-compat/builders'; +import { saveRecord } from '@ember-data/legacy-compat/builders'; +import Model, { attr } from '@ember-data/model'; +import { recordIdentifierFor } from '@ember-data/store'; + +class Post extends Model { + @attr declare name: string; +} + +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 = 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 = 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', + }, + }, + }) as Post; + 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 = store.push({ + data: { + id: '1', + type: 'post', + attributes: { + name: 'Krystan rules, you drool', + }, + }, + }) as Post; + 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 = store.push({ + data: { + id: '1', + type: 'post', + attributes: { + name: 'Krystan rules, you drool', + }, + }, + }) as Post; + 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', + }, + }, + }) as Post; + 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, 'Krystan rules, you drool', 'post has correct name'); + assert.true(savedPost.isDeleted, 'post isDeleted'); + assert.verifySteps(['adapter-updateRecord'], 'adapter-updateRecord was called'); + }); + + test('saveRecord', function (assert) { + const store = this.owner.lookup('service:store') as CompatStore; + const existingPost = store.push({ + data: { + id: '1', + type: 'post', + attributes: { + name: 'Krystan rules, you drool', + }, + }, + }) as Post; + 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 = store.push({ + data: { + id: '1', + type: 'post', + attributes: { + name: 'Krystan rules, you drool', + }, + }, + }) as Post; + 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` + ); + }); + }); +}); From f3ab5c1ac76cce483f5a6688664aaeb9ef1c1e02 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Fri, 5 Apr 2024 12:09:37 -0700 Subject: [PATCH 09/18] Docs pass --- packages/legacy-compat/src/builders.ts | 10 +++++++ .../legacy-compat/src/builders/find-all.ts | 14 ++++++---- .../legacy-compat/src/builders/find-record.ts | 14 +++++----- packages/legacy-compat/src/builders/query.ts | 19 ++++++++----- .../legacy-compat/src/builders/save-record.ts | 27 ++++++++----------- 5 files changed, 50 insertions(+), 34 deletions(-) diff --git a/packages/legacy-compat/src/builders.ts b/packages/legacy-compat/src/builders.ts index d52e599c292..3170b263023 100644 --- a/packages/legacy-compat/src/builders.ts +++ b/packages/legacy-compat/src/builders.ts @@ -1,3 +1,13 @@ +/** + 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. + + @module @ember-data/legacy-compat/builders + @main @ember-data/legacy-compat/builders +*/ + export { findAllBuilder as findAll } from './builders/find-all'; export type { FindAllBuilderOptions, FindAllRequestInput } from './builders/find-all'; diff --git a/packages/legacy-compat/src/builders/find-all.ts b/packages/legacy-compat/src/builders/find-all.ts index 4127fc30ccd..aec058fb0ff 100644 --- a/packages/legacy-compat/src/builders/find-all.ts +++ b/packages/legacy-compat/src/builders/find-all.ts @@ -1,3 +1,6 @@ +/** + * @module @ember-data/legacy-compat/builders + */ import { assert } from '@ember/debug'; import type { StoreRequestInput } from '@ember-data/store'; @@ -18,16 +21,17 @@ export type FindAllRequestInput = StoreRequestInput & { export type FindAllBuilderOptions = FindAllOptions; /** - This function builds a request config for the given type. + 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`. - @since x.x.x - @method query + @method findAll @public - @param {String} type the name of the resource + @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.query + @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; diff --git a/packages/legacy-compat/src/builders/find-record.ts b/packages/legacy-compat/src/builders/find-record.ts index 0a5c969c4b3..ba78ff7655a 100644 --- a/packages/legacy-compat/src/builders/find-record.ts +++ b/packages/legacy-compat/src/builders/find-record.ts @@ -1,3 +1,6 @@ +/** + * @module @ember-data/legacy-compat/builders + */ import { assert } from '@ember/debug'; import type { StoreRequestInput } from '@ember-data/store'; @@ -20,7 +23,7 @@ export type FindRecordRequestInput = StoreRequestInput & { export type FindRecordBuilderOptions = Omit; /** - This function builds a request config for a given identifier or type and id combination. + 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). @@ -54,13 +57,12 @@ export type FindRecordBuilderOptions = Omit; } ``` - - - @since x.x.x @method findRecord @public - @param {String|object} type - either a string representing the name of the resource or a ResourceIdentifier object containing both the type (a string) and the id (a string) for the record or an lid (a string) of an existing record - @param {(String|Integer|Object)} id - optional object with options for the request only if the first param is a ResourceIdentifier, else the string id of the record to be retrieved + @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 */ diff --git a/packages/legacy-compat/src/builders/query.ts b/packages/legacy-compat/src/builders/query.ts index a74a3f50425..96195c48694 100644 --- a/packages/legacy-compat/src/builders/query.ts +++ b/packages/legacy-compat/src/builders/query.ts @@ -1,3 +1,6 @@ +/** + * @module @ember-data/legacy-compat/builders + */ import { assert } from '@ember/debug'; import type { StoreRequestInput } from '@ember-data/store'; @@ -23,12 +26,13 @@ export type QueryBuilderOptions = QueryOptions; 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`. - @since x.x.x @method query @public - @param {String} type the name of the resource + @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 + @param {QueryBuilderOptions} [options] optional, may include `adapterOptions` hash which will be passed to adapter.query @return {QueryRequestInput} request config */ export function queryBuilder( @@ -78,12 +82,13 @@ export type QueryRecordRequestInput = StoreRequestInput & { 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`. - @since x.x.x - @method query + @method queryRecord @public - @param {String} type the name of the resource + @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 {Object} options optional, may include `adapterOptions` hash which will be passed to adapter.query + @param {QueryBuilderOptions} [options] optional, may include `adapterOptions` hash which will be passed to adapter.query @return {QueryRecordRequestInput} request config */ export function queryRecordBuilder( diff --git a/packages/legacy-compat/src/builders/save-record.ts b/packages/legacy-compat/src/builders/save-record.ts index b9ba3d345c9..d91c6b9c3e8 100644 --- a/packages/legacy-compat/src/builders/save-record.ts +++ b/packages/legacy-compat/src/builders/save-record.ts @@ -1,3 +1,6 @@ +/** + * @module @ember-data/legacy-compat/builders + */ import { assert } from '@ember/debug'; import { recordIdentifierFor, storeFor, type StoreRequestInput } from '@ember-data/store'; @@ -27,17 +30,16 @@ function resourceIsFullyDeleted(instanceCache: InstanceCache, identifier: Stable } /** - * FIXME: Docs - This function builds a request config 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`. + 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`. - @since x.x.x - @method query + @method saveRecord @public - @param {String} type the name of the resource - @param {object} query a query to be used by the adapter - @param {SaveRecordBuilderOptions} options optional, may include `adapterOptions` hash which will be passed to adapter.query + @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 { @@ -81,10 +83,3 @@ export function saveRecordBuilder(record: T, options: Record cacheOptions: { [SkipCache as symbol]: true }, }; } - -/* - -TODO: -* [] cargo cult jsdoc setup from json-api/src/-private/builders/query.ts - -*/ From e2773675758beeaedb97a1087f726f6580f69c64 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Fri, 5 Apr 2024 12:10:58 -0700 Subject: [PATCH 10/18] Fix test:docs --- tests/docs/fixtures/expected.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/docs/fixtures/expected.js b/tests/docs/fixtures/expected.js index 575b55ffdd7..fb8fc049772 100644 --- a/tests/docs/fixtures/expected.js +++ b/tests/docs/fixtures/expected.js @@ -13,6 +13,7 @@ module.exports = { '@ember-data/json-api', '@ember-data/json-api/request', '@ember-data/legacy-compat', + '@ember-data/legacy-compat/builders', '@ember-data/model', '@ember-data/request', '@ember-data/request-utils', @@ -76,6 +77,11 @@ module.exports = { '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_NON_STRICT_TYPES', '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_NON_UNIQUE_PAYLOADS', '(public) @ember-data/deprecations CurrentDeprecations#DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE', + '(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', From 910f7e94973e334cc07705f7fc6863077a89d80f Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Mon, 8 Apr 2024 12:55:41 -0700 Subject: [PATCH 11/18] Fix lint --- packages/store/src/-private/store-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/store/src/-private/store-service.ts b/packages/store/src/-private/store-service.ts index d893d784c65..1b4e425cb80 100644 --- a/packages/store/src/-private/store-service.ts +++ b/packages/store/src/-private/store-service.ts @@ -33,7 +33,7 @@ import type { ModelSchema } from '../-types/q/ds-model'; import type { OpaqueRecordInstance } from '../-types/q/record-instance'; import type { SchemaService } from '../-types/q/schema-service'; import type { FindAllOptions, FindRecordOptions, QueryOptions } from '../-types/q/store'; -import type { LifetimesService, StoreRequestContext, StoreRequestInput } from './cache-handler'; +import type { LifetimesService, StoreRequestInput } from './cache-handler'; import { IdentifierCache } from './caches/identifier-cache'; import { InstanceCache, From 22178c94378b1823a7b5146bbf6abbeeb202b314 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Mon, 8 Apr 2024 13:02:06 -0700 Subject: [PATCH 12/18] Remove outdated TODO --- packages/legacy-compat/src/builders/save-record.ts | 1 - packages/store/src/-private/store-service.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/legacy-compat/src/builders/save-record.ts b/packages/legacy-compat/src/builders/save-record.ts index d91c6b9c3e8..c1d94206abb 100644 --- a/packages/legacy-compat/src/builders/save-record.ts +++ b/packages/legacy-compat/src/builders/save-record.ts @@ -52,7 +52,6 @@ export function saveRecordBuilder(record: T, options: Record // but just in case we throw here to prevent bad things. throw new Error(`Record Is Disconnected`); } - // TODO we used to check if the record was destroyed here assert( `Cannot initiate a save request for an unloaded record: ${identifier.lid}`, store._instanceCache.recordIsLoaded(identifier) diff --git a/packages/store/src/-private/store-service.ts b/packages/store/src/-private/store-service.ts index 1b4e425cb80..07155cfa5da 100644 --- a/packages/store/src/-private/store-service.ts +++ b/packages/store/src/-private/store-service.ts @@ -2130,7 +2130,6 @@ class Store extends EmberObject { // but just in case we reject here to prevent bad things. return Promise.reject(new Error(`Record Is Disconnected`)); } - // TODO we used to check if the record was destroyed here assert( `Cannot initiate a save request for an unloaded record: ${identifier.lid}`, this._instanceCache.recordIsLoaded(identifier) From e8c46ab9d2edc432b0c41eb6e0a3d740563859af Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Mon, 8 Apr 2024 13:10:00 -0700 Subject: [PATCH 13/18] Deprecate --- packages/legacy-compat/src/builders.ts | 2 ++ packages/legacy-compat/src/builders/find-all.ts | 5 +++++ packages/legacy-compat/src/builders/find-record.ts | 5 +++++ packages/legacy-compat/src/builders/query.ts | 10 ++++++++++ packages/legacy-compat/src/builders/save-record.ts | 5 +++++ 5 files changed, 27 insertions(+) diff --git a/packages/legacy-compat/src/builders.ts b/packages/legacy-compat/src/builders.ts index 3170b263023..14eb3fb9327 100644 --- a/packages/legacy-compat/src/builders.ts +++ b/packages/legacy-compat/src/builders.ts @@ -3,9 +3,11 @@ 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'; diff --git a/packages/legacy-compat/src/builders/find-all.ts b/packages/legacy-compat/src/builders/find-all.ts index aec058fb0ff..3b6eccde205 100644 --- a/packages/legacy-compat/src/builders/find-all.ts +++ b/packages/legacy-compat/src/builders/find-all.ts @@ -25,6 +25,11 @@ export type FindAllBuilderOptions = FindAllOptions; 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. + + @deprecated @method findAll @public @static diff --git a/packages/legacy-compat/src/builders/find-record.ts b/packages/legacy-compat/src/builders/find-record.ts index ba78ff7655a..36b4c43cfb5 100644 --- a/packages/legacy-compat/src/builders/find-record.ts +++ b/packages/legacy-compat/src/builders/find-record.ts @@ -57,6 +57,11 @@ export type FindRecordBuilderOptions = Omit; } ``` + 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. + + @deprecated @method findRecord @public @static diff --git a/packages/legacy-compat/src/builders/query.ts b/packages/legacy-compat/src/builders/query.ts index 96195c48694..004ed855956 100644 --- a/packages/legacy-compat/src/builders/query.ts +++ b/packages/legacy-compat/src/builders/query.ts @@ -26,6 +26,11 @@ export type QueryBuilderOptions = QueryOptions; 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. + + @deprecated @method query @public @static @@ -82,6 +87,11 @@ export type QueryRecordRequestInput = StoreRequestInput & { 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. + + @deprecated @method queryRecord @public @static diff --git a/packages/legacy-compat/src/builders/save-record.ts b/packages/legacy-compat/src/builders/save-record.ts index c1d94206abb..5cf495a1a29 100644 --- a/packages/legacy-compat/src/builders/save-record.ts +++ b/packages/legacy-compat/src/builders/save-record.ts @@ -34,6 +34,11 @@ function resourceIsFullyDeleted(instanceCache: InstanceCache, identifier: Stable 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. + + @deprecated @method saveRecord @public @static From d96b2a841f3c72e95b2b20ba2fc2bf14cc12819d Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Mon, 8 Apr 2024 13:58:46 -0700 Subject: [PATCH 14/18] Fix test --- .../main/tests/integration/legacy-compat/save-record-test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 44311c02c80..25e6d14b6cc 100644 --- a/tests/main/tests/integration/legacy-compat/save-record-test.ts +++ b/tests/main/tests/integration/legacy-compat/save-record-test.ts @@ -222,8 +222,8 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { 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.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'); }); From 0935b7016e1e8ed528c3aa5aaf852a256495ac21 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Mon, 8 Apr 2024 15:39:58 -0700 Subject: [PATCH 15/18] Don't export options and return types --- packages/legacy-compat/src/builders.ts | 4 ---- packages/legacy-compat/src/builders/find-all.ts | 4 ++-- packages/legacy-compat/src/builders/find-record.ts | 4 ++-- packages/legacy-compat/src/builders/query.ts | 6 +++--- packages/legacy-compat/src/builders/save-record.ts | 4 ++-- .../tests/integration/legacy-compat/find-all-test.ts | 9 ++++++--- .../tests/integration/legacy-compat/find-record-test.ts | 3 ++- tests/main/tests/integration/legacy-compat/query-test.ts | 6 ++++-- .../tests/integration/legacy-compat/save-record-test.ts | 3 ++- 9 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/legacy-compat/src/builders.ts b/packages/legacy-compat/src/builders.ts index 14eb3fb9327..5f5f457c4ad 100644 --- a/packages/legacy-compat/src/builders.ts +++ b/packages/legacy-compat/src/builders.ts @@ -11,13 +11,9 @@ */ export { findAllBuilder as findAll } from './builders/find-all'; -export type { FindAllBuilderOptions, FindAllRequestInput } from './builders/find-all'; export { findRecordBuilder as findRecord } from './builders/find-record'; -export type { FindRecordBuilderOptions, FindRecordRequestInput } from './builders/find-record'; export { queryBuilder as query, queryRecordBuilder as queryRecord } from './builders/query'; -export type { QueryBuilderOptions, QueryRecordRequestInput, QueryRequestInput } from './builders/query'; export { saveRecordBuilder as saveRecord } from './builders/save-record'; -export type { SaveRecordBuilderOptions, SaveRecordRequestInput } from './builders/save-record'; diff --git a/packages/legacy-compat/src/builders/find-all.ts b/packages/legacy-compat/src/builders/find-all.ts index 3b6eccde205..956d2768782 100644 --- a/packages/legacy-compat/src/builders/find-all.ts +++ b/packages/legacy-compat/src/builders/find-all.ts @@ -10,7 +10,7 @@ import { SkipCache } from '@warp-drive/core-types/request'; import { normalizeModelName } from './utils'; -export type FindAllRequestInput = StoreRequestInput & { +type FindAllRequestInput = StoreRequestInput & { op: 'findAll'; data: { type: string; @@ -18,7 +18,7 @@ export type FindAllRequestInput = StoreRequestInput & { }; }; -export type FindAllBuilderOptions = FindAllOptions; +type FindAllBuilderOptions = FindAllOptions; /** This function builds a request config to perform a `findAll` request for the given type. diff --git a/packages/legacy-compat/src/builders/find-record.ts b/packages/legacy-compat/src/builders/find-record.ts index 36b4c43cfb5..85a9bc21356 100644 --- a/packages/legacy-compat/src/builders/find-record.ts +++ b/packages/legacy-compat/src/builders/find-record.ts @@ -12,7 +12,7 @@ import type { ResourceIdentifierObject } from '@warp-drive/core-types/spec/raw'; import { isMaybeIdentifier, normalizeModelName } from './utils'; -export type FindRecordRequestInput = StoreRequestInput & { +type FindRecordRequestInput = StoreRequestInput & { op: 'findRecord'; data: { record: ResourceIdentifierObject; @@ -20,7 +20,7 @@ export type FindRecordRequestInput = StoreRequestInput & { }; }; -export type FindRecordBuilderOptions = Omit; +type FindRecordBuilderOptions = Omit; /** This function builds a request config to find the record for a given identifier or type and id combination. diff --git a/packages/legacy-compat/src/builders/query.ts b/packages/legacy-compat/src/builders/query.ts index 004ed855956..b3fe8000859 100644 --- a/packages/legacy-compat/src/builders/query.ts +++ b/packages/legacy-compat/src/builders/query.ts @@ -10,7 +10,7 @@ import { SkipCache } from '@warp-drive/core-types/request'; import { normalizeModelName } from './utils'; -export type QueryRequestInput = StoreRequestInput & { +type QueryRequestInput = StoreRequestInput & { op: 'query'; data: { type: string; @@ -19,7 +19,7 @@ export type QueryRequestInput = StoreRequestInput & { }; }; -export type QueryBuilderOptions = QueryOptions; +type QueryBuilderOptions = QueryOptions; /** This function builds a request config for a given type and query object. @@ -73,7 +73,7 @@ export function queryBuilder( }; } -export type QueryRecordRequestInput = StoreRequestInput & { +type QueryRecordRequestInput = StoreRequestInput & { op: 'queryRecord'; data: { type: string; diff --git a/packages/legacy-compat/src/builders/save-record.ts b/packages/legacy-compat/src/builders/save-record.ts index 5cf495a1a29..8cc0c2c1a58 100644 --- a/packages/legacy-compat/src/builders/save-record.ts +++ b/packages/legacy-compat/src/builders/save-record.ts @@ -9,7 +9,7 @@ import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { Cache } from '@warp-drive/core-types/cache'; import { SkipCache } from '@warp-drive/core-types/request'; -export type SaveRecordRequestInput = StoreRequestInput & { +type SaveRecordRequestInput = StoreRequestInput & { op: 'createRecord' | 'deleteRecord' | 'updateRecord'; data: { record: StableRecordIdentifier; @@ -18,7 +18,7 @@ export type SaveRecordRequestInput = StoreRequestInput & { records: [StableRecordIdentifier]; }; -export type SaveRecordBuilderOptions = Record; +type SaveRecordBuilderOptions = Record; function _resourceIsFullDeleted(identifier: StableRecordIdentifier, cache: Cache): boolean { return cache.isDeletionCommitted(identifier) || (cache.isNew(identifier) && cache.isDeleted(identifier)); diff --git a/tests/main/tests/integration/legacy-compat/find-all-test.ts b/tests/main/tests/integration/legacy-compat/find-all-test.ts index eb153284bae..bf9275a0362 100644 --- a/tests/main/tests/integration/legacy-compat/find-all-test.ts +++ b/tests/main/tests/integration/legacy-compat/find-all-test.ts @@ -3,10 +3,11 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import type { CompatStore } from '@ember-data/legacy-compat'; -import type { QueryBuilderOptions } from '@ember-data/legacy-compat/builders'; import { findAll } from '@ember-data/legacy-compat/builders'; import Model, { attr } from '@ember-data/model'; +type FindAllBuilderOptions = Exclude[1], undefined>; + module('Integration - legacy-compat/builders/findAll', function (hooks) { setupTest(hooks); @@ -64,8 +65,10 @@ module('Integration - legacy-compat/builders/findAll', function (hooks) { }); test('findAll with options', function (assert) { - const options: Required = { - whatever: true, + const options: Required = { + reload: true, + backgroundReload: false, + include: 'author,comments', adapterOptions: {}, }; const result = findAll('post', 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 index dbc6cc47ecf..2471cc908ff 100644 --- a/tests/main/tests/integration/legacy-compat/find-record-test.ts +++ b/tests/main/tests/integration/legacy-compat/find-record-test.ts @@ -3,11 +3,12 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import type { CompatStore } from '@ember-data/legacy-compat'; -import type { FindRecordBuilderOptions } from '@ember-data/legacy-compat/builders'; 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'; +type FindRecordBuilderOptions = Exclude[1], undefined>; + module('Integration - legacy-compat/builders/findRecord', function (hooks) { setupTest(hooks); diff --git a/tests/main/tests/integration/legacy-compat/query-test.ts b/tests/main/tests/integration/legacy-compat/query-test.ts index 58ffd04457e..83916bbb381 100644 --- a/tests/main/tests/integration/legacy-compat/query-test.ts +++ b/tests/main/tests/integration/legacy-compat/query-test.ts @@ -3,10 +3,12 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import type { CompatStore } from '@ember-data/legacy-compat'; -import type { QueryBuilderOptions } from '@ember-data/legacy-compat/builders'; import { query, queryRecord } from '@ember-data/legacy-compat/builders'; import Model, { attr } from '@ember-data/model'; +type QueryBuilderOptions = Exclude[2], undefined>; +type QueryRecordBuilderOptions = Exclude[2], undefined>; + module('Integration - legacy-compat/builders/query', function (hooks) { setupTest(hooks); @@ -140,7 +142,7 @@ module('Integration - legacy-compat/builders/query', function (hooks) { }); test('queryRecord with options', function (assert) { - const options: Required = { + const options: Required = { whatever: true, adapterOptions: {}, }; 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 25e6d14b6cc..00ba45718e8 100644 --- a/tests/main/tests/integration/legacy-compat/save-record-test.ts +++ b/tests/main/tests/integration/legacy-compat/save-record-test.ts @@ -3,7 +3,6 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import type { CompatStore } from '@ember-data/legacy-compat'; -import type { SaveRecordBuilderOptions } from '@ember-data/legacy-compat/builders'; import { saveRecord } from '@ember-data/legacy-compat/builders'; import Model, { attr } from '@ember-data/model'; import { recordIdentifierFor } from '@ember-data/store'; @@ -12,6 +11,8 @@ class Post extends Model { @attr declare name: string; } +type SaveRecordBuilderOptions = Exclude[1], undefined>; + module('Integration - legacy-compat/builders/saveRecord', function (hooks) { setupTest(hooks); From 3862dd27dea219088879c38f6e68e0a0af54e75a Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Mon, 8 Apr 2024 15:41:06 -0700 Subject: [PATCH 16/18] Move @deprecated under @method --- packages/legacy-compat/src/builders/find-all.ts | 2 +- packages/legacy-compat/src/builders/find-record.ts | 2 +- packages/legacy-compat/src/builders/query.ts | 4 ++-- packages/legacy-compat/src/builders/save-record.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/legacy-compat/src/builders/find-all.ts b/packages/legacy-compat/src/builders/find-all.ts index 956d2768782..e2f0eddbb74 100644 --- a/packages/legacy-compat/src/builders/find-all.ts +++ b/packages/legacy-compat/src/builders/find-all.ts @@ -29,8 +29,8 @@ type FindAllBuilderOptions = FindAllOptions; 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. - @deprecated @method findAll + @deprecated @public @static @for @ember-data/legacy-compat/builders diff --git a/packages/legacy-compat/src/builders/find-record.ts b/packages/legacy-compat/src/builders/find-record.ts index 85a9bc21356..20274772cfc 100644 --- a/packages/legacy-compat/src/builders/find-record.ts +++ b/packages/legacy-compat/src/builders/find-record.ts @@ -61,8 +61,8 @@ type FindRecordBuilderOptions = Omit; 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. - @deprecated @method findRecord + @deprecated @public @static @for @ember-data/legacy-compat/builders diff --git a/packages/legacy-compat/src/builders/query.ts b/packages/legacy-compat/src/builders/query.ts index b3fe8000859..53c3361695f 100644 --- a/packages/legacy-compat/src/builders/query.ts +++ b/packages/legacy-compat/src/builders/query.ts @@ -30,8 +30,8 @@ type QueryBuilderOptions = QueryOptions; 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. - @deprecated @method query + @deprecated @public @static @for @ember-data/legacy-compat/builders @@ -91,8 +91,8 @@ type QueryRecordRequestInput = StoreRequestInput & { 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. - @deprecated @method queryRecord + @deprecated @public @static @for @ember-data/legacy-compat/builders diff --git a/packages/legacy-compat/src/builders/save-record.ts b/packages/legacy-compat/src/builders/save-record.ts index 8cc0c2c1a58..8f5059e887a 100644 --- a/packages/legacy-compat/src/builders/save-record.ts +++ b/packages/legacy-compat/src/builders/save-record.ts @@ -38,8 +38,8 @@ function resourceIsFullyDeleted(instanceCache: InstanceCache, identifier: Stable 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. - @deprecated @method saveRecord + @deprecated @public @static @for @ember-data/legacy-compat/builders From 0a84c5bd2e642462c6e228efd5e083e68bdad582 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Mon, 8 Apr 2024 20:26:28 -0700 Subject: [PATCH 17/18] Make builder return types generic --- .../legacy-compat/src/builders/find-all.ts | 18 ++++----- .../legacy-compat/src/builders/find-record.ts | 22 +++++----- packages/legacy-compat/src/builders/query.ts | 40 ++++++++++++------- .../legacy-compat/src/builders/save-record.ts | 12 ++++-- .../legacy-compat/find-all-test.ts | 13 +++--- .../legacy-compat/find-record-test.ts | 23 ++++++----- .../integration/legacy-compat/query-test.ts | 24 +++++------ .../legacy-compat/save-record-test.ts | 32 ++++++++------- 8 files changed, 103 insertions(+), 81 deletions(-) diff --git a/packages/legacy-compat/src/builders/find-all.ts b/packages/legacy-compat/src/builders/find-all.ts index e2f0eddbb74..39045c07a28 100644 --- a/packages/legacy-compat/src/builders/find-all.ts +++ b/packages/legacy-compat/src/builders/find-all.ts @@ -5,15 +5,15 @@ import { assert } from '@ember/debug'; import type { StoreRequestInput } from '@ember-data/store'; import type { FindAllOptions } from '@ember-data/store/-types/q/store'; -import type { TypeFromInstance } from '@warp-drive/core-types/record'; +import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; import { SkipCache } from '@warp-drive/core-types/request'; import { normalizeModelName } from './utils'; -type FindAllRequestInput = StoreRequestInput & { +type FindAllRequestInput = StoreRequestInput & { op: 'findAll'; data: { - type: string; + type: T; options: FindAllBuilderOptions; }; }; @@ -39,12 +39,12 @@ type FindAllBuilderOptions = FindAllOptions; @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; -export function findAllBuilder(type: string, options?: FindAllBuilderOptions): FindAllRequestInput; -export function findAllBuilder( - type: TypeFromInstance | string, - options: FindAllBuilderOptions = {} -): FindAllRequestInput { +export function findAllBuilder( + type: TypeFromInstance, + options?: FindAllBuilderOptions +): FindAllRequestInput>; +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 20274772cfc..024f9580eb0 100644 --- a/packages/legacy-compat/src/builders/find-record.ts +++ b/packages/legacy-compat/src/builders/find-record.ts @@ -6,16 +6,16 @@ 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 { TypeFromInstance } from '@warp-drive/core-types/record'; +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 { isMaybeIdentifier, normalizeModelName } from './utils'; -type FindRecordRequestInput = StoreRequestInput & { +type FindRecordRequestInput = StoreRequestInput & { op: 'findRecord'; data: { - record: ResourceIdentifierObject; + record: ResourceIdentifierObject; options: FindRecordBuilderOptions; }; }; @@ -71,29 +71,29 @@ type FindRecordBuilderOptions = Omit; @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( +export function findRecordBuilder( resource: TypeFromInstance, id: string, options?: FindRecordBuilderOptions -): FindRecordRequestInput; +): FindRecordRequestInput>; export function findRecordBuilder( resource: string, id: string, options?: FindRecordBuilderOptions -): FindRecordRequestInput; -export function findRecordBuilder( +): FindRecordRequestInput; +export function findRecordBuilder( resource: ResourceIdentifierObject>, options?: FindRecordBuilderOptions -): FindRecordRequestInput; +): FindRecordRequestInput>; export function findRecordBuilder( resource: ResourceIdentifierObject, options?: FindRecordBuilderOptions -): FindRecordRequestInput; +): FindRecordRequestInput; export function findRecordBuilder( resource: string | ResourceIdentifierObject, - idOrOptions?: string | BaseFinderOptions, + 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 53c3361695f..25794265e3a 100644 --- a/packages/legacy-compat/src/builders/query.ts +++ b/packages/legacy-compat/src/builders/query.ts @@ -5,15 +5,15 @@ import { assert } from '@ember/debug'; import type { StoreRequestInput } from '@ember-data/store'; import type { QueryOptions } from '@ember-data/store/-types/q/store'; -import type { TypeFromInstance } from '@warp-drive/core-types/record'; +import type { TypedRecordInstance, TypeFromInstance } from '@warp-drive/core-types/record'; import { SkipCache } from '@warp-drive/core-types/request'; import { normalizeModelName } from './utils'; -type QueryRequestInput = StoreRequestInput & { +type QueryRequestInput = StoreRequestInput & { op: 'query'; data: { - type: string; + type: T; query: Record; options: QueryBuilderOptions; }; @@ -40,21 +40,21 @@ type QueryBuilderOptions = QueryOptions; @param {QueryBuilderOptions} [options] optional, may include `adapterOptions` hash which will be passed to adapter.query @return {QueryRequestInput} request config */ -export function queryBuilder( +export function queryBuilder( type: TypeFromInstance, query: Record, options?: QueryBuilderOptions -): QueryRequestInput; +): QueryRequestInput>; 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,10 +73,10 @@ export function queryBuilder( }; } -type QueryRecordRequestInput = StoreRequestInput & { +type QueryRecordRequestInput = StoreRequestInput & { op: 'queryRecord'; data: { - type: string; + type: T; query: Record; options: QueryBuilderOptions; }; @@ -101,22 +101,32 @@ type QueryRecordRequestInput = StoreRequestInput & { @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>; +export function queryRecordBuilder( + type: string, + query: Record, + options?: QueryBuilderOptions +): QueryRecordRequestInput; export function queryRecordBuilder( - modelName: string, + type: string, query: Record, options?: QueryBuilderOptions -): QueryRecordRequestInput { - assert(`You need to pass a model name to the queryRecord builder`, modelName); +): 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 ${modelName}`, - typeof modelName === 'string' + `Model name passed to the queryRecord builder must be a dasherized string instead of ${type}`, + typeof type === 'string' ); return { op: 'queryRecord', data: { - type: normalizeModelName(modelName), + type: normalizeModelName(type), query, options: options || {}, }, diff --git a/packages/legacy-compat/src/builders/save-record.ts b/packages/legacy-compat/src/builders/save-record.ts index 8f5059e887a..28850bc5e70 100644 --- a/packages/legacy-compat/src/builders/save-record.ts +++ b/packages/legacy-compat/src/builders/save-record.ts @@ -7,15 +7,16 @@ import { recordIdentifierFor, storeFor, type StoreRequestInput } from '@ember-da 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'; -type SaveRecordRequestInput = StoreRequestInput & { +type SaveRecordRequestInput = StoreRequestInput & { op: 'createRecord' | 'deleteRecord' | 'updateRecord'; data: { - record: StableRecordIdentifier; + record: StableRecordIdentifier; options: SaveRecordBuilderOptions; }; - records: [StableRecordIdentifier]; + records: [StableRecordIdentifier]; }; type SaveRecordBuilderOptions = Record; @@ -47,7 +48,10 @@ function resourceIsFullyDeleted(instanceCache: InstanceCache, identifier: Stable @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 { +export function saveRecordBuilder( + record: T, + options: Record = {} +): SaveRecordRequestInput> { const store = storeFor(record); assert(`Unable to initiate save for a record in a disconnected state`, store); const identifier = recordIdentifierFor(record); diff --git a/tests/main/tests/integration/legacy-compat/find-all-test.ts b/tests/main/tests/integration/legacy-compat/find-all-test.ts index bf9275a0362..ac1a417cea1 100644 --- a/tests/main/tests/integration/legacy-compat/find-all-test.ts +++ b/tests/main/tests/integration/legacy-compat/find-all-test.ts @@ -5,16 +5,19 @@ 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) { - class Post extends Model { - @attr declare name: string; - } this.owner.register('model:post', Post); this.owner.register( 'adapter:application', @@ -40,7 +43,7 @@ module('Integration - legacy-compat/builders/findAll', function (hooks) { ); const store = this.owner.lookup('service:store') as CompatStore; - const { content: results } = await store.request(findAll('post')); + 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'); @@ -49,7 +52,7 @@ module('Integration - legacy-compat/builders/findAll', function (hooks) { }); test('findAll', function (assert) { - const result = findAll('post'); + const result = findAll('post'); assert.deepEqual( result, { diff --git a/tests/main/tests/integration/legacy-compat/find-record-test.ts b/tests/main/tests/integration/legacy-compat/find-record-test.ts index 2471cc908ff..57dc134862e 100644 --- a/tests/main/tests/integration/legacy-compat/find-record-test.ts +++ b/tests/main/tests/integration/legacy-compat/find-record-test.ts @@ -6,16 +6,19 @@ 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) { - class Post extends Model { - @attr declare name: string; - } this.owner.register('model:post', Post); this.owner.register( 'adapter:application', @@ -39,7 +42,7 @@ module('Integration - legacy-compat/builders/findRecord', function (hooks) { ); const store = this.owner.lookup('service:store') as CompatStore; - const { content: post } = await store.request(findRecord('post', '1')); + 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'); @@ -47,7 +50,7 @@ module('Integration - legacy-compat/builders/findRecord', function (hooks) { }); test('findRecord by type+id', function (assert) { - const result = findRecord('post', '1'); + const result = findRecord('post', '1'); assert.deepEqual( result, { @@ -69,7 +72,7 @@ module('Integration - legacy-compat/builders/findRecord', function (hooks) { include: 'author,comments', adapterOptions: {}, }; - const result = findRecord('post', '1', options); + const result = findRecord('post', '1', options); assert.deepEqual( result, { @@ -91,12 +94,12 @@ module('Integration - legacy-compat/builders/findRecord', function (hooks) { }; await assert.expectAssertion(() => { // @ts-expect-error TS knows the options are invalid - findRecord('post', '1', invalidOptions); + 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' }); + const result = findRecord({ type: 'post', id: '1' }); assert.deepEqual( result, { @@ -118,7 +121,7 @@ module('Integration - legacy-compat/builders/findRecord', function (hooks) { include: 'author,comments', adapterOptions: {}, }; - const result = findRecord({ type: 'post', id: '1' }, options); + const result = findRecord({ type: 'post', id: '1' }, options); assert.deepEqual( result, { @@ -140,7 +143,7 @@ module('Integration - legacy-compat/builders/findRecord', function (hooks) { }; await assert.expectAssertion(() => { // @ts-expect-error TS knows the options are invalid - findRecord({ type: 'post', id: '1' }, invalidOptions); + 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 index 83916bbb381..07837eed052 100644 --- a/tests/main/tests/integration/legacy-compat/query-test.ts +++ b/tests/main/tests/integration/legacy-compat/query-test.ts @@ -5,18 +5,21 @@ 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) { - class Post extends Model { - @attr declare name: string; - } this.owner.register('model:post', Post); this.owner.register( 'adapter:application', @@ -42,7 +45,7 @@ module('Integration - legacy-compat/builders/query', function (hooks) { ); const store = this.owner.lookup('service:store') as CompatStore; - const { content: results } = await store.request(query('post', { id: '1' })); + 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'); @@ -51,7 +54,7 @@ module('Integration - legacy-compat/builders/query', function (hooks) { }); test('query', function (assert) { - const result = query('post', { id: '1' }); + const result = query('post', { id: '1' }); assert.deepEqual( result, { @@ -72,7 +75,7 @@ module('Integration - legacy-compat/builders/query', function (hooks) { whatever: true, adapterOptions: {}, }; - const result = query('post', { id: '1' }, options); + const result = query('post', { id: '1' }, options); assert.deepEqual( result, { @@ -91,9 +94,6 @@ module('Integration - legacy-compat/builders/query', function (hooks) { module('queryRecord', function () { test('basic payload', async function (assert) { - class Post extends Model { - @attr declare name: string; - } this.owner.register('model:post', Post); this.owner.register( 'adapter:application', @@ -117,7 +117,7 @@ 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'); @@ -125,7 +125,7 @@ module('Integration - legacy-compat/builders/query', function (hooks) { }); test('queryRecord', function (assert) { - const result = queryRecord('post', { id: '1' }); + const result = queryRecord('post', { id: '1' }); assert.deepEqual( result, { @@ -146,7 +146,7 @@ module('Integration - legacy-compat/builders/query', function (hooks) { whatever: true, adapterOptions: {}, }; - const result = queryRecord('post', { id: '1' }, options); + const result = queryRecord('post', { id: '1' }, options); assert.deepEqual( result, { 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 00ba45718e8..0a7c5c3f077 100644 --- a/tests/main/tests/integration/legacy-compat/save-record-test.ts +++ b/tests/main/tests/integration/legacy-compat/save-record-test.ts @@ -6,8 +6,10 @@ 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; } @@ -44,7 +46,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { ); const store = this.owner.lookup('service:store') as CompatStore; - const newPost = store.createRecord('post', { name: 'Krystan rules, you drool' }); + 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'); @@ -54,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 = store.createRecord('post', { name: 'Krystan rules, you drool' }); + const newPost: Post = store.createRecord('post', { name: 'Krystan rules, you drool' }); const identifier = recordIdentifierFor(newPost); const result = saveRecord(newPost); assert.deepEqual( @@ -78,7 +80,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { adapterOptions: {}, }; const store = this.owner.lookup('service:store') as CompatStore; - const newPost = store.createRecord('post', { name: 'Krystan rules, you drool' }); + const newPost: Post = store.createRecord('post', { name: 'Krystan rules, you drool' }); const identifier = recordIdentifierFor(newPost); const result = saveRecord(newPost, options); assert.deepEqual( @@ -113,7 +115,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { ); const store = this.owner.lookup('service:store') as CompatStore; - const existingPost = store.push({ + const existingPost: Post = store.push({ data: { id: '1', type: 'post', @@ -121,7 +123,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { name: 'Krystan rules, you drool', }, }, - }) as Post; + }); existingPost.deleteRecord(); const { content: savedPost } = await store.request(saveRecord(existingPost)); @@ -133,7 +135,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { test('saveRecord', function (assert) { const store = this.owner.lookup('service:store') as CompatStore; - const existingPost = store.push({ + const existingPost: Post = store.push({ data: { id: '1', type: 'post', @@ -141,7 +143,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { name: 'Krystan rules, you drool', }, }, - }) as Post; + }); existingPost.deleteRecord(); const identifier = recordIdentifierFor(existingPost); const result = saveRecord(existingPost); @@ -166,7 +168,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { adapterOptions: {}, }; const store = this.owner.lookup('service:store') as CompatStore; - const existingPost = store.push({ + const existingPost: Post = store.push({ data: { id: '1', type: 'post', @@ -174,7 +176,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { name: 'Krystan rules, you drool', }, }, - }) as Post; + }); existingPost.deleteRecord(); const identifier = recordIdentifierFor(existingPost); const result = saveRecord(existingPost, options); @@ -210,7 +212,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { ); const store = this.owner.lookup('service:store') as CompatStore; - const existingPost = store.push({ + const existingPost: Post = store.push({ data: { id: '1', type: 'post', @@ -218,7 +220,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { name: 'Krystan rules, you drool', }, }, - }) as Post; + }); existingPost.name = 'Chris drools, Krystan rules'; const { content: savedPost } = await store.request(saveRecord(existingPost)); @@ -230,7 +232,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { test('saveRecord', function (assert) { const store = this.owner.lookup('service:store') as CompatStore; - const existingPost = store.push({ + const existingPost: Post = store.push({ data: { id: '1', type: 'post', @@ -238,7 +240,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { name: 'Krystan rules, you drool', }, }, - }) as Post; + }); existingPost.name = 'Chris drools, Krystan rules'; const identifier = recordIdentifierFor(existingPost); const result = saveRecord(existingPost); @@ -263,7 +265,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { adapterOptions: {}, }; const store = this.owner.lookup('service:store') as CompatStore; - const existingPost = store.push({ + const existingPost: Post = store.push({ data: { id: '1', type: 'post', @@ -271,7 +273,7 @@ module('Integration - legacy-compat/builders/saveRecord', function (hooks) { name: 'Krystan rules, you drool', }, }, - }) as Post; + }); existingPost.name = 'Chris drools, Krystan rules'; const identifier = recordIdentifierFor(existingPost); const result = saveRecord(existingPost, options); From 20d4fc395b8178e426256323c308eaf2f2c958e9 Mon Sep 17 00:00:00 2001 From: Krystan HuffMenne Date: Mon, 8 Apr 2024 20:29:33 -0700 Subject: [PATCH 18/18] Remove Ember references from docs --- .../legacy-compat/src/builders/find-record.ts | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/legacy-compat/src/builders/find-record.ts b/packages/legacy-compat/src/builders/find-record.ts index 024f9580eb0..d1b1514eb99 100644 --- a/packages/legacy-compat/src/builders/find-record.ts +++ b/packages/legacy-compat/src/builders/find-record.ts @@ -29,15 +29,9 @@ type FindRecordBuilderOptions = Omit; **Example 1** - ```app/routes/post.js - import Route from '@ember/routing/route'; + ```ts import { findRecord } from '@ember-data/legacy-compat/builders'; - - export default class PostRoute extends Route { - model({ post_id }) { - return this.store.request(findRecord('post', post_id)); - } - } + const { content: post } = await store.request(findRecord('post', '1')); ``` **Example 2** @@ -46,15 +40,9 @@ type FindRecordBuilderOptions = Omit; 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) - ```app/routes/post.js - import Route from '@ember/routing/route'; + ```ts import { findRecord } from '@ember-data/legacy-compat/builders'; - - export default class PostRoute extends Route { - model({ post_id: id }) { - return this.store.request(findRecord({ type: 'post', id }).content; - } - } + 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.