galbe 0.1.8 → 0.2.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.
@@ -32,7 +32,7 @@ jobs:
32
32
  git config user.name "${GITHUB_ACTOR}"
33
33
  - name: Install dependencies & build
34
34
  run: |
35
- bun install & bun run build
35
+ bun install && bun run build
36
36
  - name: Release
37
37
  run: |
38
38
  npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
@@ -0,0 +1,114 @@
1
+ # Context
2
+
3
+ An instance of the context object is created when a new request is initiated and carrieds out along durring all the request lifecycle.
4
+ See the [Lifecycle](https://galbe.dev/documentation/lifecycle) section to get more details.
5
+
6
+ Its purpose is to carrie all the relevent information about the request and to allow sharing informations between each step of the request lifecycle.
7
+
8
+ ## Definition
9
+
10
+ A context has the following properties:
11
+
12
+ **request**
13
+
14
+ An instance of the [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object created by th server.
15
+
16
+ **headers**
17
+
18
+ A javascript object representing the `headers` of the current request.
19
+
20
+ - key (string): header name
21
+ - value: (string | [schema defined](schemas.md#headers)): header value
22
+
23
+ ```js
24
+ {
25
+ "accept": "*/*",
26
+ "accept-encoding": "gzip, deflate, br",
27
+ "cookie": "Cookie_1=value; Cookie_2=value",
28
+ "host": "localhost:3000",
29
+ "user-agent": "galbe/1.0.0"
30
+ }
31
+ ```
32
+
33
+ **params**
34
+
35
+ A javascript object representing the request `parameters` of the current request.
36
+
37
+ - key (string): parameter name
38
+ - value: (string | [schema defined](schemas.md#params)): parameter value
39
+
40
+ ```js
41
+ galbe.get('/default/:p1/foo/:p2', ctx => console.log(ctx.params))
42
+ // GET /default/four/foo/2
43
+ { p1: "four", p2: "2" }
44
+ ```
45
+
46
+ **query**
47
+
48
+ A javascript object representing the request `query parameters` of the current request.
49
+
50
+ - key (string): query parameter name
51
+ - value: (string | [schema defined](schemas.md#query)): query parameter value
52
+
53
+ ```js
54
+ galbe.get('/test', ctx => console.log(ctx.params))
55
+ // GET /test?one=1&two=2
56
+ { one: "1", two: "2" }
57
+ ```
58
+
59
+ **body**
60
+
61
+ The body payload of the incoming request. The body type is computed according to the following rules.
62
+
63
+ If no [Schema](schemas.md) is defined, Galbe will parse the body type according to `content-type` Header value:
64
+
65
+ - `text/.*`: string
66
+ - `application/json`: object
67
+ - `application/x-www-form-urlencoded`: { [key: string]: any }
68
+ - `multipart/form-data`: { [key: string]:
69
+ { headers: { name: string; type?: string; filename?: string };
70
+ content: any
71
+ } }
72
+ - `other`: AsyncGenerator\<Uint8Array\>
73
+
74
+ If a [Schema](schemas.md) is defined, Galbe will parse the body type according to the [Schema.body](schemas.md#body) defined for the current route.
75
+
76
+ **set**
77
+
78
+ The set property contains modifiable properties which purpose are to give informations to the Response parser.
79
+
80
+ - `status`: Set the response status
81
+ - `headers`: Set the response headers
82
+
83
+ ```js
84
+ galbe.get('/example', ctx => {
85
+ ctx.set.status = 418
86
+ return "I don't do coffee"
87
+ })
88
+ ```
89
+
90
+ **state**
91
+
92
+ The state property purpose is to carry custom user object accross request lifecycle. In general it is used to share informations between the [hooks](hooks.md) and the [handler](handler.md).
93
+
94
+ - key (string): user defined key
95
+ - value (any): user defined object
96
+
97
+ ```js
98
+ galbe.get(
99
+ '/example',
100
+ [
101
+ ctx => {
102
+ ctx.state['foo'] = 'bar'
103
+ }
104
+ ],
105
+ ctx => {
106
+ return ctx.state.foo
107
+ }
108
+ )
109
+ ```
110
+
111
+ ```bash
112
+ $ curl http://localhost:3000/example
113
+ bar
114
+ ```
@@ -0,0 +1,54 @@
1
+ # Error handler
2
+
3
+ Any error happening during a request lifecycle will be intercepted by the error handler.
4
+
5
+ You can customize the default error handling behavior by defining a custom error handler using Galbe's intance `onError` method.
6
+
7
+ ```js
8
+ const galbe = new Galbe()
9
+ galbe.onError(customErrorHandler)
10
+ ```
11
+
12
+ ## Definition
13
+
14
+ The error handler should be a function that takes two aguments: an [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) and a [Context](context.md). This function may potentially return a [Response type](handler.md#response-types).
15
+
16
+ ```js
17
+ galbe.onErrorHandler((error, ctx) => {
18
+ if (error.status === 500) {
19
+ return new Response(`Server error ❌`, { status: 500 })
20
+ }
21
+ if (error.status === 404) {
22
+ return new Response(`Not found 🔎`, { status: 404 })
23
+ }
24
+ })
25
+ ```
26
+
27
+ The `error` argument could be any type of error thrown by your application. If the error originates from Galbe framework, it will be an instance of [RequestError](#request-error).
28
+
29
+ For instance, the [Router](router.md) will throw a `RequestError` with a `404` status if no route matches the incoming request path. Similarly, the Parser will throw a `RequestError` with a `400` status.
30
+
31
+ ## Request Error
32
+
33
+ The `RequestError` class is utilized to instanciate a runtime request error in Galbe. It has two optional attributes: a `status` and a `payload`.
34
+
35
+ If your application throws a `RequestError` instance, Galbe will, by default, construct a Response from your `RequestError` and send it back to the client.
36
+
37
+ ```js
38
+ import { Galbe, RequestError } from 'galbe'
39
+
40
+ const galbe = new Galbe()
41
+
42
+ galbe.get('/coffee', () => throw new RequestError({ status: 418, payload: '🫖' }))
43
+ ```
44
+
45
+ When called, above endpoint should respond:
46
+
47
+ ```bash
48
+ $ curl -i http://localhost:3000/coffee
49
+ HTTP/1.1 418 I'm a Teapot
50
+ Content-Type: application/json
51
+ Content-Length: 6
52
+
53
+ "🫖"
54
+ ```
@@ -8,14 +8,14 @@ Designed with simplicity in mind, Galbe allows you to quickly create and set up
8
8
 
9
9
  To start developing your Galbe project, you first need to install [Bun](https://bun.sh).
10
10
 
11
- ## Automatic Installation
11
+ ## Automatic installation
12
12
 
13
13
  This is the recommended way of setting up a Galbe project.
14
14
 
15
15
  ```bash
16
- bun create galbe app
17
- cd app
18
- bun install
16
+ $ bun create galbe app
17
+ $ cd app
18
+ $ bun install
19
19
  ```
20
20
 
21
21
  This will create a new project under `app` directory and install it.
@@ -23,7 +23,7 @@ This will create a new project under `app` directory and install it.
23
23
  Now you can start the dev server by running:
24
24
 
25
25
  ```bash
26
- bun dev
26
+ $ bun dev
27
27
  ```
28
28
 
29
29
  This will start a web server on `localhost:3000`.
@@ -31,20 +31,20 @@ This will start a web server on `localhost:3000`.
31
31
  To verify that the project was setup correctly and is running, try to reach `localhost:3000/hello` endpoint, this should return following greeting message:
32
32
 
33
33
  ```bash
34
- curl localhost:3000/hello
34
+ $ curl localhost:3000/hello
35
35
  Hello from Galbe!
36
36
  ```
37
37
 
38
38
  > [!TIP]
39
39
  > By default, the dev server automatically reloads on every file change.
40
40
 
41
- ## Manual Installation
41
+ ## Manual installation
42
42
 
43
43
  Init a new Bun project and add Galbe as dependency:
44
44
 
45
45
  ```bash
46
- bun init
47
- bun add galbe
46
+ $ bun init
47
+ $ bun add galbe
48
48
  ```
49
49
 
50
50
  Open `package.json` file and add the following scripts:
@@ -80,7 +80,7 @@ This is the recommended way to proceed but it is not mandatory. Galbe instances
80
80
  ### Galbe CLI
81
81
 
82
82
  ```bash
83
- galbe <command> <argument> [options]
83
+ $ galbe <command> <argument> [options]
84
84
  ```
85
85
 
86
86
  Here are the available commands:
@@ -171,7 +171,7 @@ export default new Galbe(config)
171
171
  > export default config
172
172
  > ```
173
173
 
174
- ## Project Structure
174
+ ## Project structure
175
175
 
176
176
  One key aspect of Galbe, is its versatility in terms of project structure. This is partly allowed by the [Automatic Route Analyzer](routes.md#automatic-route-analyzer) and the `routes` config property which defaults to `src/**/*.route.{js,ts}`.
177
177
 
package/docs/handler.md CHANGED
@@ -1 +1,105 @@
1
1
  # Handler
2
+
3
+ A handler is a function that gets executed when a request matches the route definition. It is responsible for processing the request and sending a response.
4
+
5
+ ## Handler declaration
6
+
7
+ The handler should be declared as last argument of the [Route Definition](routes.md#route-defintion) method.
8
+
9
+ ```js
10
+ galbe.get('foo', schema, [hook1, hook2], ctx => {})
11
+ ```
12
+
13
+ Handler are called after the last hook call, or right after the request parsing if no hook is declared. To get a better understanding of the request lifecycle, you can refer to the [Lifecycle](https://galbe.dev/documentation/lifecycle) section.
14
+
15
+ ## Handler definition
16
+
17
+ ```js
18
+ const handler = ctx => {
19
+ const { name } = ctx.query
20
+ return `Hello ${name}!`
21
+ }
22
+ ```
23
+
24
+ The handler function takes a `context` object as single argument and might return a `response`.
25
+
26
+ **context**
27
+
28
+ The `context` object contains the request information as well as a `set` object that serves as a response modifier. You can find more detailed informations about the `context` object in the [Context](context.md) section.
29
+
30
+ **response**
31
+
32
+ To send a response, your handler can return an object. The response sent will depend on the type of the object returned. There are four types of responses that can be returned by a handler method. More about that in the next section.
33
+
34
+ ## Response types
35
+
36
+ > [!NOTE]
37
+ > This section only cover response body payloads, to return specific response headers and/or status, you should define them with the `context.set` object before the return statement. More about it in the [Context](context.md) section.
38
+
39
+ ### String
40
+
41
+ Case where `string` is returned by the handler.
42
+
43
+ - status: 200
44
+ - content-type: `text-plain`
45
+
46
+ **Example**
47
+
48
+ ```js
49
+ galbe.get('/example', ctx => {
50
+ return 'Hello Mom!'
51
+ })
52
+ ```
53
+
54
+ ### Object
55
+
56
+ Case where an `object` is returned by the handler.
57
+
58
+ - status: 200
59
+ - content-type: `application/json`
60
+
61
+ **Example**
62
+
63
+ ```js
64
+ galbe.get('/example', ctx => {
65
+ return 'Hello Mom!'
66
+ })
67
+ ```
68
+
69
+ ### Response instance
70
+
71
+ Case where a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) instance is returned by the handler.
72
+
73
+ In that case, `context.set` properties are not taken into account to contruct the response.
74
+
75
+ **Example**
76
+
77
+ <!-- prettier-ignore -->
78
+ ```js
79
+ galbe.get('/example', ctx => {
80
+ return new Response(
81
+ 'Hello Mom',
82
+ { status: 200, headers: { 'content-type': 'text/plain' }
83
+ })
84
+ })
85
+ ```
86
+
87
+ ### Generator
88
+
89
+ Case where a [Generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator) instance is returned by the handler.
90
+
91
+ - status: 200
92
+ - content-type: `text/event-stream`
93
+
94
+ **Example**
95
+
96
+ ```js
97
+ async function* generator(array) {
98
+ for (const item of array) {
99
+ await Bun.sleep(500)
100
+ yield item
101
+ }
102
+ }
103
+
104
+ galbe.get('/example', ctx => generator(['one', 'two', 'three']))
105
+ ```
package/docs/hooks.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Hooks provide a simple way to perform specific actions before and/or after reaching a specific route endpoint.
4
4
 
5
- ## Hook Definition
5
+ ## Hook definition
6
6
 
7
7
  ```ts
8
8
  const hook = (context, next) => {
@@ -16,24 +16,26 @@ The hook takes only two arguments, a `context` object and a `next` function.
16
16
 
17
17
  **context**
18
18
 
19
- The `context` object contains the request information along with a state property that is modifiable and preserved across all hooks and the handler. It is useful for sharing information or objects across hooks and handler. You can find more information about it in the [Context]() section.
19
+ The `context` object contains the request information along with a state property that is modifiable and preserved across all hooks and the handler. It is useful for sharing information or objects across hooks and handler. You can find more information about it in the [Context](context.md) section.
20
20
 
21
21
  **next**
22
22
 
23
23
  The `next` function calls the next hook in the hook list or the handler if the current hook is the last one declared. The `next` function should be called at most once. If it is omitted, Galbe will call it automatically at the end of the execution of the current hook.
24
24
 
25
25
  > [!TIP]
26
- > Hooks are interruptible objects, meaning they can return a response at any moment. This provides a powerful mechanism for implementing custom logic, such as authentication, authorization, caching, and more."
26
+ > Hooks are interruptible objects, meaning they can return a response at any moment. This provides a powerful mechanism for implementing custom logic, such as authentication, authorization, caching, and more.
27
+ >
28
+ > To learn more about response types, ou can take a look at the [Response types](handler.md#response-types) section.
27
29
 
28
- ## Hooks Declaration
30
+ ## Hooks declaration
29
31
 
30
- Hooks should be declared just before the handler method in the [Route Definition]() method as a list of Hooks.
32
+ Hooks should be declared just before the handler method in the [Route Definition](routes.md#route-defintion) method as a list of Hooks.
31
33
 
32
34
  ```ts
33
35
  galbe.get('foo', [ hook1, hook2, ... ], ctx => {})
34
36
  ```
35
37
 
36
- Hooks are called just before the [Handler]() in the order that they have been declared in the hook list of the [Route Definition](). To get a better understanding of hooks execution during the request lifecycle, you can refer to the [Lifecycle]() section.
38
+ Hooks are called just before the [Handler](handler.md) in the order that they have been declared in the hook list of the [Route Definition](routes.md#route-defintion). To get a better understanding of hooks execution during the request lifecycle, you can refer to the [Lifecycle](https://galbe.dev/documentation/lifecycle) section.
37
39
 
38
40
  ### Examples
39
41
 
@@ -53,7 +55,7 @@ galbe.get('example', [hook1, hook2], ctx => {
53
55
  ```
54
56
 
55
57
  ```bash
56
- curl http://localhost:3000/example
58
+ $ curl http://localhost:3000/example
57
59
  hook1
58
60
  hook2
59
61
  handler
@@ -79,7 +81,7 @@ galbe.get('example', [hook1, hook2], ctx => {
79
81
  ```
80
82
 
81
83
  ```bash
82
- curl http://localhost:3000/example
84
+ $ curl http://localhost:3000/example
83
85
  hook1 start
84
86
  hook2 start
85
87
  handler
package/docs/plugins.md CHANGED
@@ -1 +1,124 @@
1
1
  # Plugins
2
+
3
+ Galbe provides a powerful plugin system that allows developers to extend and customize the behavior of the framework. The plugin capabilities are centered around the [Request Lifecycle](https://galbe.dev/documentation/lifecycle).
4
+
5
+ ## Definition
6
+
7
+ ```ts
8
+ type GalbePlugin = {
9
+ name: string
10
+ init?: (config: any, galbe: Galbe) => MaybePromise<void>
11
+ onFetch?: (context: Context) => MaybePromise<Response | void>
12
+ onRoute?: (context: Context) => MaybePromise<Response | void>
13
+ beforeHandle?: (context: Context) => MaybePromise<Response | void>
14
+ afterHandle?: (response: Response, context: Context) => MaybePromise<Response | void>
15
+ }
16
+ ```
17
+
18
+ **name**
19
+
20
+ The name should be a Unique Plugin Identifier. It should be chosen to be unique to avoid conflicts with other potential plugins. For example, Galbe's official plugins names will always start with `dev.galbe.*`.
21
+
22
+ **init**
23
+
24
+ This method is called right after the server starts. It takes two arguments: a `config` and a `galbe` instance. The `config` holds the configuration for the specific scope of the current plugin (See [Configuration](getting-started.md#properties) `plugin` property for more details). The `galbe` argument is the instance of the current server; you can for instance retrieve the current routes definitions with `galbe.router.routes`.
25
+
26
+ **onFetch**
27
+
28
+ This method is called at the beginning of an incoming request. It takes a single `context` argument representing the current request [Context](context.md).
29
+
30
+ It is preemptable, meaning that any returned value will be interpreted as a response to send back to the client. Therefore, the method should only return [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) instances or nothing.
31
+
32
+ **onRoute**
33
+
34
+ This method is called after the router has found a matching route for the current request. Its takes a single `context` argument representing the current request [Context](context.md).
35
+
36
+ It is preemptable, meaning that any returned value will be interpreted as a response to send back to the client. Therefore, the method should only return [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) instances or nothing.
37
+
38
+ **beforeHandle**
39
+
40
+ This method is called after the request has been validated and before the route hooks and the handler are called. It takes a single `context` argument representing the current request [Context](context.md).
41
+
42
+ It is preemptable, meaning that any returned value will be interpreted as a response to send back to the client. Therefore, the method should only return [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) instances or nothing.
43
+
44
+ **afterHandle**
45
+
46
+ This method is called after the route handler has been called and before the response is sent. Its takes two arguments: a `response` object containing the [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) returned by the handler, and a `context` argument representing the current request [Context](context.md).
47
+
48
+ It is preemptable, meaning that any returned value will be interpreted as a response to send back to the client. Therefore, the method should only return [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) instances or nothing.
49
+
50
+ ## Registration
51
+
52
+ To register a plugin with your Galbe server, you must use the `use` method from you galbe instance.
53
+
54
+ ```js
55
+ const galbe = new Galbe()
56
+ galbe.use(plugin)
57
+ ```
58
+
59
+ ## Example
60
+
61
+ Here is an example of a plugin implementation that handles routes tagged with `@deprecated` metadata (See [Route files](routes.md#route-files) section about metadata).
62
+
63
+ ```ts
64
+ // myPlugin.ts
65
+
66
+ import { Galbe, type Context, type Route } from 'galbe'
67
+
68
+ class MyPlugin {
69
+ name = 'dev.galbe.example'
70
+ deprecated: Record<string, string[]> = {}
71
+ /**
72
+ * Retrieve and store all route with a @deprecated flag metadata
73
+ */
74
+ init(config: any, galbe: Galbe) {
75
+ if (config?.enabled && galbe.meta) {
76
+ for (const f of galbe.meta) {
77
+ for (const [path, methods] of Object.entries(f.routes)) {
78
+ for (const [method, meta] of Object.entries(methods)) {
79
+ if (meta.deprecated) {
80
+ if (!this.deprecated?.[method]) this.deprecated[method] = []
81
+ this.deprecated[method].push(path)
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ /**
89
+ * Check if the current route is deprecated, flags it as is and logs it
90
+ */
91
+ onRoute(context: Context) {
92
+ let route = context.route
93
+ if (this.deprecated?.[route.method]?.includes(route.path)) {
94
+ context.state[this.name] = { deprecated: true }
95
+ console.warn(`Call to deprecated route [${route.method}]${route.path}`)
96
+ }
97
+ }
98
+ /**
99
+ * Adds a header if the request has previously been flagged as deprecated
100
+ */
101
+ afterHandle(response: Response, context: Context) {
102
+ if (context.state?.[this.name]?.deprecated) {
103
+ response.headers.set('x-deprecated', 'true')
104
+ }
105
+ }
106
+ }
107
+
108
+ export default new MyPlugin()
109
+ ```
110
+
111
+ ```ts
112
+ // index.ts
113
+
114
+ import { Galbe } from 'galbe'
115
+ import config from './galbe.config'
116
+ import myPlugin from './myPlugin'
117
+
118
+ const galbe = new Galbe(config)
119
+ galbe.use(myPlugin)
120
+
121
+ export default galbe
122
+ ```
123
+
124
+ As you can see in this example, the [Context](context.md#definition) `state` property is used to persist information between plugins interceptor methods. It is a good practice to scope any information stored in the state with the plugin name, as it can also be used by other plugins and hooks to store data in the context.
package/docs/router.md CHANGED
@@ -1 +1,10 @@
1
1
  # Router
2
+
3
+ Galbe router employs a hybrid approach to store and locating routes.
4
+
5
+ The static routes are maintained in a Map structure. This ensure that any incoming request path matching a static route is resolved in a constant time `O(1)`.
6
+
7
+ > [!NOTE]
8
+ > A static route is a route that doesn't contain any parameter (e.g.,`:param`) or wildcards `*`.
9
+
10
+ All other routes are stored in a [Trie](https://en.wikipedia.org/wiki/Trie)-like data structure. The time complexity of the search operation in this case is `O(n)`, where `n` represents the number of segments in the incoming request path.
package/docs/routes.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Routes are the entry points for handling client requests in a Galbe application. In this section, we'll cover how to define routes, the various options available for route definitions, and how to use the Automatic Route Analyzer to simplify route setup.
4
4
 
5
- ## Route Definition
5
+ ## Route definition
6
6
 
7
7
  Here is how to define routes in Galbe.
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "galbe",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Fast, lightweight and highly customizable JavaScript web framework based on Bun",
5
5
  "author": "Pierre Caillaud M (https://github.com/pierre-cm)",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -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,35 +1,20 @@
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) => ({
13
- init: galbe.plugins.reduce((l: { name: string; cb: Function }[], p) => {
14
- if (p.init) l.push({ name: p.name, cb: p.init })
15
- return l
16
- }, []),
17
- onFetch: galbe.plugins.reduce((l: Function[], p) => {
18
- if (p.onFetch) l.push(p.onFetch)
19
- return l
20
- }, []),
21
- onRoute: galbe.plugins.reduce((l: Function[], p) => {
22
- if (p.onRoute) l.push(p.onRoute)
23
- return l
24
- }, []),
25
- beforeHandle: galbe.plugins.reduce((l: Function[], p) => {
26
- if (p.beforeHandle) l.push(p.beforeHandle)
27
- return l
28
- }, []),
29
- afterHandle: galbe.plugins.reduce((l: Function[], p) => {
30
- if (p.afterHandle) l.push(p.afterHandle)
31
- return l
32
- }, [])
13
+ init: galbe.plugins.filter(p => p.init),
14
+ onFetch: galbe.plugins.filter(p => p.onFetch),
15
+ onRoute: galbe.plugins.filter(p => p.onRoute),
16
+ beforeHandle: galbe.plugins.filter(p => p.beforeHandle),
17
+ afterHandle: galbe.plugins.filter(p => p.afterHandle)
33
18
  })
34
19
 
35
20
  export default async (galbe: Galbe, port?: number) => {
@@ -37,7 +22,8 @@ export default async (galbe: Galbe, port?: number) => {
37
22
  if (galbe?.config?.basePath && galbe?.config?.basePath[0] !== '/')
38
23
  galbe.config.basePath = `/${galbe?.config?.basePath}`
39
24
  let pluginsCb = setupPluginCallbacks(galbe)
40
- for (const { name, cb } of pluginsCb.init) await cb(galbe?.config?.plugin?.[name], galbe)
25
+ //@ts-ignore
26
+ for (const p of pluginsCb.init) await p.init(galbe?.config?.plugin?.[p.name], galbe)
41
27
 
42
28
  return Bun.serve({
43
29
  port: port || galbe.config?.port || 3000,
@@ -51,8 +37,9 @@ export default async (galbe: Galbe, port?: number) => {
51
37
  body: {},
52
38
  state: {}
53
39
  }
54
- for (const cb of pluginsCb.onFetch) {
55
- const r = await cb(req)
40
+ for (const p of pluginsCb.onFetch) {
41
+ //@ts-ignore
42
+ const r = await p.onFetch(context)
56
43
  if (r) return r
57
44
  }
58
45
  const url = new URL(req.url)
@@ -60,23 +47,26 @@ export default async (galbe: Galbe, port?: number) => {
60
47
  let response: any = ''
61
48
  try {
62
49
  // find route
63
- if (!url.pathname.match(new RegExp(`^${galbe.config?.basePath || ''}`))) throw new NotFoundError()
64
50
  try {
65
51
  route = router.find(req.method, url.pathname)
66
52
  } catch (error) {
67
53
  if (error instanceof RequestError) throw error
68
54
  else throw handleInternalError(error)
69
55
  }
56
+ context.route = route
70
57
 
71
- for (const cb of pluginsCb.onRoute) {
72
- const r = await cb(route)
58
+ for (const p of pluginsCb.onRoute) {
59
+ //@ts-ignore
60
+ const r = await p.onRoute(context)
73
61
  if (r) return r
74
62
  }
75
63
 
76
64
  // parse request
77
65
  const schema = route.schema
78
- let inHeaders = Object.fromEntries(req.headers.entries())
79
- let inQuery = Object.fromEntries(url.searchParams.entries())
66
+ const inHeaders: Record<string, any> = {}
67
+ for (let [k, v] of req.headers) inHeaders[k] = v
68
+ let inQuery: Record<string, any> = {}
69
+ for (let [k, v] of url.searchParams) inQuery[k] = v
80
70
  let inParams = requestPathParser(url.pathname, route.path)
81
71
 
82
72
  context.body = await requestBodyParser(req.body, inHeaders, schema.body)
@@ -109,11 +99,12 @@ export default async (galbe: Galbe, port?: number) => {
109
99
  else throw handleInternalError(error)
110
100
  }
111
101
  if (errors.length) {
112
- throw new RequestError({ status: 400, error: errors.reduce((acc, c) => ({ ...acc, ...c.error }), {}) })
102
+ throw new RequestError({ status: 400, payload: errors.reduce((acc, c) => ({ ...acc, ...c.payload }), {}) })
113
103
  }
114
104
 
115
- for (const cb of pluginsCb.beforeHandle) {
116
- const r = await cb(context)
105
+ for (const p of pluginsCb.beforeHandle) {
106
+ //@ts-ignore
107
+ const r = await p.beforeHandle(context)
117
108
  if (r) return r
118
109
  }
119
110
 
@@ -123,7 +114,7 @@ export default async (galbe: Galbe, port?: number) => {
123
114
  handlerCalled = true
124
115
  return route.handler(context)
125
116
  }
126
- const callChain = route.hooks.map((hook, idx) => ({
117
+ const callChain: { call: () => any }[] = route.hooks.map((hook, idx) => ({
127
118
  call: async () => {
128
119
  let nextCalled = false
129
120
  let next = async () => {
@@ -133,7 +124,8 @@ export default async (galbe: Galbe, port?: number) => {
133
124
  await callChain[idx + 1].call()
134
125
  }
135
126
  }
136
- await hook(context, next)
127
+ let r = await hook(context, next)
128
+ if (r) return r
137
129
  if (!nextCalled && !handlerCalled) await next()
138
130
  }
139
131
  }))
@@ -143,34 +135,45 @@ export default async (galbe: Galbe, port?: number) => {
143
135
  context.set.status = response instanceof Response ? response.status : 200
144
136
  }
145
137
  })
146
- if (callChain.length > 1) await callChain[0].call()
147
- else response = await handlerWrapper(context)
138
+ if (callChain.length > 1) {
139
+ let r = await callChain[0].call()
140
+ if (r) response = r
141
+ } else response = await handlerWrapper(context)
142
+
148
143
  const parsedResponse = responseParser(response, context)
149
144
 
150
- for (const cb of pluginsCb.afterHandle) {
151
- const r = await cb(parsedResponse)
145
+ for (const p of pluginsCb.afterHandle) {
146
+ //@ts-ignore
147
+ const r = await p.afterHandle(parsedResponse, context)
152
148
  if (r) return r
153
149
  }
154
150
 
155
151
  return parsedResponse
156
152
  } catch (error) {
157
153
  context.set.status = error instanceof RequestError ? error.status : 500
158
- if (galbe.errorHandler) return galbe.errorHandler(error, context)
154
+ let customError
155
+ if (galbe.errorHandler) customError = responseParser(galbe.errorHandler(error, context), context)
156
+ if (customError) return customError
159
157
  if (error instanceof RequestError) {
160
- return new Response(JSON.stringify(error.error), {
158
+ return new Response(JSON.stringify(error.payload), {
161
159
  status: error.status,
162
160
  headers: { 'Content-Type': 'application/json' }
163
161
  })
164
162
  }
165
- return new Response('Internal Server Error', { status: 500 })
163
+ return new Response('"Internal Server Error"', {
164
+ status: 500,
165
+ headers: {
166
+ 'content-type': 'application/json'
167
+ }
168
+ })
166
169
  }
167
170
  },
168
171
  error(error) {
169
172
  console.error(error)
170
- return new Response('Internal Server Error', {
173
+ return new Response('"Internal Server Error"', {
171
174
  status: 500,
172
175
  headers: {
173
- 'Content-Type': 'text/plain'
176
+ 'content-type': 'application/json'
174
177
  }
175
178
  })
176
179
  }
package/src/types.ts CHANGED
@@ -72,6 +72,7 @@ export type GalbeConfig = {
72
72
  basePath?: string
73
73
  server?: Exclude<ServeOptions, 'port'> | TLSServeOptions
74
74
  routes?: boolean | string | string[]
75
+ router?: { cacheEnabled: boolean }
75
76
  plugin?: Record<string, any>
76
77
  }
77
78
  /**
@@ -123,13 +124,13 @@ export type Context<Path extends string = string, S extends RequestSchema = Requ
123
124
  query: Static<STObject<Exclude<S['query'], undefined>>>
124
125
  body: Static<Exclude<S['body'], undefined>>
125
126
  request: Request
127
+ route?: Route
126
128
  state: Record<string, any>
127
129
  set: {
128
130
  headers: {
129
131
  [header: string]: string
130
132
  }
131
133
  status?: number
132
- redirect?: string
133
134
  }
134
135
  }
135
136
  export type Next = () => void | Promise<void>
@@ -146,7 +147,7 @@ export type Endpoint = {
146
147
  H extends STHeaders,
147
148
  P extends Partial<STParams<Path>>,
148
149
  Q extends STQuery,
149
- B extends STBody = STObject
150
+ B extends STBody = any
150
151
  >(
151
152
  path: Path,
152
153
  schema: RequestSchema<Path, H, P, Q, B>,
@@ -158,7 +159,7 @@ export type Endpoint = {
158
159
  H extends STHeaders,
159
160
  P extends Partial<STParams<Path>>,
160
161
  Q extends STQuery,
161
- B extends STBody = STObject
162
+ B extends STBody = any
162
163
  >(
163
164
  path: Path,
164
165
  schema: RequestSchema<Path, H, P, Q, B>,
@@ -169,7 +170,7 @@ export type Endpoint = {
169
170
  H extends STHeaders,
170
171
  P extends Partial<STParams<Path>>,
171
172
  Q extends STQuery,
172
- B extends STBody = STObject
173
+ B extends STBody = any
173
174
  >(
174
175
  path: Path,
175
176
  hooks: Hook<Path, RequestSchema<Path, H, P, Q, B>>[],
@@ -180,7 +181,7 @@ export type Endpoint = {
180
181
  H extends STHeaders,
181
182
  P extends Partial<STParams<Path>>,
182
183
  Q extends STQuery,
183
- B extends STBody = STObject
184
+ B extends STBody = any
184
185
  >(
185
186
  path: Path,
186
187
  handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
@@ -189,10 +190,10 @@ export type Endpoint = {
189
190
 
190
191
  export class RequestError {
191
192
  status: number
192
- error: any
193
- constructor(options: { status?: number; error?: any }) {
193
+ payload: any
194
+ constructor(options: { status?: number; payload?: any }) {
194
195
  this.status = options.status ?? 500
195
- this.error = options.error ?? 'Internal server error'
196
+ this.payload = options.payload ?? 'Internal server error'
196
197
  }
197
198
  }
198
199
 
@@ -225,7 +226,13 @@ export type RouteTree = {
225
226
 
226
227
  export class NotFoundError extends RequestError {
227
228
  constructor(message?: string) {
228
- super({ status: 404, error: message ?? 'Not found' })
229
+ super({ status: 404, payload: message ?? 'Not found' })
230
+ }
231
+ }
232
+
233
+ export class InternalError extends RequestError {
234
+ constructor(message?: string) {
235
+ super({ status: 500, payload: message ?? 'Internal Server Error' })
229
236
  }
230
237
  }
231
238
 
@@ -253,8 +260,8 @@ export class NotFoundError extends RequestError {
253
260
  export type GalbePlugin = {
254
261
  name: string
255
262
  init?: (config: any, galbe: Galbe) => MaybePromise<void>
256
- onFetch?: (request: Request) => MaybePromise<Response | void>
257
- onRoute?: (route: Route) => MaybePromise<Response | void>
263
+ onFetch?: (context: Context) => MaybePromise<Response | void>
264
+ onRoute?: (context: Context) => MaybePromise<Response | void>
258
265
  beforeHandle?: (context: Context) => MaybePromise<Response | void>
259
- afterHandle?: (response: Response) => MaybePromise<Response | void>
266
+ afterHandle?: (response: Response, context: Context) => MaybePromise<Response | void>
260
267
  }
@@ -174,4 +174,27 @@ describe('hooks', async () => {
174
174
  expect(resp.status).toBe(200)
175
175
  expect(await resp?.text()).toBe('handled')
176
176
  })
177
+
178
+ test('hooks, early response', async () => {
179
+ let hook = 0
180
+ galbe.get(
181
+ '/hooks',
182
+ [
183
+ async _ => {
184
+ hook++
185
+ return 'hook'
186
+ }
187
+ ],
188
+ () => {
189
+ expect.unreachable()
190
+ }
191
+ )
192
+ expect(hook).toBe(0)
193
+ let resp = await fetch(`http://localhost:${port}/hooks`, {
194
+ method: 'GET'
195
+ })
196
+ expect(hook).toBe(1)
197
+ expect(resp.status).toBe(200)
198
+ expect(await resp?.text()).toBe('hook')
199
+ })
177
200
  })
@@ -39,8 +39,8 @@ describe('plugins', async () => {
39
39
  let request: any = null
40
40
  const plugin: GalbePlugin = {
41
41
  name: 'dev.galbe.test.init',
42
- onFetch: mock(req => {
43
- request = req
42
+ onFetch: mock(ctx => {
43
+ request = ctx.request
44
44
  })
45
45
  }
46
46
 
@@ -86,7 +86,7 @@ describe('plugins', async () => {
86
86
  const plugin: GalbePlugin = {
87
87
  name: 'dev.galbe.test.init',
88
88
  onRoute: mock(r => {
89
- route = r
89
+ route = r.route
90
90
  })
91
91
  }
92
92
 
@@ -141,7 +141,7 @@ describe('router', () => {
141
141
  } catch (err: any) {
142
142
  expect(err).toBeInstanceOf(NotFoundError)
143
143
  expect(err.status).toBe(404)
144
- expect(err.error).toBe('Not found')
144
+ expect(err.payload).toBe('Not found')
145
145
  }
146
146
 
147
147
  try {
@@ -150,7 +150,7 @@ describe('router', () => {
150
150
  } catch (err: any) {
151
151
  expect(err).toBeInstanceOf(NotFoundError)
152
152
  expect(err.status).toBe(404)
153
- expect(err.error).toBe('Not found')
153
+ expect(err.payload).toBe('Not found')
154
154
  }
155
155
  })
156
156
 
@@ -188,10 +188,6 @@ describe('router', () => {
188
188
  expect(r1.path).toBe('/test/foo/*')
189
189
  expect(r1.handler).toBe(h1)
190
190
 
191
- let r2 = router.find('GET', '/test//foo/bar')
192
- expect(r2.path).toBe('/test/foo/bar')
193
- expect(r2.handler).toBe(h2)
194
-
195
191
  let r3 = router.find('GET', '/test/foo/bar/bar')
196
192
  expect(r3.path).toBe('/test/foo/:p/bar')
197
193
  expect(r3.handler).toBe(h3)