galbe 0.1.7 → 0.1.13

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,12 +1,10 @@
1
1
  # Shemas
2
2
 
3
- Galbe offers a custom Schema Type processor that provides type safety along with data parsing and validation.
4
-
5
- The prime intention of that features is to offer an easy way to manage automatically request inputs validation and error handling. Moreover, it also greatly improve developper's experience by infering static Typescript types from schema definitions.
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.
6
4
 
7
5
  ## Schema Types
8
6
 
9
- To get started with Schema defintion, just import `$T` from `galbe` library:
7
+ To start using Schema definitions, import `$T` from the `galbe` library:
10
8
 
11
9
  ```js
12
10
  import { $T } from 'galbe'
@@ -62,32 +60,32 @@ Schema Type matching `array` values.
62
60
  const arraySchema = $T.array($T.any(), options)
63
61
  ```
64
62
 
65
- #### Union
63
+ #### Optional
66
64
 
67
- Creates an union of Schema Types. .
65
+ Makes any type optional. This allows for `undefined` values.
68
66
 
69
67
  ```ts
70
- const unionSchema = $T.union([$T.string(), $T.number()])
68
+ const optionalSchema = $T.optional($T.string())
71
69
  ```
72
70
 
73
- #### TOptional
71
+ #### Union
74
72
 
75
- Makes any type optional. In practice, this allows for `undefined` values.
73
+ Creates an union of Schema Types.
76
74
 
77
75
  ```ts
78
- const optionalSchema = $T.optional($T.string())
76
+ const unionSchema = $T.union([$T.string(), $T.number()])
79
77
  ```
80
78
 
81
79
  ## Request Schema definition
82
80
 
83
- The Request Schema definition allows you to define a schema for your request on your [Route Definition](). It must be defined right after the [path]() of your route.
81
+ 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.
84
82
 
85
83
  ```js
86
84
  const schema = {}
87
85
  galbe.get('/foo/:bar', schema, ctx => {})
88
86
  ```
89
87
 
90
- The Request Schema has 4 optional properties
88
+ The Request Schema has four optional properties:
91
89
 
92
90
  ### headers
93
91
 
@@ -95,7 +93,7 @@ The Request Schema has 4 optional properties
95
93
  headers: { [key: string]: STString | STBoolean | STNumber | STInteger | STLiteral }
96
94
  ```
97
95
 
98
- This is a key-value object where each key represents a request `header` name and the value the Schema associated.
96
+ This is a key-value object where each key represents a request header name, and the value is the associated Schema.
99
97
 
100
98
  **Example**:
101
99
 
@@ -113,7 +111,7 @@ const schema = {
113
111
  params: { [key: string]: STString | STBoolean | STNumber | STInteger | STLiteral }
114
112
  ```
115
113
 
116
- This is a key-value object where each key represents a request `path parameter` name and the value the Schema associated.
114
+ This is a key-value object where each key represents a request path parameter name, and the value is the associated Schema.
117
115
 
118
116
  **Example**:
119
117
 
