galbe 0.13.0 → 0.14.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/README.md +18 -1
- package/bin/commands/build.ts +10 -4
- package/bin/commands/generate/cli/index.ts +156 -0
- package/bin/commands/generate/cli/targets/cac.ts +535 -0
- package/bin/commands/generate/client.ts +32 -109
- package/bin/commands/generate/code/openapi.parser.ts +428 -133
- package/bin/commands/generate/code/route-merge.ts +248 -0
- package/bin/commands/generate/code.ts +110 -23
- package/bin/commands/generate/index.ts +2 -0
- package/package.json +4 -1
- package/src/cookies.ts +87 -0
- package/src/extras/spec/openapi.serializer.ts +236 -101
- package/src/extras.ts +1 -0
- package/src/index.ts +4 -7
- package/src/parser.ts +104 -63
- package/src/router.ts +34 -20
- package/src/routes.ts +14 -10
- package/src/schema.ts +82 -95
- package/src/server.ts +35 -21
- package/src/types.ts +180 -61
- package/src/util.ts +10 -18
- package/src/validator.ts +62 -32
- package/bin/res/cli.template.js +0 -122
package/src/types.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
STBoolean,
|
|
6
6
|
STByteArray,
|
|
7
7
|
STInteger,
|
|
8
|
+
STIntersection,
|
|
8
9
|
STJson,
|
|
9
10
|
STLiteral,
|
|
10
11
|
STMultipartForm,
|
|
@@ -20,6 +21,9 @@ import type {
|
|
|
20
21
|
} from './schema'
|
|
21
22
|
import type { Galbe } from './index'
|
|
22
23
|
import { HttpStatus } from './util'
|
|
24
|
+
import type { CookieOptions } from './cookies'
|
|
25
|
+
|
|
26
|
+
export type MediaType = `${string}/${string}`
|
|
23
27
|
|
|
24
28
|
export type STResponseValue =
|
|
25
29
|
| STByteArray
|
|
@@ -32,23 +36,54 @@ export type STResponseValue =
|
|
|
32
36
|
| STJson
|
|
33
37
|
| STArray
|
|
34
38
|
| STUnion
|
|
39
|
+
| STIntersection<any>
|
|
35
40
|
| STStream
|
|
36
41
|
| STAny
|
|
37
42
|
| STNull
|
|
38
|
-
|
|
43
|
+
|
|
44
|
+
export type STBodyValue =
|
|
45
|
+
| STByteArray
|
|
46
|
+
| STStream
|
|
47
|
+
| STString
|
|
48
|
+
| STLiteral
|
|
49
|
+
| STBoolean
|
|
50
|
+
| STNumber
|
|
51
|
+
| STInteger
|
|
52
|
+
| STObject
|
|
53
|
+
| STJson
|
|
54
|
+
| STArray
|
|
55
|
+
| STUnion
|
|
56
|
+
| STIntersection<any>
|
|
57
|
+
| STMultipartForm
|
|
58
|
+
| STAny
|
|
59
|
+
|
|
60
|
+
export type STBodyContent = Partial<Record<MediaType, STBodyValue>>
|
|
61
|
+
export type STBody = STNull | STBodyContent
|
|
62
|
+
export type STBodyType = MediaType
|
|
63
|
+
|
|
64
|
+
export type STResponseBodyValue =
|
|
65
|
+
| STByteArray
|
|
66
|
+
| STStream
|
|
67
|
+
| STString
|
|
68
|
+
| STLiteral
|
|
69
|
+
| STBoolean
|
|
70
|
+
| STNumber
|
|
71
|
+
| STInteger
|
|
72
|
+
| STObject
|
|
73
|
+
| STJson
|
|
74
|
+
| STArray
|
|
75
|
+
| STUnion
|
|
76
|
+
| STIntersection<any>
|
|
77
|
+
| STAny
|
|
39
78
|
| STNull
|
|
40
|
-
| Partial<{
|
|
41
|
-
byteArray?: STByteArray | STStream
|
|
42
|
-
text?: STString | STLiteral | STBoolean | STNumber | STInteger | STUnion | STStream
|
|
43
|
-
json?: STJson | STObject | STBoolean | STInteger | STNumber | STString | STArray | STUnion
|
|
44
|
-
urlForm?: STObject | STStream | STUnion
|
|
45
|
-
multipart?: STMultipartForm | STStream | STUnion
|
|
46
|
-
default?: STString | STByteArray | STStream | STAny
|
|
47
|
-
}>
|
|
48
|
-
export type STBodyType = keyof STBody
|
|
49
|
-
export type STBodyValue = STBody[STBodyType]
|
|
50
79
|
|
|
51
|
-
export type
|
|
80
|
+
export type STResponseContent = Partial<Record<MediaType, STResponseBodyValue>> & {
|
|
81
|
+
description?: string
|
|
82
|
+
responseHeaders?: Record<string, STSchema>
|
|
83
|
+
}
|
|
84
|
+
export type STResponseBodyKey = MediaType
|
|
85
|
+
export type STResponseEntry = STResponseValue | STResponseContent
|
|
86
|
+
export type STResponse = Partial<Record<number | 'default', STResponseEntry>>
|
|
52
87
|
|
|
53
88
|
export type MaybeArray<T> = T | T[]
|
|
54
89
|
export type MaybeSTArray<T extends STSchema> = T | STArray<T>
|
|
@@ -60,10 +95,10 @@ type MaybePromise<T> = T | Promise<T>
|
|
|
60
95
|
export type ExtractParams<T extends string> = T extends `/:${infer P}/${infer Rest}`
|
|
61
96
|
? P | ExtractParams<Rest>
|
|
62
97
|
: T extends `${infer _}:${infer P}/${infer Rest}`
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
98
|
+
? P | ExtractParams<Rest>
|
|
99
|
+
: T extends `${infer _}:${infer P}`
|
|
100
|
+
? P
|
|
101
|
+
: never
|
|
67
102
|
|
|
68
103
|
type STHeadersPrimaryValue = STString | STBoolean | STNumber | STInteger | STLiteral
|
|
69
104
|
type STHeadersValue = MaybeSTUnion<STHeadersPrimaryValue>
|
|
@@ -142,7 +177,7 @@ export type RequestSchema<
|
|
|
142
177
|
P extends Partial<STParams<Path>> = Partial<STParams<Path>>,
|
|
143
178
|
Q extends STQuery = STQuery,
|
|
144
179
|
B extends STBody = STBody,
|
|
145
|
-
R extends Partial<STResponse> = STResponse
|
|
180
|
+
R extends Partial<STResponse> = STResponse,
|
|
146
181
|
> = {
|
|
147
182
|
headers?: H
|
|
148
183
|
params?: P
|
|
@@ -160,13 +195,21 @@ type OmitNotDefined<S extends RequestSchema> = {
|
|
|
160
195
|
never]: Static<STObject<Exclude<S['params'], undefined>>>[K]
|
|
161
196
|
}
|
|
162
197
|
type StaticBody<T extends STSchema> = T extends STOptional<STSchema> ? Static<T> | null : Static<T>
|
|
198
|
+
export type ContextSet = {
|
|
199
|
+
headers: {
|
|
200
|
+
'set-cookie': string[]
|
|
201
|
+
[header: string]: string | string[]
|
|
202
|
+
}
|
|
203
|
+
status?: number
|
|
204
|
+
cookie: (name: string, value: string, opt?: CookieOptions) => void
|
|
205
|
+
}
|
|
163
206
|
export type Context<
|
|
164
207
|
M extends Method = Method,
|
|
165
208
|
Path extends string = string,
|
|
166
|
-
S extends RequestSchema = RequestSchema
|
|
167
|
-
> =
|
|
168
|
-
|
|
169
|
-
|
|
209
|
+
S extends RequestSchema = RequestSchema,
|
|
210
|
+
> = 0 extends 1 & Exclude<S['body'], undefined | STNull>
|
|
211
|
+
? {
|
|
212
|
+
[K in keyof Exclude<S['body'], undefined | STNull>]: {
|
|
170
213
|
headers: Static<STObject<Exclude<S['headers'], undefined>>>
|
|
171
214
|
params: {
|
|
172
215
|
[P in ExtractParams<Path>]: P extends keyof OmitNotDefined<S> ? OmitNotDefined<S>[P] : string
|
|
@@ -176,24 +219,51 @@ export type Context<
|
|
|
176
219
|
body: M extends 'get' | 'options' | 'head'
|
|
177
220
|
? null
|
|
178
221
|
: Exclude<S['body'], undefined> extends STNull
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
? StaticBody<Exclude<Exclude<S['body'], undefined>[K], undefined>>
|
|
182
|
-
: never
|
|
222
|
+
? null
|
|
223
|
+
: StaticBody<Extract<Exclude<Exclude<S['body'], undefined | STNull>[K], undefined>, STSchema>>
|
|
183
224
|
request: Request
|
|
184
225
|
remoteAddress: SocketAddress | null
|
|
185
226
|
route?: Route
|
|
186
227
|
state: Record<string, any>
|
|
187
|
-
set:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
228
|
+
set: ContextSet
|
|
229
|
+
cookies: Record<string, string>
|
|
230
|
+
}
|
|
231
|
+
}[keyof Exclude<S['body'], undefined | STNull>]
|
|
232
|
+
: [Exclude<S['body'], undefined | STNull>] extends [never]
|
|
233
|
+
? {
|
|
234
|
+
headers: Static<STObject<Exclude<S['headers'], undefined>>>
|
|
235
|
+
params: {
|
|
236
|
+
[P in ExtractParams<Path>]: P extends keyof OmitNotDefined<S> ? OmitNotDefined<S>[P] : string
|
|
193
237
|
}
|
|
238
|
+
query: Static<STObject<Exclude<S['query'], undefined>>>
|
|
239
|
+
contentType: undefined
|
|
240
|
+
body: null
|
|
241
|
+
request: Request
|
|
242
|
+
remoteAddress: SocketAddress | null
|
|
243
|
+
route?: Route
|
|
244
|
+
state: Record<string, any>
|
|
245
|
+
set: ContextSet
|
|
246
|
+
cookies: Record<string, string>
|
|
194
247
|
}
|
|
195
|
-
:
|
|
196
|
-
|
|
248
|
+
: {
|
|
249
|
+
[K in keyof Exclude<S['body'], undefined | STNull>]: {
|
|
250
|
+
headers: Static<STObject<Exclude<S['headers'], undefined>>>
|
|
251
|
+
params: {
|
|
252
|
+
[P in ExtractParams<Path>]: P extends keyof OmitNotDefined<S> ? OmitNotDefined<S>[P] : string
|
|
253
|
+
}
|
|
254
|
+
query: Static<STObject<Exclude<S['query'], undefined>>>
|
|
255
|
+
contentType: M extends 'get' | 'options' | 'head' ? undefined : K
|
|
256
|
+
body: M extends 'get' | 'options' | 'head'
|
|
257
|
+
? null
|
|
258
|
+
: StaticBody<Extract<Exclude<Exclude<S['body'], undefined | STNull>[K], undefined>, STSchema>>
|
|
259
|
+
request: Request
|
|
260
|
+
remoteAddress: SocketAddress | null
|
|
261
|
+
route?: Route
|
|
262
|
+
state: Record<string, any>
|
|
263
|
+
set: ContextSet
|
|
264
|
+
cookies: Record<string, string>
|
|
265
|
+
}
|
|
266
|
+
}[keyof Exclude<S['body'], undefined | STNull>]
|
|
197
267
|
export type Next = () => void | Promise<any>
|
|
198
268
|
export type Hook<M extends Method = Method, Path extends string = string, S extends RequestSchema = RequestSchema> = (
|
|
199
269
|
ctx: Context<M, Path, S>,
|
|
@@ -202,7 +272,7 @@ export type Hook<M extends Method = Method, Path extends string = string, S exte
|
|
|
202
272
|
export type Handler<
|
|
203
273
|
M extends Method = Method,
|
|
204
274
|
Path extends string = string,
|
|
205
|
-
S extends RequestSchema = RequestSchema
|
|
275
|
+
S extends RequestSchema = RequestSchema,
|
|
206
276
|
> = (ctx: Context<M, Path, S>) => any
|
|
207
277
|
export type Endpoint<M extends Method> = {
|
|
208
278
|
<
|
|
@@ -211,7 +281,7 @@ export type Endpoint<M extends Method> = {
|
|
|
211
281
|
H extends STHeaders = any,
|
|
212
282
|
Q extends STQuery = any,
|
|
213
283
|
B extends STBody = any,
|
|
214
|
-
R extends STResponse = STResponse
|
|
284
|
+
R extends STResponse = STResponse,
|
|
215
285
|
>(
|
|
216
286
|
path: Path,
|
|
217
287
|
schema: RequestSchema<M, Path, H, P, Q, B, R>,
|
|
@@ -224,7 +294,7 @@ export type Endpoint<M extends Method> = {
|
|
|
224
294
|
H extends STHeaders = any,
|
|
225
295
|
Q extends STQuery = any,
|
|
226
296
|
B extends STBody = any,
|
|
227
|
-
R extends STResponse = STResponse
|
|
297
|
+
R extends STResponse = STResponse,
|
|
228
298
|
>(
|
|
229
299
|
path: Path,
|
|
230
300
|
schema: RequestSchema<M, Path, H, P, Q, B, R>,
|
|
@@ -236,7 +306,7 @@ export type Endpoint<M extends Method> = {
|
|
|
236
306
|
H extends STHeaders = any,
|
|
237
307
|
Q extends STQuery = any,
|
|
238
308
|
B extends STBody = any,
|
|
239
|
-
R extends STResponse = STResponse
|
|
309
|
+
R extends STResponse = STResponse,
|
|
240
310
|
>(
|
|
241
311
|
path: Path,
|
|
242
312
|
hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[],
|
|
@@ -248,7 +318,7 @@ export type Endpoint<M extends Method> = {
|
|
|
248
318
|
H extends STHeaders = any,
|
|
249
319
|
Q extends STQuery = any,
|
|
250
320
|
B extends STBody = any,
|
|
251
|
-
R extends STResponse = STResponse
|
|
321
|
+
R extends STResponse = STResponse,
|
|
252
322
|
>(
|
|
253
323
|
path: Path,
|
|
254
324
|
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
@@ -264,12 +334,19 @@ export type StaticEndpoint<P extends string = string, T extends string = string>
|
|
|
264
334
|
options?: StaticEndpointOptions
|
|
265
335
|
) => Route<'get', P, {}, {}, {}, STBody, STResponse, T>
|
|
266
336
|
|
|
267
|
-
export class RequestError {
|
|
337
|
+
export class RequestError extends Error {
|
|
268
338
|
status: number
|
|
269
339
|
payload?: any
|
|
270
340
|
headers?: Record<string, string>
|
|
271
|
-
constructor(options: { status?: number; payload?: any; headers?: Record<string, string> }) {
|
|
272
|
-
|
|
341
|
+
constructor(options: { status?: number; payload?: any; headers?: Record<string, string> } = {}) {
|
|
342
|
+
const status = options.status ?? 400
|
|
343
|
+
const message =
|
|
344
|
+
typeof options.payload === 'string'
|
|
345
|
+
? options.payload
|
|
346
|
+
: HttpStatus[status as keyof typeof HttpStatus] ?? 'Request Error'
|
|
347
|
+
super(message)
|
|
348
|
+
this.name = new.target?.name ?? 'RequestError'
|
|
349
|
+
this.status = status
|
|
273
350
|
this.payload = options.payload
|
|
274
351
|
this.headers = options.headers
|
|
275
352
|
}
|
|
@@ -292,7 +369,7 @@ export type Route<
|
|
|
292
369
|
B extends STBody = STBody,
|
|
293
370
|
R extends STResponse = STResponse,
|
|
294
371
|
SP extends string = string,
|
|
295
|
-
SR extends string = string
|
|
372
|
+
SR extends string = string,
|
|
296
373
|
> = {
|
|
297
374
|
method: M
|
|
298
375
|
path: Path
|
|
@@ -303,29 +380,57 @@ export type Route<
|
|
|
303
380
|
static?: { path: SP; root: SR }
|
|
304
381
|
}
|
|
305
382
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
383
|
+
const mkErr = (status: number) =>
|
|
384
|
+
class extends RequestError {
|
|
385
|
+
constructor(payload?: any, headers?: Record<string, string>) {
|
|
386
|
+
super({ status, payload: payload ?? HttpStatus[status as keyof typeof HttpStatus], headers })
|
|
387
|
+
}
|
|
309
388
|
}
|
|
310
|
-
}
|
|
311
389
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
390
|
+
// 4xx
|
|
391
|
+
export class BadRequestError extends mkErr(400) {}
|
|
392
|
+
export class UnauthorizedError extends mkErr(401) {}
|
|
393
|
+
export class PaymentRequiredError extends mkErr(402) {}
|
|
394
|
+
export class ForbiddenError extends mkErr(403) {}
|
|
395
|
+
export class NotFoundError extends mkErr(404) {}
|
|
396
|
+
export class MethodNotAllowedError extends mkErr(405) {}
|
|
397
|
+
export class NotAcceptableError extends mkErr(406) {}
|
|
398
|
+
export class ProxyAuthenticationRequiredError extends mkErr(407) {}
|
|
399
|
+
export class RequestTimeoutError extends mkErr(408) {}
|
|
400
|
+
export class ConflictError extends mkErr(409) {}
|
|
401
|
+
export class GoneError extends mkErr(410) {}
|
|
402
|
+
export class LengthRequiredError extends mkErr(411) {}
|
|
403
|
+
export class PreconditionFailedError extends mkErr(412) {}
|
|
404
|
+
export class PayloadTooLargeError extends mkErr(413) {}
|
|
405
|
+
export class URITooLongError extends mkErr(414) {}
|
|
406
|
+
export class UnsupportedMediaTypeError extends mkErr(415) {}
|
|
407
|
+
export class RangeNotSatisfiableError extends mkErr(416) {}
|
|
408
|
+
export class ExpectationFailedError extends mkErr(417) {}
|
|
409
|
+
export class ImATeapotError extends mkErr(418) {}
|
|
410
|
+
export class MisdirectedRequestError extends mkErr(421) {}
|
|
411
|
+
export class UnprocessableEntityError extends mkErr(422) {}
|
|
412
|
+
export class LockedError extends mkErr(423) {}
|
|
413
|
+
export class FailedDependencyError extends mkErr(424) {}
|
|
414
|
+
export class UpgradeRequiredError extends mkErr(426) {}
|
|
415
|
+
export class PreconditionRequiredError extends mkErr(428) {}
|
|
416
|
+
export class TooManyRequestsError extends mkErr(429) {}
|
|
417
|
+
export class RequestHeaderFieldsTooLargeError extends mkErr(431) {}
|
|
418
|
+
export class UnavailableForLegalReasonsError extends mkErr(451) {}
|
|
317
419
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
420
|
+
// 5xx
|
|
421
|
+
export class InternalServerError extends mkErr(500) {}
|
|
422
|
+
export class NotImplementedError extends mkErr(501) {}
|
|
423
|
+
export class BadGatewayError extends mkErr(502) {}
|
|
424
|
+
export class ServiceUnavailableError extends mkErr(503) {}
|
|
425
|
+
export class GatewayTimeoutError extends mkErr(504) {}
|
|
426
|
+
export class HTTPVersionNotSupportedError extends mkErr(505) {}
|
|
427
|
+
export class InsufficientStorageError extends mkErr(507) {}
|
|
428
|
+
export class NetworkAuthenticationRequiredError extends mkErr(511) {}
|
|
323
429
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
430
|
+
/** @deprecated use {@link InternalServerError} */
|
|
431
|
+
export const InternalError = InternalServerError
|
|
432
|
+
/** @deprecated use {@link InternalServerError} */
|
|
433
|
+
export type InternalError = InternalServerError
|
|
329
434
|
|
|
330
435
|
/**
|
|
331
436
|
* #### GalbePlugin
|
|
@@ -363,7 +468,21 @@ export type GalbeCLICommand = {
|
|
|
363
468
|
tags: string[]
|
|
364
469
|
description?: string
|
|
365
470
|
route: Route
|
|
471
|
+
pathT: string
|
|
366
472
|
arguments?: { name: string; type: string; description: string }[]
|
|
367
473
|
options?: { name: string; short: string; type: string; description: string; default: any }[]
|
|
368
474
|
action?: (props: any) => MaybePromise<void>
|
|
475
|
+
hideOptions?: ('header' | 'query' | 'body' | 'body-file')[]
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export type GalbeCLIOptions = {
|
|
479
|
+
baseUrl?: string | (() => string)
|
|
480
|
+
headers?: Record<string, string>
|
|
481
|
+
requestInterceptor?: (
|
|
482
|
+
req: Request,
|
|
483
|
+
command: GalbeCLICommand,
|
|
484
|
+
args: Record<string, string>,
|
|
485
|
+
options: Record<string, any>
|
|
486
|
+
) => MaybePromise<Request>
|
|
487
|
+
responseFormatter?: (res: Response) => MaybePromise<string>
|
|
369
488
|
}
|
package/src/util.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Method, Route, RouteNode
|
|
1
|
+
import type { Method, Route, RouteNode } from '.'
|
|
2
2
|
import type { RouteFileMeta, RouteMeta } from './routes'
|
|
3
3
|
|
|
4
4
|
const METHOD_COLOR: Record<string, string> = {
|
|
@@ -138,28 +138,20 @@ export const HttpStatus = {
|
|
|
138
138
|
511: 'Network Authentication Required',
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
const
|
|
142
|
-
const
|
|
141
|
+
const BA_HEADER_RX = /^application\/octet-stream\b/
|
|
142
|
+
const JSON_HEADER_RX = /^application\/json\b/
|
|
143
143
|
const TXT_HEADER_RX = /^text\//
|
|
144
|
-
const FORM_HEADER_RX = /^application\/x-www-form-urlencoded/
|
|
145
|
-
const MP_HEADER_RX = /^multipart\/form-data/
|
|
144
|
+
const FORM_HEADER_RX = /^application\/x-www-form-urlencoded\b/
|
|
145
|
+
const MP_HEADER_RX = /^multipart\/form-data\b/
|
|
146
146
|
|
|
147
|
-
export
|
|
147
|
+
export type ParseMode = 'json' | 'text' | 'byteArray' | 'urlForm' | 'multipart' | 'default'
|
|
148
|
+
|
|
149
|
+
export const inferBodyType = (contentType?: string | null): ParseMode => {
|
|
148
150
|
if (!contentType) return 'default'
|
|
149
|
-
if (contentType
|
|
151
|
+
if (JSON_HEADER_RX.test(contentType)) return 'json'
|
|
150
152
|
if (TXT_HEADER_RX.test(contentType)) return 'text'
|
|
151
153
|
if (FORM_HEADER_RX.test(contentType)) return 'urlForm'
|
|
152
154
|
if (MP_HEADER_RX.test(contentType)) return 'multipart'
|
|
153
|
-
if (contentType
|
|
154
|
-
return 'default'
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export const inferContentType = (bodyType?: string | undefined): string => {
|
|
158
|
-
if (!bodyType) return 'default'
|
|
159
|
-
if (bodyType === 'json') return JSON_HEADER
|
|
160
|
-
if (bodyType === 'text') return 'text/plain'
|
|
161
|
-
if (bodyType === 'urlForm') return 'application/x-www-form-urlencoded'
|
|
162
|
-
if (bodyType === 'multipart') return 'multipart/form-data'
|
|
163
|
-
if (bodyType === 'byteArray') return BA_HEADER
|
|
155
|
+
if (BA_HEADER_RX.test(contentType)) return 'byteArray'
|
|
164
156
|
return 'default'
|
|
165
157
|
}
|
package/src/validator.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
1
|
+
import { InternalServerError, type STResponse } from './index'
|
|
2
|
+
import type {
|
|
3
|
+
STSchema,
|
|
4
|
+
STProps,
|
|
5
|
+
STUnion,
|
|
6
|
+
STIntersection,
|
|
7
|
+
STJson,
|
|
8
|
+
STLiteral,
|
|
9
|
+
STArray,
|
|
10
|
+
STNumber,
|
|
11
|
+
STInteger,
|
|
12
|
+
STString,
|
|
13
|
+
STObject,
|
|
14
|
+
} from './schema'
|
|
3
15
|
import { Kind, Optional, Stream } from './schema'
|
|
4
16
|
import { isIterator } from './util'
|
|
5
17
|
|
|
@@ -30,7 +42,8 @@ export const validate = (elt: any, schema: STSchema, opt?: { parse?: boolean }):
|
|
|
30
42
|
if (!(typeof elt === 'string')) throw `Not a valid string`
|
|
31
43
|
schemaValidation(elt, schema)
|
|
32
44
|
} else if (schema[Kind] === 'literal') {
|
|
33
|
-
|
|
45
|
+
const lit = schema as STLiteral
|
|
46
|
+
if (elt !== lit.value) throw `Not a valid value. Found "${elt}" but expected "${lit.value}"`
|
|
34
47
|
} else if (schema[Kind] === 'object') {
|
|
35
48
|
if (opt?.parse && typeof elt === 'string') {
|
|
36
49
|
try {
|
|
@@ -42,8 +55,8 @@ export const validate = (elt: any, schema: STSchema, opt?: { parse?: boolean }):
|
|
|
42
55
|
if (typeof elt !== 'object') throw `Not a valid object`
|
|
43
56
|
if (Array.isArray(elt)) throw `Expected an object, not an array`
|
|
44
57
|
const err: ValidationError = {}
|
|
45
|
-
Object.entries(schema.props
|
|
46
|
-
if (!(k in elt)) {
|
|
58
|
+
Object.entries((schema as STObject).props ?? {}).forEach(([k, s]) => {
|
|
59
|
+
if (elt === null || !(k in elt)) {
|
|
47
60
|
if (!s?.[Optional]) err[k] = 'Required'
|
|
48
61
|
return
|
|
49
62
|
}
|
|
@@ -55,7 +68,7 @@ export const validate = (elt: any, schema: STSchema, opt?: { parse?: boolean }):
|
|
|
55
68
|
})
|
|
56
69
|
if (Object.keys(err).length) errors.push(err)
|
|
57
70
|
} else if (schema[Kind] === 'json') {
|
|
58
|
-
elt = validate(elt,
|
|
71
|
+
elt = validate(elt, (schema as STJson).value, opt)
|
|
59
72
|
} else if (schema[Kind] === 'array') {
|
|
60
73
|
if (opt?.parse && typeof elt === 'string') {
|
|
61
74
|
try {
|
|
@@ -65,14 +78,14 @@ export const validate = (elt: any, schema: STSchema, opt?: { parse?: boolean }):
|
|
|
65
78
|
}
|
|
66
79
|
}
|
|
67
80
|
if (!Array.isArray(elt)) throw 'Not a valid array'
|
|
68
|
-
for (const i of elt) validate(i, schema.items)
|
|
81
|
+
for (const i of elt) validate(i, (schema as STArray).items)
|
|
69
82
|
schemaValidation(elt, schema)
|
|
70
83
|
} else if (schema[Kind] === 'byteArray') {
|
|
71
84
|
if (opt?.parse && typeof elt === 'string') elt = Uint8Array.from(elt, c => c.charCodeAt(0))
|
|
72
85
|
else if (opt?.parse && Array.isArray(elt)) elt = new Uint8Array(elt)
|
|
73
86
|
if (!(elt instanceof Uint8Array)) throw 'Not a valid byteArray'
|
|
74
|
-
} else if (schema[Kind] === '
|
|
75
|
-
const union = Object.values((schema as STUnion).
|
|
87
|
+
} else if (schema[Kind] === 'anyOf' || schema[Kind] === 'oneOf') {
|
|
88
|
+
const union = Object.values((schema as STUnion).members)
|
|
76
89
|
let valid = false
|
|
77
90
|
for (const s of union) {
|
|
78
91
|
try {
|
|
@@ -86,7 +99,7 @@ export const validate = (elt: any, schema: STSchema, opt?: { parse?: boolean }):
|
|
|
86
99
|
// @ts-ignore
|
|
87
100
|
if (!valid) throw `Could not be parsed to any of [${union.map(u => u?.value ?? u[Kind]).join(', ')}]`
|
|
88
101
|
} else if (schema[Kind] === 'intersection') {
|
|
89
|
-
const intersection = Object.values((schema as STIntersection).allOf)
|
|
102
|
+
const intersection = Object.values((schema as STIntersection<any>).allOf)
|
|
90
103
|
for (const s of intersection) validate(elt, s as STSchema, opt)
|
|
91
104
|
} else if (schema[Kind] === 'any') {
|
|
92
105
|
} else {
|
|
@@ -100,18 +113,32 @@ export const validate = (elt: any, schema: STSchema, opt?: { parse?: boolean }):
|
|
|
100
113
|
}
|
|
101
114
|
|
|
102
115
|
export const validateResponse = (response: any, schema: STResponse, status: number) => {
|
|
103
|
-
|
|
104
|
-
|
|
116
|
+
const entry = schema?.[status] ?? schema?.['default']
|
|
117
|
+
if (!entry) return
|
|
118
|
+
let s: STSchema | undefined
|
|
119
|
+
if ((entry as STSchema)[Kind]) {
|
|
120
|
+
s = entry as STSchema
|
|
121
|
+
} else {
|
|
122
|
+
const c = entry as any
|
|
123
|
+
if (response instanceof Uint8Array)
|
|
124
|
+
s = c['application/octet-stream'] ?? c['*/*']
|
|
125
|
+
else if (typeof response === 'string')
|
|
126
|
+
s = c['text/plain'] ?? c['application/json'] ?? c['*/*']
|
|
127
|
+
else if (response !== null && typeof response === 'object')
|
|
128
|
+
s = c['application/json'] ?? c['*/*']
|
|
129
|
+
else
|
|
130
|
+
s = c['*/*']
|
|
131
|
+
}
|
|
105
132
|
if (!s) return
|
|
106
133
|
if (response instanceof ReadableStream) {
|
|
107
|
-
if (!s[Stream]) throw new
|
|
134
|
+
if (!s[Stream]) throw new InternalServerError(`Expected ${s[Kind]} response, but got ReadableStream`)
|
|
108
135
|
} else if (isIterator(response)) {
|
|
109
|
-
if (!s[Stream]) throw new
|
|
136
|
+
if (!s[Stream]) throw new InternalServerError(`Expected ${s[Kind]} response, but got Iterator`)
|
|
110
137
|
} else {
|
|
111
138
|
try {
|
|
112
139
|
validate(response, s)
|
|
113
140
|
} catch (error) {
|
|
114
|
-
throw new
|
|
141
|
+
throw new InternalServerError({ ResponseValidationError: error })
|
|
115
142
|
}
|
|
116
143
|
}
|
|
117
144
|
}
|
|
@@ -119,25 +146,28 @@ export const validateResponse = (response: any, schema: STResponse, status: numb
|
|
|
119
146
|
const schemaValidation = (value: any, schema: STSchema) => {
|
|
120
147
|
const errors = []
|
|
121
148
|
if (schema[Kind] === 'integer' || schema[Kind] === 'number') {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (
|
|
149
|
+
const n = schema as STNumber | STInteger
|
|
150
|
+
if (n.exclusiveMin !== undefined)
|
|
151
|
+
if ((value as number) <= n.exclusiveMin) errors.push(`Is less or equal to ${n.exclusiveMin}`)
|
|
152
|
+
if (n.exclusiveMax !== undefined)
|
|
153
|
+
if ((value as number) >= n.exclusiveMax) errors.push(`Is greater or equal to ${n.exclusiveMax}`)
|
|
154
|
+
if (n.min !== undefined) if ((value as number) < n.min) errors.push(`Is less than ${n.min}`)
|
|
155
|
+
if (n.max !== undefined) if ((value as number) > n.max) errors.push(`Is greater than ${n.max}`)
|
|
128
156
|
} else if (schema[Kind] === 'string') {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
157
|
+
const str = schema as STString
|
|
158
|
+
if (str.minLength !== undefined && (value as string).length < str.minLength)
|
|
159
|
+
errors.push(`Length is too small (${str.minLength} char min)`)
|
|
160
|
+
if (str.maxLength !== undefined && (value as string).length > str.maxLength)
|
|
161
|
+
errors.push(`Length is too large (${str.maxLength} char max)`)
|
|
162
|
+
if (str.pattern !== undefined && !(value as string).match(str.pattern))
|
|
163
|
+
errors.push(`Does not match pattern ${str.pattern}`)
|
|
135
164
|
} else if (schema[Kind] === 'array') {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
165
|
+
const arr = schema as STArray
|
|
166
|
+
if (arr.minLength !== undefined && (value as any[]).length < arr.minLength)
|
|
167
|
+
errors.push(`Must contain at least ${arr.minLength} item${arr.minLength > 1 ? 's' : ''}`)
|
|
168
|
+
if (arr.maxLength !== undefined && (value as any[]).length > arr.maxLength)
|
|
169
|
+
errors.push(`Must contain at most ${arr.maxLength} item${arr.maxLength > 1 ? 's' : ''}`)
|
|
170
|
+
if (arr.unique === true && new Set(value as any[]).size !== (value as any[]).length)
|
|
141
171
|
errors.push(`Has duplicate values`)
|
|
142
172
|
}
|
|
143
173
|
if (errors.length) throw Array.isArray(errors) && errors.length === 1 ? errors[0] : errors
|