diff --git a/packages/schema-record/src/-private/compute.ts b/packages/schema-record/src/-private/compute.ts index a467cdc6285..eafcba98520 100644 --- a/packages/schema-record/src/-private/compute.ts +++ b/packages/schema-record/src/-private/compute.ts @@ -16,6 +16,7 @@ import type { LocalField, ObjectField, SchemaArrayField, + SchemaObjectField, } from '@warp-drive/core-types/schema/fields'; import type { Link, Links } from '@warp-drive/core-types/spec/json-api-raw'; import { RecordStore } from '@warp-drive/core-types/symbols'; @@ -120,8 +121,9 @@ export function computeObject( cache: Cache, record: SchemaRecord, identifier: StableRecordIdentifier, - field: ObjectField, - prop: string + field: ObjectField | SchemaObjectField, + prop: string, + isSchemaObject = false ) { const managedObjectMapForRecord = ManagedObjectMap.get(record); let managedObject; @@ -141,7 +143,7 @@ export function computeObject( rawValue = transform.hydrate(rawValue as ObjectValue, field.options ?? null, record) as object; } } - managedObject = new ManagedObject(store, schema, cache, field, rawValue, identifier, prop, record); + managedObject = new ManagedObject(store, schema, cache, field, rawValue, identifier, prop, record, isSchemaObject); if (!managedObjectMapForRecord) { ManagedObjectMap.set(record, new Map([[field, managedObject]])); } else { diff --git a/packages/schema-record/src/-private/managed-object.ts b/packages/schema-record/src/-private/managed-object.ts index 6bfa618f800..551b83b1fa7 100644 --- a/packages/schema-record/src/-private/managed-object.ts +++ b/packages/schema-record/src/-private/managed-object.ts @@ -4,7 +4,7 @@ import { addToTransaction, createSignal, subscribe } from '@ember-data/tracking/ 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'; -import type { ObjectField } from '@warp-drive/core-types/schema/fields'; +import type { ObjectField, SchemaObjectField } from '@warp-drive/core-types/schema/fields'; import type { SchemaRecord } from '../record'; import type { SchemaService } from '../schema'; @@ -15,7 +15,7 @@ export function notifyObject(obj: ManagedObject) { } type KeyType = string | symbol | number; - +const ignoredGlobalFields = new Set(['constructor', 'setInterval', 'nodeType', 'length']); export interface ManagedObject { [MUTATE]?( target: unknown[], @@ -37,11 +37,12 @@ export class ManagedObject { store: Store, schema: SchemaService, cache: Cache, - field: ObjectField, + field: ObjectField | SchemaObjectField, data: object, address: StableRecordIdentifier, key: string, - owner: SchemaRecord + owner: SchemaRecord, + isSchemaObject: boolean ) { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; @@ -68,13 +69,27 @@ export class ManagedObject { if (prop === 'owner') { return self.owner; } + if (prop === Symbol.toStringTag) { + return `ManagedObject<${address.type}:${address.id} (${address.lid})>`; + } + if (prop === 'toString') { + return function () { + return `ManagedObject<${address.type}:${address.id} (${address.lid})>`; + }; + } + + if (prop === 'toHTML') { + return function () { + return '
ManagedObject
'; + }; + } if (_SIGNAL.shouldReset) { _SIGNAL.t = false; _SIGNAL.shouldReset = false; let newData = cache.getAttr(self.address, self.key); if (newData && newData !== self[SOURCE]) { - if (field.type) { + if (!isSchemaObject && field.type) { const transform = schema.transformation(field); newData = transform.hydrate(newData as ObjectValue, field.options ?? null, self.owner) as ObjectValue; } @@ -82,6 +97,14 @@ export class ManagedObject { } } + if (isSchemaObject) { + const fields = schema.fields({ type: field.type! }); + // TODO: is there a better way to do this? + if (typeof prop === 'string' && !ignoredGlobalFields.has(prop) && !fields.has(prop)) { + throw new Error(`Field ${prop} does not exist on schema object ${field.type}`); + } + } + if (prop in self[SOURCE]) { if (!transaction) { subscribe(_SIGNAL); @@ -108,10 +131,16 @@ export class ManagedObject { self.owner = value; return true; } + if (isSchemaObject) { + const fields = schema.fields({ type: field.type! }); + if (typeof prop === 'string' && !ignoredGlobalFields.has(prop) && !fields.has(prop)) { + throw new Error(`Field ${prop} does not exist on schema object ${field.type}`); + } + } const reflect = Reflect.set(target, prop, value, receiver); if (reflect) { - if (!field.type) { + if (isSchemaObject || !field.type) { cache.setAttr(self.address, self.key, self[SOURCE] as Value); _SIGNAL.shouldReset = true; return true; diff --git a/packages/schema-record/src/record.ts b/packages/schema-record/src/record.ts index f3f286487dd..c4e5a63be31 100644 --- a/packages/schema-record/src/record.ts +++ b/packages/schema-record/src/record.ts @@ -243,7 +243,8 @@ export class SchemaRecord { case 'schema-object': // validate any access off of schema, no transform to run // use raw cache value as the object to manage - throw new Error(`Not Implemented`); + entangleSignal(signals, receiver, field.name); + return computeObject(store, schema, cache, target, identifier, field, prop as string, true); case 'object': assert( `SchemaRecord.${field.name} is not available in legacy mode because it has type '${field.kind}'`, @@ -387,6 +388,7 @@ export class SchemaRecord { } return true; } + const transform = schema.transformation(field); const rawValue = transform.serialize({ ...(value as ObjectValue) }, field.options ?? null, target); @@ -398,6 +400,27 @@ export class SchemaRecord { } return true; } + case 'schema-object': { + let newValue = value; + if (value !== null) { + newValue = { ...(value as ObjectValue) }; + const schemaFields = schema.fields({ type: field.type }); + for (const key of Object.keys(newValue as ObjectValue)) { + if (!schemaFields.has(key)) { + throw new Error(`Field ${key} does not exist on schema object ${field.type}`); + } + } + } else { + ManagedObjectMap.delete(target); + } + cache.setAttr(identifier, propArray, newValue as Value); + const peeked = peekManagedObject(self, field); + if (peeked) { + const objSignal = peeked[OBJECT_SIGNAL]; + objSignal.shouldReset = true; + } + return true; + } case 'derived': { throw new Error(`Cannot set ${String(prop)} on ${identifier.type} because it is derived`); } @@ -488,6 +511,14 @@ export class SchemaRecord { addToTransaction(arrSignal); } } + if (field?.kind === 'object' || field?.kind === 'schema-object') { + const peeked = peekManagedObject(self, field); + if (peeked) { + const objSignal = peeked[OBJECT_SIGNAL]; + objSignal.shouldReset = true; + addToTransaction(objSignal); + } + } } } break; 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 7bd964939f6..b6bb95b5252 100644 --- a/tests/warp-drive__schema-record/tests/-utils/reactive-context.ts +++ b/tests/warp-drive__schema-record/tests/-utils/reactive-context.ts @@ -53,6 +53,7 @@ export async function reactiveContext( field.kind === 'array' || field.kind === 'object' || field.kind === 'schema-array' || + field.kind === 'schema-object' || field.kind === '@id' || // @ts-expect-error we secretly allow this field.kind === '@hash' diff --git a/tests/warp-drive__schema-record/tests/reactivity/object-test.ts b/tests/warp-drive__schema-record/tests/reactivity/object-test.ts new file mode 100644 index 00000000000..76bcfa6d724 --- /dev/null +++ b/tests/warp-drive__schema-record/tests/reactivity/object-test.ts @@ -0,0 +1,121 @@ +import { rerender } from '@ember/test-helpers'; + +import { module, test } from 'qunit'; + +import { setupRenderingTest } from 'ember-qunit'; + +import type Store from '@ember-data/store'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; + +import { reactiveContext } from '../-utils/reactive-context'; + +interface Address { + street: string; + city: string; + state: string; + zip: string; +} +interface User { + id: string | null; + $type: 'user'; + name: string; + favoriteNumbers: string[]; + address: Address; + age: number; + netWorth: number; + coolometer: number; + rank: number; +} + +module('Reactivity | object fields can receive remote updates', function (hooks) { + setupRenderingTest(hooks); + + test('we can use simple fields with no `type`', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const { schema } = store; + registerDerivations(schema); + + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'address', + kind: 'object', + }, + ], + }) + ); + const resource = schema.resource({ type: 'user' }); + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + address: { + street: '123 Main St', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + }, + }, + }) as User; + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + assert.deepEqual( + record.address, + { + street: '123 Main St', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + 'address is accessible' + ); + + const { counters } = await reactiveContext.call(this, record, resource); + // TODO: actually render the address object and verify + // const addressIndex = fieldOrder.indexOf('address'); + + assert.strictEqual(counters.id, 1, 'idCount is 1'); + assert.strictEqual(counters.$type, 1, '$typeCount is 1'); + assert.strictEqual(counters.address, 1, 'addressCount is 1'); + + // remote update + store.push({ + data: { + type: 'user', + id: '1', + attributes: { + address: { + street: '456 Elm St', + city: 'Anytown', + state: 'NJ', + zip: '23456', + }, + }, + }, + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + assert.deepEqual( + record.address, + { + street: '456 Elm St', + city: 'Anytown', + state: 'NJ', + zip: '23456', + }, + 'address is accessible' + ); + + await rerender(); + + assert.strictEqual(counters.id, 1, 'idCount is 1'); + assert.strictEqual(counters.$type, 1, '$typeCount is 1'); + assert.strictEqual(counters.address, 2, 'addressCount is 2'); + }); +}); diff --git a/tests/warp-drive__schema-record/tests/reactivity/schema-object-test.ts b/tests/warp-drive__schema-record/tests/reactivity/schema-object-test.ts new file mode 100644 index 00000000000..a3c4a3c6d95 --- /dev/null +++ b/tests/warp-drive__schema-record/tests/reactivity/schema-object-test.ts @@ -0,0 +1,144 @@ +import { rerender } from '@ember/test-helpers'; + +import { module, test } from 'qunit'; + +import { setupRenderingTest } from 'ember-qunit'; + +import type Store from '@ember-data/store'; +import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; + +import { reactiveContext } from '../-utils/reactive-context'; + +interface Address { + street: string; + city: string; + state: string; + zip: string; +} +interface User { + id: string | null; + $type: 'user'; + name: string; + favoriteNumbers: string[]; + address: Address; + age: number; + netWorth: number; + coolometer: number; + rank: number; +} + +module('Reactivity | object fields can receive remote updates', function (hooks) { + setupRenderingTest(hooks); + + test('we can use simple fields with no `type`', async function (assert) { + const store = this.owner.lookup('service:store') as Store; + const { schema } = store; + registerDerivations(schema); + + schema.registerResource({ + identity: null, + type: 'address', + fields: [ + { + name: 'street', + kind: 'field', + }, + { + name: 'city', + kind: 'field', + }, + { + name: 'state', + kind: 'field', + }, + { + name: 'zip', + kind: 'field', + }, + ], + }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'address', + kind: 'schema-object', + type: 'address', + }, + ], + }) + ); + const resource = schema.resource({ type: 'user' }); + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + address: { + street: '123 Main St', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + }, + }, + }) as User; + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + assert.deepEqual( + record.address, + { + street: '123 Main St', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + 'address is accessible' + ); + + const { counters } = await reactiveContext.call(this, record, resource); + // TODO: actually render the address object and verify + // const addressIndex = fieldOrder.indexOf('address'); + + assert.strictEqual(counters.id, 1, 'idCount is 1'); + assert.strictEqual(counters.$type, 1, '$typeCount is 1'); + assert.strictEqual(counters.address, 1, 'addressCount is 1'); + + // remote update + store.push({ + data: { + type: 'user', + id: '1', + attributes: { + address: { + street: '456 Elm St', + city: 'Anytown', + state: 'NJ', + zip: '23456', + }, + }, + }, + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + assert.deepEqual( + record.address, + { + street: '456 Elm St', + city: 'Anytown', + state: 'NJ', + zip: '23456', + }, + 'address is accessible' + ); + + await rerender(); + + assert.strictEqual(counters.id, 1, 'idCount is 1'); + assert.strictEqual(counters.$type, 1, '$typeCount is 1'); + assert.strictEqual(counters.address, 2, 'addressCount is 2'); + }); +}); diff --git a/tests/warp-drive__schema-record/tests/reads/schema-object-test.ts b/tests/warp-drive__schema-record/tests/reads/schema-object-test.ts new file mode 100644 index 00000000000..26f7fcac745 --- /dev/null +++ b/tests/warp-drive__schema-record/tests/reads/schema-object-test.ts @@ -0,0 +1,114 @@ +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 { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; + +import type Store from 'warp-drive__schema-record/services/store'; + +type address = { + street: string; + city: string; + state: string; + zip: string | number; +}; + +interface CreateUserType { + id: string | null; + $type: 'user'; + name: string | null; + address: address | null; + [ResourceType]: 'user'; +} + +module('Reads | schema-object fields', function (hooks) { + setupTest(hooks); + + test('we can use schema-object fields', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const { schema } = store; + registerDerivations(schema); + + schema.registerResource({ + identity: null, + type: 'address', + fields: [ + { + name: 'street', + kind: 'field', + }, + { + name: 'city', + kind: 'field', + }, + { + name: 'state', + kind: 'field', + }, + { + name: 'zip', + kind: 'field', + }, + ], + }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'address', + type: 'address', + kind: 'schema-object', + }, + ], + }) + ); + + const sourceAddress: address = { + street: '123 Main St', + city: 'Anytown', + state: 'NY', + zip: '12345', + }; + const record = store.createRecord('user', { name: 'Rey Skybarker', address: sourceAddress }); + + 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'); + assert.deepEqual( + record.address, + { street: '123 Main St', city: 'Anytown', state: 'NY', zip: '12345' }, + 'we can access address object' + ); + assert.strictEqual(record.address, record.address, 'We have a stable object reference'); + assert.notStrictEqual(record.address, sourceAddress); + + // test that the data entered the cache properly + const identifier = recordIdentifierFor(record); + const cachedResourceData = store.cache.peek(identifier); + + assert.notStrictEqual( + cachedResourceData?.attributes?.address, + sourceAddress, + 'with no transform we will still divorce the object reference' + ); + assert.deepEqual( + cachedResourceData?.attributes?.address, + { + street: '123 Main St', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + 'the cache values are correct for the array field' + ); + // @ts-expect-error + assert.throws(() => record.address!.notField as unknown, /Field notField does not exist on schema object address/); + }); +}); diff --git a/tests/warp-drive__schema-record/tests/writes/schema-object-test.ts b/tests/warp-drive__schema-record/tests/writes/schema-object-test.ts new file mode 100644 index 00000000000..9bacc1d1045 --- /dev/null +++ b/tests/warp-drive__schema-record/tests/writes/schema-object-test.ts @@ -0,0 +1,492 @@ +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 { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; + +import type Store from 'warp-drive__schema-record/services/store'; + +type address = { + street: string; + city: string; + state: string; + zip: string | number; +}; +interface User { + id: string; + $type: 'user'; + name: string; + address: address | null; + [ResourceType]: 'user'; +} + +module('Writes | schema-object fields', function (hooks) { + setupTest(hooks); + + test('we can update to a new object', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const { schema } = store; + registerDerivations(schema); + + schema.registerResource({ + identity: null, + type: 'address', + fields: [ + { + name: 'street', + kind: 'field', + }, + { + name: 'city', + kind: 'field', + }, + { + name: 'state', + kind: 'field', + }, + { + name: 'zip', + kind: 'field', + }, + ], + }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'address', + kind: 'schema-object', + type: 'address', + }, + ], + }) + ); + + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Rey Pupatine', + address: { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + }, + }, + }); + + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + assert.strictEqual(record.name, 'Rey Pupatine', 'name is accessible'); + assert.deepEqual( + record.address, + { street: '123 Main Street', city: 'Anytown', state: 'NY', zip: '12345' }, + 'We have the correct address object' + ); + const address = record.address; + record.address = { street: '456 Elm Street', city: 'Sometown', state: 'NJ', zip: '23456' }; + assert.deepEqual( + record.address, + { street: '456 Elm Street', city: 'Sometown', state: 'NJ', zip: '23456' }, + 'we have the correct Object members' + ); + assert.strictEqual(address, record.address, 'Object reference does not change'); + + // test that the data entered the cache properly + const identifier = recordIdentifierFor(record); + const cachedResourceData = store.cache.peek(identifier); + + assert.deepEqual( + cachedResourceData?.attributes?.address, + { street: '456 Elm Street', city: 'Sometown', state: 'NJ', zip: '23456' }, + 'the cache values are correctly updated' + ); + }); + + test('we can update to null', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const { schema } = store; + registerDerivations(schema); + schema.registerResource({ + identity: null, + type: 'address', + fields: [ + { + name: 'street', + kind: 'field', + }, + { + name: 'city', + kind: 'field', + }, + { + name: 'state', + kind: 'field', + }, + { + name: 'zip', + kind: 'field', + }, + ], + }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'address', + kind: 'schema-object', + type: 'address', + }, + ], + }) + ); + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Rey Pupatine', + address: { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + }, + }, + }); + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + assert.strictEqual(record.name, 'Rey Pupatine', 'name is accessible'); + assert.deepEqual( + record.address, + { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + 'We have the correct address object' + ); + record.address = null; + assert.strictEqual(record.address, null, 'The object is correctly set to null'); + // test that the data entered the cache properly + const identifier = recordIdentifierFor(record); + const cachedResourceData = store.cache.peek(identifier); + assert.deepEqual(cachedResourceData?.attributes?.address, null, 'the cache values are correctly updated'); + record.address = { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '12345', + }; + assert.deepEqual( + record.address, + { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + 'We have the correct address object' + ); + }); + + test('we can update a single value in the object', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const { schema } = store; + registerDerivations(schema); + schema.registerResource({ + identity: null, + type: 'address', + fields: [ + { + name: 'street', + kind: 'field', + }, + { + name: 'city', + kind: 'field', + }, + { + name: 'state', + kind: 'field', + }, + { + name: 'zip', + kind: 'field', + }, + ], + }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'address', + kind: 'schema-object', + type: 'address', + }, + ], + }) + ); + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Rey Pupatine', + address: { street: '123 Main Street', city: 'Anytown', state: 'NY', zip: '12345' }, + }, + }, + }); + assert.deepEqual( + record.address, + { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + 'We have the correct address object' + ); + const address = record.address; + record.address!.state = 'NJ'; + assert.deepEqual( + record.address, + { + street: '123 Main Street', + city: 'Anytown', + state: 'NJ', + zip: '12345', + }, + 'We have the correct address object' + ); + assert.strictEqual(address, record.address, 'Object reference does not change'); + // test that the data entered the cache properly + const identifier = recordIdentifierFor(record); + const cachedResourceData = store.cache.peek(identifier); + assert.deepEqual( + cachedResourceData?.attributes?.address, + { street: '123 Main Street', city: 'Anytown', state: 'NJ', zip: '12345' }, + 'the cache values are correctly updated' + ); + }); + + test('we can assign an object value to another record', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const { schema } = store; + registerDerivations(schema); + schema.registerResource({ + identity: null, + type: 'address', + fields: [ + { + name: 'street', + kind: 'field', + }, + { + name: 'city', + kind: 'field', + }, + { + name: 'state', + kind: 'field', + }, + { + name: 'zip', + kind: 'field', + }, + ], + }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'address', + kind: 'schema-object', + type: 'address', + }, + ], + }) + ); + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Rey Pupatine', + address: { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + }, + }, + }); + const record2 = store.push({ + data: { + type: 'user', + id: '2', + attributes: { name: 'Luke Skybarker' }, + }, + }); + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + assert.strictEqual(record.name, 'Rey Pupatine', 'name is accessible'); + assert.strictEqual(record2.id, '2', 'id is accessible'); + assert.strictEqual(record2.$type, 'user', '$type is accessible'); + assert.strictEqual(record2.name, 'Luke Skybarker', 'name is accessible'); + assert.deepEqual( + record.address, + { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + 'We have the correct address object' + ); + assert.strictEqual(record.address, record.address, 'We have a stable object reference'); + const address = record.address; + record2.address = record.address; + assert.deepEqual( + record2.address, + { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + 'We have the correct address object' + ); + + assert.strictEqual(address, record.address, 'Object reference does not change'); + assert.notStrictEqual(address, record2.address, 'We have a new object reference'); + // test that the data entered the cache properly + const identifier = recordIdentifierFor(record2); + const cachedResourceData = store.cache.peek(identifier); + assert.deepEqual( + cachedResourceData?.attributes?.address, + { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + 'the cache values are correctly updated' + ); + }); + + test('throws errors when trying to set non-schema fields', function (assert) { + const store = this.owner.lookup('service:store') as Store; + const { schema } = store; + registerDerivations(schema); + schema.registerResource({ + identity: null, + type: 'address', + fields: [ + { + name: 'street', + kind: 'field', + }, + { + name: 'city', + kind: 'field', + }, + { + name: 'state', + kind: 'field', + }, + { + name: 'zip', + kind: 'field', + }, + ], + }); + schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { + name: 'name', + kind: 'field', + }, + { + name: 'address', + kind: 'schema-object', + type: 'address', + }, + ], + }) + ); + const record = store.push({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Rey Pupatine', + address: { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + }, + }, + }); + assert.strictEqual(record.id, '1', 'id is accessible'); + assert.strictEqual(record.$type, 'user', '$type is accessible'); + assert.strictEqual(record.name, 'Rey Pupatine', 'name is accessible'); + assert.deepEqual( + record.address, + { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '12345', + }, + 'We have the correct address object' + ); + assert.strictEqual(record.address, record.address, 'We have a stable object reference'); + assert.throws(() => { + //@ts-expect-error + record.address!.notAField = 'This should throw'; + }, /Field notAField does not exist on schema object address/); + assert.throws(() => { + record.address = { + street: '456 Elm Street', + city: 'Sometown', + state: 'NJ', + zip: '23456', + //@ts-expect-error + notAField: 'This should throw', + }; + }, /Field notAField does not exist on schema object address/); + }); +});