From f52c00737cbc538560ecec9603ca1d2747b091ea Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 22 May 2024 01:16:40 -0700 Subject: [PATCH 1/4] feat: implement SchemaService RFC emberjs/rfcs#1027 --- config/package.json | 4 +- config/rollup/keep-assets.js | 44 -- config/vite/config.js | 1 + config/vite/keep-assets.js | 21 + package.json | 2 +- packages/-ember-data/src/store.ts | 7 +- .../build-config/src/deprecation-versions.ts | 24 + packages/build-config/src/deprecations.ts | 1 + packages/core-types/src/schema/concepts.ts | 21 + packages/core-types/src/schema/fields.ts | 167 +++++- packages/core-types/src/symbols.ts | 45 +- packages/core-types/src/utils.ts | 1 + packages/core-types/vite.config.mjs | 1 + packages/debug/src/data-adapter.ts | 61 +- packages/diagnostic/package.json | 3 + packages/diagnostic/vite.config.mjs | 6 +- .../graph/src/-private/-edge-definition.ts | 64 ++- packages/graph/src/-private/-utils.ts | 4 +- .../-private/debug/assert-polymorphic-type.ts | 26 +- packages/json-api/src/-private/cache.ts | 73 ++- .../legacy-data-fetch.ts | 16 +- .../src/legacy-network-handler/snapshot.ts | 32 +- .../-private/debug/assert-polymorphic-type.ts | 8 +- packages/model/src/-private/hooks.ts | 5 +- .../-private/legacy-relationships-support.ts | 21 +- packages/model/src/-private/model.ts | 15 +- .../model/src/-private/schema-provider.ts | 208 +++++-- packages/model/src/migration-support.ts | 34 +- packages/schema-record/src/hooks.ts | 2 +- packages/schema-record/src/managed-array.ts | 12 +- packages/schema-record/src/managed-object.ts | 13 +- packages/schema-record/src/record.ts | 43 +- packages/schema-record/src/schema.ts | 262 ++++----- packages/serializer/src/json-api.js | 10 +- packages/serializer/src/rest.js | 32 +- .../src/-private/caches/instance-cache.ts | 10 +- .../legacy-model-support/shim-model-class.ts | 75 +-- .../managers/cache-capabilities-manager.ts | 15 +- packages/store/src/-private/store-service.ts | 402 +++++++++----- .../-types/q/cache-capabilities-manager.ts | 3 + packages/store/src/-types/q/schema-service.ts | 188 ++++++- pnpm-lock.yaml | 6 - tests/builders/app/services/store.ts | 4 +- .../tests/integration/create-record-test.ts | 4 +- .../tests/integration/delete-record-test.ts | 4 +- .../tests/integration/update-record-test.ts | 4 +- tests/docs/fixtures/expected.js | 14 + .../ember-data__adapter/app/services/store.ts | 5 +- tests/ember-data__graph/app/services/store.ts | 5 +- tests/ember-data__json-api/app/styles/app.css | 1 - .../cache/collection-data-documents-test.ts | 126 ++--- .../integration/cache/error-documents-test.ts | 10 +- .../integration/cache/meta-documents-test.ts | 55 +- .../cache/resource-data-documents-test.ts | 161 ++---- .../tests/integration/serialize-test.ts | 122 ++-- .../tests/utils/schema.ts | 126 +++++ .../tests/integration/model-for-test.ts | 128 +---- tests/ember-data__model/tests/utils/schema.ts | 126 +++++ .../app/services/store.ts | 7 +- .../app/components/book-search.ts | 3 +- tests/main/tests/helpers/reactive-context.ts | 20 +- .../cache-handler/lifetimes-test.ts | 56 +- .../cache-handler/store-package-setup-test.ts | 232 ++------ .../cache-capabilities-manager-test.ts | 123 +--- .../spec-cache-errors-test.ts} | 20 +- .../spec-cache-state-test.ts} | 10 +- .../spec-cache-test.ts} | 14 +- .../collection/mutating-has-many-test.ts | 8 +- .../explicit-polymorphic-belongs-to-test.js | 46 +- .../explicit-polymorphic-has-many-test.js | 47 +- .../custom-class-model-test.ts | 525 ++++-------------- tests/main/tests/unit/debug-test.js | 48 +- tests/main/tests/utils/schema.ts | 141 +++++ .../app/services/store.ts | 5 + .../tests/-utils/normalize-payload.ts | 4 +- .../tests/-utils/reactive-context.ts | 13 +- .../tests/legacy/mode-test.ts | 460 +++++++-------- .../legacy/reactivity/basic-fields-test.ts | 167 +++--- .../tests/legacy/reads/basic-fields-test.ts | 121 ++-- .../tests/reactivity/array-test.ts | 31 +- .../tests/reactivity/basic-fields-test.ts | 115 ++-- .../tests/reactivity/derivation-test.ts | 117 ++-- .../tests/reactivity/resource-test.ts | 42 +- .../tests/reads/array-test.ts | 75 +-- .../tests/reads/basic-fields-test.ts | 125 +++-- .../tests/reads/derivation-test.ts | 100 ++-- .../tests/reads/object-test.ts | 73 +-- .../tests/reads/resource-test.ts | 40 +- .../tests/writes/array-test.ts | 394 +++++++------ .../tests/writes/object-test.ts | 200 +++---- 90 files changed, 3320 insertions(+), 2945 deletions(-) delete mode 100644 config/rollup/keep-assets.js create mode 100644 config/vite/keep-assets.js create mode 100644 packages/core-types/src/schema/concepts.ts create mode 100644 packages/core-types/src/utils.ts create mode 100644 tests/ember-data__json-api/tests/utils/schema.ts create mode 100644 tests/ember-data__model/tests/utils/schema.ts rename tests/main/tests/integration/{record-data => cache}/cache-capabilities-manager-test.ts (58%) rename tests/main/tests/integration/{record-data/record-data-errors-test.ts => cache/spec-cache-errors-test.ts} (95%) rename tests/main/tests/integration/{record-data/record-data-state-test.ts => cache/spec-cache-state-test.ts} (97%) rename tests/main/tests/integration/{record-data/record-data-test.ts => cache/spec-cache-test.ts} (96%) create mode 100644 tests/main/tests/utils/schema.ts diff --git a/config/package.json b/config/package.json index 076114132d5..16391247123 100644 --- a/config/package.json +++ b/config/package.json @@ -21,12 +21,10 @@ "eslint-plugin-n": "^16.6.2", "eslint-plugin-qunit": "^8.1.1", "eslint-plugin-simple-import-sort": "^12.1.0", - "minimatch": "^9.0.4", "rollup": "^4.17.2", "typescript": "^5.4.5", "vite": "^5.2.11", - "vite-plugin-dts": "^3.9.1", - "walk-sync": "^3.0.0" + "vite-plugin-dts": "^3.9.1" }, "engines": { "node": ">= 22.1.0" diff --git a/config/rollup/keep-assets.js b/config/rollup/keep-assets.js deleted file mode 100644 index 2ba5bab2535..00000000000 --- a/config/rollup/keep-assets.js +++ /dev/null @@ -1,44 +0,0 @@ -import walkSync from 'walk-sync'; -import { readFileSync } from 'fs'; -import { dirname, join, resolve } from 'path'; -import { minimatch } from 'minimatch'; - -export function keepAssets({ from, include }) { - return { - name: 'copy-assets', - - // Prior to https://github.com/rollup/rollup/pull/5270, we cannot call this - // from within `generateBundle` - buildStart() { - this.addWatchFile(from); - }, - - // imports of assets should be left alone in the source code. This can cover - // the case of .css as defined in the embroider v2 addon spec. - async resolveId(source, importer, options) { - const resolution = await this.resolve(source, importer, { - skipSelf: true, - ...options, - }); - if (resolution && importer && include.some((pattern) => minimatch(resolution.id, pattern))) { - return { id: resolve(dirname(importer), source), external: 'relative' }; - } - return resolution; - }, - - // the assets go into the output directory in the same relative locations as - // in the input directory - async generateBundle() { - for (let name of walkSync(from, { - globs: include, - directories: false, - })) { - this.emitFile({ - type: 'asset', - fileName: name, - source: readFileSync(join(from, name)), - }); - } - }, - }; -} diff --git a/config/vite/config.js b/config/vite/config.js index 05f616822e9..711d562d4ef 100644 --- a/config/vite/config.js +++ b/config/vite/config.js @@ -48,6 +48,7 @@ export function createConfig(options, resolve) { : undefined, options.fixModule ? FixModuleOutputPlugin : undefined, // options.compileTypes === true && options.rollupTypes === false ? CompileTypesPlugin(options.useGlint) : undefined, + ...(options.plugins ?? []), ] .concat(options.plugins || []) .filter(Boolean), diff --git a/config/vite/keep-assets.js b/config/vite/keep-assets.js new file mode 100644 index 00000000000..c002c6c169c --- /dev/null +++ b/config/vite/keep-assets.js @@ -0,0 +1,21 @@ +import { join } from 'path'; +import { copyFileSync, globSync, mkdirSync } from 'fs'; + +export function keepAssets({ from, include, dist }) { + return { + name: 'copy-assets', + + // the assets go into the output directory in the same relative locations as + // in the input directory + async closeBundle() { + const files = globSync(include, { cwd: join(process.cwd(), from) }); + for (let name of files) { + const fromPath = join(process.cwd(), from, name); + const toPath = join(process.cwd(), dist, name); + + mkdirSync(join(toPath, '..'), { recursive: true }); + copyFileSync(fromPath, toPath); + } + }, + }; +} diff --git a/package.json b/package.json index 722fd23fc1f..b91ae3a1192 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "scripts": { "takeoff": "FORCE_COLOR=2 pnpm install --prefer-offline --reporter=append-only", "prepare": "turbo run build:infra; pnpm --filter './packages/*' run --parallel --if-present sync-hardlinks; turbo run build:pkg; pnpm run prepare:types; pnpm run _task:sync-hardlinks;", - "prepare:types": "tsc --build; turbo run build:glint;", + "prepare:types": "tsc --build --force; turbo run build:glint;", "release": "./release/index.ts", "build": "turbo _build --log-order=stream --filter=./packages/* --concurrency=10;", "_task:sync-hardlinks": "pnpm run -r --parallel --if-present sync-hardlinks;", diff --git a/packages/-ember-data/src/store.ts b/packages/-ember-data/src/store.ts index f593fc82806..7482209de23 100644 --- a/packages/-ember-data/src/store.ts +++ b/packages/-ember-data/src/store.ts @@ -15,7 +15,7 @@ import { buildSchema, instantiateRecord, modelFor, teardownRecord } from '@ember import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; import BaseStore, { CacheHandler } from '@ember-data/store'; -import type { CacheCapabilitiesManager, ModelSchema } from '@ember-data/store/types'; +import type { CacheCapabilitiesManager, ModelSchema, SchemaService } from '@ember-data/store/types'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { Cache } from '@warp-drive/core-types/cache'; import type { TypeFromInstance } from '@warp-drive/core-types/record'; @@ -35,7 +35,10 @@ export default class Store extends BaseStore { this.requestManager.use([LegacyNetworkHandler, Fetch]); } this.requestManager.useCache(CacheHandler); - this.registerSchema(buildSchema(this)); + } + + createSchemaService(): SchemaService { + return buildSchema(this); } createCache(storeWrapper: CacheCapabilitiesManager): Cache { diff --git a/packages/build-config/src/deprecation-versions.ts b/packages/build-config/src/deprecation-versions.ts index d1de95a9d53..648d0c01e3a 100644 --- a/packages/build-config/src/deprecation-versions.ts +++ b/packages/build-config/src/deprecation-versions.ts @@ -398,3 +398,27 @@ export const DEPRECATE_MANY_ARRAY_DUPLICATES = '5.3'; * @public */ export const DEPRECATE_STORE_EXTENDS_EMBER_OBJECT = '5.4'; + +/** + * **id: ember-data:schema-service-updates** + * + * When the flag is `true` (default), the legacy schema + * service features will be enabled on the store and + * the service, and deprecations will be thrown when + * they are used. + * + * Deprecated features include: + * + * - `Store.registerSchema` method is deprecated in favor of the `Store.createSchemaService` hook + * - `Store.registerSchemaDefinitionService` method is deprecated in favor of the `Store.createSchemaService` hook + * - `Store.getSchemaDefinitionService` method is deprecated in favor of `Store.schema` property + * - `SchemaService.doesTypeExist` method is deprecated in favor of the `SchemaService.hasResource` method + * - `SchemaService.attributesDefinitionFor` method is deprecated in favor of the `SchemaService.fields` method + * - `SchemaService.relationshipsDefinitionFor` method is deprecated in favor of the `SchemaService.fields` method + * + * @property ENABLE_LEGACY_SCHEMA_SERVICE + * @since 5.4 + * @until 6.0 + * @public + */ +export const ENABLE_LEGACY_SCHEMA_SERVICE = '5.4'; diff --git a/packages/build-config/src/deprecations.ts b/packages/build-config/src/deprecations.ts index d5e0c63e0d9..d105a0e51aa 100644 --- a/packages/build-config/src/deprecations.ts +++ b/packages/build-config/src/deprecations.ts @@ -8,3 +8,4 @@ export const DEPRECATE_NON_UNIQUE_PAYLOADS: boolean = true; export const DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE: boolean = true; export const DEPRECATE_MANY_ARRAY_DUPLICATES: boolean = true; export const DEPRECATE_STORE_EXTENDS_EMBER_OBJECT: boolean = true; +export const ENABLE_LEGACY_SCHEMA_SERVICE: boolean = true; diff --git a/packages/core-types/src/schema/concepts.ts b/packages/core-types/src/schema/concepts.ts new file mode 100644 index 00000000000..606deb57f5f --- /dev/null +++ b/packages/core-types/src/schema/concepts.ts @@ -0,0 +1,21 @@ +import type { StableRecordIdentifier } from '../identifier'; +import type { ObjectValue, Value } from '../json/raw'; +import type { OpaqueRecordInstance } from '../record'; +import type { Type } from '../symbols'; + +export type Transformation = { + serialize(value: PT, options: ObjectValue | null, record: OpaqueRecordInstance): T; + hydrate(value: T | undefined, options: ObjectValue | null, record: OpaqueRecordInstance): PT; + defaultValue?(options: ObjectValue | null, identifier: StableRecordIdentifier): T; + [Type]: string; +}; + +export type Derivation = { + [Type]: string; +} & ((record: R, options: FM, prop: string) => T); + +export type HashFn = { [Type]: string } & (( + data: T, + options: ObjectValue | null, + prop: string | null +) => string); diff --git a/packages/core-types/src/schema/fields.ts b/packages/core-types/src/schema/fields.ts index bbbaa4c6fa9..52fb1e70485 100644 --- a/packages/core-types/src/schema/fields.ts +++ b/packages/core-types/src/schema/fields.ts @@ -61,6 +61,61 @@ export type IdentityField = { name: string; }; +/** + * Represents a specialized field whose computed value + * will be used as the primary key of a schema-object + * for serializability and comparison purposes. + * + * This field functions similarly to derived fields in that + * it is non-settable, derived state but differs in that + * it is only able to compute off of cache state and is given + * no access to a record instance. + * + * This means that if a hashing function wants to compute its value + * taking into account transformations and derivations it must + * perform those itself. + * + * A schema-array can declare its "key" value to be `@hash` if + * a schema-object has such a field. + * + * Only one hash field is permittable per schema-object, and + * it should be placed in the `ResourceSchema`'s `@id` field + * in place of an `IdentityField`. + * + * @typedoc + */ +export type HashField = { + kind: '@hash'; + + /** + * The name of the field that serves as the + * hash for the resource. + * + * Only required if access to this value by + * the UI is desired, it can be `null` otherwise. + * + * @typedoc + */ + name: string | null; + + /** + * The name of a function to run to compute the hash. + * The function will only have access to the cached + * data for the record. + * + * @typedoc + */ + type: string; + + /** + * Any options that should be provided to the hash + * function. + * + * @typedoc + */ + options?: ObjectValue; +}; + /** * Represents a field whose value is a local * value that is not stored in the cache, nor @@ -76,8 +131,9 @@ export type IdentityField = { * * For this reason Local fields should be used sparingly. * - * In the future, we may choose to only allow our - * own SchemaRecord to utilize them. + * Currently, while we document this feature here, + * only allow our own SchemaRecord should utilize them + * and the feature should be considered private. * * Example use cases that drove the creation of local * fields are states like `isDestroying` and `isDestroyed` @@ -160,8 +216,26 @@ export type SchemaObjectField = { */ type: string; - // FIXME: would we ever need options here? - options?: ObjectValue; + options?: { + /** + * Whether this SchemaObject is Polymorphic. + * + * If the SchemaObject is polymorphic, `options.type` must also be supplied. + * + * @typedoc + */ + polymorphic?: boolean; + + /** + * If the SchemaObject is Polymorphic, the key on the raw cache data to use + * as the "resource-type" value for the schema-object. + * + * Defaults to "type". + * + * @typedoc + */ + type?: string; + }; }; /** @@ -219,8 +293,64 @@ export type SchemaArrayField = { */ type: string; - // FIXME: would we ever need options here? - options?: ObjectValue; + /** + * Options for configuring the behavior of the + * SchemaArray. + * + * @typedoc + */ + + /** + * Options for configuring the behavior of the + * SchemaArray. + * + * @typedoc + */ + options?: { + /** + * Configures how the SchemaArray determines whether + * an object in the cache is the same as an object + * previously used to instantiate one of the schema-objects + * it contains. + * + * The default is `'@identity'`. + * + * Valid options are: + * + * - `'@identity'` (default) : the cached object's referential identity will be used. + * This may result in significant instability when resource data is updated from the API + * - `'@index'` : the cached object's index in the array will be used. + * This is only a good choice for arrays that rarely if ever change membership + * - `'@hash'` : will lookup the `@hash` function supplied in the ResourceSchema for + * The contained schema-object and use the computed result to determine and compare identity. + * - (string) : the name of a field to use as the key, only GenericFields (kind `field`) + * Are valid field names for this purpose. The cache state without transforms applied will be + * used when comparing values. The field value should be unique enough to guarantee two schema-objects + * of the same type will not collide. + * + * @typedoc + */ + key?: '@identity' | '@index' | '@hash' | string; + + /** + * Whether this SchemaArray is Polymorphic. + * + * If the SchemaArray is polymorphic, `options.type` must also be supplied. + * + * @typedoc + */ + polymorphic?: boolean; + + /** + * If the SchemaArray is Polymorphic, the key on the raw cache data to use + * as the "resource-type" value for the schema-object. + * + * Defaults to "type". + * + * @typedoc + */ + type?: string; + }; }; /** @@ -478,8 +608,7 @@ export type LegacyAttributeField = { * Represents a field that is a reference to * another resource. * - * This is the legacy version of the `ResourceField` - * type, and is used to represent fields that were + * This is the legacy version of the `ResourceField`. * * @typedoc */ @@ -570,6 +699,11 @@ export type LegacyBelongsToField = { * > [!CAUTION] * > This Field is LEGACY * + * Represents a field that is a reference to + * a collection of other resources. + * + * This is the legacy version of the `CollectionField`. + * * @typedoc */ export type LegacyHasManyField = { @@ -653,7 +787,6 @@ export type LegacyHasManyField = { export type FieldSchema = | GenericField - | IdentityField | LocalField | ObjectField | SchemaObjectField @@ -666,8 +799,16 @@ export type FieldSchema = | LegacyBelongsToField | LegacyHasManyField; -export type Schema = { - '@id': IdentityField | null; +export type ResourceSchema = { + legacy?: boolean; + /** + * For primary resources, this should be an IdentityField + * + * for schema-objects, this should be either a HashField or null + * + * @typedoc + */ + identity: IdentityField | HashField | null; /** * The name of the schema * @@ -684,8 +825,8 @@ export type Schema = { * * @typedoc */ - '@type': string; - traits: string[]; + type: string; + traits?: string[]; fields: FieldSchema[]; }; diff --git a/packages/core-types/src/symbols.ts b/packages/core-types/src/symbols.ts index 488ad82d6bc..3e6c58827a3 100644 --- a/packages/core-types/src/symbols.ts +++ b/packages/core-types/src/symbols.ts @@ -5,6 +5,47 @@ import { getOrSetGlobal } from './-private'; */ export const RecordStore = getOrSetGlobal('Store', Symbol('Store')); +/** + * Symbol for the name of a resource, transformation + * or derivation. + * + * ### With Resources + * + * This is an optional feature that can be used by + * record implementations to provide a typescript + * hint for the type of the resource. + * + * When used, EmberData and WarpDrive APIs can + * take advantage of this to provide better type + * safety and intellisense. + * + * ### With Derivations + * + * Required for derivations registered with + * `store.registerDerivation(derivation)`. + * + * ```ts + * function concat(record: object, options: ObjectValue | null, prop: string): string {} + * concat[Name] = 'concat'; + * ``` + * + * ### With Transforms + * + * Required for new-style transformations registered + * with `store.registerTransform(transform)`. + * + * For legacy transforms, if not used, + * `attr('name')` will allow any string name. + * `attr('name')` will always allow any string name. + * + * If used, `attr('name')` will enforce + * that the name is the same as the transform name. + * + * @type {Symbol} + * @typedoc + */ +export const Type = getOrSetGlobal('$type', Symbol('$type')); + /** * Symbol for the type of a resource. * @@ -19,7 +60,7 @@ export const RecordStore = getOrSetGlobal('Store', Symbol('Store')); * @type {Symbol} * @typedoc */ -export const ResourceType = getOrSetGlobal('$type', Symbol('$type')); +export const ResourceType = Type; /** * Symbol for the name of a transform. @@ -38,7 +79,7 @@ export const ResourceType = getOrSetGlobal('$type', Symbol('$type')); * @type {Symbol} * @typedoc */ -export const TransformName = getOrSetGlobal('TransformName', Symbol('$TransformName')); +export const TransformName = Type; /** * Symbol for use by builders to indicate the return type diff --git a/packages/core-types/src/utils.ts b/packages/core-types/src/utils.ts new file mode 100644 index 00000000000..96bd008d7b9 --- /dev/null +++ b/packages/core-types/src/utils.ts @@ -0,0 +1 @@ +export type WithPartial = Omit & Partial>; diff --git a/packages/core-types/vite.config.mjs b/packages/core-types/vite.config.mjs index 9a37051f9ff..32e97b83e74 100644 --- a/packages/core-types/vite.config.mjs +++ b/packages/core-types/vite.config.mjs @@ -15,6 +15,7 @@ export const entryPoints = [ './src/record.ts', './src/request.ts', './src/symbols.ts', + './src/utils.ts', // non-public './src/-private.ts', ]; diff --git a/packages/debug/src/data-adapter.ts b/packages/debug/src/data-adapter.ts index 6df93b1367b..808dae67f1d 100644 --- a/packages/debug/src/data-adapter.ts +++ b/packages/debug/src/data-adapter.ts @@ -63,36 +63,39 @@ function debugInfo(this: Model) { const expensiveProperties: string[] = []; const identifier = recordIdentifierFor(this); - const schema = this.store.getSchemaDefinitionService(); - const attrDefs = schema.attributesDefinitionFor(identifier); - const relDefs = schema.relationshipsDefinitionFor(identifier); - - const attributes = Object.keys(attrDefs); - attributes.unshift('id'); - - const groups = [ - { - name: 'Attributes', - properties: attributes, - expand: true, - }, - ]; - - Object.keys(relDefs).forEach((name) => { - const relationship = relDefs[name]; - let properties: string[] | undefined = relationships[relationship.kind]; - - if (properties === undefined) { - properties = relationships[relationship.kind] = []; - groups.push({ - name: relationship.kind, - properties, - expand: true, - }); + const fields = this.store.schema.fields(identifier); + + const attrGroup = { + name: 'Attributes', + properties: ['id'], + expand: true, + }; + const attributes = attrGroup.properties; + const groups = [attrGroup]; + + for (const field of fields.values()) { + switch (field.kind) { + case 'attribute': + attributes.push(field.name); + break; + case 'belongsTo': + case 'hasMany': { + let properties: string[] | undefined = relationships[field.kind]; + + if (properties === undefined) { + properties = relationships[field.kind] = []; + groups.push({ + name: field.kind, + properties, + expand: true, + }); + } + properties.push(field.name); + expensiveProperties.push(field.name); + break; + } } - properties.push(name); - expensiveProperties.push(name); - }); + } groups.push({ name: 'Flags', diff --git a/packages/diagnostic/package.json b/packages/diagnostic/package.json index cd706340ca7..9450a708c0b 100644 --- a/packages/diagnostic/package.json +++ b/packages/diagnostic/package.json @@ -48,6 +48,9 @@ }, "default": "./server/index.js" }, + "./*.css": { + "default": "./dist/*.css" + }, "./server/*": { "node": "./server/*.js", "bun": "./server/*.js", diff --git a/packages/diagnostic/vite.config.mjs b/packages/diagnostic/vite.config.mjs index d5af87fa73e..032e000eb20 100644 --- a/packages/diagnostic/vite.config.mjs +++ b/packages/diagnostic/vite.config.mjs @@ -1,4 +1,4 @@ -import { keepAssets } from '@warp-drive/internal-config/rollup/keep-assets'; +import { keepAssets } from '@warp-drive/internal-config/vite/keep-assets'; import { createConfig } from '@warp-drive/internal-config/vite/config.js'; export const externals = [ @@ -19,9 +19,7 @@ export default createConfig( { entryPoints, externals, - rollup: { - plugins: [keepAssets({ from: 'src', include: ['./styles/**/*.css'] })], - }, + plugins: [keepAssets({ from: 'src', include: ['./styles/**/*.css'], dist: 'dist' })], }, import.meta.resolve ); diff --git a/packages/graph/src/-private/-edge-definition.ts b/packages/graph/src/-private/-edge-definition.ts index c7e487a64f7..6412cbb0400 100644 --- a/packages/graph/src/-private/-edge-definition.ts +++ b/packages/graph/src/-private/-edge-definition.ts @@ -2,7 +2,13 @@ import type Store from '@ember-data/store'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; -import type { LegacyRelationshipSchema as RelationshipSchema } from '@warp-drive/core-types/schema/fields'; +import type { + CollectionField, + FieldSchema, + LegacyBelongsToField, + LegacyHasManyField, + ResourceField, +} from '@warp-drive/core-types/schema/fields'; import { expandingGet, expandingSet, getStore } from './-utils'; import { assertInheritedSchema } from './debug/assert-polymorphic-type'; @@ -10,6 +16,31 @@ import type { Graph } from './graph'; export type EdgeCache = Record>; +export type RelationshipField = LegacyBelongsToField | LegacyHasManyField | ResourceField | CollectionField; +export type RelationshipFieldKind = RelationshipField['kind']; +export type CollectionKind = 'hasMany' | 'collection'; +export type ResourceKind = 'belongsTo' | 'resource'; +export const RELATIONSHIP_KINDS = ['belongsTo', 'hasMany', 'resource', 'collection']; + +export function isLegacyField(field: FieldSchema): field is LegacyBelongsToField | LegacyHasManyField { + return field.kind === 'belongsTo' || field.kind === 'hasMany'; +} + +export function isRelationshipField(field: FieldSchema): field is RelationshipField { + return RELATIONSHIP_KINDS.includes(field.kind); +} + +export function temporaryConvertToLegacy( + field: ResourceField | CollectionField +): LegacyBelongsToField | LegacyHasManyField { + return { + kind: field.kind === 'resource' ? 'belongsTo' : 'hasMany', + name: field.name, + type: field.type, + options: Object.assign({}, { async: false, inverse: null, resetOnRemoteUpdate: false as const }, field.options), + }; +} + /** * * Given RHS (Right Hand Side) @@ -75,7 +106,7 @@ export type EdgeCache = Record>; * @internal */ export interface UpgradedMeta { - kind: 'hasMany' | 'belongsTo' | 'implicit'; + kind: 'implicit' | RelationshipFieldKind; /** * The field name on `this` record * @@ -94,7 +125,7 @@ export interface UpgradedMeta { isPolymorphic: boolean; resetOnRemoteUpdate: boolean; - inverseKind: 'hasMany' | 'belongsTo' | 'implicit'; + inverseKind: 'implicit' | RelationshipFieldKind; /** * The field name on the opposing record * @internal @@ -170,7 +201,10 @@ function syncMeta(definition: UpgradedMeta, inverseDefinition: UpgradedMeta) { inverseDefinition.resetOnRemoteUpdate = resetOnRemoteUpdate; } -function upgradeMeta(meta: RelationshipSchema): UpgradedMeta { +function upgradeMeta(meta: RelationshipField): UpgradedMeta { + if (!isLegacyField(meta)) { + meta = temporaryConvertToLegacy(meta); + } const niceMeta: UpgradedMeta = {} as UpgradedMeta; const options = meta.options; niceMeta.kind = meta.kind; @@ -188,7 +222,11 @@ function upgradeMeta(meta: RelationshipSchema): UpgradedMeta { niceMeta.inverseIsImplicit = (options && options.inverse === null) || BOOL_LATER; niceMeta.inverseIsCollection = BOOL_LATER; - niceMeta.resetOnRemoteUpdate = options && options.resetOnRemoteUpdate === false ? false : true; + niceMeta.resetOnRemoteUpdate = isLegacyField(meta) + ? meta.options?.resetOnRemoteUpdate === false + ? false + : true + : false; return niceMeta; } @@ -352,9 +390,9 @@ export function upgradeDefinition( !isImplicit ); - const relationships = storeWrapper.getSchemaDefinitionService().relationshipsDefinitionFor(identifier); + const relationships = storeWrapper.schema.fields(identifier); assert(`Expected to have a relationship definition for ${type} but none was found.`, relationships); - const meta = relationships[propertyName]; + const meta = relationships.get(propertyName); if (!meta) { // TODO potentially we should just be permissive here since this is an implicit relationship @@ -378,6 +416,8 @@ export function upgradeDefinition( cache[type][propertyName] = null; return null; } + + assert(`Expected ${propertyName} to be a relationship`, isRelationshipField(meta)); const definition = /*#__NOINLINE__*/ upgradeMeta(meta); let inverseDefinition: UpgradedMeta | null; @@ -412,15 +452,14 @@ export function upgradeDefinition( inverseDefinition = null; } else { // CASE: We have an explicit inverse or were able to resolve one - const inverseDefinitions = storeWrapper - .getSchemaDefinitionService() - .relationshipsDefinitionFor({ type: inverseType }); + const inverseDefinitions = storeWrapper.schema.fields({ type: inverseType }); assert(`Expected to have a relationship definition for ${inverseType} but none was found.`, inverseDefinitions); - const metaFromInverse = inverseDefinitions[inverseKey]; + const metaFromInverse = inverseDefinitions.get(inverseKey); assert( `Expected a relationship schema for '${inverseType}.${inverseKey}' to match the inverse of '${type}.${propertyName}', but no relationship schema was found.`, metaFromInverse ); + assert(`Expected ${inverseKey} to be a relationship`, isRelationshipField(metaFromInverse)); inverseDefinition = upgradeMeta(metaFromInverse); } @@ -543,11 +582,12 @@ export function upgradeDefinition( } function inverseForRelationship(store: Store, identifier: StableRecordIdentifier | { type: string }, key: string) { - const definition = store.getSchemaDefinitionService().relationshipsDefinitionFor(identifier)[key]; + const definition = store.schema.fields(identifier).get(key); if (!definition) { return null; } + assert(`Expected ${key} to be a relationship`, isRelationshipField(definition)); assert( `Expected the relationship defintion to specify the inverse type or null.`, definition.options?.inverse === null || diff --git a/packages/graph/src/-private/-utils.ts b/packages/graph/src/-private/-utils.ts index 8f970bb3884..ccdf4aa740d 100644 --- a/packages/graph/src/-private/-utils.ts +++ b/packages/graph/src/-private/-utils.ts @@ -232,7 +232,7 @@ export function assertRelationshipData( )}' } for the '${identifier.type}.${meta.key}' ${meta.kind} relationship on <${identifier.type}:${String( identifier.id )}>, but no schema exists for that type.`, - store.getSchemaDefinitionService().doesTypeExist(data.type) + store.schema.hasResource(data) ); } else { assert( @@ -241,7 +241,7 @@ export function assertRelationshipData( } relationship '${meta.key}' on <${identifier.type}:${String( identifier.id )}>, Expected an identifier with type '${meta.type}'. No schema was found for '${data.type}'.`, - data === null || !data.type || store.getSchemaDefinitionService().doesTypeExist(data.type) + data === null || !data.type || store.schema.hasResource(data) ); } } diff --git a/packages/graph/src/-private/debug/assert-polymorphic-type.ts b/packages/graph/src/-private/debug/assert-polymorphic-type.ts index 268cd9443d3..1855d91ea8f 100644 --- a/packages/graph/src/-private/debug/assert-polymorphic-type.ts +++ b/packages/graph/src/-private/debug/assert-polymorphic-type.ts @@ -4,7 +4,7 @@ import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; -import type { UpgradedMeta } from '../-edge-definition'; +import { isLegacyField, isRelationshipField, temporaryConvertToLegacy, type UpgradedMeta } from '../-edge-definition'; /* Assert that `addedRecord` has a valid type so it can be added to the @@ -212,9 +212,7 @@ if (DEBUG) { return; } if (parentDefinition.isPolymorphic) { - const meta = store.getSchemaDefinitionService().relationshipsDefinitionFor(addedIdentifier)[ - parentDefinition.inverseKey - ]; + let meta = store.schema.fields(addedIdentifier).get(parentDefinition.inverseKey); assert( `No '${parentDefinition.inverseKey}' field exists on '${ addedIdentifier.type @@ -225,6 +223,11 @@ if (DEBUG) { )}`, meta ); + assert( + `Expected the field ${parentDefinition.inverseKey} to be a relationship`, + meta && isRelationshipField(meta) + ); + meta = isLegacyField(meta) ? meta : temporaryConvertToLegacy(meta); assert( `You should not specify both options.as and options.inverse as null on ${addedIdentifier.type}.${parentDefinition.inverseKey}, as if there is no inverse field there is no abstract type to conform to. You may have intended for this relationship to be polymorphic, or you may have mistakenly set inverse to null.`, !(meta.options.inverse === null && meta?.options.as?.length) @@ -246,14 +249,17 @@ if (DEBUG) { } else if (addedIdentifier.type !== parentDefinition.type) { // if we are not polymorphic // then the addedIdentifier.type must be the same as the parentDefinition.type - const meta = store.getSchemaDefinitionService().relationshipsDefinitionFor(addedIdentifier)[ - parentDefinition.inverseKey - ]; + let meta = store.schema.fields(addedIdentifier).get(parentDefinition.inverseKey); + assert( + `Expected the field ${parentDefinition.inverseKey} to be a relationship`, + !meta || isRelationshipField(meta) + ); + meta = meta && (isLegacyField(meta) ? meta : temporaryConvertToLegacy(meta)); if (meta?.options.as === parentDefinition.type) { // inverse is likely polymorphic but missing the polymorphic flag - const meta = store - .getSchemaDefinitionService() - .relationshipsDefinitionFor({ type: parentDefinition.inverseType })[parentDefinition.key]; + let meta = store.schema.fields({ type: parentDefinition.inverseType }).get(parentDefinition.key); + assert(`Expected the field ${parentDefinition.key} to be a relationship`, meta && isRelationshipField(meta)); + meta = isLegacyField(meta) ? meta : temporaryConvertToLegacy(meta); const errors = validateSchema(definitionWithPolymorphic(inverseDefinition(parentDefinition)), meta); assert( `The '<${addedIdentifier.type}>.${ diff --git a/packages/json-api/src/-private/cache.ts b/packages/json-api/src/-private/cache.ts index 8dc0fec6aec..fc23de9ffd5 100644 --- a/packages/json-api/src/-private/cache.ts +++ b/packages/json-api/src/-private/cache.ts @@ -26,7 +26,12 @@ import type { StructuredDocument, StructuredErrorDocument, } from '@warp-drive/core-types/request'; -import type { FieldSchema } from '@warp-drive/core-types/schema/fields'; +import type { + CollectionField, + FieldSchema, + LegacyRelationshipSchema, + ResourceField, +} from '@warp-drive/core-types/schema/fields'; import type { CollectionResourceDataDocument, ResourceDataDocument, @@ -812,15 +817,14 @@ export default class JSONAPICache implements Cache { if (DEBUG) { if (!DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE) { // save off info about saved relationships - const relationships = this._capabilities.getSchemaDefinitionService().relationshipsDefinitionFor(identifier); - Object.keys(relationships).forEach((relationshipName) => { - const relationship = relationships[relationshipName]; - if (relationship.kind === 'belongsTo') { - if (this.__graph._isDirty(identifier, relationshipName)) { - const relationshipData = this.__graph.getData(identifier, relationshipName); + const fields = this._capabilities.schema.fields(identifier); + fields.forEach((schema, name) => { + if (schema.kind === 'belongsTo') { + if (this.__graph._isDirty(identifier, name)) { + const relationshipData = this.__graph.getData(identifier, name); const inFlight = (cached.inflightRelationships = cached.inflightRelationships || (Object.create(null) as Record)); - inFlight[relationshipName] = relationshipData; + inFlight[name] = relationshipData; } } }); @@ -902,15 +906,12 @@ export default class JSONAPICache implements Cache { if (!DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE) { // assert against bad API behavior where a belongsTo relationship // is saved but the return payload indicates a different final state. - const relationships = this._capabilities - .getSchemaDefinitionService() - .relationshipsDefinitionFor(identifier); - Object.keys(relationships).forEach((relationshipName) => { - const relationship = relationships[relationshipName]; - if (relationship.kind === 'belongsTo') { - const relationshipData = data.relationships![relationshipName]?.data; + const fields = this._capabilities.schema.fields(identifier); + fields.forEach((field, name) => { + if (field.kind === 'belongsTo') { + const relationshipData = data.relationships![name]?.data; if (relationshipData !== undefined) { - const inFlightData = cached.inflightRelationships?.[relationshipName] as SingleResourceRelationship; + const inFlightData = cached.inflightRelationships?.[name] as SingleResourceRelationship; if (!inFlightData || !('data' in inFlightData)) { return; } @@ -918,7 +919,7 @@ export default class JSONAPICache implements Cache { ? this._capabilities.identifierCache.getOrCreateRecordIdentifier(relationshipData) : null; assert( - `Expected the resource relationship '<${identifier.type}>.${relationshipName}' on ${ + `Expected the resource relationship '<${identifier.type}>.${name}' on ${ identifier.lid } to be saved as ${inFlightData.data ? inFlightData.data.lid : ''} but it was saved as ${ actualData ? actualData.lid : '' @@ -1465,7 +1466,7 @@ function getRemoteState(rel: CollectionEdge | ResourceEdge) { } function schemaHasLegacyDefaultValueFn(schema: FieldSchema | undefined): boolean { - if (!schema || schema?.kind === '@id') return false; + if (!schema) return false; return hasLegacyDefaultValueFn(schema.options); } @@ -1478,9 +1479,6 @@ function getDefaultValue( identifier: StableRecordIdentifier, store: Store ): Value | undefined { - if (schema?.kind === '@id') { - return null; - } const options = schema?.options; if (!schema || (!options && !schema.type)) { @@ -1508,14 +1506,7 @@ function getDefaultValue( // new style transforms } else if (schema.kind !== 'attribute' && schema.type) { - const transform = ( - store.schema as unknown as { - transforms?: Map< - string, - { defaultValue(options: Record | null, identifier: StableRecordIdentifier): Value } - >; - } - ).transforms?.get(schema.type); + const transform = store.schema.transformation(schema.type); if (transform?.defaultValue) { return transform.defaultValue(options || null, identifier); @@ -1631,7 +1622,7 @@ function _isLoading( function setupRelationships( graph: Graph, - storeWrapper: CacheCapabilitiesManager, + capabilities: CacheCapabilitiesManager, identifier: StableRecordIdentifier, data: ExistingResourceObject ) { @@ -1639,25 +1630,27 @@ function setupRelationships( // allows relationship payloads to be ignored silently if no relationship // definition exists. Ensure there's a test for this and then consider // moving this to an assertion. This check should possibly live in the graph. - const relationships = storeWrapper.schema.relationshipsDefinitionFor(identifier); - const keys = Object.keys(relationships); - for (let i = 0; i < keys.length; i++) { - const relationshipName = keys[i]; - const relationshipData = data.relationships![relationshipName]; + const fields = capabilities.schema.fields(identifier); + for (const [name, field] of fields) { + if (!isRelationship(field)) continue; - if (!relationshipData) { - continue; - } + const relationshipData = data.relationships![name]; + if (!relationshipData) continue; graph.push({ op: 'updateRelationship', record: identifier, - field: relationshipName, + field: name, value: relationshipData, }); } } +const RelationshipKinds = new Set(['hasMany', 'belongsTo', 'resource', 'collection']); +function isRelationship(field: FieldSchema): field is LegacyRelationshipSchema | CollectionField | ResourceField { + return RelationshipKinds.has(field.kind); +} + function patchLocalAttributes(cached: CachedResource): boolean { const { localAttrs, remoteAttrs, inflightAttrs, defaultAttrs, changes } = cached; if (!localAttrs) { @@ -1700,7 +1693,7 @@ function putOne( ); assert( `Missing Resource Type: received resource data with a type '${resource.type}' but no schema could be found with that name.`, - cache._capabilities.getSchemaDefinitionService().doesTypeExist(resource.type) + cache._capabilities.schema.hasResource(resource) ); let identifier: StableRecordIdentifier | undefined = identifiers.peekRecordIdentifier(resource); diff --git a/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts b/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts index ad43f4ff46c..d1618279415 100644 --- a/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts +++ b/packages/legacy-compat/src/legacy-network-handler/legacy-data-fetch.ts @@ -227,11 +227,14 @@ function ensureRelationshipIsSetToParent( } function inverseForRelationship(store: Store, identifier: { type: string; id?: string }, key: string) { - const definition = store.getSchemaDefinitionService().relationshipsDefinitionFor(identifier)[key]; + const definition = store.schema.fields(identifier).get(key); if (!definition) { return null; } - + assert( + `Expected the field definition to be a relationship`, + definition.kind === 'hasMany' || definition.kind === 'belongsTo' + ); assert( `Expected the relationship defintion to specify the inverse type or null.`, definition.options?.inverse === null || @@ -251,11 +254,14 @@ function getInverse( const inverseKey = inverseForRelationship(store, { type: parentType }, lhs_relationshipName); if (inverseKey) { - const definition = store.getSchemaDefinitionService().relationshipsDefinitionFor({ type }); - const { kind } = definition[inverseKey]; + const definition = store.schema.fields({ type }).get(inverseKey); + assert( + `Expected the field definition to be a relationship`, + definition && (definition.kind === 'hasMany' || definition.kind === 'belongsTo') + ); return { inverseKey, - kind, + kind: definition.kind, }; } } diff --git a/packages/legacy-compat/src/legacy-network-handler/snapshot.ts b/packages/legacy-compat/src/legacy-network-handler/snapshot.ts index 4bfb0561cc4..06936856fe6 100644 --- a/packages/legacy-compat/src/legacy-network-handler/snapshot.ts +++ b/packages/legacy-compat/src/legacy-network-handler/snapshot.ts @@ -167,11 +167,13 @@ export class Snapshot { } const attributes = (this.__attributes = Object.create(null) as Record); const { identifier } = this; - const attrs = Object.keys(this._store.getSchemaDefinitionService().attributesDefinitionFor(identifier)); + const attrs = this._store.schema.fields(identifier); const cache = this._store.cache; - attrs.forEach((keyName) => { - attributes[keyName] = cache.getAttr(identifier, keyName); + attrs.forEach((field, keyName) => { + if (field.kind === 'attribute') { + attributes[keyName] = cache.getAttr(identifier, keyName); + } }); return attributes; @@ -305,9 +307,7 @@ export class Snapshot { return this._belongsToRelationships[keyName]; } - const relationshipMeta = store.getSchemaDefinitionService().relationshipsDefinitionFor({ type: this.modelName })[ - keyName - ]; + const relationshipMeta = store.schema.fields({ type: this.modelName }).get(keyName); assert( `Model '${this.identifier.lid}' has no belongsTo relationship named '${keyName}' defined.`, relationshipMeta && relationshipMeta.kind === 'belongsTo' @@ -412,9 +412,7 @@ export class Snapshot { const store = this._store; upgradeStore(store); - const relationshipMeta = store.getSchemaDefinitionService().relationshipsDefinitionFor({ type: this.modelName })[ - keyName - ]; + const relationshipMeta = store.schema.fields({ type: this.modelName }).get(keyName); assert( `Model '${this.identifier.lid}' has no hasMany relationship named '${keyName}' defined.`, relationshipMeta && relationshipMeta.kind === 'hasMany' @@ -495,9 +493,11 @@ export class Snapshot { @public */ eachAttribute(callback: (key: string, meta: LegacyAttributeField) => void, binding?: unknown): void { - const attrDefs = this._store.getSchemaDefinitionService().attributesDefinitionFor(this.identifier); - Object.keys(attrDefs).forEach((key) => { - callback.call(binding, key, attrDefs[key]); + const fields = this._store.schema.fields(this.identifier); + fields.forEach((field, key) => { + if (field.kind === 'attribute') { + callback.call(binding, key, field); + } }); } @@ -519,9 +519,11 @@ export class Snapshot { @public */ eachRelationship(callback: (key: string, meta: LegacyRelationshipSchema) => void, binding?: unknown): void { - const relationshipDefs = this._store.getSchemaDefinitionService().relationshipsDefinitionFor(this.identifier); - Object.keys(relationshipDefs).forEach((key) => { - callback.call(binding, key, relationshipDefs[key]); + const fields = this._store.schema.fields(this.identifier); + fields.forEach((field, key) => { + if (field.kind === 'belongsTo' || field.kind === 'hasMany') { + callback.call(binding, key, field); + } }); } diff --git a/packages/model/src/-private/debug/assert-polymorphic-type.ts b/packages/model/src/-private/debug/assert-polymorphic-type.ts index bacca558743..49c451e0f99 100644 --- a/packages/model/src/-private/debug/assert-polymorphic-type.ts +++ b/packages/model/src/-private/debug/assert-polymorphic-type.ts @@ -35,9 +35,11 @@ if (DEBUG) { return; } if (parentDefinition.isPolymorphic) { - const meta = store.getSchemaDefinitionService().relationshipsDefinitionFor(addedIdentifier)[ - parentDefinition.inverseKey - ]; + const meta = store.schema.fields(addedIdentifier)?.get(parentDefinition.inverseKey); + assert( + `Expected the schema for the field ${parentDefinition.inverseKey} on ${addedIdentifier.type} to be for a legacy relationship`, + !meta || meta.kind === 'belongsTo' || meta.kind === 'hasMany' + ); assert( `The schema for the relationship '${parentDefinition.inverseKey}' on '${addedIdentifier.type}' type does not implement '${parentDefinition.type}' and thus cannot be assigned to the '${parentDefinition.key}' relationship in '${parentIdentifier.type}'. The definition should specify 'as: "${parentDefinition.type}"' in options.`, meta?.options.as === parentDefinition.type diff --git a/packages/model/src/-private/hooks.ts b/packages/model/src/-private/hooks.ts index 20c068a7716..a4d765cdd16 100644 --- a/packages/model/src/-private/hooks.ts +++ b/packages/model/src/-private/hooks.ts @@ -72,10 +72,7 @@ export function modelFor(this: Store, modelName: TypeFromInstanceOrString) if (!ignoreType) { return klass; } - assert( - `No model was found for '${type}' and no schema handles the type`, - this.getSchemaDefinitionService().doesTypeExist(type) - ); + assert(`No model was found for '${type}' and no schema handles the type`, this.schema.hasResource({ type })); } function secretInit(record: Model, cache: Cache, identifier: StableRecordIdentifier, store: Store): void { diff --git a/packages/model/src/-private/legacy-relationships-support.ts b/packages/model/src/-private/legacy-relationships-support.ts index f26420b1a9d..d425a600dbc 100644 --- a/packages/model/src/-private/legacy-relationships-support.ts +++ b/packages/model/src/-private/legacy-relationships-support.ts @@ -450,13 +450,15 @@ export class LegacySupport { (typeof adapter.findHasMany === 'function' || typeof identifiers === 'undefined') && (shouldForceReload || hasDematerializedInverse || isStale || (!allInverseRecordsAreLoaded && !isEmpty)); - const relationshipMeta = this.store - .getSchemaDefinitionService() - .relationshipsDefinitionFor({ type: definition.inverseType })[definition.key]; + const field = this.store.schema.fields({ type: definition.inverseType }).get(definition.key); + assert( + `Expected a hasMany field definition for ${definition.inverseType}.${definition.key}`, + field && field.kind === 'hasMany' + ); const request = { useLink: shouldFindViaLink, - field: relationshipMeta, + field, links: resource.links, meta: resource.meta, options, @@ -534,13 +536,14 @@ export class LegacySupport { resource.links?.related && (shouldForceReload || hasDematerializedInverse || isStale || (!allInverseRecordsAreLoaded && !isEmpty)); - const relationshipMeta = this.store.getSchemaDefinitionService().relationshipsDefinitionFor(this.identifier)[ - relationship.definition.key - ]; - assert(`Attempted to access a belongsTo relationship but no definition exists for it`, relationshipMeta); + const field = this.store.schema.fields(this.identifier).get(relationship.definition.key); + assert( + `Attempted to access a belongsTo relationship but no definition exists for it`, + field && field.kind === 'belongsTo' + ); const request = { useLink: shouldFindViaLink, - field: relationshipMeta, + field, links: resource.links, meta: resource.meta, options, diff --git a/packages/model/src/-private/model.ts b/packages/model/src/-private/model.ts index fb0174d3721..222e8219535 100644 --- a/packages/model/src/-private/model.ts +++ b/packages/model/src/-private/model.ts @@ -1259,7 +1259,7 @@ class Model extends EmberObject implements MinimalLegacyRecord { return null; } - const schemaExists = store.schema.doesTypeExist(relationship.type); + const schemaExists = store.schema.hasResource(relationship); assert( `No associated schema found for '${relationship.type}' while calculating the inverse of ${name} on ${this.modelName}`, @@ -1270,12 +1270,13 @@ class Model extends EmberObject implements MinimalLegacyRecord { return null; } - const inverseSchemas = store.schema.relationshipsDefinitionFor({ type: relationship.type }); - const inverseSchema = inverseSchemas[options.inverse]; - - assert(`No inverse relationship found for '${name}' on '${this.modelName}'`, inverseSchema !== undefined); + const inverseField = store.schema.fields(relationship).get(options.inverse); + assert( + `No inverse relationship found for '${name}' on '${this.modelName}'`, + inverseField && (inverseField.kind === 'belongsTo' || inverseField.kind === 'hasMany') + ); - return inverseSchema || null; + return inverseField || null; } /** @@ -1734,7 +1735,7 @@ class Model extends EmberObject implements MinimalLegacyRecord { this.modelName ); - const map = new Map(); + const map = new Map(); this.eachComputedProperty((name, meta) => { if (isAttributeSchema(meta)) { diff --git a/packages/model/src/-private/schema-provider.ts b/packages/model/src/-private/schema-provider.ts index f192a7bd143..33abf3db3a9 100644 --- a/packages/model/src/-private/schema-provider.ts +++ b/packages/model/src/-private/schema-provider.ts @@ -2,96 +2,194 @@ import { getOwner } from '@ember/application'; import type Store from '@ember-data/store'; import type { SchemaService } from '@ember-data/store/types'; -import type { RecordIdentifier } from '@warp-drive/core-types/identifier'; -import type { LegacyFieldSchema } from '@warp-drive/core-types/schema/fields'; +import { ENABLE_LEGACY_SCHEMA_SERVICE } from '@warp-drive/build-config/deprecations'; +import { assert } from '@warp-drive/build-config/macros'; +import type { RecordIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { ObjectValue } from '@warp-drive/core-types/json/raw'; +import type { Derivation, HashFn, Transformation } from '@warp-drive/core-types/schema/concepts'; +import type { + LegacyAttributeField, + LegacyFieldSchema, + LegacyRelationshipSchema, + ResourceSchema, +} from '@warp-drive/core-types/schema/fields'; import type { FactoryCache, Model, ModelFactory, ModelStore } from './model'; import _modelForMixin from './model-for-mixin'; import { normalizeModelName } from './util'; -type AttributesSchema = ReturnType; -type RelationshipsSchema = ReturnType; +type AttributesSchema = ReturnType>; +type RelationshipsSchema = ReturnType>; -export class ModelSchemaProvider { +type InternalSchema = { + schema: ResourceSchema; + fields: Map; + attributes: Record; + relationships: Record; +}; + +export interface ModelSchemaProvider { + attributesDefinitionFor(resource: RecordIdentifier | { type: string }): AttributesSchema; + + relationshipsDefinitionFor(resource: RecordIdentifier | { type: string }): RelationshipsSchema; + + doesTypeExist(type: string): boolean; +} +export class ModelSchemaProvider implements SchemaService { declare store: ModelStore; - declare _relationshipsDefCache: Record; - declare _attributesDefCache: Record; - declare _fieldsDefCache: Record>; + declare _schemas: Map; + declare _typeMisses: Set; constructor(store: ModelStore) { this.store = store; - this._relationshipsDefCache = Object.create(null) as Record; - this._attributesDefCache = Object.create(null) as Record; - this._fieldsDefCache = Object.create(null) as Record>; + this._schemas = new Map(); + this._typeMisses = new Set(); } - fields(identifier: RecordIdentifier | { type: string }): Map { - const { type } = identifier; - let fieldDefs: Map | undefined = this._fieldsDefCache[type]; + hasTrait(type: string): boolean { + assert(`hasTrait is not available with @ember-data/model's SchemaService`); + return false; + } + resourceHasTrait(resource: StableRecordIdentifier | { type: string }, trait: string): boolean { + assert(`resourceHasTrait is not available with @ember-data/model's SchemaService`); + return false; + } + transformation(name: string): Transformation { + assert(`transformation is not available with @ember-data/model's SchemaService`); + } + hashFn(name: string): HashFn { + assert(`hashFn is not available with @ember-data/model's SchemaService`); + } + derivation(name: string): Derivation { + assert(`derivation is not available with @ember-data/model's SchemaService`); + } + resource(resource: StableRecordIdentifier | { type: string }): ResourceSchema { + const type = normalizeModelName(resource.type); - if (fieldDefs === undefined) { - fieldDefs = new Map(); - this._fieldsDefCache[type] = fieldDefs; + if (!this._schemas.has(type)) { + this._loadModelSchema(type); + } - const attributes = this.attributesDefinitionFor(identifier); - const relationships = this.relationshipsDefinitionFor(identifier); + return this._schemas.get(type)!.schema; + } + registerResources(schemas: ResourceSchema[]): void { + assert(`registerResources is not available with @ember-data/model's SchemaService`); + } + registerResource(schema: ResourceSchema): void { + assert(`registerResource is not available with @ember-data/model's SchemaService`); + } + registerTransformation(transform: Transformation): void { + assert(`registerTransformation is not available with @ember-data/model's SchemaService`); + } + registerDerivation(derivation: Derivation): void { + assert(`registerDerivation is not available with @ember-data/model's SchemaService`); + } + registerHashFn(hashFn: HashFn): void { + assert(`registerHashFn is not available with @ember-data/model's SchemaService`); + } + _loadModelSchema(type: string) { + const modelClass = this.store.modelFor(type) as typeof Model; + const attributeMap = modelClass.attributes; - for (const attr of Object.values(attributes)) { - fieldDefs.set(attr.name, attr); - } + const attributes = Object.create(null) as AttributesSchema; + attributeMap.forEach((meta, name) => (attributes[name] = meta)); + const relationships = modelClass.relationshipsObject || null; + const fields = new Map(); - for (const rel of Object.values(relationships)) { - fieldDefs.set(rel.name, rel); - } + for (const attr of Object.values(attributes)) { + fields.set(attr.name, attr); } - return fieldDefs; - } + for (const rel of Object.values(relationships)) { + fields.set(rel.name, rel); + } + + const schema: ResourceSchema = { + legacy: true, + identity: { name: 'id', kind: '@id' }, + type, + fields: Array.from(fields.values()), + }; - // Following the existing RD implementation - attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema { - const { type } = identifier; - let attributes: AttributesSchema; + const internalSchema: InternalSchema = { + schema, + attributes, + relationships, + fields, + }; - attributes = this._attributesDefCache[type]; + this._schemas.set(type, internalSchema); - if (attributes === undefined) { - const modelClass = this.store.modelFor(type); - const attributeMap = modelClass.attributes; + return internalSchema; + } - attributes = Object.create(null) as AttributesSchema; - attributeMap.forEach((meta, name) => (attributes[name] = meta)); - this._attributesDefCache[type] = attributes; + fields(resource: RecordIdentifier | { type: string }): Map { + const type = normalizeModelName(resource.type); + + if (!this._schemas.has(type)) { + this._loadModelSchema(type); } - return attributes; + return this._schemas.get(type)!.fields; } - // Following the existing RD implementation - relationshipsDefinitionFor(identifier: RecordIdentifier | { type: string }): RelationshipsSchema { - const { type } = identifier; - let relationships: RelationshipsSchema; - - relationships = this._relationshipsDefCache[type]; + hasResource(resource: { type: string }): boolean { + const type = normalizeModelName(resource.type); - if (relationships === undefined) { - const modelClass = this.store.modelFor(type) as typeof Model; - relationships = modelClass.relationshipsObject || null; - this._relationshipsDefCache[type] = relationships; + if (this._schemas.has(type)) { + return true; } - return relationships; - } + if (this._typeMisses.has(type)) { + return false; + } - doesTypeExist(modelName: string): boolean { - const type = normalizeModelName(modelName); const factory = getModelFactory(this.store, type); + const exists = factory !== null; - return factory !== null; + if (!exists) { + this._typeMisses.add(type); + return false; + } + + return true; } } -export function buildSchema(store: Store) { +if (ENABLE_LEGACY_SCHEMA_SERVICE) { + ModelSchemaProvider.prototype.doesTypeExist = function (type: string): boolean { + // TODO @pr add deprecation here + return this.hasResource({ type }); + }; + + ModelSchemaProvider.prototype.attributesDefinitionFor = function ( + resource: RecordIdentifier | { type: string } + ): AttributesSchema { + // TODO @pr add deprecation here + const type = normalizeModelName(resource.type); + + if (!this._schemas.has(type)) { + this._loadModelSchema(type); + } + + return this._schemas.get(type)!.attributes; + }; + + ModelSchemaProvider.prototype.relationshipsDefinitionFor = function ( + resource: RecordIdentifier | { type: string } + ): RelationshipsSchema { + // TODO @pr add deprecation here + const type = normalizeModelName(resource.type); + + if (!this._schemas.has(type)) { + this._loadModelSchema(type); + } + + return this._schemas.get(type)!.relationships; + }; +} + +export function buildSchema(store: Store): SchemaService { return new ModelSchemaProvider(store as ModelStore); } diff --git a/packages/model/src/migration-support.ts b/packages/model/src/migration-support.ts index f396be00f0c..70873c70fbf 100644 --- a/packages/model/src/migration-support.ts +++ b/packages/model/src/migration-support.ts @@ -1,7 +1,11 @@ import { recordIdentifierFor } from '@ember-data/store'; +import type { SchemaService } from '@ember-data/store/types'; import { assert } from '@warp-drive/build-config/macros'; import { getOrSetGlobal } from '@warp-drive/core-types/-private'; -import type { FieldSchema } from '@warp-drive/core-types/schema/fields'; +import type { ObjectValue } from '@warp-drive/core-types/json/raw'; +import type { ResourceSchema } from '@warp-drive/core-types/schema/fields'; +import { Type } from '@warp-drive/core-types/symbols'; +import type { WithPartial } from '@warp-drive/core-types/utils'; import { Errors } from './-private'; import type { MinimalLegacyRecord } from './-private/model-methods'; @@ -20,10 +24,6 @@ import { } from './-private/model-methods'; import RecordState from './-private/record-state'; -type Derivation = (record: R, options: Record | null, prop: string) => T; -type SchemaService = { - registerDerivation(name: string, derivation: Derivation): void; -}; // 'isDestroying', 'isDestroyed' const LegacyFields = [ '_createSnapshot', @@ -55,7 +55,7 @@ const LegacyFields = [ const LegacySupport = getOrSetGlobal('LegacySupport', new WeakMap>()); -function legacySupport(record: MinimalLegacyRecord, options: Record | null, prop: string): unknown { +function legacySupport(record: MinimalLegacyRecord, options: ObjectValue | null, prop: string): unknown { let state = LegacySupport.get(record); if (!state) { state = {}; @@ -123,40 +123,40 @@ function legacySupport(record: MinimalLegacyRecord, options: Record): ResourceSchema { + schema.legacy = true; + schema.identity = { kind: '@id', name: 'id' }; -export function withFields(fields: FieldSchema[]) { LegacyFields.forEach((field) => { - fields.push({ + schema.fields.push({ type: '@legacy', name: field, kind: 'derived', }); }); - fields.push({ - name: 'id', - kind: '@id', - }); - fields.push({ + schema.fields.push({ name: 'isReloading', kind: '@local', type: 'boolean', options: { defaultValue: false }, }); - fields.push({ + schema.fields.push({ name: 'isDestroying', kind: '@local', type: 'boolean', options: { defaultValue: false }, }); - fields.push({ + schema.fields.push({ name: 'isDestroyed', kind: '@local', type: 'boolean', options: { defaultValue: false }, }); - return fields; + return schema as ResourceSchema; } export function registerDerivations(schema: SchemaService) { - schema.registerDerivation('@legacy', legacySupport as Derivation); + schema.registerDerivation(legacySupport); } diff --git a/packages/schema-record/src/hooks.ts b/packages/schema-record/src/hooks.ts index b0f1e80d5f5..f4d6c8d694e 100644 --- a/packages/schema-record/src/hooks.ts +++ b/packages/schema-record/src/hooks.ts @@ -11,7 +11,7 @@ export function instantiateRecord( createArgs?: Record ): SchemaRecord { const schema = store.schema as unknown as SchemaService; - const isLegacy = schema.schemas.get(identifier.type)?.legacy ?? false; + const isLegacy = schema.resource(identifier)?.legacy ?? false; const isEditable = isLegacy || Boolean(createArgs); const record = new SchemaRecord(store, identifier, { [Editable]: isEditable, diff --git a/packages/schema-record/src/managed-array.ts b/packages/schema-record/src/managed-array.ts index c6b4c93e9ed..d8c8e82261b 100644 --- a/packages/schema-record/src/managed-array.ts +++ b/packages/schema-record/src/managed-array.ts @@ -163,10 +163,8 @@ export class ManagedArray { subscribe(_SIGNAL); } if (field.type) { - const transform = schema.transforms.get(field.type); - if (!transform) { - throw new Error(`No '${field.type}' transform defined for use by ${address.type}.${String(prop)}`); - } + const transform = schema.transformation(field.type); + assert(`No '${field.type}' transform defined for use by ${address.type}.${String(prop)}`, transform); return transform.hydrate(val as Value, field.options ?? null, self.owner); } return val; @@ -227,10 +225,8 @@ export class ManagedArray { return true; } - const transform = schema.transforms.get(field.type); - if (!transform) { - throw new Error(`No '${field.type}' transform defined for use by ${address.type}.${String(prop)}`); - } + const transform = schema.transformation(field.type); + assert(`No '${field.type}' transform defined for use by ${address.type}.${String(prop)}`, transform); const rawValue = (self[SOURCE] as ArrayValue).map((item) => transform.serialize(item, field.options ?? null, self.owner) ); diff --git a/packages/schema-record/src/managed-object.ts b/packages/schema-record/src/managed-object.ts index b68fb2a0edc..ad0ac402445 100644 --- a/packages/schema-record/src/managed-object.ts +++ b/packages/schema-record/src/managed-object.ts @@ -1,6 +1,7 @@ import type Store from '@ember-data/store'; import type { Signal } from '@ember-data/tracking/-private'; import { addToTransaction, createSignal, subscribe } from '@ember-data/tracking/-private'; +import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { Cache } from '@warp-drive/core-types/cache'; import type { ObjectValue, Value } from '@warp-drive/core-types/json/raw'; @@ -75,10 +76,8 @@ export class ManagedObject { let newData = cache.getAttr(self.address, self.key); if (newData && newData !== self[SOURCE]) { if (field.type) { - const transform = schema.transforms.get(field.type); - if (!transform) { - throw new Error(`No '${field.type}' transform defined for use by ${address.type}.${String(prop)}`); - } + const transform = schema.transformation(field.type); + assert(`No '${field.type}' transform defined for use by ${address.type}.${String(prop)}`, transform); newData = transform.hydrate(newData as ObjectValue, field.options ?? null, self.owner) as ObjectValue; } self[SOURCE] = { ...(newData as ObjectValue) }; // Add type assertion for newData @@ -120,10 +119,8 @@ export class ManagedObject { return true; } - const transform = schema.transforms.get(field.type); - if (!transform) { - throw new Error(`No '${field.type}' transform defined for use by ${address.type}.${String(prop)}`); - } + const transform = schema.transformation(field.type); + assert(`No '${field.type}' transform defined for use by ${address.type}.${String(prop)}`, transform); const val = transform.serialize(self[SOURCE], field.options ?? null, self.owner); cache.setAttr(self.address, self.key, val); _SIGNAL.shouldReset = true; diff --git a/packages/schema-record/src/record.ts b/packages/schema-record/src/record.ts index a4c2eb5e765..848b9269921 100644 --- a/packages/schema-record/src/record.ts +++ b/packages/schema-record/src/record.ts @@ -81,10 +81,8 @@ function computeField( if (!field.type) { return rawValue; } - const transform = schema.transforms.get(field.type); - if (!transform) { - throw new Error(`No '${field.type}' transform defined for use by ${identifier.type}.${String(prop)}`); - } + const transform = schema.transformation(field.type); + assert(`No '${field.type}' transform defined for use by ${identifier.type}.${String(prop)}`, transform); return transform.hydrate(rawValue, field.options ?? null, record); } @@ -148,10 +146,8 @@ function computeObject( } if (field.kind === 'object') { if (field.type) { - const transform = schema.transforms.get(field.type); - if (!transform) { - throw new Error(`No '${field.type}' transform defined for use by ${identifier.type}.${String(prop)}`); - } + const transform = schema.transformation(field.type); + assert(`No '${field.type}' transform defined for use by ${identifier.type}.${String(prop)}`, transform); rawValue = transform.hydrate(rawValue as ObjectValue, field.options ?? null, record) as object; } } @@ -180,10 +176,8 @@ function computeDerivation( throw new Error(`The schema for ${identifier.type}.${String(prop)} is missing the type of the derivation`); } - const derivation = schema.derivations.get(field.type); - if (!derivation) { - throw new Error(`No '${field.type}' derivation defined for use by ${identifier.type}.${String(prop)}`); - } + const derivation = schema.derivation(field.type); + assert(`No '${field.type}' derivation defined for use by ${identifier.type}.${String(prop)}`, derivation); return derivation(record, field.options ?? null, prop); } @@ -297,6 +291,7 @@ export class SchemaRecord { const schema = store.schema as unknown as SchemaService; const cache = store.cache; + const identityField = schema.resource(identifier).identity; const fields = schema.fields(identifier); const signals: Map = new Map(); @@ -340,7 +335,7 @@ export class SchemaRecord { // for its own usage. // _, @, $, * - const field = fields.get(prop as string); + const field = prop === identityField?.name ? identityField : fields.get(prop as string); if (!field) { if (IgnoredGlobalFields.has(prop as string)) { return undefined; @@ -352,6 +347,9 @@ export class SchemaRecord { case '@id': entangleSignal(signals, receiver, '@identity'); return identifier.id; + case '@hash': + // TODO pass actual cache value not {} + return schema.hashFn(field.type)({}, field.options ?? null, field.name ?? null); case '@local': { const lastValue = computeLocal(receiver, field, prop as string); entangleSignal(signals, receiver, prop as string); @@ -425,11 +423,8 @@ export class SchemaRecord { cache.setAttr(identifier, prop as string, value as Value); return true; } - const transform = schema.transforms.get(field.type); - - if (!transform) { - throw new Error(`No '${field.type}' transform defined for use by ${identifier.type}.${String(prop)}`); - } + const transform = schema.transformation(field.type); + assert(`No '${field.type}' transform defined for use by ${identifier.type}.${String(prop)}`, transform); const rawValue = transform.serialize(value, field.options ?? null, target); cache.setAttr(identifier, prop as string, rawValue); @@ -453,10 +448,8 @@ export class SchemaRecord { return true; } - const transform = schema.transforms.get(field.type); - if (!transform) { - throw new Error(`No '${field.type}' transform defined for use by ${identifier.type}.${String(prop)}`); - } + const transform = schema.transformation(field.type); + assert(`No '${field.type}' transform defined for use by ${identifier.type}.${String(prop)}`, transform); const rawValue = (value as ArrayValue).map((item) => transform.serialize(item, field.options ?? null, target) @@ -487,10 +480,8 @@ export class SchemaRecord { } return true; } - const transform = schema.transforms.get(field.type); - if (!transform) { - throw new Error(`No '${field.type}' transform defined for use by ${identifier.type}.${String(prop)}`); - } + const transform = schema.transformation(field.type); + assert(`No '${field.type}' transform defined for use by ${identifier.type}.${String(prop)}`, transform); const rawValue = transform.serialize({ ...(value as ObjectValue) }, field.options ?? null, target); cache.setAttr(identifier, prop as string, rawValue); diff --git a/packages/schema-record/src/schema.ts b/packages/schema-record/src/schema.ts index 9e7f1f8ab09..f2cde23bdbd 100644 --- a/packages/schema-record/src/schema.ts +++ b/packages/schema-record/src/schema.ts @@ -1,13 +1,22 @@ import { recordIdentifierFor } from '@ember-data/store'; +import type { SchemaService as SchemaServiceInterface } from '@ember-data/store/types'; import { createCache, getValue } from '@ember-data/tracking'; import type { Signal } from '@ember-data/tracking/-private'; import { Signals } from '@ember-data/tracking/-private'; +import { ENABLE_LEGACY_SCHEMA_SERVICE } from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import { getOrSetGlobal } from '@warp-drive/core-types/-private'; -import type { Value } from '@warp-drive/core-types/json/raw'; -import type { OpaqueRecordInstance } from '@warp-drive/core-types/record'; -import type { FieldSchema, LegacyAttributeField, LegacyRelationshipSchema } from '@warp-drive/core-types/schema/fields'; +import type { ObjectValue, Value } from '@warp-drive/core-types/json/raw'; +import type { Derivation, HashFn } from '@warp-drive/core-types/schema/concepts'; +import type { + FieldSchema, + LegacyAttributeField, + LegacyRelationshipSchema, + ResourceSchema, +} from '@warp-drive/core-types/schema/fields'; +import { Type } from '@warp-drive/core-types/symbols'; +import type { WithPartial } from '@warp-drive/core-types/utils'; import type { SchemaRecord } from './record'; import { Identifier } from './symbols'; @@ -20,10 +29,6 @@ export const SchemaRecordFields: FieldSchema[] = [ name: 'constructor', kind: 'derived', }, - { - name: 'id', - kind: '@id', - }, { type: '@identity', name: '$type', @@ -32,7 +37,7 @@ export const SchemaRecordFields: FieldSchema[] = [ }, ]; -const _constructor: Derivation = function (record) { +function _constructor(record: SchemaRecord) { let state = Support.get(record as WeakKey); if (!state) { state = {}; @@ -45,17 +50,19 @@ const _constructor: Derivation = function (record throw new Error('Cannot access record.constructor.modelName on non-Legacy Schema Records.'); }, }); -}; +} +_constructor[Type] = '@constructor'; -export function withFields(fields: FieldSchema[]) { - fields.push(...SchemaRecordFields); - return fields; +export function withDefaults(schema: WithPartial): ResourceSchema { + schema.identity = schema.identity || { name: 'id', kind: '@id' }; + schema.fields.push(...SchemaRecordFields); + return schema as ResourceSchema; } -export function fromIdentity(record: SchemaRecord, options: null, key: string): asserts options; export function fromIdentity(record: SchemaRecord, options: { key: 'lid' } | { key: 'type' }, key: string): string; export function fromIdentity(record: SchemaRecord, options: { key: 'id' }, key: string): string | null; export function fromIdentity(record: SchemaRecord, options: { key: '^' }, key: string): StableRecordIdentifier; +export function fromIdentity(record: SchemaRecord, options: null, key: string): asserts options; export function fromIdentity( record: SchemaRecord, options: { key: 'id' | 'lid' | 'type' | '^' } | null, @@ -70,55 +77,28 @@ export function fromIdentity( return options.key === '^' ? identifier : identifier[options.key]; } +fromIdentity[Type] = '@identity'; -export function registerDerivations(schema: SchemaService) { - schema.registerDerivation( - '@identity', - fromIdentity as Derivation - ); - schema.registerDerivation('@constructor', _constructor); +export function registerDerivations(schema: SchemaServiceInterface) { + schema.registerDerivation(fromIdentity); + schema.registerDerivation(_constructor); } -/** - * The full schema for a resource - * - * @class FieldSpec - * @internal - */ -type FieldSpec = { - '@id': FieldSchema | null; - /** - * legacy schema service separated attribute - * from relationship lookup - * @internal - */ +type InternalSchema = { + original: ResourceSchema; + traits: Set; + fields: Map; attributes: Record; - /** - * legacy schema service separated attribute - * from relationship lookup - * @internal - */ relationships: Record; - /** - * new schema service is fields based - * @internal - */ - fields: Map; - /** - * legacy model mode support - * @internal - */ - legacy?: boolean; }; -export type Transform = { +export type Transformation = { serialize(value: PT, options: Record | null, record: SchemaRecord): T; hydrate(value: T | undefined, options: Record | null, record: SchemaRecord): PT; defaultValue?(options: Record | null, identifier: StableRecordIdentifier): T; + [Type]: string; }; -export type Derivation = (record: R, options: Record | null, prop: string) => T; - /** * Wraps a derivation in a new function with Derivation signature but that looks * up the value in the cache before recomputing. @@ -127,8 +107,10 @@ export type Derivation = (record: R, options: Record | nu * @param options * @param prop */ -function makeCachedDerivation(derivation: Derivation): Derivation { - return (record: R, options: Record | null, prop: string): T => { +function makeCachedDerivation( + derivation: Derivation +): Derivation { + const memoizedDerivation = (record: R, options: FM, prop: string): T => { const signals = (record as { [Signals]: Map })[Signals]; let signal = signals.get(prop); if (!signal) { @@ -140,84 +122,98 @@ function makeCachedDerivation(derivation: Derivation): Derivation) as T; }; + memoizedDerivation[Type] = derivation[Type]; + return memoizedDerivation; } -export class SchemaService { - declare schemas: Map; - declare transforms: Map>; - declare derivations: Map>; +export interface SchemaService { + doesTypeExist(type: string): boolean; + attributesDefinitionFor(identifier: { type: string }): InternalSchema['attributes']; + relationshipsDefinitionFor(identifier: { type: string }): InternalSchema['relationships']; +} +export class SchemaService implements SchemaServiceInterface { + declare _schemas: Map; + declare _transforms: Map; + declare _hashFns: Map; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + declare _derivations: Map>; + declare _traits: Set; constructor() { - this.schemas = new Map(); - this.transforms = new Map(); - this.derivations = new Map(); + this._schemas = new Map(); + this._transforms = new Map(); + this._hashFns = new Map(); + this._derivations = new Map(); + } + hasTrait(type: string): boolean { + return this._traits.has(type); + } + resourceHasTrait(resource: StableRecordIdentifier | { type: string }, trait: string): boolean { + return this._schemas.get(resource.type)!.traits.has(trait); + } + transformation(name: string): Transformation { + assert(`No transformation registered with name '${name}'`, this._transforms.has(name)); + return this._transforms.get(name)!; + } + derivation(name: string): Derivation { + assert(`No derivation registered with name '${name}'`, this._derivations.has(name)); + return this._derivations.get(name)!; + } + resource(resource: StableRecordIdentifier | { type: string }): ResourceSchema { + assert(`No resource registered with name '${resource.type}'`, this._schemas.has(resource.type)); + return this._schemas.get(resource.type)!.original; } + hashFn(name: string): HashFn { + assert(`No hash function registered with name '${name}'`, this._hashFns.has(name)); + return this._hashFns.get(name)!; + } + registerResources(schemas: ResourceSchema[]): void { + schemas.forEach((schema) => { + this.registerResource(schema); + }); + } + registerResource(schema: ResourceSchema): void { + const fields = new Map(); + const relationships: Record = {}; + const attributes: Record = {}; + + schema.fields.forEach((field) => { + assert( + `${field.kind} is not valid inside a ResourceSchema's fields.`, + // @ts-expect-error we are checking for mistakes at runtime + field.kind !== '@id' && field.kind !== '@hash' + ); + fields.set(field.name, field); + if (field.kind === 'attribute') { + attributes[field.name] = field; + } else if (field.kind === 'belongsTo' || field.kind === 'hasMany') { + relationships[field.name] = field; + } + }); - registerTransform(type: string, transform: Transform): void { - this.transforms.set(type, transform); + const traits = new Set(schema.traits); + traits.forEach((trait) => { + this._traits.add(trait); + }); + + const internalSchema: InternalSchema = { original: schema, fields, relationships, attributes, traits }; + this._schemas.set(schema.type, internalSchema); } - registerDerivation(type: string, derivation: Derivation): void { - this.derivations.set(type, makeCachedDerivation(derivation) as Derivation); + registerTransformation(transformation: Transformation): void { + this._transforms.set(transformation[Type], transformation as Transformation); } - defineSchema(name: string, schema: { legacy?: boolean; fields: FieldSchema[] }): void { - const { legacy, fields } = schema; - const fieldSpec: FieldSpec = { - '@id': null, - attributes: {}, - relationships: {}, - fields: new Map(), - legacy: legacy ?? false, - }; - - assert( - `Only one field can be defined as @id, ${name} has more than one: ${fields - .filter((f) => f.kind === '@id') - .map((f) => f.name) - .join(' ')}`, - fields.filter((f) => f.kind === '@id').length <= 1 - ); - fields.forEach((field) => { - fieldSpec.fields.set(field.name, field); - - if (field.kind === '@id') { - fieldSpec['@id'] = field; - } else if (field.kind === 'field') { - // We don't add 'field' fields to attributes in order to allow simpler - // migration between transformation behaviors - // serializers and things which call attributesDefinitionFor will - // only run on the things that are legacy attribute mode, while all fields - // will have their serialize/hydrate logic managed by the cache and record - // - // This means that if you want to normalize fields pre-cache insertion - // Or pre-api call you wil need to use the newer `schema.fields()` API - // To opt-in to that ability (which note, is now an anti-pattern) - // - // const attr = Object.assign({}, field, { kind: 'attribute' }) as AttributeSchema; - // fieldSpec.attributes[attr.name] = attr; - } else if (field.kind === 'attribute') { - fieldSpec.attributes[field.name] = field; - } else if (field.kind === 'resource' || field.kind === 'collection') { - const relSchema = Object.assign({}, field, { - kind: field.kind === 'resource' ? 'belongsTo' : 'hasMany', - }) as unknown as LegacyRelationshipSchema; - fieldSpec.relationships[field.name] = relSchema; - } else if ( - field.kind !== 'derived' && - field.kind !== '@local' && - field.kind !== 'array' && - field.kind !== 'object' - ) { - throw new Error(`Unknown field kind ${field.kind}`); - } - }); + registerDerivation(derivation: Derivation): void { + this._derivations.set(derivation[Type], makeCachedDerivation(derivation)); + } - this.schemas.set(name, fieldSpec); + registerHashFn(hashFn: HashFn): void { + this._hashFns.set(hashFn[Type], hashFn as HashFn); } - fields({ type }: { type: string }): FieldSpec['fields'] { - const schema = this.schemas.get(type); + fields({ type }: { type: string }): InternalSchema['fields'] { + const schema = this._schemas.get(type); if (!schema) { throw new Error(`No schema defined for ${type}`); @@ -226,27 +222,41 @@ export class SchemaService { return schema.fields; } - attributesDefinitionFor({ type }: { type: string }): FieldSpec['attributes'] { - const schema = this.schemas.get(type); + hasResource(resource: { type: string }): boolean { + return this._schemas.has(resource.type); + } +} + +if (ENABLE_LEGACY_SCHEMA_SERVICE) { + SchemaService.prototype.attributesDefinitionFor = function ({ + type, + }: { + type: string; + }): InternalSchema['attributes'] { + const schema = this._schemas.get(type); if (!schema) { throw new Error(`No schema defined for ${type}`); } return schema.attributes; - } + }; - relationshipsDefinitionFor({ type }: { type: string }): FieldSpec['relationships'] { - const schema = this.schemas.get(type); + SchemaService.prototype.relationshipsDefinitionFor = function ({ + type, + }: { + type: string; + }): InternalSchema['relationships'] { + const schema = this._schemas.get(type); if (!schema) { throw new Error(`No schema defined for ${type}`); } return schema.relationships; - } + }; - doesTypeExist(type: string): boolean { - return this.schemas.has(type); - } + SchemaService.prototype.doesTypeExist = function (type: string): boolean { + return this._schemas.has(type); + }; } diff --git a/packages/serializer/src/json-api.js b/packages/serializer/src/json-api.js index b2cb29c2244..bd23979e5ff 100644 --- a/packages/serializer/src/json-api.js +++ b/packages/serializer/src/json-api.js @@ -195,17 +195,17 @@ const JSONAPISerializer = JSONSerializer.extend({ _normalizeResourceHelper(resourceHash) { assert(this.warnMessageForUndefinedType(), resourceHash.type); - const modelName = this.modelNameFromPayloadKey(resourceHash.type); + const type = this.modelNameFromPayloadKey(resourceHash.type); - if (!this.store.getSchemaDefinitionService().doesTypeExist(modelName)) { - warn(this.warnMessageNoModelForType(modelName, resourceHash.type, 'modelNameFromPayloadKey'), false, { + if (!this.store.schema.hasResource({ type })) { + warn(this.warnMessageNoModelForType(type, resourceHash.type, 'modelNameFromPayloadKey'), false, { id: 'ds.serializer.model-for-type-missing', }); return null; } - const modelClass = this.store.modelFor(modelName); - const serializer = this.store.serializerFor(modelName); + const modelClass = this.store.modelFor(type); + const serializer = this.store.serializerFor(type); const { data } = serializer.normalize(modelClass, resourceHash); return data; }, diff --git a/packages/serializer/src/rest.js b/packages/serializer/src/rest.js index c472c83d342..de12b422034 100644 --- a/packages/serializer/src/rest.js +++ b/packages/serializer/src/rest.js @@ -209,11 +209,11 @@ const RESTSerializer = JSONSerializer.extend({ if (!primaryHasTypeAttribute && hash.type) { // Support polymorphic records in async relationships - const modelName = this.modelNameFromPayloadKey(hash.type); + const type = this.modelNameFromPayloadKey(hash.type); - if (store.getSchemaDefinitionService().doesTypeExist(modelName)) { - serializer = store.serializerFor(modelName); - modelClass = store.modelFor(modelName); + if (store.schema.hasResource({ type })) { + serializer = store.serializerFor(type); + modelClass = store.modelFor(type); } } @@ -278,15 +278,15 @@ const RESTSerializer = JSONSerializer.extend({ modelName = prop.substr(1); } - var typeName = this.modelNameFromPayloadKey(modelName); - if (!store.getSchemaDefinitionService().doesTypeExist(typeName)) { - warn(this.warnMessageNoModelForKey(modelName, typeName), false, { + const type = this.modelNameFromPayloadKey(modelName); + if (!store.schema.hasResource({ type })) { + warn(this.warnMessageNoModelForKey(modelName, type), false, { id: 'ds.serializer.model-for-key-missing', }); continue; } - var isPrimary = !forcedSecondary && this.isPrimaryType(store, typeName, primaryModelClass); + var isPrimary = !forcedSecondary && this.isPrimaryType(store, type, primaryModelClass); var value = payload[prop]; if (value === null) { @@ -318,7 +318,7 @@ const RESTSerializer = JSONSerializer.extend({ continue; } - const { data, included } = this._normalizeArray(store, typeName, value, prop); + const { data, included } = this._normalizeArray(store, type, value, prop); if (included) { documentHash.included = documentHash.included.concat(included); @@ -400,19 +400,19 @@ const RESTSerializer = JSONSerializer.extend({ included: [], }; - for (var prop in payload) { - var modelName = this.modelNameFromPayloadKey(prop); - if (!store.getSchemaDefinitionService().doesTypeExist(modelName)) { - warn(this.warnMessageNoModelForKey(prop, modelName), false, { + for (const prop in payload) { + const type = this.modelNameFromPayloadKey(prop); + if (!store.schema.hasResource({ type })) { + warn(this.warnMessageNoModelForKey(prop, type), false, { id: 'ds.serializer.model-for-key-missing', }); continue; } - var type = store.modelFor(modelName); - var typeSerializer = store.serializerFor(type.modelName); + const ModelSchema = store.modelFor(type); + const typeSerializer = store.serializerFor(ModelSchema.modelName); makeArray(payload[prop]).forEach((hash) => { - const { data, included } = typeSerializer.normalize(type, hash, prop); + const { data, included } = typeSerializer.normalize(ModelSchema, hash, prop); documentHash.data.push(data); if (included) { documentHash.included = documentHash.included.concat(included); diff --git a/packages/store/src/-private/caches/instance-cache.ts b/packages/store/src/-private/caches/instance-cache.ts index 17d86f34f1a..1fa2a0bd4f6 100644 --- a/packages/store/src/-private/caches/instance-cache.ts +++ b/packages/store/src/-private/caches/instance-cache.ts @@ -416,17 +416,17 @@ type PreloadRelationshipValue = OpaqueRecordInstance | string; export function preloadData(store: Store, identifier: StableRecordIdentifier, preload: Record) { const jsonPayload: Partial = {}; //TODO(Igor) consider the polymorphic case - const schemas = store.getSchemaDefinitionService(); - const relationships = schemas.relationshipsDefinitionFor(identifier); + const schemas = store.schema; + const fields = schemas.fields(identifier); Object.keys(preload).forEach((key) => { const preloadValue = preload[key]; - const relationshipMeta = relationships[key]; - if (relationshipMeta) { + const field = fields.get(key); + if (field && (field.kind === 'hasMany' || field.kind === 'belongsTo')) { if (!jsonPayload.relationships) { jsonPayload.relationships = {}; } - jsonPayload.relationships[key] = preloadRelationship(relationshipMeta, preloadValue); + jsonPayload.relationships[key] = preloadRelationship(field, preloadValue); } else { if (!jsonPayload.attributes) { jsonPayload.attributes = {}; diff --git a/packages/store/src/-private/legacy-model-support/shim-model-class.ts b/packages/store/src/-private/legacy-model-support/shim-model-class.ts index 851725015f7..2b32ad46a03 100644 --- a/packages/store/src/-private/legacy-model-support/shim-model-class.ts +++ b/packages/store/src/-private/legacy-model-support/shim-model-class.ts @@ -28,16 +28,6 @@ export function getShimClass( return shim; } -function mapFromHash(hash: Record): Map { - const map: Map = new Map(); - for (const i in hash) { - if (Object.prototype.hasOwnProperty.call(hash, i)) { - map.set(i, hash[i]); - } - } - return map; -} - // Mimics the static apis of @ember-data/model export default class ShimModelClass implements ModelSchema { declare __store: Store; @@ -48,32 +38,49 @@ export default class ShimModelClass implements ModelSchema { } get fields(): Map, 'attribute' | 'belongsTo' | 'hasMany'> { - const attrs = this.__store.getSchemaDefinitionService().attributesDefinitionFor({ type: this.modelName }); - const relationships = this.__store - .getSchemaDefinitionService() - .relationshipsDefinitionFor({ type: this.modelName }); const fields = new Map, 'attribute' | 'belongsTo' | 'hasMany'>(); - Object.keys(attrs).forEach((key) => fields.set(key as KeyOrString, 'attribute')); - Object.keys(relationships).forEach((key) => fields.set(key as KeyOrString, relationships[key].kind)); + const fieldSchemas = this.__store.schema.fields({ type: this.modelName }); + + fieldSchemas.forEach((schema, key) => { + if (schema.kind === 'attribute' || schema.kind === 'belongsTo' || schema.kind === 'hasMany') { + fields.set(key as KeyOrString, schema.kind); + } + }); + return fields; } get attributes(): Map, LegacyAttributeField> { - const attrs = this.__store.getSchemaDefinitionService().attributesDefinitionFor({ type: this.modelName }); - return mapFromHash(attrs as Record); + const attrs = new Map, LegacyAttributeField>(); + const fields = this.__store.schema.fields({ type: this.modelName }); + + fields.forEach((schema, key) => { + if (schema.kind === 'attribute') { + attrs.set(key as KeyOrString, schema); + } + }); + + return attrs; } get relationshipsByName(): Map, LegacyRelationshipSchema> { - const relationships = this.__store - .getSchemaDefinitionService() - .relationshipsDefinitionFor({ type: this.modelName }); - return mapFromHash(relationships as Record); + const rels = new Map, LegacyRelationshipSchema>(); + const fields = this.__store.schema.fields({ type: this.modelName }); + + fields.forEach((schema, key) => { + if (schema.kind === 'belongsTo' || schema.kind === 'hasMany') { + rels.set(key as KeyOrString, schema); + } + }); + + return rels; } eachAttribute>(callback: (key: K, attribute: LegacyAttributeField) => void, binding?: T) { - const attrDefs = this.__store.getSchemaDefinitionService().attributesDefinitionFor({ type: this.modelName }); - Object.keys(attrDefs).forEach((key) => { - callback.call(binding, key as K, attrDefs[key]); + this.__store.schema.fields({ type: this.modelName }).forEach((schema, key) => { + if (schema.kind === 'attribute') { + callback.call(binding, key as K, schema); + } }); } @@ -81,20 +88,18 @@ export default class ShimModelClass implements ModelSchema { callback: (key: K, relationship: LegacyRelationshipSchema) => void, binding?: T ) { - const relationshipDefs = this.__store - .getSchemaDefinitionService() - .relationshipsDefinitionFor({ type: this.modelName }); - Object.keys(relationshipDefs).forEach((key) => { - callback.call(binding, key as K, relationshipDefs[key]); + this.__store.schema.fields({ type: this.modelName }).forEach((schema, key) => { + if (schema.kind === 'belongsTo' || schema.kind === 'hasMany') { + callback.call(binding, key as K, schema); + } }); } eachTransformedAttribute>(callback: (key: K, type: string | null) => void, binding?: T) { - const attrDefs = this.__store.getSchemaDefinitionService().attributesDefinitionFor({ type: this.modelName }); - Object.keys(attrDefs).forEach((key) => { - const type = attrDefs[key].type; - if (type) { - callback.call(binding, key as K, type); + this.__store.schema.fields({ type: this.modelName }).forEach((schema, key) => { + if (schema.kind === 'attribute') { + const type = schema.type; + if (type) callback.call(binding, key as K, type); } }); } diff --git a/packages/store/src/-private/managers/cache-capabilities-manager.ts b/packages/store/src/-private/managers/cache-capabilities-manager.ts index 9a2c205e7c1..d614eca9799 100644 --- a/packages/store/src/-private/managers/cache-capabilities-manager.ts +++ b/packages/store/src/-private/managers/cache-capabilities-manager.ts @@ -1,3 +1,4 @@ +import { ENABLE_LEGACY_SCHEMA_SERVICE } from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; import type { StableDocumentIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; @@ -12,6 +13,9 @@ import type { NotificationType } from './notification-manager'; @module @ember-data/store */ +export interface CacheCapabilitiesManager { + getSchemaDefinitionService(): SchemaService; +} export class CacheCapabilitiesManager implements StoreWrapper { declare _willNotify: boolean; declare _pendingNotifies: Map>; @@ -88,10 +92,6 @@ export class CacheCapabilitiesManager implements StoreWrapper { this._store.notifications.notify(identifier, namespace, key); } - getSchemaDefinitionService(): SchemaService { - return this._store.getSchemaDefinitionService(); - } - get schema() { return this._store.schema; } @@ -111,3 +111,10 @@ export class CacheCapabilitiesManager implements StoreWrapper { this._pendingNotifies.delete(identifier); } } + +if (ENABLE_LEGACY_SCHEMA_SERVICE) { + CacheCapabilitiesManager.prototype.getSchemaDefinitionService = function () { + // FIXME add deprecation for this + return this._store.schema; + }; +} diff --git a/packages/store/src/-private/store-service.ts b/packages/store/src/-private/store-service.ts index 6834302cd8d..31edf4e9e40 100644 --- a/packages/store/src/-private/store-service.ts +++ b/packages/store/src/-private/store-service.ts @@ -9,7 +9,10 @@ import { dependencySatisfies, importSync, macroCondition } from '@embroider/macr import type RequestManager from '@ember-data/request'; import type { Future } from '@ember-data/request'; import { LOG_PAYLOADS, LOG_REQUESTS } from '@warp-drive/build-config/debugging'; -import { DEPRECATE_STORE_EXTENDS_EMBER_OBJECT } from '@warp-drive/build-config/deprecations'; +import { + DEPRECATE_STORE_EXTENDS_EMBER_OBJECT, + ENABLE_LEGACY_SCHEMA_SERVICE, +} from '@warp-drive/build-config/deprecations'; import { DEBUG, TESTING } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; import type { Cache } from '@warp-drive/core-types/cache'; @@ -221,7 +224,7 @@ const app = new EmberApp(defaults, { } export interface Store { - createCache(storeWrapper: CacheCapabilitiesManager): Cache; + createCache(capabilities: CacheCapabilitiesManager): Cache; // eslint-disable-next-line @typescript-eslint/no-unused-vars instantiateRecord( @@ -230,6 +233,193 @@ export interface Store { ): OpaqueRecordInstance; teardownRecord(record: OpaqueRecordInstance): void; + + /* This hook enables an app to supply a SchemaService + * for use when information about a resource's schema needs + * to be queried. + * + * This method will only be called once to instantiate the singleton + * service, which can then be accessed via `store.schema`. + * + * For Example, to use the default SchemaService for SchemaRecord + * + * ```ts + * import { SchemaService } from '@warp-drive/schema-record/schema'; + * + * class extends Store { + * createSchemaService() { + * return new SchemaService(); + * } + * } + * ``` + * + * Or to use the SchemaService for @ember-data/model + * + * ```ts + * import { buildSchema } from '@ember-data/model/hooks'; + * + * class extends Store { + * createSchemaService() { + * return buildSchema(this); + * } + * } + * ``` + * + * If you wish to chain services, you must either + * instantiate each schema source directly or super to retrieve + * an existing service. For convenience, when migrating from + * `@ember-data/model` to `@warp-drive/schema-record` a + * SchemaService is provided that handles this transition + * for you: + * + * ```ts + * import { DelegatingSchemaService } from '@ember-data/model/migration-support'; + * import { SchemaService } from '@warp-drive/schema-record/schema'; + * + * class extends Store { + * createSchemaService() { + * const schema = new SchemaService(); + * return new DelegatingSchemaService(this, schema); + * } + * } + * ``` + * + * When using the DelegateSchemaService, the schema will first + * be sourced from directly registered schemas, then will fallback + * to sourcing a schema from available models if no schema is found. + * + * @method createSchemaService (hook) + * @return {SchemaService} + * @public + */ + createSchemaService(): SchemaService; + + /** + * DEPRECATED - Use the property `store.schema` instead. + * + * Provides access to the SchemaDefinitionService instance + * for this Store instance. + * + * The SchemaDefinitionService can be used to query for + * information about the schema of a resource. + * + * @method getSchemaDefinitionService + * @deprecated + * @public + */ + getSchemaDefinitionService(): SchemaService; + + /** + * DEPRECATED - Use `createSchemaService` instead. + * + * Allows an app to register a custom SchemaService + * for use when information about a resource's schema needs + * to be queried. + * + * This method can only be called more than once, but only one schema + * definition service may exist. Therefore if you wish to chain services + * you must lookup the existing service and close over it with the new + * service by accessing `store.schema` prior to registration. + * + * For Example: + * + * ```ts + * import Store from '@ember-data/store'; + * + * class SchemaDelegator { + * constructor(schema) { + * this._schema = schema; + * } + * + * hasResource(resource: { type: string }): boolean { + * if (AbstractSchemas.has(resource.type)) { + * return true; + * } + * return this._schema.hasResource(resource); + * } + * + * attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema { + * return this._schema.attributesDefinitionFor(identifier); + * } + * + * relationshipsDefinitionFor(identifier: RecordIdentifier | { type: string }): RelationshipsSchema { + * const schema = AbstractSchemas.get(identifier.type); + * return schema || this._schema.relationshipsDefinitionFor(identifier); + * } + * } + * + * export default class extends Store { + * constructor(...args) { + * super(...args); + * + * const schema = this.createSchemaService(); + * this.registerSchemaDefinitionService(new SchemaDelegator(schema)); + * } + * } + * ``` + * + * @method registerSchemaDefinitionService + * @param {SchemaService} schema + * @deprecated + * @public + */ + registerSchemaDefinitionService(schema: SchemaService): void; + + /** + * DEPRECATED - Use `createSchemaService` instead. + * + * Allows an app to register a custom SchemaService + * for use when information about a resource's schema needs + * to be queried. + * + * This method can only be called more than once, but only one schema + * definition service may exist. Therefore if you wish to chain services + * you must lookup the existing service and close over it with the new + * service by accessing `store.schema` prior to registration. + * + * For Example: + * + * ```ts + * import Store from '@ember-data/store'; + * + * class SchemaDelegator { + * constructor(schema) { + * this._schema = schema; + * } + * + * hasResource(resource: { type: string }): boolean { + * if (AbstractSchemas.has(resource.type)) { + * return true; + * } + * return this._schema.hasResource(resource); + * } + * + * attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema { + * return this._schema.attributesDefinitionFor(identifier); + * } + * + * relationshipsDefinitionFor(identifier: RecordIdentifier | { type: string }): RelationshipsSchema { + * const schema = AbstractSchemas.get(identifier.type); + * return schema || this._schema.relationshipsDefinitionFor(identifier); + * } + * } + * + * export default class extends Store { + * constructor(...args) { + * super(...args); + * + * const schema = this.schema; + * this.registerSchema(new SchemaDelegator(schema)); + * } + * } + * ``` + * + * @method registerSchema + * @param {SchemaService} schema + * @deprecated + * @public + */ + registerSchema(schema: SchemaService): void; } export class Store extends BaseClass { @@ -257,8 +447,11 @@ export class Store extends BaseClass { * @property {SchemaService} schema * @public */ - get schema(): SchemaService { - return this.getSchemaDefinitionService(); + get schema(): ReturnType { + if (!this._schema) { + this._schema = this.createSchemaService(); + } + return this._schema as ReturnType; } declare _schema: SchemaService; @@ -614,133 +807,6 @@ export class Store extends BaseClass { * @param record */ - /** - * Provides access to the SchemaDefinitionService instance - * for this Store instance. - * - * The SchemaDefinitionService can be used to query for - * information about the schema of a resource. - * - * @method getSchemaDefinitionService - * @public - */ - getSchemaDefinitionService(): SchemaService { - assert(`You must registerSchemaDefinitionService with the store to use custom model classes`, this._schema); - return this._schema; - } - - /** - * DEPRECATED - Use `registerSchema` instead. - * - * Allows an app to register a custom SchemaService - * for use when information about a resource's schema needs - * to be queried. - * - * This method can only be called more than once, but only one schema - * definition service may exist. Therefore if you wish to chain services - * you must lookup the existing service and close over it with the new - * service by accessing `store.schema` prior to registration. - * - * For Example: - * - * ```ts - * import Store from '@ember-data/store'; - * - * class SchemaDelegator { - * constructor(schema) { - * this._schema = schema; - * } - * - * doesTypeExist(type: string): boolean { - * if (AbstractSchemas.has(type)) { - * return true; - * } - * return this._schema.doesTypeExist(type); - * } - * - * attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema { - * return this._schema.attributesDefinitionFor(identifier); - * } - * - * relationshipsDefinitionFor(identifier: RecordIdentifier | { type: string }): RelationshipsSchema { - * const schema = AbstractSchemas.get(identifier.type); - * return schema || this._schema.relationshipsDefinitionFor(identifier); - * } - * } - * - * export default class extends Store { - * constructor(...args) { - * super(...args); - * - * const schema = this.schema; - * this.registerSchemaDefinitionService(new SchemaDelegator(schema)); - * } - * } - * ``` - * - * @method registerSchemaDefinitionService - * @param {SchemaService} schema - * @deprecated - * @public - */ - registerSchemaDefinitionService(schema: SchemaService) { - this._schema = schema; - } - /** - * Allows an app to register a custom SchemaService - * for use when information about a resource's schema needs - * to be queried. - * - * This method can only be called more than once, but only one schema - * definition service may exist. Therefore if you wish to chain services - * you must lookup the existing service and close over it with the new - * service by accessing `store.schema` prior to registration. - * - * For Example: - * - * ```ts - * import Store from '@ember-data/store'; - * - * class SchemaDelegator { - * constructor(schema) { - * this._schema = schema; - * } - * - * doesTypeExist(type: string): boolean { - * if (AbstractSchemas.has(type)) { - * return true; - * } - * return this._schema.doesTypeExist(type); - * } - * - * attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema { - * return this._schema.attributesDefinitionFor(identifier); - * } - * - * relationshipsDefinitionFor(identifier: RecordIdentifier | { type: string }): RelationshipsSchema { - * const schema = AbstractSchemas.get(identifier.type); - * return schema || this._schema.relationshipsDefinitionFor(identifier); - * } - * } - * - * export default class extends Store { - * constructor(...args) { - * super(...args); - * - * const schema = this.schema; - * this.registerSchema(new SchemaDelegator(schema)); - * } - * } - * ``` - * - * @method registerSchema - * @param {SchemaService} schema - * @public - */ - registerSchema(schema: SchemaService) { - this._schema = schema; - } - /** Returns the schema for a particular resource type (modelName). @@ -760,21 +826,20 @@ export class Store extends BaseClass { @method modelFor @public + @deprecated @param {string} type @return {ModelSchema} */ - // TODO @deprecate in favor of schema APIs, requires adapter/serializer overhaul or replacement modelFor(type: TypeFromInstance): ModelSchema; modelFor(type: string): ModelSchema; modelFor(type: T extends TypedRecordInstance ? TypeFromInstance : string): ModelSchema { + // FIXME add deprecation and deprecation stripping + // FIXME/TODO update RFC to remove this method if (DEBUG) { assertDestroyedStoreOnly(this, 'modelFor'); } assert(`You need to pass to the store's modelFor method`, typeof type === 'string' && type.length); - assert( - `No model was found for '${type}' and no schema handles the type`, - this.getSchemaDefinitionService().doesTypeExist(type) - ); + assert(`No model was found for '${type}' and no schema handles the type`, this.schema.hasResource({ type })); return getShimClass(this, type); } @@ -2299,6 +2364,46 @@ export class Store extends BaseClass { } } +if (ENABLE_LEGACY_SCHEMA_SERVICE) { + Store.prototype.getSchemaDefinitionService = function (): SchemaService { + assert(`You must registerSchemaDefinitionService with the store to use custom model classes`, this._schema); + deprecate(`Use \`store.schema\` instead of \`store.getSchemaDefinitionService()\``, false, { + id: 'ember-data:schema-service-updates', + until: '5.0', + for: 'ember-data', + since: { + available: '5.4', + enabled: '5.4', + }, + }); + return this._schema; + }; + Store.prototype.registerSchemaDefinitionService = function (schema: SchemaService) { + deprecate(`Use \`store.createSchemaService\` instead of \`store.registerSchemaDefinitionService()\``, false, { + id: 'ember-data:schema-service-updates', + until: '5.0', + for: 'ember-data', + since: { + available: '5.4', + enabled: '5.4', + }, + }); + this._schema = schema; + }; + Store.prototype.registerSchema = function (schema: SchemaService) { + deprecate(`Use \`store.createSchemaService\` instead of \`store.registerSchema()\``, false, { + id: 'ember-data:schema-service-updates', + until: '5.0', + for: 'ember-data', + since: { + available: '5.4', + enabled: '5.4', + }, + }); + this._schema = schema; + }; +} + let assertDestroyingStore: (store: Store, method: string) => void; let assertDestroyedStoreOnly: (store: Store, method: string) => void; @@ -2348,27 +2453,24 @@ function normalizeProperties( const { type } = identifier; // convert relationship Records to RecordDatas before passing to RecordData - const defs = store.getSchemaDefinitionService().relationshipsDefinitionFor({ type }); + const defs = store.schema.fields({ type }); - if (defs !== null) { + if (defs.size) { const keys = Object.keys(properties); - let relationshipValue; for (let i = 0; i < keys.length; i++) { const prop = keys[i]; - const def = defs[prop]; + const field = defs.get(prop); - if (def !== undefined) { - if (def.kind === 'hasMany') { - if (DEBUG) { - assertRecordsPassedToHasMany(properties[prop] as OpaqueRecordInstance[]); - } - relationshipValue = extractIdentifiersFromRecords(properties[prop] as OpaqueRecordInstance[]); - } else { - relationshipValue = extractIdentifierFromRecord(properties[prop]); - } + if (!field) continue; - properties[prop] = relationshipValue; + if (field.kind === 'hasMany') { + if (DEBUG) { + assertRecordsPassedToHasMany(properties[prop] as OpaqueRecordInstance[]); + } + properties[prop] = extractIdentifiersFromRecords(properties[prop] as OpaqueRecordInstance[]); + } else if (field.kind === 'belongsTo') { + properties[prop] = extractIdentifierFromRecord(properties[prop]); } } } diff --git a/packages/store/src/-types/q/cache-capabilities-manager.ts b/packages/store/src/-types/q/cache-capabilities-manager.ts index 867deb4b386..43abf1eb8e1 100644 --- a/packages/store/src/-types/q/cache-capabilities-manager.ts +++ b/packages/store/src/-types/q/cache-capabilities-manager.ts @@ -34,6 +34,8 @@ export type CacheCapabilitiesManager = { identifierCache: IdentifierCache; /** + * DEPRECATED - use the schema property + * * Provides access to the SchemaService instance * for this Store instance. * @@ -41,6 +43,7 @@ export type CacheCapabilitiesManager = { * information about the schema of a resource. * * @method getSchemaDefinitionService + * @deprecated * @public */ getSchemaDefinitionService(): SchemaService; diff --git a/packages/store/src/-types/q/schema-service.ts b/packages/store/src/-types/q/schema-service.ts index 31dfc66ce39..7dfe075cecb 100644 --- a/packages/store/src/-types/q/schema-service.ts +++ b/packages/store/src/-types/q/schema-service.ts @@ -2,12 +2,16 @@ @module @ember-data/store */ +import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { RecordIdentifier } from '@warp-drive/core-types/identifier'; +import type { ObjectValue } from '@warp-drive/core-types/json/raw'; +import type { Derivation, HashFn, Transformation } from '@warp-drive/core-types/schema/concepts'; import type { FieldSchema, LegacyAttributeField, LegacyBelongsToField, LegacyHasManyField, + ResourceSchema, } from '@warp-drive/core-types/schema/fields'; export type AttributesSchema = Record; @@ -32,18 +36,16 @@ export type RelationshipsSchema = Record SchemaService * @public */ export interface SchemaService { /** - * Queries whether the schema-definition-service recognizes `type` as a resource type + * DEPRECATED - use `hasResource` instead + * + * Queries whether the SchemaService recognizes `type` as a resource type * * @method doesTypeExist * @public + * @deprecated + * @param {string} type + * @return {boolean} + */ + doesTypeExist?(type: string): boolean; + + /** + * Queries whether the SchemaService recognizes `type` as a resource type + * + * @method hasResource + * @public + * @param {StableRecordIdentifier|{ type: string }} resource + * @return {boolean} + */ + hasResource(resource: { type: string } | StableRecordIdentifier): boolean; + + /** + * Queries whether the SchemaService recognizes `type` as a resource trait + * + * @method hasTrait + * @public * @param {string} type * @return {boolean} */ - doesTypeExist(type: string): boolean; + hasTrait(type: string): boolean; + + /** + * Queries whether the given resource has the given trait + * + * @method resourceHasTrait + * @public + * @param {StableRecordIdentifier|{ type: string }} resource + * @param {string} trait + * @return {boolean} + */ + resourceHasTrait(resource: { type: string } | StableRecordIdentifier, trait: string): boolean; + + /** + * Queries for the fields of a given resource type or resource identity. + * + * Should error if the resource type is not recognized. + * + * @method fields + * @public + * @param {StableRecordIdentifier|{ type: string }} resource + * @return {Map} + */ + fields(resource: { type: string } | StableRecordIdentifier): Map; + + /** + * Returns the transformation registered with the provided name. + * + * @method transformation + * @public + * @param name + * @returns {Transformation} + */ + transformation(name: string): Transformation; - fields({ type }: { type: string }): Map; + /** + * Returns the hash function registered with the provided name. + * + * @method hashFn + * @public + * @param name + * @returns {HashFn} + */ + hashFn(name: string): HashFn; /** + * Returns the derivation registered with the provided name. + * + * @method derivation + * @public + * @param name + * @returns {Derivation} + */ + derivation(name: string): Derivation; + + /** + * Returns the schema for the provided resource type. + * + * @method resource + * @public + * @param {StableRecordIdentifier|{ type: string }} resource + * @return {ResourceSchema} + */ + resource(resource: { type: string } | StableRecordIdentifier): ResourceSchema; + + /** + * Enables registration of multiple ResourceSchemas at once. + * + * This can be useful for either pre-loading schema information + * or for registering schema information delivered by API calls + * or other sources just-in-time. + * + * @method registerResources + * @public + * @param schemas + */ + registerResources(schemas: ResourceSchema[]): void; + + /** + * Enables registration of a single ResourceSchema. + * + * This can be useful for either pre-loading schema information + * or for registering schema information delivered by API calls + * or other sources just-in-time. + * + * @method registerResource + * @public + * @param {ResourceSchema} schema + */ + registerResource(schema: ResourceSchema): void; + + /** + * Enables registration of a transformation. + * + * The transformation can later be retrieved by the name + * attached to it's `[Type]` property. + * + * @method registerTransformations + * @public + * @param {Transformation} transform + */ + registerTransformation(transform: Transformation): void; + + /** + * Enables registration of a derivation. + * + * The derivation can later be retrieved by the name + * attached to it's `[Type]` property. + * + * @method registerDerivations + * @public + * @param {Derivation} derivation + */ + registerDerivation(derivation: Derivation): void; + + /** + * Enables registration of a hashing function + * + * The hashing function can later be retrieved by the name + * attached to it's `[Type]` property. + * + * @method registerHashFn + * @public + * @param {HashFn} hashfn + */ + registerHashFn(hashFn: HashFn): void; + + /** + * DEPRECATED - use `fields` instead + * * Returns definitions for all properties of the specified resource * that are considered "attributes". Generally these are properties * that are not related to book-keeping state on the client and do @@ -125,12 +267,15 @@ export interface SchemaService { * * @method attributesDefinitionFor * @public + * @deprecated * @param {RecordIdentifier|{ type: string }} identifier * @return {AttributesSchema} */ - attributesDefinitionFor(identifier: RecordIdentifier | { type: string }): AttributesSchema; + attributesDefinitionFor?(identifier: RecordIdentifier | { type: string }): AttributesSchema; /** + * DEPRECATED - use `fields` instead + * * Returns definitions for all properties of the specified resource * that are considered "relationships". Generally these are properties * that represent a linkage to another resource. @@ -205,8 +350,9 @@ export interface SchemaService { * * @method relationshipsDefinitionFor * @public + * @deprecated * @param {RecordIdentifier|{ type: string }} identifier * @return {RelationshipsSchema} */ - relationshipsDefinitionFor(identifier: RecordIdentifier | { type: string }): RelationshipsSchema; + relationshipsDefinitionFor?(identifier: RecordIdentifier | { type: string }): RelationshipsSchema; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c95e6a05c84..ac4a9613be0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -175,9 +175,6 @@ importers: globals: specifier: ^15.2.0 version: 15.2.0 - minimatch: - specifier: ^9.0.4 - version: 9.0.4 rollup: specifier: ^4.17.2 version: 4.17.2 @@ -193,9 +190,6 @@ importers: vite-plugin-dts: specifier: ^3.9.1 version: 3.9.1(rollup@4.17.2)(typescript@5.4.5)(vite@5.2.11) - walk-sync: - specifier: ^3.0.0 - version: 3.0.0 packages/-ember-data: dependencies: diff --git a/tests/builders/app/services/store.ts b/tests/builders/app/services/store.ts index 8d1250cbd70..0b3c4821783 100644 --- a/tests/builders/app/services/store.ts +++ b/tests/builders/app/services/store.ts @@ -17,8 +17,10 @@ export default class Store extends DataStore { const manager = (this.requestManager = new RequestManager()); manager.use([Fetch]); manager.useCache(CacheHandler); + } - this.registerSchema(buildSchema(this)); + createSchemaService(): ReturnType { + return buildSchema(this); } override createCache(capabilities: CacheCapabilitiesManager): Cache { diff --git a/tests/builders/tests/integration/create-record-test.ts b/tests/builders/tests/integration/create-record-test.ts index 6381b66acc5..74049fc87e9 100644 --- a/tests/builders/tests/integration/create-record-test.ts +++ b/tests/builders/tests/integration/create-record-test.ts @@ -23,8 +23,10 @@ class TestStore extends DataStore { const manager = (this.requestManager = new RequestManager()); manager.useCache(CacheHandler); + } - this.registerSchema(buildSchema(this)); + createSchemaService(): ReturnType { + return buildSchema(this); } override createCache(capabilities: CacheCapabilitiesManager): Cache { diff --git a/tests/builders/tests/integration/delete-record-test.ts b/tests/builders/tests/integration/delete-record-test.ts index ed763163585..cb90491a5ed 100644 --- a/tests/builders/tests/integration/delete-record-test.ts +++ b/tests/builders/tests/integration/delete-record-test.ts @@ -23,8 +23,10 @@ class TestStore extends DataStore { const manager = (this.requestManager = new RequestManager()); manager.useCache(CacheHandler); + } - this.registerSchema(buildSchema(this)); + createSchemaService(): ReturnType { + return buildSchema(this); } override createCache(capabilities: CacheCapabilitiesManager): Cache { diff --git a/tests/builders/tests/integration/update-record-test.ts b/tests/builders/tests/integration/update-record-test.ts index 85ca8cf3ce5..98acf038590 100644 --- a/tests/builders/tests/integration/update-record-test.ts +++ b/tests/builders/tests/integration/update-record-test.ts @@ -23,8 +23,10 @@ class TestStore extends DataStore { const manager = (this.requestManager = new RequestManager()); manager.useCache(CacheHandler); + } - this.registerSchema(buildSchema(this)); + createSchemaService(): ReturnType { + return buildSchema(this); } override createCache(capabilities: CacheCapabilitiesManager): Cache { diff --git a/tests/docs/fixtures/expected.js b/tests/docs/fixtures/expected.js index 4785b53d051..e566e10750d 100644 --- a/tests/docs/fixtures/expected.js +++ b/tests/docs/fixtures/expected.js @@ -80,6 +80,7 @@ module.exports = { '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_NON_UNIQUE_PAYLOADS', '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE', '(public) @warp-drive/build-config/deprecations CurrentDeprecations#DEPRECATE_STORE_EXTENDS_EMBER_OBJECT', + '(public) @warp-drive/build-config/deprecations CurrentDeprecations#ENABLE_LEGACY_SCHEMA_SERVICE', '(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', @@ -542,6 +543,19 @@ module.exports = { '(public) @ember-data/store SchemaService#attributesDefinitionFor', '(public) @ember-data/store SchemaService#doesTypeExist', '(public) @ember-data/store SchemaService#relationshipsDefinitionFor', + '(public) @ember-data/store SchemaService#derivation', + '(public) @ember-data/store SchemaService#fields', + '(public) @ember-data/store SchemaService#hasResource', + '(public) @ember-data/store SchemaService#hasTrait', + '(public) @ember-data/store SchemaService#hashFn', + '(public) @ember-data/store SchemaService#registerDerivations', + '(public) @ember-data/store SchemaService#registerHashFn', + '(public) @ember-data/store SchemaService#registerResource', + '(public) @ember-data/store SchemaService#registerResources', + '(public) @ember-data/store SchemaService#registerTransformations', + '(public) @ember-data/store SchemaService#resource', + '(public) @ember-data/store SchemaService#resourceHasTrait', + '(public) @ember-data/store SchemaService#transformation', '(public) @ember-data/store Snapshot#adapterOptions', '(public) @ember-data/store Snapshot#attr', '(public) @ember-data/store Snapshot#attributes', diff --git a/tests/ember-data__adapter/app/services/store.ts b/tests/ember-data__adapter/app/services/store.ts index a6b0de4bebc..2e360e8abb2 100644 --- a/tests/ember-data__adapter/app/services/store.ts +++ b/tests/ember-data__adapter/app/services/store.ts @@ -23,7 +23,10 @@ export default class Store extends BaseStore { this.requestManager = new RequestManager(); this.requestManager.use([LegacyNetworkHandler, Fetch]); this.requestManager.useCache(CacheHandler); - this.registerSchema(buildSchema(this)); + } + + createSchemaService(): ReturnType { + return buildSchema(this); } override createCache(capabilities: CacheCapabilitiesManager) { diff --git a/tests/ember-data__graph/app/services/store.ts b/tests/ember-data__graph/app/services/store.ts index a6b0de4bebc..2e360e8abb2 100644 --- a/tests/ember-data__graph/app/services/store.ts +++ b/tests/ember-data__graph/app/services/store.ts @@ -23,7 +23,10 @@ export default class Store extends BaseStore { this.requestManager = new RequestManager(); this.requestManager.use([LegacyNetworkHandler, Fetch]); this.requestManager.useCache(CacheHandler); - this.registerSchema(buildSchema(this)); + } + + createSchemaService(): ReturnType { + return buildSchema(this); } override createCache(capabilities: CacheCapabilitiesManager) { diff --git a/tests/ember-data__json-api/app/styles/app.css b/tests/ember-data__json-api/app/styles/app.css index 5ad62c57d68..e69de29bb2d 100644 --- a/tests/ember-data__json-api/app/styles/app.css +++ b/tests/ember-data__json-api/app/styles/app.css @@ -1 +0,0 @@ -/* @import "@warp-drive/diagnostic/dist/styles/dom-reporter.css"; */ diff --git a/tests/ember-data__json-api/tests/integration/cache/collection-data-documents-test.ts b/tests/ember-data__json-api/tests/integration/cache/collection-data-documents-test.ts index eb4a6c0225c..d99aa8a37c1 100644 --- a/tests/ember-data__json-api/tests/integration/cache/collection-data-documents-test.ts +++ b/tests/ember-data__json-api/tests/integration/cache/collection-data-documents-test.ts @@ -2,15 +2,13 @@ import Cache from '@ember-data/json-api'; import type { StructuredDataDocument } from '@ember-data/request'; import type { NotificationType } from '@ember-data/store'; import Store from '@ember-data/store'; -import type { CacheCapabilitiesManager, SchemaService } from '@ember-data/store/types'; +import type { CacheCapabilitiesManager } from '@ember-data/store/types'; import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; -import type { FieldSchema } from '@warp-drive/core-types/schema/fields'; import type { CollectionResourceDataDocument } from '@warp-drive/core-types/spec/document'; import type { CollectionResourceDocument, ResourceObject } from '@warp-drive/core-types/spec/json-api-raw'; import { module, test } from '@warp-drive/diagnostic'; -type AttributesSchema = ReturnType; -type RelationshipsSchema = ReturnType; +import { TestSchema } from '../../utils/schema'; function asStructuredDocument(doc: { request?: { url: string; cacheOptions?: { key?: string } }; @@ -20,7 +18,12 @@ function asStructuredDocument(doc: { } type FakeRecord = { [key: string]: unknown; destroy: () => void }; + class TestStore extends Store { + createSchemaService() { + return new TestSchema(); + } + override createCache(wrapper: CacheCapabilitiesManager) { return new Cache(wrapper); } @@ -51,55 +54,9 @@ class TestStore extends Store { } } -type Schemas = Record; -class TestSchema { - declare schemas: Schemas; - constructor(schemas?: Schemas) { - this.schemas = schemas || ({} as Schemas); - } - - attributesDefinitionFor(identifier: { type: T }): AttributesSchema { - return this.schemas[identifier.type]?.attributes || {}; - } - - _fieldsDefCache: Record> = {}; - - fields(identifier: { type: T }): Map { - const { type } = identifier; - let fieldDefs: Map | undefined = this._fieldsDefCache[type]; - - if (fieldDefs === undefined) { - fieldDefs = new Map(); - this._fieldsDefCache[type] = fieldDefs; - - const attributes = this.attributesDefinitionFor(identifier); - const relationships = this.relationshipsDefinitionFor(identifier); - - for (const attr of Object.values(attributes)) { - fieldDefs.set(attr.name, attr); - } - - for (const rel of Object.values(relationships)) { - fieldDefs.set(rel.name, rel); - } - } - - return fieldDefs; - } - - relationshipsDefinitionFor(identifier: { type: T }): RelationshipsSchema { - return this.schemas[identifier.type]?.relationships || {}; - } - - doesTypeExist(type: string) { - return type in this.schemas ? true : Object.keys(this.schemas).length === 0 ? true : false; - } -} - module('Integration | @ember-data/json-api Cache.put()', function () { test('simple collection resource documents are correctly managed', function (assert) { const store = new TestStore(); - store.registerSchema(new TestSchema()); const responseDocument = store.cache.put( asStructuredDocument({ @@ -119,7 +76,6 @@ module('Integration | @ember-data/json-api Cache.put()', test('collection resource documents are correctly cached', function (assert) { const store = new TestStore(); - store.registerSchema(new TestSchema()); const responseDocument = store.cache.put( asStructuredDocument({ @@ -170,7 +126,6 @@ module('Integration | @ember-data/json-api Cache.put()', test('resources are accessible via `peek`', function (assert) { const store = new TestStore(); - store.registerSchema(new TestSchema()); const responseDocument = store.cache.put( asStructuredDocument({ @@ -242,45 +197,40 @@ module('Integration | @ember-data/json-api Cache.put()', test('resource relationships are accessible via `peek`', function (assert) { const store = new TestStore(); - - store.registerSchema( - new TestSchema<'user'>({ - user: { - attributes: { - name: { kind: 'attribute', name: 'name', type: null }, + store.schema.registerResource({ + identity: null, + type: 'user', + fields: [ + { kind: 'attribute', name: 'name', type: null }, + { + kind: 'belongsTo', + type: 'user', + name: 'bestFriend', + options: { + async: false, + inverse: 'bestFriend', }, - relationships: { - bestFriend: { - kind: 'belongsTo', - type: 'user', - name: 'bestFriend', - options: { - async: false, - inverse: 'bestFriend', - }, - }, - worstEnemy: { - kind: 'belongsTo', - type: 'user', - name: 'worstEnemy', - options: { - async: false, - inverse: null, - }, - }, - friends: { - kind: 'hasMany', - type: 'user', - name: 'friends', - options: { - async: false, - inverse: 'friends', - }, - }, + }, + { + kind: 'belongsTo', + type: 'user', + name: 'worstEnemy', + options: { + async: false, + inverse: null, }, }, - }) - ); + { + kind: 'hasMany', + type: 'user', + name: 'friends', + options: { + async: false, + inverse: 'friends', + }, + }, + ], + }); let responseDocument: CollectionResourceDataDocument; store._run(() => { diff --git a/tests/ember-data__json-api/tests/integration/cache/error-documents-test.ts b/tests/ember-data__json-api/tests/integration/cache/error-documents-test.ts index 5890598b964..143e01776c5 100644 --- a/tests/ember-data__json-api/tests/integration/cache/error-documents-test.ts +++ b/tests/ember-data__json-api/tests/integration/cache/error-documents-test.ts @@ -1,6 +1,7 @@ import Cache from '@ember-data/json-api'; import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; +import { buildBaseURL } from '@ember-data/request-utils'; import Store, { CacheHandler } from '@ember-data/store'; import type { CacheCapabilitiesManager } from '@ember-data/store/types'; import { module, test } from '@warp-drive/diagnostic'; @@ -59,17 +60,14 @@ module('Integration | @ember-data/json-api Cach.put()', function RECORD ); + const url = buildBaseURL({ resourcePath: 'users/1' }); try { - await store.request({ url: 'https://localhost:1135/users/1' }); + await store.request({ url }); assert.ok(false, 'Should have thrown'); } catch (e) { isNetworkError(e); assert.true(e instanceof AggregateError, 'The error is an AggregateError'); - assert.equal( - e.message, - '[404 Not Found] GET (cors) - https://localhost:1135/users/1', - 'The error message is correct' - ); + assert.equal(e.message, `[404 Not Found] GET (cors) - ${url}`, 'The error message is correct'); assert.equal(e.status, 404, 'The error status is correct'); assert.equal(e.statusText, 'Not Found', 'The error statusText is correct'); assert.equal(e.code, 404, 'The error code is correct'); diff --git a/tests/ember-data__json-api/tests/integration/cache/meta-documents-test.ts b/tests/ember-data__json-api/tests/integration/cache/meta-documents-test.ts index 315e80134b0..677f39a5d02 100644 --- a/tests/ember-data__json-api/tests/integration/cache/meta-documents-test.ts +++ b/tests/ember-data__json-api/tests/integration/cache/meta-documents-test.ts @@ -2,14 +2,12 @@ import Cache from '@ember-data/json-api'; import type { StructuredDataDocument, StructuredDocument } from '@ember-data/request'; import type { CacheOperation } from '@ember-data/store'; import Store from '@ember-data/store'; -import type { CacheCapabilitiesManager, SchemaService } from '@ember-data/store/types'; +import type { CacheCapabilitiesManager } from '@ember-data/store/types'; import type { StableDocumentIdentifier, StableExistingRecordIdentifier } from '@warp-drive/core-types/identifier'; -import type { LegacyFieldSchema as FieldSchema } from '@warp-drive/core-types/schema/fields'; import type { CollectionResourceDataDocument, ResourceMetaDocument } from '@warp-drive/core-types/spec/document'; import { module, test } from '@warp-drive/diagnostic'; -type AttributesSchema = ReturnType; -type RelationshipsSchema = ReturnType; +import { TestSchema } from '../../utils/schema'; function asStructuredDocument(doc: { request?: { url: string; cacheOptions?: { key?: string } }; @@ -19,56 +17,14 @@ function asStructuredDocument(doc: { } class TestStore extends Store { + createSchemaService() { + return new TestSchema(); + } override createCache(wrapper: CacheCapabilitiesManager) { return new Cache(wrapper); } } -type Schemas = Record; -class TestSchema { - declare schemas: Schemas; - constructor(schemas?: Schemas) { - this.schemas = schemas || ({} as Schemas); - } - - attributesDefinitionFor(identifier: { type: T }): AttributesSchema { - return this.schemas[identifier.type]?.attributes || {}; - } - - _fieldsDefCache: Record> = {}; - - fields(identifier: { type: T }): Map { - const { type } = identifier; - let fieldDefs: Map | undefined = this._fieldsDefCache[type]; - - if (fieldDefs === undefined) { - fieldDefs = new Map(); - this._fieldsDefCache[type] = fieldDefs; - - const attributes = this.attributesDefinitionFor(identifier); - const relationships = this.relationshipsDefinitionFor(identifier); - - for (const attr of Object.values(attributes)) { - fieldDefs.set(attr.name, attr); - } - - for (const rel of Object.values(relationships)) { - fieldDefs.set(rel.name, rel); - } - } - - return fieldDefs; - } - - relationshipsDefinitionFor(identifier: { type: T }): RelationshipsSchema { - return this.schemas[identifier.type]?.relationships || {}; - } - - doesTypeExist(type: string) { - return type in this.schemas ? true : Object.keys(this.schemas).length === 0 ? true : false; - } -} - module('Integration | @ember-data/json-api Cach.put()', function (hooks) { test('meta documents are correctly cached', function (assert) { const store = new TestStore(); @@ -232,7 +188,6 @@ module('Integration | @ember-data/json-api Cach.put()', function ( test('updating cache with a meta document disregards prior data', function (assert) { const store = new TestStore(); - store.registerSchema(new TestSchema()); const responseDocument = store.cache.put( asStructuredDocument({ diff --git a/tests/ember-data__json-api/tests/integration/cache/resource-data-documents-test.ts b/tests/ember-data__json-api/tests/integration/cache/resource-data-documents-test.ts index 59c2e3f358e..e3c6f9a44a0 100644 --- a/tests/ember-data__json-api/tests/integration/cache/resource-data-documents-test.ts +++ b/tests/ember-data__json-api/tests/integration/cache/resource-data-documents-test.ts @@ -2,22 +2,25 @@ import Cache from '@ember-data/json-api'; import type { StructuredDataDocument, StructuredDocument } from '@ember-data/request'; import type { CacheOperation, NotificationType } from '@ember-data/store'; import Store from '@ember-data/store'; -import type { CacheCapabilitiesManager, SchemaService } from '@ember-data/store/types'; +import type { CacheCapabilitiesManager } from '@ember-data/store/types'; import type { StableDocumentIdentifier, StableExistingRecordIdentifier, StableRecordIdentifier, } from '@warp-drive/core-types/identifier'; -import type { FieldSchema } from '@warp-drive/core-types/schema/fields'; import type { SingleResourceDataDocument } from '@warp-drive/core-types/spec/document'; import type { SingleResourceDocument } from '@warp-drive/core-types/spec/json-api-raw'; import { module, test } from '@warp-drive/diagnostic'; -type AttributesSchema = ReturnType; -type RelationshipsSchema = ReturnType; +import { TestSchema } from '../../utils/schema'; type FakeRecord = { [key: string]: unknown; destroy: () => void }; + class TestStore extends Store { + createSchemaService() { + return new TestSchema(); + } + override createCache(wrapper: CacheCapabilitiesManager) { return new Cache(wrapper); } @@ -48,51 +51,6 @@ class TestStore extends Store { } } -type Schemas = Record; -class TestSchema { - declare schemas: Schemas; - constructor(schemas?: Schemas) { - this.schemas = schemas || ({} as Schemas); - } - - attributesDefinitionFor(identifier: { type: T }): AttributesSchema { - return this.schemas[identifier.type]?.attributes || {}; - } - - _fieldsDefCache: Record> = {}; - - fields(identifier: { type: T }): Map { - const { type } = identifier; - let fieldDefs: Map | undefined = this._fieldsDefCache[type]; - - if (fieldDefs === undefined) { - fieldDefs = new Map(); - this._fieldsDefCache[type] = fieldDefs; - - const attributes = this.attributesDefinitionFor(identifier); - const relationships = this.relationshipsDefinitionFor(identifier); - - for (const attr of Object.values(attributes)) { - fieldDefs.set(attr.name, attr); - } - - for (const rel of Object.values(relationships)) { - fieldDefs.set(rel.name, rel); - } - } - - return fieldDefs; - } - - relationshipsDefinitionFor(identifier: { type: T }): RelationshipsSchema { - return this.schemas[identifier.type]?.relationships || {}; - } - - doesTypeExist(type: string) { - return type in this.schemas ? true : Object.keys(this.schemas).length === 0 ? true : false; - } -} - function asStructuredDocument(doc: { request?: { url: string; cacheOptions?: { key?: string } }; content: T; @@ -103,7 +61,6 @@ function asStructuredDocument(doc: { module('Integration | @ember-data/json-api Cache.put()', function (hooks) { test('simple single resource documents are correctly managed', function (assert) { const store = new TestStore(); - store.registerSchema(new TestSchema()); const responseDocument = store.cache.put( asStructuredDocument({ @@ -122,7 +79,6 @@ module('Integration | @ember-data/json-api Cache.put()', f test('single resource documents are correctly cached', function (assert) { const store = new TestStore(); - store.registerSchema(new TestSchema()); const responseDocument = store.cache.put( asStructuredDocument({ @@ -164,7 +120,6 @@ module('Integration | @ember-data/json-api Cache.put()', f test('data documents respect cacheOptions.key', function (assert) { const store = new TestStore(); - store.registerSchema(new TestSchema()); const responseDocument = store.cache.put( asStructuredDocument({ @@ -212,7 +167,6 @@ module('Integration | @ember-data/json-api Cache.put()', f test("notifications are generated for create and update of the document's cache key", function (assert) { assert.expect(10); const store = new TestStore(); - store.registerSchema(new TestSchema()); const documentIdentifier = store.identifierCache.getOrCreateDocumentIdentifier({ url: '/api/v1/query?type=user&name=Chris&limit=1', })!; @@ -273,7 +227,6 @@ module('Integration | @ember-data/json-api Cache.put()', f test('resources are accessible via `peek`', function (assert) { const store = new TestStore(); - store.registerSchema(new TestSchema()); const responseDocument = store.cache.put( asStructuredDocument({ @@ -345,45 +298,40 @@ module('Integration | @ember-data/json-api Cache.put()', f test('single resource relationships are accessible via `peek`', function (assert) { const store = new TestStore(); - - store.registerSchema( - new TestSchema<'user'>({ - user: { - attributes: { - name: { kind: 'attribute', name: 'name', type: null }, + store.schema.registerResource({ + identity: null, + type: 'user', + fields: [ + { name: 'name', kind: 'attribute', type: null }, + { + name: 'bestFriend', + kind: 'belongsTo', + type: 'user', + options: { + async: false, + inverse: 'bestFriend', }, - relationships: { - bestFriend: { - kind: 'belongsTo', - type: 'user', - name: 'bestFriend', - options: { - async: false, - inverse: 'bestFriend', - }, - }, - worstEnemy: { - kind: 'belongsTo', - type: 'user', - name: 'worstEnemy', - options: { - async: false, - inverse: null, - }, - }, - friends: { - kind: 'hasMany', - type: 'user', - name: 'friends', - options: { - async: false, - inverse: 'friends', - }, - }, + }, + { + name: 'worstEnemy', + kind: 'belongsTo', + type: 'user', + options: { + async: false, + inverse: null, }, }, - }) - ); + { + name: 'friends', + kind: 'hasMany', + type: 'user', + options: { + async: false, + inverse: 'friends', + }, + }, + ], + }); let responseDocument: SingleResourceDataDocument; store._run(() => { @@ -520,27 +468,24 @@ module('Integration | @ember-data/json-api Cache.put()', f const store = new TestStore(); let i = 0; - store.registerSchema( - new TestSchema<'user'>({ - user: { - attributes: { - name: { - kind: 'attribute', - name: 'name', - type: null, - options: { - // @ts-expect-error functions are not allowed in schema - defaultValue: () => { - i++; - return `Name ${i}`; - }, - }, + store.schema.registerResource({ + identity: null, + type: 'user', + fields: [ + { + name: 'name', + kind: 'attribute', + type: null, + options: { + // @ts-expect-error functions are not allowed in schema + defaultValue: () => { + i++; + return `Name ${i}`; }, }, - relationships: {}, }, - }) - ); + ], + }); store._run(() => { store.cache.put( diff --git a/tests/ember-data__json-api/tests/integration/serialize-test.ts b/tests/ember-data__json-api/tests/integration/serialize-test.ts index 17480a093bf..b1eefbaebcc 100644 --- a/tests/ember-data__json-api/tests/integration/serialize-test.ts +++ b/tests/ember-data__json-api/tests/integration/serialize-test.ts @@ -4,12 +4,16 @@ import type { NotificationType } from '@ember-data/store'; import Store from '@ember-data/store'; import type { CacheCapabilitiesManager } from '@ember-data/store/types'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; -import type { FieldSchema, LegacyAttributeField, LegacyRelationshipSchema } from '@warp-drive/core-types/schema/fields'; import type { ResourceObject } from '@warp-drive/core-types/spec/json-api-raw'; import { module, test } from '@warp-drive/diagnostic'; +import { TestSchema } from '../utils/schema'; + type FakeRecord = { [key: string]: unknown; destroy: () => void }; class TestStore extends Store { + createSchemaService() { + return new TestSchema(); + } override createCache(wrapper: CacheCapabilitiesManager) { return new Cache(wrapper); } @@ -40,97 +44,45 @@ class TestStore extends Store { } } -type AttributesSchema = Record; -type RelationshipsSchema = Record; -type Schemas = Record; -class TestSchema { - declare schemas: Schemas; - constructor(schemas?: Schemas) { - this.schemas = schemas || ({} as Schemas); - } - - attributesDefinitionFor(identifier: { type: T }): AttributesSchema { - return this.schemas[identifier.type]?.attributes || {}; - } - - _fieldsDefCache: Record> = {}; - - fields(identifier: { type: T }): Map { - const { type } = identifier; - let fieldDefs: Map | undefined = this._fieldsDefCache[type]; - - if (fieldDefs === undefined) { - fieldDefs = new Map(); - this._fieldsDefCache[type] = fieldDefs; - - const attributes = this.attributesDefinitionFor(identifier); - const relationships = this.relationshipsDefinitionFor(identifier); - - for (const attr of Object.values(attributes)) { - fieldDefs.set(attr.name, attr); - } - - for (const rel of Object.values(relationships)) { - fieldDefs.set(rel.name, rel); - } - } - - return fieldDefs; - } - - relationshipsDefinitionFor(identifier: { type: T }): RelationshipsSchema { - return this.schemas[identifier.type]?.relationships || {}; - } - - doesTypeExist(type: string) { - return type in this.schemas ? true : Object.keys(this.schemas).length === 0 ? true : false; - } -} - module('Integration | @ember-data/json-api/request', function (hooks) { let store: TestStore; hooks.beforeEach(function () { store = new TestStore(); - - store.registerSchema( - new TestSchema<'user'>({ - user: { - attributes: { - firstName: { kind: 'attribute', name: 'firstName', type: null }, - lastName: { kind: 'attribute', name: 'lastName', type: null }, + store.schema.registerResource({ + identity: null, + type: 'user', + fields: [ + { kind: 'attribute', name: 'firstName', type: null }, + { kind: 'attribute', name: 'lastName', type: null }, + { + kind: 'belongsTo', + type: 'user', + name: 'bestFriend', + options: { + async: false, + inverse: 'bestFriend', }, - relationships: { - bestFriend: { - kind: 'belongsTo', - type: 'user', - name: 'bestFriend', - options: { - async: false, - inverse: 'bestFriend', - }, - }, - worstEnemy: { - kind: 'belongsTo', - type: 'user', - name: 'worstEnemy', - options: { - async: false, - inverse: null, - }, - }, - friends: { - kind: 'hasMany', - type: 'user', - name: 'friends', - options: { - async: false, - inverse: 'friends', - }, - }, + }, + { + kind: 'belongsTo', + type: 'user', + name: 'worstEnemy', + options: { + async: false, + inverse: null, }, }, - }) - ); + { + kind: 'hasMany', + type: 'user', + name: 'friends', + options: { + async: false, + inverse: 'friends', + }, + }, + ], + }); store.push({ data: { diff --git a/tests/ember-data__json-api/tests/utils/schema.ts b/tests/ember-data__json-api/tests/utils/schema.ts new file mode 100644 index 00000000000..e50f84da213 --- /dev/null +++ b/tests/ember-data__json-api/tests/utils/schema.ts @@ -0,0 +1,126 @@ +import type { SchemaService } from '@ember-data/store/types'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { Value } from '@warp-drive/core-types/json/raw'; +import type { Derivation, HashFn, Transformation } from '@warp-drive/core-types/schema/concepts'; +import type { + FieldSchema, + LegacyAttributeField, + LegacyRelationshipSchema, + ResourceSchema, +} from '@warp-drive/core-types/schema/fields'; +import { Type } from '@warp-drive/core-types/symbols'; + +type InternalSchema = { + original: ResourceSchema; + traits: Set; + fields: Map; + attributes: Record; + relationships: Record; +}; + +export class TestSchema implements SchemaService { + declare _schemas: Map; + declare _transforms: Map; + declare _hashFns: Map; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + declare _derivations: Map>; + declare _traits: Set; + + constructor() { + this._schemas = new Map(); + this._transforms = new Map(); + this._hashFns = new Map(); + this._derivations = new Map(); + } + hasTrait(type: string): boolean { + return this._traits.has(type); + } + resourceHasTrait(resource: StableRecordIdentifier | { type: string }, trait: string): boolean { + return this._schemas.get(resource.type)!.traits.has(trait); + } + transformation(name: string): Transformation { + assert(`No transformation registered with name ${name}`, this._transforms.has(name)); + return this._transforms.get(name)!; + } + derivation(name: string): Derivation { + assert(`No derivation registered with name ${name}`, this._derivations.has(name)); + return this._derivations.get(name)!; + } + resource(resource: StableRecordIdentifier | { type: string }): ResourceSchema { + assert(`No resource registered with name ${resource.type}`, this._schemas.has(resource.type)); + return this._schemas.get(resource.type)!.original; + } + hashFn(name: string): HashFn { + assert(`No hash function registered with name ${name}`, this._hashFns.has(name)); + return this._hashFns.get(name)!; + } + + registerTransformation(transformation: Transformation): void { + this._transforms.set(transformation[Type], transformation as Transformation); + } + + registerDerivation(derivation: Derivation): void { + this._derivations.set(derivation[Type], derivation); + } + + registerHashFn(hashFn: HashFn): void { + this._hashFns.set(hashFn[Type], hashFn as HashFn); + } + + registerResource(schema: ResourceSchema): void { + const fields = new Map(); + const relationships: Record = {}; + const attributes: Record = {}; + + schema.fields.forEach((field) => { + assert( + `${field.kind} is not valid inside a ResourceSchema's fields.`, + // @ts-expect-error we are checking for mistakes at runtime + field.kind !== '@id' && field.kind !== '@hash' + ); + fields.set(field.name, field); + if (field.kind === 'attribute') { + attributes[field.name] = field; + } else if (field.kind === 'belongsTo' || field.kind === 'hasMany') { + relationships[field.name] = field; + } + }); + + const traits = new Set(schema.traits); + traits.forEach((trait) => { + this._traits.add(trait); + }); + + const internalSchema: InternalSchema = { original: schema, fields, relationships, attributes, traits }; + this._schemas.set(schema.type, internalSchema); + } + + registerResources(resources: ResourceSchema[]) { + resources.forEach((resource) => { + this.registerResource(resource); + }); + } + + fields({ type }: { type: string }): InternalSchema['fields'] { + const schema = this._schemas.get(type); + + if (!schema) { + if (this._schemas.size === 0) { + return new Map(); + } + throw new Error(`No schema defined for ${type}`); + } + + return schema.fields; + } + + hasResource({ type }: { type: string }) { + return this._schemas.has(type) + ? true + : // in tests we intentionally allow "schemaless" resources + this._schemas.size === 0 + ? true + : false; + } +} diff --git a/tests/ember-data__model/tests/integration/model-for-test.ts b/tests/ember-data__model/tests/integration/model-for-test.ts index dc199b57085..e1fc5e4af4b 100644 --- a/tests/ember-data__model/tests/integration/model-for-test.ts +++ b/tests/ember-data__model/tests/integration/model-for-test.ts @@ -1,11 +1,15 @@ import Store from '@ember-data/store'; import type { SchemaService } from '@ember-data/store/types'; -import type { FieldSchema } from '@warp-drive/core-types/schema/fields'; import { module, test } from '@warp-drive/diagnostic'; +import { TestSchema } from '../utils/schema'; + module('modelFor without @ember-data/model', function () { test('We can call modelFor', function (assert) { class TestStore extends Store { + createSchemaService(): SchemaService { + return new TestSchema(); + } override instantiateRecord() { return { id: '1', @@ -18,55 +22,17 @@ module('modelFor without @ember-data/model', function () { } } const store = new TestStore(); - - class TestSchema implements SchemaService { - attributesDefinitionFor(_identifier: { type: string }) { - return { - name: { - name: 'name', - kind: 'attribute' as const, - type: null, - }, - }; - } - - _fieldsDefCache = {} as Record>; - - fields(identifier: { type: string }): Map { - const { type } = identifier; - let fieldDefs: Map | undefined = this._fieldsDefCache[type]; - - if (fieldDefs === undefined) { - fieldDefs = new Map(); - this._fieldsDefCache[type] = fieldDefs; - - const attributes = this.attributesDefinitionFor(identifier); - const relationships = this.relationshipsDefinitionFor(identifier); - - for (const attr of Object.values(attributes)) { - fieldDefs.set(attr.name, attr); - } - - for (const rel of Object.values(relationships)) { - fieldDefs.set(rel.name, rel); - } - } - - return fieldDefs; - } - - relationshipsDefinitionFor(_identifier: { - type: string; - }): ReturnType { - return {}; - } - - doesTypeExist(type: string) { - return type === 'user'; - } - } - - store.registerSchema(new TestSchema()); + store.schema.registerResource({ + identity: { name: 'id', kind: '@id' }, + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }); try { store.modelFor('user'); @@ -89,6 +55,9 @@ module('modelFor without @ember-data/model', function () { test('modelFor returns a stable reference', function (assert) { class TestStore extends Store { + createSchemaService(): SchemaService { + return new TestSchema(); + } override instantiateRecord() { return { id: '1', @@ -101,54 +70,17 @@ module('modelFor without @ember-data/model', function () { } } const store = new TestStore(); - class TestSchema implements SchemaService { - attributesDefinitionFor(_identifier: { type: string }) { - return { - name: { - name: 'name', - kind: 'attribute' as const, - type: null, - }, - }; - } - - _fieldsDefCache = {} as Record>; - - fields(identifier: { type: string }): Map { - const { type } = identifier; - let fieldDefs: Map | undefined = this._fieldsDefCache[type]; - - if (fieldDefs === undefined) { - fieldDefs = new Map(); - this._fieldsDefCache[type] = fieldDefs; - - const attributes = this.attributesDefinitionFor(identifier); - const relationships = this.relationshipsDefinitionFor(identifier); - - for (const attr of Object.values(attributes)) { - fieldDefs.set(attr.name, attr); - } - - for (const rel of Object.values(relationships)) { - fieldDefs.set(rel.name, rel); - } - } - - return fieldDefs; - } - - relationshipsDefinitionFor(_identifier: { - type: string; - }): ReturnType { - return {}; - } - - doesTypeExist(type: string) { - return type === 'user'; - } - } - - store.registerSchema(new TestSchema()); + store.schema.registerResource({ + identity: { name: 'id', kind: '@id' }, + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }); const ShimUser1 = store.modelFor('user'); const ShimUser2 = store.modelFor('user'); diff --git a/tests/ember-data__model/tests/utils/schema.ts b/tests/ember-data__model/tests/utils/schema.ts new file mode 100644 index 00000000000..e50f84da213 --- /dev/null +++ b/tests/ember-data__model/tests/utils/schema.ts @@ -0,0 +1,126 @@ +import type { SchemaService } from '@ember-data/store/types'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { Value } from '@warp-drive/core-types/json/raw'; +import type { Derivation, HashFn, Transformation } from '@warp-drive/core-types/schema/concepts'; +import type { + FieldSchema, + LegacyAttributeField, + LegacyRelationshipSchema, + ResourceSchema, +} from '@warp-drive/core-types/schema/fields'; +import { Type } from '@warp-drive/core-types/symbols'; + +type InternalSchema = { + original: ResourceSchema; + traits: Set; + fields: Map; + attributes: Record; + relationships: Record; +}; + +export class TestSchema implements SchemaService { + declare _schemas: Map; + declare _transforms: Map; + declare _hashFns: Map; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + declare _derivations: Map>; + declare _traits: Set; + + constructor() { + this._schemas = new Map(); + this._transforms = new Map(); + this._hashFns = new Map(); + this._derivations = new Map(); + } + hasTrait(type: string): boolean { + return this._traits.has(type); + } + resourceHasTrait(resource: StableRecordIdentifier | { type: string }, trait: string): boolean { + return this._schemas.get(resource.type)!.traits.has(trait); + } + transformation(name: string): Transformation { + assert(`No transformation registered with name ${name}`, this._transforms.has(name)); + return this._transforms.get(name)!; + } + derivation(name: string): Derivation { + assert(`No derivation registered with name ${name}`, this._derivations.has(name)); + return this._derivations.get(name)!; + } + resource(resource: StableRecordIdentifier | { type: string }): ResourceSchema { + assert(`No resource registered with name ${resource.type}`, this._schemas.has(resource.type)); + return this._schemas.get(resource.type)!.original; + } + hashFn(name: string): HashFn { + assert(`No hash function registered with name ${name}`, this._hashFns.has(name)); + return this._hashFns.get(name)!; + } + + registerTransformation(transformation: Transformation): void { + this._transforms.set(transformation[Type], transformation as Transformation); + } + + registerDerivation(derivation: Derivation): void { + this._derivations.set(derivation[Type], derivation); + } + + registerHashFn(hashFn: HashFn): void { + this._hashFns.set(hashFn[Type], hashFn as HashFn); + } + + registerResource(schema: ResourceSchema): void { + const fields = new Map(); + const relationships: Record = {}; + const attributes: Record = {}; + + schema.fields.forEach((field) => { + assert( + `${field.kind} is not valid inside a ResourceSchema's fields.`, + // @ts-expect-error we are checking for mistakes at runtime + field.kind !== '@id' && field.kind !== '@hash' + ); + fields.set(field.name, field); + if (field.kind === 'attribute') { + attributes[field.name] = field; + } else if (field.kind === 'belongsTo' || field.kind === 'hasMany') { + relationships[field.name] = field; + } + }); + + const traits = new Set(schema.traits); + traits.forEach((trait) => { + this._traits.add(trait); + }); + + const internalSchema: InternalSchema = { original: schema, fields, relationships, attributes, traits }; + this._schemas.set(schema.type, internalSchema); + } + + registerResources(resources: ResourceSchema[]) { + resources.forEach((resource) => { + this.registerResource(resource); + }); + } + + fields({ type }: { type: string }): InternalSchema['fields'] { + const schema = this._schemas.get(type); + + if (!schema) { + if (this._schemas.size === 0) { + return new Map(); + } + throw new Error(`No schema defined for ${type}`); + } + + return schema.fields; + } + + hasResource({ type }: { type: string }) { + return this._schemas.has(type) + ? true + : // in tests we intentionally allow "schemaless" resources + this._schemas.size === 0 + ? true + : false; + } +} diff --git a/tests/ember-data__serializer/app/services/store.ts b/tests/ember-data__serializer/app/services/store.ts index a6b0de4bebc..65a035fc230 100644 --- a/tests/ember-data__serializer/app/services/store.ts +++ b/tests/ember-data__serializer/app/services/store.ts @@ -13,7 +13,7 @@ import { buildSchema, instantiateRecord, modelFor, teardownRecord } from '@ember import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; import BaseStore, { CacheHandler } from '@ember-data/store'; -import type { CacheCapabilitiesManager, ModelSchema } from '@ember-data/store/types'; +import type { CacheCapabilitiesManager, ModelSchema, SchemaService } from '@ember-data/store/types'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import type { TypeFromInstance } from '@warp-drive/core-types/record'; @@ -23,7 +23,10 @@ export default class Store extends BaseStore { this.requestManager = new RequestManager(); this.requestManager.use([LegacyNetworkHandler, Fetch]); this.requestManager.useCache(CacheHandler); - this.registerSchema(buildSchema(this)); + } + + createSchemaService(): SchemaService { + return buildSchema(this); } override createCache(capabilities: CacheCapabilitiesManager) { diff --git a/tests/example-json-api/app/components/book-search.ts b/tests/example-json-api/app/components/book-search.ts index 8aab0aceac3..694c65caae3 100644 --- a/tests/example-json-api/app/components/book-search.ts +++ b/tests/example-json-api/app/components/book-search.ts @@ -26,7 +26,8 @@ export default class BookListComponent extends Component { @cached get sortOptions() { - return Object.keys(this.store.getSchemaDefinitionService().attributesDefinitionFor({ type: 'book' })); + const fields = this.store.schema.fields({ type: 'book' }); + return Array.from(fields.keys()).filter((key) => fields.get(key)!.kind === 'attribute'); } @cached diff --git a/tests/main/tests/helpers/reactive-context.ts b/tests/main/tests/helpers/reactive-context.ts index 480bb346bfe..a83a567aaa4 100644 --- a/tests/main/tests/helpers/reactive-context.ts +++ b/tests/main/tests/helpers/reactive-context.ts @@ -5,6 +5,7 @@ import Component from '@glimmer/component'; import { hbs } from 'ember-cli-htmlbars'; import type Model from '@ember-data/model'; +import type { FieldSchema, IdentityField, ResourceSchema } from '@warp-drive/core-types/schema/fields'; export interface ReactiveContext { counters: Record; @@ -12,20 +13,16 @@ export interface ReactiveContext { reset: () => void; } -export async function unboundReactiveContext( - context: TestContext, - record: T, - fields: { name: string; type: 'field' | 'hasMany' | 'belongsTo' }[] -): Promise { - return reactiveContext.call(context, record, fields); -} - export async function reactiveContext( this: TestContext, record: T, - fields: { name: string; type: 'field' | 'hasMany' | 'belongsTo' }[] + resource: ResourceSchema ): Promise { const _fields: string[] = []; + const fields: Array = resource.fields.slice(); + if (resource.identity?.name) { + fields.unshift(resource.identity as IdentityField); + } fields.forEach((field) => { _fields.push(field.name + 'Count'); _fields.push(field.name); @@ -48,7 +45,7 @@ export async function reactiveContext( Object.defineProperty(ReactiveComponent.prototype, field.name, { get() { counters[field.name]++; - switch (field.type) { + switch (field.kind) { case 'hasMany': return `[${(record[field.name as keyof T] as Model[]).map((r) => r.id).join(',')}]`; case 'belongsTo': @@ -56,8 +53,7 @@ export async function reactiveContext( case 'field': return record[field.name as keyof T] as unknown; default: - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - throw new Error(`Unknown field type ${field.type} for field ${field.name}`); + throw new Error(`Unknown field kind ${field.kind} for field ${field.name}`); } }, }); diff --git a/tests/main/tests/integration/cache-handler/lifetimes-test.ts b/tests/main/tests/integration/cache-handler/lifetimes-test.ts index 09444c68dc6..92b336297a8 100644 --- a/tests/main/tests/integration/cache-handler/lifetimes-test.ts +++ b/tests/main/tests/integration/cache-handler/lifetimes-test.ts @@ -10,31 +10,61 @@ import RequestManager from '@ember-data/request'; import { CachePolicy } from '@ember-data/request-utils'; import type { NotificationType } from '@ember-data/store'; import Store, { CacheHandler } from '@ember-data/store'; -import type { CacheCapabilitiesManager } from '@ember-data/store/types'; +import type { CacheCapabilitiesManager, SchemaService } from '@ember-data/store/types'; import type { Cache } from '@warp-drive/core-types/cache'; import type { StableDocumentIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; -import type { FieldSchema } from '@warp-drive/core-types/schema/fields'; +import type { ObjectValue } from '@warp-drive/core-types/json/raw'; +import type { Derivation, HashFn, Transformation } from '@warp-drive/core-types/schema/concepts'; +import type { FieldSchema, ResourceSchema } from '@warp-drive/core-types/schema/fields'; import type { ResourceType } from '@warp-drive/core-types/symbols'; type FakeRecord = { [key: string]: unknown; destroy: () => void }; class BaseTestStore extends Store { - constructor() { - super(...arguments); - this.registerSchema({ - attributesDefinitionFor() { - return {}; - }, + createSchemaService(): SchemaService { + const schemaService: SchemaService = { fields(identifier: StableRecordIdentifier | { type: string }): Map { return new Map(); }, - relationshipsDefinitionFor() { - return {}; - }, - doesTypeExist() { + hasResource() { return true; }, - }); + hasTrait: function (type: string): boolean { + throw new Error('Function not implemented.'); + }, + resourceHasTrait: function (resource: StableRecordIdentifier | { type: string }, trait: string): boolean { + throw new Error('Function not implemented.'); + }, + transformation: function (name: string): Transformation { + throw new Error('Function not implemented.'); + }, + derivation: function (name: string): Derivation { + throw new Error('Function not implemented.'); + }, + resource: function (resource: StableRecordIdentifier | { type: string }): ResourceSchema { + throw new Error('Function not implemented.'); + }, + registerResources: function (schemas: ResourceSchema[]): void { + throw new Error('Function not implemented.'); + }, + registerResource: function (schema: ResourceSchema): void { + throw new Error('Function not implemented.'); + }, + registerTransformation: function (transform: Transformation): void { + throw new Error('Function not implemented.'); + }, + registerDerivation(derivation: Derivation): void { + throw new Error('Function not implemented.'); + }, + hashFn: function (name: string): HashFn { + throw new Error('Function not implemented.'); + }, + registerHashFn: function (hashFn: HashFn): void { + throw new Error('Function not implemented.'); + }, + }; + + return schemaService; } override createCache(wrapper: CacheCapabilitiesManager) { diff --git a/tests/main/tests/integration/cache-handler/store-package-setup-test.ts b/tests/main/tests/integration/cache-handler/store-package-setup-test.ts index c473e290d45..4b1474e7ae2 100644 --- a/tests/main/tests/integration/cache-handler/store-package-setup-test.ts +++ b/tests/main/tests/integration/cache-handler/store-package-setup-test.ts @@ -12,7 +12,7 @@ import Fetch from '@ember-data/request/fetch'; import type { Document, NotificationType } from '@ember-data/store'; import Store, { CacheHandler, recordIdentifierFor } from '@ember-data/store'; import type { CollectionRecordArray } from '@ember-data/store/-private'; -import type { CacheCapabilitiesManager } from '@ember-data/store/types'; +import type { CacheCapabilitiesManager, SchemaService } from '@ember-data/store/types'; import type { StableDocumentIdentifier, StableExistingRecordIdentifier, @@ -20,6 +20,7 @@ import type { } from '@warp-drive/core-types/identifier'; import type { OpaqueRecordInstance } from '@warp-drive/core-types/record'; import type { RequestContext } from '@warp-drive/core-types/request'; +import type { HashFn } from '@warp-drive/core-types/schema/concepts'; import type { FieldSchema } from '@warp-drive/core-types/schema/fields'; import type { CollectionResourceDataDocument, @@ -49,22 +50,50 @@ class RequestManagerService extends RequestManager { class TestStore extends Store { @service('request') declare requestManager: RequestManager; - constructor() { - super(...arguments); - this.registerSchema({ - attributesDefinitionFor() { - return {}; + createSchemaService(): SchemaService { + const schemaService: SchemaService = { + registerDerivation() { + throw new Error('Method not implemented.'); + }, + registerTransformation() { + throw new Error('Method not implemented.'); + }, + registerResources() { + throw new Error('Method not implemented.'); + }, + registerResource() { + throw new Error('Method not implemented.'); + }, + resource() { + throw new Error('Method not implemented.'); + }, + transformation() { + throw new Error('Method not implemented.'); + }, + derivation() { + throw new Error('Method not implemented.'); }, fields(identifier: StableRecordIdentifier | { type: string }): Map { return new Map(); }, - relationshipsDefinitionFor() { - return {}; + hasTrait() { + return false; }, - doesTypeExist() { + resourceHasTrait() { + return false; + }, + hasResource() { return true; }, - }); + hashFn: function (name: string): HashFn { + throw new Error('Function not implemented.'); + }, + registerHashFn: function (hashFn: HashFn): void { + throw new Error('Function not implemented.'); + }, + }; + + return schemaService; } override createCache(wrapper: CacheCapabilitiesManager) { @@ -265,67 +294,6 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('When using @ember-data/store, the cache-handler can hydrate any op code', async function (assert) { const { owner } = this; - // eslint-disable-next-line @typescript-eslint/no-shadow - class RequestManagerService extends RequestManager { - constructor() { - super(...arguments); - this.use([LegacyNetworkHandler, Fetch]); - this.useCache(CacheHandler); - } - } - - // eslint-disable-next-line @typescript-eslint/no-shadow - class TestStore extends Store { - @service('request') declare requestManager: RequestManager; - - constructor() { - super(...arguments); - this.registerSchemaDefinitionService({ - attributesDefinitionFor() { - return {}; - }, - fields(identifier: StableRecordIdentifier | { type: string }): Map { - return new Map(); - }, - relationshipsDefinitionFor() { - return {}; - }, - doesTypeExist() { - return true; - }, - }); - } - - override createCache(wrapper: CacheCapabilitiesManager) { - return new Cache(wrapper); - } - - override instantiateRecord(identifier: StableRecordIdentifier) { - const { id, lid, type } = identifier; - const record: FakeRecord = { id, lid, type } as unknown as FakeRecord; - Object.assign(record, this.cache.peek(identifier)!.attributes); - - const token = this.notifications.subscribe( - identifier, - (_: StableRecordIdentifier, kind: NotificationType, key?: string) => { - if (kind === 'attributes' && key) { - record[key] = this.cache.getAttr(identifier, key); - } - } - ); - - record.destroy = () => { - this.notifications.unsubscribe(token); - }; - - return record; - } - - override teardownRecord(record: FakeRecord) { - record.destroy(); - } - } - owner.register('service:store', TestStore); owner.register('service:request', RequestManagerService); @@ -363,67 +331,6 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('When using @ember-data/store, the cache-handler will cache but not hydrate if the request has the store but does not originate from the store', async function (assert) { const { owner } = this; - // eslint-disable-next-line @typescript-eslint/no-shadow - class RequestManagerService extends RequestManager { - constructor() { - super(...arguments); - this.use([LegacyNetworkHandler, Fetch]); - this.useCache(CacheHandler); - } - } - - // eslint-disable-next-line @typescript-eslint/no-shadow - class TestStore extends Store { - @service('request') declare requestManager: RequestManager; - - constructor() { - super(...arguments); - this.registerSchemaDefinitionService({ - attributesDefinitionFor() { - return {}; - }, - fields(identifier: StableRecordIdentifier | { type: string }): Map { - return new Map(); - }, - relationshipsDefinitionFor() { - return {}; - }, - doesTypeExist() { - return true; - }, - }); - } - - override createCache(wrapper: CacheCapabilitiesManager) { - return new Cache(wrapper); - } - - override instantiateRecord(identifier: StableRecordIdentifier) { - const { id, lid, type } = identifier; - const record: FakeRecord = { id, lid, type } as unknown as FakeRecord; - Object.assign(record, this.cache.peek(identifier)!.attributes); - - const token = this.notifications.subscribe( - identifier, - (_: StableRecordIdentifier, kind: NotificationType, key?: string) => { - if (kind === 'attributes' && key) { - record[key] = this.cache.getAttr(identifier, key); - } - } - ); - - record.destroy = () => { - this.notifications.unsubscribe(token); - }; - - return record; - } - - override teardownRecord(record: FakeRecord) { - record.destroy(); - } - } - owner.register('service:store', TestStore); owner.register('service:request', RequestManagerService); @@ -462,67 +369,6 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { test('When using @ember-data/store, the cache-handler will neither cache nor hydrate if the request does not originate from the store and no store is included', async function (assert) { const { owner } = this; - // eslint-disable-next-line @typescript-eslint/no-shadow - class RequestManagerService extends RequestManager { - constructor() { - super(...arguments); - this.use([LegacyNetworkHandler, Fetch]); - this.useCache(CacheHandler); - } - } - - // eslint-disable-next-line @typescript-eslint/no-shadow - class TestStore extends Store { - @service('request') declare requestManager: RequestManager; - - constructor() { - super(...arguments); - this.registerSchemaDefinitionService({ - attributesDefinitionFor() { - return {}; - }, - fields(identifier: StableRecordIdentifier | { type: string }): Map { - return new Map(); - }, - relationshipsDefinitionFor() { - return {}; - }, - doesTypeExist() { - return true; - }, - }); - } - - override createCache(wrapper: CacheCapabilitiesManager) { - return new Cache(wrapper); - } - - override instantiateRecord(identifier: StableRecordIdentifier) { - const { id, lid, type } = identifier; - const record: FakeRecord = { id, lid, type } as unknown as FakeRecord; - Object.assign(record, this.cache.peek(identifier)!.attributes); - - const token = this.notifications.subscribe( - identifier, - (_: StableRecordIdentifier, kind: NotificationType, key?: string) => { - if (kind === 'attributes' && key) { - record[key] = this.cache.getAttr(identifier, key); - } - } - ); - - record.destroy = () => { - this.notifications.unsubscribe(token); - }; - - return record; - } - - override teardownRecord(record: FakeRecord) { - record.destroy(); - } - } - owner.register('service:store', TestStore); owner.register('service:request', RequestManagerService); diff --git a/tests/main/tests/integration/record-data/cache-capabilities-manager-test.ts b/tests/main/tests/integration/cache/cache-capabilities-manager-test.ts similarity index 58% rename from tests/main/tests/integration/record-data/cache-capabilities-manager-test.ts rename to tests/main/tests/integration/cache/cache-capabilities-manager-test.ts index 695daa7bff1..fec878e3556 100644 --- a/tests/main/tests/integration/record-data/cache-capabilities-manager-test.ts +++ b/tests/main/tests/integration/cache/cache-capabilities-manager-test.ts @@ -9,8 +9,6 @@ import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import { recordIdentifierFor } from '@ember-data/store'; import type { CacheCapabilitiesManager } from '@ember-data/store/types'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; -import type { LegacyRelationshipSchema } from '@warp-drive/core-types/schema/fields'; -import type { ExistingResourceObject } from '@warp-drive/core-types/spec/json-api-raw'; class Person extends Model { @attr('string', {}) @@ -39,43 +37,25 @@ class House extends Model { tenants; } -let houseHash: ExistingResourceObject; -let houseHash2: ExistingResourceObject; - -module('integration/store-wrapper - RecordData StoreWrapper tests', function (hooks) { +module('integration/cache-capabilities', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { const { owner } = this; - houseHash = { - type: 'house', - id: '1', - attributes: { - name: 'Moomin', - }, - }; - - houseHash2 = { - type: 'house', - id: '2', - attributes: { - name: 'Lodge', - }, - }; owner.register('model:person', Person); owner.register('model:house', House); owner.register('model:car', Car); }); - test('Relationship definitions', function (assert) { + test('schema', function (assert) { const { owner } = this; - let storeWrapper!: CacheCapabilitiesManager; + let capabilities!: CacheCapabilitiesManager; class TestStore extends Store { - override createCache(wrapper: CacheCapabilitiesManager) { - storeWrapper = wrapper; - return super.createCache(wrapper); + override createCache(cacheCapabilities: CacheCapabilitiesManager) { + capabilities = cacheCapabilities; + return super.createCache(cacheCapabilities); } } @@ -83,81 +63,16 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho const store = owner.lookup('service:store') as unknown as Store; store.cache; - const houseAttrs = { - name: { - type: 'string', - isAttribute: true, - kind: 'attribute' as const, - options: {}, - key: 'name', - name: 'name', - }, - }; - - assert.deepEqual( - storeWrapper.getSchemaDefinitionService().attributesDefinitionFor({ type: 'house' }), - houseAttrs, - 'can lookup attribute definitions for self' - ); - - const carAttrs = { - make: { - type: 'string', - isAttribute: true, - kind: 'attribute' as const, - options: {}, - key: 'make', - name: 'make', - }, - }; - - assert.deepEqual( - storeWrapper.getSchemaDefinitionService().attributesDefinitionFor({ type: 'car' }), - carAttrs, - 'can lookup attribute definitions for other models' - ); - - const houseRelationships = { - landlord: { - key: 'landlord', - kind: 'belongsTo', - name: 'landlord', - type: 'person', - options: { async: false, inverse: null }, - }, - car: { - key: 'car', - kind: 'belongsTo', - name: 'car', - type: 'car', - options: { async: false, inverse: 'garage' }, - }, - tenants: { - key: 'tenants', - kind: 'hasMany', - name: 'tenants', - options: { async: false, inverse: null }, - type: 'person', - }, - }; - const schema = storeWrapper.getSchemaDefinitionService().relationshipsDefinitionFor({ type: 'house' }); - - // Retrieve only public values from the result - // This should go away once we put private things in symbols/weakmaps - assert.deepEqual( - houseRelationships as Record, - schema, - 'can lookup relationship definitions' - ); + assert.strictEqual(capabilities.schema, store.schema, 'capabilities exposes the schema service'); }); test('setRecordId', function (assert) { const { owner } = this; - let storeWrapper!: CacheCapabilitiesManager; + let capabilities!: CacheCapabilitiesManager; class TestStore extends Store { override createCache(wrapper: CacheCapabilitiesManager) { - storeWrapper = wrapper; + capabilities = wrapper; return super.createCache(wrapper); } } @@ -166,7 +81,7 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho const store = owner.lookup('service:store') as unknown as Store; const house = store.createRecord('house', {}) as Model; - storeWrapper.setRecordId(recordIdentifierFor(house), '17'); + capabilities.setRecordId(recordIdentifierFor(house), '17'); assert.strictEqual(house.id, '17', 'setRecordId correctly set the id'); assert.strictEqual( store.peekRecord('house', '17'), @@ -190,7 +105,23 @@ module('integration/store-wrapper - RecordData StoreWrapper tests', function (ho const store = owner.lookup('service:store') as unknown as Store; store.push({ - data: [houseHash, houseHash2], + data: [ + { + type: 'house', + id: '1', + attributes: { + name: 'Moomin', + }, + }, + + { + type: 'house', + id: '2', + attributes: { + name: 'Lodge', + }, + }, + ], }); store.peekRecord('house', '1'); diff --git a/tests/main/tests/integration/record-data/record-data-errors-test.ts b/tests/main/tests/integration/cache/spec-cache-errors-test.ts similarity index 95% rename from tests/main/tests/integration/record-data/record-data-errors-test.ts rename to tests/main/tests/integration/cache/spec-cache-errors-test.ts index 86e1cf011d0..7a603a16881 100644 --- a/tests/main/tests/integration/record-data/record-data-errors-test.ts +++ b/tests/main/tests/integration/cache/spec-cache-errors-test.ts @@ -191,15 +191,15 @@ class TestCache implements Cache { } } -module('integration/record-data Custom RecordData (v2) Errors', function (hooks) { +module('integration/record-data Custom Cache (v2) Errors', function (hooks) { setupTest(hooks); - test('RecordData Invalid Errors', async function (assert) { + test('Cache Invalid Errors', async function (assert) { assert.expect(3); const { owner } = this; - class LifecycleRecordData extends TestCache { + class LifecycleCache extends TestCache { override commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]) { super.commitWasRejected(identifier, errors); assert.strictEqual(errors?.[0]?.detail, 'is a generally unsavoury character', 'received the error'); @@ -208,7 +208,7 @@ module('integration/record-data Custom RecordData (v2) Errors', function (hooks) } class TestStore extends Store { override createCache(wrapper: CacheCapabilitiesManager) { - return new LifecycleRecordData(wrapper) as Cache; + return new LifecycleCache(wrapper) as Cache; } } class TestAdapter extends EmberObject { @@ -256,12 +256,12 @@ module('integration/record-data Custom RecordData (v2) Errors', function (hooks) } }); - test('RecordData Network Errors', async function (assert) { + test('Cache Network Errors', async function (assert) { assert.expect(2); const { owner } = this; - class LifecycleRecordData extends TestCache { + class LifecycleCache extends TestCache { override commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]) { super.commitWasRejected(identifier, errors); assert.strictEqual(errors, undefined, 'Did not pass adapter errors'); @@ -269,7 +269,7 @@ module('integration/record-data Custom RecordData (v2) Errors', function (hooks) } class TestStore extends Store { override createCache(wrapper: CacheCapabilitiesManager) { - return new LifecycleRecordData(wrapper) as Cache; + return new LifecycleCache(wrapper) as Cache; } } class TestAdapter extends EmberObject { @@ -306,12 +306,12 @@ module('integration/record-data Custom RecordData (v2) Errors', function (hooks) } }); - test('RecordData Invalid Errors Can Be Reflected On The Record', function (assert) { + test('Cache Invalid Errors Can Be Reflected On The Record', function (assert) { const { owner } = this; let errorsToReturn: ApiError[] | undefined; let storeWrapper!: CacheCapabilitiesManager; - class LifecycleRecordData extends TestCache { + class LifecycleCache extends TestCache { override getErrors(): ApiError[] { return errorsToReturn || []; } @@ -320,7 +320,7 @@ module('integration/record-data Custom RecordData (v2) Errors', function (hooks) class TestStore extends Store { override createCache(wrapper: CacheCapabilitiesManager) { storeWrapper = wrapper; - return new LifecycleRecordData(wrapper) as Cache; + return new LifecycleCache(wrapper) as Cache; } } diff --git a/tests/main/tests/integration/record-data/record-data-state-test.ts b/tests/main/tests/integration/cache/spec-cache-state-test.ts similarity index 97% rename from tests/main/tests/integration/record-data/record-data-state-test.ts rename to tests/main/tests/integration/cache/spec-cache-state-test.ts index c283397d49e..6d209fe45e4 100644 --- a/tests/main/tests/integration/record-data/record-data-state-test.ts +++ b/tests/main/tests/integration/cache/spec-cache-state-test.ts @@ -47,7 +47,7 @@ class Person extends Model { lastName; } -class TestRecordData implements Cache { +class TestCache implements Cache { _storeWrapper: CacheCapabilitiesManager; _identifier: StableRecordIdentifier; @@ -242,7 +242,7 @@ module('integration/record-data - Record Data State', function (hooks) { }; const { owner } = this; - class LifecycleRecordData extends TestRecordData { + class LifecycleCache extends TestCache { override isNew(): boolean { return isNew; } @@ -263,7 +263,7 @@ module('integration/record-data - Record Data State', function (hooks) { class TestStore extends Store { override createCache(wrapper: CacheCapabilitiesManager) { // @ts-expect-error - return new LifecycleRecordData(wrapper) as Cache; + return new LifecycleCache(wrapper) as Cache; } } @@ -327,7 +327,7 @@ module('integration/record-data - Record Data State', function (hooks) { }; const { owner } = this; - class LifecycleRecordData extends TestRecordData { + class LifecycleCache extends TestCache { constructor(sw: CacheCapabilitiesManager, identifier: StableRecordIdentifier) { super(sw, identifier); storeWrapper = sw; @@ -358,7 +358,7 @@ module('integration/record-data - Record Data State', function (hooks) { class TestStore extends Store { override createCache(wrapper: CacheCapabilitiesManager) { // @ts-expect-error - return new LifecycleRecordData(wrapper) as Cache; + return new LifecycleCache(wrapper) as Cache; } } diff --git a/tests/main/tests/integration/record-data/record-data-test.ts b/tests/main/tests/integration/cache/spec-cache-test.ts similarity index 96% rename from tests/main/tests/integration/record-data/record-data-test.ts rename to tests/main/tests/integration/cache/spec-cache-test.ts index 570f8e39d97..07c2c000e44 100644 --- a/tests/main/tests/integration/record-data/record-data-test.ts +++ b/tests/main/tests/integration/cache/spec-cache-test.ts @@ -59,7 +59,7 @@ class House extends Model { tenants; } -class TestRecordData implements Cache { +class TestCache implements Cache { version = '2' as const; _errors?: ApiError[]; @@ -210,7 +210,7 @@ class TestRecordData implements Cache { } } -module('integration/record-data - Custom RecordData Implementations', function (hooks) { +module('integration/record-data - Custom Cache Implementations', function (hooks) { setupTest(hooks); hooks.beforeEach(function () { @@ -226,7 +226,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( owner.register('serializer:application', class extends JSONAPISerializer {}); }); - test('A RecordData implementation that has the required spec methods should not error out', async function (assert) { + test('A Cache implementation that has the required spec methods should not error out', async function (assert) { const { owner } = this; const store: Store = owner.lookup('service:store') as unknown as Store; @@ -289,7 +289,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( let calledDidCommit = 0; let isNew = false; - class LifecycleRecordData extends TestRecordData { + class LifecycleCache extends TestCache { override upsert() { calledUpsert++; } @@ -338,7 +338,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( class TestStore extends Store { override createCache(storeWrapper: CacheCapabilitiesManager) { // @ts-expect-error - return new LifecycleRecordData(storeWrapper) as Cache; + return new LifecycleCache(storeWrapper) as Cache; } } @@ -436,7 +436,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( const { owner } = this; let calledGet = 0; - class AttributeRecordData extends TestRecordData { + class AttributeCache extends TestCache { changedAttributes() { return { name: ['old', 'new'] as [string, string] }; } @@ -474,7 +474,7 @@ module('integration/record-data - Custom RecordData Implementations', function ( class TestStore extends Store { override createCache(storeWrapper: CacheCapabilitiesManager) { // @ts-expect-error - return new AttributeRecordData(storeWrapper) as Cache; + return new AttributeCache(storeWrapper) as Cache; } } diff --git a/tests/main/tests/integration/relationships/collection/mutating-has-many-test.ts b/tests/main/tests/integration/relationships/collection/mutating-has-many-test.ts index 6ccffcdaf41..6e83242d93e 100644 --- a/tests/main/tests/integration/relationships/collection/mutating-has-many-test.ts +++ b/tests/main/tests/integration/relationships/collection/mutating-has-many-test.ts @@ -13,7 +13,7 @@ import type { ExistingResourceIdentifierObject } from '@warp-drive/core-types/sp import { ResourceType } from '@warp-drive/core-types/symbols'; import type { ReactiveContext } from '../../../helpers/reactive-context'; -import { unboundReactiveContext } from '../../../helpers/reactive-context'; +import { reactiveContext } from '../../../helpers/reactive-context'; let IS_DEPRECATE_MANY_ARRAY_DUPLICATES = false; @@ -410,7 +410,11 @@ module('Integration | Relationships | Collection | Mutation', function (hooks) { test(`followed by Mutation: ${mutation2.name}`, async function (assert) { const store = this.owner.lookup('service:store') as Store; const user = startingState.cb(store); - const rc = await unboundReactiveContext(this, user, [{ name: 'friends', type: 'hasMany' }]); + const rc = await reactiveContext.call(this, user, { + identity: null, + type: 'user', + fields: [{ name: 'friends', kind: 'hasMany', type: 'user', options: { async: false, inverse: null } }], + }); rc.reset(); await applyMutation(assert, store, user, mutation, rc); diff --git a/tests/main/tests/integration/relationships/explicit-polymorphic-belongs-to-test.js b/tests/main/tests/integration/relationships/explicit-polymorphic-belongs-to-test.js index 6ed5bbf7af7..998414e1719 100644 --- a/tests/main/tests/integration/relationships/explicit-polymorphic-belongs-to-test.js +++ b/tests/main/tests/integration/relationships/explicit-polymorphic-belongs-to-test.js @@ -357,16 +357,21 @@ module('Integration | Relationships | Explicit Polymorphic BelongsTo', function [ 'taggable', { - tag: { - kind: 'belongsTo', - type: 'tag', - name: 'tag', - options: { - async: false, - inverse: 'tagged', - as: 'taggable', - }, - }, + fields: new Map([ + [ + 'tag', + { + kind: 'belongsTo', + type: 'tag', + name: 'tag', + options: { + async: false, + inverse: 'tagged', + as: 'taggable', + }, + }, + ], + ]), }, ], ]); @@ -376,28 +381,23 @@ module('Integration | Relationships | Explicit Polymorphic BelongsTo', function this._schema = schema; } - doesTypeExist(type) { + hasResource({ type }) { if (AbstractSchemas.has(type)) { return true; // some apps may want `true` } - return this._schema.doesTypeExist(type); - } - - attributesDefinitionFor(identifier) { - return this._schema.attributesDefinitionFor(identifier); + return this._schema.hasResource({ type }); } fields(identifier) { - return this._schema.fields(identifier); - } - - relationshipsDefinitionFor(identifier) { const schema = AbstractSchemas.get(identifier.type); - return schema || this._schema.relationshipsDefinitionFor(identifier); + if (schema) { + return schema.fields; + } + return this._schema.fields(identifier); } } - const schema = store.getSchemaDefinitionService(); - store.registerSchemaDefinitionService(new SchemaDelegator(schema)); + const schema = store.createSchemaService(); + store.createSchemaService = () => new SchemaDelegator(schema); owner.register( 'model:tag', diff --git a/tests/main/tests/integration/relationships/explicit-polymorphic-has-many-test.js b/tests/main/tests/integration/relationships/explicit-polymorphic-has-many-test.js index f0438dd38b8..aaab4ffc90b 100644 --- a/tests/main/tests/integration/relationships/explicit-polymorphic-has-many-test.js +++ b/tests/main/tests/integration/relationships/explicit-polymorphic-has-many-test.js @@ -372,21 +372,25 @@ module('Integration | Relationships | Explicit Polymorphic HasMany', function (h test('a polymorphic hasMany relationship with a specified inverse can use an abstract-type defined via the schema service', async function (assert) { const { owner } = this; const store = owner.lookup('service:store'); - const AbstractSchemas = new Map([ [ 'taggable', { - tag: { - kind: 'belongsTo', - type: 'tag', - name: 'tag', - options: { - async: false, - inverse: 'tagged', - as: 'taggable', - }, - }, + fields: new Map([ + [ + 'tag', + { + kind: 'belongsTo', + type: 'tag', + name: 'tag', + options: { + async: false, + inverse: 'tagged', + as: 'taggable', + }, + }, + ], + ]), }, ], ]); @@ -396,28 +400,23 @@ module('Integration | Relationships | Explicit Polymorphic HasMany', function (h this._schema = schema; } - doesTypeExist(type) { + hasResource({ type }) { if (AbstractSchemas.has(type)) { return true; // some apps may want `true` } - return this._schema.doesTypeExist(type); - } - - attributesDefinitionFor(identifier) { - return this._schema.attributesDefinitionFor(identifier); + return this._schema.hasResource({ type }); } fields(identifier) { - return this._schema.fields(identifier); - } - - relationshipsDefinitionFor(identifier) { const schema = AbstractSchemas.get(identifier.type); - return schema || this._schema.relationshipsDefinitionFor(identifier); + if (schema) { + return schema.fields; + } + return this._schema.fields(identifier); } } - const schema = store.getSchemaDefinitionService(); - store.registerSchemaDefinitionService(new SchemaDelegator(schema)); + const schema = store.createSchemaService(); + store.createSchemaService = () => new SchemaDelegator(schema); owner.register( 'model:tag', diff --git a/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts b/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts index 504d6059df6..318cff585d7 100644 --- a/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts +++ b/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts @@ -6,10 +6,10 @@ import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import type { Snapshot } from '@ember-data/legacy-compat/-private'; import JSONAPISerializer from '@ember-data/serializer/json-api'; -import type { SchemaService } from '@ember-data/store/types'; import type { Cache } from '@warp-drive/core-types/cache'; -import type { RecordIdentifier, StableRecordIdentifier } from '@warp-drive/core-types/identifier'; -import type { FieldSchema, LegacyAttributeField } from '@warp-drive/core-types/schema/fields'; +import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier'; + +import { TestSchema } from '../../utils/schema'; module('unit/model - Custom Class Model', function (hooks: NestedHooks) { class Person { @@ -24,56 +24,21 @@ module('unit/model - Custom Class Model', function (hooks: NestedHooks) { } } - class TestSchema { - attributesDefinitionFor(identifier: { type: T }): Record { - const schema: Record = {}; - schema.name = { - kind: 'attribute', - options: {}, - type: 'string', - name: 'name', - }; - return schema; - } - - _fieldsDefCache: Record> = {}; - - fields(identifier: { type: T }): Map { - const { type } = identifier; - let fieldDefs: Map | undefined = this._fieldsDefCache[type]; - - if (fieldDefs === undefined) { - fieldDefs = new Map(); - this._fieldsDefCache[type] = fieldDefs; - - const attributes = this.attributesDefinitionFor(identifier); - const relationships = this.relationshipsDefinitionFor(identifier); - - for (const attr of Object.values(attributes)) { - fieldDefs.set(attr.name, attr); - } - - for (const rel of Object.values(relationships)) { - fieldDefs.set(rel.name, rel); - } - } - - return fieldDefs; - } - - relationshipsDefinitionFor(identifier: { type: T }): ReturnType { - return {}; - } - - doesTypeExist(type: string) { - return true; - } - } - class CustomStore extends Store { - constructor(args: Record) { - super(args); - this.registerSchema(new TestSchema()); + createSchemaService() { + const schema = new TestSchema(); + schema.registerResource({ + identity: { name: 'id', kind: '@id' }, + type: 'person', + fields: [ + { + name: 'name', + kind: 'attribute', + type: null, + }, + ], + }); + return schema; } instantiateRecord(identifier, createOptions) { return new Person(this); @@ -120,14 +85,14 @@ module('unit/model - Custom Class Model', function (hooks: NestedHooks) { } this.owner.register('service:store', CreationStore); const store = this.owner.lookup('service:store') as Store; - const storeWrapper = store._instanceCache._storeWrapper; + const capabilities = store._instanceCache._storeWrapper; store.push({ data: { id: '1', type: 'person', attributes: { name: 'chris' } } }); // emulate this happening within a single push store._join(() => { - storeWrapper.notifyChange(identifier, 'relationships', 'key'); - storeWrapper.notifyChange(identifier, 'relationships', 'key'); - storeWrapper.notifyChange(identifier, 'state'); - storeWrapper.notifyChange(identifier, 'errors'); + capabilities.notifyChange(identifier, 'relationships', 'key'); + capabilities.notifyChange(identifier, 'relationships', 'key'); + capabilities.notifyChange(identifier, 'state'); + capabilities.notifyChange(identifier, 'errors'); }); assert.strictEqual(notificationCount, 3, 'called notification callback'); @@ -154,14 +119,17 @@ module('unit/model - Custom Class Model', function (hooks: NestedHooks) { assert.deepEqual(returnValue, person, 'record instantiating does not modify the returned value'); }); - test('attribute and relationship with custom schema definition', async function (assert) { + test('fields with custom schema definition', async function (assert) { this.owner.register( 'adapter:application', JSONAPIAdapter.extend({ shouldBackgroundReloadRecord: () => false, createRecord: (store, type, snapshot: Snapshot) => { let count = 0; - assert.verifySteps(['Schema:attributesDefinitionFor', 'Schema:fields']); + assert.verifySteps( + ['TestSchema:fields', 'TestSchema:fields', 'TestSchema:hasResource', 'TestSchema:hasResource'], + 'serialization of record for save' + ); assert.step('Adapter:createRecord'); snapshot.eachAttribute((attr, attrDef) => { if (count === 0) { @@ -219,10 +187,10 @@ module('unit/model - Custom Class Model', function (hooks: NestedHooks) { }); assert.verifySteps([ 'Adapter:createRecord', - 'Schema:attributesDefinitionFor', + 'TestSchema:fields', 'Adapter:createRecord:attr:name', 'Adapter:createRecord:attr:age', - 'Schema:relationshipsDefinitionFor', + 'TestSchema:fields', 'Adapter:createRecord:rel:boats', 'Adapter:createRecord:rel:house', ]); @@ -230,121 +198,62 @@ module('unit/model - Custom Class Model', function (hooks: NestedHooks) { }, }) ); - // eslint-disable-next-line @typescript-eslint/no-shadow - class CustomStore extends Store { - instantiateRecord(identifier, createOptions) { - return new Person(this); - } - teardownRecord(record) {} - } - this.owner.register('service:store', CustomStore); - const store = this.owner.lookup('service:store') as Store; - class TestSchema2 { - attributesDefinitionFor( - identifier: RecordIdentifier | { type: string } - ): ReturnType { - assert.step('Schema:attributesDefinitionFor'); - if (typeof identifier === 'string') { - assert.strictEqual(identifier, 'person', 'type passed in to the schema hooks'); - } else { - assert.strictEqual(identifier.type, 'person', 'type passed in to the schema hooks'); - } - return { - name: { - type: 'string', - kind: 'attribute', - options: {}, - name: 'name', - }, - age: { - type: 'number', - kind: 'attribute', - options: {}, - name: 'age', - }, - }; - } - - _fieldsDefCache: Record> = {}; - - fields(identifier: StableRecordIdentifier | { type: string }): Map { - assert.step('Schema:fields'); - const { type } = identifier; - let fieldDefs: Map | undefined = this._fieldsDefCache[type]; - - if (fieldDefs === undefined) { - assert.step('Schema:fields(calc)'); - fieldDefs = new Map(); - this._fieldsDefCache[type] = fieldDefs; - - const attributes = this.attributesDefinitionFor(identifier); - const relationships = this.relationshipsDefinitionFor(identifier); - for (const attr of Object.values(attributes)) { - fieldDefs.set(attr.name, attr); - } - - for (const rel of Object.values(relationships)) { - fieldDefs.set(rel.name, rel); - } - } - - return fieldDefs; - } - - relationshipsDefinitionFor( - identifier: RecordIdentifier | { type: string } - ): ReturnType { - assert.step('Schema:relationshipsDefinitionFor'); - if (typeof identifier === 'string') { - assert.strictEqual(identifier, 'person', 'type passed in to the schema hooks'); - } else { - assert.strictEqual(identifier.type, 'person', 'type passed in to the schema hooks'); - } - return { - boats: { - type: 'ship', - kind: 'hasMany', - options: { - inverse: null, - async: false, - }, - name: 'boats', + this.owner.register('service:store', CustomStore); + const store = this.owner.lookup('service:store') as CustomStore; + store.schema._assert = assert; + store.schema.registerResource({ + identity: { name: 'id', kind: '@id' }, + type: 'person', + fields: [ + { + type: 'string', + kind: 'attribute', + options: {}, + name: 'name', + }, + { + type: 'number', + kind: 'attribute', + options: {}, + name: 'age', + }, + { + type: 'ship', + kind: 'hasMany', + options: { + inverse: null, + async: false, }, - house: { - type: 'house', - kind: 'belongsTo', - options: { - inverse: null, - async: false, - }, - name: 'house', + name: 'boats', + }, + { + type: 'house', + kind: 'belongsTo', + options: { + inverse: null, + async: false, }, - }; - } - - doesTypeExist() { - return true; - } - } + name: 'house', + }, + ], + }); - const schema: SchemaService = new TestSchema2(); - store.registerSchemaDefinitionService(schema); - assert.verifySteps([]); + assert.verifySteps(['TestSchema:registerResource'], 'initial population of schema'); const person = store.createRecord('person', { name: 'chris' }) as Person; - assert.verifySteps([ - 'Schema:relationshipsDefinitionFor', - 'Schema:fields', - 'Schema:fields(calc)', - 'Schema:attributesDefinitionFor', - 'Schema:relationshipsDefinitionFor', - ]); + assert.verifySteps(['TestSchema:fields', 'TestSchema:fields'], 'population of record on create'); await person.save(); - assert.verifySteps([ - 'Schema:attributesDefinitionFor', - 'Schema:attributesDefinitionFor', - 'Schema:relationshipsDefinitionFor', - ]); + assert.verifySteps( + [ + 'TestSchema:hasResource', + 'TestSchema:hasResource', + 'TestSchema:hasResource', + 'TestSchema:fields', + 'TestSchema:fields', + 'TestSchema:fields', + ], + 'update of record on save completion' + ); }); test('store.saveRecord', async function (assert) { @@ -417,95 +326,45 @@ module('unit/model - Custom Class Model', function (hooks: NestedHooks) { }, }) ); - // eslint-disable-next-line @typescript-eslint/no-shadow - class CustomStore extends Store { - instantiateRecord(identifier, createOptions) { - return new Person(this); - } - teardownRecord(record) {} - } - this.owner.register('service:store', CustomStore); - const store = this.owner.lookup('service:store') as Store; - class TestSchema2 { - attributesDefinitionFor( - identifier: RecordIdentifier | { type: string } - ): ReturnType { - const modelName = (identifier as RecordIdentifier).type || identifier; - if (modelName === 'person') { - return { - name: { - type: 'string', - kind: 'attribute', - options: {}, - name: 'name', - }, - }; - } else if (modelName === 'house') { - return { - address: { - type: 'string', - kind: 'attribute', - options: {}, - name: 'address', - }, - }; - } else { - return {}; - } - } - - _fieldsDefCache: Record> = {}; - - fields(identifier: StableRecordIdentifier | { type: string }): Map { - const { type } = identifier; - let fieldDefs: Map | undefined = this._fieldsDefCache[type]; - - if (fieldDefs === undefined) { - fieldDefs = new Map(); - this._fieldsDefCache[type] = fieldDefs; - - const attributes = this.attributesDefinitionFor(identifier); - const relationships = this.relationshipsDefinitionFor(identifier); - for (const attr of Object.values(attributes)) { - fieldDefs.set(attr.name, attr); - } - - for (const rel of Object.values(relationships)) { - fieldDefs.set(rel.name, rel); - } - } - - return fieldDefs; - } - - relationshipsDefinitionFor( - identifier: RecordIdentifier | { type: string } - ): ReturnType { - const modelName = (identifier as RecordIdentifier).type || identifier; - if (modelName === 'person') { - return { - house: { - type: 'house', - kind: 'belongsTo', - options: { - inverse: null, - async: true, - }, - name: 'house', + this.owner.register('service:store', CustomStore); + const store = this.owner.lookup('service:store') as CustomStore; + store.schema.registerResources([ + { + identity: { name: 'id', kind: '@id' }, + type: 'person', + fields: [ + { + type: 'house', + kind: 'belongsTo', + options: { + inverse: null, + async: true, }, - }; - } else { - return {}; - } - } + name: 'house', + }, + { + type: 'string', + kind: 'attribute', + options: {}, + name: 'name', + }, + ], + }, + { + identity: { name: 'id', kind: '@id' }, + type: 'house', + fields: [ + { + type: 'string', + kind: 'attribute', + options: {}, + name: 'address', + }, + ], + }, + ]); - doesTypeExist() { - return true; - } - } - const schema = new TestSchema2(); - store.registerSchemaDefinitionService(schema); const person = store.push({ data: { type: 'person', @@ -537,158 +396,4 @@ module('unit/model - Custom Class Model', function (hooks: NestedHooks) { 'serializes record correctly' ); }); - - /* - TODO determine if there's any validity to keeping these - tes('relationshipReferenceFor belongsTo', async function (assert) { - assert.expect(3); - this.owner.register('service:store', CustomStore); - store = this.owner.lookup('service:store') as unknown as Store; - let schema: SchemaDefinitionService = { - attributesDefinitionFor({ type: modelName }: { type: string }): AttributesSchema { - if (modelName === 'person') { - return { - name: { - type: 'string', - kind: 'attribute', - options: {}, - name: 'name', - }, - }; - } else if (modelName === 'house') { - return { - address: { - type: 'string', - kind: 'attribute', - options: {}, - name: 'address', - }, - }; - } else { - return {}; - } - }, - relationshipsDefinitionFor({ type: modelName }: { type: string }): RelationshipsSchema { - if (modelName === 'person') { - return { - house: { - type: 'house', - kind: 'belongsTo', - options: { - inverse: null, - }, - key: 'house', - name: 'house', - }, - }; - } else { - return {}; - } - }, - doesTypeExist() { - return true; - }, - }; - store.registerSchemaDefinitionService(schema); - store.push({ - data: { - type: 'house', - id: '1', - attributes: { address: 'boat' }, - }, - }); - let person = store.push({ - data: { - type: 'person', - id: '7', - attributes: { name: 'chris' }, - relationships: { house: { data: { type: 'house', id: '1' } } }, - }, - }); - let identifier = recordIdentifierFor(person); - let relationship = store.relationshipReferenceFor({ type: 'person', id: '7', lid: identifier.lid }, 'house'); - assert.strictEqual(relationship.id(), '1', 'house relationship id found'); - assert.strictEqual(relationship.type, 'house', 'house relationship type found'); - assert.strictEqual(relationship.parent.id(), '7', 'house relationship parent found'); - }); - - tes('relationshipReferenceFor hasMany', async function (assert) { - assert.expect(3); - this.owner.register('service:store', CustomStore); - store = this.owner.lookup('service:store') as unknown as Store; - let schema: SchemaDefinitionService = { - attributesDefinitionFor({ type: modelName }: { type: string }): AttributesSchema { - if (modelName === 'person') { - return { - name: { - type: 'string', - kind: 'attribute', - options: {}, - name: 'name', - }, - }; - } else if (modelName === 'house') { - return { - address: { - type: 'string', - kind: 'attribute', - options: {}, - name: 'address', - }, - }; - } else { - return {}; - } - }, - relationshipsDefinitionFor({ type: modelName }: { type: string }): RelationshipsSchema { - if (modelName === 'person') { - return { - house: { - type: 'house', - kind: 'hasMany', - options: { - inverse: null, - }, - key: 'house', - name: 'house', - }, - }; - } else { - return {}; - } - }, - doesTypeExist() { - return true; - }, - }; - store.registerSchemaDefinitionService(schema); - store.push({ - data: { - type: 'house', - id: '1', - attributes: { address: 'boat' }, - }, - }); - let person = store.push({ - data: { - type: 'person', - id: '7', - attributes: { name: 'chris' }, - relationships: { - house: { - data: [ - { type: 'house', id: '1' }, - { type: 'house', id: '2' }, - ], - }, - }, - }, - }); - let identifier = recordIdentifierFor(person); - let relationship = store.relationshipReferenceFor({ type: 'person', id: '7', lid: identifier.lid }, 'house'); - assert.deepEqual(relationship.ids(), ['1', '2'], 'relationship found'); - assert.strictEqual(relationship.type, 'house', 'house relationship type found'); - assert.strictEqual(relationship.parent.id(), '7', 'house relationship parent found'); - }); - */ }); diff --git a/tests/main/tests/unit/debug-test.js b/tests/main/tests/unit/debug-test.js index 02cd743f6c6..40d232813bf 100644 --- a/tests/main/tests/unit/debug-test.js +++ b/tests/main/tests/unit/debug-test.js @@ -44,7 +44,7 @@ module('Debug', function (hooks) { assert.deepEqual(propertyInfo.groups[2].properties, ['posts']); }); - test('_debugInfo supports arbitray relationship types', async function (assert) { + test('_debugInfo supports arbitrary relationship types', async function (assert) { class MaritalStatus extends Model { @attr('string') name; } @@ -59,15 +59,6 @@ module('Debug', function (hooks) { @belongsTo('marital-status', { async: false, inverse: null }) maritalStatus; } - // posts: computed(() => [1, 2, 3]) - // .readOnly() - // .meta({ - // options: { inverse: null }, - // kind: 'customRelationship', - // name: 'posts', - // type: 'post', - // }), - this.owner.register('model:marital-status', MaritalStatus); this.owner.register('model:post', Post); this.owner.register('model:user', User); @@ -79,35 +70,30 @@ module('Debug', function (hooks) { this._schema = schema; } - doesTypeExist(type) { - return this._schema.doesTypeExist(type); - } - - attributesDefinitionFor(identifier) { - return this._schema.attributesDefinitionFor(identifier); + hasResource({ type }) { + return this._schema.hasResource({ type }); } fields(identifier) { - return this._schema.fields(identifier); - } - - relationshipsDefinitionFor(identifier) { - const sup = this._schema.relationshipsDefinitionFor(identifier); + const sup = this._schema.fields(identifier); if (identifier.type === 'user') { - return Object.assign(sup, { - posts: { - kind: 'customRelationship', - name: 'posts', - type: 'post', - options: { async: false, inverse: null }, - }, - }); + return new Map([ + [ + 'posts', + { + kind: 'customRelationship', + name: 'posts', + type: 'post', + options: { async: false, inverse: null }, + }, + ], + ]); } return sup; } } - const schema = store.getSchemaDefinitionService(); - store.registerSchemaDefinitionService(new SchemaDelegator(schema)); + const schema = store.createSchemaService(); + store.createSchemaService = () => new SchemaDelegator(schema); const record = store.createRecord('user'); const propertyInfo = record._debugInfo().propertyInfo; diff --git a/tests/main/tests/utils/schema.ts b/tests/main/tests/utils/schema.ts new file mode 100644 index 00000000000..f6f402f1616 --- /dev/null +++ b/tests/main/tests/utils/schema.ts @@ -0,0 +1,141 @@ +import type { SchemaService } from '@ember-data/store/types'; +import { assert } from '@warp-drive/build-config/macros'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import type { Value } from '@warp-drive/core-types/json/raw'; +import type { Derivation, HashFn, Transformation } from '@warp-drive/core-types/schema/concepts'; +import type { + FieldSchema, + LegacyAttributeField, + LegacyRelationshipSchema, + ResourceSchema, +} from '@warp-drive/core-types/schema/fields'; +import { Type } from '@warp-drive/core-types/symbols'; + +type InternalSchema = { + original: ResourceSchema; + traits: Set; + fields: Map; + attributes: Record; + relationships: Record; +}; + +export class TestSchema implements SchemaService { + declare _schemas: Map; + declare _transforms: Map; + declare _hashFns: Map; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + declare _derivations: Map>; + declare _traits: Set; + declare _assert: Assert | null; + + constructor() { + this._schemas = new Map(); + this._transforms = new Map(); + this._hashFns = new Map(); + this._derivations = new Map(); + this._assert = null; + } + hasTrait(type: string): boolean { + this._assert?.step('TestSchema:hasTrait'); + return this._traits.has(type); + } + resourceHasTrait(resource: StableRecordIdentifier | { type: string }, trait: string): boolean { + this._assert?.step('TestSchema:resourceHasTrait'); + return this._schemas.get(resource.type)!.traits.has(trait); + } + transformation(name: string): Transformation { + this._assert?.step('TestSchema:transformation'); + assert(`No transformation registered with name ${name}`, this._transforms.has(name)); + return this._transforms.get(name)!; + } + derivation(name: string): Derivation { + this._assert?.step('TestSchema:derivation'); + assert(`No derivation registered with name ${name}`, this._derivations.has(name)); + return this._derivations.get(name)!; + } + resource(resource: StableRecordIdentifier | { type: string }): ResourceSchema { + this._assert?.step('TestSchema:resource'); + assert(`No resource registered with name ${resource.type}`, this._schemas.has(resource.type)); + return this._schemas.get(resource.type)!.original; + } + hashFn(name: string): HashFn { + this._assert?.step('TestSchema:hashFn'); + assert(`No hash function registered with name ${name}`, this._hashFns.has(name)); + return this._hashFns.get(name)!; + } + + registerTransformation(transformation: Transformation): void { + this._assert?.step('TestSchema:registerTransformation'); + this._transforms.set(transformation[Type], transformation as Transformation); + } + + registerDerivation(derivation: Derivation): void { + this._assert?.step('TestSchema:registerDerivation'); + this._derivations.set(derivation[Type], derivation); + } + + registerHashFn(hashFn: HashFn): void { + this._assert?.step('TestSchema:registerHashFn'); + this._hashFns.set(hashFn[Type], hashFn as HashFn); + } + + registerResource(schema: ResourceSchema): void { + this._assert?.step('TestSchema:registerResource'); + const fields = new Map(); + const relationships: Record = {}; + const attributes: Record = {}; + + schema.fields.forEach((field) => { + assert( + `${field.kind} is not valid inside a ResourceSchema's fields.`, + // @ts-expect-error we are checking for mistakes at runtime + field.kind !== '@id' && field.kind !== '@hash' + ); + fields.set(field.name, field); + if (field.kind === 'attribute') { + attributes[field.name] = field; + } else if (field.kind === 'belongsTo' || field.kind === 'hasMany') { + relationships[field.name] = field; + } + }); + + const traits = new Set(schema.traits); + traits.forEach((trait) => { + this._traits.add(trait); + }); + + const internalSchema: InternalSchema = { original: schema, fields, relationships, attributes, traits }; + this._schemas.set(schema.type, internalSchema); + } + + registerResources(resources: ResourceSchema[]) { + this._assert?.step('TestSchema:registerResources'); + resources.forEach((resource) => { + this.registerResource(resource); + }); + } + + fields({ type }: { type: string }): InternalSchema['fields'] { + this._assert?.step('TestSchema:fields'); + const schema = this._schemas.get(type); + + if (!schema) { + if (this._schemas.size === 0) { + return new Map(); + } + throw new Error(`No schema defined for ${type}`); + } + + return schema.fields; + } + + hasResource({ type }: { type: string }) { + this._assert?.step('TestSchema:hasResource'); + return this._schemas.has(type) + ? true + : // in tests we intentionally allow "schemaless" resources + this._schemas.size === 0 + ? true + : false; + } +} diff --git a/tests/warp-drive__schema-record/app/services/store.ts b/tests/warp-drive__schema-record/app/services/store.ts index 81faa38cd29..a04e6a705d7 100644 --- a/tests/warp-drive__schema-record/app/services/store.ts +++ b/tests/warp-drive__schema-record/app/services/store.ts @@ -6,6 +6,7 @@ import type { CacheCapabilitiesManager } from '@ember-data/store/types'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import { instantiateRecord, teardownRecord } from '@warp-drive/schema-record/hooks'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; +import { SchemaService } from '@warp-drive/schema-record/schema'; export default class Store extends DataStore { constructor(args: unknown) { @@ -16,6 +17,10 @@ export default class Store extends DataStore { manager.useCache(CacheHandler); } + createSchemaService() { + return new SchemaService(); + } + override createCache(capabilities: CacheCapabilitiesManager) { return new JSONAPICache(capabilities); } diff --git a/tests/warp-drive__schema-record/tests/-utils/normalize-payload.ts b/tests/warp-drive__schema-record/tests/-utils/normalize-payload.ts index ed3744d080f..a57edfa1225 100644 --- a/tests/warp-drive__schema-record/tests/-utils/normalize-payload.ts +++ b/tests/warp-drive__schema-record/tests/-utils/normalize-payload.ts @@ -6,7 +6,7 @@ import type { SingleResourceDocument } from '@warp-drive/core-types/spec/json-ap export function simplePayloadNormalize(owner: Owner, payload: SingleResourceDocument): SingleResourceDocument { const store = owner.lookup('service:store') as Store; - const attrSchema = store.schema.attributesDefinitionFor(payload.data); + const fields = store.schema.fields(payload.data); const attrs = payload.data.attributes; if (!attrs) { @@ -14,7 +14,7 @@ export function simplePayloadNormalize(owner: Owner, payload: SingleResourceDocu } Object.keys(attrs).forEach((key) => { - const schema = attrSchema[key]; + const schema = fields.get(key); if (schema) { if (schema.type) { diff --git a/tests/warp-drive__schema-record/tests/-utils/reactive-context.ts b/tests/warp-drive__schema-record/tests/-utils/reactive-context.ts index ec19480e05d..9d6f4171636 100644 --- a/tests/warp-drive__schema-record/tests/-utils/reactive-context.ts +++ b/tests/warp-drive__schema-record/tests/-utils/reactive-context.ts @@ -6,14 +6,18 @@ import { hbs } from 'ember-cli-htmlbars'; import type { ResourceRelationship } from '@warp-drive/core-types/cache/relationship'; import type { OpaqueRecordInstance } from '@warp-drive/core-types/record'; -import type { FieldSchema } from '@warp-drive/core-types/schema/fields'; +import type { FieldSchema, IdentityField, ResourceSchema } from '@warp-drive/core-types/schema/fields'; export async function reactiveContext( this: TestContext, record: T, - fields: FieldSchema[] + resource: ResourceSchema ) { const _fields: string[] = []; + const fields: Array = resource.fields.slice(); + if (resource.identity?.name) { + fields.unshift(resource.identity as IdentityField); + } fields.forEach((field) => { _fields.push(field.name + 'Count'); _fields.push(field.name); @@ -41,7 +45,10 @@ export async function reactiveContext( field.kind === 'attribute' || field.kind === 'field' || field.kind === 'derived' || - field.kind === 'array' + field.kind === 'array' || + field.kind === '@id' || + // @ts-expect-error we secretly allow this + field.kind === '@hash' ) { return record[field.name as keyof T] as unknown; } else if (field.kind === 'resource') { diff --git a/tests/warp-drive__schema-record/tests/legacy/mode-test.ts b/tests/warp-drive__schema-record/tests/legacy/mode-test.ts index 57f6f77d4c9..443f5672e4c 100644 --- a/tests/warp-drive__schema-record/tests/legacy/mode-test.ts +++ b/tests/warp-drive__schema-record/tests/legacy/mode-test.ts @@ -7,14 +7,14 @@ import type { Snapshot } from '@ember-data/legacy-compat/-private'; import type Model from '@ember-data/model'; import { registerDerivations as registerLegacyDerivations, - withFields as withLegacyFields, + withDefaults as withLegacyFields, } from '@ember-data/model/migration-support'; import RequestManager from '@ember-data/request'; import type Store from '@ember-data/store'; import { CacheHandler } from '@ember-data/store'; import type { ResourceType } from '@warp-drive/core-types/symbols'; import { Editable, Legacy } from '@warp-drive/schema-record/record'; -import { registerDerivations, SchemaService, withFields } from '@warp-drive/schema-record/schema'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; type Errors = Model['errors']; type RecordState = Model['currentState']; @@ -63,20 +63,21 @@ module('Legacy Mode', function (hooks) { test('we can create a record in legacy mode', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - ]), - }); + schema.registerResource( + withLegacyFields({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }) + ); const record = store.push({ data: { @@ -101,19 +102,20 @@ module('Legacy Mode', function (hooks) { test('records not in legacy mode do not set their constructor modelName value to their type', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - legacy: false, - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + ], + }) + ); const record = store.push({ data: { @@ -140,20 +142,21 @@ module('Legacy Mode', function (hooks) { test('records in legacy mode set their constructor modelName value to the correct type', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - ]), - }); + schema.registerResource( + withLegacyFields({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }) + ); const record = store.push({ data: { @@ -175,19 +178,20 @@ module('Legacy Mode', function (hooks) { test('records not in legacy mode can access values with type "field"', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - legacy: false, - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + ], + }) + ); const record = store.push({ data: { @@ -205,19 +209,20 @@ module('Legacy Mode', function (hooks) { test('records in legacy mode cannot access values with type "field"', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - kind: 'field', - }, - ]), - }); + schema.registerResource( + withLegacyFields({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + ], + }) + ); const record = store.push({ data: { @@ -243,26 +248,27 @@ module('Legacy Mode', function (hooks) { test('records in legacy mode cannot access resources', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - { - name: 'bestFriend', - type: 'user', - kind: 'resource', - options: { inverse: 'bestFriend', async: true }, - }, - ]), - }); + schema.registerResource( + withLegacyFields({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + { + name: 'bestFriend', + type: 'user', + kind: 'resource', + options: { inverse: 'bestFriend', async: true }, + }, + ], + }) + ); const record = store.push({ data: { @@ -309,20 +315,21 @@ module('Legacy Mode', function (hooks) { test('we can access errors', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - ]), - }); + schema.registerResource( + withLegacyFields({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }) + ); const record = store.push({ data: { @@ -345,20 +352,21 @@ module('Legacy Mode', function (hooks) { test('we can access currentState', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - ]), - }); + schema.registerResource( + withLegacyFields({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }) + ); const record = store.push({ data: { @@ -383,20 +391,21 @@ module('Legacy Mode', function (hooks) { test('we can use unloadRecord', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - ]), - }); + schema.registerResource( + withLegacyFields({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }) + ); const record = store.push({ data: { @@ -418,20 +427,21 @@ module('Legacy Mode', function (hooks) { test('we can use deleteRecord', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - ]), - }); + schema.registerResource( + withLegacyFields({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }) + ); const record = store.push({ data: { @@ -448,20 +458,21 @@ module('Legacy Mode', function (hooks) { test('we can use _createSnapshot', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - ]), - }); + schema.registerResource( + withLegacyFields({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }) + ); const record = store.push({ data: { @@ -480,20 +491,21 @@ module('Legacy Mode', function (hooks) { test('we can access state flags', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - ]), - }); + schema.registerResource( + withLegacyFields({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }) + ); const record = store.push({ data: { @@ -518,20 +530,21 @@ module('Legacy Mode', function (hooks) { test('we can access object lifecycle flags', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - ]), - }); + schema.registerResource( + withLegacyFields({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }) + ); const record = store.push({ data: { @@ -572,20 +585,21 @@ module('Legacy Mode', function (hooks) { // @ts-expect-error return serializeRecord.apply(this, arguments); }; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - ]), - }); + schema.registerResource( + withLegacyFields({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }) + ); const record = store.push({ data: { @@ -639,20 +653,21 @@ module('Legacy Mode', function (hooks) { store.requestManager = new RequestManager(); store.requestManager.useCache(CacheHandler); store.requestManager.use([LegacyNetworkHandler]); - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - ]), - }); + schema.registerResource( + withLegacyFields({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }) + ); const record = store.push({ data: { @@ -672,20 +687,21 @@ module('Legacy Mode', function (hooks) { test('we can rollbackAttributes', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - ]), - }); + schema.registerResource( + withLegacyFields({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }) + ); const record = store.push({ data: { @@ -741,20 +757,21 @@ module('Legacy Mode', function (hooks) { store.requestManager = new RequestManager(); store.requestManager.useCache(CacheHandler); store.requestManager.use([LegacyNetworkHandler]); - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - ]), - }); + schema.registerResource( + withLegacyFields({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }) + ); const record = store.push({ data: { @@ -806,20 +823,21 @@ module('Legacy Mode', function (hooks) { store.requestManager = new RequestManager(); store.requestManager.useCache(CacheHandler); store.requestManager.use([LegacyNetworkHandler]); - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - ]), - }); + schema.registerResource( + withLegacyFields({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }) + ); const record = store.push({ data: { diff --git a/tests/warp-drive__schema-record/tests/legacy/reactivity/basic-fields-test.ts b/tests/warp-drive__schema-record/tests/legacy/reactivity/basic-fields-test.ts index ef7d6b33a71..06e20c7619a 100644 --- a/tests/warp-drive__schema-record/tests/legacy/reactivity/basic-fields-test.ts +++ b/tests/warp-drive__schema-record/tests/legacy/reactivity/basic-fields-test.ts @@ -7,14 +7,13 @@ import { setupRenderingTest } from 'ember-qunit'; import { registerDerivations as registerLegacyDerivations, - withFields as withLegacyFields, + withDefaults as withLegacy, } from '@ember-data/model/migration-support'; import type Store from '@ember-data/store'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; -import type { FieldSchema } from '@warp-drive/core-types/schema/fields'; +import { Type } from '@warp-drive/core-types/symbols'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; -import type { Transform } from '@warp-drive/schema-record/schema'; -import { SchemaService } from '@warp-drive/schema-record/schema'; +import type { Transformation } from '@warp-drive/schema-record/schema'; import { simplePayloadNormalize } from '../../-utils/normalize-payload'; import { reactiveContext } from '../../-utils/reactive-context'; @@ -33,23 +32,23 @@ module('Legacy | Reactivity | basic fields can receive remote updates', function test('we can use simple fields with no `type`', async function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - ]), - }); - const fieldsMap = schema.schemas.get('user')!.fields; - const fields: FieldSchema[] = [...fieldsMap.values()]; + schema.registerResource( + withLegacy({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }) + ); + const resource = schema.resource({ type: 'user' }); const record = store.push({ data: { type: 'user', @@ -61,7 +60,7 @@ module('Legacy | Reactivity | basic fields can receive remote updates', function assert.strictEqual(record.id, '1', 'id is accessible'); assert.strictEqual(record.name, 'Rey Pupatine', 'name is accessible'); - const { counters, fieldOrder } = await reactiveContext.call(this, record, fields); + const { counters, fieldOrder } = await reactiveContext.call(this, record, resource); const nameIndex = fieldOrder.indexOf('name'); assert.strictEqual(counters.id, 1, 'idCount is 1'); @@ -91,8 +90,7 @@ module('Legacy | Reactivity | basic fields can receive remote updates', function test('we can use simple fields with a `type`', async function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); this.owner.register( @@ -108,7 +106,7 @@ module('Legacy | Reactivity | basic fields can receive remote updates', function } ); - const FloatTransform: Transform = { + const FloatTransform: Transformation = { serialize(value: string | number, options: { precision?: number } | null, _record: SchemaRecord): never { assert.ok(false, 'unexpected serialize'); throw new Error('unexpected serialize'); @@ -121,48 +119,49 @@ module('Legacy | Reactivity | basic fields can receive remote updates', function assert.ok(false, 'unexpected defaultValue'); throw new Error('unexpected defaultValue'); }, + [Type]: 'float', }; - schema.registerTransform('float', FloatTransform); + schema.registerTransformation(FloatTransform); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - { - name: 'rank', - type: 'float', - kind: 'attribute', - options: { precision: 0, defaultValue: 0 }, - }, - { - name: 'age', - type: 'float', - options: { precision: 0, defaultValue: 0 }, - kind: 'attribute', - }, - { - name: 'netWorth', - type: 'float', - options: { precision: 2, defaultValue: 0 }, - kind: 'attribute', - }, - { - name: 'coolometer', - type: 'float', - options: { defaultValue: 0 }, - kind: 'attribute', - }, - ]), - }); - - const fieldsMap = schema.schemas.get('user')!.fields; - const fields: FieldSchema[] = [...fieldsMap.values()]; + schema.registerResource( + withLegacy({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + { + name: 'rank', + type: 'float', + kind: 'attribute', + options: { precision: 0, defaultValue: 0 }, + }, + { + name: 'age', + type: 'float', + options: { precision: 0, defaultValue: 0 }, + kind: 'attribute', + }, + { + name: 'netWorth', + type: 'float', + options: { precision: 2, defaultValue: 0 }, + kind: 'attribute', + }, + { + name: 'coolometer', + type: 'float', + options: { defaultValue: 0 }, + kind: 'attribute', + }, + ], + }) + ); + const resource = schema.resource({ type: 'user' }); const record = store.push( simplePayloadNormalize(this.owner, { data: { @@ -187,7 +186,7 @@ module('Legacy | Reactivity | basic fields can receive remote updates', function assert.strictEqual(record.coolometer, 100, 'coolometer is accessible'); assert.strictEqual(record.rank, 0, 'rank is accessible'); - const { counters, fieldOrder } = await reactiveContext.call(this, record, fields); + const { counters, fieldOrder } = await reactiveContext.call(this, record, resource); const nameIndex = fieldOrder.indexOf('name'); assert.strictEqual(counters.id, 1, 'idCount is 1'); @@ -252,8 +251,7 @@ module('Legacy | Reactivity | basic fields can receive remote updates', function test('When attribute does not declare defaultValue but a matching new-style transform does, we ignore it', async function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); this.owner.register( @@ -269,7 +267,7 @@ module('Legacy | Reactivity | basic fields can receive remote updates', function } ); - const FloatTransform: Transform = { + const FloatTransform: Transformation = { serialize(value: string | number, options: { precision?: number } | null, _record: SchemaRecord): never { assert.ok(false, 'unexpected serialize'); throw new Error('unexpected serialize'); @@ -282,29 +280,30 @@ module('Legacy | Reactivity | basic fields can receive remote updates', function assert.ok(false, 'unexpected defaultValue'); throw new Error('unexpected defaultValue'); }, + [Type]: 'float', }; - schema.registerTransform('float', FloatTransform); + schema.registerTransformation(FloatTransform); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - { - name: 'coolometer', - type: 'float', - kind: 'attribute', - }, - ]), - }); - - const fieldsMap = schema.schemas.get('user')!.fields; - const fields: FieldSchema[] = [...fieldsMap.values()]; + schema.registerResource( + withLegacy({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + { + name: 'coolometer', + type: 'float', + kind: 'attribute', + }, + ], + }) + ); + const resource = schema.resource({ type: 'user' }); const record = store.push( simplePayloadNormalize(this.owner, { data: { @@ -320,7 +319,7 @@ module('Legacy | Reactivity | basic fields can receive remote updates', function assert.strictEqual(record.id, '1', 'id is accessible'); assert.strictEqual(record.name, 'Rey Pupatine', 'name is accessible'); assert.strictEqual(record.coolometer, undefined, 'coolometer is accessible'); - const { counters, fieldOrder } = await reactiveContext.call(this, record, fields); + const { counters, fieldOrder } = await reactiveContext.call(this, record, resource); const nameIndex = fieldOrder.indexOf('name'); assert.strictEqual(counters.id, 1, 'idCount is 1'); diff --git a/tests/warp-drive__schema-record/tests/legacy/reads/basic-fields-test.ts b/tests/warp-drive__schema-record/tests/legacy/reads/basic-fields-test.ts index 0d3004bb91f..93e77934ec3 100644 --- a/tests/warp-drive__schema-record/tests/legacy/reads/basic-fields-test.ts +++ b/tests/warp-drive__schema-record/tests/legacy/reads/basic-fields-test.ts @@ -6,13 +6,13 @@ import { setupTest } from 'ember-qunit'; import { registerDerivations as registerLegacyDerivations, - withFields as withLegacyFields, + withDefaults as withLegacy, } from '@ember-data/model/migration-support'; import { recordIdentifierFor } from '@ember-data/store'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import { Type } from '@warp-drive/core-types/symbols'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; -import type { Transform } from '@warp-drive/schema-record/schema'; -import { SchemaService } from '@warp-drive/schema-record/schema'; +import type { Transformation } from '@warp-drive/schema-record/schema'; import type Store from 'warp-drive__schema-record/services/store'; @@ -31,20 +31,21 @@ module('Legacy | Reads | basic fields', function (hooks) { test('we can use simple fields with no `type`', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - ]), - }); + schema.registerResource( + withLegacy({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + ], + }) + ); const record = store.createRecord('user', { name: 'Rey Skybarker' }) as User; @@ -72,8 +73,7 @@ module('Legacy | Reads | basic fields', function (hooks) { test('we can use simple fields with a `type`', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerLegacyDerivations(schema); this.owner.register( @@ -88,7 +88,7 @@ module('Legacy | Reads | basic fields', function (hooks) { } ); - const FloatTransform: Transform = { + const FloatTransform: Transformation = { serialize(value: string | number, options: { precision?: number } | null, _record: SchemaRecord): never { assert.ok(false, 'unexpected serialize'); throw new Error('unexpected serialize'); @@ -101,49 +101,52 @@ module('Legacy | Reads | basic fields', function (hooks) { assert.ok(false, 'unexpected defaultValue'); throw new Error('unexpected defaultValue'); }, + [Type]: 'float', }; - schema.registerTransform('float', FloatTransform); - - schema.defineSchema('user', { - legacy: true, - fields: withLegacyFields([ - { - name: 'name', - type: null, - kind: 'attribute', - }, - { - name: 'lastName', - type: 'string', - kind: 'attribute', - }, - { - name: 'rank', - type: 'float', - kind: 'attribute', - options: { precision: 0, defaultValue: 0 }, - }, - { - name: 'age', - type: 'float', - options: { precision: 0, defaultValue: 0 }, - kind: 'attribute', - }, - { - name: 'netWorth', - type: 'float', - options: { precision: 2, defaultValue: 0 }, - kind: 'attribute', - }, - { - name: 'coolometer', - type: 'float', - options: { defaultValue: 0 }, - kind: 'attribute', - }, - ]), - }); + schema.registerTransformation(FloatTransform); + + schema.registerResource( + withLegacy({ + type: 'user', + fields: [ + { + name: 'name', + type: null, + kind: 'attribute', + }, + { + name: 'lastName', + type: 'string', + kind: 'attribute', + }, + { + name: 'rank', + type: 'float', + kind: 'attribute', + options: { precision: 0, defaultValue: 0 }, + }, + { + name: 'age', + type: 'float', + options: { precision: 0, defaultValue: 0 }, + kind: 'attribute', + }, + { + name: 'netWorth', + type: 'float', + options: { precision: 2, defaultValue: 0 }, + kind: 'attribute', + }, + { + name: 'coolometer', + type: 'float', + options: { defaultValue: 0 }, + kind: 'attribute', + }, + ], + }) + ); const record = store.createRecord('user', { name: 'Rey Skybarker', diff --git a/tests/warp-drive__schema-record/tests/reactivity/array-test.ts b/tests/warp-drive__schema-record/tests/reactivity/array-test.ts index 3dd3a97f7c6..77757866f19 100644 --- a/tests/warp-drive__schema-record/tests/reactivity/array-test.ts +++ b/tests/warp-drive__schema-record/tests/reactivity/array-test.ts @@ -5,8 +5,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import type Store from '@ember-data/store'; -import type { FieldSchema } from '@warp-drive/core-types/schema/fields'; -import { registerDerivations, SchemaService, withFields } from '@warp-drive/schema-record/schema'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; import { reactiveContext } from '../-utils/reactive-context'; @@ -26,21 +25,21 @@ module('Reactivity | array fields can receive remote updates', function (hooks) test('we can use simple fields with no `type`', async function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'favoriteNumbers', - kind: 'array', - }, - ]), - }); - const fieldsMap = schema.schemas.get('user')!.fields; - const fields: FieldSchema[] = [...fieldsMap.values()]; - + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'favoriteNumbers', + kind: 'array', + }, + ], + }) + ); + const resource = schema.resource({ type: 'user' }); const record = store.push({ data: { type: 'user', @@ -53,7 +52,7 @@ module('Reactivity | array fields can receive remote updates', function (hooks) assert.strictEqual(record.$type, 'user', '$type is accessible'); assert.deepEqual(record.favoriteNumbers, ['1', '2'], 'favoriteNumbers is accessible'); - const { counters, fieldOrder } = await reactiveContext.call(this, record, fields); + const { counters, fieldOrder } = await reactiveContext.call(this, record, resource); const favoriteNumbersIndex = fieldOrder.indexOf('favoriteNumbers'); assert.strictEqual(counters.id, 1, 'idCount is 1'); diff --git a/tests/warp-drive__schema-record/tests/reactivity/basic-fields-test.ts b/tests/warp-drive__schema-record/tests/reactivity/basic-fields-test.ts index 386fec74a07..5008e87befb 100644 --- a/tests/warp-drive__schema-record/tests/reactivity/basic-fields-test.ts +++ b/tests/warp-drive__schema-record/tests/reactivity/basic-fields-test.ts @@ -6,10 +6,10 @@ import { setupRenderingTest } from 'ember-qunit'; import type Store from '@ember-data/store'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; -import type { FieldSchema } from '@warp-drive/core-types/schema/fields'; +import { Type } from '@warp-drive/core-types/symbols'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; -import type { Transform } from '@warp-drive/schema-record/schema'; -import { registerDerivations, SchemaService, withFields } from '@warp-drive/schema-record/schema'; +import type { Transformation } from '@warp-drive/schema-record/schema'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; import { reactiveContext } from '../-utils/reactive-context'; @@ -28,21 +28,21 @@ module('Reactivity | basic fields can receive remote updates', function (hooks) test('we can use simple fields with no `type`', async function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - ]), - }); - const fieldsMap = schema.schemas.get('user')!.fields; - const fields: FieldSchema[] = [...fieldsMap.values()]; - + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + ], + }) + ); + const resource = schema.resource({ type: 'user' }); const record = store.push({ data: { type: 'user', @@ -55,7 +55,7 @@ module('Reactivity | basic fields can receive remote updates', function (hooks) assert.strictEqual(record.$type, 'user', '$type is accessible'); assert.strictEqual(record.name, 'Rey Pupatine', 'name is accessible'); - const { counters, fieldOrder } = await reactiveContext.call(this, record, fields); + const { counters, fieldOrder } = await reactiveContext.call(this, record, resource); const nameIndex = fieldOrder.indexOf('name'); assert.strictEqual(counters.id, 1, 'idCount is 1'); @@ -88,11 +88,10 @@ module('Reactivity | basic fields can receive remote updates', function (hooks) test('we can use simple fields with a `type`', async function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - const FloatTransform: Transform = { + const FloatTransform: Transformation = { serialize(value: string | number, options: { precision?: number } | null, _record: SchemaRecord): string { return typeof value === 'number' ? value.toFixed(options?.precision ?? 3) @@ -108,45 +107,47 @@ module('Reactivity | basic fields can receive remote updates', function (hooks) const v = 0; return v.toFixed(_options?.precision ?? 3); }, + [Type]: 'float', }; - schema.registerTransform('float', FloatTransform); - - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'rank', - type: 'float', - kind: 'field', - options: { precision: 0 }, - }, - { - name: 'age', - type: 'float', - options: { precision: 0 }, - kind: 'field', - }, - { - name: 'netWorth', - type: 'float', - options: { precision: 2 }, - kind: 'field', - }, - { - name: 'coolometer', - type: 'float', - kind: 'field', - }, - ]), - }); - - const fieldsMap = schema.schemas.get('user')!.fields; - const fields: FieldSchema[] = [...fieldsMap.values()]; + schema.registerTransformation(FloatTransform); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'rank', + type: 'float', + kind: 'field', + options: { precision: 0 }, + }, + { + name: 'age', + type: 'float', + options: { precision: 0 }, + kind: 'field', + }, + { + name: 'netWorth', + type: 'float', + options: { precision: 2 }, + kind: 'field', + }, + { + name: 'coolometer', + type: 'float', + kind: 'field', + }, + ], + }) + ); + + const resource = schema.resource({ type: 'user' }); const record = store.push({ data: { type: 'user', @@ -168,7 +169,7 @@ module('Reactivity | basic fields can receive remote updates', function (hooks) assert.strictEqual(record.coolometer, 100, 'coolometer is accessible'); assert.strictEqual(record.rank, 0, 'rank is accessible'); - const { counters, fieldOrder } = await reactiveContext.call(this, record, fields); + const { counters, fieldOrder } = await reactiveContext.call(this, record, resource); const nameIndex = fieldOrder.indexOf('name'); assert.strictEqual(counters.id, 1, 'idCount is 1'); diff --git a/tests/warp-drive__schema-record/tests/reactivity/derivation-test.ts b/tests/warp-drive__schema-record/tests/reactivity/derivation-test.ts index f722a54b61e..ab8d1f53e53 100644 --- a/tests/warp-drive__schema-record/tests/reactivity/derivation-test.ts +++ b/tests/warp-drive__schema-record/tests/reactivity/derivation-test.ts @@ -5,9 +5,9 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import type Store from '@ember-data/store'; -import type { FieldSchema } from '@warp-drive/core-types/schema/fields'; +import { Type } from '@warp-drive/core-types/symbols'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; -import { registerDerivations, SchemaService, withFields } from '@warp-drive/schema-record/schema'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; import { reactiveContext } from '../-utils/reactive-context'; @@ -24,8 +24,7 @@ module('Reactivity | derivation', function (hooks) { test('we can derive from simple fields', async function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; function concat( record: SchemaRecord & { [key: string]: unknown }, @@ -36,32 +35,34 @@ module('Reactivity | derivation', function (hooks) { const opts = options as { fields: string[]; separator?: string }; return opts.fields.map((field) => record[field]).join(opts.separator ?? ''); } + concat[Type] = 'concat'; - schema.registerDerivation('concat', concat); + schema.registerDerivation(concat); registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'firstName', - kind: 'field', - }, - { - name: 'lastName', - kind: 'field', - }, - { - name: 'fullName', - type: 'concat', - options: { fields: ['firstName', 'lastName'], separator: ' ' }, - kind: 'derived', - }, - ]), - }); - - const fieldsMap = schema.schemas.get('user')!.fields; - const fields: FieldSchema[] = [...fieldsMap.values()]; - + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'firstName', + kind: 'field', + }, + { + name: 'lastName', + kind: 'field', + }, + { + name: 'fullName', + type: 'concat', + options: { fields: ['firstName', 'lastName'], separator: ' ' }, + kind: 'derived', + }, + ], + }) + ); + + const resource = schema.resource({ type: 'user' }); const record = store.push({ data: { id: '1', @@ -79,7 +80,7 @@ module('Reactivity | derivation', function (hooks) { assert.strictEqual(record.lastName, 'Pupatine', 'lastName is accessible'); assert.strictEqual(record.fullName, 'Rey Pupatine', 'fullName is accessible'); - const { counters, fieldOrder } = await reactiveContext.call(this, record, fields); + const { counters, fieldOrder } = await reactiveContext.call(this, record, resource); const nameIndex = fieldOrder.indexOf('firstName'); assert.strictEqual(counters.id, 1, 'id Count is 1'); @@ -122,8 +123,7 @@ module('Reactivity | derivation', function (hooks) { test('derivations do not re-run unless the tracked state they consume is dirtied', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); function concat( @@ -137,31 +137,34 @@ module('Reactivity | derivation', function (hooks) { assert.step(`concat: ${result}`); return result; } + concat[Type] = 'concat'; - schema.registerDerivation('concat', concat); - - schema.defineSchema('user', { - fields: withFields([ - { - name: 'age', - kind: 'field', - }, - { - name: 'firstName', - kind: 'field', - }, - { - name: 'lastName', - kind: 'field', - }, - { - name: 'fullName', - type: 'concat', - options: { fields: ['firstName', 'lastName'], separator: ' ' }, - kind: 'derived', - }, - ]), - }); + schema.registerDerivation(concat); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'age', + kind: 'field', + }, + { + name: 'firstName', + kind: 'field', + }, + { + name: 'lastName', + kind: 'field', + }, + { + name: 'fullName', + type: 'concat', + options: { fields: ['firstName', 'lastName'], separator: ' ' }, + kind: 'derived', + }, + ], + }) + ); const record = store.push({ data: { @@ -179,15 +182,10 @@ module('Reactivity | derivation', function (hooks) { assert.strictEqual(record.$type, 'user', '$type is accessible'); assert.strictEqual(record.firstName, 'Rey', 'firstName is accessible'); assert.strictEqual(record.lastName, 'Pupatine', 'lastName is accessible'); - assert.verifySteps([], 'no concat yet'); - assert.strictEqual(record.fullName, 'Rey Pupatine', 'fullName is accessible'); - assert.verifySteps(['concat: Rey Pupatine'], 'concat happened'); - assert.strictEqual(record.fullName, 'Rey Pupatine', 'fullName is accessible'); - assert.verifySteps([], 'no additional concat'); store.push({ @@ -203,7 +201,6 @@ module('Reactivity | derivation', function (hooks) { }) as User; assert.strictEqual(record.fullName, 'Rey Pupatine', 'fullName is accessible'); - assert.verifySteps([], 'no additional concat'); store.push({ diff --git a/tests/warp-drive__schema-record/tests/reactivity/resource-test.ts b/tests/warp-drive__schema-record/tests/reactivity/resource-test.ts index 04f22cc51e6..8085b1e0f55 100644 --- a/tests/warp-drive__schema-record/tests/reactivity/resource-test.ts +++ b/tests/warp-drive__schema-record/tests/reactivity/resource-test.ts @@ -4,8 +4,9 @@ import { setupTest } from 'ember-qunit'; import type Store from '@ember-data/store'; import type { Document } from '@ember-data/store'; +import { Type } from '@warp-drive/core-types/symbols'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; -import { SchemaService } from '@warp-drive/schema-record/schema'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; interface User { id: string | null; @@ -19,8 +20,7 @@ module('Reactivity | resource', function (hooks) { test('we can use simple fields with no `type`', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; function concat( record: SchemaRecord & { [key: string]: unknown }, @@ -31,23 +31,27 @@ module('Reactivity | resource', function (hooks) { const opts = options as { fields: string[]; separator?: string }; return opts.fields.map((field) => record[field]).join(opts.separator ?? ''); } + concat[Type] = 'concat'; - schema.registerDerivation('concat', concat); - - schema.defineSchema('user', { - fields: [ - { - name: 'name', - kind: 'field', - }, - { - name: 'bestFriend', - type: 'user', - kind: 'resource', - options: { inverse: 'bestFriend', async: true }, - }, - ], - }); + registerDerivations(schema); + schema.registerDerivation(concat); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'bestFriend', + type: 'user', + kind: 'resource', + options: { inverse: 'bestFriend', async: true }, + }, + ], + }) + ); const record = store.push({ data: { diff --git a/tests/warp-drive__schema-record/tests/reads/array-test.ts b/tests/warp-drive__schema-record/tests/reads/array-test.ts index b44cff82b8e..94664e2c350 100644 --- a/tests/warp-drive__schema-record/tests/reads/array-test.ts +++ b/tests/warp-drive__schema-record/tests/reads/array-test.ts @@ -3,9 +3,9 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import { recordIdentifierFor } from '@ember-data/store'; -import type { ResourceType } from '@warp-drive/core-types/symbols'; -import type { Transform } from '@warp-drive/schema-record/schema'; -import { registerDerivations, SchemaService, withFields } from '@warp-drive/schema-record/schema'; +import { type ResourceType, Type } from '@warp-drive/core-types/symbols'; +import type { Transformation } from '@warp-drive/schema-record/schema'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; import type Store from 'warp-drive__schema-record/services/store'; @@ -22,22 +22,24 @@ module('Reads | array fields', function (hooks) { test('we can use simple array fields with no `type`', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'favoriteNumbers', - kind: 'array', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'favoriteNumbers', + kind: 'array', + }, + ], + }) + ); const sourceArray = ['1', '2']; const record = store.createRecord('user', { name: 'Rey Skybarker', favoriteNumbers: sourceArray }); @@ -68,25 +70,27 @@ module('Reads | array fields', function (hooks) { test('we can use simple array fields with a `type`', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'favoriteNumbers', - type: 'string-from-int', - kind: 'array', - }, - ]), - }); - - const StringFromIntTransform: Transform = { + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'favoriteNumbers', + type: 'string-from-int', + kind: 'array', + }, + ], + }) + ); + + const StringFromIntTransform: Transformation = { serialize(value: string, options, _record): number { return parseInt(value); }, @@ -97,9 +101,10 @@ module('Reads | array fields', function (hooks) { assert.ok(false, 'unexpected defaultValue'); throw new Error('unexpected defaultValue'); }, + [Type]: 'string-from-int', }; - schema.registerTransform('string-from-int', StringFromIntTransform); + schema.registerTransformation(StringFromIntTransform); const sourceArray = ['1', '2']; const record = store.createRecord('user', { name: 'Rey Skybarker', favoriteNumbers: sourceArray }); diff --git a/tests/warp-drive__schema-record/tests/reads/basic-fields-test.ts b/tests/warp-drive__schema-record/tests/reads/basic-fields-test.ts index 13b0afa16bb..22609b01641 100644 --- a/tests/warp-drive__schema-record/tests/reads/basic-fields-test.ts +++ b/tests/warp-drive__schema-record/tests/reads/basic-fields-test.ts @@ -4,9 +4,10 @@ import { setupTest } from 'ember-qunit'; import { recordIdentifierFor } from '@ember-data/store'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; +import { Type } from '@warp-drive/core-types/symbols'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; -import type { Transform } from '@warp-drive/schema-record/schema'; -import { registerDerivations, SchemaService, withFields } from '@warp-drive/schema-record/schema'; +import type { Transformation } from '@warp-drive/schema-record/schema'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; import type Store from 'warp-drive__schema-record/services/store'; @@ -25,18 +26,20 @@ module('Reads | basic fields', function (hooks) { test('we can use simple fields with no `type`', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + ], + }) + ); const record = store.createRecord('user', { name: 'Rey Skybarker' }) as User; @@ -60,10 +63,9 @@ module('Reads | basic fields', function (hooks) { test('we can use simple fields with a `type`', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; - const FloatTransform: Transform = { + const FloatTransform: Transformation = { serialize(value: string | number, options: { precision?: number } | null, _record: SchemaRecord): string { return typeof value === 'number' ? value.toFixed(options?.precision ?? 3) @@ -79,47 +81,51 @@ module('Reads | basic fields', function (hooks) { const v = 0; return v.toFixed(_options?.precision ?? 3); }, + [Type]: 'float', }; - schema.registerTransform('float', FloatTransform); + schema.registerTransformation(FloatTransform); registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'lastName', - type: 'string', - kind: 'field', - }, - { - name: 'rank', - type: 'float', - kind: 'field', - options: { precision: 0 }, - }, - { - name: 'age', - type: 'float', - options: { precision: 0 }, - kind: 'field', - }, - { - name: 'netWorth', - type: 'float', - options: { precision: 2 }, - kind: 'field', - }, - { - name: 'coolometer', - type: 'float', - kind: 'field', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'lastName', + type: 'string', + kind: 'field', + }, + { + name: 'rank', + type: 'float', + kind: 'field', + options: { precision: 0 }, + }, + { + name: 'age', + type: 'float', + options: { precision: 0 }, + kind: 'field', + }, + { + name: 'netWorth', + type: 'float', + options: { precision: 2 }, + kind: 'field', + }, + { + name: 'coolometer', + type: 'float', + kind: 'field', + }, + ], + }) + ); const record = store.createRecord('user', { name: 'Rey Skybarker', @@ -144,11 +150,24 @@ module('Reads | basic fields', function (hooks) { } catch (e) { assert.strictEqual( (e as Error).message, - `No 'string' transform defined for use by user.lastName`, + `No transformation registered with name 'string'`, 'should error when accessing unknown field transform' ); } + store.schema.registerTransformation({ + serialize(value: string, _options, _record): string { + return value; + }, + hydrate(value: string, _options, _record): string { + return value; + }, + defaultValue(_options, _identifier) { + return ''; + }, + [Type]: 'string', + }); + const resource = store.cache.peek(identifier)!; assert.strictEqual(store.cache.getAttr(identifier, 'name'), 'Rey Skybarker', 'cache value for name is correct'); diff --git a/tests/warp-drive__schema-record/tests/reads/derivation-test.ts b/tests/warp-drive__schema-record/tests/reads/derivation-test.ts index b398af90484..d0b5c096104 100644 --- a/tests/warp-drive__schema-record/tests/reads/derivation-test.ts +++ b/tests/warp-drive__schema-record/tests/reads/derivation-test.ts @@ -3,8 +3,9 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import type Store from '@ember-data/store'; +import { Type } from '@warp-drive/core-types/symbols'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; -import { registerDerivations, SchemaService, withFields } from '@warp-drive/schema-record/schema'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; interface User { id: string | null; @@ -19,8 +20,7 @@ module('Reads | derivation', function (hooks) { test('we can use simple fields with no `type`', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; function concat( record: SchemaRecord & { [key: string]: unknown }, @@ -31,28 +31,32 @@ module('Reads | derivation', function (hooks) { const opts = options as { fields: string[]; separator?: string }; return opts.fields.map((field) => record[field]).join(opts.separator ?? ''); } + concat[Type] = 'concat'; - schema.registerDerivation('concat', concat); + schema.registerDerivation(concat); registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'firstName', - kind: 'field', - }, - { - name: 'lastName', - kind: 'field', - }, - { - name: 'fullName', - type: 'concat', - options: { fields: ['firstName', 'lastName'], separator: ' ' }, - kind: 'derived', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'firstName', + kind: 'field', + }, + { + name: 'lastName', + kind: 'field', + }, + { + name: 'fullName', + type: 'concat', + options: { fields: ['firstName', 'lastName'], separator: ' ' }, + kind: 'derived', + }, + ], + }) + ); const record = store.createRecord('user', { firstName: 'Rey', lastName: 'Skybarker' }) as User; @@ -66,30 +70,32 @@ module('Reads | derivation', function (hooks) { test('throws an error if derivation is not found', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'firstName', - type: null, - kind: 'attribute', - }, - { - name: 'lastName', - type: null, - kind: 'attribute', - }, - { - name: 'fullName', - type: 'concat', - options: { fields: ['firstName', 'lastName'], separator: ' ' }, - kind: 'derived', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'firstName', + type: null, + kind: 'attribute', + }, + { + name: 'lastName', + type: null, + kind: 'attribute', + }, + { + name: 'fullName', + type: 'concat', + options: { fields: ['firstName', 'lastName'], separator: ' ' }, + kind: 'derived', + }, + ], + }) + ); const record = store.push({ data: { @@ -106,11 +112,7 @@ module('Reads | derivation', function (hooks) { record.fullName; assert.ok(false, 'record.fullName should throw'); } catch (e) { - assert.strictEqual( - (e as Error).message, - "No 'concat' derivation defined for use by user.fullName", - 'record.fullName throws' - ); + assert.strictEqual((e as Error).message, "No derivation registered with name 'concat'", 'record.fullName throws'); } }); }); diff --git a/tests/warp-drive__schema-record/tests/reads/object-test.ts b/tests/warp-drive__schema-record/tests/reads/object-test.ts index 2932be8aa31..96e4d4828df 100644 --- a/tests/warp-drive__schema-record/tests/reads/object-test.ts +++ b/tests/warp-drive__schema-record/tests/reads/object-test.ts @@ -3,9 +3,9 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import { recordIdentifierFor } from '@ember-data/store'; -import type { ResourceType } from '@warp-drive/core-types/symbols'; -import type { Transform } from '@warp-drive/schema-record/schema'; -import { registerDerivations, SchemaService, withFields } from '@warp-drive/schema-record/schema'; +import { type ResourceType, Type } from '@warp-drive/core-types/symbols'; +import type { Transformation } from '@warp-drive/schema-record/schema'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; import type Store from 'warp-drive__schema-record/services/store'; @@ -29,22 +29,24 @@ module('Reads | object fields', function (hooks) { test('we can use simple object fields with no `type`', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'address', - kind: 'object', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'address', + kind: 'object', + }, + ], + }) + ); const sourceAddress: address = { street: '123 Main St', @@ -88,23 +90,25 @@ module('Reads | object fields', function (hooks) { test('we can use simple object fields with a `type`', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'address', - type: 'zip-string-from-int', - kind: 'object', - }, - ]), - }); - const ZipStringFromIntTransform: Transform = { + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'address', + type: 'zip-string-from-int', + kind: 'object', + }, + ], + }) + ); + const ZipStringFromIntTransform: Transformation = { serialize(value: address, options, _record): address { if (typeof value.zip === 'string') { return { @@ -128,8 +132,9 @@ module('Reads | object fields', function (hooks) { assert.ok(false, 'unexpected defaultValue'); throw new Error('unexpected defaultValue'); }, + [Type]: 'zip-string-from-int', }; - schema.registerTransform('zip-string-from-int', ZipStringFromIntTransform); + schema.registerTransformation(ZipStringFromIntTransform); const sourceAddress = { street: '123 Main St', city: 'Anytown', diff --git a/tests/warp-drive__schema-record/tests/reads/resource-test.ts b/tests/warp-drive__schema-record/tests/reads/resource-test.ts index 0eb60756d3a..6b3c529e79b 100644 --- a/tests/warp-drive__schema-record/tests/reads/resource-test.ts +++ b/tests/warp-drive__schema-record/tests/reads/resource-test.ts @@ -6,8 +6,9 @@ import { setupTest } from 'ember-qunit'; import type Store from '@ember-data/store'; import type { Document } from '@ember-data/store'; +import { Type } from '@warp-drive/core-types/symbols'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; -import { registerDerivations, SchemaService, withFields } from '@warp-drive/schema-record/schema'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; interface User { id: string | null; @@ -21,8 +22,7 @@ module('Reads | resource', function (hooks) { test('we can use simple fields with no `type`', function (this: TestContext, assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; function concat( record: SchemaRecord & { [key: string]: unknown }, @@ -33,24 +33,28 @@ module('Reads | resource', function (hooks) { const opts = options as { fields: string[]; separator?: string }; return opts.fields.map((field) => record[field]).join(opts.separator ?? ''); } + concat[Type] = 'concat'; - schema.registerDerivation('concat', concat); + schema.registerDerivation(concat); registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'bestFriend', - type: 'user', - kind: 'resource', - options: { inverse: 'bestFriend', async: true }, - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'bestFriend', + type: 'user', + kind: 'resource', + options: { inverse: 'bestFriend', async: true }, + }, + ], + }) + ); const record = store.push({ data: { diff --git a/tests/warp-drive__schema-record/tests/writes/array-test.ts b/tests/warp-drive__schema-record/tests/writes/array-test.ts index d40ede40e1b..69809f55f77 100644 --- a/tests/warp-drive__schema-record/tests/writes/array-test.ts +++ b/tests/warp-drive__schema-record/tests/writes/array-test.ts @@ -3,9 +3,9 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import { recordIdentifierFor } from '@ember-data/store'; -import type { ResourceType } from '@warp-drive/core-types/symbols'; -import type { Transform } from '@warp-drive/schema-record/schema'; -import { registerDerivations, SchemaService, withFields } from '@warp-drive/schema-record/schema'; +import { type ResourceType, Type } from '@warp-drive/core-types/symbols'; +import type { Transformation } from '@warp-drive/schema-record/schema'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; import type Store from 'warp-drive__schema-record/services/store'; @@ -29,22 +29,24 @@ module('Writes | array fields', function (hooks) { test('we can update to a new array', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'favoriteNumbers', - kind: 'array', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'favoriteNumbers', + kind: 'array', + }, + ], + }) + ); const record = store.push({ data: { @@ -78,22 +80,24 @@ module('Writes | array fields', function (hooks) { test('we can update to null', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'favoriteNumbers', - kind: 'array', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'favoriteNumbers', + kind: 'array', + }, + ], + }) + ); const record = store.push({ data: { @@ -127,22 +131,24 @@ module('Writes | array fields', function (hooks) { test('we can update a single value in the array', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'favoriteNumbers', - kind: 'array', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'favoriteNumbers', + kind: 'array', + }, + ], + }) + ); const record = store.push({ data: { @@ -171,22 +177,24 @@ module('Writes | array fields', function (hooks) { test('we can push a new value on to the array', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'favoriteNumbers', - kind: 'array', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'favoriteNumbers', + kind: 'array', + }, + ], + }) + ); const record = store.push({ data: { @@ -220,22 +228,24 @@ module('Writes | array fields', function (hooks) { test('we can pop a value off of the array', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'favoriteNumbers', - kind: 'array', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'favoriteNumbers', + kind: 'array', + }, + ], + }) + ); const record = store.push({ data: { @@ -266,22 +276,24 @@ module('Writes | array fields', function (hooks) { test('we can unshift a value on to the array', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'favoriteNumbers', - kind: 'array', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'favoriteNumbers', + kind: 'array', + }, + ], + }) + ); const record = store.push({ data: { @@ -315,22 +327,24 @@ module('Writes | array fields', function (hooks) { test('we can shift a value off of the array', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'favoriteNumbers', - kind: 'array', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'favoriteNumbers', + kind: 'array', + }, + ], + }) + ); const record = store.push({ data: { @@ -361,22 +375,24 @@ module('Writes | array fields', function (hooks) { test('we can assign an array value to another record', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'favoriteNumbers', - kind: 'array', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'favoriteNumbers', + kind: 'array', + }, + ], + }) + ); const record = store.push({ data: { @@ -421,25 +437,27 @@ module('Writes | array fields', function (hooks) { test('we can edit simple array fields with a `type`', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'favoriteNumbers', - type: 'string-from-int', - kind: 'array', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'favoriteNumbers', + type: 'string-from-int', + kind: 'array', + }, + ], + }) + ); - const StringFromIntTransform: Transform = { + const StringFromIntTransform: Transformation = { serialize(value: string, options, _record): number { return parseInt(value); }, @@ -450,9 +468,10 @@ module('Writes | array fields', function (hooks) { assert.ok(false, 'unexpected defaultValue'); throw new Error('unexpected defaultValue'); }, + [Type]: 'string-from-int', }; - schema.registerTransform('string-from-int', StringFromIntTransform); + schema.registerTransformation(StringFromIntTransform); const sourceArray = ['1', '2']; const record = store.createRecord('user', { name: 'Rey Skybarker', favoriteNumbers: sourceArray }); @@ -486,25 +505,27 @@ module('Writes | array fields', function (hooks) { test('we can edit single values in array fields with a `type`', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'favoriteNumbers', - type: 'string-from-int', - kind: 'array', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'favoriteNumbers', + type: 'string-from-int', + kind: 'array', + }, + ], + }) + ); - const StringFromIntTransform: Transform = { + const StringFromIntTransform: Transformation = { serialize(value: string, options, _record): number { return parseInt(value); }, @@ -515,9 +536,10 @@ module('Writes | array fields', function (hooks) { assert.ok(false, 'unexpected defaultValue'); throw new Error('unexpected defaultValue'); }, + [Type]: 'string-from-int', }; - schema.registerTransform('string-from-int', StringFromIntTransform); + schema.registerTransformation(StringFromIntTransform); const sourceArray = ['1', '2']; const record = store.createRecord('user', { name: 'Rey Skybarker', favoriteNumbers: sourceArray }); @@ -551,25 +573,27 @@ module('Writes | array fields', function (hooks) { test('we can push a new value on to array fields with a `type`', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'favoriteNumbers', - type: 'string-from-int', - kind: 'array', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'favoriteNumbers', + type: 'string-from-int', + kind: 'array', + }, + ], + }) + ); - const StringFromIntTransform: Transform = { + const StringFromIntTransform: Transformation = { serialize(value: string, options, _record): number { return parseInt(value); }, @@ -580,9 +604,10 @@ module('Writes | array fields', function (hooks) { assert.ok(false, 'unexpected defaultValue'); throw new Error('unexpected defaultValue'); }, + [Type]: 'string-from-int', }; - schema.registerTransform('string-from-int', StringFromIntTransform); + schema.registerTransformation(StringFromIntTransform); const sourceArray = ['1', '2']; const record = store.createRecord('user', { name: 'Rey Skybarker', favoriteNumbers: sourceArray }); @@ -616,25 +641,27 @@ module('Writes | array fields', function (hooks) { test('we can pop a value off of an array fields with a `type`', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'favoriteNumbers', - type: 'string-from-int', - kind: 'array', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'favoriteNumbers', + type: 'string-from-int', + kind: 'array', + }, + ], + }) + ); - const StringFromIntTransform: Transform = { + const StringFromIntTransform: Transformation = { serialize(value: string, options, _record): number { return parseInt(value); }, @@ -645,9 +672,10 @@ module('Writes | array fields', function (hooks) { assert.ok(false, 'unexpected defaultValue'); throw new Error('unexpected defaultValue'); }, + [Type]: 'string-from-int', }; - schema.registerTransform('string-from-int', StringFromIntTransform); + schema.registerTransformation(StringFromIntTransform); const sourceArray = ['1', '2']; const record = store.createRecord('user', { name: 'Rey Skybarker', favoriteNumbers: sourceArray }); diff --git a/tests/warp-drive__schema-record/tests/writes/object-test.ts b/tests/warp-drive__schema-record/tests/writes/object-test.ts index 0ff4fd179a7..fa8387b6bb3 100644 --- a/tests/warp-drive__schema-record/tests/writes/object-test.ts +++ b/tests/warp-drive__schema-record/tests/writes/object-test.ts @@ -3,9 +3,9 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import { recordIdentifierFor } from '@ember-data/store'; -import type { ResourceType } from '@warp-drive/core-types/symbols'; -import type { Transform } from '@warp-drive/schema-record/schema'; -import { registerDerivations, SchemaService, withFields } from '@warp-drive/schema-record/schema'; +import { type ResourceType, Type } from '@warp-drive/core-types/symbols'; +import type { Transformation } from '@warp-drive/schema-record/schema'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; import type Store from 'warp-drive__schema-record/services/store'; @@ -36,22 +36,24 @@ module('Writes | object fields', function (hooks) { test('we can update to a new object', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'address', - kind: 'object', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'address', + kind: 'object', + }, + ], + }) + ); const record = store.push({ data: { @@ -99,21 +101,23 @@ module('Writes | object fields', function (hooks) { test('we can update to null', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'address', - kind: 'object', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'address', + kind: 'object', + }, + ], + }) + ); const record = store.push({ data: { type: 'user', @@ -168,21 +172,23 @@ module('Writes | object fields', function (hooks) { test('we can update a single value in the object', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'address', - kind: 'object', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'address', + kind: 'object', + }, + ], + }) + ); const record = store.push({ data: { type: 'user', @@ -228,21 +234,23 @@ module('Writes | object fields', function (hooks) { test('we can assign an object value to another record', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'address', - kind: 'object', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'address', + kind: 'object', + }, + ], + }) + ); const record = store.push({ data: { type: 'user', @@ -314,24 +322,26 @@ module('Writes | object fields', function (hooks) { test('we can edit simple object fields with a `type`', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'address', - type: 'zip-string-from-int', - kind: 'object', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'address', + type: 'zip-string-from-int', + kind: 'object', + }, + ], + }) + ); - const ZipStringFromIntTransform: Transform = { + const ZipStringFromIntTransform: Transformation = { serialize(value: address, options, _record): address { if (typeof value.zip === 'string') { return { @@ -355,8 +365,9 @@ module('Writes | object fields', function (hooks) { assert.ok(false, 'unexpected defaultValue'); throw new Error('unexpected defaultValue'); }, + [Type]: 'zip-string-from-int', }; - schema.registerTransform('zip-string-from-int', ZipStringFromIntTransform); + schema.registerTransformation(ZipStringFromIntTransform); const sourceAddress = { street: '123 Main St', @@ -425,24 +436,26 @@ module('Writes | object fields', function (hooks) { test('we can edit single values in object fields with a `type`', function (assert) { const store = this.owner.lookup('service:store') as Store; - const schema = new SchemaService(); - store.registerSchema(schema); + const { schema } = store; registerDerivations(schema); - schema.defineSchema('user', { - fields: withFields([ - { - name: 'name', - kind: 'field', - }, - { - name: 'address', - type: 'zip-string-from-int', - kind: 'object', - }, - ]), - }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'address', + type: 'zip-string-from-int', + kind: 'object', + }, + ], + }) + ); - const ZipStringFromIntTransform: Transform = { + const ZipStringFromIntTransform: Transformation = { serialize(value: address, options, _record): address { if (typeof value.zip === 'string') { return { @@ -466,8 +479,9 @@ module('Writes | object fields', function (hooks) { assert.ok(false, 'unexpected defaultValue'); throw new Error('unexpected defaultValue'); }, + [Type]: 'zip-string-from-int', }; - schema.registerTransform('zip-string-from-int', ZipStringFromIntTransform); + schema.registerTransformation(ZipStringFromIntTransform); const sourceAddress = { street: '123 Main St', From a5b3ab4870ae5726015b3091cff5661fc8bb61e8 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 22 May 2024 01:33:11 -0700 Subject: [PATCH 2/4] add deprecations, fix prod test --- .../model/src/-private/schema-provider.ts | 31 +++++++++++++++++-- packages/schema-record/eslint.config.mjs | 2 +- packages/schema-record/src/schema.ts | 29 +++++++++++++++++ packages/schema-record/vite.config.mjs | 2 +- .../custom-class-model-test.ts | 5 ++- 5 files changed, 63 insertions(+), 6 deletions(-) diff --git a/packages/model/src/-private/schema-provider.ts b/packages/model/src/-private/schema-provider.ts index 33abf3db3a9..381e6c567df 100644 --- a/packages/model/src/-private/schema-provider.ts +++ b/packages/model/src/-private/schema-provider.ts @@ -17,6 +17,7 @@ import type { import type { FactoryCache, Model, ModelFactory, ModelStore } from './model'; import _modelForMixin from './model-for-mixin'; import { normalizeModelName } from './util'; +import { deprecate } from '@ember/debug'; type AttributesSchema = ReturnType>; type RelationshipsSchema = ReturnType>; @@ -158,14 +159,30 @@ export class ModelSchemaProvider implements SchemaService { if (ENABLE_LEGACY_SCHEMA_SERVICE) { ModelSchemaProvider.prototype.doesTypeExist = function (type: string): boolean { - // TODO @pr add deprecation here + deprecate(`Use \`schema.hasResource({ type })\` instead of \`schema.doesTypeExist(type)\``, false, { + id: 'ember-data:schema-service-updates', + until: '5.0', + for: 'ember-data', + since: { + available: '5.4', + enabled: '5.4', + }, + }); return this.hasResource({ type }); }; ModelSchemaProvider.prototype.attributesDefinitionFor = function ( resource: RecordIdentifier | { type: string } ): AttributesSchema { - // TODO @pr add deprecation here + deprecate(`Use \`schema.fields({ type })\` instead of \`schema.attributesDefinitionFor({ type })\``, false, { + id: 'ember-data:schema-service-updates', + until: '5.0', + for: 'ember-data', + since: { + available: '5.4', + enabled: '5.4', + }, + }); const type = normalizeModelName(resource.type); if (!this._schemas.has(type)) { @@ -178,7 +195,15 @@ if (ENABLE_LEGACY_SCHEMA_SERVICE) { ModelSchemaProvider.prototype.relationshipsDefinitionFor = function ( resource: RecordIdentifier | { type: string } ): RelationshipsSchema { - // TODO @pr add deprecation here + deprecate(`Use \`schema.fields({ type })\` instead of \`schema.relationshipsDefinitionFor({ type })\``, false, { + id: 'ember-data:schema-service-updates', + until: '5.0', + for: 'ember-data', + since: { + available: '5.4', + enabled: '5.4', + }, + }); const type = normalizeModelName(resource.type); if (!this._schemas.has(type)) { diff --git a/packages/schema-record/eslint.config.mjs b/packages/schema-record/eslint.config.mjs index 3b45156a9d4..63296bac802 100644 --- a/packages/schema-record/eslint.config.mjs +++ b/packages/schema-record/eslint.config.mjs @@ -11,7 +11,7 @@ export default [ // browser (js/ts) ================ typescript.browser({ srcDirs: ['src'], - allowedImports: [], + allowedImports: ['@ember/debug'], }), // node (module) ================ diff --git a/packages/schema-record/src/schema.ts b/packages/schema-record/src/schema.ts index f2cde23bdbd..72b0e8558f5 100644 --- a/packages/schema-record/src/schema.ts +++ b/packages/schema-record/src/schema.ts @@ -1,3 +1,5 @@ +import { deprecate } from '@ember/debug'; + import { recordIdentifierFor } from '@ember-data/store'; import type { SchemaService as SchemaServiceInterface } from '@ember-data/store/types'; import { createCache, getValue } from '@ember-data/tracking'; @@ -233,6 +235,15 @@ if (ENABLE_LEGACY_SCHEMA_SERVICE) { }: { type: string; }): InternalSchema['attributes'] { + deprecate(`Use \`schema.fields({ type })\` instead of \`schema.attributesDefinitionFor({ type })\``, false, { + id: 'ember-data:schema-service-updates', + until: '5.0', + for: 'ember-data', + since: { + available: '5.4', + enabled: '5.4', + }, + }); const schema = this._schemas.get(type); if (!schema) { @@ -247,6 +258,15 @@ if (ENABLE_LEGACY_SCHEMA_SERVICE) { }: { type: string; }): InternalSchema['relationships'] { + deprecate(`Use \`schema.fields({ type })\` instead of \`schema.relationshipsDefinitionFor({ type })\``, false, { + id: 'ember-data:schema-service-updates', + until: '5.0', + for: 'ember-data', + since: { + available: '5.4', + enabled: '5.4', + }, + }); const schema = this._schemas.get(type); if (!schema) { @@ -257,6 +277,15 @@ if (ENABLE_LEGACY_SCHEMA_SERVICE) { }; SchemaService.prototype.doesTypeExist = function (type: string): boolean { + deprecate(`Use \`schema.hasResource({ type })\` instead of \`schema.doesTypeExist(type)\``, false, { + id: 'ember-data:schema-service-updates', + until: '5.0', + for: 'ember-data', + since: { + available: '5.4', + enabled: '5.4', + }, + }); return this._schemas.has(type); }; } diff --git a/packages/schema-record/vite.config.mjs b/packages/schema-record/vite.config.mjs index 1fe014056bc..c7dc6c62ac0 100644 --- a/packages/schema-record/vite.config.mjs +++ b/packages/schema-record/vite.config.mjs @@ -1,6 +1,6 @@ import { createConfig } from '@warp-drive/internal-config/vite/config.js'; -export const externals = []; +export const externals = ['@ember/debug']; export const entryPoints = ['src/hooks.ts', 'src/record.ts', 'src/schema.ts']; diff --git a/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts b/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts index 318cff585d7..d431a9799a0 100644 --- a/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts +++ b/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts @@ -6,6 +6,7 @@ import { setupTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; import type { Snapshot } from '@ember-data/legacy-compat/-private'; import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { DEBUG } from '@warp-drive/build-config/env'; import type { Cache } from '@warp-drive/core-types/cache'; import type { StableRecordIdentifier } from '@warp-drive/core-types/identifier'; @@ -127,7 +128,9 @@ module('unit/model - Custom Class Model', function (hooks: NestedHooks) { createRecord: (store, type, snapshot: Snapshot) => { let count = 0; assert.verifySteps( - ['TestSchema:fields', 'TestSchema:fields', 'TestSchema:hasResource', 'TestSchema:hasResource'], + DEBUG + ? ['TestSchema:fields', 'TestSchema:fields', 'TestSchema:hasResource', 'TestSchema:hasResource'] + : ['TestSchema:fields', 'TestSchema:fields'], 'serialization of record for save' ); assert.step('Adapter:createRecord'); From 44bb2da38937a3684c8d7b3ba74e3dc090b74104 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 22 May 2024 01:46:35 -0700 Subject: [PATCH 3/4] delegating schema provider --- packages/model/src/migration-support.ts | 100 +++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/packages/model/src/migration-support.ts b/packages/model/src/migration-support.ts index 70873c70fbf..b3adbac1c0d 100644 --- a/packages/model/src/migration-support.ts +++ b/packages/model/src/migration-support.ts @@ -1,9 +1,13 @@ +import type Store from '@ember-data/store'; import { recordIdentifierFor } from '@ember-data/store'; import type { SchemaService } from '@ember-data/store/types'; +import { ENABLE_LEGACY_SCHEMA_SERVICE } from '@warp-drive/build-config/deprecations'; import { assert } from '@warp-drive/build-config/macros'; +import type { StableRecordIdentifier } from '@warp-drive/core-types'; import { getOrSetGlobal } from '@warp-drive/core-types/-private'; import type { ObjectValue } from '@warp-drive/core-types/json/raw'; -import type { ResourceSchema } from '@warp-drive/core-types/schema/fields'; +import type { Derivation, HashFn, Transformation } from '@warp-drive/core-types/schema/concepts'; +import type { FieldSchema, ResourceSchema } from '@warp-drive/core-types/schema/fields'; import { Type } from '@warp-drive/core-types/symbols'; import type { WithPartial } from '@warp-drive/core-types/utils'; @@ -23,6 +27,10 @@ import { unloadRecord, } from './-private/model-methods'; import RecordState from './-private/record-state'; +import { buildSchema } from './hooks'; + +type AttributesSchema = ReturnType>; +type RelationshipsSchema = ReturnType>; // 'isDestroying', 'isDestroyed' const LegacyFields = [ @@ -160,3 +168,93 @@ export function withDefaults(schema: WithPartial { + if (this._preferred.hasResource(resource)) { + return this._preferred.fields(resource); + } + return this._secondary.fields(resource); + } + transformation(name: string): Transformation { + return this._preferred.transformation(name); + } + hashFn(name: string): HashFn { + return this._preferred.hashFn(name); + } + derivation(name: string): Derivation { + return this._preferred.derivation(name); + } + resource(resource: StableRecordIdentifier | { type: string }): ResourceSchema { + if (this._preferred.hasResource(resource)) { + return this._preferred.resource(resource); + } + return this._secondary.resource(resource); + } + registerResources(schemas: ResourceSchema[]): void { + this._preferred.registerResources(schemas); + } + registerResource(schema: ResourceSchema): void { + this._preferred.registerResource(schema); + } + registerTransformation(transform: Transformation): void { + this._preferred.registerTransformation(transform); + } + registerDerivation(derivation: Derivation): void { + this._preferred.registerDerivation(derivation); + } + registerHashFn(hashFn: HashFn): void { + this._preferred.registerHashFn(hashFn); + } +} + +if (ENABLE_LEGACY_SCHEMA_SERVICE) { + DelegatingSchemaService.prototype.attributesDefinitionFor = function ( + resource: StableRecordIdentifier | { type: string } + ) { + if (this._preferred.hasResource(resource)) { + return this._preferred.attributesDefinitionFor!(resource); + } + + return this._secondary.attributesDefinitionFor!(resource); + }; + DelegatingSchemaService.prototype.relationshipsDefinitionFor = function ( + resource: StableRecordIdentifier | { type: string } + ) { + if (this._preferred.hasResource(resource)) { + return this._preferred.relationshipsDefinitionFor!(resource); + } + + return this._secondary.relationshipsDefinitionFor!(resource); + }; + DelegatingSchemaService.prototype.doesTypeExist = function (type: string) { + return this._preferred.doesTypeExist?.(type) || this._secondary.doesTypeExist?.(type) || false; + }; +} From 6c97308b1f7bb37f541e847bcee276e9445b2d93 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 22 May 2024 01:49:03 -0700 Subject: [PATCH 4/4] cleanup' --- packages/model/src/-private/schema-provider.ts | 2 +- .../custom-class-model-test.ts | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/model/src/-private/schema-provider.ts b/packages/model/src/-private/schema-provider.ts index 381e6c567df..7b012b1db02 100644 --- a/packages/model/src/-private/schema-provider.ts +++ b/packages/model/src/-private/schema-provider.ts @@ -1,4 +1,5 @@ import { getOwner } from '@ember/application'; +import { deprecate } from '@ember/debug'; import type Store from '@ember-data/store'; import type { SchemaService } from '@ember-data/store/types'; @@ -17,7 +18,6 @@ import type { import type { FactoryCache, Model, ModelFactory, ModelStore } from './model'; import _modelForMixin from './model-for-mixin'; import { normalizeModelName } from './util'; -import { deprecate } from '@ember/debug'; type AttributesSchema = ReturnType>; type RelationshipsSchema = ReturnType>; diff --git a/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts b/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts index d431a9799a0..d83f92b9b3e 100644 --- a/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts +++ b/tests/main/tests/unit/custom-class-support/custom-class-model-test.ts @@ -247,14 +247,16 @@ module('unit/model - Custom Class Model', function (hooks: NestedHooks) { assert.verifySteps(['TestSchema:fields', 'TestSchema:fields'], 'population of record on create'); await person.save(); assert.verifySteps( - [ - 'TestSchema:hasResource', - 'TestSchema:hasResource', - 'TestSchema:hasResource', - 'TestSchema:fields', - 'TestSchema:fields', - 'TestSchema:fields', - ], + DEBUG + ? [ + 'TestSchema:hasResource', + 'TestSchema:hasResource', + 'TestSchema:hasResource', + 'TestSchema:fields', + 'TestSchema:fields', + 'TestSchema:fields', + ] + : ['TestSchema:hasResource', 'TestSchema:fields', 'TestSchema:fields', 'TestSchema:fields'], 'update of record on save completion' ); });