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 +0 -0
- package/docs/getting-started.md +8 -0
- package/docs/hooks.md +4 -4
- package/docs/plugins.md +3 -9
- package/docs/schemas.md +19 -2
- package/package.json +1 -1
- package/src/index.ts +61 -49
- package/src/parser.ts +2 -3
- package/src/router.ts +1 -0
- package/src/routes.ts +13 -10
- package/src/schema.ts +10 -0
- package/src/server.ts +49 -28
- package/src/types.ts +47 -23
- package/src/util.ts +18 -2
- package/src/validator.ts +25 -5
- package/test/responses.test.ts +288 -13
package/bun.lockb
CHANGED
|
Binary file
|
package/docs/getting-started.md
CHANGED
|
@@ -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
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]:
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
158
|
-
|
|
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
|
|
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?:
|
|
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?:
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
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.
|
|
96
|
-
|
|
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)
|
package/test/responses.test.ts
CHANGED
|
@@ -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
|
-
'/
|
|
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.
|
|
46
|
-
return { next: () => {} }
|
|
47
|
-
})
|
|
47
|
+
galbe.post('', { response: { test: '' } }, () => {})
|
|
48
48
|
|
|
49
|
-
galbe.
|
|
50
|
-
|
|
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}/
|
|
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}/
|
|
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}/
|
|
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
|
})
|