galbe 0.12.0 → 0.12.2

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.
Files changed (42) hide show
  1. package/bin/commands/generate/code/openapi.parser.ts +2 -2
  2. package/package.json +6 -1
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -35
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -23
  5. package/.github/workflows/build_test.yml +0 -17
  6. package/.github/workflows/deploy_website.yml +0 -20
  7. package/.github/workflows/release.yml +0 -39
  8. package/.prettierrc +0 -10
  9. package/bun.lock +0 -904
  10. package/bunfig.toml +0 -2
  11. package/docs/CONTRIBUTING.md +0 -105
  12. package/docs/cli.md +0 -343
  13. package/docs/configuration.md +0 -80
  14. package/docs/context.md +0 -104
  15. package/docs/error-handler.md +0 -54
  16. package/docs/getting-started.md +0 -164
  17. package/docs/handler.md +0 -119
  18. package/docs/hooks.md +0 -90
  19. package/docs/plugins.md +0 -146
  20. package/docs/router.md +0 -10
  21. package/docs/routes.md +0 -133
  22. package/docs/schemas.md +0 -327
  23. package/test/hooks.test.ts +0 -200
  24. package/test/parser.test.ts +0 -1358
  25. package/test/plugins.test.ts +0 -239
  26. package/test/requests.test.ts +0 -917
  27. package/test/resources/image.png +0 -0
  28. package/test/resources/object.badSyntax.json +0 -8
  29. package/test/resources/object.json +0 -8
  30. package/test/resources/object.missing.json +0 -6
  31. package/test/resources/static/chameleon.png +0 -0
  32. package/test/resources/static/index.html +0 -13
  33. package/test/resources/static/sub/index.html +0 -13
  34. package/test/resources/static/sub/other.html +0 -13
  35. package/test/resources/test.route.comment.ts +0 -58
  36. package/test/resources/test.route.empty.ts +0 -9
  37. package/test/responses.test.ts +0 -483
  38. package/test/routeFiles.test.ts +0 -239
  39. package/test/router.test.ts +0 -231
  40. package/test/test.utils.ts +0 -109
  41. package/test/types.test.ts +0 -909
  42. package/tsconfig.json +0 -23
