galbe 0.13.1 → 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 +88 -65
- 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 +179 -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
|
@@ -21,6 +21,9 @@ import type {
|
|
|
21
21
|
} from './schema'
|
|
22
22
|
import type { Galbe } from './index'
|
|
23
23
|
import { HttpStatus } from './util'
|
|
24
|
+
import type { CookieOptions } from './cookies'
|
|
25
|
+
|
|
26
|
+
export type MediaType = `${string}/${string}`
|
|
24
27
|
|
|
25
28
|
export type STResponseValue =
|
|
26
29
|
| STByteArray
|
|
@@ -33,23 +36,54 @@ export type STResponseValue =
|
|
|
33
36
|
| STJson
|
|
34
37
|
| STArray
|
|
35
38
|
| STUnion
|
|
39
|
+
| STIntersection<any>
|
|
36
40
|
| STStream
|
|
37
41
|
| STAny
|
|
38
42
|
| STNull
|
|
39
|
-
|
|
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
|
|
40
78
|
| STNull
|
|
41
|
-
| Partial<{
|
|
42
|
-
byteArray?: STByteArray | STStream
|
|
43
|
-
text?: STString | STLiteral | STBoolean | STNumber | STInteger | STUnion | STStream
|
|
44
|
-
json?: STJson | STObject | STBoolean | STInteger | STNumber | STString | STArray | STUnion | STIntersection
|
|
45
|
-
urlForm?: STObject | STStream | STUnion
|
|
46
|
-
multipart?: STMultipartForm | STStream | STUnion
|
|
47
|
-
default?: STString | STByteArray | STStream | STAny
|
|
48
|
-
}>
|
|
49
|
-
export type STBodyType = keyof STBody
|
|
50
|
-
export type STBodyValue = STBody[STBodyType]
|
|
51
79
|
|
|
52
|
-
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>>
|
|
53
87
|
|
|
54
88
|
export type MaybeArray<T> = T | T[]
|
|
55
89
|
export type MaybeSTArray<T extends STSchema> = T | STArray<T>
|
|
@@ -61,10 +95,10 @@ type MaybePromise<T> = T | Promise<T>
|
|
|
61
95
|
export type ExtractParams<T extends string> = T extends `/:${infer P}/${infer Rest}`
|
|
62
96
|
? P | ExtractParams<Rest>
|
|
63
97
|
: T extends `${infer _}:${infer P}/${infer Rest}`
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
98
|
+
? P | ExtractParams<Rest>
|
|
99
|
+
: T extends `${infer _}:${infer P}`
|
|
100
|
+
? P
|
|
101
|
+
: never
|
|
68
102
|
|
|
69
103
|
type STHeadersPrimaryValue = STString | STBoolean | STNumber | STInteger | STLiteral
|
|
70
104
|
type STHeadersValue = MaybeSTUnion<STHeadersPrimaryValue>
|
|
@@ -143,7 +177,7 @@ export type RequestSchema<
|
|
|
143
177
|
P extends Partial<STParams<Path>> = Partial<STParams<Path>>,
|
|
144
178
|
Q extends STQuery = STQuery,
|
|
145
179
|
B extends STBody = STBody,
|
|
146
|
-
R extends Partial<STResponse> = STResponse
|
|
180
|
+
R extends Partial<STResponse> = STResponse,
|
|
147
181
|
> = {
|
|
148
182
|
headers?: H
|
|
149
183
|
params?: P
|
|
@@ -161,13 +195,21 @@ type OmitNotDefined<S extends RequestSchema> = {
|
|
|
161
195
|
never]: Static<STObject<Exclude<S['params'], undefined>>>[K]
|
|
162
196
|
}
|
|
163
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
|
+
}
|
|
164
206
|
export type Context<
|
|
165
207
|
M extends Method = Method,
|
|
166
208
|
Path extends string = string,
|
|
167
|
-
S extends RequestSchema = RequestSchema
|
|
168
|
-
> =
|
|
169
|
-
|
|
170
|
-
|
|
209
|
+
S extends RequestSchema = RequestSchema,
|
|
210
|
+
> = 0 extends 1 & Exclude<S['body'], undefined | STNull>
|
|
211
|
+
? {
|
|
212
|
+
[K in keyof Exclude<S['body'], undefined | STNull>]: {
|
|
171
213
|
headers: Static<STObject<Exclude<S['headers'], undefined>>>
|
|
172
214
|
params: {
|
|
173
215
|
[P in ExtractParams<Path>]: P extends keyof OmitNotDefined<S> ? OmitNotDefined<S>[P] : string
|
|
@@ -177,24 +219,51 @@ export type Context<
|
|
|
177
219
|
body: M extends 'get' | 'options' | 'head'
|
|
178
220
|
? null
|
|
179
221
|
: Exclude<S['body'], undefined> extends STNull
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
? StaticBody<Exclude<Exclude<S['body'], undefined>[K], undefined>>
|
|
183
|
-
: never
|
|
222
|
+
? null
|
|
223
|
+
: StaticBody<Extract<Exclude<Exclude<S['body'], undefined | STNull>[K], undefined>, STSchema>>
|
|
184
224
|
request: Request
|
|
185
225
|
remoteAddress: SocketAddress | null
|
|
186
226
|
route?: Route
|
|
187
227
|
state: Record<string, any>
|
|
188
|
-
set:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
194
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>
|
|
195
247
|
}
|
|
196
|
-
:
|
|
197
|
-
|
|
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>]
|
|
198
267
|
export type Next = () => void | Promise<any>
|
|
199
268
|
export type Hook<M extends Method = Method, Path extends string = string, S extends RequestSchema = RequestSchema> = (
|
|
200
269
|
ctx: Context<M, Path, S>,
|
|
@@ -203,7 +272,7 @@ export type Hook<M extends Method = Method, Path extends string = string, S exte
|
|
|
203
272
|
export type Handler<
|
|
204
273
|
M extends Method = Method,
|
|
205
274
|
Path extends string = string,
|
|
206
|
-
S extends RequestSchema = RequestSchema
|
|
275
|
+
S extends RequestSchema = RequestSchema,
|
|
207
276
|
> = (ctx: Context<M, Path, S>) => any
|
|
208
277
|
export type Endpoint<M extends Method> = {
|
|
209
278
|
<
|
|
@@ -212,7 +281,7 @@ export type Endpoint<M extends Method> = {
|
|
|
212
281
|
H extends STHeaders = any,
|
|
213
282
|
Q extends STQuery = any,
|
|
214
283
|
B extends STBody = any,
|
|
215
|
-
R extends STResponse = STResponse
|
|
284
|
+
R extends STResponse = STResponse,
|
|
216
285
|
>(
|
|
217
286
|
path: Path,
|
|
218
287
|
schema: RequestSchema<M, Path, H, P, Q, B, R>,
|
|
@@ -225,7 +294,7 @@ export type Endpoint<M extends Method> = {
|
|
|
225
294
|
H extends STHeaders = any,
|
|
226
295
|
Q extends STQuery = any,
|
|
227
296
|
B extends STBody = any,
|
|
228
|
-
R extends STResponse = STResponse
|
|
297
|
+
R extends STResponse = STResponse,
|
|
229
298
|
>(
|
|
230
299
|
path: Path,
|
|
231
300
|
schema: RequestSchema<M, Path, H, P, Q, B, R>,
|
|
@@ -237,7 +306,7 @@ export type Endpoint<M extends Method> = {
|
|
|
237
306
|
H extends STHeaders = any,
|
|
238
307
|
Q extends STQuery = any,
|
|
239
308
|
B extends STBody = any,
|
|
240
|
-
R extends STResponse = STResponse
|
|
309
|
+
R extends STResponse = STResponse,
|
|
241
310
|
>(
|
|
242
311
|
path: Path,
|
|
243
312
|
hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[],
|
|
@@ -249,7 +318,7 @@ export type Endpoint<M extends Method> = {
|
|
|
249
318
|
H extends STHeaders = any,
|
|
250
319
|
Q extends STQuery = any,
|
|
251
320
|
B extends STBody = any,
|
|
252
|
-
R extends STResponse = STResponse
|
|
321
|
+
R extends STResponse = STResponse,
|
|
253
322
|
>(
|
|
254
323
|
path: Path,
|
|
255
324
|
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
@@ -265,12 +334,19 @@ export type StaticEndpoint<P extends string = string, T extends string = string>
|
|
|
265
334
|
options?: StaticEndpointOptions
|
|
266
335
|
) => Route<'get', P, {}, {}, {}, STBody, STResponse, T>
|
|
267
336
|
|
|
268
|
-
export class RequestError {
|
|
337
|
+
export class RequestError extends Error {
|
|
269
338
|
status: number
|
|
270
339
|
payload?: any
|
|
271
340
|
headers?: Record<string, string>
|
|
272
|
-
constructor(options: { status?: number; payload?: any; headers?: Record<string, string> }) {
|
|
273
|
-
|
|
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
|
|
274
350
|
this.payload = options.payload
|
|
275
351
|
this.headers = options.headers
|
|
276
352
|
}
|
|
@@ -293,7 +369,7 @@ export type Route<
|
|
|
293
369
|
B extends STBody = STBody,
|
|
294
370
|
R extends STResponse = STResponse,
|
|
295
371
|
SP extends string = string,
|
|
296
|
-
SR extends string = string
|
|
372
|
+
SR extends string = string,
|
|
297
373
|
> = {
|
|
298
374
|
method: M
|
|
299
375
|
path: Path
|
|
@@ -304,29 +380,57 @@ export type Route<
|
|
|
304
380
|
static?: { path: SP; root: SR }
|
|
305
381
|
}
|
|
306
382
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
+
}
|
|
310
388
|
}
|
|
311
|
-
}
|
|
312
389
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
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) {}
|
|
318
419
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
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) {}
|
|
324
429
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
430
|
+
/** @deprecated use {@link InternalServerError} */
|
|
431
|
+
export const InternalError = InternalServerError
|
|
432
|
+
/** @deprecated use {@link InternalServerError} */
|
|
433
|
+
export type InternalError = InternalServerError
|
|
330
434
|
|
|
331
435
|
/**
|
|
332
436
|
* #### GalbePlugin
|
|
@@ -364,7 +468,21 @@ export type GalbeCLICommand = {
|
|
|
364
468
|
tags: string[]
|
|
365
469
|
description?: string
|
|
366
470
|
route: Route
|
|
471
|
+
pathT: string
|
|
367
472
|
arguments?: { name: string; type: string; description: string }[]
|
|
368
473
|
options?: { name: string; short: string; type: string; description: string; default: any }[]
|
|
369
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>
|
|
370
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
|