galbe 0.7.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/context.md +5 -1
- 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 +99 -91
- package/src/server.ts +7 -5
- package/src/types.ts +41 -21
- 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/src/routes.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { cpSync } from 'fs'
|
|
2
|
+
import type { GalbeConfig, GalbePlugin, Method, Route } from './types'
|
|
2
3
|
|
|
3
4
|
import { readdir, lstat } from 'fs/promises'
|
|
4
5
|
import { extname } from 'path'
|
|
@@ -10,10 +11,15 @@ import { Glob } from 'bun'
|
|
|
10
11
|
|
|
11
12
|
export const DEFAULT_ROUTE_PATTERN = 'src/**/*.route.{js,ts}'
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
const IGNORE_COMMENT_RGX = /^\s*\@galbe-ignore\s*$/
|
|
15
|
+
const HIDE_COMMENT_RGX = /^\s*\@galbe-hide\s*$/
|
|
16
|
+
|
|
17
|
+
export type RouteMeta = { head?: string, ignore?: boolean, hide?: boolean } & Record<string, boolean | string | string[]>
|
|
14
18
|
export type RoutesMeta = {
|
|
15
19
|
header: Record<string, boolean | string | string[]>
|
|
16
|
-
routes: Record<string, Partial<Record<Method, RouteMeta>>>
|
|
20
|
+
routes: Record<string, Partial<Record<Method | 'static', RouteMeta>>>
|
|
21
|
+
ignore?: boolean
|
|
22
|
+
hide?: boolean
|
|
17
23
|
}
|
|
18
24
|
export type RouteInstanciationCallback = <T extends 'add' | 'error'>(event: {
|
|
19
25
|
type: T
|
|
@@ -26,98 +32,89 @@ export type RouteFileMeta = {
|
|
|
26
32
|
file: string
|
|
27
33
|
} & RoutesMeta
|
|
28
34
|
|
|
29
|
-
class GalbeProxy {
|
|
35
|
+
export class GalbeProxy {
|
|
30
36
|
#g: Galbe
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
#plugins: GalbePlugin[] = []
|
|
38
|
+
_cb?: RouteInstanciationCallback
|
|
39
|
+
_metaTmp?: RoutesMeta
|
|
40
|
+
_filepath?: string
|
|
41
|
+
_meta: Array<RouteFileMeta> = []
|
|
34
42
|
constructor(g: Galbe, cb?: RouteInstanciationCallback) {
|
|
35
43
|
this.#g = g
|
|
36
|
-
this
|
|
44
|
+
this._cb = cb
|
|
45
|
+
this.#plugins = g.plugins
|
|
37
46
|
}
|
|
38
|
-
|
|
47
|
+
get server() {
|
|
48
|
+
return this.#g.server
|
|
49
|
+
}
|
|
50
|
+
get router() {
|
|
51
|
+
return this.#g.router
|
|
52
|
+
}
|
|
53
|
+
get config() {
|
|
54
|
+
return this.#g.config
|
|
55
|
+
}
|
|
56
|
+
get meta() {
|
|
57
|
+
return this._meta
|
|
58
|
+
}
|
|
59
|
+
set meta(meta: Array<RouteFileMeta>) {
|
|
60
|
+
this._meta = meta
|
|
61
|
+
this.#g.meta = meta
|
|
62
|
+
}
|
|
63
|
+
async init() {
|
|
64
|
+
for (const p of this.#plugins) {
|
|
65
|
+
// @ts-ignore
|
|
66
|
+
if (p.init) await p.init(this.#g.config?.plugin?.[p.name] || {}, this)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
private async handleRoute(method: Method | 'static', ...args: any[]): Promise<Route | undefined> {
|
|
70
|
+
let path = args[0]
|
|
71
|
+
let meta = {
|
|
72
|
+
...this._metaTmp?.routes?.[path]?.[method],
|
|
73
|
+
...(this._metaTmp?.hide ? { hide: true } : {}),
|
|
74
|
+
...(this._metaTmp?.ignore ? { ignore: true } : {})
|
|
75
|
+
}
|
|
76
|
+
if (meta?.ignore) return
|
|
77
|
+
|
|
39
78
|
//@ts-ignore
|
|
40
|
-
const route = this.#g
|
|
41
|
-
|
|
42
|
-
|
|
79
|
+
const route = this.#g[method](...args) as Route
|
|
80
|
+
|
|
81
|
+
if (this._cb && !meta?.ignore) {
|
|
82
|
+
await this._cb({
|
|
43
83
|
type: 'add',
|
|
44
84
|
route,
|
|
45
|
-
filepath: this.
|
|
46
|
-
meta
|
|
85
|
+
filepath: this._filepath || '',
|
|
86
|
+
meta
|
|
47
87
|
})
|
|
88
|
+
}
|
|
48
89
|
return route
|
|
49
90
|
}
|
|
91
|
+
async get(...args: any[]) {
|
|
92
|
+
return this.handleRoute('get', ...args)
|
|
93
|
+
}
|
|
50
94
|
async post(...args: any[]) {
|
|
51
|
-
|
|
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
|
|
95
|
+
return this.handleRoute('post', ...args)
|
|
61
96
|
}
|
|
62
97
|
async put(...args: any[]) {
|
|
63
|
-
|
|
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
|
|
98
|
+
return this.handleRoute('put', ...args)
|
|
73
99
|
}
|
|
74
100
|
async patch(...args: any[]) {
|
|
75
|
-
|
|
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
|
|
101
|
+
return this.handleRoute('patch', ...args)
|
|
85
102
|
}
|
|
86
103
|
async delete(...args: any[]) {
|
|
87
|
-
|
|
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
|
|
104
|
+
return this.handleRoute('delete', ...args)
|
|
97
105
|
}
|
|
98
106
|
async options(...args: any[]) {
|
|
99
|
-
|
|
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
|
|
107
|
+
return this.handleRoute('options', ...args)
|
|
109
108
|
}
|
|
110
109
|
async head(...args: any[]) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
})
|
|
120
|
-
return route
|
|
110
|
+
return this.handleRoute('head', ...args)
|
|
111
|
+
}
|
|
112
|
+
async static(...args: any[]) {
|
|
113
|
+
if (!!Bun.env.GALBE_BUILD_OUT) {
|
|
114
|
+
let [_, target] = args
|
|
115
|
+
cpSync(target, `${Bun.env.GALBE_BUILD_OUT}/static-${Bun.env.GALBE_BUILD}/${target}`, { recursive: true, dereference: true })
|
|
116
|
+
}
|
|
117
|
+
return this.handleRoute('static', ...args)
|
|
121
118
|
}
|
|
122
119
|
}
|
|
123
120
|
|
|
@@ -172,15 +169,22 @@ export const metaAnalysis = async (filePath: string): Promise<RoutesMeta> => {
|
|
|
172
169
|
}
|
|
173
170
|
|
|
174
171
|
const comments: Record<number, Record<number, string>> = {}
|
|
175
|
-
|
|
172
|
+
const ignoredLines = new Set<number>()
|
|
173
|
+
const hideLines = new Set<number>()
|
|
176
174
|
const ast = parse(content, {
|
|
177
175
|
ecmaVersion: 'latest',
|
|
178
176
|
sourceType: 'module',
|
|
179
177
|
locations: true,
|
|
180
|
-
onComment: (
|
|
181
|
-
if (
|
|
178
|
+
onComment: (_isBlock, text, _start, _end, _locStart, locEnd) => {
|
|
179
|
+
if (locEnd?.line !== undefined && locEnd?.column !== undefined) {
|
|
182
180
|
if (!comments?.[locEnd.line]) comments[locEnd.line] = []
|
|
183
|
-
|
|
181
|
+
if (IGNORE_COMMENT_RGX.test(text)) {
|
|
182
|
+
ignoredLines.add(locEnd.line + 1)
|
|
183
|
+
}
|
|
184
|
+
else if (HIDE_COMMENT_RGX.test(text)) {
|
|
185
|
+
hideLines.add(locEnd.line + 1)
|
|
186
|
+
}
|
|
187
|
+
else comments[locEnd.line][locEnd.column] = text
|
|
184
188
|
}
|
|
185
189
|
}
|
|
186
190
|
})
|
|
@@ -189,6 +193,12 @@ export const metaAnalysis = async (filePath: string): Promise<RoutesMeta> => {
|
|
|
189
193
|
const headerLine = node.loc?.start.line || -1
|
|
190
194
|
const headerCol = node.loc?.start.column || -1
|
|
191
195
|
const headerCom = comments?.[headerLine]?.[headerCol - 1] ? comments[headerLine][headerCol - 1] : ''
|
|
196
|
+
const hide = hideLines.has(headerLine)
|
|
197
|
+
if (hide) meta.hide = true
|
|
198
|
+
if (ignoredLines.has(headerLine)) {
|
|
199
|
+
meta.ignore = true
|
|
200
|
+
return meta
|
|
201
|
+
}
|
|
192
202
|
const headerRef = parseComment(headerCom)
|
|
193
203
|
meta.header = headerRef
|
|
194
204
|
|
|
@@ -202,17 +212,16 @@ export const metaAnalysis = async (filePath: string): Promise<RoutesMeta> => {
|
|
|
202
212
|
// @ts-ignore
|
|
203
213
|
if (node?.callee?.object?.name === galbeIdentifier) {
|
|
204
214
|
// @ts-ignore
|
|
205
|
-
|
|
215
|
+
let path = node.arguments[0].value
|
|
216
|
+
if (!path?.startsWith("/")) path = `/${path}`
|
|
206
217
|
// @ts-ignore
|
|
207
218
|
const method = node.callee.property.name as Method
|
|
208
219
|
const line = node.loc?.start.line || -1
|
|
209
220
|
const col = node.loc?.start.column || -1
|
|
210
221
|
const com = comments?.[line]?.[col - 1] ? comments[line][col - 1] : ''
|
|
211
|
-
|
|
212
|
-
const routeRefs = parseComment(com)
|
|
213
|
-
|
|
222
|
+
const routeRefs = ignoredLines.has(line) ? { ignore: true } : { ...parseComment(com), ...(hide || hideLines.has(line) ? { hide: true } : {}) }
|
|
214
223
|
if (!(path in meta.routes)) meta.routes[path] = {}
|
|
215
|
-
if (!(method in meta.routes[path])) meta.routes[path][method] = routeRefs
|
|
224
|
+
if (!(method in meta.routes[path])) meta.routes[path][method] = routeRefs as RouteMeta
|
|
216
225
|
}
|
|
217
226
|
}
|
|
218
227
|
})
|
|
@@ -223,11 +232,9 @@ export const metaAnalysis = async (filePath: string): Promise<RoutesMeta> => {
|
|
|
223
232
|
|
|
224
233
|
export const defineRoutes = async (
|
|
225
234
|
options: Pick<GalbeConfig, 'routes'>,
|
|
226
|
-
|
|
227
|
-
cb?: RouteInstanciationCallback
|
|
235
|
+
proxy: GalbeProxy,
|
|
228
236
|
) => {
|
|
229
237
|
const routes = options?.routes === true ? DEFAULT_ROUTE_PATTERN : options?.routes
|
|
230
|
-
const proxy = new GalbeProxy(galbe, cb)
|
|
231
238
|
if (!routes) return
|
|
232
239
|
const root = process.cwd()
|
|
233
240
|
if (typeof routes === 'string') {
|
|
@@ -240,22 +247,23 @@ export const defineRoutes = async (
|
|
|
240
247
|
for (const f of files) {
|
|
241
248
|
try {
|
|
242
249
|
const metadata = await metaAnalysis(f)
|
|
243
|
-
|
|
244
|
-
proxy.
|
|
245
|
-
|
|
250
|
+
if (metadata?.ignore) continue
|
|
251
|
+
proxy._filepath = f
|
|
252
|
+
proxy._metaTmp = metadata
|
|
253
|
+
proxy.meta = [...proxy.meta, { file: path, ...metadata }]
|
|
246
254
|
const imported = await import(f)
|
|
247
255
|
if (!imported?.default) throw new Error('No default export function')
|
|
248
256
|
if (typeof imported.default !== 'function') throw new Error('Default export must be a function')
|
|
249
257
|
const routes = imported.default
|
|
250
258
|
routes(proxy)
|
|
251
259
|
} catch (err: any) {
|
|
252
|
-
if (
|
|
260
|
+
if (proxy._cb) await proxy._cb({ type: 'error', error: err, filepath: f, route: undefined, meta: undefined })
|
|
253
261
|
}
|
|
254
262
|
}
|
|
255
263
|
}
|
|
256
264
|
} else if (Array.isArray(routes)) {
|
|
257
265
|
for (const r of routes) {
|
|
258
|
-
await defineRoutes({ routes: r },
|
|
266
|
+
await defineRoutes({ routes: r }, proxy)
|
|
259
267
|
}
|
|
260
268
|
}
|
|
261
269
|
}
|
package/src/server.ts
CHANGED
|
@@ -28,7 +28,7 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
|
|
|
28
28
|
galbe.config.basePath = `/${galbe?.config?.basePath}`
|
|
29
29
|
let pluginsCb = setupPluginCallbacks(galbe)
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
const server = Bun.serve({
|
|
32
32
|
port: port || galbe.config?.port || 3000,
|
|
33
33
|
hostname: hostname || galbe.config?.hostname || 'localhost',
|
|
34
34
|
tls: galbe.config?.tls,
|
|
@@ -37,7 +37,8 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
|
|
|
37
37
|
if (!METHODS.includes(req.method)) return new Response('', { status: 501 })
|
|
38
38
|
const context = {
|
|
39
39
|
request: req,
|
|
40
|
-
|
|
40
|
+
remoteAddress: server.requestIP(req),
|
|
41
|
+
set: { headers: { 'set-cookie': [] } },
|
|
41
42
|
state: {}
|
|
42
43
|
} as MakeOptional<Context, 'headers' | 'params' | 'query' | 'body'>
|
|
43
44
|
for (const p of pluginsCb.onFetch) {
|
|
@@ -128,12 +129,12 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
|
|
|
128
129
|
if (nextCalled) console.error('Hook already called - ignored')
|
|
129
130
|
else {
|
|
130
131
|
nextCalled = true
|
|
131
|
-
await callChain[idx + 1].call()
|
|
132
|
+
return await callChain[idx + 1].call()
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
let r = await hook(context as Context, next)
|
|
135
136
|
if (r) return r
|
|
136
|
-
if (!nextCalled && !handlerCalled) await next()
|
|
137
|
+
if (!nextCalled && !handlerCalled) return await next()
|
|
137
138
|
}
|
|
138
139
|
}))
|
|
139
140
|
callChain.push({
|
|
@@ -175,7 +176,7 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
|
|
|
175
176
|
if (typeof error.payload === 'string') payload = error.payload
|
|
176
177
|
try {
|
|
177
178
|
payload = JSON.stringify(error.payload)
|
|
178
|
-
} catch (err) {}
|
|
179
|
+
} catch (err) { }
|
|
179
180
|
return new Response(payload, {
|
|
180
181
|
status: error.status,
|
|
181
182
|
headers: { 'Content-Type': 'application/json' }
|
|
@@ -199,4 +200,5 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
|
|
|
199
200
|
})
|
|
200
201
|
}
|
|
201
202
|
})
|
|
203
|
+
return server
|
|
202
204
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ServeOptions, TLSOptions, TLSServeOptions } from 'bun'
|
|
1
|
+
import type { ServeOptions, SocketAddress, TLSOptions, TLSServeOptions } from 'bun'
|
|
2
2
|
import type {
|
|
3
3
|
STAny,
|
|
4
4
|
STArray,
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
STJson,
|
|
9
9
|
STLiteral,
|
|
10
10
|
STMultipartForm,
|
|
11
|
+
STNull,
|
|
11
12
|
STNumber,
|
|
12
13
|
STObject,
|
|
13
14
|
STOptional,
|
|
@@ -19,6 +20,7 @@ import type {
|
|
|
19
20
|
Static
|
|
20
21
|
} from './schema'
|
|
21
22
|
import type { Galbe } from './index'
|
|
23
|
+
import { HttpStatus } from './util'
|
|
22
24
|
|
|
23
25
|
export type STBody =
|
|
24
26
|
| STByteArray
|
|
@@ -33,6 +35,7 @@ export type STBody =
|
|
|
33
35
|
| STMultipartForm
|
|
34
36
|
| STUnion
|
|
35
37
|
| STStream
|
|
38
|
+
| STAny
|
|
36
39
|
| undefined
|
|
37
40
|
|
|
38
41
|
export type STResponseValue =
|
|
@@ -48,7 +51,8 @@ export type STResponseValue =
|
|
|
48
51
|
| STUnion
|
|
49
52
|
| STStream
|
|
50
53
|
| STAny
|
|
51
|
-
|
|
54
|
+
| STNull
|
|
55
|
+
export type STResponse = Record<number | 'default', STResponseValue>
|
|
52
56
|
|
|
53
57
|
export type MaybeArray<T> = T | T[]
|
|
54
58
|
export type MaybeSTArray<T extends STSchema> = T | STArray<T>
|
|
@@ -132,7 +136,7 @@ export type RequestSchema<
|
|
|
132
136
|
P extends Partial<STParams<Path>> = Partial<STParams<Path>>,
|
|
133
137
|
Q extends STQuery = STQuery,
|
|
134
138
|
B extends STBody = STBody,
|
|
135
|
-
R extends STResponse = STResponse
|
|
139
|
+
R extends Partial<STResponse> = Partial<STResponse>
|
|
136
140
|
> = {
|
|
137
141
|
headers?: H
|
|
138
142
|
params?: P
|
|
@@ -145,9 +149,9 @@ type OmitNotDefined<S extends RequestSchema> = {
|
|
|
145
149
|
[K in keyof Exclude<S['params'], undefined> as Exclude<S['params'], undefined>[K] extends Required<
|
|
146
150
|
Exclude<S['params'], undefined>
|
|
147
151
|
>[K]
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
? K
|
|
153
|
+
: //@ts-ignore
|
|
154
|
+
never]: Static<STObject<Exclude<S['params'], undefined>>>[K]
|
|
151
155
|
}
|
|
152
156
|
type StaticBody<T extends STSchema> = T extends STOptional<STSchema> ? Static<T> | null : Static<T>
|
|
153
157
|
export type Context<
|
|
@@ -162,16 +166,18 @@ export type Context<
|
|
|
162
166
|
query: Static<STObject<Exclude<S['query'], undefined>>>
|
|
163
167
|
body: M extends 'get' | 'options' | 'head' ? null : StaticBody<Exclude<S['body'], undefined>>
|
|
164
168
|
request: Request
|
|
169
|
+
remoteAddress: SocketAddress | null
|
|
165
170
|
route?: Route
|
|
166
171
|
state: Record<string, any>
|
|
167
172
|
set: {
|
|
168
173
|
headers: {
|
|
169
|
-
|
|
174
|
+
'set-cookie': string[]
|
|
175
|
+
[header: string]: string | string[]
|
|
170
176
|
}
|
|
171
177
|
status?: number
|
|
172
178
|
}
|
|
173
179
|
}
|
|
174
|
-
export type Next = () => void | Promise<
|
|
180
|
+
export type Next = () => void | Promise<any>
|
|
175
181
|
export type Hook<M extends Method = Method, Path extends string = string, S extends RequestSchema = RequestSchema> = (
|
|
176
182
|
ctx: Context<M, Path, S>,
|
|
177
183
|
next: Next
|
|
@@ -188,49 +194,54 @@ export type Endpoint<M extends Method> = {
|
|
|
188
194
|
H extends STHeaders = any,
|
|
189
195
|
Q extends STQuery = any,
|
|
190
196
|
B extends STBody = any,
|
|
191
|
-
R extends STResponse = STResponse
|
|
197
|
+
R extends Partial<STResponse> = Partial<STResponse>
|
|
192
198
|
>(
|
|
193
199
|
path: Path,
|
|
194
200
|
schema: RequestSchema<M, Path, H, P, Q, B, R>,
|
|
195
201
|
hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[],
|
|
196
202
|
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
197
|
-
):
|
|
203
|
+
): Route<M, Path, P, H, Q, B, R>
|
|
198
204
|
<
|
|
199
205
|
Path extends string,
|
|
200
206
|
P extends Partial<STParams<Path>>,
|
|
201
207
|
H extends STHeaders = any,
|
|
202
208
|
Q extends STQuery = any,
|
|
203
209
|
B extends STBody = any,
|
|
204
|
-
R extends STResponse = STResponse
|
|
210
|
+
R extends Partial<STResponse> = Partial<STResponse>
|
|
205
211
|
>(
|
|
206
212
|
path: Path,
|
|
207
213
|
schema: RequestSchema<M, Path, H, P, Q, B, R>,
|
|
208
214
|
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
209
|
-
):
|
|
215
|
+
): Route<M, Path, P, H, Q, B, R>
|
|
210
216
|
<
|
|
211
217
|
Path extends string,
|
|
212
218
|
P extends Partial<STParams<Path>>,
|
|
213
219
|
H extends STHeaders = any,
|
|
214
220
|
Q extends STQuery = any,
|
|
215
221
|
B extends STBody = any,
|
|
216
|
-
R extends STResponse = STResponse
|
|
222
|
+
R extends Partial<STResponse> = Partial<STResponse>
|
|
217
223
|
>(
|
|
218
224
|
path: Path,
|
|
219
225
|
hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[],
|
|
220
226
|
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
221
|
-
):
|
|
227
|
+
): Route<M, Path, P, H, Q, B, R>
|
|
222
228
|
<
|
|
223
229
|
Path extends string,
|
|
224
230
|
P extends Partial<STParams<Path>>,
|
|
225
231
|
H extends STHeaders = any,
|
|
226
232
|
Q extends STQuery = any,
|
|
227
233
|
B extends STBody = any,
|
|
228
|
-
R extends STResponse = STResponse
|
|
234
|
+
R extends Partial<STResponse> = Partial<STResponse>
|
|
229
235
|
>(
|
|
230
236
|
path: Path,
|
|
231
237
|
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
232
|
-
):
|
|
238
|
+
): Route<M, Path, P, H, Q, B, R>
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export type StaticEndpointOptions = {
|
|
242
|
+
resolve?: (path: string, target: string) => string | null | undefined | void
|
|
233
243
|
}
|
|
244
|
+
export type StaticEndpoint<P extends string = string, T extends string = string> = (path: P, target: T, options?: StaticEndpointOptions) => Route<"get", P, {}, {}, {}, STBody, STResponse, T>
|
|
234
245
|
|
|
235
246
|
export class RequestError {
|
|
236
247
|
status: number
|
|
@@ -256,31 +267,40 @@ export type Route<
|
|
|
256
267
|
H extends STHeaders = STHeaders,
|
|
257
268
|
Q extends STQuery = STQuery,
|
|
258
269
|
B extends STBody = STBody,
|
|
259
|
-
R extends STResponse = STResponse
|
|
270
|
+
R extends Partial<STResponse> = Partial<STResponse>,
|
|
271
|
+
SP extends string = string,
|
|
272
|
+
SR extends string = string
|
|
260
273
|
> = {
|
|
261
274
|
method: M
|
|
262
275
|
path: Path
|
|
263
276
|
schema: RequestSchema<M, Path, H, P, Q, B, R>
|
|
264
277
|
context: Context<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
265
|
-
hooks: Hook[]
|
|
278
|
+
hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[]
|
|
266
279
|
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
280
|
+
static?: { path: SP, root: SR }
|
|
267
281
|
}
|
|
268
282
|
|
|
269
283
|
export class NotFoundError extends RequestError {
|
|
270
284
|
constructor(message?: any) {
|
|
271
|
-
super({ status: 404, payload: message ??
|
|
285
|
+
super({ status: 404, payload: message ?? HttpStatus[404] })
|
|
272
286
|
}
|
|
273
287
|
}
|
|
274
288
|
|
|
275
289
|
export class MethodNotAllowedError extends RequestError {
|
|
276
290
|
constructor(message?: any) {
|
|
277
|
-
super({ status: 405, payload: message ??
|
|
291
|
+
super({ status: 405, payload: message ?? HttpStatus[405] })
|
|
278
292
|
}
|
|
279
293
|
}
|
|
280
294
|
|
|
281
295
|
export class InternalError extends RequestError {
|
|
282
296
|
constructor(message?: any) {
|
|
283
|
-
super({ status: 500, payload: message ??
|
|
297
|
+
super({ status: 500, payload: message ?? HttpStatus[500] })
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export class NotImplementedError extends RequestError {
|
|
302
|
+
constructor(message?: any) {
|
|
303
|
+
super({ status: 501, payload: message ?? HttpStatus[501] })
|
|
284
304
|
}
|
|
285
305
|
}
|
|
286
306
|
|
package/src/util.ts
CHANGED
|
@@ -13,16 +13,30 @@ const METHOD_COLOR: Record<string, string> = {
|
|
|
13
13
|
const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g
|
|
14
14
|
|
|
15
15
|
export const logRoute = (
|
|
16
|
-
r: { method: string; path: string },
|
|
16
|
+
r: { method: string; path: string, static?: { path: string, root: string } },
|
|
17
17
|
meta?: RouteMeta,
|
|
18
18
|
format?: { maxPathLength?: number }
|
|
19
19
|
) => {
|
|
20
|
-
let
|
|
21
|
-
let
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
let path = r.path === '' ? '/' : r.path
|
|
21
|
+
let method = r.method
|
|
22
|
+
|
|
23
|
+
let routeLog = ''
|
|
24
|
+
|
|
25
|
+
if (r?.static) {
|
|
26
|
+
routeLog = `[\x1b[0;33m${`STATIC\x1b[0m]`.padEnd(12, ' ')} ${path
|
|
27
|
+
.padEnd(format?.maxPathLength ?? path.length, ' ')} \x1b[0;33m⇒\x1b[0m ${r.static.path}\x1b[0m`
|
|
28
|
+
if (meta?.deprecated) routeLog = `\x1b[0;9m\x1b[38;5;244m${routeLog.replaceAll(ansiRegex, '')}\x1b[0m`
|
|
29
|
+
} else {
|
|
30
|
+
let color = METHOD_COLOR?.[method] || ''
|
|
31
|
+
let [_, summary, _description] = meta?.head?.match(/^([^\n]*)\n\n(.*)/) || []
|
|
32
|
+
if(!summary) _description = meta?.head || ''
|
|
33
|
+
routeLog = `[${color}${`${method.toUpperCase()}\x1b[0m]`.padEnd(12, ' ')} ${path
|
|
34
|
+
.padEnd(format?.maxPathLength ?? path.length, ' ')
|
|
35
|
+
.replaceAll(/:([^\/]+)/g, '\x1b[0;33m:$1\x1b[0m')}${(summary?` ${summary}`:'').replace(/\n/, '')}\x1b[0m`
|
|
36
|
+
if (meta?.deprecated) routeLog = `\x1b[0;9m\x1b[38;5;244m${routeLog.replaceAll(ansiRegex, '')}\x1b[0m`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (routeLog) console.log(` ${routeLog}`)
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
/**
|
package/src/validator.ts
CHANGED
|
@@ -15,29 +15,29 @@ export const validate = (elt: any, schema: STSchema, parse = false): any => {
|
|
|
15
15
|
if (parse) elt = elt === 'true' ? true : elt === 'false' ? false : null
|
|
16
16
|
else throw `Expected boolean, got string.`
|
|
17
17
|
}
|
|
18
|
-
if (elt !== true && elt !== false) throw
|
|
18
|
+
if (elt !== true && elt !== false) throw `Not a valid boolean. Should be 'true' or 'false'`
|
|
19
19
|
} else if (schema[Kind] === 'integer') {
|
|
20
20
|
if (parse && typeof elt === 'string') elt = Number(elt)
|
|
21
|
-
if (!Number.isInteger(elt)) throw
|
|
21
|
+
if (!Number.isInteger(elt)) throw `Not a valid integer`
|
|
22
22
|
schemaValidation(elt, schema)
|
|
23
23
|
} else if (schema[Kind] === 'number') {
|
|
24
24
|
if (parse && typeof elt === 'string') elt = Number(elt)
|
|
25
|
-
if (!Number.isFinite(elt)) throw
|
|
25
|
+
if (!Number.isFinite(elt)) throw `Not a valid number`
|
|
26
26
|
schemaValidation(elt, schema)
|
|
27
27
|
} else if (schema[Kind] === 'string') {
|
|
28
|
-
if (!(typeof elt === 'string')) throw
|
|
28
|
+
if (!(typeof elt === 'string')) throw `Not a valid string`
|
|
29
29
|
schemaValidation(elt, schema)
|
|
30
30
|
} else if (schema[Kind] === 'literal') {
|
|
31
|
-
if (elt !== schema.value) throw
|
|
31
|
+
if (elt !== schema.value) throw `Not a valid value`
|
|
32
32
|
} else if (schema[Kind] === 'object') {
|
|
33
33
|
if (parse && typeof elt === 'string') {
|
|
34
34
|
try {
|
|
35
35
|
elt = JSON.parse(elt)
|
|
36
36
|
} catch {
|
|
37
|
-
throw
|
|
37
|
+
throw `Not a valid object`
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
if (typeof elt !== 'object') throw
|
|
40
|
+
if (typeof elt !== 'object') throw `Not a valid object`
|
|
41
41
|
if (Array.isArray(elt)) throw `Expected an object, not an array`
|
|
42
42
|
const err: ValidationError = {}
|
|
43
43
|
Object.entries(schema.props as STProps).forEach(([k, s]) => {
|
|
@@ -81,7 +81,7 @@ export const validate = (elt: any, schema: STSchema, parse = false): any => {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
// @ts-ignore
|
|
84
|
-
if (!valid) throw
|
|
84
|
+
if (!valid) throw `Could not be parsed to any of [${union.map(u => u?.value ?? u[Kind]).join(', ')}]`
|
|
85
85
|
} else if (schema[Kind] === 'any') {
|
|
86
86
|
} else {
|
|
87
87
|
throw `Unsupported schema type ${schema[Kind]}`
|
|
@@ -95,7 +95,7 @@ export const validate = (elt: any, schema: STSchema, parse = false): any => {
|
|
|
95
95
|
|
|
96
96
|
export const validateResponse = (response: any, schema: STResponse, status: number) => {
|
|
97
97
|
if (!(status in schema)) return
|
|
98
|
-
const s = schema[status]
|
|
98
|
+
const s = schema?.[status] || schema?.['default']
|
|
99
99
|
if (response instanceof ReadableStream) {
|
|
100
100
|
if (!s[Stream]) throw new InternalError(`Expected ${s[Kind]} response, but got ReadableStream`)
|
|
101
101
|
} else if (isIterator(response)) {
|
|
@@ -113,20 +113,20 @@ const schemaValidation = (value: any, schema: STSchema) => {
|
|
|
113
113
|
const errors = []
|
|
114
114
|
if (schema[Kind] === 'integer' || schema[Kind] === 'number') {
|
|
115
115
|
if (schema.exclusiveMin !== undefined)
|
|
116
|
-
if ((value as number) <= schema.exclusiveMin) errors.push(
|
|
116
|
+
if ((value as number) <= schema.exclusiveMin) errors.push(`Is less or equal to ${schema.exclusiveMin}`)
|
|
117
117
|
if (schema.exclusiveMax !== undefined)
|
|
118
118
|
if ((value as number) >= schema.exclusiveMax)
|
|
119
|
-
errors.push(
|
|
120
|
-
if (schema.min !== undefined) if ((value as number) < schema.min) errors.push(
|
|
119
|
+
errors.push(`Is greater or equal to ${schema.exclusiveMax}`)
|
|
120
|
+
if (schema.min !== undefined) if ((value as number) < schema.min) errors.push(`Is less than ${schema.min}`)
|
|
121
121
|
if (schema.max !== undefined)
|
|
122
|
-
if ((value as number) > schema.max) errors.push(
|
|
122
|
+
if ((value as number) > schema.max) errors.push(`Is greater than ${schema.max}`)
|
|
123
123
|
} else if (schema[Kind] === 'string') {
|
|
124
124
|
if (schema.minLength !== undefined && (value as string).length < schema.minLength)
|
|
125
|
-
errors.push(
|
|
125
|
+
errors.push(`Length is too small (${schema.minLength} char min)`)
|
|
126
126
|
if (schema.maxLength !== undefined && (value as string).length > schema.maxLength)
|
|
127
|
-
errors.push(
|
|
127
|
+
errors.push(`Length is too large (${schema.maxLength} char max)`)
|
|
128
128
|
if (schema.pattern !== undefined && !(value as string).match(schema.pattern))
|
|
129
|
-
errors.push(
|
|
129
|
+
errors.push(`Does not match pattern ${schema.pattern}`)
|
|
130
130
|
} else if (schema[Kind] === 'array') {
|
|
131
131
|
if (schema.minItems !== undefined && (value as any[]).length < schema.minItems)
|
|
132
132
|
errors.push(`Must contain at least ${schema.minItems} item${schema.minItems > 1 ? 's' : ''}`)
|