@@ -127,7 +125,7 @@ const schema = {
127
125
  ```
128
126
 
129
127
  > [!WARNING]
130
- > Every key should match an existing [route path]() parameter. Otherwise Typescript will show you an error.
128
+ > Every key should match an existing [route path](routes.md#route-defintion) parameter. Otherwise Typescript will show an error.
131
129
  >
132
130
  > By default, if no schema is defined for a given parameter. Galbe will assume it is of type `string`.
133
131
 
@@ -137,7 +135,7 @@ const schema = {
137
135
  query: { [key: string]: STString | STBoolean | STNumber | STInteger | STLiteral }
138
136
  ```
139
137
 
140
- This is a key-value object where each key represents a request `query parameter` name and the value the Schema associated.
138
+ This is a key-value object where each key represents a request query parameter name, and the value is the associated Schema.
141
139
 
142
140
  **Example**:
143
141
 
@@ -152,13 +150,15 @@ const schema = {
152
150
 
153
151
  ### body
154
152
 
153
+ <!-- prettier-ignore -->
155
154
  ```ts
156
- body: STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STMulripartForm | STUrlForm
155
+ body: STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral |
156
+ STObject | STMulripartForm | STUrlForm
157
157
  ```
158
158
 
159
159
  #### Json
160
160
 
161
- To define an `application/json` request body. You must use `STObject` Schema Type. Example:
161
+ To define an `application/json` request body, use `STObject` Schema Type. Example:
162
162
 
163
163
  ```ts
164
164
  const jsonBody = $T.object({
@@ -169,7 +169,7 @@ const jsonBody = $T.object({
169
169
 
170
170
  #### Multipart
171
171
 
172
- To define a `multipart/form-data` request body. You must use `TMultipartForm` Schema Type. Example:
172
+ To define a `multipart/form-data` request body, use `TMultipartForm` Schema Type. Example:
173
173
 
174
174
  ```ts
175
175
  const multipartBody = $T.multipartForm({
@@ -180,7 +180,7 @@ const multipartBody = $T.multipartForm({
180
180
 
181
181
  #### Url Form
182
182
 
183
- To define an `application/x-www-form-urlencoded` request body. You must use `TUrlForm` Schema Type. Example:
183
+ To define an `application/x-www-form-urlencoded` request body, use `TUrlForm` Schema Type. Example:
184
184
 
185
185
  ```ts
186
186
  const urlBody = $T.urlForm({
@@ -191,11 +191,9 @@ const urlBody = $T.urlForm({
191
191
 
192
192
  #### Stream
193
193
 
194
- Some body request types can be streamed by using `STStream` Schema Type wrapper. Streamable Schema Types are `STByteArray`, `STString`, `STUrlForm` and `STMultipartForm`.
194
+ 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.
195
195
 
196
- This can be usefull to imporve performances in case you have heavy body payloads and you want to perform early validations on the body.
197
-
198
- Let's see a concrete example where that could be usefull. Imagine you want a `multipart/form-data` body request that has two properties `username` and `heavyImageFile`. In the normal case you would define something like that:
196
+ 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:
199
197
 
200
198
  ```ts
201
199
  galbe.post(
@@ -215,11 +213,9 @@ galbe.post(
215
213
  })
216
214
  ```
217
215
 
218
- 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. Inducing unnecessary time and resource consumption.
219
-
220
- The `STStream` Schema Type wrapper was created to remediate to that issue. In practice it allows you to perform validations on the fly.
216
+ 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.
221
217
 
222
- Now in your handler, instead of receiving an object as ctx.body, you'll receive an [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator).
218
+ 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).
223
219
 
224
220
  ```ts
225
221
  galbe.post(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "galbe",
3
- "version": "0.1.7",
3
+ "version": "0.1.13",
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
@@ -80,7 +80,6 @@ const galbeMethod = <
80
80
  [header: string]: string
81
81
  }
82
82
  status?: number
83
- redirect?: string
84
83
  }
85
84
  }
86
85
  return {
@@ -123,7 +122,10 @@ export class Galbe {
123
122
  constructor(config?: GalbeConfig) {
124
123
  this.config = config ?? {}
125
124
  this.config.routes = this.config.routes ?? true
126
- this.router = new GalbeRouter(this.config?.basePath || '')
125
+ this.router = new GalbeRouter({
126
+ prefix: this.config?.basePath || '',
127
+ cacheEnabled: this.config?.router?.cacheEnabled
128
+ })
127
129
  }
128
130
  private add(route: any) {
129
131
  this.router.add(route)
package/src/parser.ts CHANGED
@@ -9,16 +9,14 @@ import type {
9
9
  MultipartFormData,
10
10
  STUrlFormValues,
11
11
  STMultipartFormValues,
12
- STUnion,
13
12
  STLiteral,
14
- STArray,
15
13
  STSchema
16
14
  } from './schema'
17
15
 
18
16
  import { readableStreamToArrayBuffer } from 'bun'
19
17
  import { Kind, Optional, Stream } from './schema'
20
18
  import { validate } from './validator'
21
- import { RequestError, $T } from './index'
19
+ import { InternalError, RequestError } from './index'
22
20
 
23
21
  const textDecoder = new TextDecoder()
24
22
  const textEncoder = new TextEncoder()
@@ -39,10 +37,10 @@ const MP_HEADER_RX = /^multipart\/form-data/
39
37
 
40
38
  export const requestBodyParser = async (
41
39
  body: ReadableStream | null,
42
- headers: { 'content-type'?: string; 'content-length'?: string },
40
+ headers: Record<string, string>,
43
41
  schema?: STBody
44
42
  ) => {
45
- const { 'content-type': contentType, 'content-length': _contentLength } = headers
43
+ const contentType = headers?.['content-type']
46
44
  const kind = schema?.[Kind]
47
45
  const isStream = schema && Stream in schema
48
46
  try {
@@ -60,19 +58,19 @@ export const requestBodyParser = async (
60
58
  } else if (schema?.[Optional]) {
61
59
  return null
62
60
  } else {
63
- throw new RequestError({ status: 400, error: { body: `Not a valid ${kind}` } })
61
+ throw new RequestError({ status: 400, payload: { body: `Not a valid ${kind}` } })
64
62
  }
65
63
  } else {
66
64
  if (contentType === BA_HEADER) {
67
65
  return new Uint8Array()
68
66
  } else if (contentType === JSON_HEADER) {
69
- throw new RequestError({ status: 400, error: { body: 'Not a valid json' } })
67
+ throw new RequestError({ status: 400, payload: { body: 'Not a valid json' } })
70
68
  } else if (contentType?.match(TXT_HEADER_RX)) {
71
- throw new RequestError({ status: 400, error: { body: 'Not a valid text' } })
69
+ throw new RequestError({ status: 400, payload: { body: 'Not a valid text' } })
72
70
  } else if (contentType?.match(FORM_HEADER_RX)) {
73
- throw new RequestError({ status: 400, error: { body: 'Not a valid form' } })
71
+ throw new RequestError({ status: 400, payload: { body: 'Not a valid form' } })
74
72
  } else if (contentType?.match(MP_HEADER_RX)) {
75
- throw new RequestError({ status: 400, error: { body: 'Not a valid multipart form' } })
73
+ throw new RequestError({ status: 400, payload: { body: 'Not a valid multipart form' } })
76
74
  } else return null
77
75
  }
78
76
  } else {
@@ -91,7 +89,7 @@ export const requestBodyParser = async (
91
89
  : contentType?.match(MP_HEADER_RX)
92
90
  ? 'multipartForm'
93
91
  : contentType
94
- throw new RequestError({ status: 400, error: { body: `Expected ${kind}, received ${received}` } })
92
+ throw new RequestError({ status: 400, payload: { body: `Expected ${kind}, received ${received}` } })
95
93
  } else if (!contentType || contentType === BA_HEADER) {
96
94
  if (!schema) {
97
95
  if (isStream) return rsToAsyncIterator(body)
@@ -104,7 +102,14 @@ export const requestBodyParser = async (
104
102
  }
105
103
  } else if (contentType === JSON_HEADER) {
106
104
  if (!schema) {
107
- return await streamToString(body, $T.object($T.any()))
105
+ try {
106
+ return JSON.parse(await streamToString(body))
107
+ } catch (err: any) {
108
+ throw new RequestError({
109
+ status: 400,
110
+ payload: { body: err?.message ?? 'Parsing error' }
111
+ })
112
+ }
108
113
  } else {
109
114
  const str = await streamToString(body)
110
115
  let json
@@ -113,7 +118,7 @@ export const requestBodyParser = async (
113
118
  } catch (err: any) {
114
119
  throw new RequestError({
115
120
  status: 400,
116
- error: { body: err?.message ?? 'Parsing error' }
121
+ payload: { body: err?.message ?? 'Parsing error' }
117
122
  })
118
123
  }
119
124
  return validate(json, schema, true)
@@ -129,7 +134,7 @@ export const requestBodyParser = async (
129
134
  return await streamToUrlForm(body)
130
135
  } else {
131
136
  if (kind !== 'urlForm')
132
- throw new RequestError({ status: 400, error: { body: `Expected ${kind}, received urlForm` } })
137
+ throw new RequestError({ status: 400, payload: { body: `Expected ${kind}, received urlForm` } })
133
138
  if (isStream) return $streamToUrlForm(body, schema as STStream<STUrlForm>)
134
139
  else return await streamToUrlForm(body, schema as STUrlForm)
135
140
  }
@@ -139,7 +144,7 @@ export const requestBodyParser = async (
139
144
  return await streamToMultipartForm(body, boundary)
140
145
  } else {
141
146
  if (kind !== 'multipartForm')
142
- throw new RequestError({ status: 400, error: { body: `Expected ${kind}, received MultipartForm` } })
147
+ throw new RequestError({ status: 400, payload: { body: `Expected ${kind}, received MultipartForm` } })
143
148
  if (isStream) return $streamToMultipartForm(body, boundary, schema as STStream<STMultipartForm>)
144
149
  else {
145
150
  return streamToMultipartForm(body, boundary, schema as STMultipartForm)
@@ -151,7 +156,7 @@ export const requestBodyParser = async (
151
156
  }
152
157
  } catch (error) {
153
158
  if (error instanceof RequestError) throw error
154
- else throw new RequestError({ status: 400, error: { body: error } })
159
+ else throw new RequestError({ status: 400, payload: { body: error } })
155
160
  }
156
161
  }
157
162
  async function* $streamToString(body: ReadableStream) {
@@ -189,7 +194,7 @@ async function* $streamToUrlForm(
189
194
  let s = schema?.props?.[key]?.[Kind] === 'array' ? schema?.props?.[key].items : schema?.props?.[key]
190
195
  val = s ? paramParser(val, s) : val
191
196
  } catch (error) {
192
- throw new RequestError({ status: 400, error: { body: { [key]: error } } })
197
+ throw new RequestError({ status: 400, payload: { body: { [key]: error } } })
193
198
  }
194
199
  delete required[key]
195
200
  yield [key, val]
@@ -220,7 +225,7 @@ async function* $streamToUrlForm(
220
225
  let s = schema?.props?.[key]?.[Kind] === 'array' ? schema?.props?.[key].items : schema?.props?.[key]
221
226
  val = s ? paramParser(val, s) : val
222
227
  } catch (error) {
223
- throw new RequestError({ status: 400, error: { body: { [key]: error } } })
228
+ throw new RequestError({ status: 400, payload: { body: { [key]: error } } })
224
229
  }
225
230
  delete required[key]
226
231
  yield [key, val]
@@ -234,7 +239,7 @@ async function* $streamToUrlForm(
234
239
  if (reqKeys.length > 0)
235
240
  throw new RequestError({
236
241
  status: 400,
237
- error: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
242
+ payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
238
243
  })
239
244
  }
240
245
  const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?: STUrlForm) => {
@@ -260,7 +265,7 @@ const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?: STUrlF
260
265
  errors[k] = k in errors ? [...errors[k], error] : error
261
266
  }
262
267
  }
263
- if (Object.keys(errors).length) throw new RequestError({ status: 400, error: { body: errors } })
268
+ if (Object.keys(errors).length) throw new RequestError({ status: 400, payload: { body: errors } })
264
269
  for (const [k, s] of Object.entries(required)) {
265
270
  if (s[Kind] === 'array') {
266
271
  object[k] = []
@@ -271,7 +276,7 @@ const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?: STUrlF
271
276
  if (reqKeys.length > 0)
272
277
  throw new RequestError({
273
278
  status: 400,
274
- error: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
279
+ payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
275
280
  })
276
281
  return object
277
282
  }
@@ -310,7 +315,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
310
315
  }
311
316
  } catch (err) {
312
317
  if (err instanceof RequestError) throw err
313
- throw new RequestError({ status: 400, error: { body: { [headers.name]: err } } })
318
+ throw new RequestError({ status: 400, payload: { body: { [headers.name]: err } } })
314
319
  }
315
320
  }
316
321
  bK = new Uint8Array()
@@ -343,7 +348,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
343
348
  content: parseMultipartContent(bV, headers, schema)
344
349
  }
345
350
  } catch (err) {
346
- throw new RequestError({ status: 400, error: { body: { [headers.name]: err } } })
351
+ throw new RequestError({ status: 400, payload: { body: { [headers.name]: err } } })
347
352
  }
348
353
  }
349
354
  for (const [k, s] of Object.entries(required)) {
@@ -356,7 +361,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
356
361
  if (reqKeys.length > 0)
357
362
  throw new RequestError({
358
363
  status: 400,
359
- error: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
364
+ payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
360
365
  })
361
366
  }
362
367
  const parseMultipartHeader = (header: string): { name: string; [key: string]: string } | null => {
@@ -394,19 +399,22 @@ const parseMultipartContent = (
394
399
  try {
395
400
  result = JSON.parse(textDecoder.decode(content).trim())
396
401
  } catch (err: any) {
397
- throw new RequestError({ status: 400, error: { body: { [headers.name]: err?.message || 'Parsing error' } } })
402
+ throw new RequestError({ status: 400, payload: { body: { [headers.name]: err?.message || 'Parsing error' } } })
398
403
  }
399
404
  } else if (schema?.props) {
400
405
  if (schema?.props[headers.name][Kind] === 'object') {
401
406
  try {
402
407
  result = JSON.parse(textDecoder.decode(content).trim())
403
408
  } catch (err: any) {
404
- throw new RequestError({ status: 400, error: { body: { [headers.name]: err?.message || 'Parsing error' } } })
409
+ throw new RequestError({
410
+ status: 400,
411
+ payload: { body: { [headers.name]: err?.message || 'Parsing error' } }
412
+ })
405
413
  }
406
414
  try {
407
415
  validate(result, schema?.props[headers.name])
408
416
  } catch (err) {
409
- throw new RequestError({ status: 400, error: { body: { [headers.name]: err } } })
417
+ throw new RequestError({ status: 400, payload: { body: { [headers.name]: err } } })
410
418
  }
411
419
  } else if (schema?.props[headers.name][Kind] === 'byteArray') {
412
420
  return content
@@ -415,7 +423,7 @@ const parseMultipartContent = (
415
423
  } else {
416
424
  throw new RequestError({
417
425
  status: 400,
418
- error: { body: { [headers.name]: `Expect ${schema?.props[headers.name][Kind]} found json` } }
426
+ payload: { body: { [headers.name]: `Expect ${schema?.props[headers.name][Kind]} found json` } }
419
427
  })
420
428
  }
421
429
  }
@@ -424,7 +432,7 @@ const parseMultipartContent = (
424
432
  let s = schema?.props[headers.name]
425
433
  validate(result, s?.[Kind] === 'array' ? s?.items : s)
426
434
  } catch (err) {
427
- throw new RequestError({ status: 400, error: { body: { [headers.name]: err } } })
435
+ throw new RequestError({ status: 400, payload: { body: { [headers.name]: err } } })
428
436
  }
429
437
  }
430
438
  return result
@@ -473,7 +481,7 @@ const streamToMultipartForm = async (data: ReadableStream<Uint8Array>, boundary:
473
481
  if (Object.keys(errors).length)
474
482
  throw new RequestError({
475
483
  status: 400,
476
- error: { body: errors }
484
+ payload: { body: errors }
477
485
  })
478
486
  for (const [k, s] of Object.entries(required)) {
479
487
  if (s[Kind] === 'array') {
@@ -485,7 +493,7 @@ const streamToMultipartForm = async (data: ReadableStream<Uint8Array>, boundary:
485
493
  if (reqKeys.length > 0)
486
494
  throw new RequestError({
487
495
  status: 400,
488
- error: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
496
+ payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
489
497
  })
490
498
  return res
491
499
  }
@@ -559,14 +567,28 @@ const paramParser = (
559
567
  }
560
568
 
561
569
  export const requestPathParser = (input: string, path: string) => {
562
- let pPath = path.replace(/^\/$(.*)\/?$/, '$1').split('/')
563
- let pInput = input.replace(/^\/$(.*)\/?$/, '$1').split('/')
570
+ let pInput = input.split('/')
564
571
  let params: Record<string, any> = {}
565
- pPath.shift()
566
- pInput.shift()
567
- for (const [i, p] of pPath.entries()) {
568
- const match = p.match(/^:(.*)/)
569
- if (match) params[match[1]] = pInput[i]
572
+ let idx = 0
573
+ for (let i = 0; i < path.length; i++) {
574
+ let c = path[i]
575
+ if (c === '/') {
576
+ idx++
577
+ continue
578
+ }
579
+ if (c === ':') {
580
+ let name = ''
581
+ while (true) {
582
+ i++
583
+ c = path[i]
584
+ if (c === '/' || i >= path.length) {
585
+ i--
586
+ break
587
+ }
588
+ name += c
589
+ }
590
+ params[name] = pInput[idx]
591
+ }
570
592
  }
571
593
  return params
572
594
  }
@@ -601,7 +623,7 @@ export const parseEntry = <T extends STProps>(
601
623
  })
602
624
 
603
625
  if (Object.keys(errors).length) {
604
- throw new RequestError({ status: 400, error: options?.name ? { [options.name]: errors } : errors })
626
+ throw new RequestError({ status: 400, payload: options?.name ? { [options.name]: errors } : errors })
605
627
  }
606
628
 
607
629
  return parsedParams as Static<STObject<T>>
@@ -631,7 +653,7 @@ export const responseParser = (response: any, ctx: Context) => {
631
653
  let data = `id:${id}\ndata:${r}\n\n`
632
654
  try {
633
655
  await controller.write(data)
634
- // await controller.flush()
656
+ await controller.flush()
635
657
  } catch (err) {
636
658
  console.error(err)
637
659
  }
@@ -648,7 +670,7 @@ export const responseParser = (response: any, ctx: Context) => {
648
670
  return new Response(JSON.stringify(response), details)
649
671
  } catch (error) {
650
672
  console.error(error)
651
- throw new RequestError({ status: 500, error: 'Internal Server Error' })
673
+ throw new InternalError()
652
674
  }
653
675
  }
654
676
  }
package/src/router.ts CHANGED
@@ -44,19 +44,21 @@ const walkRoutes = (path: string[], node: RouteNode, alts: RouteNode[] = []): Ro
44
44
  export class GalbeRouter {
45
45
  routes: RouteTree
46
46
  prefix: string
47
- staticRoutes: Map<string, Route>
48
- constructor(prefix?: string) {
47
+ cacheEnabled: boolean
48
+ cachedRoutes: Map<string, Route | null>
49
+ constructor(options?: { prefix?: string; cacheEnabled?: boolean }) {
49
50
  this.routes = { GET: {}, POST: {}, PUT: {}, PATCH: {}, DELETE: {}, OPTIONS: {} }
50
- prefix = prefix || ''
51
+ let prefix = options?.prefix || ''
51
52
  if (prefix && !prefix.match(/^\//)) prefix = `/${prefix}`
52
53
  this.prefix = prefix
53
- this.staticRoutes = new Map()
54
+ this.cachedRoutes = new Map()
55
+ this.cacheEnabled = options?.cacheEnabled ?? false
54
56
  }
55
57
  add(route: Route) {
56
58
  route.path = route?.path?.[0] === '/' ? route.path : `/${route.path}`
57
59
  if (!route.path.match(ROUTE_REGEX)) throw new SyntaxError(`${route.path} is not a valid route path.`)
58
60
  const isStatic = !route.path.match(/(:[\w\d-]+|\*)/)
59
- if (isStatic) this.staticRoutes.set(`[${route.method.toUpperCase()}]${route.path}`, route)
61
+ if (isStatic) this.cachedRoutes.set(`[${route.method.toUpperCase()}]${route.path}`, route)
60
62
  route.path = `${this.prefix || ''}${route.path}`
61
63
  let path = route.path.replace(/^\/$(.*)\/?$/, '$1').split('/')
62
64
  path.shift()
@@ -87,14 +89,16 @@ export class GalbeRouter {
87
89
  }
88
90
  }
89
91
  find(method: string, path: string) {
90
- const staticRoute = this.staticRoutes.get(`[${method}]${path}`)
92
+ const staticRoute = this.cachedRoutes.get(`[${method}]${path}`)
93
+ if (staticRoute === null) throw new NotFoundError()
91
94
  if (staticRoute !== undefined) return staticRoute
92
- let parts = path
93
- .replace(/\/+/g, '/')
94
- .replace(/^\/$(.*)\/?$/, '$1')
95
- .split('/')
95
+ let parts = path.split('/')
96
96
  const route = walkRoutes(parts, this.routes[method]).route
97
- if (!route) throw new NotFoundError()
97
+ if (!route) {
98
+ if (this.cacheEnabled) this.cachedRoutes.set(`[${method}]${path}`, null)
99
+ throw new NotFoundError()
100
+ }
101
+ if (this.cacheEnabled) this.cachedRoutes.set(`[${method}]${path}`, route)
98
102
  return route
99
103
  }
100
104
  }
package/src/server.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import type { Context, Route } from './types'
2
2
 
3
- import { NotFoundError, RequestError } from './types'
3
+ import { InternalError, RequestError } from './types'
4
4
  import { parseEntry, requestBodyParser, requestPathParser, responseParser } from './parser'
5
5
  import { Galbe } from './index'
6
6
 
7
7
  const handleInternalError = (error: any) => {
8
8
  console.error(error)
9
- return new RequestError({ status: 500, error: 'Internal Server Error' })
9
+ return new InternalError()
10
10
  }
11
11
 
12
12
  const setupPluginCallbacks = (galbe: Galbe) => ({
@@ -60,7 +60,6 @@ export default async (galbe: Galbe, port?: number) => {
60
60
  let response: any = ''
61
61
  try {
62
62
  // find route
63
- if (!url.pathname.match(new RegExp(`^${galbe.config?.basePath || ''}`))) throw new NotFoundError()
64
63
  try {
65
64
  route = router.find(req.method, url.pathname)
66
65
  } catch (error) {
@@ -75,8 +74,10 @@ export default async (galbe: Galbe, port?: number) => {
75
74
 
76
75
  // parse request
77
76
  const schema = route.schema
78
- let inHeaders = Object.fromEntries(req.headers.entries())
79
- let inQuery = Object.fromEntries(url.searchParams.entries())
77
+ const inHeaders: Record<string, any> = {}
78
+ for (let [k, v] of req.headers) inHeaders[k] = v
79
+ let inQuery: Record<string, any> = {}
80
+ for (let [k, v] of url.searchParams) inQuery[k] = v
80
81
  let inParams = requestPathParser(url.pathname, route.path)
81
82
 
82
83
  context.body = await requestBodyParser(req.body, inHeaders, schema.body)
@@ -109,7 +110,7 @@ export default async (galbe: Galbe, port?: number) => {
109
110
  else throw handleInternalError(error)
110
111
  }
111
112
  if (errors.length) {
112
- throw new RequestError({ status: 400, error: errors.reduce((acc, c) => ({ ...acc, ...c.error }), {}) })
113
+ throw new RequestError({ status: 400, payload: errors.reduce((acc, c) => ({ ...acc, ...c.payload }), {}) })
113
114
  }
114
115
 
115
116
  for (const cb of pluginsCb.beforeHandle) {
@@ -123,13 +124,18 @@ export default async (galbe: Galbe, port?: number) => {
123
124
  handlerCalled = true
124
125
  return route.handler(context)
125
126
  }
126
- const callChain = route.hooks.map((hook, idx) => ({
127
+ const callChain: { call: () => any }[] = route.hooks.map((hook, idx) => ({
127
128
  call: async () => {
128
129
  let nextCalled = false
129
130
  let next = async () => {
130
- await callChain[idx + 1].call()
131
+ if (nextCalled) console.error('Hook already called - ignored')
132
+ else {
133
+ nextCalled = true
134
+ await callChain[idx + 1].call()
135
+ }
131
136
  }
132
- await hook(context, next)
137
+ let r = await hook(context, next)
138
+ if (r) return r
133
139
  if (!nextCalled && !handlerCalled) await next()
134
140
  }
135
141
  }))
@@ -139,8 +145,11 @@ export default async (galbe: Galbe, port?: number) => {
139
145
  context.set.status = response instanceof Response ? response.status : 200
140
146
  }
141
147
  })
142
- if (callChain.length > 1) await callChain[0].call()
143
- else response = await handlerWrapper(context)
148
+ if (callChain.length > 1) {
149
+ let r = await callChain[0].call()
150
+ if (r) response = r
151
+ } else response = await handlerWrapper(context)
152
+
144
153
  const parsedResponse = responseParser(response, context)
145
154
 
146
155
  for (const cb of pluginsCb.afterHandle) {
@@ -151,22 +160,29 @@ export default async (galbe: Galbe, port?: number) => {
151
160
  return parsedResponse
152
161
  } catch (error) {
153
162
  context.set.status = error instanceof RequestError ? error.status : 500
154
- if (galbe.errorHandler) return galbe.errorHandler(error, context)
163
+ let customError
164
+ if (galbe.errorHandler) customError = responseParser(galbe.errorHandler(error, context), context)
165
+ if (customError) return customError
155
166
  if (error instanceof RequestError) {
156
- return new Response(JSON.stringify(error.error), {
167
+ return new Response(JSON.stringify(error.payload), {
157
168
  status: error.status,
158
169
  headers: { 'Content-Type': 'application/json' }
159
170
  })
160
171
  }
161
- return new Response('Internal Server Error', { status: 500 })
172
+ return new Response('"Internal Server Error"', {
173
+ status: 500,
174
+ headers: {
175
+ 'content-type': 'application/json'
176
+ }
177
+ })
162
178
  }
163
179
  },
164
180
  error(error) {
165
181
  console.error(error)
166
- return new Response('Internal Server Error', {
182
+ return new Response('"Internal Server Error"', {
167
183
  status: 500,
168
184
  headers: {
169
- 'Content-Type': 'text/plain'
185
+ 'content-type': 'application/json'
170
186
  }
171
187
  })
172
188
  }