@mpen/routekit 0.1.0 → 0.1.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.
Files changed (133) hide show
  1. package/dist/bin.d.mts +4 -0
  2. package/dist/client/react.d.mts +178 -0
  3. package/dist/client/react.mjs +142 -0
  4. package/dist/client.d.mts +433 -0
  5. package/dist/client.mjs +264 -0
  6. package/dist/content-BuDOmhH_.mjs +102 -0
  7. package/dist/core-CzUCxvGk.d.mts +140 -0
  8. package/dist/core-DbmQauwS.mjs +81 -0
  9. package/dist/handlers.d.mts +72 -0
  10. package/dist/handlers.mjs +153 -0
  11. package/dist/index.d.mts +3 -0
  12. package/dist/index.mjs +1152 -0
  13. package/dist/middleware.d.mts +388 -0
  14. package/dist/middleware.mjs +1222 -0
  15. package/dist/request-Dn0zc-xm.mjs +1025 -0
  16. package/dist/response/content.d.mts +79 -0
  17. package/dist/response/content.mjs +2 -0
  18. package/dist/response/json-rpc.d.mts +1 -0
  19. package/dist/response/json-rpc.mjs +1 -0
  20. package/dist/response/problem/valibot.d.mts +230 -0
  21. package/dist/response/problem/valibot.mjs +258 -0
  22. package/dist/response/problem.d.mts +415 -0
  23. package/dist/response/problem.mjs +183 -0
  24. package/dist/response/status.d.mts +45 -0
  25. package/dist/response/status.mjs +2 -0
  26. package/dist/responses-B379Ep9Y.d.mts +296 -0
  27. package/dist/responses-BpVrgeYi.mjs +101 -0
  28. package/dist/router-Cwb7ak0J.d.mts +1819 -0
  29. package/dist/routes.d.mts +282 -0
  30. package/dist/routes.mjs +311 -0
  31. package/dist/status-C-8mw-FB.mjs +59 -0
  32. package/dist/valibot-D7liFYyB.d.mts +290 -0
  33. package/dist/valibot-Du97X-TS.mjs +326 -0
  34. package/package.json +8 -2
  35. package/src/bin/gen-api-client.test.ts +0 -70
  36. package/src/bin/gen-api-client.ts +0 -986
  37. package/src/client/headers.ts +0 -31
  38. package/src/client/index.ts +0 -8
  39. package/src/client/promise.ts +0 -11
  40. package/src/client/react/index.test.tsx +0 -266
  41. package/src/client/react/index.ts +0 -431
  42. package/src/client/responses.test.ts +0 -151
  43. package/src/client/responses.ts +0 -278
  44. package/src/client/transport.ts +0 -74
  45. package/src/client/transports/body-codec.ts +0 -61
  46. package/src/client/transports/fetch.ts +0 -113
  47. package/src/client/tsconfig.json +0 -9
  48. package/src/client/types.ts +0 -15
  49. package/src/client/url.ts +0 -31
  50. package/src/index.ts +0 -63
  51. package/src/router/fetch-types.ts +0 -13
  52. package/src/router/handlers/index.ts +0 -2
  53. package/src/router/handlers/openapi/index.ts +0 -2
  54. package/src/router/handlers/openapi/openapi.ts +0 -293
  55. package/src/router/integration/zod-openapi.test.ts +0 -74
  56. package/src/router/lib/charset.test.ts +0 -22
  57. package/src/router/lib/charset.ts +0 -133
  58. package/src/router/lib/collections.ts +0 -3
  59. package/src/router/lib/format.test.ts +0 -67
  60. package/src/router/lib/format.ts +0 -35
  61. package/src/router/lib/host.ts +0 -4
  62. package/src/router/lib/json-schema.ts +0 -6
  63. package/src/router/lib/media-type.test.ts +0 -122
  64. package/src/router/lib/media-type.ts +0 -289
  65. package/src/router/lib/pathname.test.ts +0 -18
  66. package/src/router/lib/pathname.ts +0 -19
  67. package/src/router/lib/route-names.ts +0 -70
  68. package/src/router/lib/route-normalize.test.ts +0 -36
  69. package/src/router/lib/route-normalize.ts +0 -67
  70. package/src/router/lib/schema-merge.ts +0 -56
  71. package/src/router/middleware/accept-ctx.test.ts +0 -33
  72. package/src/router/middleware/accept-ctx.ts +0 -12
  73. package/src/router/middleware/body-limit.test.ts +0 -112
  74. package/src/router/middleware/body-limit.ts +0 -121
  75. package/src/router/middleware/content-type-context.ts +0 -0
  76. package/src/router/middleware/cors.test.ts +0 -269
  77. package/src/router/middleware/cors.ts +0 -490
  78. package/src/router/middleware/csrf.test.ts +0 -106
  79. package/src/router/middleware/csrf.ts +0 -192
  80. package/src/router/middleware/define.ts +0 -249
  81. package/src/router/middleware/index.ts +0 -34
  82. package/src/router/middleware/jsxhtml-response.ts +0 -0
  83. package/src/router/middleware/oas-swagger.ts +0 -0
  84. package/src/router/middleware/rate-limit.test.ts +0 -886
  85. package/src/router/middleware/rate-limit.ts +0 -920
  86. package/src/router/middleware/request-id-ctx.test.ts +0 -183
  87. package/src/router/middleware/request-id-ctx.ts +0 -135
  88. package/src/router/middleware/request-logger-format.test.ts +0 -16
  89. package/src/router/middleware/request-logger-format.ts +0 -269
  90. package/src/router/middleware/request-logger.test.ts +0 -267
  91. package/src/router/middleware/request-logger.ts +0 -131
  92. package/src/router/middleware/start-time-ctx.ts +0 -5
  93. package/src/router/request.ts +0 -611
  94. package/src/router/response/core.ts +0 -181
  95. package/src/router/response/directives.ts +0 -233
  96. package/src/router/response/formats/content/bodyless.ts +0 -54
  97. package/src/router/response/formats/content/content.ts +0 -79
  98. package/src/router/response/formats/content/index.ts +0 -2
  99. package/src/router/response/formats/json-rpc/index.ts +0 -2
  100. package/src/router/response/formats/problem/badRequest.ts +0 -90
  101. package/src/router/response/formats/problem/conflict.ts +0 -90
  102. package/src/router/response/formats/problem/created.ts +0 -40
  103. package/src/router/response/formats/problem/index.ts +0 -27
  104. package/src/router/response/formats/problem/notFound.ts +0 -90
  105. package/src/router/response/formats/problem/permissionDenied.ts +0 -90
  106. package/src/router/response/formats/problem/problem.test.ts +0 -888
  107. package/src/router/response/formats/problem/rateLimited.ts +0 -90
  108. package/src/router/response/formats/problem/responses.ts +0 -219
  109. package/src/router/response/formats/problem/root-errors.ts +0 -48
  110. package/src/router/response/formats/problem/sessionExpired.ts +0 -90
  111. package/src/router/response/formats/problem/types.ts +0 -170
  112. package/src/router/response/formats/problem/unauthenticated.ts +0 -90
  113. package/src/router/response/formats/problem/valibot.ts +0 -410
  114. package/src/router/response/formats/status/index.ts +0 -1
  115. package/src/router/response/formats/status/responses.ts +0 -59
  116. package/src/router/response/formats/status/status.test.ts +0 -21
  117. package/src/router/response/framers.ts +0 -85
  118. package/src/router/response/index.ts +0 -28
  119. package/src/router/response/openapi.test.ts +0 -96
  120. package/src/router/response/openapi.ts +0 -1
  121. package/src/router/response/serializers.ts +0 -66
  122. package/src/router/response/stream.ts +0 -35
  123. package/src/router/router.test.ts +0 -1571
  124. package/src/router/router.ts +0 -1965
  125. package/src/router/routes/index.ts +0 -46
  126. package/src/router/routes/valibot/index.ts +0 -18
  127. package/src/router/routes/valibot/valibot.ts +0 -1393
  128. package/src/router/routes/valibot.test.ts +0 -286
  129. package/src/router/routes/zod/index.ts +0 -18
  130. package/src/router/routes/zod/zod.ts +0 -1318
  131. package/src/router/routes/zod.test.ts +0 -280
  132. package/src/router/server-interface.ts +0 -31
  133. package/src/router/types.ts +0 -657
