@fragno-dev/core 0.0.1
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/.turbo/turbo-build.log +61 -0
- package/.turbo/turbo-types$colon$check.log +2 -0
- package/dist/api/api.d.ts +2 -0
- package/dist/api/api.js +3 -0
- package/dist/api-CBDGZiLC.d.ts +278 -0
- package/dist/api-CBDGZiLC.d.ts.map +1 -0
- package/dist/api-DgHfYjq2.js +54 -0
- package/dist/api-DgHfYjq2.js.map +1 -0
- package/dist/client/client.d.ts +3 -0
- package/dist/client/client.js +6 -0
- package/dist/client/client.svelte.d.ts +33 -0
- package/dist/client/client.svelte.d.ts.map +1 -0
- package/dist/client/client.svelte.js +123 -0
- package/dist/client/client.svelte.js.map +1 -0
- package/dist/client/react.d.ts +58 -0
- package/dist/client/react.d.ts.map +1 -0
- package/dist/client/react.js +80 -0
- package/dist/client/react.js.map +1 -0
- package/dist/client/vanilla.d.ts +61 -0
- package/dist/client/vanilla.d.ts.map +1 -0
- package/dist/client/vanilla.js +136 -0
- package/dist/client/vanilla.js.map +1 -0
- package/dist/client/vue.d.ts +39 -0
- package/dist/client/vue.d.ts.map +1 -0
- package/dist/client/vue.js +108 -0
- package/dist/client/vue.js.map +1 -0
- package/dist/client-DWjxKDnE.js +703 -0
- package/dist/client-DWjxKDnE.js.map +1 -0
- package/dist/client-XFdAy-IQ.d.ts +287 -0
- package/dist/client-XFdAy-IQ.d.ts.map +1 -0
- package/dist/integrations/astro.d.ts +18 -0
- package/dist/integrations/astro.d.ts.map +1 -0
- package/dist/integrations/astro.js +16 -0
- package/dist/integrations/astro.js.map +1 -0
- package/dist/integrations/next-js.d.ts +15 -0
- package/dist/integrations/next-js.d.ts.map +1 -0
- package/dist/integrations/next-js.js +17 -0
- package/dist/integrations/next-js.js.map +1 -0
- package/dist/integrations/react-ssr.d.ts +19 -0
- package/dist/integrations/react-ssr.d.ts.map +1 -0
- package/dist/integrations/react-ssr.js +38 -0
- package/dist/integrations/react-ssr.js.map +1 -0
- package/dist/integrations/svelte-kit.d.ts +21 -0
- package/dist/integrations/svelte-kit.d.ts.map +1 -0
- package/dist/integrations/svelte-kit.js +18 -0
- package/dist/integrations/svelte-kit.js.map +1 -0
- package/dist/mod.d.ts +3 -0
- package/dist/mod.js +177 -0
- package/dist/mod.js.map +1 -0
- package/dist/route-Bp6eByhz.js +331 -0
- package/dist/route-Bp6eByhz.js.map +1 -0
- package/dist/ssr-tJHqcNSw.js +48 -0
- package/dist/ssr-tJHqcNSw.js.map +1 -0
- package/package.json +127 -0
- package/src/api/api.test.ts +140 -0
- package/src/api/api.ts +106 -0
- package/src/api/error.ts +47 -0
- package/src/api/fragment.test.ts +509 -0
- package/src/api/fragment.ts +277 -0
- package/src/api/internal/path-runtime.test.ts +121 -0
- package/src/api/internal/path-type.test.ts +602 -0
- package/src/api/internal/path.ts +322 -0
- package/src/api/internal/response-stream.ts +118 -0
- package/src/api/internal/route.test.ts +56 -0
- package/src/api/internal/route.ts +9 -0
- package/src/api/request-input-context.test.ts +437 -0
- package/src/api/request-input-context.ts +201 -0
- package/src/api/request-middleware.test.ts +544 -0
- package/src/api/request-middleware.ts +126 -0
- package/src/api/request-output-context.test.ts +626 -0
- package/src/api/request-output-context.ts +175 -0
- package/src/api/route.test.ts +176 -0
- package/src/api/route.ts +152 -0
- package/src/client/client-builder.test.ts +264 -0
- package/src/client/client-error.test.ts +15 -0
- package/src/client/client-error.ts +141 -0
- package/src/client/client-types.test.ts +493 -0
- package/src/client/client.ssr.test.ts +173 -0
- package/src/client/client.svelte.test.ts +837 -0
- package/src/client/client.svelte.ts +278 -0
- package/src/client/client.test.ts +1690 -0
- package/src/client/client.ts +1035 -0
- package/src/client/component.test.svelte +21 -0
- package/src/client/internal/ndjson-streaming.test.ts +457 -0
- package/src/client/internal/ndjson-streaming.ts +248 -0
- package/src/client/react.test.ts +947 -0
- package/src/client/react.ts +241 -0
- package/src/client/vanilla.test.ts +867 -0
- package/src/client/vanilla.ts +265 -0
- package/src/client/vue.test.ts +754 -0
- package/src/client/vue.ts +242 -0
- package/src/http/http-status.ts +60 -0
- package/src/integrations/astro.ts +17 -0
- package/src/integrations/next-js.ts +31 -0
- package/src/integrations/react-ssr.ts +40 -0
- package/src/integrations/svelte-kit.ts +41 -0
- package/src/mod.ts +20 -0
- package/src/util/async.test.ts +85 -0
- package/src/util/async.ts +96 -0
- package/src/util/content-type.test.ts +136 -0
- package/src/util/content-type.ts +84 -0
- package/src/util/nanostores.test.ts +28 -0
- package/src/util/nanostores.ts +65 -0
- package/src/util/ssr.ts +75 -0
- package/src/util/types-util.ts +16 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +21 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import { test, expect, expectTypeOf } from "vitest";
|
|
2
|
+
import type {
|
|
3
|
+
ExtractPathParams,
|
|
4
|
+
ExtractPathParamNames,
|
|
5
|
+
ExtractPathParamNamesAsTuple,
|
|
6
|
+
ExtractPathParamsAsLabeledTuple,
|
|
7
|
+
HasPathParams,
|
|
8
|
+
ExtractPathParamsOrWiden,
|
|
9
|
+
MaybeExtractPathParamsOrWiden,
|
|
10
|
+
QueryParamsHint,
|
|
11
|
+
} from "./path";
|
|
12
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
13
|
+
|
|
14
|
+
// Type-only tests using expectTypeOf from vitest
|
|
15
|
+
test("ExtractPathParams type tests", () => {
|
|
16
|
+
// Simple path without parameters
|
|
17
|
+
expectTypeOf<ExtractPathParams<"/path">>().toEqualTypeOf<Record<string, never>>();
|
|
18
|
+
|
|
19
|
+
// Single named parameter
|
|
20
|
+
expectTypeOf<ExtractPathParams<"/path/:name">>().toEqualTypeOf<Record<"name", string>>();
|
|
21
|
+
|
|
22
|
+
// Parameter with no name
|
|
23
|
+
expectTypeOf<ExtractPathParams<"/path/:">>().toEqualTypeOf<Record<"", string>>();
|
|
24
|
+
expectTypeOf<ExtractPathParams<"/path/:/x">>().toEqualTypeOf<Record<"", string>>();
|
|
25
|
+
|
|
26
|
+
// Duplicate identifiers
|
|
27
|
+
expectTypeOf<ExtractPathParams<"/path/:/x/:/">>().toEqualTypeOf<Record<"", string>>();
|
|
28
|
+
expectTypeOf<ExtractPathParams<"/path/:var/x/:var">>().toEqualTypeOf<Record<"var", string>>();
|
|
29
|
+
|
|
30
|
+
// Multiple named parameters
|
|
31
|
+
expectTypeOf<ExtractPathParams<"/users/:id/posts/:postId">>().toEqualTypeOf<
|
|
32
|
+
Record<"id" | "postId", string>
|
|
33
|
+
>();
|
|
34
|
+
|
|
35
|
+
// Wildcard without name
|
|
36
|
+
expectTypeOf<ExtractPathParams<"/path/foo/**">>().toEqualTypeOf<Record<"**", string>>();
|
|
37
|
+
|
|
38
|
+
// Named wildcard
|
|
39
|
+
expectTypeOf<ExtractPathParams<"/path/foo/**:name">>().toEqualTypeOf<Record<"name", string>>();
|
|
40
|
+
|
|
41
|
+
// Complex path with mixed parameters
|
|
42
|
+
expectTypeOf<
|
|
43
|
+
ExtractPathParams<"/api/:version/users/:userId/posts/:postId/**:remaining">
|
|
44
|
+
>().toEqualTypeOf<Record<"version" | "userId" | "postId" | "remaining", string>>();
|
|
45
|
+
|
|
46
|
+
// Root path
|
|
47
|
+
expectTypeOf<ExtractPathParams<"/">>().toEqualTypeOf<Record<string, never>>();
|
|
48
|
+
|
|
49
|
+
// Empty string
|
|
50
|
+
expectTypeOf<ExtractPathParams<"">>().toEqualTypeOf<Record<string, never>>();
|
|
51
|
+
|
|
52
|
+
// Path with only parameter
|
|
53
|
+
expectTypeOf<ExtractPathParams<":id">>().toEqualTypeOf<Record<"id", string>>();
|
|
54
|
+
|
|
55
|
+
// Path with parameter at root
|
|
56
|
+
expectTypeOf<ExtractPathParams<"/:id">>().toEqualTypeOf<Record<"id", string>>();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("ExtractPathParams configurable value type tests", () => {
|
|
60
|
+
// Test with number type
|
|
61
|
+
expectTypeOf<ExtractPathParams<"/path", number>>().toEqualTypeOf<Record<string, never>>();
|
|
62
|
+
expectTypeOf<ExtractPathParams<"/path/:id", number>>().toEqualTypeOf<Record<"id", number>>();
|
|
63
|
+
expectTypeOf<ExtractPathParams<"/users/:id/posts/:postId", number>>().toEqualTypeOf<
|
|
64
|
+
Record<"id" | "postId", number>
|
|
65
|
+
>();
|
|
66
|
+
|
|
67
|
+
// Test with boolean type
|
|
68
|
+
expectTypeOf<ExtractPathParams<"/path/:enabled", boolean>>().toEqualTypeOf<
|
|
69
|
+
Record<"enabled", boolean>
|
|
70
|
+
>();
|
|
71
|
+
expectTypeOf<ExtractPathParams<"/api/:debug/users/:active", boolean>>().toEqualTypeOf<
|
|
72
|
+
Record<"debug" | "active", boolean>
|
|
73
|
+
>();
|
|
74
|
+
|
|
75
|
+
// Test with custom object type
|
|
76
|
+
type CustomType = { value: string; parsed: boolean };
|
|
77
|
+
expectTypeOf<ExtractPathParams<"/path/:data", CustomType>>().toEqualTypeOf<
|
|
78
|
+
Record<"data", CustomType>
|
|
79
|
+
>();
|
|
80
|
+
expectTypeOf<ExtractPathParams<"/api/:config/**:metadata", CustomType>>().toEqualTypeOf<
|
|
81
|
+
Record<"config" | "metadata", CustomType>
|
|
82
|
+
>();
|
|
83
|
+
|
|
84
|
+
// Test with union type
|
|
85
|
+
type StringOrNumber = string | number;
|
|
86
|
+
expectTypeOf<ExtractPathParams<"/path/:value", StringOrNumber>>().toEqualTypeOf<
|
|
87
|
+
Record<"value", StringOrNumber>
|
|
88
|
+
>();
|
|
89
|
+
|
|
90
|
+
// Test with undefined (should work but not very useful)
|
|
91
|
+
expectTypeOf<ExtractPathParams<"/path/:id", undefined>>().toEqualTypeOf<
|
|
92
|
+
Record<"id", undefined>
|
|
93
|
+
>();
|
|
94
|
+
|
|
95
|
+
// Test backward compatibility - default should be string
|
|
96
|
+
expectTypeOf<ExtractPathParams<"/path/:id">>().toEqualTypeOf<
|
|
97
|
+
ExtractPathParams<"/path/:id", string>
|
|
98
|
+
>();
|
|
99
|
+
|
|
100
|
+
// Complex example with custom type
|
|
101
|
+
type ParsedParam = { raw: string; validated: boolean; converted: number };
|
|
102
|
+
expectTypeOf<
|
|
103
|
+
ExtractPathParams<"/api/:version/users/:userId/posts/:postId", ParsedParam>
|
|
104
|
+
>().toEqualTypeOf<Record<"version" | "userId" | "postId", ParsedParam>>();
|
|
105
|
+
|
|
106
|
+
// Wildcard with custom type
|
|
107
|
+
expectTypeOf<ExtractPathParams<"/files/**:path", File>>().toEqualTypeOf<Record<"path", File>>();
|
|
108
|
+
|
|
109
|
+
// Mixed parameters and wildcards with custom type
|
|
110
|
+
expectTypeOf<
|
|
111
|
+
ExtractPathParams<"/api/:version/users/:userId/**:remaining", ParsedParam>
|
|
112
|
+
>().toEqualTypeOf<Record<"version" | "userId" | "remaining", ParsedParam>>();
|
|
113
|
+
|
|
114
|
+
// No parameters should still return Record<string, never> regardless of ValueType
|
|
115
|
+
expectTypeOf<ExtractPathParams<"/static/assets", number>>().toEqualTypeOf<
|
|
116
|
+
Record<string, never>
|
|
117
|
+
>();
|
|
118
|
+
expectTypeOf<ExtractPathParams<"/static/assets", CustomType>>().toEqualTypeOf<
|
|
119
|
+
Record<string, never>
|
|
120
|
+
>();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("ExtractPathParamNames type tests", () => {
|
|
124
|
+
// Simple path without parameters
|
|
125
|
+
expectTypeOf<ExtractPathParamNames<"/path">>().toEqualTypeOf<never>();
|
|
126
|
+
|
|
127
|
+
// Single named parameter
|
|
128
|
+
expectTypeOf<ExtractPathParamNames<"/path/:name">>().toEqualTypeOf<"name">();
|
|
129
|
+
|
|
130
|
+
// Multiple named parameters
|
|
131
|
+
expectTypeOf<ExtractPathParamNames<"/users/:id/posts/:postId">>().toEqualTypeOf<
|
|
132
|
+
"id" | "postId"
|
|
133
|
+
>();
|
|
134
|
+
|
|
135
|
+
// Wildcard without name
|
|
136
|
+
expectTypeOf<ExtractPathParamNames<"/path/foo/**">>().toEqualTypeOf<"**">();
|
|
137
|
+
|
|
138
|
+
// Named wildcard
|
|
139
|
+
expectTypeOf<ExtractPathParamNames<"/path/foo/**:name">>().toEqualTypeOf<"name">();
|
|
140
|
+
|
|
141
|
+
// Complex path
|
|
142
|
+
expectTypeOf<ExtractPathParamNames<"/api/:version/users/:userId/**:files">>().toEqualTypeOf<
|
|
143
|
+
"version" | "userId" | "files"
|
|
144
|
+
>();
|
|
145
|
+
|
|
146
|
+
// Root and empty
|
|
147
|
+
expectTypeOf<ExtractPathParamNames<"/">>().toEqualTypeOf<never>();
|
|
148
|
+
expectTypeOf<ExtractPathParamNames<"">>().toEqualTypeOf<never>();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("ExtractPathParamNamesAsTuple type tests", () => {
|
|
152
|
+
// Simple path without parameters
|
|
153
|
+
expectTypeOf<ExtractPathParamNamesAsTuple<"/path">>().toEqualTypeOf<[]>();
|
|
154
|
+
|
|
155
|
+
// Single named parameter
|
|
156
|
+
expectTypeOf<ExtractPathParamNamesAsTuple<"/path/:name">>().toEqualTypeOf<["name"]>();
|
|
157
|
+
|
|
158
|
+
// Multiple named parameters (should preserve order)
|
|
159
|
+
expectTypeOf<ExtractPathParamNamesAsTuple<"/users/:id/posts/:postId">>().toEqualTypeOf<
|
|
160
|
+
["id", "postId"]
|
|
161
|
+
>();
|
|
162
|
+
|
|
163
|
+
// Wildcard without name
|
|
164
|
+
expectTypeOf<ExtractPathParamNamesAsTuple<"/path/foo/**">>().toEqualTypeOf<["**"]>();
|
|
165
|
+
|
|
166
|
+
// Named wildcard
|
|
167
|
+
expectTypeOf<ExtractPathParamNamesAsTuple<"/path/foo/**:name">>().toEqualTypeOf<["name"]>();
|
|
168
|
+
|
|
169
|
+
// Complex path with mixed parameters (should preserve order)
|
|
170
|
+
expectTypeOf<
|
|
171
|
+
ExtractPathParamNamesAsTuple<"/api/:version/users/:userId/**:files">
|
|
172
|
+
>().toEqualTypeOf<["version", "userId", "files"]>();
|
|
173
|
+
|
|
174
|
+
// Root and empty
|
|
175
|
+
expectTypeOf<ExtractPathParamNamesAsTuple<"/">>().toEqualTypeOf<[]>();
|
|
176
|
+
expectTypeOf<ExtractPathParamNamesAsTuple<"">>().toEqualTypeOf<[]>();
|
|
177
|
+
|
|
178
|
+
// Path with only parameter
|
|
179
|
+
expectTypeOf<ExtractPathParamNamesAsTuple<":id">>().toEqualTypeOf<["id"]>();
|
|
180
|
+
expectTypeOf<ExtractPathParamNamesAsTuple<"/:id">>().toEqualTypeOf<["id"]>();
|
|
181
|
+
|
|
182
|
+
// More complex examples
|
|
183
|
+
expectTypeOf<
|
|
184
|
+
ExtractPathParamNamesAsTuple<"/api/:version/users/:userId/posts/:postId/**:remaining">
|
|
185
|
+
>().toEqualTypeOf<["version", "userId", "postId", "remaining"]>();
|
|
186
|
+
|
|
187
|
+
// Edge cases
|
|
188
|
+
expectTypeOf<ExtractPathParamNamesAsTuple<"/path/:user_id">>().toEqualTypeOf<["user_id"]>();
|
|
189
|
+
expectTypeOf<ExtractPathParamNamesAsTuple<"/path/:user-id">>().toEqualTypeOf<["user-id"]>();
|
|
190
|
+
|
|
191
|
+
// Mixed wildcards and parameters
|
|
192
|
+
expectTypeOf<ExtractPathParamNamesAsTuple<"/api/:version/**:rest">>().toEqualTypeOf<
|
|
193
|
+
["version", "rest"]
|
|
194
|
+
>();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("ExtractPathParamsAsLabeledTuple type tests", () => {
|
|
198
|
+
// Simple path without parameters
|
|
199
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path">>().toEqualTypeOf<[]>();
|
|
200
|
+
|
|
201
|
+
// Single named parameter
|
|
202
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:name">>().toEqualTypeOf<[name: string]>();
|
|
203
|
+
|
|
204
|
+
// Multiple named parameters (should preserve order with labels)
|
|
205
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/users/:id/posts/:postId">>().toEqualTypeOf<
|
|
206
|
+
[id: string, postId: string]
|
|
207
|
+
>();
|
|
208
|
+
|
|
209
|
+
// Wildcard without name
|
|
210
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/foo/**">>().toEqualTypeOf<[string]>();
|
|
211
|
+
|
|
212
|
+
// Named wildcard
|
|
213
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/foo/**:name">>().toEqualTypeOf<
|
|
214
|
+
[name: string]
|
|
215
|
+
>();
|
|
216
|
+
|
|
217
|
+
// Complex path with mixed parameters (the example from the user)
|
|
218
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/api/:version/**:rest">>().toEqualTypeOf<
|
|
219
|
+
[version: string, rest: string]
|
|
220
|
+
>();
|
|
221
|
+
|
|
222
|
+
// More complex examples
|
|
223
|
+
expectTypeOf<
|
|
224
|
+
ExtractPathParamsAsLabeledTuple<"/api/:version/users/:userId/posts/:postId/**:remaining">
|
|
225
|
+
>().toEqualTypeOf<[version: string, userId: string, postId: string, remaining: string]>();
|
|
226
|
+
|
|
227
|
+
// Root and empty
|
|
228
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/">>().toEqualTypeOf<[]>();
|
|
229
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"">>().toEqualTypeOf<[]>();
|
|
230
|
+
|
|
231
|
+
// Path with only parameter
|
|
232
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<":id">>().toEqualTypeOf<[id: string]>();
|
|
233
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/:id">>().toEqualTypeOf<[id: string]>();
|
|
234
|
+
|
|
235
|
+
// Edge cases with special characters
|
|
236
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:user_id">>().toEqualTypeOf<
|
|
237
|
+
[user_id: string]
|
|
238
|
+
>();
|
|
239
|
+
// "user-id" is not a valid identifier in the tuple, so it doesn't become labeled.
|
|
240
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:user-id">>().toEqualTypeOf<[string]>();
|
|
241
|
+
|
|
242
|
+
// Real-world examples
|
|
243
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/api/v1/users/:userId">>().toEqualTypeOf<
|
|
244
|
+
[userId: string]
|
|
245
|
+
>();
|
|
246
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/uploads/:userId/**:filename">>().toEqualTypeOf<
|
|
247
|
+
[userId: string, filename: string]
|
|
248
|
+
>();
|
|
249
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/admin/:section/:action">>().toEqualTypeOf<
|
|
250
|
+
[section: string, action: string]
|
|
251
|
+
>();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("ExtractPathParamsAsLabeledTuple configurable element type tests", () => {
|
|
255
|
+
// Test with number type
|
|
256
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path", number>>().toEqualTypeOf<[]>();
|
|
257
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:id", number>>().toEqualTypeOf<
|
|
258
|
+
[id: number]
|
|
259
|
+
>();
|
|
260
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/users/:id/posts/:postId", number>>().toEqualTypeOf<
|
|
261
|
+
[id: number, postId: number]
|
|
262
|
+
>();
|
|
263
|
+
|
|
264
|
+
// Test with boolean type
|
|
265
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:enabled", boolean>>().toEqualTypeOf<
|
|
266
|
+
[enabled: boolean]
|
|
267
|
+
>();
|
|
268
|
+
expectTypeOf<
|
|
269
|
+
ExtractPathParamsAsLabeledTuple<"/api/:debug/users/:active", boolean>
|
|
270
|
+
>().toEqualTypeOf<[debug: boolean, active: boolean]>();
|
|
271
|
+
|
|
272
|
+
// Test with custom object type
|
|
273
|
+
type CustomType = { value: string; parsed: boolean };
|
|
274
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:data", CustomType>>().toEqualTypeOf<
|
|
275
|
+
[data: CustomType]
|
|
276
|
+
>();
|
|
277
|
+
expectTypeOf<
|
|
278
|
+
ExtractPathParamsAsLabeledTuple<"/api/:config/**:metadata", CustomType>
|
|
279
|
+
>().toEqualTypeOf<[config: CustomType, metadata: CustomType]>();
|
|
280
|
+
|
|
281
|
+
// Test with union type
|
|
282
|
+
type StringOrNumber = string | number;
|
|
283
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:value", StringOrNumber>>().toEqualTypeOf<
|
|
284
|
+
[value: StringOrNumber]
|
|
285
|
+
>();
|
|
286
|
+
|
|
287
|
+
// Test with undefined (should work but not very useful)
|
|
288
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:id", undefined>>().toEqualTypeOf<
|
|
289
|
+
[id: undefined]
|
|
290
|
+
>();
|
|
291
|
+
|
|
292
|
+
// Test backward compatibility - default should be string
|
|
293
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/path/:id">>().toEqualTypeOf<
|
|
294
|
+
ExtractPathParamsAsLabeledTuple<"/path/:id", string>
|
|
295
|
+
>();
|
|
296
|
+
|
|
297
|
+
// Complex example with custom type
|
|
298
|
+
type ParsedParam = { raw: string; validated: boolean; converted: number };
|
|
299
|
+
expectTypeOf<
|
|
300
|
+
ExtractPathParamsAsLabeledTuple<"/api/:version/users/:userId/posts/:postId", ParsedParam>
|
|
301
|
+
>().toEqualTypeOf<[version: ParsedParam, userId: ParsedParam, postId: ParsedParam]>();
|
|
302
|
+
|
|
303
|
+
// Wildcard with custom type
|
|
304
|
+
expectTypeOf<ExtractPathParamsAsLabeledTuple<"/files/**:path", File>>().toEqualTypeOf<
|
|
305
|
+
[path: File]
|
|
306
|
+
>();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("HasPathParams type tests", () => {
|
|
310
|
+
// Paths without parameters
|
|
311
|
+
expectTypeOf<HasPathParams<"/path">>().toEqualTypeOf<false>();
|
|
312
|
+
expectTypeOf<HasPathParams<"/path/foo/bar">>().toEqualTypeOf<false>();
|
|
313
|
+
expectTypeOf<HasPathParams<"/path/foo/**">>().toEqualTypeOf<true>();
|
|
314
|
+
expectTypeOf<HasPathParams<"/">>().toEqualTypeOf<false>();
|
|
315
|
+
expectTypeOf<HasPathParams<"">>().toEqualTypeOf<false>();
|
|
316
|
+
|
|
317
|
+
// Paths with parameters
|
|
318
|
+
expectTypeOf<HasPathParams<"/path/:name">>().toEqualTypeOf<true>();
|
|
319
|
+
expectTypeOf<HasPathParams<"/users/:id">>().toEqualTypeOf<true>();
|
|
320
|
+
expectTypeOf<HasPathParams<"/users/:id/posts/:postId">>().toEqualTypeOf<true>();
|
|
321
|
+
expectTypeOf<HasPathParams<"/path/foo/**:name">>().toEqualTypeOf<true>();
|
|
322
|
+
expectTypeOf<HasPathParams<":id">>().toEqualTypeOf<true>();
|
|
323
|
+
expectTypeOf<HasPathParams<"/:id">>().toEqualTypeOf<true>();
|
|
324
|
+
|
|
325
|
+
type _T = HasPathParams<string>;
|
|
326
|
+
type _T2 = StandardSchemaV1.InferOutput<StandardSchemaV1>;
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Edge case tests
|
|
330
|
+
test("ExtractPathParams edge cases", () => {
|
|
331
|
+
// Parameter names with special characters (though not recommended in practice)
|
|
332
|
+
expectTypeOf<ExtractPathParams<"/path/:user_id">>().toEqualTypeOf<Record<"user_id", string>>();
|
|
333
|
+
expectTypeOf<ExtractPathParams<"/path/:user-id">>().toEqualTypeOf<Record<"user-id", string>>();
|
|
334
|
+
|
|
335
|
+
// Consecutive slashes (malformed but should handle gracefully)
|
|
336
|
+
expectTypeOf<ExtractPathParams<"//path//:name">>().toEqualTypeOf<Record<"name", string>>();
|
|
337
|
+
|
|
338
|
+
// Mixed wildcards and parameters
|
|
339
|
+
expectTypeOf<ExtractPathParams<"/api/:version/**:rest">>().toEqualTypeOf<
|
|
340
|
+
Record<"version" | "rest", string>
|
|
341
|
+
>();
|
|
342
|
+
|
|
343
|
+
// Multiple wildcards (edge case)
|
|
344
|
+
expectTypeOf<ExtractPathParams<"/api/**:first/**:second">>().toEqualTypeOf<
|
|
345
|
+
Record<"first" | "second", string>
|
|
346
|
+
>();
|
|
347
|
+
|
|
348
|
+
expectTypeOf<ExtractPathParams<string>>().toEqualTypeOf<Record<string, never>>();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("Real-world route examples", () => {
|
|
352
|
+
// Common REST API patterns
|
|
353
|
+
expectTypeOf<ExtractPathParams<"/api/v1/users/:userId">>().toEqualTypeOf<
|
|
354
|
+
Record<"userId", string>
|
|
355
|
+
>();
|
|
356
|
+
expectTypeOf<
|
|
357
|
+
ExtractPathParams<"/api/v1/users/:userId/posts/:postId/comments/:commentId">
|
|
358
|
+
>().toEqualTypeOf<Record<"userId" | "postId" | "commentId", string>>();
|
|
359
|
+
|
|
360
|
+
// File serving patterns
|
|
361
|
+
expectTypeOf<ExtractPathParams<"/static/**:filepath">>().toEqualTypeOf<
|
|
362
|
+
Record<"filepath", string>
|
|
363
|
+
>();
|
|
364
|
+
expectTypeOf<ExtractPathParams<"/uploads/:userId/**:filename">>().toEqualTypeOf<
|
|
365
|
+
Record<"userId" | "filename", string>
|
|
366
|
+
>();
|
|
367
|
+
|
|
368
|
+
// Admin/dashboard patterns
|
|
369
|
+
expectTypeOf<ExtractPathParams<"/admin/:section/:action">>().toEqualTypeOf<
|
|
370
|
+
Record<"section" | "action", string>
|
|
371
|
+
>();
|
|
372
|
+
expectTypeOf<ExtractPathParams<"/dashboard/:org/projects/:projectId/settings">>().toEqualTypeOf<
|
|
373
|
+
Record<"org" | "projectId", string>
|
|
374
|
+
>();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Runtime verification tests (ensuring the types work as expected in practice)
|
|
378
|
+
test("Type compatibility runtime tests", () => {
|
|
379
|
+
// These tests verify that the types actually work as expected at runtime
|
|
380
|
+
// by checking if type assignments would be valid
|
|
381
|
+
|
|
382
|
+
// Function that expects no params
|
|
383
|
+
function _handleNoParams(_params: ExtractPathParams<"/static/assets">) {
|
|
384
|
+
// Should receive Record<string, never> which is essentially {}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Function that expects specific params
|
|
388
|
+
function _handleUserRoute(params: ExtractPathParams<"/users/:id">) {
|
|
389
|
+
// Should have id property
|
|
390
|
+
expect(typeof params).toBe("object");
|
|
391
|
+
// Type system ensures params has 'id' property of type string
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Function that expects multiple params
|
|
395
|
+
function _handleComplexRoute(
|
|
396
|
+
params: ExtractPathParams<"/api/:version/users/:userId/posts/:postId">,
|
|
397
|
+
) {
|
|
398
|
+
// Should have version, userId, and postId properties
|
|
399
|
+
expect(typeof params).toBe("object");
|
|
400
|
+
// Type system ensures all required properties exist
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Test type narrowing works
|
|
404
|
+
const _path1 = "/users/:id" as const;
|
|
405
|
+
type Path1Params = ExtractPathParams<typeof _path1>;
|
|
406
|
+
expectTypeOf<Path1Params>().toEqualTypeOf<Record<"id", string>>();
|
|
407
|
+
|
|
408
|
+
const _path2 = "/static/files" as const;
|
|
409
|
+
type Path2Params = ExtractPathParams<typeof _path2>;
|
|
410
|
+
expectTypeOf<Path2Params>().toEqualTypeOf<Record<string, never>>();
|
|
411
|
+
|
|
412
|
+
// Verify HasPathParams utility
|
|
413
|
+
const hasParams1: HasPathParams<"/users/:id"> = true;
|
|
414
|
+
const hasParams2: HasPathParams<"/static"> = false;
|
|
415
|
+
|
|
416
|
+
expect(hasParams1).toBe(true);
|
|
417
|
+
expect(hasParams2).toBe(false);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("ExtractPathParamsOrWiden type tests", () => {
|
|
421
|
+
expectTypeOf<ExtractPathParamsOrWiden<"/path">>().toEqualTypeOf<Record<string, never>>();
|
|
422
|
+
expectTypeOf<ExtractPathParamsOrWiden<"/path/:id">>().toEqualTypeOf<Record<"id", string>>();
|
|
423
|
+
expectTypeOf<ExtractPathParamsOrWiden<"/path/:id", number>>().toEqualTypeOf<
|
|
424
|
+
Record<"id", number>
|
|
425
|
+
>();
|
|
426
|
+
expectTypeOf<ExtractPathParamsOrWiden<"/path/:id", boolean>>().toEqualTypeOf<
|
|
427
|
+
Record<"id", boolean>
|
|
428
|
+
>();
|
|
429
|
+
expectTypeOf<ExtractPathParamsOrWiden<"/path/:id", undefined>>().toEqualTypeOf<
|
|
430
|
+
Record<"id", undefined>
|
|
431
|
+
>();
|
|
432
|
+
|
|
433
|
+
// This is the actual tests
|
|
434
|
+
expectTypeOf<ExtractPathParamsOrWiden<string>>().toEqualTypeOf<Record<string, string>>();
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("MaybeExtractPathParamsOrWiden type tests", () => {
|
|
438
|
+
expectTypeOf<MaybeExtractPathParamsOrWiden<"/path">>().toEqualTypeOf<undefined>();
|
|
439
|
+
expectTypeOf<MaybeExtractPathParamsOrWiden<"/path/:id">>().toEqualTypeOf<Record<"id", string>>();
|
|
440
|
+
expectTypeOf<MaybeExtractPathParamsOrWiden<"/path/:id", number>>().toEqualTypeOf<
|
|
441
|
+
Record<"id", number>
|
|
442
|
+
>();
|
|
443
|
+
expectTypeOf<MaybeExtractPathParamsOrWiden<string>>().toEqualTypeOf<undefined>();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("QueryParamsHint type tests", () => {
|
|
447
|
+
// Basic usage with string union
|
|
448
|
+
expectTypeOf<QueryParamsHint<"page" | "limit">>().toEqualTypeOf<
|
|
449
|
+
Partial<Record<"page" | "limit", string>> & Record<string, string>
|
|
450
|
+
>();
|
|
451
|
+
|
|
452
|
+
// Single parameter hint
|
|
453
|
+
expectTypeOf<QueryParamsHint<"search">>().toEqualTypeOf<
|
|
454
|
+
Partial<Record<"search", string>> & Record<string, string>
|
|
455
|
+
>();
|
|
456
|
+
|
|
457
|
+
// Empty hint (never) - should still allow any string keys
|
|
458
|
+
expectTypeOf<QueryParamsHint<never>>().toEqualTypeOf<
|
|
459
|
+
Partial<Record<never, string>> & Record<string, string>
|
|
460
|
+
>();
|
|
461
|
+
|
|
462
|
+
// With custom value type
|
|
463
|
+
expectTypeOf<QueryParamsHint<"page" | "limit", number>>().toEqualTypeOf<
|
|
464
|
+
Partial<Record<"page" | "limit", number>> & Record<string, number>
|
|
465
|
+
>();
|
|
466
|
+
|
|
467
|
+
// With boolean value type
|
|
468
|
+
expectTypeOf<QueryParamsHint<"enabled" | "debug", boolean>>().toEqualTypeOf<
|
|
469
|
+
Partial<Record<"enabled" | "debug", boolean>> & Record<string, boolean>
|
|
470
|
+
>();
|
|
471
|
+
|
|
472
|
+
// With union value type
|
|
473
|
+
type StringOrNumber = string | number;
|
|
474
|
+
expectTypeOf<QueryParamsHint<"value", StringOrNumber>>().toEqualTypeOf<
|
|
475
|
+
Partial<Record<"value", StringOrNumber>> & Record<string, StringOrNumber>
|
|
476
|
+
>();
|
|
477
|
+
|
|
478
|
+
// With custom object type
|
|
479
|
+
type CustomType = { raw: string; parsed: boolean };
|
|
480
|
+
expectTypeOf<QueryParamsHint<"data", CustomType>>().toEqualTypeOf<
|
|
481
|
+
Partial<Record<"data", CustomType>> & Record<string, CustomType>
|
|
482
|
+
>();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test("QueryParamsHint assignability tests", () => {
|
|
486
|
+
// Test that the type allows the expected assignments
|
|
487
|
+
type TestQuery = QueryParamsHint<"page" | "limit">;
|
|
488
|
+
|
|
489
|
+
// Empty object should be assignable
|
|
490
|
+
const query1: TestQuery = {};
|
|
491
|
+
expect(query1).toEqual({});
|
|
492
|
+
|
|
493
|
+
// Hinted parameters should be assignable
|
|
494
|
+
const query2: TestQuery = { page: "1" };
|
|
495
|
+
expect(query2.page).toBe("1");
|
|
496
|
+
|
|
497
|
+
const query3: TestQuery = { limit: "10" };
|
|
498
|
+
expect(query3.limit).toBe("10");
|
|
499
|
+
|
|
500
|
+
const query4: TestQuery = { page: "1", limit: "10" };
|
|
501
|
+
expect(query4.page).toBe("1");
|
|
502
|
+
expect(query4.limit).toBe("10");
|
|
503
|
+
|
|
504
|
+
// Additional parameters should be assignable
|
|
505
|
+
const query5: TestQuery = { page: "1", sort: "asc" };
|
|
506
|
+
expect(query5.page).toBe("1");
|
|
507
|
+
expect(query5["sort"]).toBe("asc");
|
|
508
|
+
|
|
509
|
+
const query6: TestQuery = { search: "test", filter: "active" };
|
|
510
|
+
expect(query6["search"]).toBe("test");
|
|
511
|
+
expect(query6["filter"]).toBe("active");
|
|
512
|
+
|
|
513
|
+
// Mixed hinted and additional parameters
|
|
514
|
+
const query7: TestQuery = { page: "1", limit: "10", sort: "desc", filter: "all" };
|
|
515
|
+
expect(query7.page).toBe("1");
|
|
516
|
+
expect(query7.limit).toBe("10");
|
|
517
|
+
expect(query7["sort"]).toBe("desc");
|
|
518
|
+
expect(query7["filter"]).toBe("all");
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("QueryParamsHint with different value types", () => {
|
|
522
|
+
// Number value type
|
|
523
|
+
type NumberQuery = QueryParamsHint<"count" | "offset", number>;
|
|
524
|
+
|
|
525
|
+
const numQuery1: NumberQuery = {};
|
|
526
|
+
expect(numQuery1).toEqual({});
|
|
527
|
+
|
|
528
|
+
const numQuery2: NumberQuery = { count: 5 };
|
|
529
|
+
expect(numQuery2.count).toBe(5);
|
|
530
|
+
|
|
531
|
+
const numQuery3: NumberQuery = { count: 5, extra: 10 };
|
|
532
|
+
expect(numQuery3.count).toBe(5);
|
|
533
|
+
expect(numQuery3["extra"]).toBe(10);
|
|
534
|
+
|
|
535
|
+
// Boolean value type
|
|
536
|
+
type BooleanQuery = QueryParamsHint<"enabled" | "debug", boolean>;
|
|
537
|
+
|
|
538
|
+
const boolQuery1: BooleanQuery = {};
|
|
539
|
+
expect(boolQuery1).toEqual({});
|
|
540
|
+
|
|
541
|
+
const boolQuery2: BooleanQuery = { enabled: true };
|
|
542
|
+
expect(boolQuery2.enabled).toBe(true);
|
|
543
|
+
|
|
544
|
+
const boolQuery3: BooleanQuery = { enabled: true, verbose: false };
|
|
545
|
+
expect(boolQuery3.enabled).toBe(true);
|
|
546
|
+
expect(boolQuery3["verbose"]).toBe(false);
|
|
547
|
+
|
|
548
|
+
// Union type
|
|
549
|
+
type MixedQuery = QueryParamsHint<"value", string | number>;
|
|
550
|
+
|
|
551
|
+
const mixedQuery1: MixedQuery = { value: "text" };
|
|
552
|
+
expect(mixedQuery1.value).toBe("text");
|
|
553
|
+
|
|
554
|
+
const mixedQuery2: MixedQuery = { value: 42 };
|
|
555
|
+
expect(mixedQuery2.value).toBe(42);
|
|
556
|
+
|
|
557
|
+
const mixedQuery3: MixedQuery = { value: "text", other: 123 };
|
|
558
|
+
expect(mixedQuery3.value).toBe("text");
|
|
559
|
+
expect(mixedQuery3["other"]).toBe(123);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("QueryParamsHint real-world examples", () => {
|
|
563
|
+
// Pagination query
|
|
564
|
+
type PaginationQuery = QueryParamsHint<"page" | "limit" | "offset">;
|
|
565
|
+
|
|
566
|
+
const paginationQuery: PaginationQuery = {
|
|
567
|
+
page: "1",
|
|
568
|
+
limit: "20",
|
|
569
|
+
sort: "created_at",
|
|
570
|
+
order: "desc",
|
|
571
|
+
};
|
|
572
|
+
expect(paginationQuery.page).toBe("1");
|
|
573
|
+
expect(paginationQuery.limit).toBe("20");
|
|
574
|
+
expect(paginationQuery["sort"]).toBe("created_at");
|
|
575
|
+
expect(paginationQuery["order"]).toBe("desc");
|
|
576
|
+
|
|
577
|
+
// Search and filter query
|
|
578
|
+
type SearchQuery = QueryParamsHint<"q" | "category" | "tags">;
|
|
579
|
+
|
|
580
|
+
const searchQuery: SearchQuery = {
|
|
581
|
+
q: "typescript",
|
|
582
|
+
status: "published",
|
|
583
|
+
author: "john",
|
|
584
|
+
};
|
|
585
|
+
expect(searchQuery.q).toBe("typescript");
|
|
586
|
+
expect(searchQuery["status"]).toBe("published");
|
|
587
|
+
expect(searchQuery["author"]).toBe("john");
|
|
588
|
+
|
|
589
|
+
// API configuration query
|
|
590
|
+
type ApiQuery = QueryParamsHint<"version" | "format">;
|
|
591
|
+
|
|
592
|
+
const apiQuery: ApiQuery = {
|
|
593
|
+
version: "v2",
|
|
594
|
+
format: "json",
|
|
595
|
+
include: "metadata",
|
|
596
|
+
fields: "id,name,created_at",
|
|
597
|
+
};
|
|
598
|
+
expect(apiQuery.version).toBe("v2");
|
|
599
|
+
expect(apiQuery.format).toBe("json");
|
|
600
|
+
expect(apiQuery["include"]).toBe("metadata");
|
|
601
|
+
expect(apiQuery["fields"]).toBe("id,name,created_at");
|
|
602
|
+
});
|