galbe 0.2.0 → 0.3.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/bun.lockb CHANGED
Binary file
@@ -137,6 +137,14 @@ A Glob Pattern or a list of Glob patterns defining the route files to be analyze
137
137
 
138
138
  A property that can be used by plugins to add plugin's specific configuration. Every key should correspond to a [Unique Plugin Identifier](plugins.md).
139
139
 
140
+ **requestValidator.enabled**
141
+
142
+ Enable or disable the _request_ schema validation (See [Request Schema definition](schemas.md#request-schema-definition)). Default value is `true`.
143
+
144
+ **responseValidator.enabled**
145
+
146
+ Enable or disable the _response_ schema validation (See [Request Schema definition](schemas.md#request-schema-definition)). Default value is `true`.
147
+
140
148
  ### Examples
141
149
 
142
150
  A common way to handle server configuration is to create new file `galbe.config.(js|ts|json)` at the root of your project directory and import it in your code. Here is an example:
package/docs/hooks.md CHANGED
@@ -49,7 +49,7 @@ const hook2 = context => {
49
49
  console.log('hook2 called')
50
50
  }
51
51
 
52
- galbe.get('example', [hook1, hook2], ctx => {
52
+ galbe.get('/example', [hook1, hook2], ctx => {
53
53
  console.log('handler')
54
54
  })
55
55
  ```
@@ -64,18 +64,18 @@ handler
64
64
  Nested hooks declaration:
65
65
 
66
66
  ```ts
67
- const hook1 = (context, next) => {
67
+ const hook1 = async (context, next) => {
68
68
  console.log('hook1 start')
69
69
  await next()
70
70
  console.log('hook1 end')
71
71
  }
72
- const hook2 = context => {
72
+ const hook2 = async (context, next) => {
73
73
  console.log('hook2 start')
74
74
  await next()
75
75
  console.log('hook2 end')
76
76
  }
77
77
 
78
- galbe.get('example', [hook1, hook2], ctx => {
78
+ galbe.get('/example', [hook1, hook2], ctx => {
79
79
  console.log('handler')
80
80
  })
81
81
  ```
package/docs/plugins.md CHANGED
@@ -68,9 +68,7 @@ import { Galbe, type Context, type Route } from 'galbe'
68
68
  class MyPlugin {
69
69
  name = 'dev.galbe.example'
70
70
  deprecated: Record<string, string[]> = {}
71
- /**
72
- * Retrieve and store all route with a @deprecated flag metadata
73
- */
71
+ // Retrieve and store all route with a @deprecated flag metadata
74
72
  init(config: any, galbe: Galbe) {
75
73
  if (config?.enabled && galbe.meta) {
76
74
  for (const f of galbe.meta) {
@@ -85,9 +83,7 @@ class MyPlugin {
85
83
  }
86
84
  }
87
85
  }
88
- /**
89
- * Check if the current route is deprecated, flags it as is and logs it
90
- */
86
+ // Check if the current route is deprecated; if so, flag it as such and log it
91
87
  onRoute(context: Context) {
92
88
  let route = context.route
93
89
  if (this.deprecated?.[route.method]?.includes(route.path)) {
@@ -95,9 +91,7 @@ class MyPlugin {
95
91
  console.warn(`Call to deprecated route [${route.method}]${route.path}`)
96
92
  }
97
93
  }
98
- /**
99
- * Adds a header if the request has previously been flagged as deprecated
100
- */
94
+ // Add a header if the request has previously been flagged as deprecated
101
95
  afterHandle(response: Response, context: Context) {
102
96
  if (context.state?.[this.name]?.deprecated) {
103
97
  response.headers.set('x-deprecated', 'true')
package/docs/schemas.md CHANGED
@@ -152,8 +152,7 @@ const schema = {
152
152
 
153
153
  <!-- prettier-ignore -->
154
154
  ```ts
155
- body: STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral |
156
- STObject | STMulripartForm | STUrlForm
155
+ body: STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STArray | STMulripartForm | STUrlForm | STStream
157
156
  ```
158
157
 
159
158
  #### Json
@@ -238,3 +237,21 @@ galbe.post(
238
237
  }
239
238
  })
240
239
  ```
240
+
241
+ ### response
242
+
243
+ <!-- prettier-ignore -->
244
+ ```ts
245
+ response: Record<number, STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STArray | STStream>
246
+ ```
247
+
248
+ Same as for request body validation but to validate handler responses. Every schema type must be associated to a specific response status.
249
+
250
+ #### Example
251
+
252
+ ```ts
253
+ const response = {
254
+ 200: $T.object({ data: $T.array($T.number()) })
255
+ 404: $T.literal("Not found")
256
+ }
257
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "galbe",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Fast, lightweight and highly customizable JavaScript web framework based on Bun",
5
5
  "author": "Pierre Caillaud M (https://github.com/pierre-cm)",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ import type {
11
11
  ErrorHandler,
12
12
  GalbePlugin,
13
13
  STBody,
14
+ STResponse,
14
15
  STParams,
15
16
  STHeaders,
16
17
  STQuery
@@ -19,7 +20,7 @@ import type {
19
20
  import server from './server'
20
21
  import { GalbeRouter } from './router'
21
22
  import { defineRoutes } from './routes'
22
- import { logRoute } from './util'
23
+ import { extractMetaRoute, logRoute } from './util'
23
24
  import { SchemaType, type STObject, type Static } from './schema'
24
25
 
25
26
  const overloadDiscriminer = <
@@ -27,17 +28,18 @@ const overloadDiscriminer = <
27
28
  H extends STHeaders,
28
29
  P extends Partial<STParams<Path>>,
29
30
  Q extends STQuery,
30
- B extends STBody
31
+ B extends STBody,
32
+ R extends STResponse
31
33
  >(
32
34
  galbe: Galbe,
33
35
  method: Method,
34
36
  path: Path,
35
37
  arg2:
36
- | RequestSchema<Path, H, P, Q, B>
37
- | Hook<Path, RequestSchema<Path, H, P, Q, B>>[]
38
- | Handler<Path, RequestSchema<Path, H, P, Q, B>>,
39
- arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B>>,
40
- arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B>>
38
+ | RequestSchema<Path, H, P, Q, B, R>
39
+ | Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
40
+ | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
41
+ arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
42
+ arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
41
43
  ) => {
42
44
  const defaultSchema = {}
43
45
  if (typeof arg2 === 'function') {
@@ -57,14 +59,15 @@ const galbeMethod = <
57
59
  H extends STHeaders,
58
60
  P extends Partial<STParams<Path>>,
59
61
  Q extends STQuery,
60
- B extends STBody
62
+ B extends STBody,
63
+ R extends STResponse
61
64
  >(
62
65
  _galbe: Galbe,
63
66
  method: Method,
64
67
  path: Path,
65
- schema: RequestSchema<Path, H, P, Q, B> | undefined,
66
- hooks: Hook<Path, RequestSchema<Path, H, P, Q, B>>[] | undefined,
67
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
68
+ schema: RequestSchema<Path, H, P, Q, B, R> | undefined,
69
+ hooks: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | undefined,
70
+ handler: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
68
71
  ) => {
69
72
  schema = schema ?? {}
70
73
  hooks = hooks || []
@@ -92,6 +95,7 @@ const galbeMethod = <
92
95
  }
93
96
  }
94
97
 
98
+ /** Galbe Schema Type builder. See {@link https://galbe.dev/documentation/schemas#schema-types Schema Types} */
95
99
  export const $T = new SchemaType()
96
100
 
97
101
  export { RequestError } from './types'
@@ -126,12 +130,14 @@ export class Galbe {
126
130
  prefix: this.config?.basePath || '',
127
131
  cacheEnabled: this.config?.router?.cacheEnabled
128
132
  })
133
+ this.config.requestValidator = config?.requestValidator ?? { enabled: true }
134
+ this.config.responseValidator = config?.responseValidator ?? { enabled: true }
129
135
  }
130
136
  private add(route: any) {
131
137
  this.router.add(route)
132
138
  if (Bun.env.BUN_ENV === 'development') {
133
139
  if (!this.#prepare) indexRoutes.push({ method: route.method, path: route.path })
134
- else logRoute(route)
140
+ else logRoute(route, extractMetaRoute(route, this.meta))
135
141
  }
136
142
  }
137
143
  async use(plugin: GalbePlugin) {
@@ -144,7 +150,7 @@ export class Galbe {
144
150
  if (Bun.env.BUN_ENV === 'development') {
145
151
  this.#prepare = true
146
152
  console.log('🏗️ \x1b[1;30mConstructing routes\x1b[0m')
147
- for (const r of indexRoutes) logRoute(r)
153
+ for (const r of indexRoutes) logRoute(r, extractMetaRoute(r, this.meta))
148
154
  await defineRoutes(this.config || {}, this)
149
155
  console.log('\n✅ \x1b[1;30mdone\x1b[0m')
150
156
  this.server = await server(this, port)
@@ -168,90 +174,96 @@ export class Galbe {
168
174
  H extends STHeaders,
169
175
  P extends Partial<STParams<Path>>,
170
176
  Q extends STQuery,
171
- B extends STBody
177
+ B extends STBody,
178
+ R extends STResponse
172
179
  >(
173
180
  path: Path,
174
181
  arg2:
175
- | RequestSchema<Path, H, P, Q, B>
176
- | Hook<Path, RequestSchema<Path, H, P, Q, B>>[]
177
- | Handler<Path, RequestSchema<Path, H, P, Q, B>>,
178
- arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B>>,
179
- arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B>>
182
+ | RequestSchema<Path, H, P, Q, B, R>
183
+ | Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
184
+ | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
185
+ arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
186
+ arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
180
187
  ) => this.add(overloadDiscriminer(this, 'get', path, arg2, arg3, arg4))
181
188
  post: Endpoint = <
182
189
  Path extends string,
183
190
  H extends STHeaders,
184
191
  P extends Partial<STParams<Path>>,
185
192
  Q extends STQuery,
186
- B extends STBody
193
+ B extends STBody,
194
+ R extends STResponse
187
195
  >(
188
196
  path: Path,
189
197
  arg2:
190
- | RequestSchema<Path, H, P, Q, B>
191
- | Hook<Path, RequestSchema<Path, H, P, Q, B>>[]
192
- | Handler<Path, RequestSchema<Path, H, P, Q, B>>,
193
- arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B>>,
194
- arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B>>
198
+ | RequestSchema<Path, H, P, Q, B, R>
199
+ | Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
200
+ | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
201
+ arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
202
+ arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
195
203
  ) => this.add(overloadDiscriminer(this, 'post', path, arg2, arg3, arg4))
196
204
  put: Endpoint = <
197
205
  Path extends string,
198
206
  H extends STHeaders,
199
207
  P extends Partial<STParams<Path>>,
200
208
  Q extends STQuery,
201
- B extends STBody
209
+ B extends STBody,
210
+ R extends STResponse
202
211
  >(
203
212
  path: Path,
204
213
  arg2:
205
- | RequestSchema<Path, H, P, Q, B>
206
- | Hook<Path, RequestSchema<Path, H, P, Q, B>>[]
207
- | Handler<Path, RequestSchema<Path, H, P, Q, B>>,
208
- arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B>>,
209
- arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B>>
214
+ | RequestSchema<Path, H, P, Q, B, R>
215
+ | Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
216
+ | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
217
+ arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
218
+ arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
210
219
  ) => this.add(overloadDiscriminer(this, 'put', path, arg2, arg3, arg4))
211
220
  patch: Endpoint = <
212
221
  Path extends string,
213
222
  H extends STHeaders,
214
223
  P extends Partial<STParams<Path>>,
215
224
  Q extends STQuery,
216
- B extends STBody
225
+ B extends STBody,
226
+ R extends STResponse
217
227
  >(
218
228
  path: Path,
219
229
  arg2:
220
- | RequestSchema<Path, H, P, Q, B>
221
- | Hook<Path, RequestSchema<Path, H, P, Q, B>>[]
222
- | Handler<Path, RequestSchema<Path, H, P, Q, B>>,
223
- arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B>>,
224
- arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B>>
230
+ | RequestSchema<Path, H, P, Q, B, R>
231
+ | Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
232
+ | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
233
+ arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
234
+ arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
225
235
  ) => this.add(overloadDiscriminer(this, 'patch', path, arg2, arg3, arg4))
226
236
  delete: Endpoint = <
227
237
  Path extends string,
228
238
  H extends STHeaders,
229
239
  P extends Partial<STParams<Path>>,
230
240
  Q extends STQuery,
231
- B extends STBody
241
+ B extends STBody,
242
+ R extends STResponse
232
243
  >(
233
244
  path: Path,
234
245
  arg2:
235
- | RequestSchema<Path, H, P, Q, B>
236
- | Hook<Path, RequestSchema<Path, H, P, Q, B>>[]
237
- | Handler<Path, RequestSchema<Path, H, P, Q, B>>,
238
- arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B>>,
239
- arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B>>
246
+ | RequestSchema<Path, H, P, Q, B, R>
247
+ | Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
248
+ | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
249
+ arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
250
+ arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
240
251
  ) => this.add(overloadDiscriminer(this, 'delete', path, arg2, arg3, arg4))
241
252
  options: Endpoint = <
242
253
  Path extends string,
243
254
  H extends STHeaders,
244
255
  P extends Partial<STParams<Path>>,
245
256
  Q extends STQuery,
246
- B extends STBody
257
+ B extends STBody,
258
+ R extends STResponse
247
259
  >(
248
260
  path: Path,
249
261
  arg2:
250
- | RequestSchema<Path, H, P, Q, B>
251
- | Hook<Path, RequestSchema<Path, H, P, Q, B>>[]
252
- | Handler<Path, RequestSchema<Path, H, P, Q, B>>,
253
- arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B>>,
254
- arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B>>
262
+ | RequestSchema<Path, H, P, Q, B, R>
263
+ | Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
264
+ | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
265
+ arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
266
+ arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
255
267
  ) => this.add(overloadDiscriminer(this, 'options', path, arg2, arg3, arg4))
256
268
  }
257
269
 
package/src/parser.ts CHANGED
@@ -17,6 +17,7 @@ import { readableStreamToArrayBuffer } from 'bun'
17
17
  import { Kind, Optional, Stream } from './schema'
18
18
  import { validate } from './validator'
19
19
  import { InternalError, RequestError } from './index'
20
+ import { isIterator } from './util'
20
21
 
21
22
  const textDecoder = new TextDecoder()
22
23
  const textEncoder = new TextEncoder()
@@ -594,7 +595,7 @@ export const requestPathParser = (input: string, path: string) => {
594
595
  }
595
596
 
596
597
  export const parseEntry = <T extends STProps>(
597
- params: { [key: string]: string | string[] },
598
+ params: { [key: string]: any },
598
599
  schema: T,
599
600
  options?: { name?: string; i?: boolean }
600
601
  ): Static<STObject<T>> => {
@@ -629,8 +630,6 @@ export const parseEntry = <T extends STProps>(
629
630
  return parsedParams as Static<STObject<T>>
630
631
  }
631
632
 
632
- const isIterator = (obj: any) => typeof obj?.next === 'function'
633
-
634
633
  export const responseParser = (response: any, ctx: Context) => {
635
634
  const details = {
636
635
  status: ctx.set.status || 200,
package/src/router.ts CHANGED
@@ -89,6 +89,7 @@ export class GalbeRouter {
89
89
  }
90
90
  }
91
91
  find(method: string, path: string) {
92
+ method = method.toUpperCase()
92
93
  const staticRoute = this.cachedRoutes.get(`[${method}]${path}`)
93
94
  if (staticRoute === null) throw new NotFoundError()
94
95
  if (staticRoute !== undefined) return staticRoute
package/src/routes.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { GalbeConfig } from './types'
2
2
 
3
3
  import { readdir, lstat } from 'fs/promises'
4
- import { extname } from 'path'
4
+ import { extname, relative } from 'path'
5
5
  import { parse } from 'acorn'
6
6
  import { simple } from 'acorn-walk'
7
7
  import { Galbe } from './index'
@@ -81,13 +81,15 @@ export const metaAnalysis = async (filePath: string): Promise<RouteMeta> => {
81
81
  })
82
82
  simple(ast, {
83
83
  ExportDefaultDeclaration(node) {
84
- // @ts-ignore
85
- let galbeIdentifier = node.declaration.params[0].name
86
-
87
84
  const headerLine = node.loc?.start.line
88
85
  const headerCom = headerLine !== undefined && comments?.[headerLine] ? comments[headerLine] : ''
89
86
  const headerRef = parseComment(headerCom)
90
87
  meta.header = headerRef
88
+
89
+ // @ts-ignore
90
+ let galbeIdentifier = node.declaration?.params?.[0]?.name
91
+ if (!galbeIdentifier) return meta
92
+
91
93
  // @ts-ignore
92
94
  simple(node.declaration.body, {
93
95
  CallExpression(node) {
@@ -113,7 +115,10 @@ export const metaAnalysis = async (filePath: string): Promise<RouteMeta> => {
113
115
  }
114
116
 
115
117
  const importRoutes = async (filePath: string, galbe: Galbe) => {
116
- const routes = (await import(filePath)).default
118
+ const imported = await import(filePath)
119
+ if (!imported?.default) throw new Error('No default export function')
120
+ if (typeof imported.default !== 'function') throw new Error('Default export must be a function')
121
+ const routes = imported.default
117
122
  routes(galbe)
118
123
  }
119
124
 
@@ -138,16 +143,14 @@ export const defineRoutes = async (options: GalbeConfig, galbe: Galbe) => {
138
143
  try {
139
144
  const metadata = await metaAnalysis(f)
140
145
  galbe.meta?.push({ file: path, ...metadata })
141
- console.log(`\n\x1b\[0;36m ${f}\x1b[0m`)
146
+ console.log(`\n\x1b\[0;36m ${relative('.', f)}\x1b[0m`)
142
147
  await importRoutes(f, galbe)
143
- } catch (err) {
144
- // console.log(`\x1b\[0;31m ${f}\x1b[0m`)
145
- throw err
148
+ } catch (err: any) {
149
+ console.log(`\x1b\[0;31m Error: ${err?.message}\x1b[0m`)
146
150
  }
147
151
  }
148
152
  }
149
153
  if (noRouteFound) {
150
- process.stdout.write('\r\x1b[K')
151
154
  console.log(`\x1b\[38;5;245m No route found\x1b[0m`)
152
155
  return
153
156
  }
package/src/schema.ts CHANGED
@@ -63,6 +63,16 @@ export type STPropsValue =
63
63
  export type STProps = Record<string | number, STPropsValue>
64
64
 
65
65
  type Evaluate<T> = T extends infer O ? { [K in keyof O]: O[K] } : never
66
+
67
+ /**
68
+ * Infer the static TypeScript type from a {@link https://galbe.dev/documentation/schemas#schema-types Schema Type}
69
+ * @example
70
+ * ```ts
71
+ * const schema = $T.object({ foo: $T.string() })
72
+ * type T = Static<typeof schema>
73
+ * // ^? type T = { foo: string }
74
+ * ```
75
+ */
66
76
  export type Static<T extends STSchema, P extends unknown[] = unknown[]> = (T & { params: P })['static']
67
77
 
68
78
  // Utils
package/src/server.ts CHANGED
@@ -3,6 +3,9 @@ import type { Context, Route } from './types'
3
3
  import { InternalError, RequestError } from './types'
4
4
  import { parseEntry, requestBodyParser, requestPathParser, responseParser } from './parser'
5
5
  import { Galbe } from './index'
6
+ import { validateResponse } from './validator'
7
+
8
+ const LOADABLE_METHODS = ['POST', 'PUT', 'PATCH']
6
9
 
7
10
  const handleInternalError = (error: any) => {
8
11
  console.error(error)
@@ -69,37 +72,41 @@ export default async (galbe: Galbe, port?: number) => {
69
72
  for (let [k, v] of url.searchParams) inQuery[k] = v
70
73
  let inParams = requestPathParser(url.pathname, route.path)
71
74
 
72
- context.body = await requestBodyParser(req.body, inHeaders, schema.body)
75
+ context.body = LOADABLE_METHODS.includes(req.method)
76
+ ? await requestBodyParser(req.body, inHeaders, schema.body)
77
+ : null
73
78
  context.headers = inHeaders
74
79
  context.query = inQuery
75
80
  context.params = inParams
76
81
 
77
82
  // request validation
78
- let errors: RequestError[] = []
79
- try {
80
- if (schema?.headers)
81
- context.headers = {
82
- ...context.headers,
83
- ...parseEntry(context.headers, schema.headers, { name: 'headers', i: true })
84
- }
85
- } catch (error) {
86
- if (error instanceof RequestError) errors.push(error)
87
- else throw handleInternalError(error)
88
- }
89
- try {
90
- if (schema?.query) context.query = parseEntry(context.query, schema.query, { name: 'query' })
91
- } catch (error) {
92
- if (error instanceof RequestError) errors.push(error)
93
- else throw handleInternalError(error)
94
- }
95
- try {
96
- if (schema?.params) context.params = parseEntry(context.params, schema.params, { name: 'params' })
97
- } catch (error) {
98
- if (error instanceof RequestError) errors.push(error)
99
- else throw handleInternalError(error)
100
- }
101
- if (errors.length) {
102
- throw new RequestError({ status: 400, payload: errors.reduce((acc, c) => ({ ...acc, ...c.payload }), {}) })
83
+ if (galbe.config?.requestValidator?.enabled) {
84
+ let errors: RequestError[] = []
85
+ try {
86
+ if (schema?.headers)
87
+ context.headers = {
88
+ ...context.headers,
89
+ ...parseEntry(context.headers, schema.headers, { name: 'headers', i: true })
90
+ }
91
+ } catch (error) {
92
+ if (error instanceof RequestError) errors.push(error)
93
+ else throw handleInternalError(error)
94
+ }
95
+ try {
96
+ if (schema?.query) context.query = parseEntry(context.query, schema.query, { name: 'query' })
97
+ } catch (error) {
98
+ if (error instanceof RequestError) errors.push(error)
99
+ else throw handleInternalError(error)
100
+ }
101
+ try {
102
+ if (schema?.params) context.params = parseEntry(context.params, schema.params, { name: 'params' })
103
+ } catch (error) {
104
+ if (error instanceof RequestError) errors.push(error)
105
+ else throw handleInternalError(error)
106
+ }
107
+ if (errors.length) {
108
+ throw new RequestError({ status: 400, payload: errors.reduce((acc, c) => ({ ...acc, ...c.payload }), {}) })
109
+ }
103
110
  }
104
111
 
105
112
  for (const p of pluginsCb.beforeHandle) {
@@ -142,6 +149,9 @@ export default async (galbe: Galbe, port?: number) => {
142
149
 
143
150
  const parsedResponse = responseParser(response, context)
144
151
 
152
+ if (galbe.config?.responseValidator?.enabled && schema.response)
153
+ validateResponse(response, schema.response, context.set.status || 200)
154
+
145
155
  for (const p of pluginsCb.afterHandle) {
146
156
  //@ts-ignore
147
157
  const r = await p.afterHandle(parsedResponse, context)
@@ -154,8 +164,19 @@ export default async (galbe: Galbe, port?: number) => {
154
164
  let customError
155
165
  if (galbe.errorHandler) customError = responseParser(galbe.errorHandler(error, context), context)
156
166
  if (customError) return customError
157
- if (error instanceof RequestError) {
158
- return new Response(JSON.stringify(error.payload), {
167
+ if (error instanceof InternalError) {
168
+ console.log(`Internal Error`, error?.payload || '')
169
+ return new Response('Internal Server Error', {
170
+ status: error.status,
171
+ headers: { 'Content-Type': 'application/json' }
172
+ })
173
+ } else if (error instanceof RequestError) {
174
+ let payload = ''
175
+ if (typeof error.payload === 'string') payload = error.payload
176
+ try {
177
+ payload = JSON.stringify(error.payload)
178
+ } catch (err) {}
179
+ return new Response(payload, {
159
180
  status: error.status,
160
181
  headers: { 'Content-Type': 'application/json' }
161
182
  })
package/src/types.ts CHANGED
@@ -22,12 +22,27 @@ export type STBody =
22
22
  | STBoolean
23
23
  | STNumber
24
24
  | STInteger
25
+ | STLiteral
25
26
  | STObject
26
27
  | STArray
27
28
  | STUrlForm
28
29
  | STMultipartForm
29
30
  | STUnion
30
31
  | STStream
32
+
33
+ export type STResponseValue =
34
+ | STByteArray
35
+ | STString
36
+ | STBoolean
37
+ | STNumber
38
+ | STInteger
39
+ | STLiteral
40
+ | STObject
41
+ | STArray
42
+ | STUnion
43
+ | STStream
44
+ export type STResponse = Record<number, STResponseValue>
45
+
31
46
  export type MaybeArray<T> = T | T[]
32
47
 
33
48
  export type Method = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options'
@@ -74,6 +89,8 @@ export type GalbeConfig = {
74
89
  routes?: boolean | string | string[]
75
90
  router?: { cacheEnabled: boolean }
76
91
  plugin?: Record<string, any>
92
+ requestValidator?: { enabled: boolean }
93
+ responseValidator?: { enabled: boolean }
77
94
  }
78
95
  /**
79
96
  * #### Schema
@@ -96,15 +113,17 @@ export type GalbeConfig = {
96
113
  */
97
114
  export type RequestSchema<
98
115
  Path extends string = string,
99
- H extends STHeaders = {},
100
- P extends Partial<STParams<Path>> = {},
101
- Q extends STQuery = {},
102
- B extends STBody = STBody
116
+ H extends STHeaders = STHeaders,
117
+ P extends Partial<STParams<Path>> = Partial<STParams<Path>>,
118
+ Q extends STQuery = STQuery,
119
+ B extends STBody = STBody,
120
+ R extends STResponse = STResponse
103
121
  > = {
104
122
  headers?: H
105
123
  params?: P
106
124
  query?: Q
107
125
  body?: B
126
+ response?: R
108
127
  }
109
128
 
110
129
  type OmitNotDefined<S extends RequestSchema> = {
@@ -147,44 +166,48 @@ export type Endpoint = {
147
166
  H extends STHeaders,
148
167
  P extends Partial<STParams<Path>>,
149
168
  Q extends STQuery,
150
- B extends STBody = any
169
+ B extends STBody = any,
170
+ R extends STResponse = STResponse
151
171
  >(
152
172
  path: Path,
153
- schema: RequestSchema<Path, H, P, Q, B>,
154
- hooks: Hook<Path, RequestSchema<Path, H, P, Q, B>>[],
155
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
173
+ schema: RequestSchema<Path, H, P, Q, B, R>,
174
+ hooks: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[],
175
+ handler: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
156
176
  ): void
157
177
  <
158
178
  Path extends string,
159
179
  H extends STHeaders,
160
180
  P extends Partial<STParams<Path>>,
161
181
  Q extends STQuery,
162
- B extends STBody = any
182
+ B extends STBody = any,
183
+ R extends STResponse = STResponse
163
184
  >(
164
185
  path: Path,
165
- schema: RequestSchema<Path, H, P, Q, B>,
166
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
186
+ schema: RequestSchema<Path, H, P, Q, B, R>,
187
+ handler: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
167
188
  ): void
168
189
  <
169
190
  Path extends string,
170
191
  H extends STHeaders,
171
192
  P extends Partial<STParams<Path>>,
172
193
  Q extends STQuery,
173
- B extends STBody = any
194
+ B extends STBody = any,
195
+ R extends STResponse = STResponse
174
196
  >(
175
197
  path: Path,
176
- hooks: Hook<Path, RequestSchema<Path, H, P, Q, B>>[],
177
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
198
+ hooks: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[],
199
+ handler: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
178
200
  ): void
179
201
  <
180
202
  Path extends string,
181
203
  H extends STHeaders,
182
204
  P extends Partial<STParams<Path>>,
183
205
  Q extends STQuery,
184
- B extends STBody = any
206
+ B extends STBody = any,
207
+ R extends STResponse = STResponse
185
208
  >(
186
209
  path: Path,
187
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
210
+ handler: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
188
211
  ): void
189
212
  }
190
213
 
@@ -193,7 +216,7 @@ export class RequestError {
193
216
  payload: any
194
217
  constructor(options: { status?: number; payload?: any }) {
195
218
  this.status = options.status ?? 500
196
- this.payload = options.payload ?? 'Internal server error'
219
+ this.payload = options.payload
197
220
  }
198
221
  }
199
222
 
@@ -210,14 +233,15 @@ export type Route<
210
233
  H extends STHeaders = {},
211
234
  P extends Partial<STParams<Path>> = {},
212
235
  Q extends STQuery = {},
213
- B extends STBody = STBody
236
+ B extends STBody = STBody,
237
+ R extends STResponse = STResponse
214
238
  > = {
215
239
  method: Method
216
240
  path: Path
217
- schema: RequestSchema<Path, H, P, Q, B>
218
- context: Context<Path, RequestSchema<Path, H, P, Q, B>>
241
+ schema: RequestSchema<Path, H, P, Q, B, R>
242
+ context: Context<Path, RequestSchema<Path, H, P, Q, B, R>>
219
243
  hooks: Hook[]
220
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
244
+ handler: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
221
245
  }
222
246
 
223
247
  export type RouteTree = {
@@ -225,13 +249,13 @@ export type RouteTree = {
225
249
  }
226
250
 
227
251
  export class NotFoundError extends RequestError {
228
- constructor(message?: string) {
252
+ constructor(message?: any) {
229
253
  super({ status: 404, payload: message ?? 'Not found' })
230
254
  }
231
255
  }
232
256
 
233
257
  export class InternalError extends RequestError {
234
- constructor(message?: string) {
258
+ constructor(message?: any) {
235
259
  super({ status: 500, payload: message ?? 'Internal Server Error' })
236
260
  }
237
261
  }
package/src/util.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { RouteFileMeta } from './routes'
2
+
1
3
  const METHOD_COLOR: Record<string, string> = {
2
4
  get: '\x1b[32m',
3
5
  post: '\x1b[34m',
@@ -6,7 +8,21 @@ const METHOD_COLOR: Record<string, string> = {
6
8
  delete: '\x1b[31m',
7
9
  options: ''
8
10
  }
9
- export const logRoute = (r: { method: string; path: string }) => {
11
+
12
+ export const extractMetaRoute = (route: { path: string; method: string }, meta?: Array<RouteFileMeta>) => {
13
+ return meta?.map(e => (route.path in e.routes ? e.routes[route.path]?.[route.method] : null)).filter(e => e)?.[0]
14
+ }
15
+
16
+ export const logRoute = (
17
+ r: { method: string; path: string },
18
+ meta?: Record<string, boolean | string | string[]> | null
19
+ ) => {
10
20
  let color = METHOD_COLOR?.[r.method] || ''
11
- console.log(` [${color}${`${r.method.toUpperCase()}\x1b[0m]`.padEnd(12, ' ')} ${r.path}`)
21
+ console.log(
22
+ ` [${color}${`${r.method.toUpperCase()}\x1b[0m]`.padEnd(12, ' ')} ${r.path}${
23
+ meta?.head ? ` - ${meta.head}` : ''
24
+ }`
25
+ )
12
26
  }
27
+
28
+ export const isIterator = (obj: any) => typeof obj?.next === 'function'
package/src/validator.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { InternalError, type STResponse } from './index'
1
2
  import type { STSchema, STProps, STUnion } from './schema'
2
- import { Kind, Optional } from './schema'
3
+ import { Kind, Optional, Stream } from './schema'
4
+ import { isIterator } from './util'
3
5
 
4
6
  export const validate = (elt: any, schema: STSchema, parse = false): any => {
5
7
  type ValidationError = string | string[] | { [key: string]: ValidationError }
@@ -7,7 +9,10 @@ export const validate = (elt: any, schema: STSchema, parse = false): any => {
7
9
  const iElt = elt
8
10
 
9
11
  if (schema[Kind] === 'boolean') {
10
- if (parse && typeof elt === 'string') elt = elt === 'true' ? true : elt === 'false' ? false : null
12
+ if (typeof elt === 'string') {
13
+ if (parse) elt = elt === 'true' ? true : elt === 'false' ? false : null
14
+ else throw `Expected boolean, got string.`
15
+ }
11
16
  if (elt !== true && elt !== false) throw `${iElt} is not a valid boolean. Should be 'true' or 'false'`
12
17
  } else if (schema[Kind] === 'integer') {
13
18
  if (parse && typeof elt === 'string') elt = Number(elt)
@@ -84,6 +89,22 @@ export const validate = (elt: any, schema: STSchema, parse = false): any => {
84
89
  return elt
85
90
  }
86
91
 
92
+ export const validateResponse = (response: any, schema: STResponse, status: number) => {
93
+ if (!(status in schema)) return
94
+ const s = schema[status]
95
+ if (response instanceof ReadableStream) {
96
+ if (!s[Stream]) throw new InternalError(`Expected ${s[Kind]} response, but got ReadableStream`)
97
+ } else if (isIterator(response)) {
98
+ if (!s[Stream]) throw new InternalError(`Expected ${s[Kind]} response, but got Iterator`)
99
+ } else {
100
+ try {
101
+ validate(response, s)
102
+ } catch (error) {
103
+ throw new InternalError({ ResponseValidationError: error })
104
+ }
105
+ }
106
+ }
107
+
87
108
  const schemaValidation = (value: any, schema: STSchema) => {
88
109
  const errors = []
89
110
  if (schema[Kind] === 'integer' || schema[Kind] === 'number') {
@@ -92,9 +113,8 @@ const schemaValidation = (value: any, schema: STSchema) => {
92
113
  if (schema.exclusiveMax !== undefined)
93
114
  if ((value as number) >= schema.exclusiveMax)
94
115
  errors.push(`${value} is greater or equal to ${schema.exclusiveMax}`)
95
- if (schema.minimum !== undefined)
96
- if ((value as number) < schema.min) errors.push(`${value} is less than ${schema.min}`)
97
- if (schema.maximum !== undefined)
116
+ if (schema.min !== undefined) if ((value as number) < schema.min) errors.push(`${value} is less than ${schema.min}`)
117
+ if (schema.max !== undefined)
98
118
  if ((value as number) > schema.max) errors.push(`${value} is greater than ${schema.max}`)
99
119
  } else if (schema[Kind] === 'string') {
100
120
  if (schema.minLength !== undefined && (value as string).length < schema.minLength)
@@ -21,12 +21,14 @@ const rsTxt = (text: string) => {
21
21
  })
22
22
  }
23
23
 
24
+ const handleResp = ctx => ctx.body
25
+
24
26
  describe('responses', () => {
25
27
  beforeAll(async () => {
26
28
  const galbe = new Galbe()
27
29
 
28
30
  galbe.post(
29
- '/response',
31
+ '/none',
30
32
  {
31
33
  body: $T.optional($T.object($T.any())),
32
34
  query: { text: $T.optional($T.string()), stream: $T.optional($T.string()) }
@@ -42,20 +44,25 @@ describe('responses', () => {
42
44
  }
43
45
  )
44
46
 
45
- galbe.get('/error', _ => {
46
- return { next: () => {} }
47
- })
47
+ galbe.post('', { response: { test: '' } }, () => {})
48
48
 
49
- galbe.onError(_ => {
50
- throw new Error()
51
- })
49
+ galbe.post('/ba', { response: { 200: $T.byteArray() } }, handleResp)
50
+ galbe.post('/bool', { response: { 200: $T.boolean() } }, handleResp)
51
+ galbe.post('/num', { response: { 200: $T.number() } }, handleResp)
52
+ galbe.post('/str', { response: { 200: $T.string() } }, handleResp)
53
+ galbe.post('/arr', { response: { 200: $T.array() } }, handleResp)
54
+ galbe.post('/obj', { response: { 200: $T.object($T.any()) } }, handleResp)
55
+ galbe.post('/stream/ba', { response: { 200: $T.stream($T.byteArray()) } }, ctx =>
56
+ ctx.body ? genTxt(ctx.body) : ''
57
+ )
58
+ galbe.post('/stream/str', { response: { 200: $T.stream($T.string()) } }, ctx => (ctx.body ? genTxt(ctx.body) : 42))
52
59
 
53
60
  await galbe.listen(port)
54
61
  })
55
62
 
56
- test('response, string', async () => {
63
+ test('response, no schema, string', async () => {
57
64
  const reqTxt = 'Hello Mom!'
58
- let resp = await fetch(`http://localhost:${port}/response?text=${reqTxt}`, {
65
+ let resp = await fetch(`http://localhost:${port}/none?text=${reqTxt}`, {
59
66
  method: 'POST'
60
67
  })
61
68
 
@@ -66,9 +73,9 @@ describe('responses', () => {
66
73
  expect(body).toBe(reqTxt)
67
74
  })
68
75
 
69
- test('response, object', async () => {
76
+ test('response, no schema, object', async () => {
70
77
  const reqBody = { foo: 'bar' }
71
- let resp = await fetch(`http://localhost:${port}/response`, {
78
+ let resp = await fetch(`http://localhost:${port}/none`, {
72
79
  method: 'POST',
73
80
  body: JSON.stringify(reqBody),
74
81
  headers: {
@@ -83,12 +90,12 @@ describe('responses', () => {
83
90
  expect(body).toEqual(reqBody)
84
91
  })
85
92
 
86
- test('response, stream', async () => {
93
+ test('response, no schema, stream', async () => {
87
94
  const reqTxt = 'Hello Mom!'
88
95
  const cases = [{ stream: 'generatorFunction' }, { stream: 'readableStream' }]
89
96
 
90
97
  for (const c of cases) {
91
- let resp = await fetch(`http://localhost:${port}/response?text=${reqTxt}&stream=${c.stream}`, {
98
+ let resp = await fetch(`http://localhost:${port}/none?text=${reqTxt}&stream=${c.stream}`, {
92
99
  method: 'POST',
93
100
  headers: {
94
101
  'content-type': 'application/json'
@@ -106,4 +113,272 @@ describe('responses', () => {
106
113
  expect(body).toMatch(new RegExp(`id:${UUID_RGX}\ndata:Hello\n\nid:${UUID_RGX}\ndata:Mom!\n\n`))
107
114
  }
108
115
  })
116
+
117
+ test('response, ba, validation OK', async () => {
118
+ let bodyStr = 'Hello mom!'
119
+ let reqBody = new TextEncoder().encode(bodyStr)
120
+
121
+ let resp = await fetch(`http://localhost:${port}/ba`, {
122
+ method: 'POST',
123
+ body: bodyStr,
124
+ headers: {
125
+ 'content-type': 'application/octet-stream'
126
+ }
127
+ })
128
+
129
+ const body = await resp.json()
130
+
131
+ expect(resp.status).toBe(200)
132
+ expect(resp.headers.get('content-type')).toBe('application/json')
133
+ expect(body).toEqual(reqBody)
134
+ })
135
+
136
+ test('response, ba, validation failed', async () => {
137
+ let bodyStr = 'Hello mom!'
138
+
139
+ let resp = await fetch(`http://localhost:${port}/ba`, {
140
+ method: 'POST',
141
+ body: bodyStr,
142
+ headers: {
143
+ 'content-type': 'text/plain'
144
+ }
145
+ })
146
+
147
+ expect(resp.status).toBe(500)
148
+ })
149
+
150
+ test('response, bool, validation OK', async () => {
151
+ let bodyStr = 'true'
152
+ let reqBody = true
153
+
154
+ let resp = await fetch(`http://localhost:${port}/bool`, {
155
+ method: 'POST',
156
+ body: bodyStr,
157
+ headers: {
158
+ 'content-type': 'application/json'
159
+ }
160
+ })
161
+
162
+ const body = await resp.json()
163
+
164
+ expect(resp.status).toBe(200)
165
+ expect(resp.headers.get('content-type')).toBe('application/json')
166
+ expect(body).toEqual(reqBody)
167
+ })
168
+
169
+ test('response, bool, validation failed', async () => {
170
+ let bodyStr = 'true'
171
+
172
+ let resp = await fetch(`http://localhost:${port}/bool`, {
173
+ method: 'POST',
174
+ body: bodyStr,
175
+ headers: {
176
+ 'content-type': 'text/plain'
177
+ }
178
+ })
179
+
180
+ expect(resp.status).toBe(500)
181
+ })
182
+
183
+ test('response, num, validation OK', async () => {
184
+ let bodyStr = '42'
185
+ let reqBody = 42
186
+
187
+ let resp = await fetch(`http://localhost:${port}/num`, {
188
+ method: 'POST',
189
+ body: bodyStr,
190
+ headers: {
191
+ 'content-type': 'application/json'
192
+ }
193
+ })
194
+
195
+ const body = await resp.json()
196
+
197
+ expect(resp.status).toBe(200)
198
+ expect(resp.headers.get('content-type')).toBe('application/json')
199
+ expect(body).toEqual(reqBody)
200
+ })
201
+
202
+ test('response, num, validation failed', async () => {
203
+ let bodyStr = '"test"'
204
+
205
+ let resp = await fetch(`http://localhost:${port}/num`, {
206
+ method: 'POST',
207
+ body: bodyStr,
208
+ headers: {
209
+ 'content-type': 'application/json'
210
+ }
211
+ })
212
+
213
+ expect(resp.status).toBe(500)
214
+ })
215
+
216
+ test('response, str, validation OK', async () => {
217
+ let bodyStr = 'Hello Mom!'
218
+
219
+ let resp = await fetch(`http://localhost:${port}/str`, {
220
+ method: 'POST',
221
+ body: bodyStr,
222
+ headers: {
223
+ 'content-type': 'text/plain'
224
+ }
225
+ })
226
+
227
+ const body = await resp.text()
228
+
229
+ expect(resp.status).toBe(200)
230
+ expect(resp.headers.get('content-type')).toBe('text/plain')
231
+ expect(body).toEqual(bodyStr)
232
+ })
233
+
234
+ test('response, str, validation failed', async () => {
235
+ let bodyStr = '3.14'
236
+
237
+ let resp = await fetch(`http://localhost:${port}/str`, {
238
+ method: 'POST',
239
+ body: bodyStr,
240
+ headers: {
241
+ 'content-type': 'application/json'
242
+ }
243
+ })
244
+
245
+ expect(resp.status).toBe(500)
246
+ })
247
+
248
+ test('response, array, validation OK', async () => {
249
+ let bodyStr = '[false, "one", 2]'
250
+ let reqBody = [false, 'one', 2]
251
+
252
+ let resp = await fetch(`http://localhost:${port}/arr`, {
253
+ method: 'POST',
254
+ body: bodyStr,
255
+ headers: {
256
+ 'content-type': 'application/json'
257
+ }
258
+ })
259
+
260
+ const body = await resp.json()
261
+
262
+ expect(resp.status).toBe(200)
263
+ expect(resp.headers.get('content-type')).toBe('application/json')
264
+ expect(body).toEqual(reqBody)
265
+ })
266
+
267
+ test('response, array, validation failed', async () => {
268
+ let bodyStr = '0'
269
+
270
+ let resp = await fetch(`http://localhost:${port}/arr`, {
271
+ method: 'POST',
272
+ body: bodyStr,
273
+ headers: {
274
+ 'content-type': 'application/json'
275
+ }
276
+ })
277
+
278
+ expect(resp.status).toBe(500)
279
+ })
280
+
281
+ test('response, object, validation OK', async () => {
282
+ let bodyStr = '{"str":"str", "num":0, "arr":[1,2,3], "nested": {"foo":"bar"}}'
283
+ let reqBody = { str: 'str', num: 0, arr: [1, 2, 3], nested: { foo: 'bar' } }
284
+
285
+ let resp = await fetch(`http://localhost:${port}/obj`, {
286
+ method: 'POST',
287
+ body: bodyStr,
288
+ headers: {
289
+ 'content-type': 'application/json'
290
+ }
291
+ })
292
+
293
+ const body = await resp.json()
294
+
295
+ expect(resp.status).toBe(200)
296
+ expect(resp.headers.get('content-type')).toBe('application/json')
297
+ expect(body).toEqual(reqBody)
298
+ })
299
+
300
+ test('response, object, validation failed', async () => {
301
+ let bodyStr = '"This"'
302
+
303
+ let resp = await fetch(`http://localhost:${port}/obj`, {
304
+ method: 'POST',
305
+ body: bodyStr,
306
+ headers: {
307
+ 'content-type': 'application/json'
308
+ }
309
+ })
310
+
311
+ expect(resp.status).toBe(500)
312
+ })
313
+
314
+ test('response, stream ba, validation OK', async () => {
315
+ let bodyStr = 'Hello Mom!'
316
+
317
+ let resp = await fetch(`http://localhost:${port}/stream/ba`, {
318
+ method: 'POST',
319
+ body: bodyStr,
320
+ headers: {
321
+ 'content-type': 'text/plain'
322
+ }
323
+ })
324
+
325
+ const reader = resp.body?.getReader()
326
+ let body = ''
327
+ while (reader) {
328
+ const { value, done } = await reader.read()
329
+ if (done) break
330
+ body += decoder.decode(value)
331
+ }
332
+ expect(resp.status).toBe(200)
333
+ expect(resp.headers.get('content-type')).toBe('text/event-stream')
334
+ expect(body).toMatch(new RegExp(`id:${UUID_RGX}\ndata:Hello\n\nid:${UUID_RGX}\ndata:Mom!\n\n`))
335
+ })
336
+
337
+ test('response, stream ba, validation failed', async () => {
338
+ let bodyStr = ''
339
+
340
+ let resp = await fetch(`http://localhost:${port}/stream/ba`, {
341
+ method: 'POST',
342
+ body: bodyStr
343
+ })
344
+
345
+ expect(resp.status).toBe(500)
346
+ })
347
+
348
+ test('response, stream str, validation OK', async () => {
349
+ let bodyStr = 'Hello Mom!'
350
+
351
+ let resp = await fetch(`http://localhost:${port}/stream/str`, {
352
+ method: 'POST',
353
+ body: bodyStr,
354
+ headers: {
355
+ 'content-type': 'text/plain'
356
+ }
357
+ })
358
+
359
+ const reader = resp.body?.getReader()
360
+ let body = ''
361
+ while (reader) {
362
+ const { value, done } = await reader.read()
363
+ if (done) break
364
+ body += decoder.decode(value)
365
+ }
366
+ expect(resp.status).toBe(200)
367
+ expect(resp.headers.get('content-type')).toBe('text/event-stream')
368
+ expect(body).toMatch(new RegExp(`id:${UUID_RGX}\ndata:Hello\n\nid:${UUID_RGX}\ndata:Mom!\n\n`))
369
+ })
370
+
371
+ test('response, stream str, validation failed', async () => {
372
+ let bodyStr = '0'
373
+
374
+ let resp = await fetch(`http://localhost:${port}/stream/str`, {
375
+ method: 'POST',
376
+ body: bodyStr,
377
+ headers: {
378
+ 'content-type': 'application/json'
379
+ }
380
+ })
381
+
382
+ expect(resp.status).toBe(500)
383
+ })
109
384
  })