galbe 0.1.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/.github/workflows/build_test.yml +19 -0
- package/.github/workflows/deploy_website.yml +20 -0
- package/.prettierrc +10 -0
- package/README.md +39 -0
- package/bin/cli.ts +114 -0
- package/bun.lockb +0 -0
- package/docs/Getting Started/basic.md +1 -0
- package/docs/Guides/routes.md +1 -0
- package/docs/getting-started.md +5 -0
- package/package.json +49 -0
- package/scripts/build.ts +14 -0
- package/scripts/hooks/prepare-commit-msg +10 -0
- package/scripts/postinstall.ts +9 -0
- package/src/index.ts +353 -0
- package/src/parser.ts +656 -0
- package/src/router.ts +100 -0
- package/src/routes.ts +157 -0
- package/src/server.ts +174 -0
- package/src/types.ts +279 -0
- package/src/util.ts +12 -0
- package/src/validator.ts +117 -0
- package/test/hooks.test.ts +177 -0
- package/test/parser.test.ts +1198 -0
- package/test/plugins.test.ts +239 -0
- package/test/requests.test.ts +909 -0
- package/test/resources/image.png +0 -0
- package/test/resources/object.badSyntax.json +8 -0
- package/test/resources/object.json +8 -0
- package/test/resources/object.missing.json +6 -0
- package/test/resources/test.route.comment.ts +38 -0
- package/test/resources/test.route.empty.ts +9 -0
- package/test/responses.test.ts +109 -0
- package/test/routeFiles.test.ts +209 -0
- package/test/router.test.ts +207 -0
- package/test/test.utils.ts +102 -0
- package/tsconfig.json +23 -0
package/src/parser.ts
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import type { Static, TObject, TProperties } from '@sinclair/typebox'
|
|
2
|
+
import type {
|
|
3
|
+
MultipartFormData,
|
|
4
|
+
MaybeArray,
|
|
5
|
+
TBody,
|
|
6
|
+
TMultipartForm,
|
|
7
|
+
TUrlForm,
|
|
8
|
+
TUrlFormParam,
|
|
9
|
+
TMultipartFormParam,
|
|
10
|
+
TStream,
|
|
11
|
+
Context
|
|
12
|
+
} from './index'
|
|
13
|
+
|
|
14
|
+
import { Kind, Optional } from '@sinclair/typebox'
|
|
15
|
+
import { RequestError, Stream, T } from './index'
|
|
16
|
+
import { validate } from './validator'
|
|
17
|
+
import { readableStreamToArrayBuffer } from 'bun'
|
|
18
|
+
|
|
19
|
+
const textDecoder = new TextDecoder()
|
|
20
|
+
const textEncoder = new TextEncoder()
|
|
21
|
+
|
|
22
|
+
async function* rsToAsyncIterator(readableStream: ReadableStream) {
|
|
23
|
+
try {
|
|
24
|
+
for await (const chunk of readableStream) yield chunk
|
|
25
|
+
} finally {
|
|
26
|
+
readableStream.cancel()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const BA_HEADER = 'application/octet-stream'
|
|
31
|
+
const JSON_HEADER = 'application/json'
|
|
32
|
+
const TXT_HEADER_RX = /^text\//
|
|
33
|
+
const FORM_HEADER_RX = /^application\/x-www-form-urlencoded/
|
|
34
|
+
const MP_HEADER_RX = /^multipart\/form-data/
|
|
35
|
+
|
|
36
|
+
export const requestBodyParser = async (
|
|
37
|
+
body: ReadableStream | null,
|
|
38
|
+
headers: { 'content-type'?: string; 'content-length'?: string },
|
|
39
|
+
schema?: TBody
|
|
40
|
+
) => {
|
|
41
|
+
const { 'content-type': contentType, 'content-length': _contentLength } = headers
|
|
42
|
+
const kind = schema?.[Kind]
|
|
43
|
+
const isStream = schema && Stream in schema
|
|
44
|
+
try {
|
|
45
|
+
if (body === null) {
|
|
46
|
+
if (schema) {
|
|
47
|
+
if (kind === 'ByteArray') {
|
|
48
|
+
if (Stream in schema) {
|
|
49
|
+
return new ReadableStream({
|
|
50
|
+
start(controller) {
|
|
51
|
+
controller.enqueue(new Uint8Array())
|
|
52
|
+
controller.close()
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
} else return new Uint8Array()
|
|
56
|
+
} else if (schema?.[Optional]) {
|
|
57
|
+
return null
|
|
58
|
+
} else {
|
|
59
|
+
throw new RequestError({ status: 400, error: { body: `Not a valid ${kind}` } })
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
if (contentType === BA_HEADER) {
|
|
63
|
+
return new Uint8Array()
|
|
64
|
+
} else if (contentType === JSON_HEADER) {
|
|
65
|
+
throw new RequestError({ status: 400, error: { body: 'Not a valid json' } })
|
|
66
|
+
} else if (contentType?.match(TXT_HEADER_RX)) {
|
|
67
|
+
throw new RequestError({ status: 400, error: { body: 'Not a valid text' } })
|
|
68
|
+
} else if (contentType?.match(FORM_HEADER_RX)) {
|
|
69
|
+
throw new RequestError({ status: 400, error: { body: 'Not a valid form' } })
|
|
70
|
+
} else if (contentType?.match(MP_HEADER_RX)) {
|
|
71
|
+
throw new RequestError({ status: 400, error: { body: 'Not a valid multipart form' } })
|
|
72
|
+
} else return null
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
if (kind === 'ByteArray') {
|
|
76
|
+
if (isStream) return rsToAsyncIterator(body)
|
|
77
|
+
const bytes = await readableStreamToArrayBuffer(body)
|
|
78
|
+
return new Uint8Array(bytes)
|
|
79
|
+
} else if (kind === 'String' && isStream) {
|
|
80
|
+
return $streamToString(body)
|
|
81
|
+
} else if (
|
|
82
|
+
(!contentType?.match(FORM_HEADER_RX) && kind === 'UrlForm') ||
|
|
83
|
+
(!contentType?.match(MP_HEADER_RX) && kind === 'MultipartForm')
|
|
84
|
+
) {
|
|
85
|
+
let received = contentType?.match(FORM_HEADER_RX)
|
|
86
|
+
? 'UrlForm'
|
|
87
|
+
: contentType?.match(MP_HEADER_RX)
|
|
88
|
+
? 'MultipartForm'
|
|
89
|
+
: contentType
|
|
90
|
+
throw new RequestError({ status: 400, error: { body: `Expected ${kind}, received ${received}` } })
|
|
91
|
+
} else if (!contentType || contentType === BA_HEADER) {
|
|
92
|
+
if (!schema) {
|
|
93
|
+
if (isStream) return rsToAsyncIterator(body)
|
|
94
|
+
const bytes = []
|
|
95
|
+
for await (const b of body) bytes.push(...b)
|
|
96
|
+
return new Uint8Array(bytes)
|
|
97
|
+
} else {
|
|
98
|
+
if (isStream) return rsToAsyncIterator(body)
|
|
99
|
+
else return await streamToString(body, schema)
|
|
100
|
+
}
|
|
101
|
+
} else if (contentType === JSON_HEADER) {
|
|
102
|
+
if (!schema) {
|
|
103
|
+
return await streamToString(body, T.Object(T.Any()))
|
|
104
|
+
} else {
|
|
105
|
+
const str = await streamToString(body)
|
|
106
|
+
let json
|
|
107
|
+
try {
|
|
108
|
+
json = JSON.parse(str)
|
|
109
|
+
} catch (err: any) {
|
|
110
|
+
throw new RequestError({
|
|
111
|
+
status: 400,
|
|
112
|
+
error: { body: err?.message ?? 'Parsing error' }
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
return validate(json, schema, true)
|
|
116
|
+
}
|
|
117
|
+
} else if (contentType.match(TXT_HEADER_RX)) {
|
|
118
|
+
if (!schema) {
|
|
119
|
+
return await streamToString(body)
|
|
120
|
+
} else {
|
|
121
|
+
return await streamToString(body, schema)
|
|
122
|
+
}
|
|
123
|
+
} else if (contentType.match(FORM_HEADER_RX)) {
|
|
124
|
+
if (!schema) {
|
|
125
|
+
return await streamToUrlForm(body)
|
|
126
|
+
} else {
|
|
127
|
+
if (kind !== 'UrlForm')
|
|
128
|
+
throw new RequestError({ status: 400, error: { body: `Expected ${kind}, received UrlForm` } })
|
|
129
|
+
if (isStream) return $streamToUrlForm(body, schema as TStream<TUrlForm>)
|
|
130
|
+
else return await streamToUrlForm(body, schema as TUrlForm)
|
|
131
|
+
}
|
|
132
|
+
} else if (contentType.match(MP_HEADER_RX)) {
|
|
133
|
+
const boundary = contentType.match(/boundary\="?([^"]*)"?;?.*$/)?.[1] || ''
|
|
134
|
+
if (!schema) {
|
|
135
|
+
return await streamToMultipartForm(body, boundary)
|
|
136
|
+
} else {
|
|
137
|
+
if (kind !== 'MultipartForm')
|
|
138
|
+
throw new RequestError({ status: 400, error: { body: `Expected ${kind}, received MultipartForm` } })
|
|
139
|
+
if (isStream) return $streamToMultipartForm(body, boundary, schema as TStream<TMultipartForm>)
|
|
140
|
+
else {
|
|
141
|
+
return streamToMultipartForm(body, boundary, schema as TMultipartForm)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
return rsToAsyncIterator(body)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error instanceof RequestError) throw error
|
|
150
|
+
else throw new RequestError({ status: 400, error: { body: error } })
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function* $streamToString(body: ReadableStream) {
|
|
154
|
+
for await (const chunk of body) yield textDecoder.decode(chunk)
|
|
155
|
+
}
|
|
156
|
+
const streamToString = async (body: ReadableStream, schema?: TBody): Promise<any> => {
|
|
157
|
+
let res = ''
|
|
158
|
+
for await (const chunk of $streamToString(body)) res += chunk
|
|
159
|
+
if (schema) return validate(res, schema, true)
|
|
160
|
+
return res
|
|
161
|
+
}
|
|
162
|
+
async function* $streamToUrlForm(
|
|
163
|
+
body: ReadableStream<Uint8Array>,
|
|
164
|
+
schema?: TStream<TUrlForm>
|
|
165
|
+
): AsyncGenerator<[string, any]> {
|
|
166
|
+
let rest: Uint8Array = new Uint8Array()
|
|
167
|
+
let bK: Uint8Array = new Uint8Array()
|
|
168
|
+
let bV: Uint8Array = new Uint8Array()
|
|
169
|
+
let start = 0
|
|
170
|
+
const required = Object.fromEntries(
|
|
171
|
+
Object.entries(schema?.properties || {}).filter(([_, v]: [string, any]) => !(Optional in v))
|
|
172
|
+
)
|
|
173
|
+
for await (const chunk of body) {
|
|
174
|
+
start = 0
|
|
175
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
176
|
+
if (chunk[i] === 0x26) {
|
|
177
|
+
bV = new Uint8Array(rest.length + i - start)
|
|
178
|
+
bV.set(rest)
|
|
179
|
+
bV.set(chunk.slice(start, i), rest.length)
|
|
180
|
+
let [key, val]: [string, any] = [
|
|
181
|
+
decodeURIComponent(textDecoder.decode(bK)),
|
|
182
|
+
decodeURIComponent(textDecoder.decode(bV))
|
|
183
|
+
]
|
|
184
|
+
try {
|
|
185
|
+
let s =
|
|
186
|
+
schema?.properties?.[key]?.[Kind] === 'Array' ? schema?.properties?.[key].items : schema?.properties?.[key]
|
|
187
|
+
val = s ? paramParser(val, s) : val
|
|
188
|
+
} catch (error) {
|
|
189
|
+
throw new RequestError({ status: 400, error: { body: { [key]: error } } })
|
|
190
|
+
}
|
|
191
|
+
delete required[key]
|
|
192
|
+
yield [key, val]
|
|
193
|
+
bK = new Uint8Array()
|
|
194
|
+
bV = new Uint8Array()
|
|
195
|
+
start = i + 1
|
|
196
|
+
rest = new Uint8Array()
|
|
197
|
+
} else if (chunk[i] === 0x3d) {
|
|
198
|
+
bK = new Uint8Array(rest.length + i - start)
|
|
199
|
+
bK.set(rest)
|
|
200
|
+
bK.set(chunk.slice(start, i), rest.length)
|
|
201
|
+
start = i + 1
|
|
202
|
+
rest = new Uint8Array()
|
|
203
|
+
}
|
|
204
|
+
if (i === chunk.length - 1) {
|
|
205
|
+
const newRest = new Uint8Array(rest.length + i + 1 - start)
|
|
206
|
+
newRest.set(rest)
|
|
207
|
+
newRest.set(chunk.slice(start, i + 1), rest.length)
|
|
208
|
+
rest = newRest
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
let [key, val]: [string, any] = [
|
|
213
|
+
decodeURIComponent(textDecoder.decode(bK)),
|
|
214
|
+
decodeURIComponent(textDecoder.decode(rest))
|
|
215
|
+
]
|
|
216
|
+
try {
|
|
217
|
+
let s = schema?.properties?.[key]?.[Kind] === 'Array' ? schema?.properties?.[key].items : schema?.properties?.[key]
|
|
218
|
+
val = s ? paramParser(val, s) : val
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throw new RequestError({ status: 400, error: { body: { [key]: error } } })
|
|
221
|
+
}
|
|
222
|
+
delete required[key]
|
|
223
|
+
yield [key, val]
|
|
224
|
+
for (const [k, s] of Object.entries(required)) {
|
|
225
|
+
//@ts-ignore
|
|
226
|
+
if (s[Kind] === 'Array') {
|
|
227
|
+
yield [k, []]
|
|
228
|
+
delete required[k]
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const reqKeys = Object.keys(required)
|
|
232
|
+
if (reqKeys.length > 0)
|
|
233
|
+
throw new RequestError({
|
|
234
|
+
status: 400,
|
|
235
|
+
error: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?: TUrlForm) => {
|
|
239
|
+
let entries = []
|
|
240
|
+
const required = Object.fromEntries(
|
|
241
|
+
Object.entries(schema?.properties || {}).filter(([_, v]: [string, any]) => !(Optional in v))
|
|
242
|
+
)
|
|
243
|
+
let errors: Record<string, any> = {}
|
|
244
|
+
for await (const chunk of $streamToUrlForm(body)) entries.push(chunk)
|
|
245
|
+
const object: Record<string, any> = {}
|
|
246
|
+
for (let e of entries) {
|
|
247
|
+
if (e[0] in object) {
|
|
248
|
+
if (Array.isArray(object[e[0]])) object[e[0]].push(e[1])
|
|
249
|
+
else object[e[0]] = [object[e[0]], e[1]]
|
|
250
|
+
} else object[e[0]] = schema?.properties?.[e[0]]?.[Kind] === 'Array' ? [e[1]] : e[1]
|
|
251
|
+
}
|
|
252
|
+
if (schema?.properties)
|
|
253
|
+
for (let [k, v] of Object.entries(object)) {
|
|
254
|
+
delete required[k]
|
|
255
|
+
try {
|
|
256
|
+
object[k] = schema?.properties && k in schema?.properties ? paramParser(v, schema?.properties[k]) : v
|
|
257
|
+
} catch (error) {
|
|
258
|
+
errors[k] = k in errors ? [...errors[k], error] : error
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (Object.keys(errors).length) throw new RequestError({ status: 400, error: { body: errors } })
|
|
262
|
+
for (const [k, s] of Object.entries(required)) {
|
|
263
|
+
//@ts-ignore
|
|
264
|
+
if (s[Kind] === 'Array') {
|
|
265
|
+
object[k] = []
|
|
266
|
+
delete required[k]
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const reqKeys = Object.keys(required)
|
|
270
|
+
if (reqKeys.length > 0)
|
|
271
|
+
throw new RequestError({
|
|
272
|
+
status: 400,
|
|
273
|
+
error: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
|
|
274
|
+
})
|
|
275
|
+
return object
|
|
276
|
+
}
|
|
277
|
+
async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundary: string, schema?: TMultipartForm) {
|
|
278
|
+
const bound = textEncoder.encode(boundary)
|
|
279
|
+
let rest = new Uint8Array()
|
|
280
|
+
let bK: Uint8Array = new Uint8Array()
|
|
281
|
+
let bV: Uint8Array = new Uint8Array()
|
|
282
|
+
let start = 0
|
|
283
|
+
const required = Object.fromEntries(
|
|
284
|
+
Object.entries(schema?.properties || {}).filter(([_, v]: [string, any]) => !(Optional in v))
|
|
285
|
+
)
|
|
286
|
+
for await (const chunk of data) {
|
|
287
|
+
start = 0
|
|
288
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
289
|
+
let matchBound = true
|
|
290
|
+
for (let b = 0; b < bound.length; b++) {
|
|
291
|
+
if (chunk[i + b] === bound[b]) continue
|
|
292
|
+
else {
|
|
293
|
+
matchBound = false
|
|
294
|
+
break
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (matchBound) {
|
|
298
|
+
bV = new Uint8Array(rest.length + i - start)
|
|
299
|
+
bV.set(rest)
|
|
300
|
+
bV.set(chunk.slice(start, i), rest.length)
|
|
301
|
+
bV = bV.slice(1, bV.length - 4)
|
|
302
|
+
const headers = parseMultipartHeader(textDecoder.decode(bK))
|
|
303
|
+
if (headers) {
|
|
304
|
+
try {
|
|
305
|
+
delete required[headers.name]
|
|
306
|
+
yield {
|
|
307
|
+
headers,
|
|
308
|
+
content: parseMultipartContent(bV, headers, schema)
|
|
309
|
+
}
|
|
310
|
+
} catch (err) {
|
|
311
|
+
if (err instanceof RequestError) throw err
|
|
312
|
+
throw new RequestError({ status: 400, error: { body: { [headers.name]: err } } })
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
bK = new Uint8Array()
|
|
316
|
+
bV = new Uint8Array()
|
|
317
|
+
start = i + bound.length
|
|
318
|
+
rest = new Uint8Array()
|
|
319
|
+
i = start
|
|
320
|
+
} else if (chunk[i] === 0x0d && chunk[i + 1] === 0x0a && chunk[i + 2] === 0x0d) {
|
|
321
|
+
bK = new Uint8Array(rest.length + i - start)
|
|
322
|
+
bK.set(rest)
|
|
323
|
+
bK.set(chunk.slice(start, i), rest.length)
|
|
324
|
+
start = i + 3
|
|
325
|
+
rest = new Uint8Array()
|
|
326
|
+
i = start
|
|
327
|
+
}
|
|
328
|
+
if (i === chunk.length - 1) {
|
|
329
|
+
const newRest = new Uint8Array(rest.length + i - start + 1)
|
|
330
|
+
newRest.set(rest)
|
|
331
|
+
newRest.set(chunk.slice(start, i + 1), rest.length)
|
|
332
|
+
rest = newRest
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const headers = parseMultipartHeader(textDecoder.decode(bK))
|
|
337
|
+
if (headers) {
|
|
338
|
+
try {
|
|
339
|
+
delete required[headers.name]
|
|
340
|
+
yield {
|
|
341
|
+
headers,
|
|
342
|
+
content: parseMultipartContent(bV, headers, schema)
|
|
343
|
+
}
|
|
344
|
+
} catch (err) {
|
|
345
|
+
throw new RequestError({ status: 400, error: { body: { [headers.name]: err } } })
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
for (const [k, s] of Object.entries(required)) {
|
|
349
|
+
//@ts-ignore
|
|
350
|
+
if (s[Kind] === 'Array') {
|
|
351
|
+
yield { headers: { name: k }, content: [] }
|
|
352
|
+
delete required[k]
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const reqKeys = Object.keys(required)
|
|
356
|
+
if (reqKeys.length > 0)
|
|
357
|
+
throw new RequestError({
|
|
358
|
+
status: 400,
|
|
359
|
+
error: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
const parseMultipartHeader = (header: string): { name: string; [key: string]: string } | null => {
|
|
363
|
+
if (!header) return null
|
|
364
|
+
let disposition = 'form-data'
|
|
365
|
+
const multipartHeader = [
|
|
366
|
+
...header.matchAll(/\s*([\w-]+)\s*:\s*([^;]*);?/g),
|
|
367
|
+
...header.matchAll(/;?\s*(\w+)\s*=\s*\"([^"]*)\";?/g)
|
|
368
|
+
].reduce((acc: Record<string, string>, v: string[]) => {
|
|
369
|
+
const key = v[1].toLowerCase().replace(/^content-/, '')
|
|
370
|
+
if (key === 'disposition') {
|
|
371
|
+
disposition = v[2]
|
|
372
|
+
return acc
|
|
373
|
+
}
|
|
374
|
+
acc[key] = v[2]
|
|
375
|
+
return acc
|
|
376
|
+
}, {})
|
|
377
|
+
if (disposition !== 'form-data') null
|
|
378
|
+
//@ts-ignore
|
|
379
|
+
return multipartHeader
|
|
380
|
+
}
|
|
381
|
+
const parseMultipartContent = (
|
|
382
|
+
content: Uint8Array,
|
|
383
|
+
headers: { name: string; type?: string },
|
|
384
|
+
schema?: TMultipartForm
|
|
385
|
+
) => {
|
|
386
|
+
const type = headers?.type ?? 'text/plain'
|
|
387
|
+
let result: any = content
|
|
388
|
+
if (type === 'text/plain') {
|
|
389
|
+
const str = textDecoder.decode(content).trim()
|
|
390
|
+
let s = schema?.properties?.[headers.name]
|
|
391
|
+
return s ? paramParser(str, s?.[Kind] === 'Array' ? s?.items : s) : str
|
|
392
|
+
} else if (type === 'application/json') {
|
|
393
|
+
if (!schema?.properties || !(headers.name in schema?.properties)) {
|
|
394
|
+
try {
|
|
395
|
+
result = JSON.parse(textDecoder.decode(content).trim())
|
|
396
|
+
} catch (err: any) {
|
|
397
|
+
throw new RequestError({ status: 400, error: { body: { [headers.name]: err?.message || 'Parsing error' } } })
|
|
398
|
+
}
|
|
399
|
+
} else if (schema?.properties) {
|
|
400
|
+
if (schema?.properties[headers.name][Kind] === 'Object') {
|
|
401
|
+
try {
|
|
402
|
+
result = JSON.parse(textDecoder.decode(content).trim())
|
|
403
|
+
} catch (err: any) {
|
|
404
|
+
throw new RequestError({ status: 400, error: { body: { [headers.name]: err?.message || 'Parsing error' } } })
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
validate(result, schema?.properties[headers.name])
|
|
408
|
+
} catch (err) {
|
|
409
|
+
throw new RequestError({ status: 400, error: { body: { [headers.name]: err } } })
|
|
410
|
+
}
|
|
411
|
+
} else if (schema?.properties[headers.name][Kind] === 'ByteArray') {
|
|
412
|
+
return content
|
|
413
|
+
} else if (schema?.properties[headers.name][Kind] === 'String') {
|
|
414
|
+
result = textDecoder.decode(content).trim()
|
|
415
|
+
} else {
|
|
416
|
+
throw new RequestError({
|
|
417
|
+
status: 400,
|
|
418
|
+
error: { body: { [headers.name]: `Expect ${schema?.properties[headers.name][Kind]} found json` } }
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} else if (schema?.properties?.[headers.name]) {
|
|
423
|
+
try {
|
|
424
|
+
let s = schema?.properties[headers.name]
|
|
425
|
+
validate(result, s?.[Kind] === 'Array' ? s?.items : s)
|
|
426
|
+
} catch (err) {
|
|
427
|
+
throw new RequestError({ status: 400, error: { body: { [headers.name]: err } } })
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return result
|
|
431
|
+
}
|
|
432
|
+
const streamToMultipartForm = async (data: ReadableStream<Uint8Array>, boundary: string, schema?: TMultipartForm) => {
|
|
433
|
+
const res: Record<string, MultipartFormData> = {}
|
|
434
|
+
const errors: Record<string, any> = {}
|
|
435
|
+
const required = Object.fromEntries(
|
|
436
|
+
Object.entries(schema?.properties || {}).filter(([_, v]: [string, any]) => !(Optional in v))
|
|
437
|
+
)
|
|
438
|
+
for await (const chunk of $streamToMultipartForm(data, boundary)) {
|
|
439
|
+
if (chunk.headers.name in res) {
|
|
440
|
+
if (!Array.isArray(res[chunk.headers.name].content))
|
|
441
|
+
res[chunk.headers.name].content = [res[chunk.headers.name].content]
|
|
442
|
+
res[chunk.headers.name].content.push(chunk.content)
|
|
443
|
+
} else {
|
|
444
|
+
if (schema?.properties?.[chunk.headers.name]?.[Kind] === 'Array')
|
|
445
|
+
res[chunk.headers.name] = { ...chunk, content: [chunk.content] }
|
|
446
|
+
else res[chunk.headers.name] = chunk
|
|
447
|
+
}
|
|
448
|
+
delete required[chunk.headers.name]
|
|
449
|
+
|
|
450
|
+
if (schema?.properties && chunk?.headers?.name in schema.properties) {
|
|
451
|
+
try {
|
|
452
|
+
if (
|
|
453
|
+
Array.isArray(res[chunk.headers.name].content) &&
|
|
454
|
+
schema?.properties?.[chunk.headers.name]?.[Kind] !== 'Array'
|
|
455
|
+
)
|
|
456
|
+
throw `Multiple values found`
|
|
457
|
+
res[chunk.headers.name].content = validate(
|
|
458
|
+
res[chunk.headers.name].content,
|
|
459
|
+
schema?.properties[chunk.headers.name],
|
|
460
|
+
true
|
|
461
|
+
)
|
|
462
|
+
if (schema.properties[chunk.headers.name][Kind] === 'Array')
|
|
463
|
+
for (let [k, v] of Object.entries(res[chunk.headers.name].content)) {
|
|
464
|
+
try {
|
|
465
|
+
//@ts-ignore
|
|
466
|
+
res[chunk.headers.name].content[k] = paramParser(v, schema.properties[chunk.headers.name].items)
|
|
467
|
+
} catch (error) {
|
|
468
|
+
errors[chunk.headers.name] = chunk.headers.name in errors ? [...errors[chunk.headers.name], error] : error
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
} catch (err) {
|
|
472
|
+
errors[chunk.headers.name] = err
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (Object.keys(errors).length)
|
|
477
|
+
throw new RequestError({
|
|
478
|
+
status: 400,
|
|
479
|
+
error: { body: errors }
|
|
480
|
+
})
|
|
481
|
+
for (const [k, s] of Object.entries(required)) {
|
|
482
|
+
//@ts-ignore
|
|
483
|
+
if (s[Kind] === 'Array') {
|
|
484
|
+
res[k] = { headers: { name: k }, content: [] }
|
|
485
|
+
delete required[k]
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const reqKeys = Object.keys(required)
|
|
489
|
+
if (reqKeys.length > 0)
|
|
490
|
+
throw new RequestError({
|
|
491
|
+
status: 400,
|
|
492
|
+
error: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
|
|
493
|
+
})
|
|
494
|
+
return res
|
|
495
|
+
}
|
|
496
|
+
const paramParser = (value: string | string[] | null, type: TMultipartFormParam): MaybeArray<Static<TUrlFormParam>> => {
|
|
497
|
+
if (value === undefined) {
|
|
498
|
+
if (type[Optional]) return type?.default
|
|
499
|
+
else throw `Required`
|
|
500
|
+
} else if (value === null) return null
|
|
501
|
+
else if (Array.isArray(value)) {
|
|
502
|
+
if (type[Kind] !== 'Array') throw `Multiple values found`
|
|
503
|
+
validate(value, type)
|
|
504
|
+
let pv = []
|
|
505
|
+
let errors: Record<number, any> = {}
|
|
506
|
+
for (let [idx, v] of value.entries()) {
|
|
507
|
+
try {
|
|
508
|
+
pv.push(paramParser(v, type.items as TMultipartFormParam) as Static<TUrlFormParam>)
|
|
509
|
+
} catch (error) {
|
|
510
|
+
errors[idx] = error
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (Object.keys(errors).length) throw errors
|
|
514
|
+
return pv
|
|
515
|
+
} else {
|
|
516
|
+
if (type[Kind] === 'Boolean') {
|
|
517
|
+
if (typeof value === 'boolean') return value
|
|
518
|
+
if (value === 'true') return true
|
|
519
|
+
if (value === 'false') return false
|
|
520
|
+
else throw `${value} is not a valid boolean. Should be 'true' or 'false'`
|
|
521
|
+
} else if (type[Kind] === 'Integer') {
|
|
522
|
+
if (value === null || value === undefined) throw `${value} is not a valid integer`
|
|
523
|
+
const parsedValue = parseInt(value, 10)
|
|
524
|
+
if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `${value} is not a valid integer`
|
|
525
|
+
validate(parsedValue, type)
|
|
526
|
+
return parsedValue
|
|
527
|
+
} else if (type[Kind] === 'Number') {
|
|
528
|
+
if (value === null || value === undefined) throw `${value} is not a valid number`
|
|
529
|
+
const parsedValue = Number(value)
|
|
530
|
+
if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `${value} is not a valid number`
|
|
531
|
+
validate(parsedValue, type)
|
|
532
|
+
return parsedValue
|
|
533
|
+
} else if (type[Kind] === 'String') {
|
|
534
|
+
validate(value, type)
|
|
535
|
+
return value
|
|
536
|
+
} else if (type[Kind] === 'Literal') {
|
|
537
|
+
if (value !== type.const) throw `${value} is not a valid value`
|
|
538
|
+
return value
|
|
539
|
+
} else if (type[Kind] === 'Array') {
|
|
540
|
+
return [paramParser(value, type.items as TMultipartFormParam) as Static<TUrlFormParam>]
|
|
541
|
+
} else if (type[Kind] === 'ByteArray') {
|
|
542
|
+
return Uint8Array.from(value, c => c.charCodeAt(0))
|
|
543
|
+
} else if (type[Kind] === 'Union') {
|
|
544
|
+
const union = Object.values(type.anyOf)
|
|
545
|
+
for (const elt of union) {
|
|
546
|
+
try {
|
|
547
|
+
return paramParser(value, elt as TMultipartFormParam)
|
|
548
|
+
} catch (err) {
|
|
549
|
+
continue
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
throw `${value} could not be parsed to any of ${union.map(u => u?.const ?? u[Kind]).join(', ')}`
|
|
553
|
+
} else if (type[Kind] === 'Any') {
|
|
554
|
+
return value
|
|
555
|
+
}
|
|
556
|
+
throw `Unknown parsing type ${type[Kind]}`
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export const requestPathParser = (input: string, path: string) => {
|
|
561
|
+
let pPath = path.replace(/^\/$(.*)\/?$/, '$1').split('/')
|
|
562
|
+
let pInput = input.replace(/^\/$(.*)\/?$/, '$1').split('/')
|
|
563
|
+
let params: Record<string, any> = {}
|
|
564
|
+
pPath.shift()
|
|
565
|
+
pInput.shift()
|
|
566
|
+
for (const [i, p] of pPath.entries()) {
|
|
567
|
+
const match = p.match(/^:(.*)/)
|
|
568
|
+
if (match) params[match[1]] = pInput[i]
|
|
569
|
+
}
|
|
570
|
+
return params
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export const parseEntry = <T extends TProperties>(
|
|
574
|
+
params: { [key: string]: string | string[] },
|
|
575
|
+
schema: T,
|
|
576
|
+
options?: { name?: string; i?: boolean }
|
|
577
|
+
): Static<TObject<T>> => {
|
|
578
|
+
const parsedParams: Partial<Static<TObject<T>>> = {}
|
|
579
|
+
const errors: { [key: string]: string | string[] } = {}
|
|
580
|
+
|
|
581
|
+
if (options?.i === true) {
|
|
582
|
+
params = Object.keys(params).reduce((acc, key) => {
|
|
583
|
+
acc[key.toLowerCase()] = params[key]
|
|
584
|
+
return acc
|
|
585
|
+
}, {} as { [key: string]: string | string[] })
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
Object.entries(schema).forEach(([key, s]) => {
|
|
589
|
+
const k = options?.i === true ? key.toLowerCase() : key
|
|
590
|
+
let v = params[k]
|
|
591
|
+
if (s[Kind] === 'Array' && options?.name === 'query' && typeof v === 'string') v = v.split(',')
|
|
592
|
+
try {
|
|
593
|
+
let p = paramParser(v, s as TMultipartFormParam)
|
|
594
|
+
//@ts-ignore
|
|
595
|
+
if (p !== undefined) parsedParams[k] = p
|
|
596
|
+
} catch (errMsg) {
|
|
597
|
+
//@ts-ignore
|
|
598
|
+
errors[k] = errMsg
|
|
599
|
+
}
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
if (Object.keys(errors).length) {
|
|
603
|
+
throw new RequestError({ status: 400, error: options?.name ? { [options.name]: errors } : errors })
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return parsedParams as Static<TObject<T>>
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const isIterator = (obj: any) => typeof obj?.next === 'function'
|
|
610
|
+
|
|
611
|
+
export const responseParser = (response: any, ctx: Context) => {
|
|
612
|
+
const details = {
|
|
613
|
+
status: ctx.set.status || 200,
|
|
614
|
+
headers: new Headers(ctx.set.headers)
|
|
615
|
+
}
|
|
616
|
+
if (response instanceof Response) return response
|
|
617
|
+
else if (typeof response === 'string') {
|
|
618
|
+
if (!details?.headers?.has('content-type')) details?.headers?.set('content-type', 'text/plain')
|
|
619
|
+
//@ts-ignore
|
|
620
|
+
return new Response(response, details)
|
|
621
|
+
}
|
|
622
|
+
if (response instanceof ReadableStream) {
|
|
623
|
+
response = rsToAsyncIterator(response)
|
|
624
|
+
}
|
|
625
|
+
if (isIterator(response)) {
|
|
626
|
+
const rs = new ReadableStream({
|
|
627
|
+
type: 'direct',
|
|
628
|
+
async pull(controller) {
|
|
629
|
+
let id = ctx.request.headers.get('last-event-id') ?? crypto.randomUUID()
|
|
630
|
+
for await (const r of response) {
|
|
631
|
+
let data = `id:${id}\ndata:${r}\n\n`
|
|
632
|
+
try {
|
|
633
|
+
await controller.write(data)
|
|
634
|
+
// await controller.flush()
|
|
635
|
+
} catch (err) {
|
|
636
|
+
console.error(err)
|
|
637
|
+
}
|
|
638
|
+
id = crypto.randomUUID()
|
|
639
|
+
}
|
|
640
|
+
controller.close()
|
|
641
|
+
}
|
|
642
|
+
})
|
|
643
|
+
details.headers.set('Content-Type', 'text/event-stream')
|
|
644
|
+
//@ts-ignore
|
|
645
|
+
return new Response(rs, details)
|
|
646
|
+
} else {
|
|
647
|
+
try {
|
|
648
|
+
if (!details?.headers?.has('content-type')) details?.headers?.set('content-type', 'application/json')
|
|
649
|
+
//@ts-ignore
|
|
650
|
+
return new Response(JSON.stringify(response), details)
|
|
651
|
+
} catch (error) {
|
|
652
|
+
console.error(error)
|
|
653
|
+
throw new RequestError({ status: 500, error: 'Internal Server Error' })
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|