@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
package/src/query.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { getContext, reactive } from 'signalium';
|
|
2
|
+
import {
|
|
3
|
+
APITypes,
|
|
4
|
+
ArrayDef,
|
|
5
|
+
DiscriminatedQueryResult,
|
|
6
|
+
EntityDef,
|
|
7
|
+
Mask,
|
|
8
|
+
ObjectDef,
|
|
9
|
+
RecordDef,
|
|
10
|
+
ObjectFieldTypeDef,
|
|
11
|
+
UnionDef,
|
|
12
|
+
} from './types.js';
|
|
13
|
+
import { QueryCacheOptions, QueryClientContext, QueryContext, QueryDefinition } from './QueryClient.js';
|
|
14
|
+
import { entity, t, ValidatorDef } from './typeDefs.js';
|
|
15
|
+
import { createPathInterpolator } from './pathInterpolator.js';
|
|
16
|
+
|
|
17
|
+
type ExtractPrimitiveTypeFromMask<T extends number> = T extends Mask.UNDEFINED
|
|
18
|
+
? undefined
|
|
19
|
+
: T extends Mask.NULL
|
|
20
|
+
? null
|
|
21
|
+
: T extends Mask.NUMBER
|
|
22
|
+
? number
|
|
23
|
+
: T extends Mask.STRING
|
|
24
|
+
? string
|
|
25
|
+
: T extends Mask.BOOLEAN
|
|
26
|
+
? boolean
|
|
27
|
+
: T extends Mask.ID
|
|
28
|
+
? string
|
|
29
|
+
: never;
|
|
30
|
+
|
|
31
|
+
export type ExtractType<T extends ObjectFieldTypeDef | string> = T extends number
|
|
32
|
+
? ExtractPrimitiveTypeFromMask<T>
|
|
33
|
+
: T extends string
|
|
34
|
+
? T
|
|
35
|
+
: T extends Set<infer TSet>
|
|
36
|
+
? TSet
|
|
37
|
+
: T extends ObjectDef<infer S>
|
|
38
|
+
? Prettify<ExtractTypesFromShape<S>>
|
|
39
|
+
: T extends EntityDef<infer S>
|
|
40
|
+
? Prettify<ExtractTypesFromShape<S>>
|
|
41
|
+
: T extends ArrayDef<infer S>
|
|
42
|
+
? ExtractType<S>[]
|
|
43
|
+
: T extends RecordDef<infer S>
|
|
44
|
+
? Record<string, ExtractType<S>>
|
|
45
|
+
: T extends UnionDef<infer VS>
|
|
46
|
+
? ExtractType<VS[number]>
|
|
47
|
+
: never;
|
|
48
|
+
|
|
49
|
+
type ExtractTypesFromShape<S extends Record<string, ObjectFieldTypeDef | string>> = {
|
|
50
|
+
[K in keyof S]: ExtractType<S[K]>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type IsParameter<Part> = Part extends `[${infer ParamName}]` ? ParamName : never;
|
|
54
|
+
type FilteredParts<Path> = Path extends `${infer PartA}/${infer PartB}`
|
|
55
|
+
? IsParameter<PartA> | FilteredParts<PartB>
|
|
56
|
+
: IsParameter<Path>;
|
|
57
|
+
type ParamValue<Key> = Key extends `...${infer Anything}` ? (string | number)[] : string | number;
|
|
58
|
+
type RemovePrefixDots<Key> = Key extends `...${infer Name}` ? Name : Key;
|
|
59
|
+
type PathParams<Path> = {
|
|
60
|
+
[Key in FilteredParts<Path> as RemovePrefixDots<Key>]: ParamValue<Key>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
interface RESTQueryDefinition {
|
|
64
|
+
path: string;
|
|
65
|
+
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
66
|
+
searchParams?: Record<string, ObjectFieldTypeDef>;
|
|
67
|
+
response: Record<string, ObjectFieldTypeDef> | ObjectFieldTypeDef;
|
|
68
|
+
|
|
69
|
+
cache?: QueryCacheOptions;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type ExtractTypesFromObjectOrTypeDef<S extends Record<string, ObjectFieldTypeDef> | ObjectFieldTypeDef | undefined> =
|
|
73
|
+
S extends Record<string, ObjectFieldTypeDef>
|
|
74
|
+
? {
|
|
75
|
+
[K in keyof S]: ExtractType<S[K]>;
|
|
76
|
+
}
|
|
77
|
+
: S extends ObjectFieldTypeDef
|
|
78
|
+
? ExtractType<S>
|
|
79
|
+
: // eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
80
|
+
{};
|
|
81
|
+
|
|
82
|
+
type QueryParams<QDef extends RESTQueryDefinition> = PathParams<QDef['path']> &
|
|
83
|
+
ExtractTypesFromObjectOrTypeDef<QDef['searchParams']>;
|
|
84
|
+
|
|
85
|
+
type QueryParamsOrUndefined<QDef extends RESTQueryDefinition> =
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
87
|
+
{} extends QueryParams<QDef> ? undefined : QueryParams<QDef>;
|
|
88
|
+
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
90
|
+
type HasRequiredKeys<T> = {} extends T ? false : { [K in keyof T]: undefined } extends T ? false : true;
|
|
91
|
+
|
|
92
|
+
type Optionalize<T> = T extends object
|
|
93
|
+
? {
|
|
94
|
+
-readonly [K in keyof T as undefined extends T[K] ? never : K]: T[K];
|
|
95
|
+
} & {
|
|
96
|
+
-readonly [K in keyof T as undefined extends T[K] ? K : never]?: T[K];
|
|
97
|
+
}
|
|
98
|
+
: T;
|
|
99
|
+
|
|
100
|
+
type Prettify<T> = T extends object
|
|
101
|
+
? {
|
|
102
|
+
-readonly [K in keyof T]: T[K];
|
|
103
|
+
} & {}
|
|
104
|
+
: T;
|
|
105
|
+
|
|
106
|
+
export function query<const QDef extends RESTQueryDefinition>(
|
|
107
|
+
queryDefinitionBuilder: (t: APITypes) => QDef,
|
|
108
|
+
): (
|
|
109
|
+
...args: HasRequiredKeys<QueryParams<QDef>> extends true
|
|
110
|
+
? [params: Prettify<Optionalize<QueryParams<QDef>>>]
|
|
111
|
+
: [params?: Prettify<Optionalize<QueryParamsOrUndefined<QDef>>>]
|
|
112
|
+
) => DiscriminatedQueryResult<Readonly<Prettify<ExtractTypesFromObjectOrTypeDef<QDef['response']>>>> {
|
|
113
|
+
let queryDefinition:
|
|
114
|
+
| QueryDefinition<Record<string, unknown>, ExtractTypesFromObjectOrTypeDef<QDef['response']>>
|
|
115
|
+
| undefined;
|
|
116
|
+
|
|
117
|
+
return reactive(
|
|
118
|
+
(params: Record<string, unknown>): DiscriminatedQueryResult<ExtractTypesFromObjectOrTypeDef<QDef['response']>> => {
|
|
119
|
+
const queryClient = getContext(QueryClientContext);
|
|
120
|
+
|
|
121
|
+
if (queryClient === undefined) {
|
|
122
|
+
throw new Error('QueryClient not found');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (queryDefinition === undefined) {
|
|
126
|
+
const { path, method = 'GET', response, cache } = queryDefinitionBuilder(t);
|
|
127
|
+
|
|
128
|
+
const id = `${method}:${path}`;
|
|
129
|
+
|
|
130
|
+
const shape: ObjectFieldTypeDef =
|
|
131
|
+
typeof response === 'object' && !(response instanceof ValidatorDef)
|
|
132
|
+
? t.object(response as Record<string, ObjectFieldTypeDef>)
|
|
133
|
+
: (response as ObjectFieldTypeDef);
|
|
134
|
+
|
|
135
|
+
// Create optimized path interpolator (parses template once)
|
|
136
|
+
const interpolatePath = createPathInterpolator(path);
|
|
137
|
+
|
|
138
|
+
const fetchFn = async (context: QueryContext, params: Record<string, unknown>) => {
|
|
139
|
+
// Interpolate path params and append search params automatically
|
|
140
|
+
const url = interpolatePath(params);
|
|
141
|
+
|
|
142
|
+
const response = await context.fetch(url, {
|
|
143
|
+
method,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return response.json();
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
queryDefinition = {
|
|
150
|
+
id,
|
|
151
|
+
shape,
|
|
152
|
+
fetchFn,
|
|
153
|
+
cache,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return queryClient.getQuery(queryDefinition, params);
|
|
158
|
+
},
|
|
159
|
+
// TODO: Getting a lot of type errors due to infinite recursion here.
|
|
160
|
+
// For now, we return as any to coerce to the external type signature,
|
|
161
|
+
// and internally we manage the difference.
|
|
162
|
+
) as any;
|
|
163
|
+
}
|