galbe 0.2.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/getting-started.md +8 -0
- package/docs/hooks.md +4 -4
- package/docs/plugins.md +3 -9
- package/docs/routes.md +3 -3
- package/docs/schemas.md +19 -2
- 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 +122 -80
- package/src/parser.ts +29 -8
- package/src/router.ts +27 -28
- package/src/routes.ts +142 -40
- package/src/schema.ts +79 -8
- package/src/server.ts +55 -35
- package/src/types.ts +109 -53
- package/src/util.ts +85 -3
- package/src/validator.ts +27 -5
- 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 +386 -13
- package/test/routeFiles.test.ts +44 -27
- package/test/router.test.ts +67 -42
- package/scripts/build.ts +0 -14
package/src/server.ts
CHANGED
|
@@ -1,8 +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
|
+
import { validateResponse } from './validator'
|
|
7
|
+
|
|
8
|
+
const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']
|
|
9
|
+
const EMPTY_BODY_METHODS = ['GET', 'OPTIONS', 'HEAD']
|
|
6
10
|
|
|
7
11
|
const handleInternalError = (error: any) => {
|
|
8
12
|
console.error(error)
|
|
@@ -10,7 +14,6 @@ const handleInternalError = (error: any) => {
|
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
const setupPluginCallbacks = (galbe: Galbe) => ({
|
|
13
|
-
init: galbe.plugins.filter(p => p.init),
|
|
14
17
|
onFetch: galbe.plugins.filter(p => p.onFetch),
|
|
15
18
|
onRoute: galbe.plugins.filter(p => p.onRoute),
|
|
16
19
|
beforeHandle: galbe.plugins.filter(p => p.beforeHandle),
|
|
@@ -22,12 +25,11 @@ export default async (galbe: Galbe, port?: number) => {
|
|
|
22
25
|
if (galbe?.config?.basePath && galbe?.config?.basePath[0] !== '/')
|
|
23
26
|
galbe.config.basePath = `/${galbe?.config?.basePath}`
|
|
24
27
|
let pluginsCb = setupPluginCallbacks(galbe)
|
|
25
|
-
//@ts-ignore
|
|
26
|
-
for (const p of pluginsCb.init) await p.init(galbe?.config?.plugin?.[p.name], galbe)
|
|
27
28
|
|
|
28
29
|
return Bun.serve({
|
|
29
30
|
port: port || galbe.config?.port || 3000,
|
|
30
31
|
async fetch(req) {
|
|
32
|
+
if (!METHODS.includes(req.method)) return new Response('', { status: 501 })
|
|
31
33
|
const context: Context = {
|
|
32
34
|
request: req,
|
|
33
35
|
set: { headers: {} },
|
|
@@ -48,7 +50,7 @@ export default async (galbe: Galbe, port?: number) => {
|
|
|
48
50
|
try {
|
|
49
51
|
// find route
|
|
50
52
|
try {
|
|
51
|
-
route = router.find(req.method, url.pathname)
|
|
53
|
+
route = router.find(req.method.toLowerCase() as Method, url.pathname)
|
|
52
54
|
} catch (error) {
|
|
53
55
|
if (error instanceof RequestError) throw error
|
|
54
56
|
else throw handleInternalError(error)
|
|
@@ -69,37 +71,41 @@ export default async (galbe: Galbe, port?: number) => {
|
|
|
69
71
|
for (let [k, v] of url.searchParams) inQuery[k] = v
|
|
70
72
|
let inParams = requestPathParser(url.pathname, route.path)
|
|
71
73
|
|
|
72
|
-
context.body =
|
|
74
|
+
context.body = !EMPTY_BODY_METHODS.includes(req.method)
|
|
75
|
+
? await requestBodyParser(req.body, inHeaders, schema.body)
|
|
76
|
+
: null
|
|
73
77
|
context.headers = inHeaders
|
|
74
78
|
context.query = inQuery
|
|
75
79
|
context.params = inParams
|
|
76
80
|
|
|
77
81
|
// request validation
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
82
|
+
if (galbe.config?.requestValidator?.enabled) {
|
|
83
|
+
let errors: RequestError[] = []
|
|
84
|
+
try {
|
|
85
|
+
if (schema?.headers)
|
|
86
|
+
context.headers = {
|
|
87
|
+
...context.headers,
|
|
88
|
+
...parseEntry(context.headers, schema.headers, { name: 'headers', i: true })
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (error instanceof RequestError) errors.push(error)
|
|
92
|
+
else throw handleInternalError(error)
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
if (schema?.query) context.query = parseEntry(context.query, schema.query, { name: 'query' })
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error instanceof RequestError) errors.push(error)
|
|
98
|
+
else throw handleInternalError(error)
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
if (schema?.params) context.params = parseEntry(context.params, schema.params, { name: 'params' })
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error instanceof RequestError) errors.push(error)
|
|
104
|
+
else throw handleInternalError(error)
|
|
105
|
+
}
|
|
106
|
+
if (errors.length) {
|
|
107
|
+
throw new RequestError({ status: 400, payload: errors.reduce((acc, c) => ({ ...acc, ...c.payload }), {}) })
|
|
108
|
+
}
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
for (const p of pluginsCb.beforeHandle) {
|
|
@@ -140,7 +146,10 @@ export default async (galbe: Galbe, port?: number) => {
|
|
|
140
146
|
if (r) response = r
|
|
141
147
|
} else response = await handlerWrapper(context)
|
|
142
148
|
|
|
143
|
-
const parsedResponse = responseParser(response, context)
|
|
149
|
+
const parsedResponse = responseParser(response, context, schema.response)
|
|
150
|
+
|
|
151
|
+
if (galbe.config?.responseValidator?.enabled && schema.response)
|
|
152
|
+
validateResponse(response, schema.response, parsedResponse.status || 200)
|
|
144
153
|
|
|
145
154
|
for (const p of pluginsCb.afterHandle) {
|
|
146
155
|
//@ts-ignore
|
|
@@ -154,12 +163,23 @@ export default async (galbe: Galbe, port?: number) => {
|
|
|
154
163
|
let customError
|
|
155
164
|
if (galbe.errorHandler) customError = responseParser(galbe.errorHandler(error, context), context)
|
|
156
165
|
if (customError) return customError
|
|
157
|
-
if (error instanceof
|
|
158
|
-
|
|
166
|
+
if (error instanceof InternalError) {
|
|
167
|
+
console.log(`Internal Error`, error?.payload || '')
|
|
168
|
+
return new Response('Internal Server Error', {
|
|
159
169
|
status: error.status,
|
|
160
170
|
headers: { 'Content-Type': 'application/json' }
|
|
161
171
|
})
|
|
162
|
-
}
|
|
172
|
+
} else if (error instanceof RequestError) {
|
|
173
|
+
let payload = ''
|
|
174
|
+
if (typeof error.payload === 'string') payload = error.payload
|
|
175
|
+
try {
|
|
176
|
+
payload = JSON.stringify(error.payload)
|
|
177
|
+
} catch (err) {}
|
|
178
|
+
return new Response(payload, {
|
|
179
|
+
status: error.status,
|
|
180
|
+
headers: { 'Content-Type': 'application/json' }
|
|
181
|
+
})
|
|
182
|
+
} else console.log(error)
|
|
163
183
|
return new Response('"Internal Server Error"', {
|
|
164
184
|
status: 500,
|
|
165
185
|
headers: {
|
package/src/types.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { ServeOptions, TLSServeOptions } from 'bun'
|
|
2
2
|
import type {
|
|
3
|
+
STAny,
|
|
3
4
|
STArray,
|
|
4
5
|
STBoolean,
|
|
5
6
|
STByteArray,
|
|
6
7
|
STInteger,
|
|
8
|
+
STJson,
|
|
7
9
|
STLiteral,
|
|
8
10
|
STMultipartForm,
|
|
9
11
|
STNumber,
|
|
10
12
|
STObject,
|
|
13
|
+
STOptional,
|
|
14
|
+
STSchema,
|
|
11
15
|
STStream,
|
|
12
16
|
STString,
|
|
13
17
|
STUnion,
|
|
@@ -22,15 +26,35 @@ export type STBody =
|
|
|
22
26
|
| STBoolean
|
|
23
27
|
| STNumber
|
|
24
28
|
| STInteger
|
|
29
|
+
| STLiteral
|
|
25
30
|
| STObject
|
|
26
31
|
| STArray
|
|
27
32
|
| STUrlForm
|
|
28
33
|
| STMultipartForm
|
|
29
34
|
| STUnion
|
|
30
35
|
| STStream
|
|
36
|
+
| undefined
|
|
37
|
+
|
|
38
|
+
export type STResponseValue =
|
|
39
|
+
| STByteArray
|
|
40
|
+
| STString
|
|
41
|
+
| STBoolean
|
|
42
|
+
| STNumber
|
|
43
|
+
| STInteger
|
|
44
|
+
| STLiteral
|
|
45
|
+
| STObject
|
|
46
|
+
| STJson
|
|
47
|
+
| STArray
|
|
48
|
+
| STUnion
|
|
49
|
+
| STStream
|
|
50
|
+
| STAny
|
|
51
|
+
export type STResponse = Record<number, STResponseValue>
|
|
52
|
+
|
|
31
53
|
export type MaybeArray<T> = T | T[]
|
|
54
|
+
export type MaybeSTArray<T extends STSchema> = T | STArray<T>
|
|
55
|
+
export type MaybeSTUnion<T extends STSchema> = T | STUnion<[T, ...T[]]>
|
|
32
56
|
|
|
33
|
-
export type Method = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options'
|
|
57
|
+
export type Method = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' | 'head'
|
|
34
58
|
type MaybePromise<T> = T | Promise<T>
|
|
35
59
|
|
|
36
60
|
export type ExtractParams<T extends string> = T extends `/:${infer P}/${infer Rest}`
|
|
@@ -41,13 +65,16 @@ export type ExtractParams<T extends string> = T extends `/:${infer P}/${infer Re
|
|
|
41
65
|
? P
|
|
42
66
|
: never
|
|
43
67
|
|
|
44
|
-
type
|
|
68
|
+
type STHeadersPrimaryValue = STString | STBoolean | STNumber | STInteger | STLiteral
|
|
69
|
+
type STHeadersValue = MaybeSTUnion<STHeadersPrimaryValue>
|
|
45
70
|
export type STHeaders = Record<string, STHeadersValue>
|
|
46
71
|
|
|
47
|
-
type
|
|
72
|
+
type STParamsPrimaryValue = STString | STBoolean | STNumber | STInteger | STLiteral
|
|
73
|
+
type STParamsValue = MaybeSTUnion<STParamsPrimaryValue>
|
|
48
74
|
export type STParams<Path extends string = string> = Record<ExtractParams<Path>, STParamsValue>
|
|
49
75
|
|
|
50
|
-
type
|
|
76
|
+
type STQueryPrimaryValue = STString | STBoolean | STNumber | STInteger | STLiteral
|
|
77
|
+
type STQueryValue = MaybeSTArray<MaybeSTUnion<STQueryPrimaryValue>>
|
|
51
78
|
export type STQuery = Record<string, STQueryValue>
|
|
52
79
|
|
|
53
80
|
/**
|
|
@@ -74,6 +101,8 @@ export type GalbeConfig = {
|
|
|
74
101
|
routes?: boolean | string | string[]
|
|
75
102
|
router?: { cacheEnabled: boolean }
|
|
76
103
|
plugin?: Record<string, any>
|
|
104
|
+
requestValidator?: { enabled: boolean }
|
|
105
|
+
responseValidator?: { enabled: boolean }
|
|
77
106
|
}
|
|
78
107
|
/**
|
|
79
108
|
* #### Schema
|
|
@@ -95,16 +124,19 @@ export type GalbeConfig = {
|
|
|
95
124
|
* ```
|
|
96
125
|
*/
|
|
97
126
|
export type RequestSchema<
|
|
127
|
+
M extends Method = Method,
|
|
98
128
|
Path extends string = string,
|
|
99
|
-
H extends STHeaders =
|
|
100
|
-
P extends Partial<STParams<Path>> =
|
|
101
|
-
Q extends STQuery =
|
|
102
|
-
B extends STBody = STBody
|
|
129
|
+
H extends STHeaders = STHeaders,
|
|
130
|
+
P extends Partial<STParams<Path>> = Partial<STParams<Path>>,
|
|
131
|
+
Q extends STQuery = STQuery,
|
|
132
|
+
B extends STBody = STBody,
|
|
133
|
+
R extends STResponse = STResponse
|
|
103
134
|
> = {
|
|
104
135
|
headers?: H
|
|
105
136
|
params?: P
|
|
106
137
|
query?: Q
|
|
107
138
|
body?: B
|
|
139
|
+
response?: R
|
|
108
140
|
}
|
|
109
141
|
|
|
110
142
|
type OmitNotDefined<S extends RequestSchema> = {
|
|
@@ -115,14 +147,18 @@ type OmitNotDefined<S extends RequestSchema> = {
|
|
|
115
147
|
: //@ts-ignore
|
|
116
148
|
never]: Static<STObject<Exclude<S['params'], undefined>>>[K]
|
|
117
149
|
}
|
|
118
|
-
|
|
119
|
-
export type Context<
|
|
150
|
+
type StaticBody<T extends STSchema> = T extends STOptional<STSchema> ? Static<T> | null : Static<T>
|
|
151
|
+
export type Context<
|
|
152
|
+
M extends Method = Method,
|
|
153
|
+
Path extends string = string,
|
|
154
|
+
S extends RequestSchema = RequestSchema
|
|
155
|
+
> = {
|
|
120
156
|
headers: Static<STObject<Exclude<S['headers'], undefined>>>
|
|
121
157
|
params: {
|
|
122
158
|
[K in ExtractParams<Path>]: K extends keyof OmitNotDefined<S> ? OmitNotDefined<S>[K] : string
|
|
123
159
|
}
|
|
124
160
|
query: Static<STObject<Exclude<S['query'], undefined>>>
|
|
125
|
-
body:
|
|
161
|
+
body: M extends 'get' | 'options' | 'head' ? null : StaticBody<Exclude<S['body'], undefined>>
|
|
126
162
|
request: Request
|
|
127
163
|
route?: Route
|
|
128
164
|
state: Record<string, any>
|
|
@@ -134,57 +170,63 @@ export type Context<Path extends string = string, S extends RequestSchema = Requ
|
|
|
134
170
|
}
|
|
135
171
|
}
|
|
136
172
|
export type Next = () => void | Promise<void>
|
|
137
|
-
export type Hook<Path extends string = string, S extends RequestSchema = RequestSchema> = (
|
|
138
|
-
ctx: Context<Path, S>,
|
|
173
|
+
export type Hook<M extends Method = Method, Path extends string = string, S extends RequestSchema = RequestSchema> = (
|
|
174
|
+
ctx: Context<M, Path, S>,
|
|
139
175
|
next: Next
|
|
140
176
|
) => any | Promise<any>
|
|
141
|
-
export type Handler<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
177
|
+
export type Handler<
|
|
178
|
+
M extends Method = Method,
|
|
179
|
+
Path extends string = string,
|
|
180
|
+
S extends RequestSchema = RequestSchema
|
|
181
|
+
> = (ctx: Context<M, Path, S>) => any
|
|
182
|
+
export type Endpoint<M extends Method> = {
|
|
145
183
|
<
|
|
146
184
|
Path extends string,
|
|
147
|
-
H extends STHeaders,
|
|
148
185
|
P extends Partial<STParams<Path>>,
|
|
149
|
-
|
|
150
|
-
|
|
186
|
+
H extends STHeaders = any,
|
|
187
|
+
Q extends STQuery = any,
|
|
188
|
+
B extends STBody = any,
|
|
189
|
+
R extends STResponse = STResponse
|
|
151
190
|
>(
|
|
152
191
|
path: Path,
|
|
153
|
-
schema: RequestSchema<Path, H, P, Q, B>,
|
|
154
|
-
hooks: Hook<Path, RequestSchema<Path, H, P, Q, B>>[],
|
|
155
|
-
handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
|
|
192
|
+
schema: RequestSchema<M, Path, H, P, Q, B, R>,
|
|
193
|
+
hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[],
|
|
194
|
+
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
156
195
|
): void
|
|
157
196
|
<
|
|
158
197
|
Path extends string,
|
|
159
|
-
H extends STHeaders,
|
|
160
198
|
P extends Partial<STParams<Path>>,
|
|
161
|
-
|
|
162
|
-
|
|
199
|
+
H extends STHeaders = any,
|
|
200
|
+
Q extends STQuery = any,
|
|
201
|
+
B extends STBody = any,
|
|
202
|
+
R extends STResponse = STResponse
|
|
163
203
|
>(
|
|
164
204
|
path: Path,
|
|
165
|
-
schema: RequestSchema<Path, H, P, Q, B>,
|
|
166
|
-
handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
|
|
205
|
+
schema: RequestSchema<M, Path, H, P, Q, B, R>,
|
|
206
|
+
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
167
207
|
): void
|
|
168
208
|
<
|
|
169
209
|
Path extends string,
|
|
170
|
-
H extends STHeaders,
|
|
171
210
|
P extends Partial<STParams<Path>>,
|
|
172
|
-
|
|
173
|
-
|
|
211
|
+
H extends STHeaders = any,
|
|
212
|
+
Q extends STQuery = any,
|
|
213
|
+
B extends STBody = any,
|
|
214
|
+
R extends STResponse = STResponse
|
|
174
215
|
>(
|
|
175
216
|
path: Path,
|
|
176
|
-
hooks: Hook<Path, RequestSchema<Path, H, P, Q, B>>[],
|
|
177
|
-
handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
|
|
217
|
+
hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[],
|
|
218
|
+
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
178
219
|
): void
|
|
179
220
|
<
|
|
180
221
|
Path extends string,
|
|
181
|
-
H extends STHeaders,
|
|
182
222
|
P extends Partial<STParams<Path>>,
|
|
183
|
-
|
|
184
|
-
|
|
223
|
+
H extends STHeaders = any,
|
|
224
|
+
Q extends STQuery = any,
|
|
225
|
+
B extends STBody = any,
|
|
226
|
+
R extends STResponse = STResponse
|
|
185
227
|
>(
|
|
186
228
|
path: Path,
|
|
187
|
-
handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
|
|
229
|
+
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
188
230
|
): void
|
|
189
231
|
}
|
|
190
232
|
|
|
@@ -193,52 +235,56 @@ export class RequestError {
|
|
|
193
235
|
payload: any
|
|
194
236
|
constructor(options: { status?: number; payload?: any }) {
|
|
195
237
|
this.status = options.status ?? 500
|
|
196
|
-
this.payload = options.payload
|
|
238
|
+
this.payload = options.payload
|
|
197
239
|
}
|
|
198
240
|
}
|
|
199
241
|
|
|
200
242
|
export type ErrorHandler = (error: any, context: Context) => any
|
|
201
243
|
|
|
202
244
|
export type RouteNode = {
|
|
203
|
-
|
|
245
|
+
routes: { [K in Method]?: Route }
|
|
204
246
|
param?: RouteNode
|
|
205
247
|
children?: Record<string, RouteNode>
|
|
206
248
|
}
|
|
207
249
|
|
|
208
250
|
export type Route<
|
|
251
|
+
M extends Method = Method,
|
|
209
252
|
Path extends string = string,
|
|
210
|
-
H extends STHeaders = {},
|
|
211
253
|
P extends Partial<STParams<Path>> = {},
|
|
212
|
-
|
|
213
|
-
|
|
254
|
+
H extends STHeaders = STHeaders,
|
|
255
|
+
Q extends STQuery = STQuery,
|
|
256
|
+
B extends STBody = STBody,
|
|
257
|
+
R extends STResponse = STResponse
|
|
214
258
|
> = {
|
|
215
|
-
method:
|
|
259
|
+
method: M
|
|
216
260
|
path: Path
|
|
217
|
-
schema: RequestSchema<Path, H, P, Q, B>
|
|
218
|
-
context: Context<Path, RequestSchema<Path, H, P, Q, B>>
|
|
261
|
+
schema: RequestSchema<M, Path, H, P, Q, B, R>
|
|
262
|
+
context: Context<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
219
263
|
hooks: Hook[]
|
|
220
|
-
handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export type RouteTree = {
|
|
224
|
-
[key: string]: RouteNode
|
|
264
|
+
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
225
265
|
}
|
|
226
266
|
|
|
227
267
|
export class NotFoundError extends RequestError {
|
|
228
|
-
constructor(message?:
|
|
268
|
+
constructor(message?: any) {
|
|
229
269
|
super({ status: 404, payload: message ?? 'Not found' })
|
|
230
270
|
}
|
|
231
271
|
}
|
|
232
272
|
|
|
273
|
+
export class MethodNotAllowedError extends RequestError {
|
|
274
|
+
constructor(message?: any) {
|
|
275
|
+
super({ status: 405, payload: message ?? 'Method not allowed' })
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
233
279
|
export class InternalError extends RequestError {
|
|
234
|
-
constructor(message?:
|
|
280
|
+
constructor(message?: any) {
|
|
235
281
|
super({ status: 500, payload: message ?? 'Internal Server Error' })
|
|
236
282
|
}
|
|
237
283
|
}
|
|
238
284
|
|
|
239
285
|
/**
|
|
240
286
|
* #### GalbePlugin
|
|
241
|
-
* Define a plugin for a Galbe
|
|
287
|
+
* Define a plugin for a Galbe application
|
|
242
288
|
*
|
|
243
289
|
* ---
|
|
244
290
|
* @example
|
|
@@ -264,4 +310,14 @@ export type GalbePlugin = {
|
|
|
264
310
|
onRoute?: (context: Context) => MaybePromise<Response | void>
|
|
265
311
|
beforeHandle?: (context: Context) => MaybePromise<Response | void>
|
|
266
312
|
afterHandle?: (response: Response, context: Context) => MaybePromise<Response | void>
|
|
313
|
+
cli?: (commands: GalbeCLICommand[]) => MaybePromise<GalbeCLICommand[] | void>
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export type GalbeCLICommand = {
|
|
317
|
+
name: string
|
|
318
|
+
description?: string
|
|
319
|
+
route: Route
|
|
320
|
+
arguments?: { name: string; type: string; description: string }[]
|
|
321
|
+
options?: { name: string; short: string; type: string; description: string; default: any }[]
|
|
322
|
+
action?: (props: any) => MaybePromise<void>
|
|
267
323
|
}
|
package/src/util.ts
CHANGED
|
@@ -1,12 +1,94 @@
|
|
|
1
|
+
import type { Route, RouteNode } from '.'
|
|
2
|
+
import type { RouteMeta } from './routes'
|
|
3
|
+
|
|
1
4
|
const METHOD_COLOR: Record<string, string> = {
|
|
2
5
|
get: '\x1b[32m',
|
|
3
6
|
post: '\x1b[34m',
|
|
4
7
|
put: '\x1b[36m',
|
|
5
8
|
patch: '\x1b[33m',
|
|
6
9
|
delete: '\x1b[31m',
|
|
7
|
-
options: ''
|
|
10
|
+
options: '',
|
|
11
|
+
head: ''
|
|
8
12
|
}
|
|
9
|
-
|
|
13
|
+
|
|
14
|
+
export const logRoute = (
|
|
15
|
+
r: { method: string; path: string },
|
|
16
|
+
meta?: RouteMeta,
|
|
17
|
+
format?: { maxPathLength?: number }
|
|
18
|
+
) => {
|
|
10
19
|
let color = METHOD_COLOR?.[r.method] || ''
|
|
11
|
-
console.log(
|
|
20
|
+
console.log(
|
|
21
|
+
` [${color}${`${r.method.toUpperCase()}\x1b[0m]`.padEnd(12, ' ')} ${r.path
|
|
22
|
+
.padEnd(format?.maxPathLength ?? r.path.length, ' ')
|
|
23
|
+
.replaceAll(/:([^\/]+)/g, '\x1b[0;33m:$1\x1b[0m')}${meta?.head ? ` ${meta.head}` : ''}`
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const walkRoutes = (node: RouteNode, cb: (route: Route) => void) => {
|
|
28
|
+
if (node?.routes) Object.values(node.routes).forEach(r => cb(r))
|
|
29
|
+
for (let c of Object.values(node?.children || {})) walkRoutes(c, cb)
|
|
30
|
+
if (node?.param) walkRoutes(node.param, cb)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const isIterator = (obj: any) => typeof obj?.next === 'function'
|
|
34
|
+
|
|
35
|
+
export const HttpStatus = {
|
|
36
|
+
100: 'Continue',
|
|
37
|
+
101: 'Switching Protocols',
|
|
38
|
+
102: 'Processing',
|
|
39
|
+
103: 'Early Hints',
|
|
40
|
+
200: 'OK',
|
|
41
|
+
201: 'Created',
|
|
42
|
+
202: 'Accepted',
|
|
43
|
+
203: 'Non Authoritative Information',
|
|
44
|
+
204: 'No Content',
|
|
45
|
+
205: 'Reset Content',
|
|
46
|
+
206: 'Partial Content',
|
|
47
|
+
207: 'Multi-Status',
|
|
48
|
+
300: 'Multiple Choices',
|
|
49
|
+
301: 'Moved Permanently',
|
|
50
|
+
302: 'Moved Temporarily',
|
|
51
|
+
303: 'See Other',
|
|
52
|
+
304: 'Not Modified',
|
|
53
|
+
305: 'Use Proxy',
|
|
54
|
+
307: 'Temporary Redirect',
|
|
55
|
+
308: 'Permanent Redirect',
|
|
56
|
+
400: 'Bad Request',
|
|
57
|
+
401: 'Unauthorized',
|
|
58
|
+
402: 'Payment Required',
|
|
59
|
+
403: 'Forbidden',
|
|
60
|
+
404: 'Not Found',
|
|
61
|
+
405: 'Method Not Allowed',
|
|
62
|
+
406: 'Not Acceptable',
|
|
63
|
+
407: 'Proxy Authentication Required',
|
|
64
|
+
408: 'Request Timeout',
|
|
65
|
+
409: 'Conflict',
|
|
66
|
+
410: 'Gone',
|
|
67
|
+
411: 'Length Required',
|
|
68
|
+
412: 'Precondition Failed',
|
|
69
|
+
413: 'Request Entity Too Large',
|
|
70
|
+
414: 'Request-URI Too Long',
|
|
71
|
+
415: 'Unsupported Media Type',
|
|
72
|
+
416: 'Requested Range Not Satisfiable',
|
|
73
|
+
417: 'Expectation Failed',
|
|
74
|
+
418: "I'm a teapot",
|
|
75
|
+
419: 'Insufficient Space on Resource',
|
|
76
|
+
420: 'Method Failure',
|
|
77
|
+
421: 'Misdirected Request',
|
|
78
|
+
422: 'Unprocessable Entity',
|
|
79
|
+
423: 'Locked',
|
|
80
|
+
424: 'Failed Dependency',
|
|
81
|
+
426: 'Upgrade Required',
|
|
82
|
+
428: 'Precondition Required',
|
|
83
|
+
429: 'Too Many Requests',
|
|
84
|
+
431: 'Request Header Fields Too Large',
|
|
85
|
+
451: 'Unavailable For Legal Reasons',
|
|
86
|
+
500: 'Internal Server Error',
|
|
87
|
+
501: 'Not Implemented',
|
|
88
|
+
502: 'Bad Gateway',
|
|
89
|
+
503: 'Service Unavailable',
|
|
90
|
+
504: 'Gateway Timeout',
|
|
91
|
+
505: 'HTTP Version Not Supported',
|
|
92
|
+
507: 'Insufficient Storage',
|
|
93
|
+
511: 'Network Authentication Required'
|
|
12
94
|
}
|
package/src/validator.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { InternalError, type STResponse } from './index'
|
|
1
2
|
import type { STSchema, STProps, STUnion } from './schema'
|
|
2
|
-
import { Kind, Optional } from './schema'
|
|
3
|
+
import { Kind, Optional, Stream } from './schema'
|
|
4
|
+
import { isIterator } from './util'
|
|
3
5
|
|
|
4
6
|
export const validate = (elt: any, schema: STSchema, parse = false): any => {
|
|
5
7
|
type ValidationError = string | string[] | { [key: string]: ValidationError }
|
|
@@ -7,7 +9,10 @@ export const validate = (elt: any, schema: STSchema, parse = false): any => {
|
|
|
7
9
|
const iElt = elt
|
|
8
10
|
|
|
9
11
|
if (schema[Kind] === 'boolean') {
|
|
10
|
-
if (
|
|
12
|
+
if (typeof elt === 'string') {
|
|
13
|
+
if (parse) elt = elt === 'true' ? true : elt === 'false' ? false : null
|
|
14
|
+
else throw `Expected boolean, got string.`
|
|
15
|
+
}
|
|
11
16
|
if (elt !== true && elt !== false) throw `${iElt} is not a valid boolean. Should be 'true' or 'false'`
|
|
12
17
|
} else if (schema[Kind] === 'integer') {
|
|
13
18
|
if (parse && typeof elt === 'string') elt = Number(elt)
|
|
@@ -45,6 +50,8 @@ export const validate = (elt: any, schema: STSchema, parse = false): any => {
|
|
|
45
50
|
}
|
|
46
51
|
})
|
|
47
52
|
if (Object.keys(err).length) errors.push(err)
|
|
53
|
+
} else if (schema[Kind] === 'json') {
|
|
54
|
+
elt = validate(elt, { ...schema, [Kind]: schema.type }, parse)
|
|
48
55
|
} else if (schema[Kind] === 'array') {
|
|
49
56
|
if (parse && typeof elt === 'string') {
|
|
50
57
|
try {
|
|
@@ -84,6 +91,22 @@ export const validate = (elt: any, schema: STSchema, parse = false): any => {
|
|
|
84
91
|
return elt
|
|
85
92
|
}
|
|
86
93
|
|
|
94
|
+
export const validateResponse = (response: any, schema: STResponse, status: number) => {
|
|
95
|
+
if (!(status in schema)) return
|
|
96
|
+
const s = schema[status]
|
|
97
|
+
if (response instanceof ReadableStream) {
|
|
98
|
+
if (!s[Stream]) throw new InternalError(`Expected ${s[Kind]} response, but got ReadableStream`)
|
|
99
|
+
} else if (isIterator(response)) {
|
|
100
|
+
if (!s[Stream]) throw new InternalError(`Expected ${s[Kind]} response, but got Iterator`)
|
|
101
|
+
} else {
|
|
102
|
+
try {
|
|
103
|
+
validate(response, s)
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw new InternalError({ ResponseValidationError: error })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
87
110
|
const schemaValidation = (value: any, schema: STSchema) => {
|
|
88
111
|
const errors = []
|
|
89
112
|
if (schema[Kind] === 'integer' || schema[Kind] === 'number') {
|
|
@@ -92,9 +115,8 @@ const schemaValidation = (value: any, schema: STSchema) => {
|
|
|
92
115
|
if (schema.exclusiveMax !== undefined)
|
|
93
116
|
if ((value as number) >= schema.exclusiveMax)
|
|
94
117
|
errors.push(`${value} is greater or equal to ${schema.exclusiveMax}`)
|
|
95
|
-
if (schema.
|
|
96
|
-
|
|
97
|
-
if (schema.maximum !== undefined)
|
|
118
|
+
if (schema.min !== undefined) if ((value as number) < schema.min) errors.push(`${value} is less than ${schema.min}`)
|
|
119
|
+
if (schema.max !== undefined)
|
|
98
120
|
if ((value as number) > schema.max) errors.push(`${value} is greater than ${schema.max}`)
|
|
99
121
|
} else if (schema[Kind] === 'string') {
|
|
100
122
|
if (schema.minLength !== undefined && (value as string).length < schema.minLength)
|
package/test/parser.test.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
isAsyncIterator
|
|
11
11
|
} from './test.utils'
|
|
12
12
|
import { Galbe, $T } from '../src'
|
|
13
|
+
import { schemaToTypeStr } from '../src/schema'
|
|
13
14
|
|
|
14
15
|
const port = 7357
|
|
15
16
|
|
|
@@ -1195,4 +1196,37 @@ describe('parser', () => {
|
|
|
1195
1196
|
expect(body).toEqual(expected.body)
|
|
1196
1197
|
}
|
|
1197
1198
|
})
|
|
1199
|
+
|
|
1200
|
+
test('schema to type', async () => {
|
|
1201
|
+
let type = schemaToTypeStr(
|
|
1202
|
+
$T.object({
|
|
1203
|
+
boolean: $T.boolean(),
|
|
1204
|
+
byteArray: $T.byteArray(),
|
|
1205
|
+
number: $T.number(),
|
|
1206
|
+
integer: $T.integer(),
|
|
1207
|
+
string: $T.string(),
|
|
1208
|
+
any: $T.any(),
|
|
1209
|
+
literal: $T.literal('literal'),
|
|
1210
|
+
array: $T.array($T.string()),
|
|
1211
|
+
object: $T.object({
|
|
1212
|
+
foo: $T.string()
|
|
1213
|
+
}),
|
|
1214
|
+
union: $T.union([$T.number(), $T.string()])
|
|
1215
|
+
})
|
|
1216
|
+
)
|
|
1217
|
+
expect(type).toBe(
|
|
1218
|
+
`{` +
|
|
1219
|
+
`'boolean':boolean;` +
|
|
1220
|
+
`'byteArray':Uint8Array;` +
|
|
1221
|
+
`'number':number;` +
|
|
1222
|
+
`'integer':number;` +
|
|
1223
|
+
`'string':string;` +
|
|
1224
|
+
`'any':any;` +
|
|
1225
|
+
`'literal':'literal';` +
|
|
1226
|
+
`'array':Array<string>;` +
|
|
1227
|
+
`'object':{'foo':string};` +
|
|
1228
|
+
`'union':number|string` +
|
|
1229
|
+
`}`
|
|
1230
|
+
)
|
|
1231
|
+
})
|
|
1198
1232
|
})
|