galbe 0.8.0 → 0.9.1
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/bin/commands/build.ts +42 -25
- package/bin/commands/dev.ts +8 -6
- package/bin/commands/generate/client.ts +26 -15
- package/bin/commands/generate/code/openapi.parser.ts +92 -57
- package/bin/commands/generate/code.ts +14 -13
- package/bin/commands/generate/spec.ts +2 -2
- package/bin/res/cli.template.js +13 -13
- package/bin/res/client.template.ts +26 -16
- package/bin/util.ts +11 -8
- package/bun.lock +904 -0
- package/docs/error-handler.md +24 -13
- package/docs/getting-started.md +72 -37
- package/docs/routes.md +3 -0
- package/package.json +2 -1
- package/src/extras/spec/openapi.serializer.ts +27 -21
- package/src/index.ts +44 -4
- package/src/parser.ts +18 -13
- package/src/routes.ts +96 -91
- package/src/server.ts +2 -2
- package/src/types.ts +32 -15
- package/src/util.ts +21 -7
- package/src/validator.ts +17 -16
- package/test/parser.test.ts +38 -38
- package/test/plugins.test.ts +1 -1
- package/test/requests.test.ts +8 -5
- package/test/responses.test.ts +3 -2
- package/test/routeFiles.test.ts +6 -6
- package/test/router.test.ts +2 -2
- package/tsconfig.json +1 -1
- package/bun.lockb +0 -0
package/docs/error-handler.md
CHANGED
|
@@ -1,38 +1,49 @@
|
|
|
1
1
|
# Error handler
|
|
2
2
|
|
|
3
|
-
Any error happening during a request lifecycle will be intercepted by the error
|
|
3
|
+
Any error happening during a request lifecycle will be intercepted by the error
|
|
4
|
+
handler.
|
|
4
5
|
|
|
5
|
-
You can customize the default error handling behavior by defining a custom error
|
|
6
|
+
You can customize the default error handling behavior by defining a custom error
|
|
7
|
+
handler using Galbe's intance `onError` method.
|
|
6
8
|
|
|
7
9
|
```js
|
|
8
|
-
const galbe = new Galbe()
|
|
9
|
-
galbe.onError(customErrorHandler)
|
|
10
|
+
const galbe = new Galbe();
|
|
11
|
+
galbe.onError(customErrorHandler);
|
|
10
12
|
```
|
|
11
13
|
|
|
12
14
|
## Definition
|
|
13
15
|
|
|
14
|
-
The error handler should be a function that takes two aguments: an
|
|
16
|
+
The error handler should be a function that takes two aguments: an
|
|
17
|
+
[Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)
|
|
18
|
+
and a [Context](context.md). This function may potentially return a
|
|
19
|
+
[Response type](handler.md#response-types).
|
|
15
20
|
|
|
16
21
|
```js
|
|
17
|
-
galbe.
|
|
22
|
+
galbe.onError((error, ctx) => {
|
|
18
23
|
if (error.status === 500) {
|
|
19
|
-
return new Response(`Server error ❌`, { status: 500 })
|
|
24
|
+
return new Response(`Server error ❌`, { status: 500 });
|
|
20
25
|
}
|
|
21
26
|
if (error.status === 404) {
|
|
22
|
-
return new Response(`Not found 🔎`, { status: 404 })
|
|
27
|
+
return new Response(`Not found 🔎`, { status: 404 });
|
|
23
28
|
}
|
|
24
|
-
})
|
|
29
|
+
});
|
|
25
30
|
```
|
|
26
31
|
|
|
27
|
-
The `error` argument could be any type of error thrown by your application. If
|
|
32
|
+
The `error` argument could be any type of error thrown by your application. If
|
|
33
|
+
the error originates from Galbe framework, it will be an instance of
|
|
34
|
+
[RequestError](#request-error).
|
|
28
35
|
|
|
29
|
-
For instance, the [Router](router.md) will throw a `RequestError` with a `404`
|
|
36
|
+
For instance, the [Router](router.md) will throw a `RequestError` with a `404`
|
|
37
|
+
status if no route matches the incoming request path. Similarly, the Parser will
|
|
38
|
+
throw a `RequestError` with a `400` status.
|
|
30
39
|
|
|
31
40
|
## Request Error
|
|
32
41
|
|
|
33
|
-
The `RequestError` class is utilized to instanciate a runtime request error in
|
|
42
|
+
The `RequestError` class is utilized to instanciate a runtime request error in
|
|
43
|
+
Galbe. It has two optional attributes: a `status` and a `payload`.
|
|
34
44
|
|
|
35
|
-
If your application throws a `RequestError` instance, Galbe will, by default,
|
|
45
|
+
If your application throws a `RequestError` instance, Galbe will, by default,
|
|
46
|
+
construct a Response from your `RequestError` and send it back to the client.
|
|
36
47
|
|
|
37
48
|
```js
|
|
38
49
|
import { Galbe, RequestError } from 'galbe'
|
package/docs/getting-started.md
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# Getting started
|
|
2
2
|
|
|
3
|
-
Galbe is a Javascript web framework for building fast and versatile backend
|
|
3
|
+
Galbe is a Javascript web framework for building fast and versatile backend
|
|
4
|
+
servers with Bun.
|
|
4
5
|
|
|
5
|
-
Designed with simplicity in mind, Galbe allows you to quickly create and set up
|
|
6
|
+
Designed with simplicity in mind, Galbe allows you to quickly create and set up
|
|
7
|
+
a project. In addition to its ease of use, Galbe also offers a range of useful
|
|
8
|
+
features that help you focus on the core logic of your application.
|
|
6
9
|
|
|
7
10
|
## Requirements
|
|
8
11
|
|
|
9
|
-
To start developing your Galbe project, you first need to install
|
|
12
|
+
To start developing your Galbe project, you first need to install
|
|
13
|
+
[Bun](https://bun.sh).
|
|
10
14
|
|
|
11
15
|
## Automatic installation
|
|
12
16
|
|
|
@@ -16,7 +20,9 @@ This is the recommended way of setting up a Galbe project.
|
|
|
16
20
|
$ bun create galbe app
|
|
17
21
|
```
|
|
18
22
|
|
|
19
|
-
The Galbe starter CLI will request you to chose a template and a target language
|
|
23
|
+
The Galbe starter CLI will request you to chose a template and a target language
|
|
24
|
+
for your project. Let's select `hello` as template and `ts` as language. This
|
|
25
|
+
will create a new project under `app` directory.
|
|
20
26
|
|
|
21
27
|
Now you can navigate to your newly created project and install it:
|
|
22
28
|
|
|
@@ -46,8 +52,9 @@ $ curl localhost:3000/hello/John?age=32
|
|
|
46
52
|
Hello John! You're 32 y.o.
|
|
47
53
|
```
|
|
48
54
|
|
|
49
|
-
> [!TIP]
|
|
50
|
-
> If you want to have a more complete view of Galbe capabilities, feel free to
|
|
55
|
+
> [!TIP]
|
|
56
|
+
> If you want to have a more complete view of Galbe capabilities, feel free to
|
|
57
|
+
> take a look at the `demo` template from the Galbe starter CLI.
|
|
51
58
|
|
|
52
59
|
## Manual installation
|
|
53
60
|
|
|
@@ -70,30 +77,37 @@ Open `package.json` file and add the following scripts:
|
|
|
70
77
|
}
|
|
71
78
|
```
|
|
72
79
|
|
|
73
|
-
As you can see, those scripts rely on Galbe CLI to run and build the
|
|
80
|
+
As you can see, those scripts rely on Galbe CLI to run and build the
|
|
81
|
+
application. You will find more info about it on the [CLI](cli.md) page.
|
|
74
82
|
|
|
75
|
-
This require your `index.ts` to export a default Galbe instance in order to
|
|
83
|
+
This require your `index.ts` to export a default Galbe instance in order to
|
|
84
|
+
work. As in the following example:
|
|
76
85
|
|
|
77
86
|
```ts
|
|
78
|
-
import { Galbe } from
|
|
87
|
+
import { Galbe } from "galbe";
|
|
79
88
|
|
|
80
|
-
const galbe = new Galbe({ port: 3000 })
|
|
81
|
-
galbe.get(
|
|
89
|
+
const galbe = new Galbe({ port: 3000 });
|
|
90
|
+
galbe.get("/hello", () => "Hello Mom!");
|
|
82
91
|
|
|
83
|
-
export default galbe
|
|
92
|
+
export default galbe;
|
|
84
93
|
```
|
|
85
94
|
|
|
86
|
-
This is the recommended way to proceed but it is not mandatory. Galbe instances
|
|
95
|
+
This is the recommended way to proceed but it is not mandatory. Galbe instances
|
|
96
|
+
also provide a `listen` method that will allow you to manually start your server
|
|
97
|
+
instance from the code.
|
|
87
98
|
|
|
88
99
|
> [!WARNING]
|
|
89
|
-
> In the case you decide to not rely on Galbe CLI to run/build your app, you
|
|
100
|
+
> In the case you decide to not rely on Galbe CLI to run/build your app, you
|
|
101
|
+
> will not have access to
|
|
102
|
+
> [Automatic Route Analyzer](routes.md#automatic-route-analyzer) feature.
|
|
90
103
|
|
|
91
104
|
## Configuration
|
|
92
105
|
|
|
93
|
-
To configure your Galbe server, you should pass your configuration to the Galbe
|
|
106
|
+
To configure your Galbe server, you should pass your configuration to the Galbe
|
|
107
|
+
constructor when you instanciate it.
|
|
94
108
|
|
|
95
109
|
```ts
|
|
96
|
-
const galbe = new Galbe(configuration)
|
|
110
|
+
const galbe = new Galbe(configuration);
|
|
97
111
|
```
|
|
98
112
|
|
|
99
113
|
### Properties
|
|
@@ -112,11 +126,14 @@ The base path is added as a prefix to all the routes created.
|
|
|
112
126
|
|
|
113
127
|
**routes**
|
|
114
128
|
|
|
115
|
-
A Glob Pattern or a list of Glob patterns defining the route files to be
|
|
129
|
+
A Glob Pattern or a list of Glob patterns defining the route files to be
|
|
130
|
+
analyzed by the [Automatic Route Analyzer](routes.md#automatic-route-analyzer).
|
|
131
|
+
Default is `src/**/*.route.{js,ts}`.
|
|
116
132
|
|
|
117
133
|
**plugin**
|
|
118
134
|
|
|
119
|
-
A property that can be used by plugins to add plugin's specific configuration.
|
|
135
|
+
A property that can be used by plugins to add plugin's specific configuration.
|
|
136
|
+
Every key should correspond to a [Unique Plugin Identifier](plugins.md).
|
|
120
137
|
|
|
121
138
|
**tls**
|
|
122
139
|
|
|
@@ -130,15 +147,21 @@ Enable or disable TLS support. Default value is `false`.
|
|
|
130
147
|
|
|
131
148
|
**requestValidator.enabled**
|
|
132
149
|
|
|
133
|
-
Enable or disable the _request_ schema validation (See
|
|
150
|
+
Enable or disable the _request_ schema validation (See
|
|
151
|
+
[Request Schema definition](schemas.md#request-schema-definition)). Default
|
|
152
|
+
value is `true`.
|
|
134
153
|
|
|
135
154
|
**responseValidator.enabled**
|
|
136
155
|
|
|
137
|
-
Enable or disable the _response_ schema validation (See
|
|
156
|
+
Enable or disable the _response_ schema validation (See
|
|
157
|
+
[Request Schema definition](schemas.md#request-schema-definition)). Default
|
|
158
|
+
value is `true`.
|
|
138
159
|
|
|
139
160
|
### Examples
|
|
140
161
|
|
|
141
|
-
A common way to handle server configuration is to create new file
|
|
162
|
+
A common way to handle server configuration is to create new file
|
|
163
|
+
`galbe.config.(js|ts|json)` at the root of your project directory and import it
|
|
164
|
+
in your code. Here is an example:
|
|
142
165
|
|
|
143
166
|
galbe.config.js
|
|
144
167
|
|
|
@@ -152,27 +175,31 @@ export default {
|
|
|
152
175
|
index.js
|
|
153
176
|
|
|
154
177
|
```js
|
|
155
|
-
import { Galbe } from
|
|
156
|
-
import config from
|
|
178
|
+
import { Galbe } from "galbe";
|
|
179
|
+
import config from "./galbe.config";
|
|
157
180
|
|
|
158
|
-
export default new Galbe(config)
|
|
181
|
+
export default new Galbe(config);
|
|
159
182
|
```
|
|
160
183
|
|
|
161
|
-
> [!TIP]
|
|
162
|
-
> If you are using Typescript, you can import `GalbeConfig` type from galbe
|
|
184
|
+
> [!TIP]
|
|
185
|
+
> If you are using Typescript, you can import `GalbeConfig` type from galbe
|
|
186
|
+
> package to ensure type consistency for your configuration. Here is an example:
|
|
163
187
|
>
|
|
164
188
|
> ```ts
|
|
165
|
-
> import type { GalbeConfig } from
|
|
189
|
+
> import type { GalbeConfig } from "galbe";
|
|
166
190
|
> const config: GalbeConfig = {
|
|
167
191
|
> port: Number(Bun.env.GALBE_PORT),
|
|
168
|
-
> routes:
|
|
169
|
-
> }
|
|
170
|
-
> export default config
|
|
192
|
+
> routes: "routes/*.route.ts",
|
|
193
|
+
> };
|
|
194
|
+
> export default config;
|
|
171
195
|
> ```
|
|
172
196
|
|
|
173
197
|
## Project structure
|
|
174
198
|
|
|
175
|
-
One key aspect of Galbe, is its versatility in terms of project structure. This
|
|
199
|
+
One key aspect of Galbe, is its versatility in terms of project structure. This
|
|
200
|
+
is partly allowed by the
|
|
201
|
+
[Automatic Route Analyzer](routes.md#automatic-route-analyzer) and the `routes`
|
|
202
|
+
config property which defaults to `src/**/*.route.{js,ts}`.
|
|
176
203
|
|
|
177
204
|
Here are two examples of valid project structures by default:
|
|
178
205
|
|
|
@@ -214,18 +241,26 @@ Here are two examples of valid project structures by default:
|
|
|
214
241
|
└── tsconfig.json
|
|
215
242
|
```
|
|
216
243
|
|
|
217
|
-
In both cases, the
|
|
244
|
+
In both cases, the
|
|
245
|
+
[Automatic Route Analyzer](routes.md#automatic-route-analyzer) will analyze
|
|
246
|
+
`foo.route.ts` and `bar.route.ts` Route Files to find route definitions.
|
|
218
247
|
|
|
219
|
-
You can find more info about Route Files definition in the
|
|
248
|
+
You can find more info about Route Files definition in the
|
|
249
|
+
[Routes Files](routes.md#route-files) section.
|
|
220
250
|
|
|
221
251
|
> [!NOTE]
|
|
222
|
-
> The examples provided above will work with the default configuration, but you
|
|
252
|
+
> The examples provided above will work with the default configuration, but you
|
|
253
|
+
> can easily customize the routes property to fit your own project structure.
|
|
254
|
+
> Simply redefine the `routes` property with your own pattern(s) to to fit your
|
|
255
|
+
> own project structure.
|
|
223
256
|
|
|
224
257
|
## How to debug
|
|
225
258
|
|
|
226
|
-
The easiest way to debug your app is by installing the
|
|
259
|
+
The easiest way to debug your app is by installing the
|
|
260
|
+
[VSCode Bun extension](https://marketplace.visualstudio.com/items?itemName=oven.bun-vscode).
|
|
227
261
|
|
|
228
|
-
You can then create a `.vscode/launch.json` config file in your project root
|
|
262
|
+
You can then create a `.vscode/launch.json` config file in your project root
|
|
263
|
+
directory. Here is an example of configuration:
|
|
229
264
|
|
|
230
265
|
```json
|
|
231
266
|
{
|
|
@@ -239,7 +274,7 @@ You can then create a `.vscode/launch.json` config file in your project root dir
|
|
|
239
274
|
"env": { "TERM": "xterm" },
|
|
240
275
|
"cwd": "${workspaceFolder}",
|
|
241
276
|
"runtime": "bun",
|
|
242
|
-
"runtimeArgs": ["dev", "index.ts", "
|
|
277
|
+
"runtimeArgs": ["dev", "index.ts", "-w", "."]
|
|
243
278
|
}
|
|
244
279
|
]
|
|
245
280
|
}
|
package/docs/routes.md
CHANGED
|
@@ -111,3 +111,6 @@ export default g => {
|
|
|
111
111
|
g.get('/foo/:bar', ctx => ctx.params.bar)
|
|
112
112
|
}
|
|
113
113
|
```
|
|
114
|
+
|
|
115
|
+
> [!TIP]
|
|
116
|
+
> You can ignore a specific route from being analyzed by adding a `//@galbe-ignore` comment before the route definition. This is useful if you want to exclude certain routes from automatic analysis or documentation generation.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "galbe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
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",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"scripts": {
|
|
31
31
|
"test": "bun test",
|
|
32
|
+
"typecheck": "tsc --noEmit --emitDeclarationOnly false",
|
|
32
33
|
"postinstall": "bun run ./scripts/postinstall.ts",
|
|
33
34
|
"release": "release-it"
|
|
34
35
|
},
|
|
@@ -12,10 +12,10 @@ const schemaToMedia = ({ type, format, isJson }: SchemaType) =>
|
|
|
12
12
|
isJson || (type && ['object', 'number', 'boolean', 'array'].includes(type))
|
|
13
13
|
? 'application/json'
|
|
14
14
|
: format === 'byte'
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
? 'application/octet-stream'
|
|
16
|
+
: type === 'string'
|
|
17
|
+
? 'text/plain'
|
|
18
|
+
: '*/*'
|
|
19
19
|
|
|
20
20
|
export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<OpenAPIV3.Document> => {
|
|
21
21
|
let paths: any = {}
|
|
@@ -115,9 +115,9 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
115
115
|
type: type,
|
|
116
116
|
...(type === 'object'
|
|
117
117
|
? {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
properties: Object.fromEntries(Object.entries(props).map(([k, v]) => [k, schemaToOpenapi(v).schema])),
|
|
119
|
+
...(required.length ? { required } : {})
|
|
120
|
+
}
|
|
121
121
|
: {})
|
|
122
122
|
}
|
|
123
123
|
} else if (kind === 'union') {
|
|
@@ -178,8 +178,13 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
178
178
|
(routes, c) => ({ ...routes, ...c.routes }),
|
|
179
179
|
{} as Record<string, Record<string, Record<string, any>>>
|
|
180
180
|
)
|
|
181
|
+
let metaStatic = Object.fromEntries(Object.entries(metaRoutes || {}).filter((([_, d]) => d?.static)))
|
|
182
|
+
|
|
181
183
|
walkRoutes(g.router.routes, r => {
|
|
182
184
|
let meta = metaRoutes?.[r.path]?.[r.method]
|
|
185
|
+
if (r.static?.root)
|
|
186
|
+
meta = metaStatic[r.static?.root]?.static
|
|
187
|
+
if (meta?.hide) return
|
|
183
188
|
let path = r.path.replaceAll(/:([^\/]+)/g, '{$1}')
|
|
184
189
|
if (!(path in paths)) paths[path] = {}
|
|
185
190
|
let tags = [
|
|
@@ -196,19 +201,19 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
196
201
|
: []
|
|
197
202
|
let headerParam = r.schema?.headers
|
|
198
203
|
? Object.entries(r.schema?.headers as Record<string, STSchema>)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
204
|
+
.map(([k, v]) => {
|
|
205
|
+
let p = parseParam(k, v, 'header')
|
|
206
|
+
if (k.match(/authorization/i)) {
|
|
207
|
+
// TODO: handle other auth methods
|
|
208
|
+
if (v.pattern && v?.pattern?.toString() === '/^Bearer /') {
|
|
209
|
+
security.push({ bearerAuth: [] })
|
|
210
|
+
components.securitySchemes = { bearerAuth: { type: 'http', scheme: 'bearer' } }
|
|
211
|
+
return null
|
|
208
212
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
213
|
+
}
|
|
214
|
+
return p
|
|
215
|
+
})
|
|
216
|
+
.filter(p => p)
|
|
212
217
|
: []
|
|
213
218
|
// TODO cookieParam
|
|
214
219
|
let parameters = [...pathParam, ...queryParam, ...headerParam]
|
|
@@ -234,6 +239,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
234
239
|
if (r.schema.response && Object.keys(r.schema.response).length) {
|
|
235
240
|
responses = Object.fromEntries(
|
|
236
241
|
Object.entries(r.schema.response).map(([status, v]) => {
|
|
242
|
+
if(!v) return []
|
|
237
243
|
let s = Number(status) as keyof typeof HttpStatus
|
|
238
244
|
let { schema, isJson } = schemaToOpenapi(v)
|
|
239
245
|
let { type, format } = resolveRef(schema)
|
|
@@ -242,8 +248,8 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
242
248
|
description: v.description || HttpStatus[Number(s) as keyof typeof HttpStatus] || 'Response',
|
|
243
249
|
content: { [media]: { schema: schema } }
|
|
244
250
|
}
|
|
245
|
-
if (components.responses && r.schema.response?.[s]
|
|
246
|
-
components.responses[r.schema.response?.[s]
|
|
251
|
+
if (components.responses && r.schema.response?.[s]?.id) {
|
|
252
|
+
components.responses[r.schema.response?.[s]?.id as string] = response
|
|
247
253
|
//@ts-ignore
|
|
248
254
|
response = { $ref: `#/components/responses/${r.schema.response?.[s].id}` }
|
|
249
255
|
}
|
package/src/index.ts
CHANGED
|
@@ -14,9 +14,14 @@ import type {
|
|
|
14
14
|
STResponse,
|
|
15
15
|
STParams,
|
|
16
16
|
STHeaders,
|
|
17
|
-
STQuery
|
|
17
|
+
STQuery,
|
|
18
|
+
StaticEndpoint,
|
|
19
|
+
Route,
|
|
20
|
+
StaticEndpointOptions
|
|
18
21
|
} from './types'
|
|
19
22
|
|
|
23
|
+
import { readdirSync, statSync } from 'fs'
|
|
24
|
+
import { resolve as resolvePath } from 'path'
|
|
20
25
|
import server from './server'
|
|
21
26
|
import { GalbeRouter } from './router'
|
|
22
27
|
import { SchemaType, type STObject, type Static } from './schema'
|
|
@@ -41,7 +46,7 @@ const overloadDiscriminer = <
|
|
|
41
46
|
| Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[]
|
|
42
47
|
| Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>,
|
|
43
48
|
arg4?: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
44
|
-
) => {
|
|
49
|
+
): Route<M, Path, P, H, Q, B, R> => {
|
|
45
50
|
const defaultSchema = {}
|
|
46
51
|
if (typeof arg2 === 'function') {
|
|
47
52
|
return galbeMethod(galbe, method, path, defaultSchema, undefined, arg2)
|
|
@@ -70,7 +75,7 @@ const galbeMethod = <
|
|
|
70
75
|
schema: RequestSchema<M, Path, H, P, Q, B, R> | undefined,
|
|
71
76
|
hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[] | undefined,
|
|
72
77
|
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
73
|
-
) => {
|
|
78
|
+
): Route<M, Path, P, H, Q, B, R> => {
|
|
74
79
|
schema = schema ?? {}
|
|
75
80
|
hooks = hooks || []
|
|
76
81
|
const context: Context<M, Path, typeof schema> = {
|
|
@@ -85,7 +90,8 @@ const galbeMethod = <
|
|
|
85
90
|
state: {},
|
|
86
91
|
set: {} as {
|
|
87
92
|
headers: {
|
|
88
|
-
|
|
93
|
+
'set-cookie': string[]
|
|
94
|
+
[header: string]: string | string[]
|
|
89
95
|
}
|
|
90
96
|
status?: number
|
|
91
97
|
}
|
|
@@ -306,6 +312,40 @@ export class Galbe {
|
|
|
306
312
|
| Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>,
|
|
307
313
|
arg4?: Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>
|
|
308
314
|
) => this.add(overloadDiscriminer(this, 'head', path, arg2, arg3, arg4))
|
|
315
|
+
static: StaticEndpoint = (path: string, target: string, options?: StaticEndpointOptions) => {
|
|
316
|
+
let { resolve } = options ?? {}
|
|
317
|
+
const rootPath = path
|
|
318
|
+
|
|
319
|
+
const walkStatic = (path: string, target: string) => {
|
|
320
|
+
path = path?.[0] === '/' ? path : `/${path}`
|
|
321
|
+
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
|
322
|
+
|
|
323
|
+
let t = target
|
|
324
|
+
if (Bun.env.BUN_ENV === 'production') {
|
|
325
|
+
t = resolvePath(import.meta.dir, `static-${Bun.env.GALBE_BUILD}/${target}`)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!statSync(t).isDirectory()) {
|
|
329
|
+
let ut: string | null | undefined | void = t
|
|
330
|
+
if (path.endsWith('.html')) path = path.slice(0, -5)
|
|
331
|
+
if (resolve) ut = resolve(path, ut)
|
|
332
|
+
if (ut) {
|
|
333
|
+
let handler = () => new Response(Bun.file(ut))
|
|
334
|
+
this.add({ ...galbeMethod(this, 'get', path, {}, undefined, handler), static: { path: ut, root: rootPath } })
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
let root = readdirSync(t)
|
|
338
|
+
for (let f of root) {
|
|
339
|
+
let p = f === 'index.html' ? path : `${path}/${f}`
|
|
340
|
+
walkStatic(p, `${target}/${f}`)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { ...galbeMethod(this, 'get', path, {}, undefined, () => { }), static: { path: t, root: rootPath } }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return walkStatic(path, target)
|
|
348
|
+
}
|
|
309
349
|
}
|
|
310
350
|
|
|
311
351
|
export * from './types'
|
package/src/parser.ts
CHANGED
|
@@ -89,8 +89,8 @@ export const requestBodyParser = async (
|
|
|
89
89
|
let received = contentType?.match(FORM_HEADER_RX)
|
|
90
90
|
? 'urlForm'
|
|
91
91
|
: contentType?.match(MP_HEADER_RX)
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
? 'multipartForm'
|
|
93
|
+
: contentType
|
|
94
94
|
throw new RequestError({ status: 400, payload: { body: `Expected ${kind}, received ${received}` } })
|
|
95
95
|
} else if (!contentType || contentType === BA_HEADER) {
|
|
96
96
|
if (!schema) {
|
|
@@ -377,7 +377,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
|
|
|
377
377
|
payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
|
|
378
378
|
})
|
|
379
379
|
}
|
|
380
|
-
const parseMultipartHeader = (header: string): { name: string;
|
|
380
|
+
const parseMultipartHeader = (header: string): { name: string;[key: string]: string } | null => {
|
|
381
381
|
if (!header) return null
|
|
382
382
|
let disposition = 'form-data'
|
|
383
383
|
const multipartHeader = [
|
|
@@ -537,24 +537,24 @@ const paramParser = (
|
|
|
537
537
|
if (typeof value === 'boolean') return value
|
|
538
538
|
if (value === 'true') return true
|
|
539
539
|
if (value === 'false') return false
|
|
540
|
-
else throw
|
|
540
|
+
else throw `Not a valid boolean. Should be 'true' or 'false'`
|
|
541
541
|
} else if (type[Kind] === 'integer') {
|
|
542
|
-
if (value === null || value === undefined) throw
|
|
542
|
+
if (value === null || value === undefined) throw `Not a valid integer`
|
|
543
543
|
const parsedValue = parseInt(value, 10)
|
|
544
|
-
if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw
|
|
544
|
+
if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `Not a valid integer`
|
|
545
545
|
validate(parsedValue, type)
|
|
546
546
|
return parsedValue
|
|
547
547
|
} else if (type[Kind] === 'number') {
|
|
548
|
-
if (value === null || value === undefined) throw
|
|
548
|
+
if (value === null || value === undefined) throw `Not a valid number`
|
|
549
549
|
const parsedValue = Number(value)
|
|
550
|
-
if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw
|
|
550
|
+
if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `Not a valid number`
|
|
551
551
|
validate(parsedValue, type)
|
|
552
552
|
return parsedValue
|
|
553
553
|
} else if (type[Kind] === 'string') {
|
|
554
554
|
validate(value, type)
|
|
555
555
|
return value
|
|
556
556
|
} else if (type[Kind] === 'literal') {
|
|
557
|
-
if (value !== type.value) throw
|
|
557
|
+
if (value !== type.value) throw `Not a valid value`
|
|
558
558
|
return value
|
|
559
559
|
} else if (type[Kind] === 'array') {
|
|
560
560
|
return [paramParser(value, type.items as STMultipartFormValues) as Static<STUrlFormValues>]
|
|
@@ -569,9 +569,9 @@ const paramParser = (
|
|
|
569
569
|
continue
|
|
570
570
|
}
|
|
571
571
|
}
|
|
572
|
-
throw
|
|
572
|
+
throw `Could not be parsed to any of [${union
|
|
573
573
|
.map(u => (u as STLiteral)?.value ?? (u as STSchema)[Kind])
|
|
574
|
-
.join(', ')}`
|
|
574
|
+
.join(', ')}]`
|
|
575
575
|
} else if (type[Kind] === 'any') {
|
|
576
576
|
return value
|
|
577
577
|
}
|
|
@@ -645,12 +645,17 @@ export const parseEntry = <T extends STProps>(
|
|
|
645
645
|
export const responseParser = (response: any, ctx: Context, schema?: STResponse) => {
|
|
646
646
|
const details = {
|
|
647
647
|
status: ctx.set.status || 200,
|
|
648
|
-
headers: new Headers(
|
|
648
|
+
headers: new Headers()
|
|
649
|
+
}
|
|
650
|
+
for (const [key, value] of Object.entries(ctx.set.headers)) {
|
|
651
|
+
if (Array.isArray(value)) {
|
|
652
|
+
value.forEach(v => details.headers.append(key, v))
|
|
653
|
+
} else details.headers.set(key, value)
|
|
649
654
|
}
|
|
650
655
|
if (response instanceof Response) return response
|
|
651
656
|
else if (typeof response === 'string') {
|
|
652
657
|
if (!details?.headers?.has('content-type')) {
|
|
653
|
-
if (schema?.[details.status][Kind] === 'json') {
|
|
658
|
+
if (schema?.[details.status]?.[Kind] === 'json') {
|
|
654
659
|
details?.headers?.set('content-type', 'application/json')
|
|
655
660
|
response = `"${response}"`
|
|
656
661
|
} else details?.headers?.set('content-type', 'text/plain')
|