Skip to content

Commit 5c10bf4

Browse files
committed
feat: ensure objects behave amazeballs
1 parent c961bc8 commit 5c10bf4

File tree

3 files changed

+95
-49
lines changed

3 files changed

+95
-49
lines changed

packages/schema-record/src/-private/fields/managed-object.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export class ManagedObject {
8383
}
8484

8585
if (prop === Symbol.toPrimitive) {
86-
return null;
86+
return () => null;
8787
}
8888
if (prop === Symbol.toStringTag) {
8989
return `ManagedObject<${identifier.type}:${identifier.id} (${identifier.lid})>`;
@@ -98,7 +98,12 @@ export class ManagedObject {
9898
}
9999
if (prop === 'toHTML') {
100100
return function () {
101-
return '<div>ManagedObject</div>';
101+
return '<span>ManagedObject</span>';
102+
};
103+
}
104+
if (prop === 'toJSON') {
105+
return function () {
106+
return structuredClone(self[SOURCE]);
102107
};
103108
}
104109

packages/schema-record/src/-private/record.ts

+69-29
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const symbolList = [
6666
const RecordSymbols = new Set(symbolList);
6767

6868
type RecordSymbol = (typeof symbolList)[number];
69+
type ProxiedMethod = (...args: unknown[]) => unknown;
6970

7071
function isPathMatch(a: string[], b: string[]) {
7172
return a.length === b.length && a.every((v, i) => v === b[i]);
@@ -106,6 +107,7 @@ export class SchemaRecord {
106107
const schema = store.schema as unknown as SchemaService;
107108
const cache = store.cache;
108109
const identityField = schema.resource(identifier).identity;
110+
const BoundFns = new Map<string | symbol, ProxiedMethod>();
109111

110112
this[EmbeddedType] = embeddedType;
111113
this[EmbeddedPath] = embeddedPath;
@@ -136,14 +138,20 @@ export class SchemaRecord {
136138
if (!fields.has(prop as string)) {
137139
throw new Error(`No field named ${String(prop)} on ${identifier.type}`);
138140
}
139-
const schemaForField = fields.get(prop as string)!;
141+
const schemaForField = prop === identityField?.name ? identityField : fields.get(prop as string)!;
140142
switch (schemaForField.kind) {
141143
case 'derived':
142144
return {
143145
writable: false,
144146
enumerable: true,
145147
configurable: true,
146148
};
149+
case '@id':
150+
return {
151+
writable: identifier.id === null,
152+
enumerable: true,
153+
configurable: true,
154+
};
147155
case '@local':
148156
case 'field':
149157
case 'attribute':
@@ -161,6 +169,12 @@ export class SchemaRecord {
161169
enumerable: true,
162170
configurable: true,
163171
};
172+
default:
173+
return {
174+
writable: false,
175+
enumerable: false,
176+
configurable: false,
177+
};
164178
}
165179
},
166180

@@ -169,26 +183,6 @@ export class SchemaRecord {
169183
return target[prop as keyof SchemaRecord];
170184
}
171185

172-
if (prop === Symbol.toStringTag) {
173-
return `SchemaRecord<${identifier.type}:${identifier.id} (${identifier.lid})>`;
174-
}
175-
176-
if (prop === 'toString') {
177-
return function () {
178-
return `SchemaRecord<${identifier.type}:${identifier.id} (${identifier.lid})>`;
179-
};
180-
}
181-
182-
if (prop === 'toHTML') {
183-
return function () {
184-
return `<div>SchemaRecord<${identifier.type}:${identifier.id} (${identifier.lid})></div>`;
185-
};
186-
}
187-
188-
if (prop === Symbol.toPrimitive) {
189-
return null;
190-
}
191-
192186
// TODO make this a symbol
193187
if (prop === '___notifications') {
194188
return target.___notifications;
@@ -203,19 +197,65 @@ export class SchemaRecord {
203197
if (IgnoredGlobalFields.has(prop as string)) {
204198
return undefined;
205199
}
200+
201+
/////////////////////////////////////////////////////////////
202+
//// Note these bound function behaviors are essentially ////
203+
//// built-in but overrideable derivations. ////
204+
//// ////
205+
//// The bar for this has to be "basic expectations of ////
206+
/// an object" – very, very high ////
207+
/////////////////////////////////////////////////////////////
208+
209+
if (prop === Symbol.toStringTag || prop === 'toString') {
210+
let fn = BoundFns.get('toString');
211+
if (!fn) {
212+
fn = function () {
213+
entangleSignal(signals, receiver, '@identity');
214+
return `Record<${identifier.type}:${identifier.id} (${identifier.lid})>`;
215+
};
216+
BoundFns.set(prop, fn);
217+
}
218+
return fn;
219+
}
220+
221+
if (prop === 'toHTML') {
222+
let fn = BoundFns.get('toHTML');
223+
if (!fn) {
224+
fn = function () {
225+
entangleSignal(signals, receiver, '@identity');
226+
return `<span>Record<${identifier.type}:${identifier.id} (${identifier.lid})></span>`;
227+
};
228+
BoundFns.set(prop, fn);
229+
}
230+
return fn;
231+
}
232+
233+
if (prop === 'toJSON') {
234+
let fn = BoundFns.get('toJSON');
235+
if (!fn) {
236+
fn = function () {
237+
const json: Record<string, unknown> = {};
238+
for (const key in receiver) {
239+
json[key] = receiver[key as keyof typeof receiver];
240+
}
241+
242+
return json;
243+
};
244+
BoundFns.set(prop, fn);
245+
}
246+
return fn;
247+
}
248+
249+
if (prop === Symbol.toPrimitive) return () => null;
250+
206251
if (prop === 'constructor') {
207252
return SchemaRecord;
208253
}
209254
// too many things check for random symbols
210-
if (typeof prop === 'symbol') {
211-
return undefined;
212-
}
213-
let type = identifier.type;
214-
if (isEmbedded) {
215-
type = embeddedType!;
216-
}
255+
if (typeof prop === 'symbol') return undefined;
217256

218-
throw new Error(`No field named ${String(prop)} on ${type}`);
257+
assert(`No field named ${String(prop)} on ${isEmbedded ? embeddedType! : identifier.type}`);
258+
return undefined;
219259
}
220260

221261
const field = maybeField.kind === 'alias' ? maybeField.options : maybeField;

packages/schema-record/src/-private/schema.ts

+19-18
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
type FieldSchema,
1818
type GenericField,
1919
type HashField,
20+
type IdentityField,
2021
isResourceSchema,
2122
type LegacyAttributeField,
2223
type LegacyRelationshipSchema,
@@ -32,19 +33,18 @@ import { Identifier } from './symbols';
3233

3334
const Support = getOrSetGlobal('Support', new WeakMap<WeakKey, Record<string, unknown>>());
3435

35-
export const SchemaRecordFields: FieldSchema[] = [
36-
{
37-
type: '@constructor',
38-
name: 'constructor',
39-
kind: 'derived',
40-
},
41-
{
42-
type: '@identity',
43-
name: '$type',
44-
kind: 'derived',
45-
options: { key: 'type' },
46-
},
47-
];
36+
const ConstructorField = {
37+
type: '@constructor',
38+
name: 'constructor',
39+
kind: 'derived',
40+
} satisfies DerivedField;
41+
const TypeField = {
42+
type: '@identity',
43+
name: '$type',
44+
kind: 'derived',
45+
options: { key: 'type' },
46+
} satisfies DerivedField;
47+
const DefaultIdentityField = { name: 'id', kind: '@id' } satisfies IdentityField;
4848

4949
function _constructor(record: SchemaRecord) {
5050
let state = Support.get(record as WeakKey);
@@ -55,16 +55,17 @@ function _constructor(record: SchemaRecord) {
5555

5656
return (state._constructor = state._constructor || {
5757
name: `SchemaRecord<${recordIdentifierFor(record).type}>`,
58-
get modelName() {
59-
throw new Error('Cannot access record.constructor.modelName on non-Legacy Schema Records.');
60-
},
6158
});
6259
}
6360
_constructor[Type] = '@constructor';
6461

62+
/**
63+
* Utility for constructing a ResourceSchema with the recommended fields
64+
* for the Polaris experience.
65+
*/
6566
export function withDefaults(schema: WithPartial<ResourceSchema, 'identity'>): ResourceSchema {
66-
schema.identity = schema.identity || { name: 'id', kind: '@id' };
67-
schema.fields.push(...SchemaRecordFields);
67+
schema.identity = schema.identity || DefaultIdentityField;
68+
schema.fields.push(TypeField, ConstructorField);
6869
return schema as ResourceSchema;
6970
}
7071

0 commit comments

Comments
 (0)