Skip to content

Commit

Permalink
feat: make @id editable and reactive (#9466)
Browse files Browse the repository at this point in the history
* feat: make @id editable and reactive

* fix lint
  • Loading branch information
runspired authored Jun 2, 2024
1 parent c61cdd3 commit a9516d0
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 1 deletion.
30 changes: 29 additions & 1 deletion packages/schema-record/src/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,12 +284,27 @@ export class SchemaRecord {
const propArray = isEmbedded ? embeddedPath!.slice() : [];
propArray.push(prop as string);

const field = fields.get(prop as string);
const field = prop === identityField?.name ? identityField : fields.get(prop as string);
if (!field) {
throw new Error(`There is no field named ${String(prop)} on ${identifier.type}`);
}

switch (field.kind) {
case '@id': {
assert(`Expected to receive a string id`, typeof value === 'string' && value.length);
const normalizedId = String(value);
const didChange = normalizedId !== identifier.id;
assert(
`Cannot set ${identifier.type} record's id to ${normalizedId}, because id is already ${identifier.id}`,
!didChange || identifier.id === null
);

if (normalizedId !== null && didChange) {
store._instanceCache.setRecordId(identifier, normalizedId);
store.notifications.notify(identifier, 'identity');
}
return true;
}
case '@local': {
const signal = getSignal(receiver, prop as string, true);
if (signal.lastValue !== value) {
Expand Down Expand Up @@ -426,6 +441,17 @@ export class SchemaRecord {
identifier,
(_: StableRecordIdentifier, type: NotificationType, key?: string | string[]) => {
switch (type) {
case 'identity': {
if (isEmbedded || !identityField) return; // base paths never apply to embedded records

if (identityField.name && identityField.kind === '@id') {
const signal = signals.get('@identity');
if (signal) {
addToTransaction(signal);
}
}
break;
}
case 'attributes':
if (key) {
if (Array.isArray(key)) {
Expand Down Expand Up @@ -516,6 +542,8 @@ export class SchemaRecord {
}
}
}

break;
}
}
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { module, test } from 'qunit';

import { setupTest } from 'ember-qunit';

import {
registerDerivations as registerLegacyDerivations,
withDefaults as withLegacy,
} from '@ember-data/model/migration-support';

import type Store from 'warp-drive__schema-record/services/store';

interface User {
id: string | null;
$type: 'user';
name: string;
age: number;
netWorth: number;
coolometer: number;
rank: number;
}

module('Legacy | Create | basic fields', function (hooks) {
setupTest(hooks);

test('attributes work when passed to createRecord', function (assert) {
const store = this.owner.lookup('service:store') as Store;
const { schema } = store;
registerLegacyDerivations(schema);

schema.registerResource(
withLegacy({
type: 'user',
fields: [
{
name: 'name',
type: null,
kind: 'attribute',
},
],
})
);

const record = store.createRecord('user', { name: 'Rey Skybarker' }) as User;

assert.strictEqual(record.id, null, 'id is accessible');
assert.strictEqual(record.name, 'Rey Skybarker', 'name is accessible');
});

test('id works when passed to createRecord', function (assert) {
const store = this.owner.lookup('service:store') as Store;
const { schema } = store;
registerLegacyDerivations(schema);

schema.registerResource(
withLegacy({
type: 'user',
fields: [
{
name: 'name',
type: null,
kind: 'attribute',
},
],
})
);

const record = store.createRecord('user', { id: '1' }) as User;

assert.strictEqual(record.id, '1', 'id is accessible');
assert.strictEqual(record.name, undefined, 'name is accessible');
});

test('attributes work when updated after createRecord', function (assert) {
const store = this.owner.lookup('service:store') as Store;
const { schema } = store;
registerLegacyDerivations(schema);

schema.registerResource(
withLegacy({
type: 'user',
fields: [
{
name: 'name',
type: null,
kind: 'attribute',
},
],
})
);

const record = store.createRecord('user', {}) as User;
assert.strictEqual(record.name, undefined, 'name is accessible');
record.name = 'Rey Skybarker';
assert.strictEqual(record.name, 'Rey Skybarker', 'name is accessible');
});

test('id works when updated after createRecord', function (assert) {
const store = this.owner.lookup('service:store') as Store;
const { schema } = store;
registerLegacyDerivations(schema);

schema.registerResource(
withLegacy({
type: 'user',
fields: [
{
name: 'name',
type: null,
kind: 'attribute',
},
],
})
);

const record = store.createRecord('user', {}) as User;
assert.strictEqual(record.id, null, 'id is accessible');
record.id = '1';
assert.strictEqual(record.id, '1', 'id is accessible');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
withDefaults as withLegacy,
} from '@ember-data/model/migration-support';
import type Store from '@ember-data/store';
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';
Expand Down Expand Up @@ -355,4 +356,86 @@ module('Legacy | Reactivity | basic fields can receive remote updates', function
assert.dom(`li:nth-child(${nameIndex + 1})`).hasText('name: Rey Skybarker', 'name is rendered');
assert.dom(`li:nth-child(${nameIndex + 3})`).hasText('coolometer:', 'coolometer is rendered');
});

test('id works when updated after createRecord', async function (assert) {
const store = this.owner.lookup('service:store') as Store;
const { schema } = store;
registerLegacyDerivations(schema);

schema.registerResource(
withLegacy({
type: 'user',
fields: [
{
name: 'name',
type: null,
kind: 'attribute',
},
],
})
);

const record = store.createRecord('user', {}) as User;
const resource = schema.resource({ type: 'user' });

const { counters, fieldOrder } = await reactiveContext.call(this, record, resource);
const idIndex = fieldOrder.indexOf('id');

assert.strictEqual(record.id, null, 'id is accessible');
assert.strictEqual(counters.id, 1, 'idCount is 1');
assert.dom(`li:nth-child(${idIndex + 1})`).hasText('id:', 'id is rendered');

record.id = '1';
assert.strictEqual(record.id, '1', 'id is accessible');

await rerender();
assert.strictEqual(counters.id, 2, 'idCount is 2');
assert.dom(`li:nth-child(${idIndex + 1})`).hasText('id: 1', 'id is rendered');
});

test('id works when updated after save', async function (assert) {
const store = this.owner.lookup('service:store') as Store;
const { schema } = store;
registerLegacyDerivations(schema);

schema.registerResource(
withLegacy({
type: 'user',
fields: [
{
name: 'name',
type: null,
kind: 'attribute',
},
],
})
);

const record = store.createRecord('user', { name: 'Rey' }) as User;
const identifier = recordIdentifierFor(record);
const resource = schema.resource({ type: 'user' });

const { counters, fieldOrder } = await reactiveContext.call(this, record, resource);
const idIndex = fieldOrder.indexOf('id');

assert.strictEqual(record.id, null, 'id is accessible');
assert.strictEqual(counters.id, 1, 'idCount is 1');
assert.dom(`li:nth-child(${idIndex + 1})`).hasText('id:', 'id is rendered');

store.push({
data: {
type: 'user',
id: '1',
lid: identifier.lid,
attributes: {
name: 'Rey',
},
},
});

assert.strictEqual(record.id, '1', 'id is accessible');
await rerender();
assert.strictEqual(counters.id, 2, 'idCount is 2');
assert.dom(`li:nth-child(${idIndex + 1})`).hasText('id: 1', 'id is rendered');
});
});

0 comments on commit a9516d0

Please sign in to comment.