Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement schema-object for schema-record #9467

Merged
merged 1 commit into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/schema-record/src/-private/compute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
41 changes: 35 additions & 6 deletions packages/schema-record/src/-private/managed-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -15,7 +15,7 @@ export function notifyObject(obj: ManagedObject) {
}

type KeyType = string | symbol | number;

const ignoredGlobalFields = new Set<string>(['constructor', 'setInterval', 'nodeType', 'length']);
export interface ManagedObject {
[MUTATE]?(
target: unknown[],
Expand All @@ -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;
Expand All @@ -68,20 +69,42 @@ 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 '<div>ManagedObject</div>';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

follow up: probably just leave it a string along the lines of [ManagedObject<${address.type}:${address.id} (${address.lid})>]

};
}
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;
}
self[SOURCE] = { ...(newData as ObjectValue) }; // Add type assertion for newData
}
}

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);
Expand All @@ -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;
Expand Down
33 changes: 32 additions & 1 deletion packages/schema-record/src/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}'`,
Expand Down Expand Up @@ -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);

Expand All @@ -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`);
}
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export async function reactiveContext<T extends OpaqueRecordInstance>(
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'
Expand Down
121 changes: 121 additions & 0 deletions tests/warp-drive__schema-record/tests/reactivity/object-test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
Loading
Loading