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/docs/schemas.md CHANGED
@@ -1,18 +1,18 @@
1
- # Shemas
1
+ # Schemas
2
2
 
3
- Galbe provides a custom Schema Type processor that offers type safety, data parsing, and validation. The primary purpose of this feature is to simplify request input validation and error handling automatically. Additionally, it enhances the developer's experience by inferring static TypeScript types from schema definitions.
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 start using Schema definitions, import `$T` from the `galbe` library:
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
- #### Boolean
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
- #### String
23
+ #### string
24
24
 
25
- Schema Type matching `string` vlues.
25
+ Schema Type matching `string` values.
26
26
 
27
27
  ```ts
28
28
  const strSchema = $T.string(options)
29
29
  ```
30
30
 
31
- #### Number
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
- #### Integer
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
- #### Null
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
- #### Any
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
- #### Array
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
- #### Optional
71
+ #### optional
72
72
 
73
- Makes any type optional. This allows for `undefined` values.
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
- #### Nullable
79
+ #### nullable
80
80
 
81
- Makes any type nullable. This allows for `null` values.
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
- #### Nullish
87
+ #### nullish
88
88
 
89
- Makes any type nullish. This allows for `undefined` and `null` values.
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
- #### Union
95
+ #### union
96
96
 
97
- Creates an union of Schema Types.
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 definition
103
+ ## Request Schema Definition
104
104
 
105
- The Request Schema definition allows you to define a schema for your request on your [Route Definition](routes.md#route-defintion). It must be defined right after the path of your route.
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
- This is a key-value object where each key represents a request header name, and the value is the associated Schema.
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
- This is a key-value object where each key represents a request path parameter name, and the value is the associated Schema.
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-defintion) parameter. Otherwise Typescript will show an error.
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
- This is a key-value object where each key represents a request query parameter name, and the value is the associated Schema.
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 | STMulripartForm | STUrlForm | STStream
177
+ body: STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STArray | STMultipartForm | STUrlForm | STStream
180
178
  ```
181
179
 
182
- #### Object
180
+ Defines the request body Schema type based on content type.
183
181
 
184
- To define an `application/json` request body, use `STObject` Schema Type. Example:
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
- #### Multipart
193
+ #### multipartForm
194
194
 
195
- To define a `multipart/form-data` request body, use `TMultipartForm` Schema Type. Example:
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({ minimum: 0 })
200
+ age: $T.integer({ min: 0 })
201
201
  })
202
202
  ```
203
203
 
204
- #### Url Form
204
+ #### urlForm
205
205
 
206
- To define an `application/x-www-form-urlencoded` request body, use `TUrlForm` Schema Type. Example:
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({ minimum: 0 })
211
+ age: $T.integer({ min: 0 })
212
212
  })
213
213
  ```
214
214
 
215
- #### Stream
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
- Some body request types can be streamed by using `STStream` Schema Type wrapper. The streamable Schema Types are `STByteArray`, `STString`, `STUrlForm` and `STMultipartForm`. This can be usefull to imporve performances in case you have heavy body payloads and you want to perform early validations on the body.
220
+ **Example**
218
221
 
219
- Let's look at a concrete example where this could be useful. Imagine you want a `multipart/form-data` body request that has two properties: `username` and `heavyImageFile`. In a normal case, you would define something like this:
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
- This means that in the case where the username wouldn't pass the validation, the full request body, including the `heavyImageFile`, would have been processed for nothing, as it is not used. This would induce unnecessary time and resource consumption.
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
- The `STStream` Schema Type wrapper was created to remediate to remediate this issue. In practice it allows you to perform validations on the fly. Now in your handler, instead of receiving an object as `ctx.body`, you will receive an [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator).
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
- Same as for request body validation but to validate handler responses. Every schema type must be associated to a specific response status.
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
- #### Example
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "galbe",
3
- "version": "0.10.0",
3
+ "version": "0.11.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/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(/^\/$(.*)\/?$/, '$1').split('/')
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) inQuery[k] = v
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)
@@ -300,16 +300,31 @@ describe('parser', () => {
300
300
  }
301
301
  ]
302
302
 
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)
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
- let resp = await fetch(`http://localhost:${port}/query/params/schema?${search.toString()}`)
308
- let body = await resp.json()
307
+ let resp = await fetch(`http://localhost:${port}/query/params/schema?${search.toString()}`)
308
+ let body = await resp.json()
309
309
 
310
- expect(resp.status).toBe(expected.status ?? 200)
311
- expect(body).toEqual(expected.body)
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 () => {
@@ -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
  })