galbe 0.3.0 → 0.4.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/.github/workflows/build_test.yml +2 -4
- package/.github/workflows/release.yml +2 -2
- package/bin/cli.ts +8 -104
- package/bin/commands/build.ts +95 -0
- package/bin/commands/dev.ts +53 -0
- package/bin/commands/generate/client.ts +192 -0
- package/bin/commands/generate/code/openapi.parser.ts +479 -0
- package/bin/commands/generate/code.ts +75 -0
- package/bin/commands/generate/index.ts +12 -0
- package/bin/commands/generate/spec.ts +94 -0
- package/bin/res/cli.template.js +120 -0
- package/bin/res/client.template.ts +161 -0
- package/bin/util.ts +164 -0
- package/bun.lockb +0 -0
- package/docs/routes.md +3 -3
- package/package.json +10 -8
- package/scripts/postinstall.ts +1 -3
- package/src/extras/spec/openapi.serializer.ts +264 -0
- package/src/extras.ts +1 -0
- package/src/index.ts +103 -73
- package/src/parser.ts +27 -5
- package/src/router.ts +27 -29
- package/src/routes.ts +137 -38
- package/src/schema.ts +69 -8
- package/src/server.ts +9 -10
- package/src/types.ts +73 -41
- package/src/util.ts +76 -10
- package/src/validator.ts +2 -0
- package/test/parser.test.ts +34 -0
- package/test/requests.test.ts +5 -12
- package/test/resources/test.route.comment.ts +20 -0
- package/test/responses.test.ts +109 -11
- package/test/routeFiles.test.ts +44 -27
- package/test/router.test.ts +67 -42
- package/scripts/build.ts +0 -14
package/src/router.ts
CHANGED
|
@@ -1,39 +1,34 @@
|
|
|
1
|
-
import type { Route, RouteNode
|
|
2
|
-
import { NotFoundError } from './types'
|
|
1
|
+
import type { Method, Route, RouteNode } from './types'
|
|
2
|
+
import { MethodNotAllowedError, NotFoundError } from './types'
|
|
3
3
|
|
|
4
4
|
const ROUTE_REGEX = /^(\/(\*|:?\d+|:?\w+|:?[\w\d][\w-]+[\w\d]))*\/?$/
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const walk = (path: string[], node: RouteNode, alts: RouteNode[] = []): RouteNode => {
|
|
7
7
|
if (path.length < 1) throw new NotFoundError()
|
|
8
|
-
if (path.length === 1 && node.
|
|
8
|
+
if (path.length === 1 && node.routes && !!Object.keys(node.routes).length) return node
|
|
9
9
|
|
|
10
10
|
if (node.children?.['*']) alts.push(node.children['*'])
|
|
11
11
|
if (node.param) alts.push(node.param)
|
|
12
12
|
|
|
13
13
|
if (node.children && path[1] in node.children) {
|
|
14
14
|
path.shift()
|
|
15
|
-
return
|
|
15
|
+
return walk(path, node.children[path[0]], alts)
|
|
16
16
|
}
|
|
17
17
|
if (node.param) {
|
|
18
18
|
path.shift()
|
|
19
19
|
alts.pop()
|
|
20
|
-
|
|
21
|
-
return walkRoutes(path, node.param, alts)
|
|
22
|
-
} catch (error) {
|
|
23
|
-
if (error instanceof NotFoundError) console.log(error)
|
|
24
|
-
else throw error
|
|
25
|
-
}
|
|
20
|
+
return walk(path, node.param, alts)
|
|
26
21
|
}
|
|
27
22
|
if (alts.length > 1) {
|
|
28
|
-
return
|
|
23
|
+
return walk(path, alts.pop() as RouteNode, alts)
|
|
29
24
|
}
|
|
30
25
|
if (alts.length === 1) {
|
|
31
26
|
let lastAlt = alts.pop() as RouteNode
|
|
32
27
|
try {
|
|
33
|
-
return
|
|
28
|
+
return walk(path, lastAlt, alts)
|
|
34
29
|
} catch (error) {
|
|
35
30
|
if (error instanceof NotFoundError) {
|
|
36
|
-
if (lastAlt?.
|
|
31
|
+
if (lastAlt?.routes) return lastAlt
|
|
37
32
|
} else throw error
|
|
38
33
|
}
|
|
39
34
|
}
|
|
@@ -42,12 +37,12 @@ const walkRoutes = (path: string[], node: RouteNode, alts: RouteNode[] = []): Ro
|
|
|
42
37
|
}
|
|
43
38
|
|
|
44
39
|
export class GalbeRouter {
|
|
45
|
-
routes:
|
|
40
|
+
routes: RouteNode
|
|
46
41
|
prefix: string
|
|
47
42
|
cacheEnabled: boolean
|
|
48
43
|
cachedRoutes: Map<string, Route | null>
|
|
49
44
|
constructor(options?: { prefix?: string; cacheEnabled?: boolean }) {
|
|
50
|
-
this.routes = {
|
|
45
|
+
this.routes = { routes: {} }
|
|
51
46
|
let prefix = options?.prefix || ''
|
|
52
47
|
if (prefix && !prefix.match(/^\//)) prefix = `/${prefix}`
|
|
53
48
|
this.prefix = prefix
|
|
@@ -62,43 +57,46 @@ export class GalbeRouter {
|
|
|
62
57
|
route.path = `${this.prefix || ''}${route.path}`
|
|
63
58
|
let path = route.path.replace(/^\/$(.*)\/?$/, '$1').split('/')
|
|
64
59
|
path.shift()
|
|
65
|
-
let r = this.routes
|
|
60
|
+
let r = this.routes
|
|
66
61
|
if (!path.length) {
|
|
67
|
-
r.route = route
|
|
62
|
+
r.routes[route.method] = route
|
|
68
63
|
} else {
|
|
69
64
|
while (path.length) {
|
|
70
65
|
let p = path.shift()
|
|
71
66
|
if (p === undefined) break
|
|
72
67
|
if (!path.length) {
|
|
73
|
-
if (p.match(/^:/))
|
|
74
|
-
|
|
68
|
+
if (p.match(/^:/)) {
|
|
69
|
+
if (!r.param) r.param = { routes: {} }
|
|
70
|
+
r.param.routes[route.method] = route
|
|
71
|
+
} else {
|
|
75
72
|
if (!r.children) r.children = {}
|
|
76
|
-
r.children[p] = {
|
|
73
|
+
if (!(p in r.children)) r.children[p] = { routes: {} }
|
|
74
|
+
r.children[p].routes[route.method] = route
|
|
77
75
|
}
|
|
78
76
|
} else {
|
|
79
77
|
if (p.match(/^:/)) {
|
|
80
|
-
if (!r.param) r.param = {}
|
|
78
|
+
if (!r.param) r.param = { routes: {} }
|
|
81
79
|
r = r.param
|
|
82
80
|
} else {
|
|
83
81
|
if (!r.children) r.children = {}
|
|
84
|
-
if (!(p in r.children)) r.children[p] = {}
|
|
82
|
+
if (!(p in r.children)) r.children[p] = { routes: {} }
|
|
85
83
|
r = r.children[p]
|
|
86
84
|
}
|
|
87
85
|
}
|
|
88
86
|
}
|
|
89
87
|
}
|
|
90
88
|
}
|
|
91
|
-
find(method:
|
|
92
|
-
method = method.toUpperCase()
|
|
89
|
+
find(method: Method, path: string): Route {
|
|
93
90
|
const staticRoute = this.cachedRoutes.get(`[${method}]${path}`)
|
|
94
91
|
if (staticRoute === null) throw new NotFoundError()
|
|
95
92
|
if (staticRoute !== undefined) return staticRoute
|
|
96
|
-
let parts = path.split('/')
|
|
97
|
-
|
|
98
|
-
if (!
|
|
93
|
+
let parts = path === '/' ? [''] : path.split('/')
|
|
94
|
+
let r = walk(parts, this.routes)
|
|
95
|
+
if (!r || !Object.keys(r.routes).length) {
|
|
99
96
|
if (this.cacheEnabled) this.cachedRoutes.set(`[${method}]${path}`, null)
|
|
100
97
|
throw new NotFoundError()
|
|
101
|
-
}
|
|
98
|
+
} else if (!(method in r.routes)) throw new MethodNotAllowedError()
|
|
99
|
+
const route = r.routes[method] as Route
|
|
102
100
|
if (this.cacheEnabled) this.cachedRoutes.set(`[${method}]${path}`, route)
|
|
103
101
|
return route
|
|
104
102
|
}
|
package/src/routes.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { GalbeConfig } from './types'
|
|
1
|
+
import type { GalbeConfig, Route } from './types'
|
|
2
2
|
|
|
3
3
|
import { readdir, lstat } from 'fs/promises'
|
|
4
|
-
import { extname
|
|
4
|
+
import { extname } from 'path'
|
|
5
5
|
import { parse } from 'acorn'
|
|
6
6
|
import { simple } from 'acorn-walk'
|
|
7
7
|
import { Galbe } from './index'
|
|
@@ -10,14 +10,116 @@ import { Glob } from 'bun'
|
|
|
10
10
|
|
|
11
11
|
export const DEFAULT_ROUTE_PATTERN = 'src/**/*.route.{js,ts}'
|
|
12
12
|
|
|
13
|
-
export type RouteMeta = {
|
|
13
|
+
export type RouteMeta = { head?: string } & Record<string, boolean | string | string[]>
|
|
14
|
+
export type RoutesMeta = {
|
|
14
15
|
header: Record<string, boolean | string | string[]>
|
|
15
|
-
routes: Record<string, Record<string,
|
|
16
|
+
routes: Record<string, Record<string, RouteMeta>>
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
+
export type RouteInstanciationCallback = <T extends 'add' | 'error'>(event: {
|
|
19
|
+
type: T
|
|
20
|
+
error?: T extends 'error' ? any : undefined
|
|
21
|
+
route: T extends 'add' ? Route : undefined
|
|
22
|
+
filepath?: string
|
|
23
|
+
meta: T extends 'add' ? RouteMeta : undefined
|
|
24
|
+
}) => any | Promise<any>
|
|
18
25
|
export type RouteFileMeta = {
|
|
19
26
|
file: string
|
|
20
|
-
} &
|
|
27
|
+
} & RoutesMeta
|
|
28
|
+
|
|
29
|
+
class GalbeProxy {
|
|
30
|
+
#g: Galbe
|
|
31
|
+
#cb?: RouteInstanciationCallback
|
|
32
|
+
filepath?: string
|
|
33
|
+
meta?: RoutesMeta
|
|
34
|
+
constructor(g: Galbe, cb?: RouteInstanciationCallback) {
|
|
35
|
+
this.#g = g
|
|
36
|
+
this.#cb = cb
|
|
37
|
+
}
|
|
38
|
+
async get(...args: any[]) {
|
|
39
|
+
//@ts-ignore
|
|
40
|
+
const route = this.#g.get(...args) as Route
|
|
41
|
+
if (this.#cb)
|
|
42
|
+
await this.#cb({
|
|
43
|
+
type: 'add',
|
|
44
|
+
route,
|
|
45
|
+
filepath: this.filepath || '',
|
|
46
|
+
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
47
|
+
})
|
|
48
|
+
return route
|
|
49
|
+
}
|
|
50
|
+
async post(...args: any[]) {
|
|
51
|
+
//@ts-ignore
|
|
52
|
+
const route = this.#g.post(...args) as Route
|
|
53
|
+
if (this.#cb)
|
|
54
|
+
await this.#cb({
|
|
55
|
+
type: 'add',
|
|
56
|
+
route,
|
|
57
|
+
filepath: this.filepath,
|
|
58
|
+
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
59
|
+
})
|
|
60
|
+
return route
|
|
61
|
+
}
|
|
62
|
+
async put(...args: any[]) {
|
|
63
|
+
//@ts-ignore
|
|
64
|
+
const route = this.#g.put(...args) as Route
|
|
65
|
+
if (this.#cb)
|
|
66
|
+
await this.#cb({
|
|
67
|
+
type: 'add',
|
|
68
|
+
route,
|
|
69
|
+
filepath: this.filepath,
|
|
70
|
+
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
71
|
+
})
|
|
72
|
+
return route
|
|
73
|
+
}
|
|
74
|
+
async patch(...args: any[]) {
|
|
75
|
+
//@ts-ignore
|
|
76
|
+
const route = this.#g.patch(...args) as Route
|
|
77
|
+
if (this.#cb)
|
|
78
|
+
await this.#cb({
|
|
79
|
+
type: 'add',
|
|
80
|
+
route,
|
|
81
|
+
filepath: this.filepath,
|
|
82
|
+
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
83
|
+
})
|
|
84
|
+
return route
|
|
85
|
+
}
|
|
86
|
+
async delete(...args: any[]) {
|
|
87
|
+
//@ts-ignore
|
|
88
|
+
const route = this.#g.delete(...args) as Route
|
|
89
|
+
if (this.#cb)
|
|
90
|
+
await this.#cb({
|
|
91
|
+
type: 'add',
|
|
92
|
+
route,
|
|
93
|
+
filepath: this.filepath,
|
|
94
|
+
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
95
|
+
})
|
|
96
|
+
return route
|
|
97
|
+
}
|
|
98
|
+
async options(...args: any[]) {
|
|
99
|
+
//@ts-ignore
|
|
100
|
+
const route = this.#g.options(...args) as Route
|
|
101
|
+
if (this.#cb)
|
|
102
|
+
await this.#cb({
|
|
103
|
+
type: 'add',
|
|
104
|
+
route,
|
|
105
|
+
filepath: this.filepath,
|
|
106
|
+
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
107
|
+
})
|
|
108
|
+
return route
|
|
109
|
+
}
|
|
110
|
+
async head(...args: any[]) {
|
|
111
|
+
//@ts-ignore
|
|
112
|
+
const route = this.#g.head(...args) as Route
|
|
113
|
+
if (this.#cb)
|
|
114
|
+
await this.#cb({
|
|
115
|
+
type: 'add',
|
|
116
|
+
route,
|
|
117
|
+
filepath: this.filepath,
|
|
118
|
+
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
119
|
+
})
|
|
120
|
+
return route
|
|
121
|
+
}
|
|
122
|
+
}
|
|
21
123
|
|
|
22
124
|
const parseComment = (comment: string): Record<string, string | string[]> => {
|
|
23
125
|
const head =
|
|
@@ -40,11 +142,11 @@ const parseComment = (comment: string): Record<string, string | string[]> => {
|
|
|
40
142
|
}
|
|
41
143
|
return refs
|
|
42
144
|
}
|
|
43
|
-
export const metaAnalysis = async (filePath: string): Promise<
|
|
145
|
+
export const metaAnalysis = async (filePath: string): Promise<RoutesMeta> => {
|
|
44
146
|
const file = Bun.file(filePath)
|
|
45
147
|
const fileExt = extname(filePath)
|
|
46
148
|
let content = await file.text()
|
|
47
|
-
let meta:
|
|
149
|
+
let meta: RoutesMeta = { header: {}, routes: {} }
|
|
48
150
|
|
|
49
151
|
if (fileExt === '.ts') {
|
|
50
152
|
//// Much faster but doesn't include comments. See https://github.com/oven-sh/bun/pull/7055
|
|
@@ -69,20 +171,24 @@ export const metaAnalysis = async (filePath: string): Promise<RouteMeta> => {
|
|
|
69
171
|
}).code
|
|
70
172
|
}
|
|
71
173
|
|
|
72
|
-
const comments: Record<number, string
|
|
174
|
+
const comments: Record<number, Record<number, string>> = {}
|
|
73
175
|
|
|
74
176
|
const ast = parse(content, {
|
|
75
177
|
ecmaVersion: 'latest',
|
|
76
178
|
sourceType: 'module',
|
|
77
179
|
locations: true,
|
|
78
180
|
onComment: (isBlock, text, _start, _end, _locStart, locEnd) => {
|
|
79
|
-
if (isBlock && locEnd?.line
|
|
181
|
+
if (isBlock && locEnd?.line !== undefined && locEnd?.column !== undefined) {
|
|
182
|
+
if (!comments?.[locEnd.line]) comments[locEnd.line] = []
|
|
183
|
+
comments[locEnd.line][locEnd.column] = text
|
|
184
|
+
}
|
|
80
185
|
}
|
|
81
186
|
})
|
|
82
187
|
simple(ast, {
|
|
83
188
|
ExportDefaultDeclaration(node) {
|
|
84
|
-
const headerLine = node.loc?.start.line
|
|
85
|
-
const
|
|
189
|
+
const headerLine = node.loc?.start.line || -1
|
|
190
|
+
const headerCol = node.loc?.start.column || -1
|
|
191
|
+
const headerCom = comments?.[headerLine]?.[headerCol - 1] ? comments[headerLine][headerCol - 1] : ''
|
|
86
192
|
const headerRef = parseComment(headerCom)
|
|
87
193
|
meta.header = headerRef
|
|
88
194
|
|
|
@@ -99,8 +205,9 @@ export const metaAnalysis = async (filePath: string): Promise<RouteMeta> => {
|
|
|
99
205
|
const path = node.arguments[0].value
|
|
100
206
|
// @ts-ignore
|
|
101
207
|
const method = node.callee.property.name
|
|
102
|
-
const line = node.loc?.start.line
|
|
103
|
-
const
|
|
208
|
+
const line = node.loc?.start.line || -1
|
|
209
|
+
const col = node.loc?.start.column || -1
|
|
210
|
+
const com = comments?.[line]?.[col - 1] ? comments[line][col - 1] : ''
|
|
104
211
|
|
|
105
212
|
const routeRefs = parseComment(com)
|
|
106
213
|
|
|
@@ -114,49 +221,41 @@ export const metaAnalysis = async (filePath: string): Promise<RouteMeta> => {
|
|
|
114
221
|
return meta
|
|
115
222
|
}
|
|
116
223
|
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
routes(galbe)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export const defineRoutes = async (options: GalbeConfig, galbe: Galbe) => {
|
|
224
|
+
export const defineRoutes = async (
|
|
225
|
+
options: Pick<GalbeConfig, 'routes'>,
|
|
226
|
+
galbe: Galbe,
|
|
227
|
+
cb?: RouteInstanciationCallback
|
|
228
|
+
) => {
|
|
126
229
|
const routes = options?.routes === true ? DEFAULT_ROUTE_PATTERN : options?.routes
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return
|
|
130
|
-
}
|
|
230
|
+
const proxy = new GalbeProxy(galbe, cb)
|
|
231
|
+
if (!routes) return
|
|
131
232
|
const root = process.cwd()
|
|
132
233
|
if (typeof routes === 'string') {
|
|
133
|
-
let noRouteFound = true
|
|
134
234
|
for await (const path of new Glob(routes).scan({ cwd: root, absolute: true, onlyFiles: false })) {
|
|
135
|
-
noRouteFound = false
|
|
136
235
|
const isDir = (await lstat(path)).isDirectory()
|
|
137
236
|
|
|
138
237
|
let files: string[] = []
|
|
139
238
|
if (!isDir) files.push(path)
|
|
140
239
|
else files = files.concat((await readdir(path)).map(f => `${path}/${f}`))
|
|
141
|
-
if (files.length === 0) console.log(`\x1b\[38;5;245m No route found\x1b[0m`)
|
|
142
240
|
for (const f of files) {
|
|
143
241
|
try {
|
|
144
242
|
const metadata = await metaAnalysis(f)
|
|
243
|
+
proxy.filepath = f
|
|
244
|
+
proxy.meta = metadata
|
|
145
245
|
galbe.meta?.push({ file: path, ...metadata })
|
|
146
|
-
|
|
147
|
-
|
|
246
|
+
const imported = await import(f)
|
|
247
|
+
if (!imported?.default) throw new Error('No default export function')
|
|
248
|
+
if (typeof imported.default !== 'function') throw new Error('Default export must be a function')
|
|
249
|
+
const routes = imported.default
|
|
250
|
+
routes(proxy)
|
|
148
251
|
} catch (err: any) {
|
|
149
|
-
|
|
252
|
+
if (cb) await cb({ type: 'error', error: err, filepath: f, route: undefined, meta: undefined })
|
|
150
253
|
}
|
|
151
254
|
}
|
|
152
255
|
}
|
|
153
|
-
if (noRouteFound) {
|
|
154
|
-
console.log(`\x1b\[38;5;245m No route found\x1b[0m`)
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
256
|
} else if (Array.isArray(routes)) {
|
|
158
257
|
for (const r of routes) {
|
|
159
|
-
await defineRoutes({ routes: r }, galbe)
|
|
258
|
+
await defineRoutes({ routes: r }, galbe, cb)
|
|
160
259
|
}
|
|
161
260
|
}
|
|
162
261
|
}
|
package/src/schema.ts
CHANGED
|
@@ -3,6 +3,7 @@ export const Optional = Symbol.for('Galbe.SchemaType.Optional')
|
|
|
3
3
|
export const Stream = Symbol.for('Galbe.SchemaType.Stream')
|
|
4
4
|
|
|
5
5
|
export interface Options {
|
|
6
|
+
id?: string
|
|
6
7
|
title?: string
|
|
7
8
|
description?: string
|
|
8
9
|
default?: any
|
|
@@ -38,11 +39,11 @@ export interface STSchema extends Options {
|
|
|
38
39
|
| 'literal'
|
|
39
40
|
| 'array'
|
|
40
41
|
| 'object'
|
|
42
|
+
| 'json'
|
|
41
43
|
| 'urlForm'
|
|
42
44
|
| 'multipartForm'
|
|
43
45
|
| 'any'
|
|
44
46
|
| 'union'
|
|
45
|
-
| 'stream'
|
|
46
47
|
[Optional]?: boolean
|
|
47
48
|
[Stream]?: boolean
|
|
48
49
|
params: unknown[]
|
|
@@ -172,6 +173,11 @@ export interface STObject<T extends STProps = STProps> extends STSchema {
|
|
|
172
173
|
static: ObjectStatic<T, this['params']>
|
|
173
174
|
props: T
|
|
174
175
|
}
|
|
176
|
+
export interface STJson<T extends STBoolean | STNumber | STString | STObject = any> extends STSchema {
|
|
177
|
+
[Kind]: 'json'
|
|
178
|
+
type: 'boolean' | 'number' | 'string' | 'object' | 'unknown'
|
|
179
|
+
static: Static<T>
|
|
180
|
+
}
|
|
175
181
|
type ObjectStatic<T extends STProps, P extends unknown[]> = ObjectStaticProps<T, { [K in keyof T]: Static<T[K], P> }>
|
|
176
182
|
type OptionalPropertyKeys<T extends STProps> = {
|
|
177
183
|
[K in keyof T]: T[K] extends STOptional<STSchema> ? K : never
|
|
@@ -190,6 +196,13 @@ function _Object<T extends STProps>(properties?: T, options: Options = {}): STOb
|
|
|
190
196
|
? { ...options, [Kind]: 'object', props: clonedProperties, required: requiredKeys }
|
|
191
197
|
: { ...options, [Kind]: 'object', props: clonedProperties }) as unknown as STObject<T>
|
|
192
198
|
}
|
|
199
|
+
function _Json<T extends STBoolean | STNumber | STString | STObject>(value?: T, options: Options = {}): STJson<T> {
|
|
200
|
+
if (value?.[Kind] === 'boolean') return { ..._Bool(options), [Kind]: 'json', type: 'boolean' }
|
|
201
|
+
if (value?.[Kind] === 'number') return { ..._Number(options), [Kind]: 'json', type: 'number' }
|
|
202
|
+
if (value?.[Kind] === 'string') return { ..._String(options), [Kind]: 'json', type: 'string' }
|
|
203
|
+
if (value?.[Kind] === 'object') return { ..._Object(value.props, options), [Kind]: 'json', type: 'object' }
|
|
204
|
+
throw Error('Invalid Json type definition')
|
|
205
|
+
}
|
|
193
206
|
|
|
194
207
|
// UrlForm
|
|
195
208
|
export type STUrlFormValues =
|
|
@@ -231,7 +244,19 @@ export interface MultipartFormData<K extends string = string, V extends Static<S
|
|
|
231
244
|
}
|
|
232
245
|
export interface STMultipartForm<T extends STProps = STProps> extends STSchema {
|
|
233
246
|
[Kind]: 'multipartForm'
|
|
234
|
-
static: T extends undefined
|
|
247
|
+
static: T extends undefined
|
|
248
|
+
? {
|
|
249
|
+
[k: string]: {
|
|
250
|
+
headers: { type?: string; name: string; filename?: string }
|
|
251
|
+
content: Static<STMultipartFormValues>
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
: {
|
|
255
|
+
[K in keyof T]: {
|
|
256
|
+
headers: { type?: string; name: K; filename?: string }
|
|
257
|
+
content: Static<T[K]>
|
|
258
|
+
}
|
|
259
|
+
}
|
|
235
260
|
props: T
|
|
236
261
|
}
|
|
237
262
|
function _MultipartForm<T extends STProps>(properties?: T, options: Options = {}): STMultipartForm<T> {
|
|
@@ -251,12 +276,12 @@ export interface STArray<T extends STSchema = STSchema> extends STSchema {
|
|
|
251
276
|
static: Static<T>[]
|
|
252
277
|
items: T
|
|
253
278
|
}
|
|
254
|
-
export function _Array<T extends STSchema>(schema?: T, options: ArrayOptions = {}): STArray<T
|
|
279
|
+
export function _Array<T extends STSchema>(schema?: T, options: ArrayOptions = {}): STArray<T> {
|
|
255
280
|
return {
|
|
256
281
|
...options,
|
|
257
282
|
[Kind]: 'array',
|
|
258
283
|
items: schema ?? _Any()
|
|
259
|
-
} as unknown as
|
|
284
|
+
} as unknown as STArray<T>
|
|
260
285
|
}
|
|
261
286
|
|
|
262
287
|
// Union
|
|
@@ -323,9 +348,12 @@ export class SchemaType {
|
|
|
323
348
|
public object<T extends STProps>(properties?: T, options: Options = {}): STObject<T> {
|
|
324
349
|
return _Object(properties, options)
|
|
325
350
|
}
|
|
326
|
-
/**
|
|
327
|
-
public json<T extends
|
|
328
|
-
|
|
351
|
+
/** Creates a JSON Schema Type */
|
|
352
|
+
public json<T extends STString | STBoolean | STNumber | STObject<STProps>>(
|
|
353
|
+
value: T,
|
|
354
|
+
options: Options = {}
|
|
355
|
+
): STJson<T> {
|
|
356
|
+
return _Json(value, options)
|
|
329
357
|
}
|
|
330
358
|
/** Creates an UrlForm Schema Type */
|
|
331
359
|
public urlForm<T extends STUrlFormProps>(properties?: T, options: Options = {}): STUrlForm<T> {
|
|
@@ -336,7 +364,7 @@ export class SchemaType {
|
|
|
336
364
|
return _MultipartForm(properties, options)
|
|
337
365
|
}
|
|
338
366
|
/** Crates an Array Schema Type */
|
|
339
|
-
public array<T extends STSchema>(schema?: T, options: Options = {}): STArray<T
|
|
367
|
+
public array<T extends STSchema>(schema?: T, options: Options = {}): STArray<T> {
|
|
340
368
|
return _Array(schema, options)
|
|
341
369
|
}
|
|
342
370
|
/** Crates an Union Schema Type */
|
|
@@ -386,3 +414,36 @@ export class SchemaType {
|
|
|
386
414
|
return _Stream(schema)
|
|
387
415
|
}
|
|
388
416
|
}
|
|
417
|
+
|
|
418
|
+
export const schemaToTypeStr = (schema: STSchema): string => {
|
|
419
|
+
let type = 'unknown'
|
|
420
|
+
let kind = schema[Kind]
|
|
421
|
+
|
|
422
|
+
if (kind === 'boolean') type = 'boolean'
|
|
423
|
+
else if (kind === 'byteArray') type = 'Uint8Array'
|
|
424
|
+
else if (kind === 'number') type = 'number'
|
|
425
|
+
else if (kind === 'integer') type = 'number'
|
|
426
|
+
else if (kind === 'string') type = 'string'
|
|
427
|
+
else if (kind === 'any') type = 'any'
|
|
428
|
+
else if (kind === 'literal') {
|
|
429
|
+
let value = (schema as STLiteral).value
|
|
430
|
+
if (typeof value === 'string') type = `'${value}'`
|
|
431
|
+
else type = String(value)
|
|
432
|
+
} else if (kind === 'array') {
|
|
433
|
+
type = `Array<${schemaToTypeStr((schema as STArray).items)}>`
|
|
434
|
+
} else if (kind === 'object') {
|
|
435
|
+
let props = (schema as STObject).props
|
|
436
|
+
type = `{${Object.entries(props)
|
|
437
|
+
.map(([k, v]) => `${typeof k === 'string' ? `'${k}'` : k}:${schemaToTypeStr(v)}`)
|
|
438
|
+
.join(';')}}`
|
|
439
|
+
} else if (kind === 'json') {
|
|
440
|
+
type = `Json<${schemaToTypeStr({ ...schema, [Kind]: schema.type })}>`
|
|
441
|
+
} else if (kind === 'union') {
|
|
442
|
+
let anyOf = (schema as STUnion).anyOf
|
|
443
|
+
type = anyOf.map(s => schemaToTypeStr(s)).join('|')
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (schema[Optional]) type = `${type}|undefined`
|
|
447
|
+
|
|
448
|
+
return type
|
|
449
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import type { Context, Route } from './types'
|
|
1
|
+
import type { Context, Method, Route } from './types'
|
|
2
2
|
|
|
3
3
|
import { InternalError, RequestError } from './types'
|
|
4
4
|
import { parseEntry, requestBodyParser, requestPathParser, responseParser } from './parser'
|
|
5
5
|
import { Galbe } from './index'
|
|
6
6
|
import { validateResponse } from './validator'
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']
|
|
9
|
+
const EMPTY_BODY_METHODS = ['GET', 'OPTIONS', 'HEAD']
|
|
9
10
|
|
|
10
11
|
const handleInternalError = (error: any) => {
|
|
11
12
|
console.error(error)
|
|
@@ -13,7 +14,6 @@ const handleInternalError = (error: any) => {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
const setupPluginCallbacks = (galbe: Galbe) => ({
|
|
16
|
-
init: galbe.plugins.filter(p => p.init),
|
|
17
17
|
onFetch: galbe.plugins.filter(p => p.onFetch),
|
|
18
18
|
onRoute: galbe.plugins.filter(p => p.onRoute),
|
|
19
19
|
beforeHandle: galbe.plugins.filter(p => p.beforeHandle),
|
|
@@ -25,12 +25,11 @@ export default async (galbe: Galbe, port?: number) => {
|
|
|
25
25
|
if (galbe?.config?.basePath && galbe?.config?.basePath[0] !== '/')
|
|
26
26
|
galbe.config.basePath = `/${galbe?.config?.basePath}`
|
|
27
27
|
let pluginsCb = setupPluginCallbacks(galbe)
|
|
28
|
-
//@ts-ignore
|
|
29
|
-
for (const p of pluginsCb.init) await p.init(galbe?.config?.plugin?.[p.name], galbe)
|
|
30
28
|
|
|
31
29
|
return Bun.serve({
|
|
32
30
|
port: port || galbe.config?.port || 3000,
|
|
33
31
|
async fetch(req) {
|
|
32
|
+
if (!METHODS.includes(req.method)) return new Response('', { status: 501 })
|
|
34
33
|
const context: Context = {
|
|
35
34
|
request: req,
|
|
36
35
|
set: { headers: {} },
|
|
@@ -51,7 +50,7 @@ export default async (galbe: Galbe, port?: number) => {
|
|
|
51
50
|
try {
|
|
52
51
|
// find route
|
|
53
52
|
try {
|
|
54
|
-
route = router.find(req.method, url.pathname)
|
|
53
|
+
route = router.find(req.method.toLowerCase() as Method, url.pathname)
|
|
55
54
|
} catch (error) {
|
|
56
55
|
if (error instanceof RequestError) throw error
|
|
57
56
|
else throw handleInternalError(error)
|
|
@@ -72,7 +71,7 @@ export default async (galbe: Galbe, port?: number) => {
|
|
|
72
71
|
for (let [k, v] of url.searchParams) inQuery[k] = v
|
|
73
72
|
let inParams = requestPathParser(url.pathname, route.path)
|
|
74
73
|
|
|
75
|
-
context.body =
|
|
74
|
+
context.body = !EMPTY_BODY_METHODS.includes(req.method)
|
|
76
75
|
? await requestBodyParser(req.body, inHeaders, schema.body)
|
|
77
76
|
: null
|
|
78
77
|
context.headers = inHeaders
|
|
@@ -147,10 +146,10 @@ export default async (galbe: Galbe, port?: number) => {
|
|
|
147
146
|
if (r) response = r
|
|
148
147
|
} else response = await handlerWrapper(context)
|
|
149
148
|
|
|
150
|
-
const parsedResponse = responseParser(response, context)
|
|
149
|
+
const parsedResponse = responseParser(response, context, schema.response)
|
|
151
150
|
|
|
152
151
|
if (galbe.config?.responseValidator?.enabled && schema.response)
|
|
153
|
-
validateResponse(response, schema.response,
|
|
152
|
+
validateResponse(response, schema.response, parsedResponse.status || 200)
|
|
154
153
|
|
|
155
154
|
for (const p of pluginsCb.afterHandle) {
|
|
156
155
|
//@ts-ignore
|
|
@@ -180,7 +179,7 @@ export default async (galbe: Galbe, port?: number) => {
|
|
|
180
179
|
status: error.status,
|
|
181
180
|
headers: { 'Content-Type': 'application/json' }
|
|
182
181
|
})
|
|
183
|
-
}
|
|
182
|
+
} else console.log(error)
|
|
184
183
|
return new Response('"Internal Server Error"', {
|
|
185
184
|
status: 500,
|
|
186
185
|
headers: {
|