@kubb/plugin-fetch 5.9.0-canary.20261024T172532 → 5.9.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/dist/index.cjs +103 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -5
- package/dist/index.d.ts +6 -5
- package/dist/index.js +103 -58
- package/dist/index.js.map +1 -1
- package/dist/templates/fetch.d.ts +12 -4
- package/dist/templates/standardSchema.d.ts +14 -0
- package/docs/options.md +16 -10
- package/package.json +5 -5
- package/templates/fetch.ts +54 -15
- package/templates/standardSchema.ts +55 -0
|
@@ -162,16 +162,19 @@ export type ContentType = string | {
|
|
|
162
162
|
* A Standard Schema validator (zod, valibot, arktype) that parses a value before it is sent or after
|
|
163
163
|
* it is received. `runValidator` runs it through `validateStandardSchema`. Wired through the per-call
|
|
164
164
|
* `validator.request` / `validator.response` / `validator.error` hooks (`error` runs on the error body when a
|
|
165
|
-
* non-2xx call does not throw).
|
|
165
|
+
* non-2xx call does not throw), and `validator.path` / `validator.query` / `validator.headers` for the
|
|
166
|
+
* request params.
|
|
166
167
|
*/
|
|
167
168
|
export type Validator<T = unknown> = StandardSchemaValidator<T>;
|
|
168
169
|
/**
|
|
169
|
-
* The failing
|
|
170
|
-
* `direction` says which
|
|
170
|
+
* The failing value and the call it came from, handed to `onValidationError` alongside the `ParseError`.
|
|
171
|
+
* `direction` says which side rejected it: the request, the success body, or the error body. `source`
|
|
172
|
+
* says which part of it: the body, or one of the request param groups.
|
|
171
173
|
*/
|
|
172
174
|
export type ValidationErrorContext = {
|
|
173
175
|
value: unknown;
|
|
174
176
|
direction: 'request' | 'response' | 'error';
|
|
177
|
+
source: 'body' | 'path' | 'query' | 'headers';
|
|
175
178
|
method?: string;
|
|
176
179
|
url?: string;
|
|
177
180
|
status?: number;
|
|
@@ -194,6 +197,7 @@ export type Auth = {
|
|
|
194
197
|
scheme?: 'bearer' | 'basic';
|
|
195
198
|
name?: string;
|
|
196
199
|
in?: 'header' | 'query' | 'cookie';
|
|
200
|
+
scopes?: Array<string>;
|
|
197
201
|
};
|
|
198
202
|
/**
|
|
199
203
|
* The raw token a consumer returns for a scheme (or `user:password` for basic), or `undefined` to skip it.
|
|
@@ -235,6 +239,9 @@ export type RequestConfig<TBody = unknown, TRequest = Request, TResponse = Respo
|
|
|
235
239
|
codecs?: Record<string, Codec>;
|
|
236
240
|
validator?: {
|
|
237
241
|
request?: Validator;
|
|
242
|
+
path?: Validator;
|
|
243
|
+
query?: Validator;
|
|
244
|
+
headers?: Validator;
|
|
238
245
|
response?: Validator;
|
|
239
246
|
error?: Validator;
|
|
240
247
|
};
|
|
@@ -323,6 +330,7 @@ export type Interceptors<TRequest = Request, TResponse = Response> = {
|
|
|
323
330
|
response: InterceptorStack<TransportResult<unknown, TRequest, TResponse>, RequestConfig<unknown, TRequest, TResponse>>;
|
|
324
331
|
error: InterceptorStack<ResponseError<unknown, TRequest, TResponse>, RequestConfig<unknown, TRequest, TResponse>>;
|
|
325
332
|
};
|
|
333
|
+
export type UrlConfig = Pick<RequestConfig, 'baseURL' | 'url' | 'path' | 'query' | 'params' | 'serializer' | 'styles'>;
|
|
326
334
|
/**
|
|
327
335
|
* A client instance: the callable send plus configuration, interceptors, and an isolated
|
|
328
336
|
* `createClient` factory bound to the same transport.
|
|
@@ -331,7 +339,7 @@ export type ClientInstance<TRequest = Request, TResponse = Response> = {
|
|
|
331
339
|
<TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>>;
|
|
332
340
|
getConfig: () => ClientConfig<TRequest, TResponse>;
|
|
333
341
|
setConfig: (config: ClientConfig<TRequest, TResponse>) => ClientConfig<TRequest, TResponse>;
|
|
334
|
-
getUrl:
|
|
342
|
+
getUrl: (config: UrlConfig) => string;
|
|
335
343
|
interceptors: Interceptors<TRequest, TResponse>;
|
|
336
344
|
createClient: (config?: ClientConfig<TRequest, TResponse>) => ClientInstance<TRequest, TResponse>;
|
|
337
345
|
};
|
|
@@ -48,4 +48,18 @@ export declare class ParseError extends Error {
|
|
|
48
48
|
* const pet = await validateStandardSchema(PetSchema, rawData)
|
|
49
49
|
*/
|
|
50
50
|
export declare function validateStandardSchema<TOutput>(schema: StandardSchemaValidator<TOutput>, value: unknown): Promise<TOutput>;
|
|
51
|
+
/**
|
|
52
|
+
* Wraps an object schema so its keys match case-insensitively.
|
|
53
|
+
* Keys the schema does not declare pass through untouched.
|
|
54
|
+
* The declared keys are validated with `schema` under their declared names.
|
|
55
|
+
* Every key, and every issue path, keeps the casing the caller used.
|
|
56
|
+
*
|
|
57
|
+
* @param schema Standard Schema-compatible object schema.
|
|
58
|
+
* @param entries The record of keys the passed object schema validates.
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* validator: { headers: toHeadersStandardSchema(putEventHeadersSchema, putEventHeadersSchema.shape) }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function toHeadersStandardSchema(schema: StandardSchemaValidator, entries: Record<string, unknown>): StandardSchemaValidator;
|
|
51
65
|
//#endregion
|
package/docs/options.md
CHANGED
|
@@ -10,7 +10,7 @@ Pass these options to `pluginFetch()`. Shared options link to [Shared plugin opt
|
|
|
10
10
|
| [`group`](https://kubb.dev/docs/5.x/reference/plugin-options#group) | Split output by tag or path: `type`, `name`. | None |
|
|
11
11
|
| [`baseURL`](#baseurl) | Base URL prepended to every request. | None |
|
|
12
12
|
| [`throwOnErrorDefault`](#throwonerrordefault) | Default error behavior and return type for generated operations. | `true` |
|
|
13
|
-
| [`validator`](#validator) | Validate request and
|
|
13
|
+
| [`validator`](#validator) | Validate request bodies, params, and responses with Zod. | `false` |
|
|
14
14
|
| [`sdk`](#sdk) | Generate a class-based SDK instead of functions. | None |
|
|
15
15
|
| [`returnType`](#returntype) | Shape of the value a generated call resolves to. | `'full'` |
|
|
16
16
|
| [`include`](https://kubb.dev/docs/5.x/reference/plugin-options#include) | Keep only operations that match. | None |
|
|
@@ -48,13 +48,13 @@ if (result.status !== 200) console.error(result.status, result.error)
|
|
|
48
48
|
|
|
49
49
|
### validator
|
|
50
50
|
|
|
51
|
-
Validates request and response bodies with schemas from `@kubb/plugin-zod`, so add `pluginZod()` too. An invalid
|
|
51
|
+
Validates request bodies, path, query, and header params, and response bodies with schemas from `@kubb/plugin-zod`, so add `pluginZod()` too. An invalid value makes the generated function throw a `ParseError`.
|
|
52
52
|
|
|
53
|
-
| |
|
|
54
|
-
| -------- |
|
|
55
|
-
| Type | `false \| 'zod' \| { request?: 'zod'; response?: 'zod' }` |
|
|
56
|
-
| Required | `false`
|
|
57
|
-
| Default | `false`
|
|
53
|
+
| | |
|
|
54
|
+
| -------- | ------------------------------------------------------------------------- |
|
|
55
|
+
| Type | `false \| 'zod' \| { request?: 'zod'; params?: 'zod'; response?: 'zod' }` |
|
|
56
|
+
| Required | `false` |
|
|
57
|
+
| Default | `false` |
|
|
58
58
|
|
|
59
59
|
::field-group
|
|
60
60
|
|
|
@@ -66,8 +66,8 @@ Default value. Does no validation and returns the response cast to the generated
|
|
|
66
66
|
Validates the success response body, and the error body when a non-2xx call does not throw.
|
|
67
67
|
:::
|
|
68
68
|
|
|
69
|
-
:::field{name="{ request?: 'zod', response?: 'zod' }"}
|
|
70
|
-
Opts in per direction. `request` validates the request body
|
|
69
|
+
:::field{name="{ request?: 'zod', params?: 'zod', response?: 'zod' }"}
|
|
70
|
+
Opts in per direction. `request` validates the request body and `params` validates the path, query, and header params before the call. `response` validates the response body after it.
|
|
71
71
|
:::
|
|
72
72
|
|
|
73
73
|
::
|
|
@@ -84,11 +84,17 @@ export default defineConfig({
|
|
|
84
84
|
plugins: [
|
|
85
85
|
pluginTs(),
|
|
86
86
|
pluginZod(),
|
|
87
|
-
pluginFetch({ validator: { request: 'zod', response: 'zod' } }),
|
|
87
|
+
pluginFetch({ validator: { request: 'zod', params: 'zod', response: 'zod' } }),
|
|
88
88
|
],
|
|
89
89
|
})
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
`params` runs each group through its generated `<operation>PathSchema`, `QuerySchema`, or `HeadersSchema`, and sends what the schema returns. A codec param such as a `Date` goes out in its wire form, and an invalid param fails before the request. Values added by `auth` are not validated.
|
|
93
|
+
|
|
94
|
+
Only the params your OpenAPI spec declares are sent. Path and query keys the spec does not declare are stripped, so a key such as an undocumented filter or a cache-buster never reaches the URL. Declare it in the spec, or leave `params` off. Headers are the exception: client-level `headers` (such as `Authorization` or tracing headers) are merged with the call's headers before validation, so headers the spec does not declare pass through unchanged, and declared header params match in any casing.
|
|
95
|
+
|
|
96
|
+
Pair `request` and `params` with `pluginZod({ inferred: 'direction' })` when the schemas use codecs, so the request types accept the domain values (such as a `Date`) that the validators encode. See [`inferred`](https://kubb.dev/extensions/kubb-labs/plugin-zod/options#inferred).
|
|
97
|
+
|
|
92
98
|
### sdk
|
|
93
99
|
|
|
94
100
|
Generates a class-based SDK. Each instance takes its own client config, so environments can use separate clients. Leave `sdk` unset to keep the standalone functions that query plugins call.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/plugin-fetch",
|
|
3
|
-
"version": "5.9.0
|
|
3
|
+
"version": "5.9.0",
|
|
4
4
|
"description": "Generate a slim, type-safe HTTP client with Kubb based on the native Fetch api.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api-client",
|
|
@@ -80,12 +80,12 @@
|
|
|
80
80
|
"registry": "https://registry.npmjs.org/"
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@kubb/plugin-ts": "^5.
|
|
84
|
-
"@kubb/plugin-zod": "^5.7.0
|
|
83
|
+
"@kubb/plugin-ts": "^5.1.0",
|
|
84
|
+
"@kubb/plugin-zod": "^5.7.0"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
|
-
"@internals/client": "0.
|
|
88
|
-
"@internals/shared": "0.
|
|
87
|
+
"@internals/client": "0.0.0",
|
|
88
|
+
"@internals/shared": "0.0.0",
|
|
89
89
|
"kubb": "^5.13.0",
|
|
90
90
|
"typescript": "^7.0.2"
|
|
91
91
|
},
|
package/templates/fetch.ts
CHANGED
|
@@ -161,17 +161,20 @@ export type ContentType = string | { request?: string; response?: string }
|
|
|
161
161
|
* A Standard Schema validator (zod, valibot, arktype) that parses a value before it is sent or after
|
|
162
162
|
* it is received. `runValidator` runs it through `validateStandardSchema`. Wired through the per-call
|
|
163
163
|
* `validator.request` / `validator.response` / `validator.error` hooks (`error` runs on the error body when a
|
|
164
|
-
* non-2xx call does not throw).
|
|
164
|
+
* non-2xx call does not throw), and `validator.path` / `validator.query` / `validator.headers` for the
|
|
165
|
+
* request params.
|
|
165
166
|
*/
|
|
166
167
|
export type Validator<T = unknown> = StandardSchemaValidator<T>
|
|
167
168
|
|
|
168
169
|
/**
|
|
169
|
-
* The failing
|
|
170
|
-
* `direction` says which
|
|
170
|
+
* The failing value and the call it came from, handed to `onValidationError` alongside the `ParseError`.
|
|
171
|
+
* `direction` says which side rejected it: the request, the success body, or the error body. `source`
|
|
172
|
+
* says which part of it: the body, or one of the request param groups.
|
|
171
173
|
*/
|
|
172
174
|
export type ValidationErrorContext = {
|
|
173
175
|
value: unknown
|
|
174
176
|
direction: 'request' | 'response' | 'error'
|
|
177
|
+
source: 'body' | 'path' | 'query' | 'headers'
|
|
175
178
|
method?: string
|
|
176
179
|
url?: string
|
|
177
180
|
status?: number
|
|
@@ -192,6 +195,7 @@ export type Auth = {
|
|
|
192
195
|
scheme?: 'bearer' | 'basic'
|
|
193
196
|
name?: string
|
|
194
197
|
in?: 'header' | 'query' | 'cookie'
|
|
198
|
+
scopes?: Array<string>
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
/**
|
|
@@ -233,7 +237,7 @@ export type RequestConfig<TBody = unknown, TRequest = Request, TResponse = Respo
|
|
|
233
237
|
transport?: Transport<TRequest, TResponse>
|
|
234
238
|
serializer?: Serializers
|
|
235
239
|
codecs?: Record<string, Codec>
|
|
236
|
-
validator?: { request?: Validator; response?: Validator; error?: Validator }
|
|
240
|
+
validator?: { request?: Validator; path?: Validator; query?: Validator; headers?: Validator; response?: Validator; error?: Validator }
|
|
237
241
|
onValidationError?: ValidationErrorHandler
|
|
238
242
|
security?: Array<Auth>
|
|
239
243
|
auth?: AuthResolver
|
|
@@ -333,6 +337,8 @@ export type Interceptors<TRequest = Request, TResponse = Response> = {
|
|
|
333
337
|
error: InterceptorStack<ResponseError<unknown, TRequest, TResponse>, RequestConfig<unknown, TRequest, TResponse>>
|
|
334
338
|
}
|
|
335
339
|
|
|
340
|
+
export type UrlConfig = Pick<RequestConfig, 'baseURL' | 'url' | 'path' | 'query' | 'params' | 'serializer' | 'styles'>
|
|
341
|
+
|
|
336
342
|
/**
|
|
337
343
|
* A client instance: the callable send plus configuration, interceptors, and an isolated
|
|
338
344
|
* `createClient` factory bound to the same transport.
|
|
@@ -341,7 +347,7 @@ export type ClientInstance<TRequest = Request, TResponse = Response> = {
|
|
|
341
347
|
<TBody = unknown>(config: RequestConfig<TBody, TRequest, TResponse>): Promise<CallResult<TRequest, TResponse>>
|
|
342
348
|
getConfig: () => ClientConfig<TRequest, TResponse>
|
|
343
349
|
setConfig: (config: ClientConfig<TRequest, TResponse>) => ClientConfig<TRequest, TResponse>
|
|
344
|
-
getUrl:
|
|
350
|
+
getUrl: (config: UrlConfig) => string
|
|
345
351
|
interceptors: Interceptors<TRequest, TResponse>
|
|
346
352
|
createClient: (config?: ClientConfig<TRequest, TResponse>) => ClientInstance<TRequest, TResponse>
|
|
347
353
|
}
|
|
@@ -391,9 +397,14 @@ export class ResponseError<TError = unknown, TRequest = Request, TResponse = Res
|
|
|
391
397
|
|
|
392
398
|
export type ResponseErrorConfig<TError = unknown> = ResponseError<TError>
|
|
393
399
|
|
|
400
|
+
function headerEntries(headers: object): Array<[string, unknown]> {
|
|
401
|
+
if (Array.isArray(headers)) return headers
|
|
402
|
+
return headers instanceof Headers ? [...headers] : Object.entries(headers)
|
|
403
|
+
}
|
|
404
|
+
|
|
394
405
|
function serializeHeaders(headers: HeadersInit | undefined): Record<string, string> {
|
|
395
406
|
if (!headers) return {}
|
|
396
|
-
const entries =
|
|
407
|
+
const entries = headerEntries(headers)
|
|
397
408
|
const result: Record<string, string> = {}
|
|
398
409
|
for (const [key, value] of entries) {
|
|
399
410
|
if (value === undefined || value === null) continue
|
|
@@ -402,8 +413,22 @@ function serializeHeaders(headers: HeadersInit | undefined): Record<string, stri
|
|
|
402
413
|
return result
|
|
403
414
|
}
|
|
404
415
|
|
|
405
|
-
function
|
|
406
|
-
|
|
416
|
+
function collectHeadersCaseInsensitive(...sources: Array<unknown>): Record<string, unknown> {
|
|
417
|
+
const keys = new Map<string, string>()
|
|
418
|
+
const result: Record<string, unknown> = {}
|
|
419
|
+
for (const source of sources) {
|
|
420
|
+
if (!source) continue
|
|
421
|
+
const entries = headerEntries(source)
|
|
422
|
+
for (const [key, value] of entries) {
|
|
423
|
+
if (value === undefined || value === null) continue
|
|
424
|
+
const lowerCase = key.toLowerCase()
|
|
425
|
+
const previous = keys.get(lowerCase)
|
|
426
|
+
if (previous !== undefined) delete result[previous]
|
|
427
|
+
keys.set(lowerCase, key)
|
|
428
|
+
result[key] = value
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return result
|
|
407
432
|
}
|
|
408
433
|
|
|
409
434
|
function getHeader(headers: Record<string, string>, name: string): string | undefined {
|
|
@@ -572,15 +597,29 @@ async function resolveRequest<TBody, TRequest, TResponse>({
|
|
|
572
597
|
}): Promise<{ request: ResolvedRequest; codecs: Record<string, Codec> }> {
|
|
573
598
|
const { querySerializer, bodySerializer, pathSerializer } = resolveSerializers({ config, requestConfig })
|
|
574
599
|
const codecs = { ...config.codecs, ...requestConfig.codecs }
|
|
600
|
+
const onValidationError = requestConfig.onValidationError ?? config.onValidationError
|
|
601
|
+
const validationContext = { direction: 'request', method: requestConfig.method, url: requestConfig.url } as const
|
|
602
|
+
|
|
603
|
+
// Params are encoded before anything reads them: headers before styling, query before auth adds its values.
|
|
604
|
+
const paramValues = {
|
|
605
|
+
path: { ...(requestConfig.path as Record<string, unknown> | undefined) },
|
|
606
|
+
query: { ...((requestConfig.query ?? requestConfig.params) as Record<string, unknown> | undefined) },
|
|
607
|
+
headers: collectHeadersCaseInsensitive(config.headers, requestConfig.headers),
|
|
608
|
+
}
|
|
609
|
+
const [validatedPath, validatedQuery, validatedHeaders] = await Promise.all(
|
|
610
|
+
(['path', 'query', 'headers'] as const).map((source) =>
|
|
611
|
+
runValidator({ validator: requestConfig.validator?.[source], value: paramValues[source], context: { ...validationContext, source }, onValidationError }),
|
|
612
|
+
),
|
|
613
|
+
)
|
|
575
614
|
|
|
576
|
-
const headers =
|
|
615
|
+
const headers = serializeHeaders(applyHeaderStyles(validatedHeaders as HeadersInit | undefined, requestConfig.styles?.header))
|
|
577
616
|
const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType)
|
|
578
617
|
const requestContentType = requestContentTypeOption ?? getHeader(headers, 'content-type')
|
|
579
618
|
if (responseContentType && !hasHeader(headers, 'accept')) {
|
|
580
619
|
headers['Accept'] = responseContentType
|
|
581
620
|
}
|
|
582
621
|
|
|
583
|
-
const query: Record<string, unknown> = { ...(
|
|
622
|
+
const query: Record<string, unknown> = { ...(validatedQuery as Record<string, unknown> | undefined) }
|
|
584
623
|
|
|
585
624
|
await resolveAuth({
|
|
586
625
|
security: requestConfig.security,
|
|
@@ -597,8 +636,8 @@ async function resolveRequest<TBody, TRequest, TResponse>({
|
|
|
597
636
|
const validatedBody = await runValidator({
|
|
598
637
|
validator: requestConfig.validator?.request,
|
|
599
638
|
value: requestConfig.body,
|
|
600
|
-
context: {
|
|
601
|
-
onValidationError
|
|
639
|
+
context: { ...validationContext, source: 'body' },
|
|
640
|
+
onValidationError,
|
|
602
641
|
})
|
|
603
642
|
const requestContentTypeBase = baseContentType(requestContentType)
|
|
604
643
|
const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined
|
|
@@ -619,7 +658,7 @@ async function resolveRequest<TBody, TRequest, TResponse>({
|
|
|
619
658
|
|
|
620
659
|
const url = serializeUrl({
|
|
621
660
|
parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url],
|
|
622
|
-
pathParams: (
|
|
661
|
+
pathParams: (validatedPath ?? {}) as Record<string, unknown>,
|
|
623
662
|
search: querySerializer(query, requestConfig.styles?.query),
|
|
624
663
|
pathSerializer,
|
|
625
664
|
pathStyles: requestConfig.styles?.path,
|
|
@@ -680,7 +719,7 @@ async function settleResult<TRequest, TResponse>({
|
|
|
680
719
|
const data = await runValidator({
|
|
681
720
|
validator: validator?.response,
|
|
682
721
|
value: decoded,
|
|
683
|
-
context: { direction: 'response', ...validationContext },
|
|
722
|
+
context: { direction: 'response', source: 'body', ...validationContext },
|
|
684
723
|
onValidationError,
|
|
685
724
|
})
|
|
686
725
|
return { status: result.status, data, error: undefined, contentType, request: result.request, response: result.response }
|
|
@@ -689,7 +728,7 @@ async function settleResult<TRequest, TResponse>({
|
|
|
689
728
|
const error = await runValidator({
|
|
690
729
|
validator: validator?.error,
|
|
691
730
|
value: decoded,
|
|
692
|
-
context: { direction: 'error', ...validationContext },
|
|
731
|
+
context: { direction: 'error', source: 'body', ...validationContext },
|
|
693
732
|
onValidationError,
|
|
694
733
|
})
|
|
695
734
|
if (throwOnError) {
|
|
@@ -52,3 +52,58 @@ export async function validateStandardSchema<TOutput>(schema: StandardSchemaVali
|
|
|
52
52
|
}
|
|
53
53
|
return (result as { value: TOutput }).value
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Wraps an object schema so its keys match case-insensitively.
|
|
58
|
+
* Keys the schema does not declare pass through untouched.
|
|
59
|
+
* The declared keys are validated with `schema` under their declared names.
|
|
60
|
+
* Every key, and every issue path, keeps the casing the caller used.
|
|
61
|
+
*
|
|
62
|
+
* @param schema Standard Schema-compatible object schema.
|
|
63
|
+
* @param entries The record of keys the passed object schema validates.
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* validator: { headers: toHeadersStandardSchema(putEventHeadersSchema, putEventHeadersSchema.shape) }
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function toHeadersStandardSchema(schema: StandardSchemaValidator, entries: Record<string, unknown>): StandardSchemaValidator {
|
|
70
|
+
const declared = new Map(Object.keys(entries).map((name) => [name.toLowerCase(), name]))
|
|
71
|
+
|
|
72
|
+
function validate(value: unknown): StandardSchemaResult<unknown> | Promise<StandardSchemaResult<unknown>> {
|
|
73
|
+
if (typeof value !== 'object' || value === null) {
|
|
74
|
+
return { issues: [{ message: 'Expected an object' }] }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const originalKeys = new Map<string, string>()
|
|
78
|
+
const declaredValues: Record<string, unknown> = {}
|
|
79
|
+
const undeclaredValues: Record<string, unknown> = {}
|
|
80
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
81
|
+
const name = declared.get(key.toLowerCase())
|
|
82
|
+
if (name === undefined) {
|
|
83
|
+
undeclaredValues[key] = entry
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
originalKeys.set(name, key)
|
|
87
|
+
declaredValues[name] = entry
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const toOriginal = (key: string) => originalKeys.get(key) ?? key
|
|
91
|
+
const settle = (result: StandardSchemaResult<unknown>): StandardSchemaResult<unknown> => {
|
|
92
|
+
if (result.issues) {
|
|
93
|
+
return {
|
|
94
|
+
issues: result.issues.map((issue): StandardSchemaIssue => ({
|
|
95
|
+
...issue,
|
|
96
|
+
path: issue.path?.map((segment, index) => (index === 0 && typeof segment === 'string' ? toOriginal(segment) : segment)),
|
|
97
|
+
})),
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const validated = Object.entries((result.value ?? {}) as Record<string, unknown>).map(([key, entry]) => [toOriginal(key), entry])
|
|
101
|
+
return { value: { ...undeclaredValues, ...Object.fromEntries(validated) } }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result = schema['~standard'].validate(declaredValues)
|
|
105
|
+
return result instanceof Promise ? result.then(settle) : settle(result)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { '~standard': { validate } }
|
|
109
|
+
}
|