galbe 0.10.0 → 0.11.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/README.md +3 -0
- package/bin/commands/build.ts +13 -1
- package/docs/cli.md +25 -22
- package/docs/configuration.md +80 -0
- package/docs/context.md +38 -52
- package/docs/error-handler.md +20 -31
- package/docs/getting-started.md +40 -157
- package/docs/handler.md +35 -21
- package/docs/hooks.md +16 -16
- package/docs/plugins.md +50 -57
- package/docs/router.md +2 -2
- package/docs/routes.md +56 -39
- package/docs/schemas.md +60 -51
- package/package.json +1 -1
- package/src/parser.ts +1 -1
- package/src/router.ts +2 -2
- package/src/server.ts +7 -1
- package/test/parser.test.ts +23 -8
- package/test/router.test.ts +14 -0
package/docs/schemas.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Schemas
|
|
2
2
|
|
|
3
|
-
Galbe provides a custom Schema Type processor that offers type safety, data parsing, and validation. The
|
|
3
|
+
Galbe provides a custom Schema Type processor that offers type safety, data parsing, and validation. The use of Schemas highly simplifies request input validation and automatic error handling. Additionally, it enhances the developer experience by inferring static TypeScript types from schema definitions.
|
|
4
4
|
|
|
5
5
|
## Schema Types
|
|
6
6
|
|
|
7
|
-
To
|
|
7
|
+
To use Schema definitions, import `$T` from the `galbe` library:
|
|
8
8
|
|
|
9
9
|
```js
|
|
10
10
|
import { $T } from 'galbe'
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Here the list of available Schema types in Galbe:
|
|
13
|
+
Here is the list of available Schema types in Galbe:
|
|
14
14
|
|
|
15
|
-
####
|
|
15
|
+
#### boolean
|
|
16
16
|
|
|
17
17
|
Schema Type matching `boolean` values.
|
|
18
18
|
|
|
@@ -20,15 +20,15 @@ Schema Type matching `boolean` values.
|
|
|
20
20
|
const boolSchema = $T.boolean()
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
####
|
|
23
|
+
#### string
|
|
24
24
|
|
|
25
|
-
Schema Type matching `string`
|
|
25
|
+
Schema Type matching `string` values.
|
|
26
26
|
|
|
27
27
|
```ts
|
|
28
28
|
const strSchema = $T.string(options)
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
####
|
|
31
|
+
#### number
|
|
32
32
|
|
|
33
33
|
Schema Type matching `number` values.
|
|
34
34
|
|
|
@@ -36,7 +36,7 @@ Schema Type matching `number` values.
|
|
|
36
36
|
const numSchema = $T.number(options)
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
####
|
|
39
|
+
#### integer
|
|
40
40
|
|
|
41
41
|
Schema Type matching integer `number` values.
|
|
42
42
|
|
|
@@ -44,7 +44,7 @@ Schema Type matching integer `number` values.
|
|
|
44
44
|
const intSchema = $T.integer(options)
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
####
|
|
47
|
+
#### null
|
|
48
48
|
|
|
49
49
|
Schema Type matching `null` values.
|
|
50
50
|
|
|
@@ -52,7 +52,7 @@ Schema Type matching `null` values.
|
|
|
52
52
|
const nullSchema = $T.null(options)
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
####
|
|
55
|
+
#### any
|
|
56
56
|
|
|
57
57
|
Schema Type matching `any` of the previous Schema Types.
|
|
58
58
|
|
|
@@ -60,7 +60,7 @@ Schema Type matching `any` of the previous Schema Types.
|
|
|
60
60
|
const anySchema = $T.any()
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
####
|
|
63
|
+
#### array
|
|
64
64
|
|
|
65
65
|
Schema Type matching `array` values.
|
|
66
66
|
|
|
@@ -68,41 +68,41 @@ Schema Type matching `array` values.
|
|
|
68
68
|
const arraySchema = $T.array($T.any(), options)
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
####
|
|
71
|
+
#### optional
|
|
72
72
|
|
|
73
|
-
Makes any type optional
|
|
73
|
+
Makes any type optional, allowing `undefined` values.
|
|
74
74
|
|
|
75
75
|
```ts
|
|
76
76
|
const optionalSchema = $T.optional($T.string())
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
####
|
|
79
|
+
#### nullable
|
|
80
80
|
|
|
81
|
-
Makes any type nullable
|
|
81
|
+
Makes any type nullable, allowing `null` values.
|
|
82
82
|
|
|
83
83
|
```ts
|
|
84
84
|
const nullableSchema = $T.nullable($T.string())
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
####
|
|
87
|
+
#### nullish
|
|
88
88
|
|
|
89
|
-
Makes any type nullish
|
|
89
|
+
Makes any type nullish, allowing both `undefined` and `null` values.
|
|
90
90
|
|
|
91
91
|
```ts
|
|
92
92
|
const nullishSchema = $T.nullish($T.string())
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
####
|
|
95
|
+
#### union
|
|
96
96
|
|
|
97
|
-
Creates
|
|
97
|
+
Creates a union of Schema Types.
|
|
98
98
|
|
|
99
99
|
```ts
|
|
100
100
|
const unionSchema = $T.union([$T.string(), $T.number()])
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
## Request Schema
|
|
103
|
+
## Request Schema Definition
|
|
104
104
|
|
|
105
|
-
The Request Schema definition allows you to define a schema for your request
|
|
105
|
+
The Request Schema definition allows you to define a schema for your request in your [Route Definition](routes.md#route-definition). It must be defined right after the route path.
|
|
106
106
|
|
|
107
107
|
```js
|
|
108
108
|
const schema = {}
|
|
@@ -117,9 +117,9 @@ The Request Schema has four optional properties:
|
|
|
117
117
|
headers: { [key: string]: STString | STBoolean | STNumber | STInteger | STLiteral }
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
Defines request headers with their respective Schema types.
|
|
121
121
|
|
|
122
|
-
**Example
|
|
122
|
+
**Example:**
|
|
123
123
|
|
|
124
124
|
```ts
|
|
125
125
|
const schema = {
|
|
@@ -135,9 +135,9 @@ const schema = {
|
|
|
135
135
|
params: { [key: string]: STString | STBoolean | STNumber | STInteger | STLiteral }
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
Defines route parameters with their respective Schema types.
|
|
139
139
|
|
|
140
|
-
**Example
|
|
140
|
+
**Example:**
|
|
141
141
|
|
|
142
142
|
```ts
|
|
143
143
|
const schema = {
|
|
@@ -149,9 +149,7 @@ const schema = {
|
|
|
149
149
|
```
|
|
150
150
|
|
|
151
151
|
> [!WARNING]
|
|
152
|
-
> Every key should match an existing [route path](routes.md#route-
|
|
153
|
-
>
|
|
154
|
-
> By default, if no schema is defined for a given parameter. Galbe will assume it is of type `string`.
|
|
152
|
+
> Every key should match an existing [route path](routes.md#route-definition) parameter. Otherwise, TypeScript will show an error. If no schema is defined for a given parameter, Galbe assumes it is of type `string`.
|
|
155
153
|
|
|
156
154
|
### query
|
|
157
155
|
|
|
@@ -159,9 +157,9 @@ const schema = {
|
|
|
159
157
|
query: { [key: string]: STString | STBoolean | STNumber | STInteger | STLiteral }
|
|
160
158
|
```
|
|
161
159
|
|
|
162
|
-
|
|
160
|
+
Defines query parameters with their respective Schema types.
|
|
163
161
|
|
|
164
|
-
**Example
|
|
162
|
+
**Example:**
|
|
165
163
|
|
|
166
164
|
```ts
|
|
167
165
|
const schema = {
|
|
@@ -176,12 +174,14 @@ const schema = {
|
|
|
176
174
|
|
|
177
175
|
<!-- prettier-ignore -->
|
|
178
176
|
```ts
|
|
179
|
-
body: STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STArray |
|
|
177
|
+
body: STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STArray | STMultipartForm | STUrlForm | STStream
|
|
180
178
|
```
|
|
181
179
|
|
|
182
|
-
|
|
180
|
+
Defines the request body Schema type based on content type.
|
|
183
181
|
|
|
184
|
-
|
|
182
|
+
#### object
|
|
183
|
+
|
|
184
|
+
Defines an `application/json` request body.
|
|
185
185
|
|
|
186
186
|
```ts
|
|
187
187
|
const jsonBody = $T.object({
|
|
@@ -190,33 +190,36 @@ const jsonBody = $T.object({
|
|
|
190
190
|
})
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
-
####
|
|
193
|
+
#### multipartForm
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
Defines a `multipart/form-data` request body.
|
|
196
196
|
|
|
197
197
|
```ts
|
|
198
198
|
const multipartBody = $T.multipartForm({
|
|
199
199
|
name: $T.string(),
|
|
200
|
-
age: $T.integer({
|
|
200
|
+
age: $T.integer({ min: 0 })
|
|
201
201
|
})
|
|
202
202
|
```
|
|
203
203
|
|
|
204
|
-
####
|
|
204
|
+
#### urlForm
|
|
205
205
|
|
|
206
|
-
|
|
206
|
+
Defines an `application/x-www-form-urlencoded` request body.
|
|
207
207
|
|
|
208
208
|
```ts
|
|
209
209
|
const urlBody = $T.urlForm({
|
|
210
210
|
name: $T.string(),
|
|
211
|
-
age: $T.integer({
|
|
211
|
+
age: $T.integer({ min: 0 })
|
|
212
212
|
})
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
-
####
|
|
215
|
+
#### stream
|
|
216
|
+
|
|
217
|
+
Certain request body types can be streamed using `STStream` wrapper, improving performance by validating data incrementally.
|
|
218
|
+
This can be usefull to imporve performances in case you have heavy body payloads by leveraging early validation and fail fast behaviors.
|
|
216
219
|
|
|
217
|
-
|
|
220
|
+
**Example**
|
|
218
221
|
|
|
219
|
-
Let's
|
|
222
|
+
Let's consider a `multipart/form-data` body request that has two properties: `username` and `heavyImageFile`:
|
|
220
223
|
|
|
221
224
|
```ts
|
|
222
225
|
galbe.post(
|
|
@@ -236,9 +239,9 @@ galbe.post(
|
|
|
236
239
|
})
|
|
237
240
|
```
|
|
238
241
|
|
|
239
|
-
|
|
242
|
+
In that case, even if the `username` doesn't pass the validation, the full request body, including the `heavyImageFile` is processed before sending the response. This induces unnecessary time and resource consumption because the `heavyImageFile` is processed despite never been used.
|
|
240
243
|
|
|
241
|
-
|
|
244
|
+
A better approach would consist in leveraging `STStream` wrapper to implement early validation and fail fast behavior. By defining the request body as a stream. Instead of receiving a plain js object as `ctx.body`, you will receive an [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator).
|
|
242
245
|
|
|
243
246
|
```ts
|
|
244
247
|
galbe.post(
|
|
@@ -249,7 +252,7 @@ galbe.post(
|
|
|
249
252
|
heavyImageFile: $T.byteArray()
|
|
250
253
|
}))
|
|
251
254
|
},
|
|
252
|
-
ctx => {
|
|
255
|
+
async ctx => {
|
|
253
256
|
// At that point, the body has not been processed yet.
|
|
254
257
|
for await (const [key, value] of ctx.body) {
|
|
255
258
|
if (key === "username" && !isValid(value)) {
|
|
@@ -266,16 +269,22 @@ galbe.post(
|
|
|
266
269
|
|
|
267
270
|
<!-- prettier-ignore -->
|
|
268
271
|
```ts
|
|
269
|
-
response: Record<number, STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STArray | STStream>
|
|
272
|
+
response: Record<number | 'default', STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STArray | STStream>
|
|
270
273
|
```
|
|
271
274
|
|
|
272
|
-
|
|
275
|
+
Defines response validation by associating schema types with specific HTTP status codes. The special key `default` can also be used to define the Response Schema for the remaining status codes.
|
|
273
276
|
|
|
274
|
-
|
|
277
|
+
**Example:**
|
|
275
278
|
|
|
276
279
|
```ts
|
|
277
280
|
const response = {
|
|
278
|
-
200: $T.object({ data: $T.array($T.number()) })
|
|
279
|
-
404: $T.literal("Not found")
|
|
281
|
+
200: $T.object({ data: $T.array($T.number()) }),
|
|
282
|
+
404: $T.literal("Not found"),
|
|
283
|
+
default: $T.string(),
|
|
280
284
|
}
|
|
281
285
|
```
|
|
286
|
+
|
|
287
|
+
This ensures all responses adhere to the defined schema.
|
|
288
|
+
|
|
289
|
+
> [!NOTE]
|
|
290
|
+
> The response validation is enabled by default, meaning that every enpoint response that has a schema defined will be validated at runtime. To disable runtime validation, you can set the `responseValidator?.enabled` option to `false` in the [configuration](getting-started.md#configuration).
|
package/package.json
CHANGED
package/src/parser.ts
CHANGED
|
@@ -392,7 +392,7 @@ const parseMultipartHeader = (header: string): { name: string;[key: string]: str
|
|
|
392
392
|
acc[key] = v[2]
|
|
393
393
|
return acc
|
|
394
394
|
}, {})
|
|
395
|
-
if (disposition !== 'form-data') null
|
|
395
|
+
if (disposition !== 'form-data') return null
|
|
396
396
|
//@ts-ignore
|
|
397
397
|
return multipartHeader
|
|
398
398
|
}
|
package/src/router.ts
CHANGED
|
@@ -55,8 +55,8 @@ export class GalbeRouter {
|
|
|
55
55
|
const isStatic = !route.path.match(/(:[\w\d-]+|\*)/)
|
|
56
56
|
if (isStatic) this.cachedRoutes.set(`[${route.method.toUpperCase()}]${route.path}`, route)
|
|
57
57
|
route.path = `${this.prefix || ''}${route.path}`
|
|
58
|
-
let path = route.path.replace(
|
|
59
|
-
path.shift()
|
|
58
|
+
let path = route.path.replace(/^\/+|\/+$/g, '').split('/')
|
|
59
|
+
if (path[0] === '') path.shift()
|
|
60
60
|
let r = this.routes
|
|
61
61
|
if (!path.length) {
|
|
62
62
|
r.routes[route.method] = route
|
package/src/server.ts
CHANGED
|
@@ -71,7 +71,13 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
|
|
|
71
71
|
const inHeaders: Record<string, any> = {}
|
|
72
72
|
for (let [k, v] of req.headers) inHeaders[k] = v
|
|
73
73
|
let inQuery: Record<string, any> = {}
|
|
74
|
-
for (let [k, v] of url.searchParams)
|
|
74
|
+
for (let [k, v] of url.searchParams) {
|
|
75
|
+
if (k in inQuery) {
|
|
76
|
+
const cur = inQuery[k]
|
|
77
|
+
if (Array.isArray(cur)) inQuery[k] = [...cur, v]
|
|
78
|
+
else inQuery[k] = [cur, v]
|
|
79
|
+
} else inQuery[k] = v
|
|
80
|
+
}
|
|
75
81
|
let inParams = requestPathParser(url.pathname, route.path)
|
|
76
82
|
|
|
77
83
|
context.body = !EMPTY_BODY_METHODS.includes(req.method)
|
package/test/parser.test.ts
CHANGED
|
@@ -300,16 +300,31 @@ describe('parser', () => {
|
|
|
300
300
|
}
|
|
301
301
|
]
|
|
302
302
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
303
|
+
for (let { p, expected } of cases) {
|
|
304
|
+
let search = new URLSearchParams()
|
|
305
|
+
for (const [k, v] of Object.entries(p)) search.append(k, v as string)
|
|
306
306
|
|
|
307
|
-
|
|
308
|
-
|
|
307
|
+
let resp = await fetch(`http://localhost:${port}/query/params/schema?${search.toString()}`)
|
|
308
|
+
let body = await resp.json()
|
|
309
309
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
310
|
+
expect(resp.status).toBe(expected.status ?? 200)
|
|
311
|
+
expect(body).toEqual(expected.body)
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
test('query params, duplicates', async () => {
|
|
316
|
+
const search = new URLSearchParams()
|
|
317
|
+
search.append('p1', 'one')
|
|
318
|
+
search.append('p1', 'two')
|
|
319
|
+
search.append('p2', '3.14')
|
|
320
|
+
search.append('p3', 'true')
|
|
321
|
+
search.append('p4', '42')
|
|
322
|
+
|
|
323
|
+
let resp = await fetch(`http://localhost:${port}/query/params/schema?${search.toString()}`)
|
|
324
|
+
let body = await resp.json()
|
|
325
|
+
|
|
326
|
+
expect(resp.status).toBe(400)
|
|
327
|
+
expect(body).toEqual({ query: { p1: 'Multiple values found' } })
|
|
313
328
|
})
|
|
314
329
|
|
|
315
330
|
test('body, json, schema object', async () => {
|
package/test/router.test.ts
CHANGED
|
@@ -214,4 +214,18 @@ describe('router', () => {
|
|
|
214
214
|
expect(err).toBeInstanceOf(MethodNotAllowedError)
|
|
215
215
|
}
|
|
216
216
|
})
|
|
217
|
+
|
|
218
|
+
test('trailing slash routes', async () => {
|
|
219
|
+
const galbe = new Galbe()
|
|
220
|
+
const router = galbe.router
|
|
221
|
+
|
|
222
|
+
const handler = () => {}
|
|
223
|
+
galbe.get('/test/', handler)
|
|
224
|
+
|
|
225
|
+
expect(router.routes.children?.test?.routes.get?.handler).toBe(handler)
|
|
226
|
+
|
|
227
|
+
const r = router.find('get', '/test')
|
|
228
|
+
expect(r.path).toBe('/test/')
|
|
229
|
+
expect(r.handler).toBe(handler)
|
|
230
|
+
})
|
|
217
231
|
})
|