diff --git a/.eslintrc.js b/.eslintrc.js index 7b88e098c51..94842dece0e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -215,7 +215,6 @@ module.exports = { 'ember-data-types/q/promise-proxies.ts', 'ember-data-types/q/minimum-serializer-interface.ts', 'ember-data-types/q/minimum-adapter-interface.ts', - 'ember-data-types/q/identifier.ts', 'ember-data-types/q/fetch-manager.ts', 'ember-data-types/q/ember-data-json-api.ts', '@types/@ember/polyfills/index.d.ts', diff --git a/packages/schema-record/src/index.ts b/packages/schema-record/src/index.ts index fc319b5baed..d007d5d1c56 100644 --- a/packages/schema-record/src/index.ts +++ b/packages/schema-record/src/index.ts @@ -1,6 +1,138 @@ -type Store = { peekRecord(identifier: StableRecordIdentifier): SchemaModel | unknown | null }; -type StableRecordIdentifier = { lid: string }; +// import type Store from '@ember-data/store'; +type Store = { schema: SchemaService, cache: Cache }; +import type { StableRecordIdentifier } from "@ember-data/types/q/identifier"; +import type { Cache } from "@ember-data/types/cache/cache"; -export default class SchemaModel { - constructor(store: Store, identifier: StableRecordIdentifier) {} +export const Destroy = Symbol('Destroy'); +export const RecordStore = Symbol('Store'); +export const Identifier = Symbol('Identifier'); + +export interface FieldSchema { + type: string; + name: string; + kind: 'attribute' | 'resource' | 'collection' | 'derived' | 'object' | 'array'; + options?: Record; +} + +type FieldSpec = { + // legacy support + attributes: Record; + relationships: Record; + // new support + fields: Map; +} + +export class SchemaService { + declare schemas: Map; + + constructor() { + this.schemas = new Map(); + } + + defineSchema(name: string, fields: FieldSchema[]): void { + const fieldSpec: FieldSpec = { + attributes: {}, + relationships: {}, + fields: new Map(), + }; + + fields.forEach((field) => { + fieldSpec.fields.set(field.name, field); + + if (field.kind === 'attribute') { + fieldSpec.attributes[field.name] = field; + } else if (field.kind === 'resource' || field.kind === 'collection') { + fieldSpec.relationships[field.name] = Object.assign({}, field, { + kind: field.kind === 'resource' ? 'belongsTo' : 'hasMany', + }); + } else { + throw new Error(`Unknown field kind ${field.kind}`); + } + }); + + this.schemas.set(name, fieldSpec); + } + + fields({ type }: { type: string }): FieldSpec['fields'] { + const schema = this.schemas.get(type); + + if (!schema) { + throw new Error(`No schema defined for ${type}`); + } + + return schema.fields; + } + + attributesDefinitionFor({ type }: { type: string }): FieldSpec['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); + + if (!schema) { + throw new Error(`No schema defined for ${type}`); + } + + return schema.relationships; + } + + doesTypeExist(type: string): boolean { + return this.schemas.has(type); + } +} + +export default class SchemaRecord { + declare [RecordStore]: Store; + declare [Identifier]: StableRecordIdentifier; + + constructor(store: Store, identifier: StableRecordIdentifier) { + this[RecordStore] = store; + this[Identifier] = identifier; + + const schema = store.schema; + const cache = store.cache; + const fields = schema.fields(identifier); + + return new Proxy(this, { + get(target, prop) { + if (prop === Destroy) { + return target[Destroy]; + } + + if (prop === 'id') { + return identifier.id; + } + if (prop === '$type') { + return identifier.type; + } + const field = fields.get(prop as string); + if (!field) { + throw new Error(`No field named ${String(prop)} on ${identifier.type}`); + } + + if (field.kind === 'attribute') { + return cache.getAttr(identifier, prop as string); + } + + throw new Error(`Unknown field kind ${field.kind}`); + }, + }); + } + + [Destroy](): void {} +} + +export function instantiateRecord(store: Store, identifier: StableRecordIdentifier): SchemaRecord { + return new SchemaRecord(store, identifier); +} + +export function teardownRecord(record: SchemaRecord): void { + record[Destroy](); } diff --git a/packages/schema-record/tsconfig.json b/packages/schema-record/tsconfig.json index 2ae51769cd7..d20a894229a 100644 --- a/packages/schema-record/tsconfig.json +++ b/packages/schema-record/tsconfig.json @@ -45,6 +45,9 @@ "paths": { "@ember-data/env": ["../../private-build-infra/virtual-packages/env.d.ts"], + "@ember-data/store": ["../../store/src"], + "@ember-data/types": ["../../../ember-data-types"], + "@ember-data/types/*": ["../../../ember-data-types/*"], } } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5e28479d26..0f6ac5f20cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3424,9 +3424,24 @@ importers: '@babel/runtime': specifier: ^7.22.15 version: 7.23.1 + '@ember-data/graph': + specifier: workspace:5.5.0-alpha.9 + version: file:packages/graph(@babel/core@7.23.0)(@ember-data/store@5.5.0-alpha.9) + '@ember-data/json-api': + specifier: workspace:5.5.0-alpha.9 + version: file:packages/json-api(@babel/core@7.23.0)(@ember-data/graph@5.5.0-alpha.9)(@ember-data/store@5.5.0-alpha.9)(ember-inflector@4.0.2) '@ember-data/private-build-infra': specifier: workspace:5.5.0-alpha.9 version: link:../../packages/private-build-infra + '@ember-data/request': + specifier: workspace:5.5.0-alpha.9 + version: file:packages/request(@babel/core@7.23.0) + '@ember-data/store': + specifier: workspace:5.5.0-alpha.9 + version: file:packages/store(@babel/core@7.23.0)(@ember-data/tracking@5.5.0-alpha.9)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.3.0) + '@ember-data/tracking': + specifier: workspace:5.5.0-alpha.9 + version: file:packages/tracking(@babel/core@7.23.0) '@ember-data/unpublished-test-infra': specifier: workspace:5.5.0-alpha.9 version: link:../../packages/unpublished-test-infra @@ -3518,6 +3533,16 @@ importers: specifier: ^5.88.2 version: 5.88.2 dependenciesMeta: + '@ember-data/graph': + injected: true + '@ember-data/json-api': + injected: true + '@ember-data/request': + injected: true + '@ember-data/store': + injected: true + '@ember-data/tracking': + injected: true '@warp-drive/schema-record': injected: true @@ -11012,6 +11037,19 @@ packages: - '@babel/core' - supports-color + /ember-cache-primitive-polyfill@1.0.1(@babel/core@7.23.0): + resolution: {integrity: sha512-hSPcvIKarA8wad2/b6jDd/eU+OtKmi6uP+iYQbzi5TQpjsqV6b4QdRqrLk7ClSRRKBAtdTuutx+m+X+WlEd2lw==} + engines: {node: 10.* || >= 12} + dependencies: + ember-cli-babel: 8.1.0(@babel/core@7.23.0) + ember-cli-version-checker: 5.1.2 + ember-compatibility-helpers: 1.2.6(patch_hash=5qtypxbsewxcs5l7lcldb5aqhq)(@babel/core@7.23.0) + silent-error: 1.1.1 + transitivePeerDependencies: + - '@babel/core' + - supports-color + dev: true + /ember-cached-decorator-polyfill@1.0.2(@babel/core@7.22.20)(ember-source@5.3.0): resolution: {integrity: sha512-hUX6OYTKltAPAu8vsVZK02BfMTV0OUXrPqvRahYPhgS7D0I6joLjlskd7mhqJMcaXLywqceIy8/s+x8bxF8bpQ==} engines: {node: 14.* || >= 16} @@ -11030,6 +11068,25 @@ packages: - '@glint/template' - supports-color + /ember-cached-decorator-polyfill@1.0.2(@babel/core@7.23.0)(ember-source@5.3.0): + resolution: {integrity: sha512-hUX6OYTKltAPAu8vsVZK02BfMTV0OUXrPqvRahYPhgS7D0I6joLjlskd7mhqJMcaXLywqceIy8/s+x8bxF8bpQ==} + engines: {node: 14.* || >= 16} + peerDependencies: + ember-source: '*' + dependencies: + '@embroider/macros': 1.13.1(@babel/core@7.23.0) + '@glimmer/tracking': 1.1.2 + babel-import-util: 1.4.1 + ember-cache-primitive-polyfill: 1.0.1(@babel/core@7.23.0) + ember-cli-babel: 8.1.0(@babel/core@7.23.0) + ember-cli-babel-plugin-helpers: 1.1.1 + ember-source: 5.3.0(@babel/core@7.23.0)(@glimmer/component@1.1.2)(webpack@5.88.2) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true + /ember-cli-babel-plugin-helpers@1.1.1: resolution: {integrity: sha512-sKvOiPNHr5F/60NLd7SFzMpYPte/nnGkq/tMIfXejfKHIhaiIkYFqX8Z9UFTKWLLn+V7NOaby6niNPZUdvKCRw==} engines: {node: 6.* || 8.* || >= 10.*} @@ -18954,6 +19011,25 @@ packages: - '@glint/template' - supports-color + file:packages/graph(@babel/core@7.23.0)(@ember-data/store@5.5.0-alpha.9): + resolution: {directory: packages/graph, type: directory} + id: file:packages/graph + name: '@ember-data/graph' + engines: {node: '>= 18.*'} + peerDependencies: + '@ember-data/store': workspace:5.5.0-alpha.9 + dependencies: + '@ember-data/private-build-infra': file:packages/private-build-infra + '@ember-data/store': file:packages/store(@babel/core@7.23.0)(@ember-data/tracking@5.5.0-alpha.9)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.3.0) + '@ember/edition-utils': 1.2.0 + '@embroider/macros': 1.13.1(@babel/core@7.23.0) + ember-cli-babel: 8.1.0(@babel/core@7.23.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true + file:packages/holodeck: resolution: {directory: packages/holodeck, type: directory} name: '@warp-drive/holodeck' @@ -19019,6 +19095,30 @@ packages: - supports-color dev: true + file:packages/json-api(@babel/core@7.23.0)(@ember-data/graph@5.5.0-alpha.9)(@ember-data/store@5.5.0-alpha.9)(ember-inflector@4.0.2): + resolution: {directory: packages/json-api, type: directory} + id: file:packages/json-api + name: '@ember-data/json-api' + engines: {node: '>= 18.*'} + peerDependencies: + '@ember-data/graph': workspace:5.5.0-alpha.9 + '@ember-data/request-utils': workspace:5.5.0-alpha.9 + '@ember-data/store': workspace:5.5.0-alpha.9 + ember-inflector: ^4.0.2 + dependencies: + '@ember-data/graph': file:packages/graph(@babel/core@7.23.0)(@ember-data/store@5.5.0-alpha.9) + '@ember-data/private-build-infra': file:packages/private-build-infra + '@ember-data/store': file:packages/store(@babel/core@7.23.0)(@ember-data/tracking@5.5.0-alpha.9)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.3.0) + '@ember/edition-utils': 1.2.0 + '@embroider/macros': 1.13.1(@babel/core@7.23.0) + ember-cli-babel: 8.1.0(@babel/core@7.23.0) + ember-inflector: 4.0.2(@babel/core@7.23.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true + file:packages/legacy-compat(@babel/core@7.22.20)(@ember-data/graph@5.5.0-alpha.9)(@ember-data/json-api@5.5.0-alpha.9)(@ember-data/request@5.5.0-alpha.9): resolution: {directory: packages/legacy-compat, type: directory} id: file:packages/legacy-compat @@ -19333,6 +19433,22 @@ packages: - '@glint/template' - supports-color + file:packages/request(@babel/core@7.23.0): + resolution: {directory: packages/request, type: directory} + id: file:packages/request + name: '@ember-data/request' + engines: {node: '>= 18.*'} + dependencies: + '@ember-data/private-build-infra': file:packages/private-build-infra + '@ember/test-waiters': 3.0.2(@babel/core@7.23.0) + '@embroider/macros': 1.13.1(@babel/core@7.23.0) + ember-cli-babel: 8.1.0(@babel/core@7.23.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true + file:packages/request-utils(@babel/core@7.22.20): resolution: {directory: packages/request-utils, type: directory} id: file:packages/request-utils @@ -19448,6 +19564,31 @@ packages: - supports-color dev: true + file:packages/store(@babel/core@7.23.0)(@ember-data/tracking@5.5.0-alpha.9)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.3.0): + resolution: {directory: packages/store, type: directory} + id: file:packages/store + name: '@ember-data/store' + engines: {node: '>= 18.*'} + peerDependencies: + '@ember-data/tracking': workspace:5.5.0-alpha.9 + '@ember/string': 3.1.1 + '@glimmer/tracking': ^1.1.2 + dependencies: + '@ember-data/private-build-infra': file:packages/private-build-infra + '@ember-data/tracking': file:packages/tracking(@babel/core@7.23.0) + '@ember/string': 3.1.1(@babel/core@7.23.0) + '@embroider/macros': 1.13.1(@babel/core@7.23.0) + '@glimmer/tracking': 1.1.2 + '@glimmer/validator': 0.84.3 + ember-cached-decorator-polyfill: 1.0.2(@babel/core@7.23.0)(ember-source@5.3.0) + ember-cli-babel: 8.1.0(@babel/core@7.23.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - ember-source + - supports-color + dev: true + file:packages/tracking(@babel/core@7.22.20): resolution: {directory: packages/tracking, type: directory} id: file:packages/tracking @@ -19462,6 +19603,21 @@ packages: - '@glint/template' - supports-color + file:packages/tracking(@babel/core@7.23.0): + resolution: {directory: packages/tracking, type: directory} + id: file:packages/tracking + name: '@ember-data/tracking' + engines: {node: '>= 18.*'} + dependencies: + '@ember-data/private-build-infra': file:packages/private-build-infra + '@embroider/macros': 1.13.1(@babel/core@7.23.0) + ember-cli-babel: 8.1.0(@babel/core@7.23.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - supports-color + dev: true + file:packages/unpublished-test-infra(@babel/core@7.22.20): resolution: {directory: packages/unpublished-test-infra, type: directory} id: file:packages/unpublished-test-infra diff --git a/tests/schema-record/app/models/user-setting.ts b/tests/schema-record/app/models/user-setting.ts deleted file mode 100644 index e8f3511d0a0..00000000000 --- a/tests/schema-record/app/models/user-setting.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Model, { attr } from '@ember-data/model'; - -export default class UserSetting extends Model { - @attr declare name: string; -} diff --git a/tests/schema-record/app/services/store.ts b/tests/schema-record/app/services/store.ts index 41ac00034c8..58814e46572 100644 --- a/tests/schema-record/app/services/store.ts +++ b/tests/schema-record/app/services/store.ts @@ -1,7 +1,7 @@ +import type SchemaRecord from '@warp-drive/schema-record'; +import { instantiateRecord, teardownRecord } from '@warp-drive/schema-record'; + import JSONAPICache from '@ember-data/json-api'; -import type Model from '@ember-data/model'; -import { instantiateRecord, teardownRecord } from '@ember-data/model'; -import { buildSchema, modelFor } from '@ember-data/model/hooks'; import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; import DataStore, { CacheHandler } from '@ember-data/store'; @@ -16,23 +16,17 @@ export default class Store extends DataStore { const manager = (this.requestManager = new RequestManager()); manager.use([Fetch]); manager.useCache(CacheHandler); - - this.registerSchema(buildSchema(this)); } createCache(capabilities: CacheCapabilitiesManager): Cache { return new JSONAPICache(capabilities); } - instantiateRecord(identifier: StableRecordIdentifier, createRecordArgs: { [key: string]: unknown }): unknown { - return instantiateRecord.call(this, identifier, createRecordArgs); - } - - teardownRecord(record: Model): void { - return teardownRecord.call(this, record); + instantiateRecord(identifier: StableRecordIdentifier): SchemaRecord { + return instantiateRecord(this, identifier); } - modelFor(type: string) { - return modelFor.call(this, type); + teardownRecord(record: SchemaRecord): void { + return teardownRecord(record); } } diff --git a/tests/schema-record/config/environment.js b/tests/schema-record/config/environment.js index 265be9f8b5e..7e28113b93b 100644 --- a/tests/schema-record/config/environment.js +++ b/tests/schema-record/config/environment.js @@ -2,7 +2,7 @@ module.exports = function (environment) { let ENV = { - modulePrefix: 'builders-test-app', + modulePrefix: 'schema-record-test-app', environment, rootURL: '/', locationType: 'history', diff --git a/tests/schema-record/package.json b/tests/schema-record/package.json index b5fbcc3ab63..c504164ed1f 100644 --- a/tests/schema-record/package.json +++ b/tests/schema-record/package.json @@ -22,11 +22,31 @@ "dependenciesMeta": { "@warp-drive/schema-record": { "injected": true + }, + "@ember-data/store": { + "injected": true + }, + "@ember-data/request": { + "injected": true + }, + "@ember-data/tracking": { + "injected": true + }, + "@ember-data/json-api": { + "injected": true + }, + "@ember-data/graph": { + "injected": true } }, "devDependencies": { "@babel/core": "^7.22.20", "@babel/runtime": "^7.22.15", + "@ember-data/store": "workspace:5.5.0-alpha.9", + "@ember-data/request": "workspace:5.5.0-alpha.9", + "@ember-data/tracking": "workspace:5.5.0-alpha.9", + "@ember-data/json-api": "workspace:5.5.0-alpha.9", + "@ember-data/graph": "workspace:5.5.0-alpha.9", "@warp-drive/schema-record": "workspace:5.5.0-alpha.9", "@ember-data/private-build-infra": "workspace:5.5.0-alpha.9", "@ember-data/unpublished-test-infra": "workspace:5.5.0-alpha.9", diff --git a/tests/schema-record/tests/integration/basic-fields-test.ts b/tests/schema-record/tests/integration/basic-fields-test.ts new file mode 100644 index 00000000000..67b4e300683 --- /dev/null +++ b/tests/schema-record/tests/integration/basic-fields-test.ts @@ -0,0 +1,49 @@ +import { SchemaService } from '@warp-drive/schema-record'; +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import type Store from '@ember-data/store'; + +interface User { + id: string | null; + $type: 'user'; + name: string; +} + +module('Integration | basic fields', function (hooks) { + setupTest(hooks); + + test('Simple Fields Work As Expected', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const schema = new SchemaService(); + store.registerSchema(schema); + + schema.defineSchema('user', [ + { + name: 'name', + type: 'string', + kind: 'attribute', + }, + ]); + + const record = store.createRecord('user', { name: 'Rey Skybarker' }) as User; + + assert.strictEqual(record.id, null, 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + + assert.strictEqual(record.name, 'Rey Skybarker', 'name is accessible'); + + try { + // @ts-expect-error intentionally accessing unknown field + record.lastName; + assert.ok(false, 'should error when accessing unknown field'); + } catch (e) { + assert.strictEqual( + (e as Error).message, + 'No field named lastName on user', + 'should error when accessing unknown field' + ); + } + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 5e5b6a2324d..60895b4e6cd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,7 +26,6 @@ "ember-data-types/q/promise-proxies.ts", "ember-data-types/q/minimum-serializer-interface.ts", "ember-data-types/q/minimum-adapter-interface.ts", - "ember-data-types/q/identifier.ts", "ember-data-types/q/fetch-manager.ts", "ember-data-types/q/ember-data-json-api.ts", "tests/graph/tests/integration/graph/polymorphism/implicit-keys-test.ts", diff --git a/tsconfig.root.json b/tsconfig.root.json index bf047b8f783..aa5fecd6292 100644 --- a/tsconfig.root.json +++ b/tsconfig.root.json @@ -53,6 +53,8 @@ "@warp-drive/holodeck/*": ["packages/holodeck/dist/*"], "@ember-data/request": ["packages/request/addon"], "@ember-data/request/*": ["packages/request/addon/*"], + "@warp-drive/schema-record": ["packages/schema-record/addon"], + "@warp-drive/schema-record/*": ["packages/schema-record/addon/*"], "@ember-data/request-utils": ["packages/request-utils/src"], "@ember-data/request-utils/*": ["packages/request-utils/src/*"], "@ember-data/rest": ["packages/rest/src"],