@@ -1,431 +0,0 @@
1
- import { useCallback, useDebugValue, useEffect, useRef, useState } from 'react'
2
-
3
- /**
4
- * A generated API client endpoint method that can be called by [`useQuery`]{@link useQuery}.
5
- *
6
- * @example
7
- * ```ts
8
- * const method: QueryMethod<[id: number], ApiResponse<{ id: number }>> = apiClient.users.get
9
- * ```
10
- *
11
- * @typeParam TArgs - The endpoint argument tuple.
12
- * @typeParam TResponse - The resolved endpoint response type.
13
- */
14
- export type QueryMethod<TArgs extends unknown[] = any[], TResponse = unknown> = (
15
- ...args: TArgs
16
- ) => Promise<TResponse>
17
-
18
- /**
19
- * Resolves the response type returned by a [`QueryMethod`]{@link QueryMethod}.
20
- *
21
- * @example
22
- * ```ts
23
- * type Response = QueryResponse<typeof apiClient.users.get>
24
- * ```
25
- *
26
- * @typeParam TMethod - The endpoint method type.
27
- */
28
- export type QueryResponse<TMethod extends QueryMethod> = Awaited<ReturnType<TMethod>>
29
-
30
- /**
31
- * Infers the default data value exposed by [`useQuery`]{@link useQuery}.
32
- *
33
- * @example
34
- * ```ts
35
- * type Data = QueryData<ApiResponse<{ ok: true }>>
36
- * ```
37
- *
38
- * @typeParam TResponse - The endpoint response type.
39
- */
40
- export type QueryData<TResponse> = TResponse extends { body: infer TBody } ? TBody : TResponse
41
-
42
- /**
43
- * Current lifecycle status for a [`useQuery`]{@link useQuery} request.
44
- *
45
- * @example
46
- * ```ts
47
- * if (query.status === 'success') {
48
- * query.data
49
- * }
50
- * ```
51
- */
52
- export type UseQueryStatus = 'idle' | 'loading' | 'success' | 'error'
53
-
54
- /**
55
- * Options for [`useQuery`]{@link useQuery}.
56
- *
57
- * @example
58
- * ```tsx
59
- * const query = useQuery(
60
- * { enabled: userId !== undefined, keepPreviousData: true },
61
- * apiClient.users.get,
62
- * userId,
63
- * )
64
- * ```
65
- *
66
- * @typeParam TResponse - The endpoint response type.
67
- * @typeParam TData - The selected data type exposed on the result.
68
- */
69
- export interface UseQueryOptions<TResponse, TData = QueryData<TResponse>> {
70
- /**
71
- * Whether the hook should automatically run the endpoint.
72
- */
73
- enabled?: boolean
74
-
75
- /**
76
- * Keeps the previous response and data visible while the next request is loading.
77
- * @defaultValue true
78
- */
79
- keepPreviousData?: boolean
80
-
81
- /**
82
- * Optional query key used instead of the endpoint arguments when deciding whether to refetch.
83
- */
84
- key?: unknown
85
-
86
- /**
87
- * Selects the result data from the endpoint response.
88
- */
89
- select?: (response: TResponse) => TData
90
- }
91
-
92
- /**
93
- * Result returned by [`useQuery`]{@link useQuery}.
94
- *
95
- * @example
96
- * ```tsx
97
- * const query = useQuery(apiClient.health.get)
98
- *
99
- * if (query.isLoading) return 'Loading'
100
- * if (query.isError) return String(query.error)
101
- *
102
- * return query.data.ok
103
- * ```
104
- *
105
- * @typeParam TResponse - The endpoint response type.
106
- * @typeParam TData - The selected data type exposed on the result.
107
- */
108
- export interface UseQueryResult<TResponse, TData = QueryData<TResponse>> {
109
- /**
110
- * The current request lifecycle status.
111
- */
112
- status: UseQueryStatus
113
-
114
- /**
115
- * The latest selected data. By default this is `response.body`.
116
- */
117
- data: TData | undefined
118
-
119
- /**
120
- * The latest full endpoint response.
121
- */
122
- response: TResponse | undefined
123
-
124
- /**
125
- * The latest request error.
126
- */
127
- error: unknown
128
-
129
- /**
130
- * Whether the query has not run yet.
131
- */
132
- isIdle: boolean
133
-
134
- /**
135
- * Whether the query is loading without any successful response.
136
- */
137
- isLoading: boolean
138
-
139
- /**
140
- * Whether a request is currently in flight.
141
- */
142
- isFetching: boolean
143
-
144
- /**
145
- * Whether the query has a successful response.
146
- */
147
- isSuccess: boolean
148
-
149
- /**
150
- * Whether the latest request failed.
151
- */
152
- isError: boolean
153
-
154
- /**
155
- * Runs the endpoint again with the latest method arguments.
156
- *
157
- * @returns A promise for the endpoint response.
158
- */
159
- refetch(): Promise<TResponse>
160
- }
161
-
162
- interface UseQueryState<TResponse, TData> {
163
- status: UseQueryStatus
164
- data: TData | undefined
165
- response: TResponse | undefined
166
- error: unknown
167
- isFetching: boolean
168
- }
169
-
170
- type UseQueryInput<TMethod extends QueryMethod, TData> =
171
- | [method: TMethod, ...args: Parameters<TMethod>]
172
- | [
173
- options: UseQueryOptions<QueryResponse<TMethod>, TData>,
174
- method: TMethod,
175
- ...args: Parameters<TMethod>,
176
- ]
177
-
178
- const isPlainObject = (value: object): value is Record<string, unknown> =>
179
- Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null
180
-
181
- const hasBody = (value: unknown): value is { body: unknown } =>
182
- typeof value === 'object' && value !== null && 'body' in value
183
-
184
- function defaultSelect<TResponse>(response: TResponse): QueryData<TResponse> {
185
- return (hasBody(response) ? response.body : response) as QueryData<TResponse>
186
- }
187
-
188
- function createInitialState<TResponse, TData>(enabled: boolean): UseQueryState<TResponse, TData> {
189
- return {
190
- status: enabled ? 'loading' : 'idle',
191
- data: undefined,
192
- response: undefined,
193
- error: undefined,
194
- isFetching: enabled,
195
- }
196
- }
197
-
198
- function startQuery<TResponse, TData>(
199
- state: UseQueryState<TResponse, TData>,
200
- keepPreviousData: boolean,
201
- ): UseQueryState<TResponse, TData> {
202
- return {
203
- status: keepPreviousData && state.response !== undefined ? state.status : 'loading',
204
- data: keepPreviousData ? state.data : undefined,
205
- response: keepPreviousData ? state.response : undefined,
206
- error: undefined,
207
- isFetching: true,
208
- }
209
- }
210
-
211
- function stopQuery<TResponse, TData>(
212
- state: UseQueryState<TResponse, TData>,
213
- ): UseQueryState<TResponse, TData> {
214
- return {
215
- ...state,
216
- status: state.response === undefined ? 'idle' : state.status,
217
- isFetching: false,
218
- }
219
- }
220
-
221
- function toQueryResult<TResponse, TData>(
222
- state: UseQueryState<TResponse, TData>,
223
- refetch: () => Promise<TResponse>,
224
- ): UseQueryResult<TResponse, TData> {
225
- return {
226
- ...state,
227
- isIdle: state.status === 'idle',
228
- isLoading: state.status === 'loading',
229
- isSuccess: state.status === 'success',
230
- isError: state.status === 'error',
231
- refetch,
232
- }
233
- }
234
-
235
- function hashQueryKey(value: unknown): string {
236
- return stableStringify(value, new WeakMap(), { value: 0 })
237
- }
238
-
239
- function stableStringify(
240
- value: unknown,
241
- seen: WeakMap<object, number>,
242
- nextSeenIndex: { value: number },
243
- ): string {
244
- if (value === null) return 'null'
245
-
246
- switch (typeof value) {
247
- case 'bigint':
248
- return `bigint:${value}`
249
- case 'boolean':
250
- case 'number':
251
- case 'string':
252
- return JSON.stringify(value)
253
- case 'symbol':
254
- return `symbol:${String(value.description)}`
255
- case 'undefined':
256
- return 'undefined'
257
- case 'function':
258
- return `function:${String(value)}`
259
- }
260
-
261
- const seenIndex = seen.get(value)
262
- if (seenIndex !== undefined) {
263
- return `[Circular:${seenIndex}]`
264
- }
265
- seen.set(value, nextSeenIndex.value)
266
- nextSeenIndex.value += 1
267
-
268
- if (Array.isArray(value)) {
269
- return `[${value.map((item) => stableStringify(item, seen, nextSeenIndex)).join(',')}]`
270
- }
271
- if (value instanceof Date) {
272
- return `Date:${value.toISOString()}`
273
- }
274
- if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) {
275
- return `URLSearchParams:${stableStringify(Array.from(value.entries()), seen, nextSeenIndex)}`
276
- }
277
- if (typeof Headers !== 'undefined' && value instanceof Headers) {
278
- return `Headers:${stableStringify(Array.from(value.entries()).sort(), seen, nextSeenIndex)}`
279
- }
280
- if (value instanceof Map) {
281
- return `Map:${stableStringify(Array.from(value.entries()).sort(), seen, nextSeenIndex)}`
282
- }
283
- if (value instanceof Set) {
284
- return `Set:${stableStringify(Array.from(value.values()).sort(), seen, nextSeenIndex)}`
285
- }
286
- if (!isPlainObject(value)) {
287
- return Object.prototype.toString.call(value)
288
- }
289
-
290
- return `{${Object.keys(value)
291
- .sort()
292
- .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key], seen, nextSeenIndex)}`)
293
- .join(',')}}`
294
- }
295
-
296
- /**
297
- * Runs a generated API endpoint method and refetches when its arguments change.
298
- *
299
- * @example
300
- * ```tsx
301
- * const query = useQuery(apiClient.usersById.get, 123, { includePosts: true })
302
- * ```
303
- *
304
- * @param method - Generated API client endpoint method to run.
305
- * @param args - Arguments passed to the endpoint method.
306
- * @returns Query state, the latest response body, the full response, and a manual refetch function.
307
- * @typeParam TMethod - The endpoint method type.
308
- */
309
- export function useQuery<TMethod extends QueryMethod>(
310
- method: TMethod,
311
- ...args: Parameters<TMethod>
312
- ): UseQueryResult<QueryResponse<TMethod>, QueryData<QueryResponse<TMethod>>>
313
-
314
- /**
315
- * Runs a generated API endpoint method with query options and refetches when its arguments change.
316
- *
317
- * @example
318
- * ```tsx
319
- * const query = useQuery(
320
- * { keepPreviousData: true, select: (response) => response.body.items },
321
- * apiClient.search.get,
322
- * { q: 'routekit' },
323
- * )
324
- * ```
325
- *
326
- * @param options - Query behavior options.
327
- * @param method - Generated API client endpoint method to run.
328
- * @param args - Arguments passed to the endpoint method.
329
- * @returns Query state, the selected data, the full response, and a manual refetch function.
330
- * @typeParam TMethod - The endpoint method type.
331
- * @typeParam TData - The selected data type.
332
- */
333
- export function useQuery<TMethod extends QueryMethod, TData = QueryData<QueryResponse<TMethod>>>(
334
- options: UseQueryOptions<QueryResponse<TMethod>, TData>,
335
- method: TMethod,
336
- ...args: Parameters<TMethod>
337
- ): UseQueryResult<QueryResponse<TMethod>, TData>
338
-
339
- export function useQuery<TMethod extends QueryMethod, TData = QueryData<QueryResponse<TMethod>>>(
340
- ...input: UseQueryInput<TMethod, TData>
341
- ): UseQueryResult<QueryResponse<TMethod>, TData> {
342
- const hasOptions = typeof input[0] !== 'function'
343
- const options = (hasOptions ? input[0] : undefined) as
344
- | UseQueryOptions<QueryResponse<TMethod>, TData>
345
- | undefined
346
- const method = (hasOptions ? input[1] : input[0]) as TMethod
347
- const args = input.slice(hasOptions ? 2 : 1) as Parameters<TMethod>
348
- const enabled = options?.enabled ?? true
349
- const queryKey = hashQueryKey(options?.key ?? args)
350
- const select = options?.select ?? (defaultSelect as (response: QueryResponse<TMethod>) => TData)
351
-
352
- const argsRef = useRef(args)
353
- const keepPreviousDataRef = useRef(options?.keepPreviousData ?? true)
354
- const mountedRef = useRef(false)
355
- const requestIdRef = useRef(0)
356
- const selectRef = useRef(select)
357
-
358
- const [state, setState] = useState<UseQueryState<QueryResponse<TMethod>, TData>>(() =>
359
- createInitialState(enabled),
360
- )
361
-
362
- useEffect(() => {
363
- argsRef.current = args
364
- keepPreviousDataRef.current = options?.keepPreviousData ?? true
365
- selectRef.current = select
366
- })
367
-
368
- useEffect(() => {
369
- mountedRef.current = true
370
-
371
- return () => {
372
- mountedRef.current = false
373
- requestIdRef.current += 1
374
- }
375
- }, [])
376
-
377
- const refetch = useCallback(async (): Promise<QueryResponse<TMethod>> => {
378
- const requestId = requestIdRef.current + 1
379
- requestIdRef.current = requestId
380
-
381
- setState((currentState) => startQuery(currentState, keepPreviousDataRef.current))
382
-
383
- try {
384
- const response = (await method(...argsRef.current)) as QueryResponse<TMethod>
385
- const data = selectRef.current(response)
386
-
387
- if (mountedRef.current && requestId === requestIdRef.current) {
388
- setState({
389
- status: 'success',
390
- data,
391
- response,
392
- error: undefined,
393
- isFetching: false,
394
- })
395
- }
396
-
397
- return response
398
- } catch (error) {
399
- if (mountedRef.current && requestId === requestIdRef.current) {
400
- setState((currentState) => ({
401
- status: 'error',
402
- data: keepPreviousDataRef.current ? currentState.data : undefined,
403
- response: keepPreviousDataRef.current ? currentState.response : undefined,
404
- error,
405
- isFetching: false,
406
- }))
407
- }
408
-
409
- throw error
410
- }
411
- }, [method])
412
-
413
- useEffect(() => {
414
- if (!enabled) {
415
- requestIdRef.current += 1
416
- setState(stopQuery)
417
- return
418
- }
419
-
420
- void refetch().catch(() => undefined)
421
-
422
- return () => {
423
- requestIdRef.current += 1
424
- }
425
- }, [enabled, queryKey, refetch])
426
-
427
- const result = toQueryResult(state, refetch)
428
- useDebugValue(result.status)
429
-
430
- return result
431
- }
@@ -1,151 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import { expectType, type TypeEqual } from '@mpen/ts-types'
3
- import {
4
- isRoutekitProblemError,
5
- resolveRoutekitProblemData,
6
- RoutekitProblemError,
7
- type ApiResponse,
8
- type RoutekitProblemSuccessData,
9
- } from './responses'
10
-
11
- interface TodoSuccessBody {
12
- success: true
13
- data: {
14
- id: string
15
- title: string
16
- }
17
- }
18
-
19
- interface TodoProblemBody {
20
- success: false
21
- error: {
22
- code: 'not_found'
23
- message: string
24
- }
25
- issues?: Array<{
26
- code: string
27
- message: string
28
- }>
29
- }
30
-
31
- describe(RoutekitProblemError.name, () => {
32
- test('preserves response details', () => {
33
- const response: ApiResponse<TodoProblemBody> = {
34
- ok: false,
35
- status: 404,
36
- headers: new Headers({ 'x-request-id': 'req_123' }),
37
- body: {
38
- success: false,
39
- error: {
40
- code: 'not_found',
41
- message: 'Todo not found.',
42
- },
43
- },
44
- }
45
-
46
- const error = new RoutekitProblemError(response)
47
-
48
- expect(error).toBeInstanceOf(Error)
49
- expect(error.name).toBe('RoutekitProblemError')
50
- expect(error.message).toBe('Todo not found.')
51
- expect(error.status).toBe(404)
52
- expect(error.headers.get('x-request-id')).toBe('req_123')
53
- expect(error.body.error.code).toBe('not_found')
54
- expect(error.response).toBe(response)
55
- })
56
- })
57
-
58
- describe(isRoutekitProblemError.name, () => {
59
- test('narrows routekit problem errors', () => {
60
- const response: ApiResponse<TodoProblemBody> = {
61
- ok: false,
62
- status: 404,
63
- headers: new Headers(),
64
- body: {
65
- success: false,
66
- error: {
67
- code: 'not_found',
68
- message: 'Todo not found.',
69
- },
70
- },
71
- }
72
- const error: unknown = new RoutekitProblemError(response)
73
-
74
- expect(isRoutekitProblemError<TodoProblemBody>(error)).toBe(true)
75
- if (isRoutekitProblemError<TodoProblemBody>(error)) {
76
- expectType<TypeEqual<typeof error.body, TodoProblemBody>>(true)
77
- expect(error.body.error.code).toBe('not_found')
78
- }
79
- })
80
- })
81
-
82
- describe(resolveRoutekitProblemData.name, () => {
83
- test('unwraps successful response data', async () => {
84
- expectType<
85
- TypeEqual<
86
- RoutekitProblemSuccessData<TodoSuccessBody>,
87
- {
88
- id: string
89
- title: string
90
- }
91
- >
92
- >(true)
93
-
94
- const data = await resolveRoutekitProblemData<TodoSuccessBody, TodoProblemBody>({
95
- ok: true,
96
- status: 200,
97
- headers: new Headers(),
98
- body: {
99
- success: true,
100
- data: {
101
- id: 'todo_123',
102
- title: 'Write tests',
103
- },
104
- },
105
- })
106
-
107
- expectType<
108
- TypeEqual<
109
- typeof data,
110
- {
111
- id: string
112
- title: string
113
- }
114
- >
115
- >(true)
116
- expect(data).toEqual({
117
- id: 'todo_123',
118
- title: 'Write tests',
119
- })
120
- })
121
-
122
- test('throws routekit problem errors with typed bodies', async () => {
123
- expect.assertions(7)
124
-
125
- try {
126
- await resolveRoutekitProblemData<TodoSuccessBody, TodoProblemBody>({
127
- status: 404,
128
- headers: { 'x-request-id': 'req_456' },
129
- body: {
130
- success: false,
131
- error: {
132
- code: 'not_found',
133
- message: 'Todo not found.',
134
- },
135
- issues: [{ code: 'missing', message: 'No matching todo exists.' }],
136
- },
137
- })
138
- } catch (error) {
139
- expect(isRoutekitProblemError<TodoProblemBody>(error)).toBe(true)
140
- if (!isRoutekitProblemError<TodoProblemBody>(error)) return
141
-
142
- expectType<TypeEqual<typeof error.body, TodoProblemBody>>(true)
143
- expect(error.status).toBe(404)
144
- expect(error.headers.get('x-request-id')).toBe('req_456')
145
- expect(error.body.error.code).toBe('not_found')
146
- expect(error.body.issues?.[0]?.code).toBe('missing')
147
- expect(error.response.ok).toBe(false)
148
- expect(error.response.body).toBe(error.body)
149
- }
150
- })
151
- })