@@ -1,1358 +0,0 @@
1
- import { describe, test, expect, beforeAll } from 'bun:test'
2
- import {
3
- formdata,
4
- type Case,
5
- fileHash,
6
- schema_object,
7
- handleBody,
8
- schema_objectBase,
9
- handleUrlFormStream,
10
- isAsyncIterator,
11
- EMPTY_BA_STR,
12
- } from './test.utils'
13
- import { Galbe, $T } from '../src'
14
- import { schemaToTypeStr } from '../src/schema'
15
-
16
- const port = 7357
17
-
18
- describe('parser', () => {
19
- beforeAll(async () => {
20
- const galbe = new Galbe()
21
-
22
- galbe.get(
23
- '/headers/schema',
24
- {
25
- headers: {
26
- string: $T.string(),
27
- zero: $T.number(),
28
- number: $T.number(),
29
- 'neg-number': $T.number(),
30
- float: $T.number(),
31
- integer: $T.integer(),
32
- 'boolean-true': $T.boolean(),
33
- 'boolean-false': $T.boolean(),
34
- },
35
- },
36
- ctx => {
37
- return ctx.headers
38
- }
39
- )
40
-
41
- galbe.get(
42
- '/params/schema/:p1/:p2/:p3/:p4',
43
- {
44
- params: {
45
- p1: $T.string(),
46
- p2: $T.number(),
47
- p3: $T.integer(),
48
- p4: $T.boolean(),
49
- },
50
- },
51
- ctx => {
52
- return ctx.params
53
- }
54
- )
55
-
56
- galbe.get(
57
- '/query/params/schema',
58
- {
59
- query: {
60
- p1: $T.string(),
61
- p2: $T.number(),
62
- p3: $T.boolean(),
63
- p4: $T.union([$T.number(), $T.boolean()]),
64
- p5: $T.optional($T.string()),
65
- },
66
- },
67
- ctx => {
68
- return ctx.query
69
- }
70
- )
71
- galbe.get(
72
- '/query/params/schema/constraints',
73
- {
74
- query: {
75
- default: $T.optional($T.string({ default: 'DEFAULT_VALUE' })),
76
- int: $T.integer({ exclusiveMin: 10, max: 42 }),
77
- num: $T.number({ min: 10, exclusiveMax: 42 }),
78
- str: $T.string({ minLength: 4, maxLength: 8, pattern: /^a.*z/ }),
79
- },
80
- },
81
- ctx => {
82
- return ctx.query
83
- }
84
- )
85
-
86
- galbe.post('/obj/schema/base', { body: { json: $T.object(schema_object) } }, handleBody)
87
-
88
- galbe.post(
89
- '/form/schema/base',
90
- {
91
- body: {
92
- urlForm: $T.object({
93
- ...schema_objectBase,
94
- object: $T.optional($T.object({ nested: $T.object({ foo: $T.literal('bar') }), baz: $T.number() })),
95
- union: $T.optional($T.union([$T.number(), $T.boolean()])),
96
- literal: $T.optional($T.literal('x')),
97
- array: $T.array(),
98
- }),
99
- },
100
- },
101
- handleBody
102
- )
103
- galbe.post(
104
- '/form/stream/schema/base',
105
- {
106
- body: {
107
- urlForm: $T.stream(
108
- $T.object({
109
- ...schema_objectBase,
110
- union: $T.optional($T.union([$T.number(), $T.boolean()])),
111
- literal: $T.optional($T.literal('x')),
112
- array: $T.array($T.any()),
113
- numArray: $T.optional($T.array($T.number())),
114
- })
115
- ),
116
- },
117
- },
118
- handleUrlFormStream
119
- )
120
- galbe.post(
121
- '/mp/schema/base',
122
- {
123
- body: {
124
- multipart: $T.multipartForm({
125
- ...schema_objectBase,
126
- union: $T.optional($T.union([$T.number(), $T.boolean()])),
127
- literal: $T.optional($T.literal('x')),
128
- array: $T.array($T.any()),
129
- }),
130
- },
131
- },
132
- handleBody
133
- )
134
- galbe.post(
135
- '/mp/schema/file',
136
- {
137
- body: {
138
- multipart: $T.multipartForm({
139
- foo: $T.byteArray(),
140
- }),
141
- },
142
- },
143
- handleBody
144
- )
145
- galbe.post(
146
- '/mp/stream/schema/base',
147
- {
148
- body: {
149
- multipart: $T.stream(
150
- $T.multipartForm({
151
- ...schema_objectBase,
152
- union: $T.optional($T.union([$T.number(), $T.boolean()])),
153
- literal: $T.optional($T.literal('x')),
154
- array: $T.array($T.any()),
155
- })
156
- ),
157
- },
158
- },
159
- handleBody
160
- )
161
-
162
- let schema_jsonFile = {
163
- string: $T.string(),
164
- number: $T.number(),
165
- bool: $T.boolean(),
166
- arrayStr: $T.array($T.string()),
167
- arrayNumber: $T.array($T.number()),
168
- arrayBool: $T.array($T.boolean()),
169
- }
170
-
171
- galbe.post(
172
- '/mp/file',
173
- { body: { multipart: $T.multipartForm({ imgFile: $T.byteArray(), jsonFile: $T.object(schema_jsonFile) }) } },
174
- handleBody
175
- )
176
- galbe.post(
177
- '/mp/stream/file',
178
- {
179
- body: {
180
- multipart: $T.stream($T.multipartForm({ imgFile: $T.byteArray(), jsonFile: $T.object(schema_jsonFile) })),
181
- },
182
- },
183
- async ctx => {
184
- if (isAsyncIterator(ctx.body)) {
185
- const chunks: any[] = []
186
- for await (const chunk of ctx.body) {
187
- const c = { ...chunk } as any
188
- if (chunk.content instanceof Uint8Array) c.content = await fileHash(chunk.content)
189
- chunks.push(c)
190
- }
191
- return { type: 'AsyncIterator', content: chunks }
192
- } else {
193
- return { type: null, content: 'error' }
194
- }
195
- }
196
- )
197
- galbe.post('/ba/file', { body: { byteArray: $T.byteArray() } }, async ctx => {
198
- if (ctx?.body instanceof Uint8Array) {
199
- return { type: 'byteArray', content: await fileHash(ctx.body) }
200
- } else {
201
- return { type: null, content: 'error' }
202
- }
203
- })
204
- galbe.post('/ba/stream/file', { body: { byteArray: $T.stream($T.byteArray()) } }, async ctx => {
205
- if (isAsyncIterator(ctx.body)) {
206
- let bytes = new Uint8Array()
207
- for await (const b of ctx.body) {
208
- bytes = new Uint8Array([...bytes, ...b])
209
- }
210
- return { type: 'AsyncIterator', content: await fileHash(bytes) }
211
- } else {
212
- return { type: null, content: 'error' }
213
- }
214
- })
215
-
216
- await galbe.listen(port)
217
- })
218
-
219
- test('headers, schema', async () => {
220
- const cases: any = [
221
- {
222
- h: {
223
- string: 'Hello',
224
- zero: '0',
225
- number: '42',
226
- 'Neg-Number': '-10',
227
- float: '3.14',
228
- integer: '42',
229
- 'boolean-true': 'true',
230
- 'boolean-false': 'false',
231
- },
232
- expected: {
233
- body: {
234
- string: 'Hello',
235
- zero: 0,
236
- number: 42,
237
- 'neg-number': -10,
238
- float: 3.14,
239
- integer: 42,
240
- 'boolean-true': true,
241
- 'boolean-false': false,
242
- },
243
- },
244
- },
245
- {
246
- h: {
247
- string: 'Hello',
248
- zero: '0',
249
- number: '42',
250
- float: '3.14',
251
- integer: '42',
252
- 'boolean-true': 'a',
253
- 'boolean-false': 'false',
254
- },
255
- expected: {
256
- status: 400,
257
- body: {
258
- headers: {
259
- 'neg-number': 'Required',
260
- 'boolean-true': "Not a valid boolean. Should be 'true' or 'false'",
261
- },
262
- },
263
- },
264
- },
265
- ]
266
-
267
- for (let { h, expected } of cases) {
268
- let resp = await fetch(`http://localhost:${port}/headers/schema`, { headers: h })
269
- let body = await resp.json()
270
- expect(resp.status).toBe(expected.status ?? 200)
271
- expect(body).toMatchObject(expected.body)
272
- }
273
- })
274
-
275
- test('path params, schema', async () => {
276
- const cases: any = [
277
- {
278
- p: { p1: 'one', p2: '3.14', p3: '42', p4: 'true' },
279
- expected: { body: { p1: 'one', p2: 3.14, p3: 42, p4: true } },
280
- },
281
- {
282
- p: { p1: '_', p2: 'test', p3: '42.5', p4: 'a' },
283
- expected: {
284
- status: 400,
285
- body: {
286
- params: {
287
- p2: 'Not a valid number',
288
- p3: 'Not a valid integer',
289
- p4: "Not a valid boolean. Should be 'true' or 'false'",
290
- },
291
- },
292
- },
293
- },
294
- ]
295
- for (let { p, expected } of cases) {
296
- let resp = await fetch(`http://localhost:${port}/params/schema/${p.p1}/${p.p2}/${p.p3}/${p.p4}`)
297
- let body = await resp.json()
298
- expect(resp.status).toBe(expected.status ?? 200)
299
- expect(body).toEqual(expected.body)
300
- }
301
- })
302
-
303
- test('query params, schema', async () => {
304
- const cases: any = [
305
- {
306
- p: { p1: 'one', p2: '3.14', p3: 'false', p4: '42' },
307
- expected: { body: { p1: 'one', p2: 3.14, p3: false, p4: 42 } },
308
- },
309
- {
310
- p: { p1: 'one', p2: '3.14', p3: 'true', p4: 'true', p5: 'hello' },
311
- expected: { body: { p1: 'one', p2: 3.14, p3: true, p4: true, p5: 'hello' } },
312
- },
313
- {
314
- p: { p1: '36', p2: 'a', p3: '0' },
315
- expected: {
316
- status: 400,
317
- body: {
318
- query: {
319
- p2: 'Not a valid number',
320
- p3: "Not a valid boolean. Should be 'true' or 'false'",
321
- p4: 'Required',
322
- },
323
- },
324
- },
325
- },
326
- ]
327
-
328
- for (let { p, expected } of cases) {
329
- let search = new URLSearchParams()
330
- for (const [k, v] of Object.entries(p)) search.append(k, v as string)
331
-
332
- let resp = await fetch(`http://localhost:${port}/query/params/schema?${search.toString()}`)
333
- let body = await resp.json()
334
-
335
- expect(resp.status).toBe(expected.status ?? 200)
336
- expect(body).toEqual(expected.body)
337
- }
338
- })
339
-
340
- test('query params, duplicates', async () => {
341
- const search = new URLSearchParams()
342
- search.append('p1', 'one')
343
- search.append('p1', 'two')
344
- search.append('p2', '3.14')
345
- search.append('p3', 'true')
346
- search.append('p4', '42')
347
-
348
- let resp = await fetch(`http://localhost:${port}/query/params/schema?${search.toString()}`)
349
- let body = await resp.json()
350
-
351
- expect(resp.status).toBe(400)
352
- expect(body).toEqual({ query: { p1: 'Multiple values found' } })
353
- })
354
-
355
- test('body, json, schema object', async () => {
356
- const type = 'application/json'
357
- const schema = 'obj/schema/base'
358
- const cases: Case[] = [
359
- {
360
- body: JSON.stringify({
361
- ba: '',
362
- string: '',
363
- number: 0,
364
- bool: false,
365
- object: {},
366
- array: [],
367
- any: false,
368
- null: null,
369
- nullable: null,
370
- nullish: 42,
371
- }),
372
- type,
373
- schema,
374
- expected: {
375
- status: 200,
376
- type: 'object',
377
- resp: {
378
- ba: '',
379
- string: '',
380
- number: 0,
381
- bool: false,
382
- object: {},
383
- array: [],
384
- any: false,
385
- null: null,
386
- nullable: null,
387
- nullish: 42,
388
- },
389
- },
390
- },
391
- {
392
- body: JSON.stringify({
393
- ba: 'Hello',
394
- string: 'Mom!',
395
- number: 42,
396
- bool: true,
397
- object: { foo: 'bar' },
398
- array: [false, 'one', 2],
399
- any: '36',
400
- optional: 'optional',
401
- null: null,
402
- nullable: 'test',
403
- nullish: null,
404
- }),
405
- type,
406
- schema,
407
- expected: {
408
- status: 200,
409
- type: 'object',
410
- resp: {
411
- ba: 'Hello',
412
- string: 'Mom!',
413
- number: 42,
414
- bool: true,
415
- object: { foo: 'bar' },
416
- array: [false, 'one', 2],
417
- any: '36',
418
- optional: 'optional',
419
- null: null,
420
- nullable: 'test',
421
- nullish: null,
422
- },
423
- },
424
- },
425
- {
426
- body: JSON.stringify({
427
- ba: false,
428
- string: 42,
429
- number: 'a',
430
- bool: 'x',
431
- object: [],
432
- array: {},
433
- any: {},
434
- null: '',
435
- }),
436
- type,
437
- schema,
438
- expected: {
439
- status: 400,
440
- resp: {
441
- body: {
442
- ba: 'Not a valid byteArray',
443
- string: 'Not a valid string',
444
- number: 'Not a valid number',
445
- bool: "Not a valid boolean. Should be 'true' or 'false'",
446
- object: 'Expected an object, not an array',
447
- array: 'Not a valid array',
448
- null: 'Expected null value got ',
449
- nullable: 'Required',
450
- },
451
- },
452
- },
453
- },
454
- ]
455
- for (let { body, type, schema, expected } of cases) {
456
- let resp = await fetch(`http://localhost:${port}/${schema}`, {
457
- method: 'POST',
458
- body,
459
- headers: { ...(type ? { 'content-type': type } : {}) },
460
- })
461
- let respBody = (await resp.json()) as { type: string; content: any }
462
-
463
- expect(resp.status).toBe(expected.status)
464
- if (resp.status === 200) {
465
- if (expected.type) expect(respBody.type).toEqual(expected.type)
466
- if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
467
- else expect(respBody.content).toEqual(expected.resp)
468
- } else {
469
- if (expected.resp === null) expect(respBody.content).toBeNull
470
- else expect(respBody).toEqual(expected.resp)
471
- }
472
- }
473
- })
474
-
475
- test('body, UrlForm, schema object', async () => {
476
- const type = 'application/x-www-form-urlencoded'
477
- const schema = 'form/schema/base'
478
-
479
- const cases: Case[] = [
480
- {
481
- body: 'ba=&string=&number=0&bool=false&any=false&union=true',
482
- type,
483
- schema,
484
- expected: {
485
- status: 200,
486
- type: 'object',
487
- resp: {
488
- ba: EMPTY_BA_STR,
489
- string: '',
490
- number: 0,
491
- bool: false,
492
- any: 'false',
493
- union: true,
494
- array: [],
495
- },
496
- },
497
- },
498
- {
499
- body: 'ba=Hello&string=Mom!&number=42&bool=true&any=36&optional=optional&array=1&array=2&literal=x&union=42',
500
- type,
501
- schema,
502
- expected: {
503
- status: 200,
504
- type: 'object',
505
- resp: {
506
- ba: '185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969',
507
- string: 'Mom!',
508
- number: 42,
509
- bool: true,
510
- any: '36',
511
- optional: 'optional',
512
- literal: 'x',
513
- union: 42,
514
- array: ['1', '2'],
515
- },
516
- },
517
- },
518
- {
519
- body: 'optional=optional',
520
- type,
521
- schema,
522
- expected: {
523
- status: 400,
524
- type: 'object',
525
- resp: {
526
- body: 'Missing fields: ba, string, number, bool, any',
527
- },
528
- },
529
- },
530
- {
531
- body: 'ba=&string=Hello&string=Mom!&number=aaa&bool=1&any=36&literal=y&union=X',
532
- type,
533
- schema,
534
- expected: {
535
- status: 400,
536
- resp: {
537
- body: {
538
- string: 'Multiple values found',
539
- number: 'Not a valid number',
540
- bool: "Not a valid boolean. Should be 'true' or 'false'",
541
- literal: 'Not a valid value',
542
- union: 'Could not be parsed to any of [number, boolean]',
543
- },
544
- },
545
- },
546
- },
547
- {
548
- body: 'object=%7B%22nested%22%3A%7B%22foo%22%3A%22bar%22%7D%2C%22baz%22%3A42%7D&ba=&string=&number=0&bool=false&any=any',
549
- type,
550
- schema,
551
- expected: {
552
- status: 200,
553
- resp: {
554
- object: { nested: { foo: 'bar' }, baz: 42 },
555
- any: 'any',
556
- string: '',
557
- array: [],
558
- ba: EMPTY_BA_STR,
559
- bool: false,
560
- number: 0,
561
- },
562
- },
563
- },
564
- {
565
- body: 'object=&ba=&string=&number=0&bool=false&any=any',
566
- type,
567
- schema,
568
- expected: {
569
- status: 400,
570
- resp: {
571
- body: { object: 'Not a valid object' },
572
- },
573
- },
574
- },
575
- {
576
- body: 'object=%7B%22nested%22%3A%7B%22foo%22%3A%22toto%22%7D%2C%22baz%22%3A42%7D&ba=&string=&number=0&bool=false&any=any',
577
- type,
578
- schema,
579
- expected: {
580
- status: 400,
581
- resp: {
582
- body: { object: { nested: { foo: 'Not a valid value. Found "toto" but expected "bar"' } } },
583
- },
584
- },
585
- },
586
- ]
587
- for (let { body, type, schema, expected } of cases) {
588
- let resp = await fetch(`http://localhost:${port}/${schema}`, {
589
- method: 'POST',
590
- body,
591
- headers: { ...(type ? { 'content-type': type } : {}) },
592
- })
593
- let respBody = (await resp.json()) as { type: string; content: any }
594
-
595
- console.log(respBody)
596
- expect(resp.status).toBe(expected.status)
597
- if (resp.status === 200) {
598
- if (expected.type) expect(respBody.type).toEqual(expected.type)
599
- if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
600
- else expect(respBody.content).toEqual(expected.resp)
601
- } else {
602
- if (expected.resp === null) expect(respBody.content).toBeNull
603
- else expect(respBody).toEqual(expected.resp)
604
- }
605
- }
606
- })
607
-
608
- test('body, UrlForm Stream, schema object', async () => {
609
- const type = 'application/x-www-form-urlencoded'
610
- const schema = 'form/stream/schema/base'
611
-
612
- const cases: Case[] = [
613
- {
614
- body: 'ba=&string=&number=0&bool=false&any=false&union=true',
615
- type,
616
- schema,
617
- expected: {
618
- status: 200,
619
- type: 'object',
620
- resp: {
621
- ba: {},
622
- string: '',
623
- number: 0,
624
- bool: false,
625
- any: 'false',
626
- union: true,
627
- array: [],
628
- },
629
- },
630
- },
631
- {
632
- body: 'ba=Hello&string=Mom!&number=42&bool=true&any=36&optional=optional&array=1&array=2&literal=x&union=42',
633
- type,
634
- schema,
635
- expected: {
636
- status: 200,
637
- type: 'object',
638
- resp: {
639
- ba: Object.fromEntries(new Uint8Array([72, 101, 108, 108, 111]).entries()),
640
- string: 'Mom!',
641
- number: 42,
642
- bool: true,
643
- any: '36',
644
- optional: 'optional',
645
- literal: 'x',
646
- union: 42,
647
- array: ['1', '2'],
648
- },
649
- },
650
- },
651
- {
652
- body: 'ba=Hello&string=Mom!&number=42&bool=true&any=36&optional=optional&array=1&array=2&literal=x&union=42&numArray=x',
653
- type,
654
- schema,
655
- expected: {
656
- status: 400,
657
- type: 'object',
658
- resp: {
659
- body: { numArray: 'Not a valid number' },
660
- },
661
- },
662
- },
663
- {
664
- body: 'ba=&string=Hello&string=Mom!&number=1&bool=true&any=36&literal=y&union=X',
665
- type,
666
- schema,
667
- expected: {
668
- status: 400,
669
- resp: {
670
- body: {
671
- literal: 'Not a valid value',
672
- },
673
- },
674
- },
675
- },
676
- {
677
- body: 'optional=otpional',
678
- type,
679
- schema,
680
- expected: {
681
- status: 400,
682
- resp: {
683
- body: 'Missing fields: ba, string, number, bool, any',
684
- },
685
- },
686
- },
687
- ]
688
- for (let { body, type, schema, expected } of cases) {
689
- let resp = await fetch(`http://localhost:${port}/${schema}`, {
690
- method: 'POST',
691
- body,
692
- headers: { ...(type ? { 'content-type': type } : {}) },
693
- })
694
- let respBody = (await resp.json()) as any
695
-
696
- expect(resp.status).toBe(expected.status)
697
- if (resp.status === 200) {
698
- if (expected.type) expect(respBody.type).toEqual(expected.type)
699
- if (expected.type === 'object' && expected.resp.content === null) expect(respBody).toBeNull
700
- else expect(respBody.content).toEqual(expected.resp)
701
- } else {
702
- if (expected.resp === null) expect(respBody).toBeNull
703
- else expect(respBody).toEqual(expected.resp)
704
- }
705
- }
706
- })
707
-
708
- test('body, MultipartForm, schema object', async () => {
709
- const schema = 'mp/schema/base'
710
-
711
- const formWithHeader = formdata({})
712
- formWithHeader.append('foo', new Blob([new Uint8Array()], { type: 'image/png' }), 'foo.png')
713
-
714
- const cases: Case[] = [
715
- {
716
- body: formdata({ ba: '', string: '', number: '0', bool: 'false', any: 'false', union: 'true' }),
717
- schema,
718
- expected: {
719
- status: 200,
720
- type: 'object',
721
- resp: {
722
- ba: {
723
- content: EMPTY_BA_STR,
724
- headers: {
725
- name: 'ba',
726
- },
727
- },
728
- string: {
729
- content: '',
730
- headers: {
731
- name: 'string',
732
- },
733
- },
734
- number: {
735
- content: 0,
736
- headers: {
737
- name: 'number',
738
- },
739
- },
740
- bool: {
741
- content: false,
742
- headers: {
743
- name: 'bool',
744
- },
745
- },
746
- any: {
747
- content: 'false',
748
- headers: {
749
- name: 'any',
750
- },
751
- },
752
- union: {
753
- content: true,
754
- headers: {
755
- name: 'union',
756
- },
757
- },
758
- array: {
759
- content: [],
760
- headers: {
761
- name: 'array',
762
- },
763
- },
764
- },
765
- },
766
- },
767
- {
768
- body: formWithHeader,
769
- schema: 'mp/schema/file',
770
- expected: {
771
- status: 200,
772
- type: 'object',
773
- resp: {
774
- foo: {
775
- content: EMPTY_BA_STR,
776
- headers: {
777
- name: 'foo',
778
- filename: 'foo.png',
779
- type: 'image/png',
780
- },
781
- },
782
- },
783
- },
784
- },
785
- {
786
- body: formdata({
787
- ba: 'Hello',
788
- string: 'Mom!',
789
- number: '42',
790
- bool: 'true',
791
- any: '36',
792
- optional: 'optional',
793
- array: ['1', '2'],
794
- literal: 'x',
795
- union: '42',
796
- }),
797
- schema,
798
- expected: {
799
- status: 200,
800
- type: 'object',
801
- resp: {
802
- ba: {
803
- content: '185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969',
804
- headers: {
805
- name: 'ba',
806
- },
807
- },
808
- string: {
809
- content: 'Mom!',
810
- headers: {
811
- name: 'string',
812
- },
813
- },
814
- number: {
815
- content: 42,
816
- headers: {
817
- name: 'number',
818
- },
819
- },
820
- bool: {
821
- content: true,
822
- headers: {
823
- name: 'bool',
824
- },
825
- },
826
- any: {
827
- content: '36',
828
- headers: {
829
- name: 'any',
830
- },
831
- },
832
- optional: {
833
- content: 'optional',
834
- headers: {
835
- name: 'optional',
836
- },
837
- },
838
- array: {
839
- content: ['1', '2'],
840
- headers: {
841
- name: 'array',
842
- },
843
- },
844
- literal: {
845
- content: 'x',
846
- headers: {
847
- name: 'literal',
848
- },
849
- },
850
- union: {
851
- content: 42,
852
- headers: {
853
- name: 'union',
854
- },
855
- },
856
- },
857
- },
858
- },
859
- {
860
- body: formdata({ optional: 'optional' }),
861
- schema,
862
- expected: {
863
- status: 400,
864
- type: 'object',
865
- resp: {
866
- body: 'Missing fields: ba, string, number, bool, any',
867
- },
868
- },
869
- },
870
- {
871
- body: formdata({
872
- ba: '',
873
- string: ['Hello', 'Mom!'],
874
- number: 'aaa',
875
- bool: '1',
876
- any: '36',
877
- literal: 'y',
878
- union: 'X',
879
- }),
880
- schema,
881
- expected: {
882
- status: 400,
883
- resp: {
884
- body: {
885
- string: 'Multiple values found',
886
- number: 'Not a valid number',
887
- bool: "Not a valid boolean. Should be 'true' or 'false'",
888
- literal: 'Not a valid value. Found "y" but expected "x"',
889
- union: 'Could not be parsed to any of [number, boolean]',
890
- },
891
- },
892
- },
893
- },
894
- ]
895
- for (let { body, type, schema, expected } of cases) {
896
- let resp = await fetch(`http://localhost:${port}/${schema}`, {
897
- method: 'POST',
898
- body,
899
- headers: { ...(type ? { 'content-type': type } : {}) },
900
- })
901
- let respBody = (await resp.json()) as { type: string; content: any }
902
-
903
- expect(resp.status).toBe(expected.status)
904
- if (resp.status === 200) {
905
- if (expected.type) expect(respBody.type).toEqual(expected.type)
906
- if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
907
- else expect(respBody.content).toEqual(expected.resp)
908
- } else {
909
- if (expected.resp === null) expect(respBody.content).toBeNull
910
- else expect(respBody).toEqual(expected.resp)
911
- }
912
- }
913
- })
914
-
915
- test('body, MultipartForm Stream, schema object', async () => {
916
- const schema = 'mp/stream/schema/base'
917
-
918
- const cases: Case[] = [
919
- {
920
- body: formdata({ ba: '', string: '', number: '0', bool: 'false', any: 'false', union: 'true' }),
921
- schema,
922
- expected: {
923
- status: 200,
924
- type: 'AsyncIterator',
925
- resp: [
926
- {
927
- content: {},
928
- headers: {
929
- name: 'ba',
930
- },
931
- },
932
- {
933
- content: '',
934
- headers: {
935
- name: 'string',
936
- },
937
- },
938
- {
939
- content: 0,
940
- headers: {
941
- name: 'number',
942
- },
943
- },
944
- {
945
- content: false,
946
- headers: {
947
- name: 'bool',
948
- },
949
- },
950
- {
951
- content: 'false',
952
- headers: {
953
- name: 'any',
954
- },
955
- },
956
- {
957
- content: true,
958
- headers: {
959
- name: 'union',
960
- },
961
- },
962
- {
963
- content: [],
964
- headers: {
965
- name: 'array',
966
- },
967
- },
968
- ],
969
- },
970
- },
971
- {
972
- body: formdata({
973
- ba: 'Hello',
974
- string: 'Mom!',
975
- number: '42',
976
- bool: 'true',
977
- any: '36',
978
- optional: 'optional',
979
- array: ['1', '2'],
980
- literal: 'x',
981
- union: '42',
982
- }),
983
- schema,
984
- expected: {
985
- status: 200,
986
- type: 'AsyncIterator',
987
- resp: [
988
- {
989
- content: Object.fromEntries(Uint8Array.from('Hello', c => c.charCodeAt(0)).entries()),
990
- headers: {
991
- name: 'ba',
992
- },
993
- },
994
- {
995
- content: 'Mom!',
996
- headers: {
997
- name: 'string',
998
- },
999
- },
1000
- {
1001
- content: 42,
1002
- headers: {
1003
- name: 'number',
1004
- },
1005
- },
1006
- {
1007
- content: true,
1008
- headers: {
1009
- name: 'bool',
1010
- },
1011
- },
1012
- {
1013
- content: '36',
1014
- headers: {
1015
- name: 'any',
1016
- },
1017
- },
1018
- {
1019
- content: 'optional',
1020
- headers: {
1021
- name: 'optional',
1022
- },
1023
- },
1024
- {
1025
- content: '1',
1026
- headers: {
1027
- name: 'array',
1028
- },
1029
- },
1030
- {
1031
- content: '2',
1032
- headers: {
1033
- name: 'array',
1034
- },
1035
- },
1036
- {
1037
- content: 'x',
1038
- headers: {
1039
- name: 'literal',
1040
- },
1041
- },
1042
- {
1043
- content: 42,
1044
- headers: {
1045
- name: 'union',
1046
- },
1047
- },
1048
- ],
1049
- },
1050
- },
1051
- {
1052
- body: formdata({ optional: 'optional' }),
1053
- schema,
1054
- expected: {
1055
- status: 400,
1056
- type: 'object',
1057
- resp: {
1058
- body: 'Missing fields: ba, string, number, bool, any',
1059
- },
1060
- },
1061
- },
1062
- {
1063
- body: formdata({
1064
- ba: '',
1065
- string: ['Hello', 'Mom!'],
1066
- number: 'aaa',
1067
- bool: '1',
1068
- any: '36',
1069
- literal: 'y',
1070
- union: 'X',
1071
- }),
1072
- schema,
1073
- expected: {
1074
- status: 400,
1075
- resp: {
1076
- body: {
1077
- number: 'Not a valid number',
1078
- },
1079
- },
1080
- },
1081
- },
1082
- ]
1083
- for (let { body, type, schema, expected } of cases) {
1084
- let resp = await fetch(`http://localhost:${port}/${schema}`, {
1085
- method: 'POST',
1086
- body,
1087
- headers: { ...(type ? { 'content-type': type } : {}) },
1088
- })
1089
- let respBody = (await resp.json()) as { type: string; content: any }
1090
-
1091
- expect(resp.status).toBe(expected.status)
1092
- if (resp.status === 200) {
1093
- if (expected.type) expect(respBody.type).toEqual(expected.type)
1094
- if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
1095
- else expect(respBody.content).toEqual(expected.resp)
1096
- } else {
1097
- if (expected.resp === null) expect(respBody.content).toBeNull
1098
- else expect(respBody).toEqual(expected.resp)
1099
- }
1100
- }
1101
- })
1102
-
1103
- test('body, FileUpload', async () => {
1104
- const imgFile = Bun.file('test/resources/image.png')
1105
- const imgFileBytes = new Uint8Array(await imgFile.arrayBuffer())
1106
-
1107
- const jsonFile = Bun.file('test/resources/object.json')
1108
- const missingJsonFile = Bun.file('test/resources/object.missing.json')
1109
- const badSyntaxJsonFile = Bun.file('test/resources/object.badSyntax.json')
1110
-
1111
- const cases: Case[] = [
1112
- {
1113
- body: formdata({ imgFile, jsonFile }),
1114
- schema: '/mp/file',
1115
- expected: {
1116
- status: 200,
1117
- type: 'object',
1118
- resp: {
1119
- imgFile: {
1120
- content: await fileHash(imgFileBytes),
1121
- headers: {
1122
- name: 'imgFile',
1123
- filename: 'test/resources/image.png',
1124
- type: 'image/png',
1125
- },
1126
- },
1127
- jsonFile: {
1128
- content: {
1129
- string: 'test',
1130
- number: 3.14,
1131
- bool: true,
1132
- arrayStr: ['un', 'deux', 'trois'],
1133
- arrayNumber: [0],
1134
- arrayBool: [true, false],
1135
- },
1136
- headers: {
1137
- name: 'jsonFile',
1138
- filename: 'test/resources/object.json',
1139
- type: 'application/json',
1140
- },
1141
- },
1142
- },
1143
- },
1144
- },
1145
- {
1146
- body: formdata({ imgFile, jsonFile: missingJsonFile }),
1147
- schema: '/mp/file',
1148
- expected: {
1149
- status: 400,
1150
- resp: {
1151
- body: { jsonFile: { arrayBool: 'Required', arrayStr: 'Required' } },
1152
- },
1153
- },
1154
- },
1155
- {
1156
- body: formdata({ imgFile, jsonFile: badSyntaxJsonFile }),
1157
- schema: '/mp/file',
1158
- expected: {
1159
- status: 400,
1160
- resp: {
1161
- body: { jsonFile: "JSON Parse error: Expected '}'" },
1162
- },
1163
- },
1164
- },
1165
- {
1166
- body: imgFileBytes,
1167
- schema: '/ba/file',
1168
- type: 'application/octet-stream',
1169
- expected: {
1170
- status: 200,
1171
- type: 'byteArray',
1172
- resp: await fileHash(imgFileBytes),
1173
- },
1174
- },
1175
- ]
1176
- for (let { body, type, schema, expected } of cases) {
1177
- let resp = await fetch(`http://localhost:${port}/${schema}`, {
1178
- method: 'POST',
1179
- body,
1180
- headers: { ...(type ? { 'content-type': type } : {}) },
1181
- })
1182
- let respBody = (await resp.json()) as { type: string; content: any }
1183
-
1184
- expect(resp.status).toBe(expected.status)
1185
- if (resp.status === 200) {
1186
- if (expected.type) expect(respBody.type).toEqual(expected.type)
1187
- if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
1188
- else expect(respBody.content).toEqual(expected.resp)
1189
- } else {
1190
- if (expected.resp === null) expect(respBody.content).toBeNull
1191
- else expect(respBody).toEqual(expected.resp)
1192
- }
1193
- }
1194
- })
1195
-
1196
- test('body, FileUpload Stream', async () => {
1197
- const imgFile = Bun.file('test/resources/image.png')
1198
- const imgFileBytes = new Uint8Array(await imgFile.arrayBuffer())
1199
-
1200
- const jsonFile = Bun.file('test/resources/object.json')
1201
- const missingJsonFile = Bun.file('test/resources/object.missing.json')
1202
- const badSyntaxJsonFile = Bun.file('test/resources/object.badSyntax.json')
1203
-
1204
- const cases: Case[] = [
1205
- {
1206
- body: formdata({ imgFile, jsonFile }),
1207
- schema: '/mp/stream/file',
1208
- expected: {
1209
- status: 200,
1210
- type: 'AsyncIterator',
1211
- resp: [
1212
- {
1213
- content: await fileHash(imgFileBytes),
1214
- headers: {
1215
- name: 'imgFile',
1216
- filename: 'test/resources/image.png',
1217
- type: 'image/png',
1218
- },
1219
- },
1220
- {
1221
- content: {
1222
- string: 'test',
1223
- number: 3.14,
1224
- bool: true,
1225
- arrayStr: ['un', 'deux', 'trois'],
1226
- arrayNumber: [0],
1227
- arrayBool: [true, false],
1228
- },
1229
- headers: {
1230
- name: 'jsonFile',
1231
- filename: 'test/resources/object.json',
1232
- type: 'application/json',
1233
- },
1234
- },
1235
- ],
1236
- },
1237
- },
1238
- {
1239
- body: formdata({ imgFile, jsonFile: missingJsonFile }),
1240
- schema: '/mp/stream/file',
1241
- expected: {
1242
- status: 400,
1243
- resp: {
1244
- body: { jsonFile: { arrayBool: 'Required', arrayStr: 'Required' } },
1245
- },
1246
- },
1247
- },
1248
- {
1249
- body: formdata({ imgFile, jsonFile: badSyntaxJsonFile }),
1250
- schema: '/mp/stream/file',
1251
- expected: {
1252
- status: 400,
1253
- resp: {
1254
- body: { jsonFile: "JSON Parse error: Expected '}'" },
1255
- },
1256
- },
1257
- },
1258
- {
1259
- body: imgFileBytes,
1260
- schema: '/ba/stream/file',
1261
- type: 'application/octet-stream',
1262
- expected: {
1263
- status: 200,
1264
- type: 'AsyncIterator',
1265
- resp: await fileHash(imgFileBytes),
1266
- },
1267
- },
1268
- ]
1269
- for (let { body, type, schema, expected } of cases) {
1270
- let resp = await fetch(`http://localhost:${port}/${schema}`, {
1271
- method: 'POST',
1272
- body,
1273
- headers: { ...(type ? { 'content-type': type } : {}) },
1274
- })
1275
- let respBody = (await resp.json()) as { type: string; content: any }
1276
-
1277
- expect(resp.status).toBe(expected.status)
1278
- if (resp.status === 200) {
1279
- if (expected.type) expect(respBody.type).toEqual(expected.type)
1280
- if (respBody.type === 'object' && expected.resp === null) expect(respBody.content).toBeNull
1281
- else expect(respBody.content).toEqual(expected.resp)
1282
- } else {
1283
- if (expected.resp === null) expect(respBody.content).toBeNull
1284
- else expect(respBody).toEqual(expected.resp)
1285
- }
1286
- }
1287
- })
1288
-
1289
- test('type constraints', async () => {
1290
- const cases: any = [
1291
- {
1292
- p: { int: '11', num: '10', str: 'aaaz' },
1293
- expected: { body: { default: 'DEFAULT_VALUE', int: 11, num: 10, str: 'aaaz' } },
1294
- },
1295
- {
1296
- p: { default: '', int: '42', num: '41.5', str: 'a______z' },
1297
- expected: { body: { default: '', int: 42, num: 41.5, str: 'a______z' } },
1298
- },
1299
- {
1300
- p: { int: '10', num: '42.01', str: 'xxxxxxxxxx' },
1301
- expected: {
1302
- status: 400,
1303
- body: {
1304
- query: {
1305
- int: 'Is less or equal to 10',
1306
- num: 'Is greater or equal to 42',
1307
- str: ['Length is too large (8 char max)', 'Does not match pattern /^a.*z/'],
1308
- },
1309
- },
1310
- },
1311
- },
1312
- ]
1313
-
1314
- for (let { p, expected } of cases) {
1315
- let search = new URLSearchParams()
1316
- for (const [k, v] of Object.entries(p)) search.append(k, v as string)
1317
-
1318
- let resp = await fetch(`http://localhost:${port}/query/params/schema/constraints?${search.toString()}`)
1319
- let body = await resp.json()
1320
-
1321
- expect(resp.status).toBe(expected.status ?? 200)
1322
- expect(body).toEqual(expected.body)
1323
- }
1324
- })
1325
-
1326
- test('schema to type', async () => {
1327
- let type = schemaToTypeStr(
1328
- $T.object({
1329
- boolean: $T.boolean(),
1330
- byteArray: $T.byteArray(),
1331
- number: $T.number(),
1332
- integer: $T.integer(),
1333
- string: $T.string(),
1334
- any: $T.any(),
1335
- literal: $T.literal('literal'),
1336
- array: $T.array($T.string()),
1337
- object: $T.object({
1338
- foo: $T.string(),
1339
- }),
1340
- union: $T.union([$T.number(), $T.string()]),
1341
- })
1342
- )
1343
- expect(type).toBe(
1344
- `{` +
1345
- `'boolean':boolean;` +
1346
- `'byteArray':Uint8Array;` +
1347
- `'number':number;` +
1348
- `'integer':number;` +
1349
- `'string':string;` +
1350
- `'any':any;` +
1351
- `'literal':'literal';` +
1352
- `'array':Array<string>;` +
1353
- `'object':{'foo':string};` +
1354
- `'union':number|string` +
1355
- `}`
1356
- )
1357
- })
1358
- })