Skip to content

Commit a9516d0

Browse files
authored
feat: make @id editable and reactive (#9466)
* feat: make @id editable and reactive * fix lint
1 parent c61cdd3 commit a9516d0

File tree

3 files changed

+232
-1
lines changed

3 files changed

+232
-1
lines changed

packages/schema-record/src/record.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -284,12 +284,27 @@ export class SchemaRecord {
284284
const propArray = isEmbedded ? embeddedPath!.slice() : [];
285285
propArray.push(prop as string);
286286

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

292292
switch (field.kind) {
293+
case '@id': {
294+
assert(`Expected to receive a string id`, typeof value === 'string' && value.length);
295+
const normalizedId = String(value);
296+
const didChange = normalizedId !== identifier.id;
297+
assert(
298+
`Cannot set ${identifier.type} record's id to ${normalizedId}, because id is already ${identifier.id}`,
299+
!didChange || identifier.id === null
300+
);
301+
302+
if (normalizedId !== null && didChange) {
303+
store._instanceCache.setRecordId(identifier, normalizedId);
304+
store.notifications.notify(identifier, 'identity');
305+
}
306+
return true;
307+
}
293308
case '@local': {
294309
const signal = getSignal(receiver, prop as string, true);
295310
if (signal.lastValue !== value) {
@@ -426,6 +441,17 @@ export class SchemaRecord {
426441
identifier,
427442
(_: StableRecordIdentifier, type: NotificationType, key?: string | string[]) => {
428443
switch (type) {
444+
case 'identity': {
445+
if (isEmbedded || !identityField) return; // base paths never apply to embedded records
446+
447+
if (identityField.name && identityField.kind === '@id') {
448+
const signal = signals.get('@identity');
449+
if (signal) {
450+
addToTransaction(signal);
451+
}
452+
}
453+
break;
454+
}
429455
case 'attributes':
430456
if (key) {
431457
if (Array.isArray(key)) {
@@ -516,6 +542,8 @@ export class SchemaRecord {
516542
}
517543
}
518544
}
545+
546+
break;
519547
}
520548
}
521549
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { module, test } from 'qunit';
2+
3+
import { setupTest } from 'ember-qunit';
4+
5+
import {
6+
registerDerivations as registerLegacyDerivations,
7+
withDefaults as withLegacy,
8+
} from '@ember-data/model/migration-support';
9+
10+
import type Store from 'warp-drive__schema-record/services/store';
11+
12+
interface User {
13+
id: string | null;
14+
$type: 'user';
15+
name: string;
16+
age: number;
17+
netWorth: number;
18+
coolometer: number;
19+
rank: number;
20+
}
21+
22+
module('Legacy | Create | basic fields', function (hooks) {
23+
setupTest(hooks);
24+
25+
test('attributes work when passed to createRecord', function (assert) {
26+
const store = this.owner.lookup('service:store') as Store;
27+
const { schema } = store;
28+
registerLegacyDerivations(schema);
29+
30+
schema.registerResource(
31+
withLegacy({
32+
type: 'user',
33+
fields: [
34+
{
35+
name: 'name',
36+
type: null,
37+
kind: 'attribute',
38+
},
39+
],
40+
})
41+
);
42+
43+
const record = store.createRecord('user', { name: 'Rey Skybarker' }) as User;
44+
45+
assert.strictEqual(record.id, null, 'id is accessible');
46+
assert.strictEqual(record.name, 'Rey Skybarker', 'name is accessible');
47+
});
48+
49+
test('id works when passed to createRecord', function (assert) {
50+
const store = this.owner.lookup('service:store') as Store;
51+
const { schema } = store;
52+
registerLegacyDerivations(schema);
53+
54+
schema.registerResource(
55+
withLegacy({
56+
type: 'user',
57+
fields: [
58+
{
59+
name: 'name',
60+
type: null,
61+
kind: 'attribute',
62+
},
63+
],
64+
})
65+
);
66+
67+
const record = store.createRecord('user', { id: '1' }) as User;
68+
69+
assert.strictEqual(record.id, '1', 'id is accessible');
70+
assert.strictEqual(record.name, undefined, 'name is accessible');
71+
});
72+
73+
test('attributes work when updated after createRecord', function (assert) {
74+
const store = this.owner.lookup('service:store') as Store;
75+
const { schema } = store;
76+
registerLegacyDerivations(schema);
77+
78+
schema.registerResource(
79+
withLegacy({
80+
type: 'user',
81+
fields: [
82+
{
83+
name: 'name',
84+
type: null,
85+
kind: 'attribute',
86+
},
87+
],
88+
})
89+
);
90+
91+
const record = store.createRecord('user', {}) as User;
92+
assert.strictEqual(record.name, undefined, 'name is accessible');
93+
record.name = 'Rey Skybarker';
94+
assert.strictEqual(record.name, 'Rey Skybarker', 'name is accessible');
95+
});
96+
97+
test('id works when updated after createRecord', function (assert) {
98+
const store = this.owner.lookup('service:store') as Store;
99+
const { schema } = store;
100+
registerLegacyDerivations(schema);
101+
102+
schema.registerResource(
103+
withLegacy({
104+
type: 'user',
105+
fields: [
106+
{
107+
name: 'name',
108+
type: null,
109+
kind: 'attribute',
110+
},
111+
],
112+
})
113+
);
114+
115+
const record = store.createRecord('user', {}) as User;
116+
assert.strictEqual(record.id, null, 'id is accessible');
117+
record.id = '1';
118+
assert.strictEqual(record.id, '1', 'id is accessible');
119+
});
120+
});

tests/warp-drive__schema-record/tests/legacy/reactivity/basic-fields-test.ts

+83
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
withDefaults as withLegacy,
1111
} from '@ember-data/model/migration-support';
1212
import type Store from '@ember-data/store';
13+
import { recordIdentifierFor } from '@ember-data/store';
1314
import type { StableRecordIdentifier } from '@warp-drive/core-types';
1415
import { Type } from '@warp-drive/core-types/symbols';
1516
import type { SchemaRecord } from '@warp-drive/schema-record/record';
@@ -355,4 +356,86 @@ module('Legacy | Reactivity | basic fields can receive remote updates', function
355356
assert.dom(`li:nth-child(${nameIndex + 1})`).hasText('name: Rey Skybarker', 'name is rendered');
356357
assert.dom(`li:nth-child(${nameIndex + 3})`).hasText('coolometer:', 'coolometer is rendered');
357358
});
359+
360+
test('id works when updated after createRecord', async function (assert) {
361+
const store = this.owner.lookup('service:store') as Store;
362+
const { schema } = store;
363+
registerLegacyDerivations(schema);
364+
365+
schema.registerResource(
366+
withLegacy({
367+
type: 'user',
368+
fields: [
369+
{
370+
name: 'name',
371+
type: null,
372+
kind: 'attribute',
373+
},
374+
],
375+
})
376+
);
377+
378+
const record = store.createRecord('user', {}) as User;
379+
const resource = schema.resource({ type: 'user' });
380+
381+
const { counters, fieldOrder } = await reactiveContext.call(this, record, resource);
382+
const idIndex = fieldOrder.indexOf('id');
383+
384+
assert.strictEqual(record.id, null, 'id is accessible');
385+
assert.strictEqual(counters.id, 1, 'idCount is 1');
386+
assert.dom(`li:nth-child(${idIndex + 1})`).hasText('id:', 'id is rendered');
387+
388+
record.id = '1';
389+
assert.strictEqual(record.id, '1', 'id is accessible');
390+
391+
await rerender();
392+
assert.strictEqual(counters.id, 2, 'idCount is 2');
393+
assert.dom(`li:nth-child(${idIndex + 1})`).hasText('id: 1', 'id is rendered');
394+
});
395+
396+
test('id works when updated after save', async function (assert) {
397+
const store = this.owner.lookup('service:store') as Store;
398+
const { schema } = store;
399+
registerLegacyDerivations(schema);
400+
401+
schema.registerResource(
402+
withLegacy({
403+
type: 'user',
404+
fields: [
405+
{
406+
name: 'name',
407+
type: null,
408+
kind: 'attribute',
409+
},
410+
],
411+
})
412+
);
413+
414+
const record = store.createRecord('user', { name: 'Rey' }) as User;
415+
const identifier = recordIdentifierFor(record);
416+
const resource = schema.resource({ type: 'user' });
417+
418+
const { counters, fieldOrder } = await reactiveContext.call(this, record, resource);
419+
const idIndex = fieldOrder.indexOf('id');
420+
421+
assert.strictEqual(record.id, null, 'id is accessible');
422+
assert.strictEqual(counters.id, 1, 'idCount is 1');
423+
assert.dom(`li:nth-child(${idIndex + 1})`).hasText('id:', 'id is rendered');
424+
425+
store.push({
426+
data: {
427+
type: 'user',
428+
id: '1',
429+
lid: identifier.lid,
430+
attributes: {
431+
name: 'Rey',
432+
},
433+
},
434+
});
435+
436+
assert.strictEqual(record.id, '1', 'id is accessible');
437+
await rerender();
438+
assert.strictEqual(counters.id, 2, 'idCount is 2');
439+
assert.dom(`li:nth-child(${idIndex + 1})`).hasText('id: 1', 'id is rendered');
440+
});
358441
});

0 commit comments

Comments
 (0)