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/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,101 +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() {
|
|
39
48
|
return this.#g.server
|
|
40
49
|
}
|
|
41
|
-
|
|
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
|
+
|
|
42
78
|
//@ts-ignore
|
|
43
|
-
const route = this.#g
|
|
44
|
-
|
|
45
|
-
|
|
79
|
+
const route = this.#g[method](...args) as Route
|
|
80
|
+
|
|
81
|
+
if (this._cb && !meta?.ignore) {
|
|
82
|
+
await this._cb({
|
|
46
83
|
type: 'add',
|
|
47
84
|
route,
|
|
48
|
-
filepath: this.
|
|
49
|
-
meta
|
|
85
|
+
filepath: this._filepath || '',
|
|
86
|
+
meta
|
|
50
87
|
})
|
|
88
|
+
}
|
|
51
89
|
return route
|
|
52
90
|
}
|
|
91
|
+
async get(...args: any[]) {
|
|
92
|
+
return this.handleRoute('get', ...args)
|
|
93
|
+
}
|
|
53
94
|
async post(...args: any[]) {
|
|
54
|
-
|
|
55
|
-
const route = this.#g.post(...args) as Route
|
|
56
|
-
if (this.#cb)
|
|
57
|
-
await this.#cb({
|
|
58
|
-
type: 'add',
|
|
59
|
-
route,
|
|
60
|
-
filepath: this.filepath,
|
|
61
|
-
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
62
|
-
})
|
|
63
|
-
return route
|
|
95
|
+
return this.handleRoute('post', ...args)
|
|
64
96
|
}
|
|
65
97
|
async put(...args: any[]) {
|
|
66
|
-
|
|
67
|
-
const route = this.#g.put(...args) as Route
|
|
68
|
-
if (this.#cb)
|
|
69
|
-
await this.#cb({
|
|
70
|
-
type: 'add',
|
|
71
|
-
route,
|
|
72
|
-
filepath: this.filepath,
|
|
73
|
-
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
74
|
-
})
|
|
75
|
-
return route
|
|
98
|
+
return this.handleRoute('put', ...args)
|
|
76
99
|
}
|
|
77
100
|
async patch(...args: any[]) {
|
|
78
|
-
|
|
79
|
-
const route = this.#g.patch(...args) as Route
|
|
80
|
-
if (this.#cb)
|
|
81
|
-
await this.#cb({
|
|
82
|
-
type: 'add',
|
|
83
|
-
route,
|
|
84
|
-
filepath: this.filepath,
|
|
85
|
-
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
86
|
-
})
|
|
87
|
-
return route
|
|
101
|
+
return this.handleRoute('patch', ...args)
|
|
88
102
|
}
|
|
89
103
|
async delete(...args: any[]) {
|
|
90
|
-
|
|
91
|
-
const route = this.#g.delete(...args) as Route
|
|
92
|
-
if (this.#cb)
|
|
93
|
-
await this.#cb({
|
|
94
|
-
type: 'add',
|
|
95
|
-
route,
|
|
96
|
-
filepath: this.filepath,
|
|
97
|
-
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
98
|
-
})
|
|
99
|
-
return route
|
|
104
|
+
return this.handleRoute('delete', ...args)
|
|
100
105
|
}
|
|
101
106
|
async options(...args: any[]) {
|
|
102
|
-
|
|
103
|
-
const route = this.#g.options(...args) as Route
|
|
104
|
-
if (this.#cb)
|
|
105
|
-
await this.#cb({
|
|
106
|
-
type: 'add',
|
|
107
|
-
route,
|
|
108
|
-
filepath: this.filepath,
|
|
109
|
-
meta: this.meta?.routes?.[route.path]?.[route.method] || {}
|
|
110
|
-
})
|
|
111
|
-
return route
|
|
107
|
+
return this.handleRoute('options', ...args)
|
|
112
108
|
}
|
|
113
109
|
async head(...args: any[]) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
})
|
|
123
|
-
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)
|
|
124
118
|
}
|
|
125
119
|
}
|
|
126
120
|
|
|
@@ -175,15 +169,22 @@ export const metaAnalysis = async (filePath: string): Promise<RoutesMeta> => {
|
|
|
175
169
|
}
|
|
176
170
|
|
|
177
171
|
const comments: Record<number, Record<number, string>> = {}
|
|
178
|
-
|
|
172
|
+
const ignoredLines = new Set<number>()
|
|
173
|
+
const hideLines = new Set<number>()
|
|
179
174
|
const ast = parse(content, {
|
|
180
175
|
ecmaVersion: 'latest',
|
|
181
176
|
sourceType: 'module',
|
|
182
177
|
locations: true,
|
|
183
|
-
onComment: (
|
|
184
|
-
if (
|
|
178
|
+
onComment: (_isBlock, text, _start, _end, _locStart, locEnd) => {
|
|
179
|
+
if (locEnd?.line !== undefined && locEnd?.column !== undefined) {
|
|
185
180
|
if (!comments?.[locEnd.line]) comments[locEnd.line] = []
|
|
186
|
-
|
|
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
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
})
|
|
@@ -192,6 +193,12 @@ export const metaAnalysis = async (filePath: string): Promise<RoutesMeta> => {
|
|
|
192
193
|
const headerLine = node.loc?.start.line || -1
|
|
193
194
|
const headerCol = node.loc?.start.column || -1
|
|
194
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
|
+
}
|
|
195
202
|
const headerRef = parseComment(headerCom)
|
|
196
203
|
meta.header = headerRef
|
|
197
204
|
|
|
@@ -205,17 +212,16 @@ export const metaAnalysis = async (filePath: string): Promise<RoutesMeta> => {
|
|
|
205
212
|
// @ts-ignore
|
|
206
213
|
if (node?.callee?.object?.name === galbeIdentifier) {
|
|
207
214
|
// @ts-ignore
|
|
208
|
-
|
|
215
|
+
let path = node.arguments[0].value
|
|
216
|
+
if (!path?.startsWith("/")) path = `/${path}`
|
|
209
217
|
// @ts-ignore
|
|
210
218
|
const method = node.callee.property.name as Method
|
|
211
219
|
const line = node.loc?.start.line || -1
|
|
212
220
|
const col = node.loc?.start.column || -1
|
|
213
221
|
const com = comments?.[line]?.[col - 1] ? comments[line][col - 1] : ''
|
|
214
|
-
|
|
215
|
-
const routeRefs = parseComment(com)
|
|
216
|
-
|
|
222
|
+
const routeRefs = ignoredLines.has(line) ? { ignore: true } : { ...parseComment(com), ...(hide || hideLines.has(line) ? { hide: true } : {}) }
|
|
217
223
|
if (!(path in meta.routes)) meta.routes[path] = {}
|
|
218
|
-
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
|
|
219
225
|
}
|
|
220
226
|
}
|
|
221
227
|
})
|
|
@@ -226,11 +232,9 @@ export const metaAnalysis = async (filePath: string): Promise<RoutesMeta> => {
|
|
|
226
232
|
|
|
227
233
|
export const defineRoutes = async (
|
|
228
234
|
options: Pick<GalbeConfig, 'routes'>,
|
|
229
|
-
|
|
230
|
-
cb?: RouteInstanciationCallback
|
|
235
|
+
proxy: GalbeProxy,
|
|
231
236
|
) => {
|
|
232
237
|
const routes = options?.routes === true ? DEFAULT_ROUTE_PATTERN : options?.routes
|
|
233
|
-
const proxy = new GalbeProxy(galbe, cb)
|
|
234
238
|
if (!routes) return
|
|
235
239
|
const root = process.cwd()
|
|
236
240
|
if (typeof routes === 'string') {
|
|
@@ -243,22 +247,23 @@ export const defineRoutes = async (
|
|
|
243
247
|
for (const f of files) {
|
|
244
248
|
try {
|
|
245
249
|
const metadata = await metaAnalysis(f)
|
|
246
|
-
|
|
247
|
-
proxy.
|
|
248
|
-
|
|
250
|
+
if (metadata?.ignore) continue
|
|
251
|
+
proxy._filepath = f
|
|
252
|
+
proxy._metaTmp = metadata
|
|
253
|
+
proxy.meta = [...proxy.meta, { file: path, ...metadata }]
|
|
249
254
|
const imported = await import(f)
|
|
250
255
|
if (!imported?.default) throw new Error('No default export function')
|
|
251
256
|
if (typeof imported.default !== 'function') throw new Error('Default export must be a function')
|
|
252
257
|
const routes = imported.default
|
|
253
258
|
routes(proxy)
|
|
254
259
|
} catch (err: any) {
|
|
255
|
-
if (
|
|
260
|
+
if (proxy._cb) await proxy._cb({ type: 'error', error: err, filepath: f, route: undefined, meta: undefined })
|
|
256
261
|
}
|
|
257
262
|
}
|
|
258
263
|
}
|
|
259
264
|
} else if (Array.isArray(routes)) {
|
|
260
265
|
for (const r of routes) {
|
|
261
|
-
await defineRoutes({ routes: r },
|
|
266
|
+
await defineRoutes({ routes: r }, proxy)
|
|
262
267
|
}
|
|
263
268
|
}
|
|
264
269
|
}
|
package/src/server.ts
CHANGED
|
@@ -38,7 +38,7 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
|
|
|
38
38
|
const context = {
|
|
39
39
|
request: req,
|
|
40
40
|
remoteAddress: server.requestIP(req),
|
|
41
|
-
set: { headers: {} },
|
|
41
|
+
set: { headers: { 'set-cookie': [] } },
|
|
42
42
|
state: {}
|
|
43
43
|
} as MakeOptional<Context, 'headers' | 'params' | 'query' | 'body'>
|
|
44
44
|
for (const p of pluginsCb.onFetch) {
|
|
@@ -176,7 +176,7 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
|
|
|
176
176
|
if (typeof error.payload === 'string') payload = error.payload
|
|
177
177
|
try {
|
|
178
178
|
payload = JSON.stringify(error.payload)
|
|
179
|
-
} catch (err) {}
|
|
179
|
+
} catch (err) { }
|
|
180
180
|
return new Response(payload, {
|
|
181
181
|
status: error.status,
|
|
182
182
|
headers: { 'Content-Type': 'application/json' }
|
package/src/types.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
Static
|
|
21
21
|
} from './schema'
|
|
22
22
|
import type { Galbe } from './index'
|
|
23
|
+
import { HttpStatus } from './util'
|
|
23
24
|
|
|
24
25
|
export type STBody =
|
|
25
26
|
| STByteArray
|
|
@@ -34,6 +35,7 @@ export type STBody =
|
|
|
34
35
|
| STMultipartForm
|
|
35
36
|
| STUnion
|
|
36
37
|
| STStream
|
|
38
|
+
| STAny
|
|
37
39
|
| undefined
|
|
38
40
|
|
|
39
41
|
export type STResponseValue =
|
|
@@ -50,7 +52,7 @@ export type STResponseValue =
|
|
|
50
52
|
| STStream
|
|
51
53
|
| STAny
|
|
52
54
|
| STNull
|
|
53
|
-
export type STResponse = Record<number, STResponseValue
|
|
55
|
+
export type STResponse = Partial<Record<number | 'default', STResponseValue>>
|
|
54
56
|
|
|
55
57
|
export type MaybeArray<T> = T | T[]
|
|
56
58
|
export type MaybeSTArray<T extends STSchema> = T | STArray<T>
|
|
@@ -134,7 +136,7 @@ export type RequestSchema<
|
|
|
134
136
|
P extends Partial<STParams<Path>> = Partial<STParams<Path>>,
|
|
135
137
|
Q extends STQuery = STQuery,
|
|
136
138
|
B extends STBody = STBody,
|
|
137
|
-
R extends STResponse = STResponse
|
|
139
|
+
R extends Partial<STResponse> = STResponse
|
|
138
140
|
> = {
|
|
139
141
|
headers?: H
|
|
140
142
|
params?: P
|
|
@@ -147,9 +149,9 @@ type OmitNotDefined<S extends RequestSchema> = {
|
|
|
147
149
|
[K in keyof Exclude<S['params'], undefined> as Exclude<S['params'], undefined>[K] extends Required<
|
|
148
150
|
Exclude<S['params'], undefined>
|
|
149
151
|
>[K]
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
? K
|
|
153
|
+
: //@ts-ignore
|
|
154
|
+
never]: Static<STObject<Exclude<S['params'], undefined>>>[K]
|
|
153
155
|
}
|
|
154
156
|
type StaticBody<T extends STSchema> = T extends STOptional<STSchema> ? Static<T> | null : Static<T>
|
|
155
157
|
export type Context<
|
|
@@ -169,7 +171,8 @@ export type Context<
|
|
|
169
171
|
state: Record<string, any>
|
|
170
172
|
set: {
|
|
171
173
|
headers: {
|
|
172
|
-
|
|
174
|
+
'set-cookie': string[]
|
|
175
|
+
[header: string]: string | string[]
|
|
173
176
|
}
|
|
174
177
|
status?: number
|
|
175
178
|
}
|
|
@@ -197,7 +200,7 @@ export type Endpoint<M extends Method> = {
|
|
|
197
200
|
schema: RequestSchema<M, Path, H, P, Q, B, R>,
|
|
198
201
|
hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[],
|
|
199
202
|
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
200
|
-
):
|
|
203
|
+
): Route<M, Path, P, H, Q, B, R>
|
|
201
204
|
<
|
|
202
205
|
Path extends string,
|
|
203
206
|
P extends Partial<STParams<Path>>,
|
|
@@ -209,7 +212,7 @@ export type Endpoint<M extends Method> = {
|
|
|
209
212
|
path: Path,
|
|
210
213
|
schema: RequestSchema<M, Path, H, P, Q, B, R>,
|
|
211
214
|
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
212
|
-
):
|
|
215
|
+
): Route<M, Path, P, H, Q, B, R>
|
|
213
216
|
<
|
|
214
217
|
Path extends string,
|
|
215
218
|
P extends Partial<STParams<Path>>,
|
|
@@ -221,7 +224,7 @@ export type Endpoint<M extends Method> = {
|
|
|
221
224
|
path: Path,
|
|
222
225
|
hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[],
|
|
223
226
|
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
224
|
-
):
|
|
227
|
+
): Route<M, Path, P, H, Q, B, R>
|
|
225
228
|
<
|
|
226
229
|
Path extends string,
|
|
227
230
|
P extends Partial<STParams<Path>>,
|
|
@@ -232,8 +235,13 @@ export type Endpoint<M extends Method> = {
|
|
|
232
235
|
>(
|
|
233
236
|
path: Path,
|
|
234
237
|
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
235
|
-
):
|
|
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
|
|
236
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>
|
|
237
245
|
|
|
238
246
|
export class RequestError {
|
|
239
247
|
status: number
|
|
@@ -259,31 +267,40 @@ export type Route<
|
|
|
259
267
|
H extends STHeaders = STHeaders,
|
|
260
268
|
Q extends STQuery = STQuery,
|
|
261
269
|
B extends STBody = STBody,
|
|
262
|
-
R extends STResponse = STResponse
|
|
270
|
+
R extends STResponse = STResponse,
|
|
271
|
+
SP extends string = string,
|
|
272
|
+
SR extends string = string
|
|
263
273
|
> = {
|
|
264
274
|
method: M
|
|
265
275
|
path: Path
|
|
266
276
|
schema: RequestSchema<M, Path, H, P, Q, B, R>
|
|
267
277
|
context: Context<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
268
|
-
hooks: Hook[]
|
|
278
|
+
hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[]
|
|
269
279
|
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
280
|
+
static?: { path: SP, root: SR }
|
|
270
281
|
}
|
|
271
282
|
|
|
272
283
|
export class NotFoundError extends RequestError {
|
|
273
284
|
constructor(message?: any) {
|
|
274
|
-
super({ status: 404, payload: message ??
|
|
285
|
+
super({ status: 404, payload: message ?? HttpStatus[404] })
|
|
275
286
|
}
|
|
276
287
|
}
|
|
277
288
|
|
|
278
289
|
export class MethodNotAllowedError extends RequestError {
|
|
279
290
|
constructor(message?: any) {
|
|
280
|
-
super({ status: 405, payload: message ??
|
|
291
|
+
super({ status: 405, payload: message ?? HttpStatus[405] })
|
|
281
292
|
}
|
|
282
293
|
}
|
|
283
294
|
|
|
284
295
|
export class InternalError extends RequestError {
|
|
285
296
|
constructor(message?: any) {
|
|
286
|
-
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] })
|
|
287
304
|
}
|
|
288
305
|
}
|
|
289
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,8 @@ 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
|
+
if(!s) return
|
|
99
100
|
if (response instanceof ReadableStream) {
|
|
100
101
|
if (!s[Stream]) throw new InternalError(`Expected ${s[Kind]} response, but got ReadableStream`)
|
|
101
102
|
} else if (isIterator(response)) {
|
|
@@ -113,20 +114,20 @@ const schemaValidation = (value: any, schema: STSchema) => {
|
|
|
113
114
|
const errors = []
|
|
114
115
|
if (schema[Kind] === 'integer' || schema[Kind] === 'number') {
|
|
115
116
|
if (schema.exclusiveMin !== undefined)
|
|
116
|
-
if ((value as number) <= schema.exclusiveMin) errors.push(
|
|
117
|
+
if ((value as number) <= schema.exclusiveMin) errors.push(`Is less or equal to ${schema.exclusiveMin}`)
|
|
117
118
|
if (schema.exclusiveMax !== undefined)
|
|
118
119
|
if ((value as number) >= schema.exclusiveMax)
|
|
119
|
-
errors.push(
|
|
120
|
-
if (schema.min !== undefined) if ((value as number) < schema.min) errors.push(
|
|
120
|
+
errors.push(`Is greater or equal to ${schema.exclusiveMax}`)
|
|
121
|
+
if (schema.min !== undefined) if ((value as number) < schema.min) errors.push(`Is less than ${schema.min}`)
|
|
121
122
|
if (schema.max !== undefined)
|
|
122
|
-
if ((value as number) > schema.max) errors.push(
|
|
123
|
+
if ((value as number) > schema.max) errors.push(`Is greater than ${schema.max}`)
|
|
123
124
|
} else if (schema[Kind] === 'string') {
|
|
124
125
|
if (schema.minLength !== undefined && (value as string).length < schema.minLength)
|
|
125
|
-
errors.push(
|
|
126
|
+
errors.push(`Length is too small (${schema.minLength} char min)`)
|
|
126
127
|
if (schema.maxLength !== undefined && (value as string).length > schema.maxLength)
|
|
127
|
-
errors.push(
|
|
128
|
+
errors.push(`Length is too large (${schema.maxLength} char max)`)
|
|
128
129
|
if (schema.pattern !== undefined && !(value as string).match(schema.pattern))
|
|
129
|
-
errors.push(
|
|
130
|
+
errors.push(`Does not match pattern ${schema.pattern}`)
|
|
130
131
|
} else if (schema[Kind] === 'array') {
|
|
131
132
|
if (schema.minItems !== undefined && (value as any[]).length < schema.minItems)
|
|
132
133
|
errors.push(`Must contain at least ${schema.minItems} item${schema.minItems > 1 ? 's' : ''}`)
|