@@ -66,11 +66,24 @@ const symbolList = [
66
66
const RecordSymbols = new Set ( symbolList ) ;
67
67
68
68
type RecordSymbol = ( typeof symbolList ) [ number ] ;
69
+ type ProxiedMethod = ( ...args : unknown [ ] ) => unknown ;
69
70
70
71
function isPathMatch ( a : string [ ] , b : string [ ] ) {
71
72
return a . length === b . length && a . every ( ( v , i ) => v === b [ i ] ) ;
72
73
}
73
74
75
+ function isNonEnumerableProp ( prop : string | number | symbol ) {
76
+ return (
77
+ prop === 'constructor' ||
78
+ prop === 'prototype' ||
79
+ prop === '__proto__' ||
80
+ prop === 'toString' ||
81
+ prop === 'toJSON' ||
82
+ prop === 'toHTML' ||
83
+ typeof prop === 'symbol'
84
+ ) ;
85
+ }
86
+
74
87
const Editables = new WeakMap < SchemaRecord , SchemaRecord > ( ) ;
75
88
export class SchemaRecord {
76
89
declare [ RecordStore ] : Store ;
@@ -105,24 +118,27 @@ export class SchemaRecord {
105
118
106
119
const schema = store . schema as unknown as SchemaService ;
107
120
const cache = store . cache ;
108
- const identityField = schema . resource ( identifier ) . identity ;
121
+ const identityField = schema . resource ( isEmbedded ? { type : embeddedType as string } : identifier ) . identity ;
122
+ const BoundFns = new Map < string | symbol , ProxiedMethod > ( ) ;
109
123
110
124
this [ EmbeddedType ] = embeddedType ;
111
125
this [ EmbeddedPath ] = embeddedPath ;
112
126
113
- let fields : Map < string , FieldSchema > ;
114
- if ( isEmbedded ) {
115
- fields = schema . fields ( { type : embeddedType as string } ) ;
116
- } else {
117
- fields = schema . fields ( identifier ) ;
118
- }
127
+ const fields : Map < string , FieldSchema > = isEmbedded
128
+ ? schema . fields ( { type : embeddedType as string } )
129
+ : schema . fields ( identifier ) ;
119
130
120
131
const signals : Map < string , Signal > = new Map ( ) ;
121
132
this [ Signals ] = signals ;
122
133
123
134
const proxy = new Proxy ( this , {
124
135
ownKeys ( ) {
125
- return Array . from ( fields . keys ( ) ) ;
136
+ const identityKey = identityField ?. name ;
137
+ const keys = Array . from ( fields . keys ( ) ) ;
138
+ if ( identityKey ) {
139
+ keys . unshift ( identityKey ) ;
140
+ }
141
+ return keys ;
126
142
} ,
127
143
128
144
has ( target : SchemaRecord , prop : string | number | symbol ) {
@@ -133,17 +149,30 @@ export class SchemaRecord {
133
149
} ,
134
150
135
151
getOwnPropertyDescriptor ( target , prop ) {
136
- if ( ! fields . has ( prop as string ) ) {
137
- throw new Error ( `No field named ${ String ( prop ) } on ${ identifier . type } ` ) ;
152
+ const schemaForField = prop === identityField ?. name ? identityField : fields . get ( prop as string ) ! ;
153
+ assert ( `No field named ${ String ( prop ) } on ${ identifier . type } ` , schemaForField ) ;
154
+
155
+ if ( isNonEnumerableProp ( prop ) ) {
156
+ return {
157
+ writable : false ,
158
+ enumerable : false ,
159
+ configurable : true ,
160
+ } ;
138
161
}
139
- const schemaForField = fields . get ( prop as string ) ! ;
162
+
140
163
switch ( schemaForField . kind ) {
141
164
case 'derived' :
142
165
return {
143
166
writable : false ,
144
167
enumerable : true ,
145
168
configurable : true ,
146
169
} ;
170
+ case '@id' :
171
+ return {
172
+ writable : identifier . id === null ,
173
+ enumerable : true ,
174
+ configurable : true ,
175
+ } ;
147
176
case '@local' :
148
177
case 'field' :
149
178
case 'attribute' :
@@ -161,6 +190,12 @@ export class SchemaRecord {
161
190
enumerable : true ,
162
191
configurable : true ,
163
192
} ;
193
+ default :
194
+ return {
195
+ writable : false ,
196
+ enumerable : false ,
197
+ configurable : false ,
198
+ } ;
164
199
}
165
200
} ,
166
201
@@ -169,26 +204,6 @@ export class SchemaRecord {
169
204
return target [ prop as keyof SchemaRecord ] ;
170
205
}
171
206
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
-
192
207
// TODO make this a symbol
193
208
if ( prop === '___notifications' ) {
194
209
return target . ___notifications ;
@@ -203,19 +218,78 @@ export class SchemaRecord {
203
218
if ( IgnoredGlobalFields . has ( prop as string ) ) {
204
219
return undefined ;
205
220
}
221
+
222
+ /////////////////////////////////////////////////////////////
223
+ //// Note these bound function behaviors are essentially ////
224
+ //// built-in but overrideable derivations. ////
225
+ //// ////
226
+ //// The bar for this has to be "basic expectations of ////
227
+ /// an object" – very, very high ////
228
+ /////////////////////////////////////////////////////////////
229
+
230
+ if ( prop === Symbol . toStringTag || prop === 'toString' ) {
231
+ let fn = BoundFns . get ( 'toString' ) ;
232
+ if ( ! fn ) {
233
+ fn = function ( ) {
234
+ entangleSignal ( signals , receiver , '@identity' ) ;
235
+ return `Record<${ identifier . type } :${ identifier . id } (${ identifier . lid } )>` ;
236
+ } ;
237
+ BoundFns . set ( prop , fn ) ;
238
+ }
239
+ return fn ;
240
+ }
241
+
242
+ if ( prop === 'toHTML' ) {
243
+ let fn = BoundFns . get ( 'toHTML' ) ;
244
+ if ( ! fn ) {
245
+ fn = function ( ) {
246
+ entangleSignal ( signals , receiver , '@identity' ) ;
247
+ return `<span>Record<${ identifier . type } :${ identifier . id } (${ identifier . lid } )></span>` ;
248
+ } ;
249
+ BoundFns . set ( prop , fn ) ;
250
+ }
251
+ return fn ;
252
+ }
253
+
254
+ if ( prop === 'toJSON' ) {
255
+ let fn = BoundFns . get ( 'toJSON' ) ;
256
+ if ( ! fn ) {
257
+ fn = function ( ) {
258
+ const json : Record < string , unknown > = { } ;
259
+ for ( const key in receiver ) {
260
+ json [ key ] = receiver [ key as keyof typeof receiver ] ;
261
+ }
262
+
263
+ return json ;
264
+ } ;
265
+ BoundFns . set ( prop , fn ) ;
266
+ }
267
+ return fn ;
268
+ }
269
+
270
+ if ( prop === Symbol . toPrimitive ) return ( ) => null ;
271
+
272
+ if ( prop === Symbol . iterator ) {
273
+ let fn = BoundFns . get ( Symbol . iterator ) ;
274
+ if ( ! fn ) {
275
+ fn = function * ( ) {
276
+ for ( const key in receiver ) {
277
+ yield [ key , receiver [ key as keyof typeof receiver ] ] ;
278
+ }
279
+ } ;
280
+ BoundFns . set ( Symbol . iterator , fn ) ;
281
+ }
282
+ return fn ;
283
+ }
284
+
206
285
if ( prop === 'constructor' ) {
207
286
return SchemaRecord ;
208
287
}
209
288
// 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
- }
289
+ if ( typeof prop === 'symbol' ) return undefined ;
217
290
218
- throw new Error ( `No field named ${ String ( prop ) } on ${ type } ` ) ;
291
+ assert ( `No field named ${ String ( prop ) } on ${ isEmbedded ? embeddedType ! : identifier . type } ` ) ;
292
+ return undefined ;
219
293
}
220
294
221
295
const field = maybeField . kind === 'alias' ? maybeField . options : maybeField ;
0 commit comments