@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/ENTITY_STORE_DESIGN.md +386 -0
- package/package.json +71 -0
- package/src/EntityMap.ts +63 -0
- package/src/QueryClient.ts +266 -0
- package/src/QueryStore.ts +314 -0
- package/src/__tests__/caching-persistence.test.ts +954 -0
- package/src/__tests__/entity-system.test.ts +552 -0
- package/src/__tests__/mock-fetch.test.ts +182 -0
- package/src/__tests__/parse-entities.test.ts +421 -0
- package/src/__tests__/path-interpolation.test.ts +225 -0
- package/src/__tests__/reactivity.test.ts +420 -0
- package/src/__tests__/rest-query-api.test.ts +564 -0
- package/src/__tests__/type-to-string.test.ts +129 -0
- package/src/__tests__/utils.ts +242 -0
- package/src/__tests__/validation-edge-cases.test.ts +820 -0
- package/src/errors.ts +124 -0
- package/src/index.ts +7 -0
- package/src/parseEntities.ts +213 -0
- package/src/pathInterpolator.ts +74 -0
- package/src/proxy.ts +257 -0
- package/src/query.ts +163 -0
- package/src/react/__tests__/basic.test.tsx +921 -0
- package/src/react/__tests__/component.test.tsx +977 -0
- package/src/react/__tests__/utils.tsx +71 -0
- package/src/typeDefs.ts +351 -0
- package/src/types.ts +121 -0
- package/src/utils.ts +66 -0
- package/tsconfig.cjs.json +14 -0
- package/tsconfig.esm.json +13 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +71 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper utilities for React tests in the query package
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface RenderCounter<Props extends Record<string, unknown>> {
|
|
8
|
+
(props: Props): React.ReactNode;
|
|
9
|
+
testId: number;
|
|
10
|
+
renderCount: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let CURRENT_ID = 0;
|
|
14
|
+
|
|
15
|
+
export type ComponentType<P = any> = (props: P) => React.ReactNode;
|
|
16
|
+
export type HOC<InProps = any, OutProps = InProps> = (Component: ComponentType<InProps>) => ComponentType<OutProps>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The wrapper passed to createRenderCounter is a HOC that will wrap the component
|
|
20
|
+
* with additional functionality. The reason we don't pass a component directly is
|
|
21
|
+
* because introducing additional components would mess with the real render counts.
|
|
22
|
+
*/
|
|
23
|
+
const EmptyWrapper: HOC = Component => props => Component(props);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates a component that tracks how many times it renders.
|
|
27
|
+
* Useful for verifying that components only re-render when expected.
|
|
28
|
+
*/
|
|
29
|
+
export function createRenderCounter<Props extends Record<string, unknown>>(
|
|
30
|
+
Component: (props: Props) => React.ReactNode,
|
|
31
|
+
wrapper: HOC<Props> = EmptyWrapper,
|
|
32
|
+
): RenderCounter<Props> {
|
|
33
|
+
const id = CURRENT_ID++;
|
|
34
|
+
|
|
35
|
+
const RenderCounterComponent = wrapper((props: Props) => {
|
|
36
|
+
RenderCounterComponent.renderCount++;
|
|
37
|
+
|
|
38
|
+
// Call the component manually so it's not a separate React component
|
|
39
|
+
const children = Component(props);
|
|
40
|
+
|
|
41
|
+
return <div data-testid={id}>{children}</div>;
|
|
42
|
+
}) as RenderCounter<Props>;
|
|
43
|
+
|
|
44
|
+
RenderCounterComponent.testId = id;
|
|
45
|
+
RenderCounterComponent.renderCount = 0;
|
|
46
|
+
|
|
47
|
+
return RenderCounterComponent;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Mock user data factory
|
|
52
|
+
*/
|
|
53
|
+
export function createUser(id: number, overrides?: Partial<{ name: string; email: string }>) {
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
name: overrides?.name ?? `User ${id}`,
|
|
57
|
+
email: overrides?.email ?? `user${id}@example.com`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Mock post data factory
|
|
63
|
+
*/
|
|
64
|
+
export function createPost(id: number, authorId: number, overrides?: Partial<{ title: string; content: string }>) {
|
|
65
|
+
return {
|
|
66
|
+
id,
|
|
67
|
+
authorId,
|
|
68
|
+
title: overrides?.title ?? `Post ${id}`,
|
|
69
|
+
content: overrides?.content ?? `Content for post ${id}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
package/src/typeDefs.ts
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import {
|
|
2
|
+
APITypes,
|
|
3
|
+
ARRAY_KEY,
|
|
4
|
+
ArrayDef,
|
|
5
|
+
ComplexTypeDef,
|
|
6
|
+
EntityDef,
|
|
7
|
+
Mask,
|
|
8
|
+
ObjectDef,
|
|
9
|
+
ObjectShape,
|
|
10
|
+
RECORD_KEY,
|
|
11
|
+
RecordDef,
|
|
12
|
+
TypeDef,
|
|
13
|
+
ObjectFieldTypeDef,
|
|
14
|
+
UnionDef,
|
|
15
|
+
UnionTypeDefs,
|
|
16
|
+
} from './types.js';
|
|
17
|
+
import { extractShape, extractShapeMetadata } from './utils.js';
|
|
18
|
+
|
|
19
|
+
export class ValidatorDef<T> {
|
|
20
|
+
private _optional: ValidatorDef<T | undefined> | undefined;
|
|
21
|
+
private _nullable: ValidatorDef<T | null> | undefined;
|
|
22
|
+
private _nullish: ValidatorDef<T | null | undefined> | undefined;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
public mask: Mask,
|
|
26
|
+
public shape: ObjectFieldTypeDef | ObjectShape | ((t: APITypes) => ObjectShape) | UnionTypeDefs | undefined,
|
|
27
|
+
public subEntityPaths: undefined | string | string[] = undefined,
|
|
28
|
+
public values: Set<string | boolean | number> | undefined = undefined,
|
|
29
|
+
public typenameField: string | undefined = undefined,
|
|
30
|
+
public typenameValue: string | undefined = undefined,
|
|
31
|
+
public idField: string | undefined = undefined,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
get optional(): ValidatorDef<T | undefined> {
|
|
35
|
+
if (this._optional === undefined) {
|
|
36
|
+
this._optional = new ValidatorDef(
|
|
37
|
+
this.mask | Mask.UNDEFINED,
|
|
38
|
+
this.shape,
|
|
39
|
+
this.subEntityPaths,
|
|
40
|
+
this.values,
|
|
41
|
+
this.typenameField,
|
|
42
|
+
this.typenameValue,
|
|
43
|
+
this.idField,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return this._optional;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get nullable(): ValidatorDef<T | null> {
|
|
50
|
+
if (this._nullable === undefined) {
|
|
51
|
+
this._nullable = new ValidatorDef(
|
|
52
|
+
this.mask | Mask.NULL,
|
|
53
|
+
this.shape,
|
|
54
|
+
this.subEntityPaths,
|
|
55
|
+
this.values,
|
|
56
|
+
this.typenameField,
|
|
57
|
+
this.typenameValue,
|
|
58
|
+
this.idField,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return this._nullable;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get nullish(): ValidatorDef<T | null | undefined> {
|
|
65
|
+
if (this._nullish === undefined) {
|
|
66
|
+
this._nullish = new ValidatorDef(
|
|
67
|
+
this.mask | Mask.UNDEFINED | Mask.NULL,
|
|
68
|
+
this.shape,
|
|
69
|
+
this.subEntityPaths,
|
|
70
|
+
this.values,
|
|
71
|
+
this.typenameField,
|
|
72
|
+
this.typenameValue,
|
|
73
|
+
this.idField,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return this._nullish;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// -----------------------------------------------------------------------------
|
|
81
|
+
// Complex Type Definitions
|
|
82
|
+
// -----------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
export function defineArray<T extends TypeDef>(shape: T): ArrayDef<T> {
|
|
85
|
+
let mask = Mask.ARRAY;
|
|
86
|
+
|
|
87
|
+
// Propagate HAS_SUB_ENTITY flag if the shape contains entities
|
|
88
|
+
if (shape instanceof ValidatorDef && (shape.mask & (Mask.ENTITY | Mask.HAS_SUB_ENTITY)) !== 0) {
|
|
89
|
+
mask |= Mask.HAS_SUB_ENTITY;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return new ValidatorDef(mask, shape) as unknown as ArrayDef<T>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function defineRecord<T extends TypeDef>(shape: T): RecordDef<T> {
|
|
96
|
+
// The mask should be OBJECT | RECORD so that values match when compared
|
|
97
|
+
let mask = Mask.RECORD | Mask.OBJECT;
|
|
98
|
+
|
|
99
|
+
// Propagate HAS_SUB_ENTITY flag if the shape contains entities
|
|
100
|
+
if (shape instanceof ValidatorDef && (shape.mask & (Mask.ENTITY | Mask.HAS_SUB_ENTITY)) !== 0) {
|
|
101
|
+
mask |= Mask.HAS_SUB_ENTITY;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return new ValidatorDef(mask, shape) as unknown as RecordDef<T>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function defineObject<T extends ObjectShape>(shape: T): ObjectDef<T> {
|
|
108
|
+
const def = new ValidatorDef(Mask.OBJECT, shape);
|
|
109
|
+
|
|
110
|
+
extractShapeMetadata(def, shape);
|
|
111
|
+
|
|
112
|
+
return def as unknown as ObjectDef<T>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const addShapeToUnion = (
|
|
116
|
+
shape: UnionTypeDefs,
|
|
117
|
+
definition: ObjectDef | EntityDef | RecordDef | UnionDef | ArrayDef,
|
|
118
|
+
unionTypenameField: string | undefined,
|
|
119
|
+
) => {
|
|
120
|
+
const mask = definition.mask;
|
|
121
|
+
|
|
122
|
+
if ((mask & Mask.UNION) !== 0) {
|
|
123
|
+
// Merge nested union into parent union
|
|
124
|
+
const nestedUnion = definition as UnionDef;
|
|
125
|
+
|
|
126
|
+
// Check typename field consistency
|
|
127
|
+
if (nestedUnion.typenameField !== undefined) {
|
|
128
|
+
if (unionTypenameField !== undefined && unionTypenameField !== nestedUnion.typenameField) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Union typename field conflict: Cannot merge unions with different typename fields ('${unionTypenameField}' vs '${nestedUnion.typenameField}')`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
unionTypenameField = nestedUnion.typenameField;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Merge nested union's shape into parent
|
|
137
|
+
if (nestedUnion.shape !== undefined) {
|
|
138
|
+
for (const key of [...Object.keys(nestedUnion.shape), ARRAY_KEY, RECORD_KEY] as const) {
|
|
139
|
+
// Check for conflicts
|
|
140
|
+
const value = nestedUnion.shape[key];
|
|
141
|
+
|
|
142
|
+
if (shape[key] !== undefined && shape[key] !== value) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Union merge conflict: Duplicate typename value '${String(key)}' found when merging nested unions`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// coerce type because we know the value is the same type as the key
|
|
149
|
+
shape[key] = value as any;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return unionTypenameField;
|
|
154
|
+
} else if ((mask & Mask.ARRAY) !== 0) {
|
|
155
|
+
if (shape[ARRAY_KEY] !== undefined) {
|
|
156
|
+
throw new Error('Array shape already defined');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
shape[ARRAY_KEY] = definition.shape as TypeDef;
|
|
160
|
+
|
|
161
|
+
return unionTypenameField;
|
|
162
|
+
} else if ((mask & Mask.RECORD) !== 0) {
|
|
163
|
+
if (shape[RECORD_KEY] !== undefined) {
|
|
164
|
+
throw new Error('Record shape already defined');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
shape[RECORD_KEY] = definition.shape as TypeDef;
|
|
168
|
+
|
|
169
|
+
return unionTypenameField;
|
|
170
|
+
} else {
|
|
171
|
+
// Make sure the type is fully extracted, so we can get the typename field and value
|
|
172
|
+
extractShape(definition);
|
|
173
|
+
|
|
174
|
+
// definition is ObjectDef | EntityDef
|
|
175
|
+
const typenameField = (definition as ObjectDef).typenameField;
|
|
176
|
+
const typename = (definition as ObjectDef).typenameValue;
|
|
177
|
+
|
|
178
|
+
if (typename === undefined) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
'Object definitions must have a typename to be in a union with other objects, records, or arrays',
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (unionTypenameField !== undefined && typenameField !== unionTypenameField) {
|
|
185
|
+
throw new Error('Object definitions must have the same typename field to be in the same union');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
shape[typename] = definition as ObjectDef;
|
|
189
|
+
|
|
190
|
+
return typenameField;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
function defineUnion<T extends readonly TypeDef[]>(...types: T): UnionDef<T> {
|
|
195
|
+
let mask = 0;
|
|
196
|
+
let definition: ObjectDef | EntityDef | RecordDef | UnionDef | ArrayDef | undefined;
|
|
197
|
+
let shape: UnionTypeDefs | undefined;
|
|
198
|
+
let values: Set<string | boolean | number> | undefined;
|
|
199
|
+
let unionTypenameField: string | undefined;
|
|
200
|
+
|
|
201
|
+
for (const type of types) {
|
|
202
|
+
if (typeof type === 'number') {
|
|
203
|
+
mask |= type;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const isSet = type instanceof Set;
|
|
208
|
+
const typeValues = isSet ? type : type.values;
|
|
209
|
+
|
|
210
|
+
// Handle Set-based constants/enums
|
|
211
|
+
if (typeValues !== undefined) {
|
|
212
|
+
if (values === undefined) {
|
|
213
|
+
values = new Set(typeValues);
|
|
214
|
+
} else {
|
|
215
|
+
for (const val of typeValues) {
|
|
216
|
+
values.add(val);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (isSet) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// We know it's a complex type at this point because if it was a Set,
|
|
226
|
+
// we would have already handled it above.
|
|
227
|
+
const typeDef = type as ComplexTypeDef;
|
|
228
|
+
|
|
229
|
+
mask |= typeDef.mask;
|
|
230
|
+
|
|
231
|
+
if (definition === undefined) {
|
|
232
|
+
definition = typeDef;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (shape === undefined) {
|
|
237
|
+
shape = Object.create(null) as UnionTypeDefs;
|
|
238
|
+
|
|
239
|
+
unionTypenameField = addShapeToUnion(shape, definition, unionTypenameField);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
unionTypenameField = addShapeToUnion(shape, typeDef, unionTypenameField);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// It was a union of primitives, so return the mask
|
|
246
|
+
if (definition === undefined && values === undefined) {
|
|
247
|
+
// This type coercion is incorrect, but we can't return the mask as a Mask
|
|
248
|
+
// because that loses the type information about the union, which breaks
|
|
249
|
+
// inference.
|
|
250
|
+
//
|
|
251
|
+
// TODO: Figure out how to make this correct type-wise
|
|
252
|
+
return mask as unknown as UnionDef<T>;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return new ValidatorDef(
|
|
256
|
+
mask | Mask.UNION,
|
|
257
|
+
shape ?? definition?.shape,
|
|
258
|
+
undefined,
|
|
259
|
+
values,
|
|
260
|
+
unionTypenameField,
|
|
261
|
+
) as UnionDef;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// -----------------------------------------------------------------------------
|
|
265
|
+
// Marker Functions
|
|
266
|
+
// -----------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
function defineTypename<T extends string>(value: T): T {
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function defineConst<T extends string | boolean | number>(value: T): Set<T> {
|
|
273
|
+
return new Set([value]);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function defineEnum<T extends readonly (string | boolean | number)[]>(...values: T): Set<T[number]> {
|
|
277
|
+
return new Set(values as unknown as T[number][]);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// -----------------------------------------------------------------------------
|
|
281
|
+
// Formatted Values
|
|
282
|
+
// -----------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
const FORMAT_MASK_SHIFT = 16;
|
|
285
|
+
|
|
286
|
+
let nextFormatId = 0;
|
|
287
|
+
const FORMAT_PARSERS: ((value: unknown) => unknown)[] = [];
|
|
288
|
+
const FORMAT_SERIALIZERS: ((value: unknown) => unknown)[] = [];
|
|
289
|
+
const FORMAT_MAP = new Map<string, number>();
|
|
290
|
+
|
|
291
|
+
function defineFormatted(format: string): number {
|
|
292
|
+
const mask = FORMAT_MAP.get(format);
|
|
293
|
+
|
|
294
|
+
if (mask === undefined) {
|
|
295
|
+
throw new Error(`Format ${format} not registered`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return mask;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function getFormat(mask: number): (value: unknown) => unknown {
|
|
302
|
+
const formatId = mask >> FORMAT_MASK_SHIFT;
|
|
303
|
+
|
|
304
|
+
return FORMAT_PARSERS[formatId];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function registerFormat<Input extends string | boolean, T>(
|
|
308
|
+
name: string,
|
|
309
|
+
type: Input extends string ? Mask.STRING : Mask.BOOLEAN,
|
|
310
|
+
parse: (value: Input) => T,
|
|
311
|
+
serialize: (value: T) => Input,
|
|
312
|
+
) {
|
|
313
|
+
const maskId = nextFormatId++;
|
|
314
|
+
FORMAT_PARSERS[maskId] = parse as (value: unknown) => unknown;
|
|
315
|
+
FORMAT_SERIALIZERS[maskId] = serialize as (value: unknown) => unknown;
|
|
316
|
+
|
|
317
|
+
const shiftedId = maskId << FORMAT_MASK_SHIFT;
|
|
318
|
+
const formatMask = type === Mask.STRING ? Mask.HAS_STRING_FORMAT : Mask.HAS_NUMBER_FORMAT;
|
|
319
|
+
const mask = shiftedId | type | formatMask;
|
|
320
|
+
|
|
321
|
+
FORMAT_MAP.set(name, mask);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// -----------------------------------------------------------------------------
|
|
325
|
+
// Entity Definitions
|
|
326
|
+
// -----------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
export function entity<T extends ObjectShape>(shape: (t: APITypes) => T): EntityDef<T> {
|
|
329
|
+
return new ValidatorDef(
|
|
330
|
+
// The mask should be OBJECT | ENTITY so that values match when compared
|
|
331
|
+
Mask.ENTITY | Mask.OBJECT,
|
|
332
|
+
shape,
|
|
333
|
+
) as unknown as EntityDef<T>;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export const t: APITypes = {
|
|
337
|
+
format: defineFormatted,
|
|
338
|
+
typename: defineTypename,
|
|
339
|
+
const: defineConst,
|
|
340
|
+
enum: defineEnum,
|
|
341
|
+
id: Mask.ID | Mask.STRING | Mask.NUMBER,
|
|
342
|
+
string: Mask.STRING,
|
|
343
|
+
number: Mask.NUMBER,
|
|
344
|
+
boolean: Mask.BOOLEAN,
|
|
345
|
+
null: Mask.NULL,
|
|
346
|
+
undefined: Mask.UNDEFINED,
|
|
347
|
+
array: defineArray,
|
|
348
|
+
object: defineObject,
|
|
349
|
+
record: defineRecord,
|
|
350
|
+
union: defineUnion,
|
|
351
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { PendingReactivePromise, ReadyReactivePromise } from 'signalium';
|
|
2
|
+
import { ReactivePromise } from 'signalium';
|
|
3
|
+
|
|
4
|
+
export const enum Mask {
|
|
5
|
+
// Fundamental types
|
|
6
|
+
UNDEFINED = 1 << 0,
|
|
7
|
+
NULL = 1 << 1,
|
|
8
|
+
NUMBER = 1 << 2,
|
|
9
|
+
STRING = 1 << 3,
|
|
10
|
+
BOOLEAN = 1 << 4,
|
|
11
|
+
OBJECT = 1 << 5,
|
|
12
|
+
ARRAY = 1 << 6,
|
|
13
|
+
ID = 1 << 7,
|
|
14
|
+
|
|
15
|
+
// Complex types
|
|
16
|
+
RECORD = 1 << 8,
|
|
17
|
+
UNION = 1 << 9,
|
|
18
|
+
ENTITY = 1 << 10,
|
|
19
|
+
|
|
20
|
+
// Flags
|
|
21
|
+
HAS_SUB_ENTITY = 1 << 11,
|
|
22
|
+
HAS_NUMBER_FORMAT = 1 << 12,
|
|
23
|
+
HAS_STRING_FORMAT = 1 << 13,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type SimpleTypeDef =
|
|
27
|
+
// Sets are constant values
|
|
28
|
+
| Set<string | boolean | number>
|
|
29
|
+
|
|
30
|
+
// Numbers are primitive type masks (potentially multiple masks combined)
|
|
31
|
+
| Mask;
|
|
32
|
+
|
|
33
|
+
export type ComplexTypeDef =
|
|
34
|
+
// Objects, arrays, records, and unions are definitions
|
|
35
|
+
ObjectDef | EntityDef | ArrayDef | RecordDef | UnionDef;
|
|
36
|
+
|
|
37
|
+
export type TypeDef = SimpleTypeDef | ComplexTypeDef;
|
|
38
|
+
|
|
39
|
+
export type ObjectFieldTypeDef = TypeDef | string;
|
|
40
|
+
|
|
41
|
+
export type ObjectShape = Record<string, ObjectFieldTypeDef>;
|
|
42
|
+
|
|
43
|
+
export const ARRAY_KEY = Symbol('array');
|
|
44
|
+
export const RECORD_KEY = Symbol('record');
|
|
45
|
+
|
|
46
|
+
export interface UnionTypeDefs {
|
|
47
|
+
[ARRAY_KEY]?: TypeDef;
|
|
48
|
+
[RECORD_KEY]?: TypeDef;
|
|
49
|
+
[key: string]: ObjectDef | EntityDef;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface BaseTypeDef {
|
|
53
|
+
mask: Mask;
|
|
54
|
+
typenameField: string;
|
|
55
|
+
typenameValue: string;
|
|
56
|
+
idField: string;
|
|
57
|
+
subEntityPaths: undefined | string | string[];
|
|
58
|
+
values: Set<string | boolean | number> | undefined;
|
|
59
|
+
|
|
60
|
+
optional: this | Mask.UNDEFINED;
|
|
61
|
+
nullable: this | Mask.NULL;
|
|
62
|
+
nullish: this | Mask.UNDEFINED | Mask.NULL;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface EntityDef<T extends ObjectShape = ObjectShape> extends BaseTypeDef {
|
|
66
|
+
mask: Mask.ENTITY;
|
|
67
|
+
shape: T | ((t: APITypes) => T);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ObjectDef<T extends ObjectShape = ObjectShape> extends BaseTypeDef {
|
|
71
|
+
mask: Mask.OBJECT;
|
|
72
|
+
shape: T;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ArrayDef<T extends TypeDef = TypeDef> extends BaseTypeDef {
|
|
76
|
+
mask: Mask.ARRAY;
|
|
77
|
+
shape: T;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface UnionDef<_T extends readonly TypeDef[] = readonly TypeDef[]> extends BaseTypeDef {
|
|
81
|
+
mask: Mask.UNION;
|
|
82
|
+
shape: UnionTypeDefs | undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface RecordDef<T extends TypeDef = TypeDef> extends BaseTypeDef {
|
|
86
|
+
mask: Mask.RECORD;
|
|
87
|
+
shape: T;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface APITypes {
|
|
91
|
+
format: (format: string) => number;
|
|
92
|
+
typename: <T extends string>(value: T) => T;
|
|
93
|
+
const: <T extends string | boolean | number>(value: T) => Set<T>;
|
|
94
|
+
enum: <T extends readonly (string | boolean | number)[]>(...values: T) => Set<T[number]>;
|
|
95
|
+
|
|
96
|
+
id: Mask.ID;
|
|
97
|
+
string: Mask.STRING;
|
|
98
|
+
number: Mask.NUMBER;
|
|
99
|
+
boolean: Mask.BOOLEAN;
|
|
100
|
+
null: Mask.NULL;
|
|
101
|
+
undefined: Mask.UNDEFINED;
|
|
102
|
+
|
|
103
|
+
array: <T extends TypeDef>(shape: T) => ArrayDef<T>;
|
|
104
|
+
|
|
105
|
+
object: <T extends ObjectShape>(shape: T) => ObjectDef<T>;
|
|
106
|
+
record: <T extends TypeDef>(shape: T) => RecordDef<T>;
|
|
107
|
+
|
|
108
|
+
union: <VS extends readonly TypeDef[]>(...types: VS) => UnionDef<VS>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type QueryResultExtensions<T> = {
|
|
112
|
+
refetch: () => Promise<T>;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export type QueryResult<T> = ReactivePromise<T> & QueryResultExtensions<T>;
|
|
116
|
+
|
|
117
|
+
export type PendingQueryResult<T> = PendingReactivePromise<T> & QueryResultExtensions<T>;
|
|
118
|
+
|
|
119
|
+
export type ReadyQueryResult<T> = ReadyReactivePromise<T> & QueryResultExtensions<T>;
|
|
120
|
+
|
|
121
|
+
export type DiscriminatedQueryResult<T> = PendingQueryResult<T> | ReadyQueryResult<T>;
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { t, ValidatorDef } from './typeDefs.js';
|
|
2
|
+
import { ComplexTypeDef, EntityDef, Mask, ObjectShape } from './types.js';
|
|
3
|
+
|
|
4
|
+
const entries = Object.entries;
|
|
5
|
+
const isArray = Array.isArray;
|
|
6
|
+
|
|
7
|
+
export function extractShapeMetadata(def: ValidatorDef<any>, shape: ObjectShape): void {
|
|
8
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
9
|
+
if (value instanceof ValidatorDef && (value.mask & (Mask.ENTITY | Mask.HAS_SUB_ENTITY)) !== 0) {
|
|
10
|
+
if (def.subEntityPaths === undefined) {
|
|
11
|
+
def.subEntityPaths = key;
|
|
12
|
+
} else if (isArray(def.subEntityPaths)) {
|
|
13
|
+
def.subEntityPaths.push(key);
|
|
14
|
+
} else {
|
|
15
|
+
def.subEntityPaths = [def.subEntityPaths, key];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Check if this is a typename field (plain string value)
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
if (def.typenameField !== undefined) {
|
|
21
|
+
throw new Error(`Duplicate typename field: ${key}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def.typenameField = key;
|
|
25
|
+
def.typenameValue = value;
|
|
26
|
+
}
|
|
27
|
+
// Check if this is an id field (Mask.ID)
|
|
28
|
+
else if (typeof value === 'number' && (value & Mask.ID) !== 0) {
|
|
29
|
+
if (def.idField !== undefined) {
|
|
30
|
+
throw new Error(`Duplicate id field: ${key}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def.idField = key;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function extractShape<T extends ComplexTypeDef>(def: T): T extends EntityDef ? ObjectShape : T['shape'] {
|
|
39
|
+
let shape = def.shape;
|
|
40
|
+
|
|
41
|
+
if (typeof shape === 'function') {
|
|
42
|
+
shape = def.shape = shape(t);
|
|
43
|
+
extractShapeMetadata(def as ValidatorDef<any>, shape);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return shape as T extends EntityDef ? ObjectShape : T['shape'];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function typeMaskOf(value: unknown): Mask {
|
|
50
|
+
if (value === null) return Mask.NULL;
|
|
51
|
+
|
|
52
|
+
switch (typeof value) {
|
|
53
|
+
case 'number':
|
|
54
|
+
return Mask.NUMBER;
|
|
55
|
+
case 'string':
|
|
56
|
+
return Mask.STRING;
|
|
57
|
+
case 'boolean':
|
|
58
|
+
return Mask.BOOLEAN;
|
|
59
|
+
case 'undefined':
|
|
60
|
+
return Mask.UNDEFINED;
|
|
61
|
+
case 'object':
|
|
62
|
+
return isArray(value) ? Mask.ARRAY : Mask.OBJECT;
|
|
63
|
+
default:
|
|
64
|
+
throw new Error(`Invalid type: ${typeof value}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist/cjs",
|
|
5
|
+
"declaration": false,
|
|
6
|
+
"declarationMap": false,
|
|
7
|
+
"sourceMap": true,
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"module": "commonjs",
|
|
10
|
+
"moduleResolution": "node"
|
|
11
|
+
},
|
|
12
|
+
"include": ["src"],
|
|
13
|
+
"exclude": ["**/__tests__"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist/esm",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"jsx": "react-jsx"
|
|
10
|
+
},
|
|
11
|
+
"include": ["src"],
|
|
12
|
+
"exclude": ["**/__tests__"]
|
|
13
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist/esm",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"declarationMap": true,
|
|
7
|
+
"emitDeclarationOnly": false,
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"baseUrl": ".",
|
|
10
|
+
"paths": {
|
|
11
|
+
"signalium": ["../signalium/src/index.ts"],
|
|
12
|
+
"signalium/utils": ["../signalium/src/utils.ts"],
|
|
13
|
+
"signalium/config": ["../signalium/src/config.ts"],
|
|
14
|
+
"signalium/debug": ["../signalium/src/debug.ts"],
|
|
15
|
+
"signalium/react": ["../signalium/src/react/index.ts"],
|
|
16
|
+
"signalium/transform": ["../signalium/src/transform/index.ts"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"include": ["src", "vitest.config.ts"]
|
|
20
|
+
}
|