@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/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
+ }