galbe 0.11.0 → 0.12.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/.prettierrc +1 -1
- package/bin/commands/generate/client.ts +58 -30
- package/bin/commands/generate/code/openapi.parser.ts +72 -45
- package/bin/res/cli.template.js +20 -18
- package/bin/res/client.template.ts +52 -26
- package/bin/util.ts +27 -7
- package/docs/context.md +1 -1
- package/docs/schemas.md +63 -26
- package/package.json +1 -1
- package/src/extras/spec/openapi.serializer.ts +83 -56
- package/src/index.ts +16 -11
- package/src/parser.ts +198 -146
- package/src/schema.ts +156 -66
- package/src/server.ts +18 -16
- package/src/types.ts +54 -42
- package/src/util.ts +37 -9
- package/src/validator.ts +23 -19
- package/test/parser.test.ts +369 -281
- package/test/requests.test.ts +242 -176
- package/test/resources/static/chameleon.png +0 -0
- package/test/resources/static/index.html +13 -0
- package/test/resources/static/sub/index.html +13 -0
- package/test/resources/static/sub/other.html +13 -0
- package/test/responses.test.ts +49 -49
- package/test/test.utils.ts +4 -2
- package/test/types.test.ts +909 -0
package/src/index.ts
CHANGED
|
@@ -17,7 +17,8 @@ import type {
|
|
|
17
17
|
STQuery,
|
|
18
18
|
StaticEndpoint,
|
|
19
19
|
Route,
|
|
20
|
-
StaticEndpointOptions
|
|
20
|
+
StaticEndpointOptions,
|
|
21
|
+
STBodyValue,
|
|
21
22
|
} from './types'
|
|
22
23
|
|
|
23
24
|
import { readdirSync, statSync } from 'fs'
|
|
@@ -78,14 +79,12 @@ const galbeMethod = <
|
|
|
78
79
|
): Route<M, Path, P, H, Q, B, R> => {
|
|
79
80
|
schema = schema ?? {}
|
|
80
81
|
hooks = hooks || []
|
|
82
|
+
//@ts-ignore
|
|
81
83
|
const context: Context<M, Path, typeof schema> = {
|
|
82
84
|
headers: {} as Static<STObject<Exclude<(typeof schema)['headers'], undefined>>>,
|
|
83
85
|
params: {} as any,
|
|
84
86
|
query: {} as Static<STObject<Exclude<(typeof schema)['query'], undefined>>>,
|
|
85
|
-
|
|
86
|
-
body: ['get', 'options', 'head'].includes(method)
|
|
87
|
-
? null
|
|
88
|
-
: ({} as Static<Exclude<(typeof schema)['body'], undefined>>),
|
|
87
|
+
body: ['get', 'options', 'head'].includes(method) ? null : ({} as unknown as STBodyValue),
|
|
89
88
|
request: {} as Request,
|
|
90
89
|
state: {},
|
|
91
90
|
set: {} as {
|
|
@@ -94,7 +93,7 @@ const galbeMethod = <
|
|
|
94
93
|
[header: string]: string | string[]
|
|
95
94
|
}
|
|
96
95
|
status?: number
|
|
97
|
-
}
|
|
96
|
+
},
|
|
98
97
|
}
|
|
99
98
|
return {
|
|
100
99
|
method,
|
|
@@ -102,7 +101,7 @@ const galbeMethod = <
|
|
|
102
101
|
schema,
|
|
103
102
|
context,
|
|
104
103
|
hooks,
|
|
105
|
-
handler
|
|
104
|
+
handler,
|
|
106
105
|
}
|
|
107
106
|
}
|
|
108
107
|
|
|
@@ -140,7 +139,7 @@ export class Galbe {
|
|
|
140
139
|
this.config = config ?? {}
|
|
141
140
|
this.router = new GalbeRouter({
|
|
142
141
|
prefix: this.config?.basePath || '',
|
|
143
|
-
cacheEnabled: this.config?.router?.cacheEnabled
|
|
142
|
+
cacheEnabled: this.config?.router?.cacheEnabled,
|
|
144
143
|
})
|
|
145
144
|
}
|
|
146
145
|
private add(route: any) {
|
|
@@ -189,7 +188,7 @@ export class Galbe {
|
|
|
189
188
|
P extends Partial<STParams<Path>>,
|
|
190
189
|
H extends STHeaders,
|
|
191
190
|
Q extends STQuery,
|
|
192
|
-
B extends
|
|
191
|
+
B extends STBody,
|
|
193
192
|
R extends STResponse
|
|
194
193
|
>(
|
|
195
194
|
path: Path,
|
|
@@ -317,7 +316,7 @@ export class Galbe {
|
|
|
317
316
|
|
|
318
317
|
const walkStatic = (path: string, target: string) => {
|
|
319
318
|
path = path?.[0] === '/' ? path : `/${path}`
|
|
320
|
-
path = path.endsWith('/') ? path.slice(0, -1) : path
|
|
319
|
+
path = path.endsWith('/') ? path.slice(0, -1) : path
|
|
321
320
|
|
|
322
321
|
let t = target
|
|
323
322
|
if (Bun.env.BUN_ENV === 'production') {
|
|
@@ -330,7 +329,13 @@ export class Galbe {
|
|
|
330
329
|
if (resolve) ut = resolve(path, ut)
|
|
331
330
|
if (ut) {
|
|
332
331
|
let handler = () => new Response(Bun.file(ut))
|
|
332
|
+
const isIndex = /index\.html$/.test(ut)
|
|
333
333
|
this.add({ ...galbeMethod(this, 'get', path, {}, undefined, handler), static: { path: ut, root: rootPath } })
|
|
334
|
+
if (isIndex)
|
|
335
|
+
this.add({
|
|
336
|
+
...galbeMethod(this, 'get', `${path}/index.html`, {}, undefined, handler),
|
|
337
|
+
static: { path: ut, root: rootPath },
|
|
338
|
+
})
|
|
334
339
|
}
|
|
335
340
|
} else {
|
|
336
341
|
let root = readdirSync(t)
|
|
@@ -340,7 +345,7 @@ export class Galbe {
|
|
|
340
345
|
}
|
|
341
346
|
}
|
|
342
347
|
|
|
343
|
-
return { ...galbeMethod(this, 'get', path, {}, undefined, () => {
|
|
348
|
+
return { ...galbeMethod(this, 'get', path, {}, undefined, () => {}), static: { path: t, root: rootPath } }
|
|
344
349
|
}
|
|
345
350
|
|
|
346
351
|
return walkStatic(path, target)
|
package/src/parser.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import type { MaybeArray, STBody, Context, STResponse } from './index'
|
|
1
|
+
import type { MaybeArray, STBody, Context, STResponse, STBodyValue, STBodyType } from './index'
|
|
2
2
|
import type {
|
|
3
3
|
STStream,
|
|
4
|
-
STUrlForm,
|
|
5
4
|
STMultipartForm,
|
|
6
5
|
Static,
|
|
7
6
|
STProps,
|
|
8
7
|
STObject,
|
|
9
8
|
MultipartFormData,
|
|
10
|
-
STUrlFormValues,
|
|
11
9
|
STMultipartFormValues,
|
|
12
10
|
STLiteral,
|
|
13
|
-
STSchema
|
|
11
|
+
STSchema,
|
|
12
|
+
STNull,
|
|
13
|
+
STPropsValue,
|
|
14
|
+
STUnion,
|
|
14
15
|
} from './schema'
|
|
15
16
|
|
|
16
17
|
import { readableStreamToArrayBuffer } from 'bun'
|
|
@@ -30,130 +31,151 @@ async function* rsToAsyncIterator(readableStream: ReadableStream) {
|
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
const BA_HEADER = 'application/octet-stream'
|
|
34
|
-
const JSON_HEADER = 'application/json'
|
|
35
|
-
const TXT_HEADER_RX = /^text\//
|
|
36
|
-
const FORM_HEADER_RX = /^application\/x-www-form-urlencoded/
|
|
37
|
-
const MP_HEADER_RX = /^multipart\/form-data/
|
|
38
|
-
|
|
39
34
|
export const requestBodyParser = async (
|
|
40
35
|
body: ReadableStream | null,
|
|
41
36
|
headers: Record<string, string>,
|
|
42
|
-
|
|
37
|
+
schemas?: STBody | STNull,
|
|
38
|
+
contentType?: STBodyType
|
|
43
39
|
) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
let schema: STBodyValue =
|
|
41
|
+
(schemas as STNull)?.[Kind] === 'null' ? schemas : contentType ? schemas?.[contentType] : undefined
|
|
42
|
+
let kind = schema?.[Kind]
|
|
43
|
+
let isStream = schema && Stream in schema
|
|
47
44
|
try {
|
|
48
|
-
if (
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
throw new RequestError({
|
|
45
|
+
if (kind === 'null') {
|
|
46
|
+
if (body === null) return null
|
|
47
|
+
throw new RequestError({ status: 400, payload: { body: `Expected null body` } })
|
|
48
|
+
}
|
|
49
|
+
if (!schemas || !Object.keys(schemas).length) {
|
|
50
|
+
// No schema defined, we base parsing on contentType only
|
|
51
|
+
if (contentType === 'byteArray') {
|
|
52
|
+
if (body === null) return new Uint8Array()
|
|
53
|
+
return new Uint8Array(await readableStreamToArrayBuffer(body))
|
|
54
|
+
} else if (contentType === 'json') {
|
|
55
|
+
if (body === null) return null
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(await streamToString(body))
|
|
58
|
+
} catch (err: any) {
|
|
59
|
+
throw new RequestError({
|
|
60
|
+
status: 400,
|
|
61
|
+
payload: { body: err?.message ?? 'Parsing error' },
|
|
62
|
+
})
|
|
63
63
|
}
|
|
64
|
-
} else {
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
} else return null
|
|
76
|
-
}
|
|
64
|
+
} else if (contentType === 'text') {
|
|
65
|
+
if (body === null) return ''
|
|
66
|
+
return streamToString(body)
|
|
67
|
+
} else if (contentType === 'urlForm') {
|
|
68
|
+
if (body === null) return {}
|
|
69
|
+
return await streamToUrlForm(body)
|
|
70
|
+
} else if (contentType === 'multipart') {
|
|
71
|
+
if (body === null) return {}
|
|
72
|
+
const boundary = headers?.['content-type'].match(/boundary\="?([^"]*)"?;?.*$/)?.[1] || ''
|
|
73
|
+
return await streamToMultipartForm(body, boundary)
|
|
74
|
+
} else return body === null ? null : rsToAsyncIterator(body)
|
|
77
75
|
} else {
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
// Schemas found
|
|
77
|
+
if (contentType === 'default' && (schema || Object.values(schemas).every(s => s?.[Optional]))) {
|
|
78
|
+
if (kind === 'byteArray') contentType = 'byteArray'
|
|
79
|
+
else if (kind === 'string') contentType = 'text'
|
|
80
|
+
else return body === null ? null : rsToAsyncIterator(body)
|
|
81
|
+
}
|
|
82
|
+
if (contentType === 'byteArray') {
|
|
83
|
+
if (kind !== 'byteArray') throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
|
|
84
|
+
if (body === null) {
|
|
85
|
+
return isStream
|
|
86
|
+
? new ReadableStream({
|
|
87
|
+
start(controller) {
|
|
88
|
+
controller.enqueue(new Uint8Array())
|
|
89
|
+
controller.close()
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
: new Uint8Array()
|
|
93
|
+
}
|
|
80
94
|
if (isStream) return rsToAsyncIterator(body)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const bytes = []
|
|
99
|
-
for await (const b of body) bytes.push(...b)
|
|
100
|
-
return new Uint8Array(bytes)
|
|
101
|
-
} else {
|
|
102
|
-
if (isStream) return rsToAsyncIterator(body)
|
|
103
|
-
else return await streamToString(body, schema)
|
|
95
|
+
return new Uint8Array(await readableStreamToArrayBuffer(body))
|
|
96
|
+
} else if (contentType === 'text') {
|
|
97
|
+
if (!['string', 'boolean', 'number', 'integer', 'union', 'literal'].includes(kind))
|
|
98
|
+
throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
|
|
99
|
+
if (body === null)
|
|
100
|
+
return isStream
|
|
101
|
+
? new ReadableStream({
|
|
102
|
+
start(controller) {
|
|
103
|
+
controller.enqueue('')
|
|
104
|
+
controller.close()
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
: validate('', schema, { parse: true })
|
|
108
|
+
if (isStream) return $streamToString(body)
|
|
109
|
+
if (kind === 'union') {
|
|
110
|
+
let str = await streamToString(body)
|
|
111
|
+
return unionize(str, schema)
|
|
104
112
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
} else {
|
|
116
|
-
const str = await streamToString(body)
|
|
117
|
-
let json
|
|
118
|
-
try {
|
|
119
|
-
json = JSON.parse(str)
|
|
120
|
-
} catch (err: any) {
|
|
121
|
-
throw new RequestError({
|
|
122
|
-
status: 400,
|
|
123
|
-
payload: { body: err?.message ?? 'Parsing error' }
|
|
124
|
-
})
|
|
125
|
-
}
|
|
126
|
-
return validate(json, schema, true)
|
|
113
|
+
return await streamToString(body, schema as STBodyValue)
|
|
114
|
+
} else if (contentType === 'json') {
|
|
115
|
+
if (!['object', 'json', 'boolean', 'number', 'integer', 'string', 'array', 'union'].includes(kind))
|
|
116
|
+
throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
|
|
117
|
+
if (kind === 'union') {
|
|
118
|
+
let str = body === null ? 'null' : await streamToString(body)
|
|
119
|
+
let json = JSON.parse(str)
|
|
120
|
+
return unionize(json, schema)
|
|
127
121
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
122
|
+
const str = body === null ? 'null' : await streamToString(body)
|
|
123
|
+
let json
|
|
124
|
+
try {
|
|
125
|
+
json = JSON.parse(str)
|
|
126
|
+
} catch (err: any) {
|
|
127
|
+
throw new RequestError({
|
|
128
|
+
status: 400,
|
|
129
|
+
payload: { body: err?.message ?? 'Parsing error' },
|
|
130
|
+
})
|
|
133
131
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
132
|
+
return validate(json, schema, { parse: true })
|
|
133
|
+
} else if (contentType === 'urlForm') {
|
|
134
|
+
if (!['object', 'union'].includes(kind))
|
|
135
|
+
throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
|
|
136
|
+
if (body === null)
|
|
137
|
+
return isStream
|
|
138
|
+
? new ReadableStream({
|
|
139
|
+
start(controller) {
|
|
140
|
+
controller.enqueue([])
|
|
141
|
+
controller.close()
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
: await streamToUrlForm(
|
|
145
|
+
new ReadableStream({
|
|
146
|
+
start(controller) {
|
|
147
|
+
controller.enqueue(new Uint8Array())
|
|
148
|
+
controller.close()
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
schema
|
|
152
|
+
)
|
|
153
|
+
if (kind === 'union') {
|
|
154
|
+
const b = await streamToUrlForm(body)
|
|
155
|
+
return unionize(b, schema)
|
|
142
156
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
if (isStream) return $streamToUrlForm(body, schema as STStream<STObject>)
|
|
158
|
+
else return await streamToUrlForm(body, schema as STObject)
|
|
159
|
+
} else if (contentType === 'multipart') {
|
|
160
|
+
if (kind !== 'multipartForm') throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
|
|
161
|
+
if (body === null)
|
|
162
|
+
return isStream
|
|
163
|
+
? new ReadableStream({
|
|
164
|
+
start(controller) {
|
|
165
|
+
controller.enqueue({ headers: {} })
|
|
166
|
+
controller.close()
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
: {}
|
|
170
|
+
const boundary = headers?.['content-type'].match(/boundary\="?([^"]*)"?;?.*$/)?.[1] || ''
|
|
171
|
+
if (kind === 'union') {
|
|
172
|
+
let mp = await streamToMultipartForm(body, boundary)
|
|
173
|
+
return unionize(mp, schema)
|
|
154
174
|
}
|
|
155
|
-
|
|
156
|
-
return
|
|
175
|
+
if (isStream) return $streamToMultipartForm(body, boundary, schema as STStream<STMultipartForm>)
|
|
176
|
+
return streamToMultipartForm(body, boundary, schema as STMultipartForm)
|
|
177
|
+
} else if (contentType === 'default') {
|
|
178
|
+
throw new RequestError({ status: 400, payload: { body: `Not a valid content-type` } })
|
|
157
179
|
}
|
|
158
180
|
}
|
|
159
181
|
} catch (error) {
|
|
@@ -164,15 +186,15 @@ export const requestBodyParser = async (
|
|
|
164
186
|
async function* $streamToString(body: ReadableStream) {
|
|
165
187
|
for await (const chunk of body) yield textDecoder.decode(chunk)
|
|
166
188
|
}
|
|
167
|
-
const streamToString = async (body: ReadableStream, schema?:
|
|
189
|
+
const streamToString = async (body: ReadableStream, schema?: STBodyValue): Promise<any> => {
|
|
168
190
|
let res = ''
|
|
169
191
|
for await (const chunk of $streamToString(body)) res += chunk
|
|
170
|
-
if (schema) return validate(res, schema, true)
|
|
192
|
+
if (schema) return validate(res, schema, { parse: true })
|
|
171
193
|
return res
|
|
172
194
|
}
|
|
173
195
|
async function* $streamToUrlForm(
|
|
174
196
|
body: ReadableStream<Uint8Array>,
|
|
175
|
-
schema?: STStream<
|
|
197
|
+
schema?: STStream<STObject>
|
|
176
198
|
): AsyncGenerator<[string, any]> {
|
|
177
199
|
let rest: Uint8Array = new Uint8Array()
|
|
178
200
|
let bK: Uint8Array = new Uint8Array()
|
|
@@ -190,7 +212,7 @@ async function* $streamToUrlForm(
|
|
|
190
212
|
bV.set(chunk.slice(start, i), rest.length)
|
|
191
213
|
let [key, val]: [string, any] = [
|
|
192
214
|
decodeURIComponent(textDecoder.decode(bK)),
|
|
193
|
-
decodeURIComponent(textDecoder.decode(bV))
|
|
215
|
+
decodeURIComponent(textDecoder.decode(bV)),
|
|
194
216
|
]
|
|
195
217
|
try {
|
|
196
218
|
let s = schema?.props?.[key]?.[Kind] === 'array' ? schema?.props?.[key].items : schema?.props?.[key]
|
|
@@ -221,7 +243,7 @@ async function* $streamToUrlForm(
|
|
|
221
243
|
}
|
|
222
244
|
let [key, val]: [string, any] = [
|
|
223
245
|
decodeURIComponent(textDecoder.decode(bK)),
|
|
224
|
-
decodeURIComponent(textDecoder.decode(rest))
|
|
246
|
+
decodeURIComponent(textDecoder.decode(rest)),
|
|
225
247
|
]
|
|
226
248
|
try {
|
|
227
249
|
let s = schema?.props?.[key]?.[Kind] === 'array' ? schema?.props?.[key].items : schema?.props?.[key]
|
|
@@ -241,10 +263,10 @@ async function* $streamToUrlForm(
|
|
|
241
263
|
if (reqKeys.length > 0)
|
|
242
264
|
throw new RequestError({
|
|
243
265
|
status: 400,
|
|
244
|
-
payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
|
|
266
|
+
payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` },
|
|
245
267
|
})
|
|
246
268
|
}
|
|
247
|
-
const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?:
|
|
269
|
+
const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?: STObject) => {
|
|
248
270
|
let entries = []
|
|
249
271
|
const required = Object.fromEntries(
|
|
250
272
|
Object.entries(schema?.props || {}).filter(([_, v]: [string, any]) => !v?.[Optional])
|
|
@@ -252,7 +274,7 @@ const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?: STUrlF
|
|
|
252
274
|
let errors: Record<string, any> = {}
|
|
253
275
|
for await (const chunk of $streamToUrlForm(body)) entries.push(chunk)
|
|
254
276
|
const object: Record<string, any> = {}
|
|
255
|
-
for (let e of entries) {
|
|
277
|
+
for (let e of entries.filter(([k]) => k)) {
|
|
256
278
|
if (e[0] in object) {
|
|
257
279
|
if (Array.isArray(object[e[0]])) object[e[0]].push(e[1])
|
|
258
280
|
else object[e[0]] = [object[e[0]], e[1]]
|
|
@@ -278,7 +300,7 @@ const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?: STUrlF
|
|
|
278
300
|
if (reqKeys.length > 0)
|
|
279
301
|
throw new RequestError({
|
|
280
302
|
status: 400,
|
|
281
|
-
payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
|
|
303
|
+
payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` },
|
|
282
304
|
})
|
|
283
305
|
return object
|
|
284
306
|
}
|
|
@@ -324,7 +346,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
|
|
|
324
346
|
delete required[headers.name]
|
|
325
347
|
yield {
|
|
326
348
|
headers,
|
|
327
|
-
content: parseMultipartContent(bV, headers, schema)
|
|
349
|
+
content: parseMultipartContent(bV, headers, schema),
|
|
328
350
|
}
|
|
329
351
|
} catch (err) {
|
|
330
352
|
if (err instanceof RequestError) throw err
|
|
@@ -358,7 +380,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
|
|
|
358
380
|
delete required[headers.name]
|
|
359
381
|
yield {
|
|
360
382
|
headers,
|
|
361
|
-
content: parseMultipartContent(bV, headers, schema)
|
|
383
|
+
content: parseMultipartContent(bV, headers, schema),
|
|
362
384
|
}
|
|
363
385
|
} catch (err) {
|
|
364
386
|
throw new RequestError({ status: 400, payload: { body: { [headers.name]: err } } })
|
|
@@ -374,15 +396,15 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
|
|
|
374
396
|
if (reqKeys.length > 0)
|
|
375
397
|
throw new RequestError({
|
|
376
398
|
status: 400,
|
|
377
|
-
payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
|
|
399
|
+
payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` },
|
|
378
400
|
})
|
|
379
401
|
}
|
|
380
|
-
const parseMultipartHeader = (header: string): { name: string;[key: string]: string } | null => {
|
|
402
|
+
const parseMultipartHeader = (header: string): { name: string; [key: string]: string } | null => {
|
|
381
403
|
if (!header) return null
|
|
382
404
|
let disposition = 'form-data'
|
|
383
405
|
const multipartHeader = [
|
|
384
406
|
...header.matchAll(/\s*([\w-]+)\s*:\s*([^;]*);?/g),
|
|
385
|
-
...header.matchAll(/;?\s*(\w+)\s*=\s*\"([^"]*)\";?/g)
|
|
407
|
+
...header.matchAll(/;?\s*(\w+)\s*=\s*\"([^"]*)\";?/g),
|
|
386
408
|
].reduce((acc: Record<string, string>, v: string[]) => {
|
|
387
409
|
const key = v[1].toLowerCase().replace(/^content-/, '')
|
|
388
410
|
if (key === 'disposition') {
|
|
@@ -421,7 +443,7 @@ const parseMultipartContent = (
|
|
|
421
443
|
} catch (err: any) {
|
|
422
444
|
throw new RequestError({
|
|
423
445
|
status: 400,
|
|
424
|
-
payload: { body: { [headers.name]: err?.message || 'Parsing error' } }
|
|
446
|
+
payload: { body: { [headers.name]: err?.message || 'Parsing error' } },
|
|
425
447
|
})
|
|
426
448
|
}
|
|
427
449
|
try {
|
|
@@ -436,7 +458,7 @@ const parseMultipartContent = (
|
|
|
436
458
|
} else {
|
|
437
459
|
throw new RequestError({
|
|
438
460
|
status: 400,
|
|
439
|
-
payload: { body: { [headers.name]: `Expected ${schema?.props[headers.name][Kind]} found json` } }
|
|
461
|
+
payload: { body: { [headers.name]: `Expected ${schema?.props[headers.name][Kind]} found json` } },
|
|
440
462
|
})
|
|
441
463
|
}
|
|
442
464
|
}
|
|
@@ -472,11 +494,9 @@ const streamToMultipartForm = async (data: ReadableStream<Uint8Array>, boundary:
|
|
|
472
494
|
try {
|
|
473
495
|
if (Array.isArray(res[chunk.headers.name].content) && schema?.props?.[chunk.headers.name]?.[Kind] !== 'array')
|
|
474
496
|
throw `Multiple values found`
|
|
475
|
-
res[chunk.headers.name].content = validate(
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
true
|
|
479
|
-
)
|
|
497
|
+
res[chunk.headers.name].content = validate(res[chunk.headers.name].content, schema?.props[chunk.headers.name], {
|
|
498
|
+
parse: true,
|
|
499
|
+
})
|
|
480
500
|
if (schema.props[chunk.headers.name][Kind] === 'array')
|
|
481
501
|
for (let [k, v] of Object.entries(res[chunk.headers.name].content)) {
|
|
482
502
|
try {
|
|
@@ -494,7 +514,7 @@ const streamToMultipartForm = async (data: ReadableStream<Uint8Array>, boundary:
|
|
|
494
514
|
if (Object.keys(errors).length)
|
|
495
515
|
throw new RequestError({
|
|
496
516
|
status: 400,
|
|
497
|
-
payload: { body: errors }
|
|
517
|
+
payload: { body: errors },
|
|
498
518
|
})
|
|
499
519
|
for (const [k, s] of Object.entries(required)) {
|
|
500
520
|
if (s[Kind] === 'array') {
|
|
@@ -506,14 +526,14 @@ const streamToMultipartForm = async (data: ReadableStream<Uint8Array>, boundary:
|
|
|
506
526
|
if (reqKeys.length > 0)
|
|
507
527
|
throw new RequestError({
|
|
508
528
|
status: 400,
|
|
509
|
-
payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
|
|
529
|
+
payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` },
|
|
510
530
|
})
|
|
511
531
|
return res
|
|
512
532
|
}
|
|
513
533
|
const paramParser = (
|
|
514
534
|
value: string | string[] | null,
|
|
515
535
|
type: STMultipartFormValues
|
|
516
|
-
): MaybeArray<Static<
|
|
536
|
+
): MaybeArray<Static<STPropsValue>> => {
|
|
517
537
|
if (value === undefined) {
|
|
518
538
|
if (type?.[Optional]) return type?.default
|
|
519
539
|
else throw `Required`
|
|
@@ -525,7 +545,7 @@ const paramParser = (
|
|
|
525
545
|
let errors: Record<number, any> = {}
|
|
526
546
|
for (let [idx, v] of value.entries()) {
|
|
527
547
|
try {
|
|
528
|
-
pv.push(paramParser(v, type.items as STMultipartFormValues) as Static<
|
|
548
|
+
pv.push(paramParser(v, type.items as STMultipartFormValues) as Static<STPropsValue>)
|
|
529
549
|
} catch (error) {
|
|
530
550
|
errors[idx] = error
|
|
531
551
|
}
|
|
@@ -554,10 +574,21 @@ const paramParser = (
|
|
|
554
574
|
validate(value, type)
|
|
555
575
|
return value
|
|
556
576
|
} else if (type[Kind] === 'literal') {
|
|
557
|
-
|
|
558
|
-
|
|
577
|
+
let val: any = value
|
|
578
|
+
if (typeof type.value === 'boolean') val = value === 'true' ? true : value === 'false' ? false : value
|
|
579
|
+
if (typeof type.value === 'number') val = Number(value)
|
|
580
|
+
if (val !== type.value) throw `Not a valid value`
|
|
581
|
+
return val
|
|
582
|
+
} else if (type[Kind] === 'object') {
|
|
583
|
+
let json
|
|
584
|
+
try {
|
|
585
|
+
json = JSON.parse(value)
|
|
586
|
+
} catch (e) {
|
|
587
|
+
throw `Not a valid object`
|
|
588
|
+
}
|
|
589
|
+
return validate(json, type)
|
|
559
590
|
} else if (type[Kind] === 'array') {
|
|
560
|
-
return [paramParser(value, type.items as STMultipartFormValues) as Static<
|
|
591
|
+
return [paramParser(value, type.items as STMultipartFormValues) as Static<STPropsValue>]
|
|
561
592
|
} else if (type[Kind] === 'byteArray') {
|
|
562
593
|
return Uint8Array.from(value, c => c.charCodeAt(0))
|
|
563
594
|
} else if (type[Kind] === 'union') {
|
|
@@ -645,7 +676,7 @@ export const parseEntry = <T extends STProps>(
|
|
|
645
676
|
export const responseParser = (response: any, ctx: Context, schema?: STResponse) => {
|
|
646
677
|
const details = {
|
|
647
678
|
status: ctx.set.status || 200,
|
|
648
|
-
headers: new Headers()
|
|
679
|
+
headers: new Headers(),
|
|
649
680
|
}
|
|
650
681
|
for (const [key, value] of Object.entries(ctx.set.headers)) {
|
|
651
682
|
if (Array.isArray(value)) {
|
|
@@ -686,9 +717,9 @@ export const responseParser = (response: any, ctx: Context, schema?: STResponse)
|
|
|
686
717
|
id = crypto.randomUUID()
|
|
687
718
|
}
|
|
688
719
|
controller.close()
|
|
689
|
-
}
|
|
720
|
+
},
|
|
690
721
|
})
|
|
691
|
-
details.headers.set('
|
|
722
|
+
details.headers.set('content-type', 'text/event-stream')
|
|
692
723
|
return new Response(rs, details)
|
|
693
724
|
} else {
|
|
694
725
|
try {
|
|
@@ -701,3 +732,24 @@ export const responseParser = (response: any, ctx: Context, schema?: STResponse)
|
|
|
701
732
|
}
|
|
702
733
|
}
|
|
703
734
|
}
|
|
735
|
+
|
|
736
|
+
const unionize = (b: any, schema: STUnion) => {
|
|
737
|
+
let res
|
|
738
|
+
let error
|
|
739
|
+
const discirminants = schema.anyOf.reduce(
|
|
740
|
+
(acc, obj) =>
|
|
741
|
+
acc.filter(k => obj?.props && k in obj.props && obj.props[k]?.[Kind] === 'literal' && !obj.props[k]?.Optional),
|
|
742
|
+
Object.keys(schema.anyOf[0]?.props || {})
|
|
743
|
+
)
|
|
744
|
+
for (let s of schema.anyOf) {
|
|
745
|
+
try {
|
|
746
|
+
res = validate(b, s, { parse: true })
|
|
747
|
+
if (res !== undefined) break
|
|
748
|
+
} catch (err: any) {
|
|
749
|
+
if (discirminants.every(d => !err?.[d]?.startsWith('Not a valid value'))) error = err
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (res !== undefined) return res
|
|
753
|
+
else if (error) throw new RequestError({ status: 400, payload: { body: error } })
|
|
754
|
+
else throw new RequestError({ status: 400, payload: { body: `No matching body schema found` } })
|
|
755
|
+
}
|