@signalium/query 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/errors.ts ADDED
@@ -0,0 +1,124 @@
1
+ import {
2
+ ARRAY_KEY,
3
+ ArrayDef,
4
+ EntityDef,
5
+ Mask,
6
+ ObjectDef,
7
+ RECORD_KEY,
8
+ RecordDef,
9
+ ObjectFieldTypeDef,
10
+ UnionDef,
11
+ } from './types.js';
12
+
13
+ export function typeToString(type: ObjectFieldTypeDef): string {
14
+ // Handle Set-based constants/enums
15
+ if (type instanceof Set) {
16
+ const values = Array.from(type).map(v => (typeof v === 'string' ? `"${v}"` : String(v)));
17
+ return values.join(' | ');
18
+ }
19
+
20
+ // Handle constants
21
+ if (typeof type === 'string') {
22
+ return `"${type}"`;
23
+ }
24
+
25
+ if (typeof type === 'boolean') {
26
+ return String(type);
27
+ }
28
+
29
+ // Handle primitive masks
30
+ if (typeof type === 'number') {
31
+ const types: string[] = [];
32
+
33
+ if (type & Mask.UNDEFINED) types.push('undefined');
34
+ if (type & Mask.NULL) types.push('null');
35
+ if (type & Mask.NUMBER) types.push('number');
36
+ if (type & Mask.STRING) types.push('string');
37
+ if (type & Mask.BOOLEAN) types.push('boolean');
38
+ if (type & Mask.OBJECT) types.push('object');
39
+ if (type & Mask.ARRAY) types.push('array');
40
+
41
+ if (types.length === 0) {
42
+ return 'unknown';
43
+ }
44
+
45
+ return types.length === 1 ? types[0] : types.join(' | ');
46
+ }
47
+
48
+ // Handle complex types - CHECK UNION FIRST since it contains other types
49
+ const mask = type.mask;
50
+
51
+ if (mask & Mask.UNION) {
52
+ const unionType = type as UnionDef;
53
+ const parts: string[] = [];
54
+
55
+ // Add primitive types from the mask
56
+ if (mask & Mask.UNDEFINED) parts.push('undefined');
57
+ if (mask & Mask.NULL) parts.push('null');
58
+ if (mask & Mask.NUMBER) parts.push('number');
59
+ if (mask & Mask.STRING) parts.push('string');
60
+ if (mask & Mask.BOOLEAN) parts.push('boolean');
61
+
62
+ // Add const/enum values from the values Set
63
+ if (unionType.values !== undefined && unionType.values.size > 0) {
64
+ for (const val of unionType.values) {
65
+ const valStr = typeof val === 'string' ? `"${val}"` : String(val);
66
+ parts.push(valStr);
67
+ }
68
+ }
69
+
70
+ // Add complex types from the shape object
71
+ if (unionType.shape !== undefined) {
72
+ if (unionType.shape[ARRAY_KEY] !== undefined) {
73
+ parts.push(`Array<${typeToString(unionType.shape[ARRAY_KEY] as ObjectFieldTypeDef)}>`);
74
+ }
75
+
76
+ if (unionType.shape[RECORD_KEY] !== undefined) {
77
+ parts.push(`Record<string, ${typeToString(unionType.shape[RECORD_KEY] as ObjectFieldTypeDef)}>`);
78
+ }
79
+
80
+ // Add entity/object types by typename
81
+ for (const [key, value] of Object.entries(unionType.shape)) {
82
+ if (key !== (ARRAY_KEY as any) && key !== (RECORD_KEY as any)) {
83
+ // key is the typename value (e.g., "User", "Post")
84
+ parts.push(key);
85
+ }
86
+ }
87
+ }
88
+
89
+ if (parts.length === 0) {
90
+ return 'union';
91
+ }
92
+
93
+ return parts.join(' | ');
94
+ }
95
+
96
+ if (mask & Mask.ENTITY) {
97
+ return `Entity<${(type as EntityDef).typenameValue}>`;
98
+ }
99
+
100
+ if (mask & Mask.ARRAY) {
101
+ const shape = (type as ArrayDef).shape;
102
+ return `Array<${typeToString(shape)}>`;
103
+ }
104
+
105
+ if (mask & Mask.RECORD) {
106
+ const shape = (type as RecordDef).shape;
107
+ return `Record<string, ${typeToString(shape)}>`;
108
+ }
109
+
110
+ if (mask & Mask.OBJECT) {
111
+ const typename = (type as ObjectDef).typenameValue;
112
+ return typename ? `Object<${typename}>` : 'object';
113
+ }
114
+
115
+ return 'unknown';
116
+ }
117
+
118
+ export function typeError(path: string, expectedType: ObjectFieldTypeDef, value: unknown): Error {
119
+ return new TypeError(
120
+ `Validation error at ${path}: expected ${typeToString(expectedType)}, got ${
121
+ typeof value === 'object' ? (value === null ? 'null' : Array.isArray(value) ? 'array' : 'object') : typeof value
122
+ }`,
123
+ );
124
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './types.js';
2
+
3
+ export { QueryClient, QueryResultImpl as QueryResult } from './QueryClient.js';
4
+ export type { QueryContext } from './QueryClient.js';
5
+ export type { QueryStore } from './QueryStore.js';
6
+ export { query } from './query.js';
7
+ export type { ExtractType } from './query.js';
@@ -0,0 +1,213 @@
1
+ // -----------------------------------------------------------------------------
2
+ // Entity System
3
+ // -----------------------------------------------------------------------------
4
+
5
+ import { hashValue } from 'signalium/utils';
6
+ import { QueryClient } from './QueryClient.js';
7
+ import {
8
+ ARRAY_KEY,
9
+ ArrayDef,
10
+ ComplexTypeDef,
11
+ EntityDef,
12
+ Mask,
13
+ ObjectDef,
14
+ RECORD_KEY,
15
+ RecordDef,
16
+ UnionDef,
17
+ } from './types.js';
18
+ import { extractShape, typeMaskOf } from './utils.js';
19
+
20
+ const entries = Object.entries;
21
+
22
+ export function parseUnionEntities(
23
+ valueType: number,
24
+ value: object | unknown[],
25
+ unionDef: UnionDef,
26
+ queryClient: QueryClient,
27
+ entityRefs?: Set<number>,
28
+ ): unknown {
29
+ if (valueType === Mask.ARRAY) {
30
+ const shape = unionDef.shape![ARRAY_KEY];
31
+
32
+ if (shape === undefined || typeof shape === 'number') {
33
+ return value;
34
+ }
35
+
36
+ return parseArrayEntities(
37
+ value as unknown[],
38
+ { mask: Mask.ARRAY, shape, values: undefined } as ArrayDef,
39
+ queryClient,
40
+ entityRefs,
41
+ );
42
+ } else {
43
+ // Use the cached typename field from the union definition
44
+ const typenameField = unionDef.typenameField;
45
+ const typename = typenameField ? (value as Record<string, unknown>)[typenameField] : undefined;
46
+
47
+ if (typename === undefined || typeof typename !== 'string') {
48
+ const recordShape = unionDef.shape![RECORD_KEY];
49
+
50
+ if (recordShape === undefined || typeof recordShape === 'number') {
51
+ return value;
52
+ }
53
+
54
+ return parseRecordEntities(
55
+ value as Record<string, unknown>,
56
+ recordShape as ComplexTypeDef,
57
+ queryClient,
58
+ entityRefs,
59
+ );
60
+ }
61
+
62
+ const matchingDef = unionDef.shape![typename];
63
+
64
+ if (matchingDef === undefined || typeof matchingDef === 'number') {
65
+ return value;
66
+ }
67
+
68
+ return parseObjectEntities(
69
+ value as Record<string, unknown>,
70
+ matchingDef as ObjectDef | EntityDef,
71
+ queryClient,
72
+ entityRefs,
73
+ );
74
+ }
75
+ }
76
+
77
+ export function parseArrayEntities(
78
+ array: unknown[],
79
+ arrayShape: ComplexTypeDef,
80
+ queryClient: QueryClient,
81
+ entityRefs?: Set<number>,
82
+ ): unknown[] {
83
+ for (let i = 0; i < array.length; i++) {
84
+ array[i] = parseEntities(array[i], arrayShape, queryClient, entityRefs);
85
+ }
86
+
87
+ return array;
88
+ }
89
+
90
+ export function parseRecordEntities(
91
+ record: Record<string, unknown>,
92
+ recordShape: ComplexTypeDef,
93
+ queryClient: QueryClient,
94
+ entityRefs?: Set<number>,
95
+ ): Record<string, unknown> {
96
+ if (typeof recordShape === 'number') {
97
+ return record;
98
+ }
99
+
100
+ for (const [key, value] of entries(record)) {
101
+ record[key] = parseEntities(value, recordShape, queryClient, entityRefs);
102
+ }
103
+
104
+ return record;
105
+ }
106
+
107
+ export function parseObjectEntities(
108
+ obj: Record<string, unknown>,
109
+ objectShape: ObjectDef | EntityDef,
110
+ queryClient: QueryClient,
111
+ entityRefs?: Set<number>,
112
+ ): Record<string, unknown> {
113
+ const entityRefId = obj.__entityRef as number;
114
+
115
+ // Check if this is an entity reference (from cache)
116
+ if (typeof entityRefId === 'number') {
117
+ return queryClient.hydrateEntity(entityRefId, objectShape as EntityDef).proxy;
118
+ }
119
+
120
+ // Process sub-entity paths (only these paths can contain entities)
121
+ const { mask } = objectShape;
122
+
123
+ const childRefs = mask & Mask.ENTITY ? new Set<number>() : entityRefs;
124
+
125
+ // Extract shape first to resolve lazy definitions and set subEntityPaths
126
+ const shape = extractShape(objectShape);
127
+ const subEntityPaths = objectShape.subEntityPaths;
128
+
129
+ if (subEntityPaths !== undefined) {
130
+ if (typeof subEntityPaths === 'string') {
131
+ // Single path - avoid array allocation
132
+ const propDef = shape[subEntityPaths];
133
+ obj[subEntityPaths] = parseEntities(obj[subEntityPaths], propDef as ComplexTypeDef, queryClient, childRefs);
134
+ } else {
135
+ // Multiple paths - iterate directly
136
+ for (const path of subEntityPaths) {
137
+ const propDef = shape[path];
138
+ obj[path] = parseEntities(obj[path], propDef as ComplexTypeDef, queryClient, childRefs);
139
+ }
140
+ }
141
+ }
142
+
143
+ // Handle entity replacement (entities get cached and replaced with proxies)
144
+ if (mask & Mask.ENTITY) {
145
+ const entityDef = objectShape as EntityDef;
146
+ const typename = entityDef.typenameValue;
147
+ const id = obj[entityDef.idField];
148
+
149
+ const desc = `${typename}:${id}`;
150
+ const key = hashValue(desc);
151
+
152
+ // Add this entity's key to the parent's entityRefs (if provided)
153
+ if (entityRefs !== undefined) {
154
+ entityRefs.add(key);
155
+ }
156
+
157
+ return queryClient.saveEntity(key, obj, entityDef, childRefs).proxy;
158
+ }
159
+
160
+ // Return the processed object (even if not an entity)
161
+ return obj;
162
+ }
163
+
164
+ export function parseEntities(
165
+ value: unknown,
166
+ def: ComplexTypeDef,
167
+ queryClient: QueryClient,
168
+ entityRefs?: Set<number>,
169
+ ): unknown {
170
+ const valueType = typeMaskOf(value);
171
+ const defType = def.mask;
172
+
173
+ // Skip primitives and incompatible types - they can't contain entities
174
+ // Note: We silently return incompatible values rather than erroring
175
+ if (valueType < Mask.OBJECT || (defType & valueType) === 0) {
176
+ return value;
177
+ }
178
+
179
+ // Handle unions first - they can contain multiple types, and all of the union
180
+ // logic is handled above, so we return early here if it's a union
181
+ if ((defType & Mask.UNION) !== 0) {
182
+ return parseUnionEntities(
183
+ valueType,
184
+ value as Record<string, unknown> | unknown[],
185
+ def as UnionDef,
186
+ queryClient,
187
+ entityRefs,
188
+ );
189
+ }
190
+
191
+ // If it's not a union, AND the value IS an array, then the definition must
192
+ // be an ArrayDef, so we can cast safely here
193
+ if (valueType === Mask.ARRAY) {
194
+ return parseArrayEntities(value as unknown[], (def as ArrayDef).shape as ComplexTypeDef, queryClient, entityRefs);
195
+ }
196
+
197
+ // Now we know the value is an object, so def must be a RecordDef, ObjectDef
198
+ // or EntityDef. We first check to see if it's a RecordDef, and if so, we can
199
+ // cast it here and return early.
200
+ if ((defType & Mask.RECORD) !== 0) {
201
+ return parseRecordEntities(
202
+ value as Record<string, unknown>,
203
+ (def as RecordDef).shape as ComplexTypeDef,
204
+ queryClient,
205
+ entityRefs,
206
+ );
207
+ }
208
+
209
+ // Now we know the def is an ObjectDef or EntityDef. These are both handled
210
+ // the same way _mostly_, with Entities just returning a proxy instead of the
211
+ // object itself
212
+ return parseObjectEntities(value as Record<string, unknown>, def as ObjectDef | EntityDef, queryClient, entityRefs);
213
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Path interpolation utilities for URL templates with parameter substitution.
3
+ *
4
+ * Converts path templates like "/users/{userId}/posts/{postId}" into functions
5
+ * that efficiently interpolate parameter values.
6
+ *
7
+ * The implementation pre-parses the path template once into segments and parameter
8
+ * keys, then uses simple string concatenation at runtime for optimal performance.
9
+ */
10
+
11
+ export type PathInterpolator = (params: Record<string, any>) => string;
12
+
13
+ /**
14
+ * Creates an optimized path interpolation function from a URL template.
15
+ *
16
+ * The template uses curly braces for parameters (e.g., "/items/{id}").
17
+ * Parameter values are URL-encoded when interpolated. Any parameters not
18
+ * found in the path template are appended as query string parameters.
19
+ *
20
+ * @param pathTemplate - URL template with {paramName} placeholders
21
+ * @returns Function that interpolates parameters into the path with search params
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * const interpolate = createPathInterpolator('/users/{userId}/posts/{postId}');
26
+ * const url = interpolate({ userId: '123', postId: '456', page: 2, limit: 10 });
27
+ * // Returns: "/users/123/posts/456?page=2&limit=10"
28
+ * ```
29
+ */
30
+ export function createPathInterpolator(pathTemplate: string): PathInterpolator {
31
+ // Pre-parse path into segments and param keys (parse once, concatenate many times)
32
+ const segments: string[] = [];
33
+ const paramKeys: string[] = [];
34
+ const paramKeysSet = new Set<string>();
35
+ let lastIndex = 0;
36
+ const paramRegex = /\[([^\]]+)\]/g;
37
+ let match: RegExpExecArray | null;
38
+
39
+ while ((match = paramRegex.exec(pathTemplate)) !== null) {
40
+ segments.push(pathTemplate.slice(lastIndex, match.index));
41
+ paramKeys.push(match[1]);
42
+ paramKeysSet.add(match[1]);
43
+ lastIndex = paramRegex.lastIndex;
44
+ }
45
+ segments.push(pathTemplate.slice(lastIndex));
46
+
47
+ // Return optimized interpolation function with pre-parsed segments
48
+ return (params: Record<string, any>): string => {
49
+ // Build the path with interpolated path parameters
50
+ let result = segments[0];
51
+ for (let i = 0; i < paramKeys.length; i++) {
52
+ result += encodeURIComponent(String(params[paramKeys[i]])) + segments[i + 1];
53
+ }
54
+
55
+ // Collect remaining parameters as search params
56
+ let searchParams: URLSearchParams | null = null;
57
+ for (const key in params) {
58
+ if (!paramKeysSet.has(key) && params[key] !== undefined) {
59
+ if (searchParams === null) {
60
+ searchParams = new URLSearchParams();
61
+ }
62
+
63
+ searchParams.append(key, String(params[key]));
64
+ }
65
+ }
66
+
67
+ // Append search params if any exist
68
+ if (searchParams !== null) {
69
+ result += '?' + searchParams.toString();
70
+ }
71
+
72
+ return result;
73
+ };
74
+ }
package/src/proxy.ts ADDED
@@ -0,0 +1,257 @@
1
+ import { typeError } from './errors.js';
2
+ import { getFormat } from './typeDefs.js';
3
+ import {
4
+ ARRAY_KEY,
5
+ ArrayDef,
6
+ ComplexTypeDef,
7
+ EntityDef,
8
+ Mask,
9
+ ObjectDef,
10
+ RECORD_KEY,
11
+ ObjectFieldTypeDef,
12
+ UnionDef,
13
+ TypeDef,
14
+ } from './types.js';
15
+ import { extractShape, typeMaskOf } from './utils.js';
16
+ import { PreloadedEntityRecord } from './EntityMap.js';
17
+
18
+ const entries = Object.entries;
19
+
20
+ const PROXY_BRAND = new WeakSet();
21
+
22
+ function parseUnionValue(
23
+ valueType: number,
24
+ value: Record<string, unknown> | unknown[],
25
+ unionDef: UnionDef,
26
+ path: string,
27
+ ): unknown {
28
+ if (valueType === Mask.ARRAY) {
29
+ const shape = unionDef.shape![ARRAY_KEY];
30
+
31
+ if (shape === undefined || typeof shape === 'number') {
32
+ return value;
33
+ }
34
+
35
+ return parseArrayValue(value as unknown[], shape, path);
36
+ } else {
37
+ // Use the cached typename field from the union definition
38
+ const typenameField = unionDef.typenameField;
39
+ const typename = typenameField ? (value as Record<string, unknown>)[typenameField] : undefined;
40
+
41
+ if (typename === undefined || typeof typename !== 'string') {
42
+ const recordShape = unionDef.shape![RECORD_KEY];
43
+
44
+ if (recordShape === undefined || typeof recordShape === 'number') {
45
+ return value;
46
+ }
47
+
48
+ return parseRecordValue(value as Record<string, unknown>, recordShape as ComplexTypeDef, path);
49
+ }
50
+
51
+ const matchingDef = unionDef.shape![typename];
52
+
53
+ if (matchingDef === undefined || typeof matchingDef === 'number') {
54
+ return value;
55
+ }
56
+
57
+ return parseObjectValue(value as Record<string, unknown>, matchingDef as ObjectDef | EntityDef, path);
58
+ }
59
+ }
60
+
61
+ export function parseArrayValue(array: unknown[], arrayShape: TypeDef, path: string) {
62
+ for (let i = 0; i < array.length; i++) {
63
+ array[i] = parseValue(array[i], arrayShape, `${path}[${i}]`);
64
+ }
65
+
66
+ return array;
67
+ }
68
+
69
+ export function parseRecordValue(record: Record<string, unknown>, recordShape: ComplexTypeDef, path: string) {
70
+ for (const [key, value] of entries(record)) {
71
+ record[key] = parseValue(value, recordShape, `${path}["${key}"]`);
72
+ }
73
+
74
+ return record;
75
+ }
76
+
77
+ export function parseObjectValue(object: Record<string, unknown>, objectShape: ObjectDef | EntityDef, path: string) {
78
+ if (PROXY_BRAND.has(object)) {
79
+ // Is an entity proxy, so return it directly
80
+ return object;
81
+ }
82
+
83
+ const shape = extractShape(objectShape);
84
+
85
+ for (const [key, propShape] of entries(shape)) {
86
+ // parse and replace the property in place
87
+ object[key] = parseValue(object[key], propShape, `${path}.${key}`);
88
+ }
89
+
90
+ return object;
91
+ }
92
+
93
+ export function parseValue(value: unknown, propDef: ObjectFieldTypeDef, path: string): unknown {
94
+ // Handle Set-based constants/enums
95
+ if (propDef instanceof Set) {
96
+ if (!propDef.has(value as string | boolean | number)) {
97
+ throw typeError(path, propDef as any, value);
98
+ }
99
+ return value;
100
+ }
101
+
102
+ switch (typeof propDef) {
103
+ case 'string':
104
+ if (value !== propDef) {
105
+ throw typeError(path, propDef, value);
106
+ }
107
+
108
+ return value;
109
+
110
+ // handle primitives
111
+ case 'number': {
112
+ let valueType = typeMaskOf(value);
113
+
114
+ if ((propDef & valueType) === 0) {
115
+ throw typeError(path, propDef, value);
116
+ }
117
+
118
+ if ((propDef & Mask.HAS_NUMBER_FORMAT) !== 0 && valueType === Mask.NUMBER) {
119
+ return getFormat(propDef)(value);
120
+ }
121
+
122
+ if ((propDef & Mask.HAS_STRING_FORMAT) !== 0 && valueType === Mask.STRING) {
123
+ return getFormat(propDef)(value);
124
+ }
125
+ return value;
126
+ }
127
+
128
+ // handle complex objects
129
+ default: {
130
+ // Note: Keep in mind that at this point, we're using `valueType`
131
+ // primarily, so some of the logic is "reversed" from the above where
132
+ // we use the `propDef` type primarily
133
+ let valueType = typeMaskOf(value);
134
+ const propMask = propDef.mask;
135
+
136
+ // Check if the value type is allowed by the propMask
137
+ // Also check if it's in a values set (for enums/constants stored in ValidatorDef)
138
+ if ((propMask & valueType) === 0 && !propDef.values?.has(value as string | boolean | number)) {
139
+ throw typeError(path, propMask, value);
140
+ }
141
+
142
+ if (valueType < Mask.OBJECT) {
143
+ if ((propMask & Mask.HAS_NUMBER_FORMAT) !== 0 && valueType === Mask.NUMBER) {
144
+ return getFormat(propMask)(value);
145
+ }
146
+
147
+ if ((propMask & Mask.HAS_STRING_FORMAT) !== 0 && valueType === Mask.STRING) {
148
+ return getFormat(propMask)(value);
149
+ }
150
+
151
+ // value is a primitive, it has already passed the mask so return it now
152
+ return value;
153
+ }
154
+
155
+ if ((valueType & Mask.UNION) !== 0) {
156
+ return parseUnionValue(valueType, value as Record<string, unknown> | unknown[], propDef as UnionDef, path);
157
+ }
158
+
159
+ if (valueType === Mask.ARRAY) {
160
+ return parseArrayValue(value as unknown[], propDef.shape as ComplexTypeDef, path);
161
+ }
162
+
163
+ return parseObjectValue(value as Record<string, unknown>, propDef as ObjectDef | EntityDef, path);
164
+ }
165
+ }
166
+ }
167
+
168
+ const CustomNodeInspect = Symbol.for('nodejs.util.inspect.custom');
169
+
170
+ export function createEntityProxy(
171
+ id: number,
172
+ entityRecord: PreloadedEntityRecord,
173
+ def: ObjectDef | EntityDef,
174
+ desc?: string,
175
+ ): Record<string, unknown> {
176
+ // Cache for nested proxies - each proxy gets its own cache
177
+ const shape = extractShape(def);
178
+
179
+ const toJSON = () => ({
180
+ __entityRef: id,
181
+ });
182
+
183
+ const handler: ProxyHandler<any> = {
184
+ get(target, prop) {
185
+ // Handle toJSON for serialization
186
+ if (prop === 'toJSON') {
187
+ return toJSON;
188
+ }
189
+
190
+ const { signal, cache } = entityRecord;
191
+ const obj = signal.value;
192
+
193
+ // Check cache first, BEFORE any expensive checks
194
+ if (cache.has(prop)) {
195
+ return cache.get(prop);
196
+ }
197
+
198
+ let value = obj[prop as string];
199
+ let propDef = shape[prop as string];
200
+
201
+ if (!Object.hasOwnProperty.call(shape, prop)) {
202
+ return value;
203
+ }
204
+
205
+ const parsed = parseValue(value, propDef, `[[${desc}]].${prop as string}`);
206
+
207
+ cache.set(prop, parsed);
208
+
209
+ return parsed;
210
+ },
211
+
212
+ has(target, prop) {
213
+ return prop in shape;
214
+ },
215
+
216
+ ownKeys(target) {
217
+ const keys = Object.keys(shape);
218
+ // Add typename field if it exists on the definition
219
+ const typenameField = (def as ObjectDef | EntityDef).typenameField;
220
+ if (typenameField && !keys.includes(typenameField)) {
221
+ keys.push(typenameField);
222
+ }
223
+ return keys;
224
+ },
225
+
226
+ getOwnPropertyDescriptor(target, prop) {
227
+ const typenameField = (def as ObjectDef | EntityDef).typenameField;
228
+ if (prop in shape || prop === typenameField) {
229
+ return {
230
+ enumerable: true,
231
+ configurable: true,
232
+ };
233
+ }
234
+ return undefined;
235
+ },
236
+ };
237
+
238
+ const proxy = new Proxy(
239
+ {
240
+ [CustomNodeInspect]: () => {
241
+ return Object.keys(shape).reduce(
242
+ (acc, key) => {
243
+ acc[key] = proxy[key];
244
+ return acc;
245
+ },
246
+ {} as Record<string, unknown>,
247
+ );
248
+ },
249
+ },
250
+ handler,
251
+ );
252
+
253
+ // Add the proxy to the proxy brand set so we can easily identify it later
254
+ PROXY_BRAND.add(proxy);
255
+
256
+ return proxy;
257
+ }