galbe 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/docs/error-handler.md +24 -13
- package/docs/routes.md +3 -0
- package/package.json +1 -1
- package/src/extras/spec/openapi.serializer.ts +25 -20
- package/src/index.ts +44 -4
- package/src/parser.ts +17 -12
- package/src/routes.ts +96 -91
- package/src/server.ts +2 -2
- package/src/types.ts +36 -19
- package/src/util.ts +21 -7
- package/src/validator.ts +16 -16
- package/test/parser.test.ts +38 -38
- package/test/requests.test.ts +8 -5
- package/test/router.test.ts +2 -2
package/bin/res/cli.template.js
CHANGED
|
@@ -31,17 +31,17 @@ const fmtRes = (r, p = false) => {
|
|
|
31
31
|
if (typeof r === 'object') {
|
|
32
32
|
try {
|
|
33
33
|
let resp = fmtObject(r, p, 2)
|
|
34
|
-
return
|
|
34
|
+
return Bun.write(Bun.stdout, `${resp}\n`)
|
|
35
35
|
} catch (err) {
|
|
36
36
|
console.error(err)
|
|
37
|
-
return
|
|
37
|
+
return Bun.write(Bun.stdout, `${r}\n`)
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
if (!p) return
|
|
41
|
-
if (typeof r === 'boolean') return
|
|
42
|
-
if (typeof r === 'number') return
|
|
43
|
-
if (typeof r === 'string') return
|
|
44
|
-
return
|
|
40
|
+
if (!p) return Bun.write(Bun.stdout, `${r}\n`)
|
|
41
|
+
if (typeof r === 'boolean') return Bun.write(Bun.stdout, `${ansi(p, '38;2;255;128;0', r)}\n`)
|
|
42
|
+
if (typeof r === 'number') return Bun.write(Bun.stdout, `${ansi(p, '38;2;10;180;220', r)}\n`)
|
|
43
|
+
if (typeof r === 'string') return Bun.write(Bun.stdout, `${ansi(p, '38;2;125;170;0', r)}\n`)
|
|
44
|
+
return Bun.write(Bun.stdout, `${r}\n`)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const fetchApi = async (method, path, props) => {
|
|
@@ -84,9 +84,9 @@ const fetchApi = async (method, path, props) => {
|
|
|
84
84
|
let isJson = res.headers.get('content-type') === 'application/json'
|
|
85
85
|
if (isJson) fmtRes(await res.json(), format.has('p'))
|
|
86
86
|
else if (res.headers.get('content-type')?.match(/^text\//)) fmtRes(await res.text(), format.has('p'))
|
|
87
|
-
else
|
|
87
|
+
else Bun.write(Bun.stdout, await res.arrayBuffer())
|
|
88
88
|
}
|
|
89
|
-
if (format.has('t'))
|
|
89
|
+
if (format.has('t')) Bun.write(Bun.stdout, `${(endTime / 1_000_000).toFixed(2)}ms\n`)
|
|
90
90
|
process.exit(res.ok ? 0 : 1)
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -100,7 +100,7 @@ const formatDefault = def =>
|
|
|
100
100
|
: def ?? 'undefined';
|
|
101
101
|
|
|
102
102
|
result = commands.map(c=>{
|
|
103
|
-
let args = c.arguments.map(a=>`.argument("${a.name}", "${a.description || a.name+' argument' || ''}")`)
|
|
103
|
+
let args = c.arguments.map(a=>`.argument("${a.name}", "${JSON.stringify(a.description).slice(1,-1) || a.name+' argument' || ''}")`)
|
|
104
104
|
let optionsBase = [
|
|
105
105
|
{name: '%format', short:'%f', type: '[string]', description: 'response format [\'s\',\'h\',\'b\',\'t\',\'p\']', default:["s","b","p"]},
|
|
106
106
|
{name: '%header', short:'%h', type: '<string...>', description: 'request header formated as headerName=headerValue', default:[]},
|
|
@@ -108,12 +108,12 @@ result = commands.map(c=>{
|
|
|
108
108
|
{name: '%body', short:'%b', type: '<string>', description: 'request body', default:''},
|
|
109
109
|
{name: '%bodyFile', short:'%bf', type: '<path>', description: 'request body file', default:''}
|
|
110
110
|
]
|
|
111
|
-
let options = [...optionsBase,...(c.options||[])].map(o=>`.addOption(new Option("-${o.short}, --${o.name} ${o.type}", "${o.description}").default(${formatDefault(o.default)}))`)
|
|
112
|
-
let action = `.action(async (${c.arguments.map(a=>`${a.name},`)} props) => {
|
|
111
|
+
let options = [...optionsBase,...(c.options||[])].map(o=>`.addOption(new Option("-${o.short}, --${o.name} ${o.type}", "${JSON.stringify(o.description).slice(1,-1)}").default(${formatDefault(o.default)}))`)
|
|
112
|
+
let action = `.action(async (${c.arguments.map(a=>`${a.name},`).join('')} props) => {
|
|
113
113
|
${c.action ? ';('+c.action.toString()+')(props)' : ''}
|
|
114
114
|
return await fetchApi("${c.route.method.toUpperCase()}",\`${c.route.pathT}\`, props)
|
|
115
115
|
})`
|
|
116
|
-
return `program.command("${c.name}").description("${c.description}")${args.join('')}${options.join('')}${action}`
|
|
116
|
+
return `program.command("${c.name}").description("${JSON.stringify(c.description).slice(1,-1)}")${args.join('')}${options.join('')}${action}`
|
|
117
117
|
})
|
|
118
118
|
%*/
|
|
119
119
|
|
|
@@ -6,8 +6,8 @@ export type GalbeClientConfig = {
|
|
|
6
6
|
export const Kind = Symbol.for('json.string')
|
|
7
7
|
type Json<T> = { T: T }
|
|
8
8
|
|
|
9
|
-
interface GR<S extends number =
|
|
10
|
-
status: S
|
|
9
|
+
interface GR<S extends number | 'default' = 'default', B = any, OKS extends number = OKStatusCode> {
|
|
10
|
+
status: Exclude<S, "default">
|
|
11
11
|
ok: S extends OKS ? true : false
|
|
12
12
|
redirected: boolean
|
|
13
13
|
statusText: string
|
|
@@ -18,16 +18,16 @@ interface GR<S extends number = number, B = any, OKS extends number = OKStatusCo
|
|
|
18
18
|
stream?: ST
|
|
19
19
|
) => B extends Uint8Array
|
|
20
20
|
? ST extends true
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
? Promise<AsyncGenerator<Uint8Array, void, unknown>>
|
|
22
|
+
: B extends Json<infer T>
|
|
23
|
+
? Promise<T>
|
|
24
|
+
: Promise<B>
|
|
25
25
|
: B extends string
|
|
26
26
|
? ST extends true
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
? Promise<AsyncGenerator<string, void, unknown>>
|
|
28
|
+
: B extends Json<infer T>
|
|
29
|
+
? Promise<T>
|
|
30
|
+
: Promise<B>
|
|
31
31
|
: B extends Json<infer T>
|
|
32
32
|
? Promise<T>
|
|
33
33
|
: Promise<B>
|
|
@@ -35,9 +35,9 @@ interface GR<S extends number = number, B = any, OKS extends number = OKStatusCo
|
|
|
35
35
|
|
|
36
36
|
export type OKStatusCode = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226
|
|
37
37
|
// prettier-ignore
|
|
38
|
-
export type HttpStatusCode = 100|101|102|103|OKStatusCode|300|301|302|303|304|305|307|308|400|401|402|403|404|405|406|407|408|409|410|411|412|413|414|415|416|417|418|421|422|423|424|426|428|429|431|451|500|501|502|503|504|505|506|507|508|510|511
|
|
38
|
+
export type HttpStatusCode = 100 | 101 | 102 | 103 | OKStatusCode | 300 | 301 | 302 | 303 | 304 | 305 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511
|
|
39
39
|
type PGR<
|
|
40
|
-
S extends number =
|
|
40
|
+
S extends number | 'default' = 'default',
|
|
41
41
|
B = any,
|
|
42
42
|
O extends number = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226
|
|
43
43
|
> = Promise<GR<S, B, O>>
|
|
@@ -54,9 +54,15 @@ const DEFAULT_HEADERS = {
|
|
|
54
54
|
'user-agent': 'Galbe//*%(()=>version)()%*/'
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// Typescript types
|
|
58
|
+
/*%
|
|
59
|
+
Object.entries(types).map(([tk, t])=>{
|
|
60
|
+
return `export type ${tk} = ${t}`
|
|
61
|
+
})
|
|
62
|
+
%*/
|
|
63
|
+
|
|
57
64
|
export default class GalbeClient {
|
|
58
65
|
config?: GalbeClientConfig
|
|
59
|
-
|
|
60
66
|
/*%
|
|
61
67
|
Object.entries(routes).map(([method, list])=>{
|
|
62
68
|
return`${method} = {\n${list.map( r => {
|
|
@@ -66,7 +72,7 @@ export default class GalbeClient {
|
|
|
66
72
|
''
|
|
67
73
|
let oks = Object.keys(r.schemas?.response||{}).filter(s=>s>=200&&s<300)
|
|
68
74
|
let responses = Object.keys(r.schemas?.response||{}).length ?
|
|
69
|
-
`${Object.entries(r.schemas.response).map(([k,v])=>`PGR<${k},${v}${oks?.length?`,${oks.join('|')}`:''}>`).join('|')}|PGR<Exclude<HttpStatusCode,${Object.keys(r.schemas.response).join('|')}
|
|
75
|
+
`${Object.entries(r.schemas.response).filter(([s,_])=>s!=='"default"').map(([k,v])=>`PGR<${k},${v}${oks?.length?`,${oks.join('|')}`:''}>`).join('|')}|PGR<Exclude<HttpStatusCode,${Object.keys(r.schemas.response).filter(s=>s!=='"default"').join('|')}>,${'"default"' in r.schemas.response ? r.schemas.response['"default"'] : 'any'}${oks?.length?`,${oks.join('|')}`:''}>`:
|
|
70
76
|
`PGR<HttpStatusCode,any${oks?.length?`,${oks.join('|')}`:',any'}>`
|
|
71
77
|
return ` '${r.path}':(${p.length?p.map(([k,v])=>`${k}:${v.type}`).join(',')+', ':''}options:RequestOptions${schemas}={})=>this.fetch(\`${r.pathT}\`,{...options,method:'${r.method.toUpperCase()}'}) as ${responses}`
|
|
72
78
|
}).join(',\n')}\n}`
|
|
@@ -155,9 +161,13 @@ export default class GalbeClient {
|
|
|
155
161
|
''
|
|
156
162
|
let oks = Object.keys(r.schemas?.response||{}).filter(s=>s>=200&&s<300)
|
|
157
163
|
let responses = Object.keys(r.schemas?.response||{}).length ?
|
|
158
|
-
`${Object.entries(r.schemas.response).map(([k,v])=>`PGR<${k},${v}${oks?.length?`,${oks.join('|')}`:''}>`).join('|')}|PGR<Exclude<HttpStatusCode,${Object.keys(r.schemas.response).join('|')}
|
|
164
|
+
`${Object.entries(r.schemas.response).filter(([s,_])=>s!=='"default"').map(([k,v])=>`PGR<${k},${v}${oks?.length?`,${oks.join('|')}`:''}>`).join('|')}|PGR<Exclude<HttpStatusCode,${Object.keys(r.schemas.response).filter(s=>s!=='"default"').join('|')}>,${'"default"' in r.schemas.response ? r.schemas.response['"default"'] : 'any'}${oks?.length?`,${oks.join('|')}`:''}>`:
|
|
159
165
|
`PGR<HttpStatusCode,any${oks?.length?`,${oks.join('|')}`:',any'}>`
|
|
160
|
-
|
|
166
|
+
let summary = r.summary ? ` * ${r.summary || ''}\n *\n` : ''
|
|
167
|
+
let description = r.description ? ` * ${r.description.replace(/\n/g,'\n * ')}` : ''
|
|
168
|
+
let params = Object.entries(r.schema.params || {}).map( ([k,v])=>`\n * @param ${k} - ${v.description?.replace(/\n/g,'\n ')}` ).join('')
|
|
169
|
+
let query = Object.entries(r.schema.query || {}).map( ([k,v])=>`\n * @param options.query.${k} - ${v.description?.replace(/\n/g,'\n ')}` ).join('')
|
|
170
|
+
return `/**\n${summary}${description}\n *${params}${query}\n *\/\n ${r.alias}(${p.length ? p.map(([k,v])=>`${k}: ${v.type}`).join(', ')+', ':''}options: RequestOptions${schemas} = {}){return this.fetch(\`${r.pathT}\`, {...options, method: '${r.method.toUpperCase()}'}) as ${responses}}\n`
|
|
161
171
|
}).join(' ')
|
|
162
172
|
})
|
|
163
173
|
%*/
|
package/bin/util.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { relative } from 'path'
|
|
|
2
2
|
import { watch } from 'chokidar'
|
|
3
3
|
import { Galbe, Route } from '../src'
|
|
4
4
|
import { logRoute, walkRoutes } from '../src/util'
|
|
5
|
-
import { RouteMeta, defineRoutes } from '../src/routes'
|
|
5
|
+
import { GalbeProxy, RouteMeta, defineRoutes } from '../src/routes'
|
|
6
6
|
|
|
7
7
|
export { default as pckg } from '../package.json'
|
|
8
8
|
|
|
@@ -22,7 +22,7 @@ export const silentExec = async (fn: () => any) => {
|
|
|
22
22
|
let consoleMock = Object.fromEntries(
|
|
23
23
|
Object.entries(console)
|
|
24
24
|
.filter(([_, v]) => typeof v === 'function')
|
|
25
|
-
.map(([k, _]) => [k, () => {}])
|
|
25
|
+
.map(([k, _]) => [k, () => { }])
|
|
26
26
|
)
|
|
27
27
|
const _console = console
|
|
28
28
|
const _processStdoutWrite = process.stdout.write
|
|
@@ -30,9 +30,9 @@ export const silentExec = async (fn: () => any) => {
|
|
|
30
30
|
//@ts-ignore
|
|
31
31
|
global.console = consoleMock
|
|
32
32
|
//@ts-ignore
|
|
33
|
-
process.stdout.write = function () {}
|
|
33
|
+
process.stdout.write = function () { }
|
|
34
34
|
//@ts-ignore
|
|
35
|
-
process.stderr.write = function () {}
|
|
35
|
+
process.stderr.write = function () { }
|
|
36
36
|
const r = await fn()
|
|
37
37
|
global.console = _console
|
|
38
38
|
process.stdout.write = _processStdoutWrite
|
|
@@ -65,29 +65,32 @@ export const instanciateRoutes = async (g: Galbe) => {
|
|
|
65
65
|
hasMainRoutes = true
|
|
66
66
|
logRoute(r)
|
|
67
67
|
})
|
|
68
|
-
if (hasMainRoutes)
|
|
68
|
+
if (hasMainRoutes) Bun.write(Bun.stdout, '\n')
|
|
69
69
|
// Route Files Analysis
|
|
70
70
|
let routes: Record<string, { route?: Route; meta?: RouteMeta; error?: any }[]> = {}
|
|
71
71
|
let errors: Record<string, any> = {}
|
|
72
|
-
|
|
72
|
+
|
|
73
|
+
const proxy = new GalbeProxy(g, ({ type, route, error, filepath, meta }) => {
|
|
74
|
+
if (meta?.ignore || meta?.hide) return
|
|
73
75
|
if (!filepath) return
|
|
74
76
|
if (!(filepath in routes)) routes[filepath] = []
|
|
75
77
|
if (type === 'add' && route && filepath) routes[filepath].push({ route, meta })
|
|
76
78
|
if (type === 'error') errors[filepath] = error
|
|
77
79
|
})
|
|
80
|
+
await defineRoutes({ routes: g?.config?.routes }, proxy)
|
|
78
81
|
for (let [fp, e] of Object.entries(routes)) {
|
|
79
82
|
console.log(`\x1b\[0;36m ${relative(CWD, fp)}\x1b[0m`)
|
|
80
83
|
let maxPathLength = e.reduce((p, c) => {
|
|
81
84
|
return Math.max(p, c.route?.path.length || 0)
|
|
82
85
|
}, 0)
|
|
83
86
|
for (let r of e) {
|
|
84
|
-
if (r.route) logRoute(r.route, r.meta, { maxPathLength })
|
|
87
|
+
if (r.route && !r.meta?.ignore && !r.meta?.hide) logRoute(r.route, r.meta, { maxPathLength })
|
|
85
88
|
}
|
|
86
89
|
if (errors?.[fp]) {
|
|
87
90
|
console.log(`\x1b\[0;31m Error:\x1b[0m`)
|
|
88
91
|
console.log(errors?.[fp])
|
|
89
92
|
}
|
|
90
|
-
|
|
93
|
+
console.log("")
|
|
91
94
|
}
|
|
92
95
|
console.log('\x1b[1;30m\x1b[32mdone\x1b[0m\n')
|
|
93
96
|
}
|
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/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
|
@@ -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]
|
|
@@ -242,7 +247,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
|
|
|
242
247
|
description: v.description || HttpStatus[Number(s) as keyof typeof HttpStatus] || 'Response',
|
|
243
248
|
content: { [media]: { schema: schema } }
|
|
244
249
|
}
|
|
245
|
-
if (components.responses && r.schema.response?.[s]
|
|
250
|
+
if (components.responses && r.schema.response?.[s]?.id) {
|
|
246
251
|
components.responses[r.schema.response?.[s].id as string] = response
|
|
247
252
|
//@ts-ignore
|
|
248
253
|
response = { $ref: `#/components/responses/${r.schema.response?.[s].id}` }
|
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,7 +645,12 @@ 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') {
|