galbe 0.3.0 → 0.5.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/.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/plugins.md +36 -36
- 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 +138 -39
- package/src/schema.ts +69 -8
- package/src/server.ts +9 -10
- package/src/types.ts +73 -41
- package/src/util.ts +95 -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
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import type { STArray, STJson, STLiteral, STObject, STProps, STSchema, STUnion } from '../../../src/schema'
|
|
2
|
+
|
|
3
|
+
import { Galbe } from '../../../src'
|
|
4
|
+
import { walkRoutes, HttpStatus } from '../../../src/util'
|
|
5
|
+
import { Kind, Optional } from '../../../src/schema'
|
|
6
|
+
|
|
7
|
+
import { OpenAPIV3 } from 'openapi-types'
|
|
8
|
+
|
|
9
|
+
type SchemaType = { type: string; format: string; isJson: boolean }
|
|
10
|
+
|
|
11
|
+
const schemaToMedia = ({ type, format, isJson }: SchemaType) =>
|
|
12
|
+
isJson || (type && ['object', 'number', 'boolean', 'array'].includes(type))
|
|
13
|
+
? 'application/json'
|
|
14
|
+
: format === 'byte'
|
|
15
|
+
? 'application/octet-stream'
|
|
16
|
+
: type === 'string'
|
|
17
|
+
? 'text/plain'
|
|
18
|
+
: '*/*'
|
|
19
|
+
|
|
20
|
+
export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<OpenAPIV3.Document> => {
|
|
21
|
+
let paths: any = {}
|
|
22
|
+
let components: OpenAPIV3.ComponentsObject = {
|
|
23
|
+
securitySchemes: {},
|
|
24
|
+
schemas: {},
|
|
25
|
+
parameters: {},
|
|
26
|
+
requestBodies: {},
|
|
27
|
+
responses: {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const schemaToOpenapi = (
|
|
31
|
+
schema: STSchema
|
|
32
|
+
): { schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject; isJson?: boolean } => {
|
|
33
|
+
let s = {}
|
|
34
|
+
let kind = schema[Kind]
|
|
35
|
+
let isJson = false
|
|
36
|
+
|
|
37
|
+
let pattern = schema?.pattern?.toString()
|
|
38
|
+
if (pattern) pattern = pattern.substring(1, pattern.length - 1)
|
|
39
|
+
|
|
40
|
+
let minLength = schema?.minLength
|
|
41
|
+
let maxLength = schema?.maxLength
|
|
42
|
+
let minimum = schema?.min
|
|
43
|
+
let maximum = schema?.max
|
|
44
|
+
let exclusiveMinimum = schema?.exclusiveMin
|
|
45
|
+
let exclusiveMaximum = schema?.exclusiveMax
|
|
46
|
+
let minItems = schema?.minItems
|
|
47
|
+
let maxItems = schema?.maxItems
|
|
48
|
+
let uniqueItems = schema?.unique
|
|
49
|
+
|
|
50
|
+
if (components.schemas && (schema.id as string) in components.schemas) {
|
|
51
|
+
//@ts-ignore
|
|
52
|
+
return { schema: { $ref: `#/components/schemas/${schema.id}` } }
|
|
53
|
+
}
|
|
54
|
+
// TODO add constraints min, max etc.
|
|
55
|
+
if (kind === 'boolean') s = { type: 'boolean' }
|
|
56
|
+
else if (kind === 'byteArray') s = { type: 'string', format: 'byte' }
|
|
57
|
+
else if (kind === 'number')
|
|
58
|
+
s = {
|
|
59
|
+
type: 'number',
|
|
60
|
+
...(exclusiveMinimum ? { exclusiveMinimum } : {}),
|
|
61
|
+
...(exclusiveMaximum ? { exclusiveMaximum } : {}),
|
|
62
|
+
...(minimum ? { minimum } : {}),
|
|
63
|
+
...(maximum ? { maximum } : {})
|
|
64
|
+
}
|
|
65
|
+
else if (kind === 'integer')
|
|
66
|
+
s = {
|
|
67
|
+
type: 'integer',
|
|
68
|
+
...(exclusiveMinimum ? { exclusiveMinimum } : {}),
|
|
69
|
+
...(exclusiveMaximum ? { exclusiveMaximum } : {}),
|
|
70
|
+
...(minimum ? { minimum } : {}),
|
|
71
|
+
...(maximum ? { maximum } : {})
|
|
72
|
+
}
|
|
73
|
+
else if (kind === 'string')
|
|
74
|
+
s = {
|
|
75
|
+
type: 'string',
|
|
76
|
+
...(pattern ? { pattern } : {}),
|
|
77
|
+
...(minLength ? { minLength } : {}),
|
|
78
|
+
...(maxLength ? { maxLength } : {})
|
|
79
|
+
}
|
|
80
|
+
else if (kind === 'any') s = { type: 'string' }
|
|
81
|
+
else if (kind === 'literal') {
|
|
82
|
+
let value = (schema as STLiteral).value
|
|
83
|
+
s = { type: 'string', enum: [value] }
|
|
84
|
+
} else if (kind === 'array') {
|
|
85
|
+
s = {
|
|
86
|
+
type: 'array',
|
|
87
|
+
items: schemaToOpenapi((schema as STArray).items).schema,
|
|
88
|
+
...(minItems ? { minItems } : {}),
|
|
89
|
+
...(maxItems ? { maxItems } : {}),
|
|
90
|
+
...(uniqueItems ? { uniqueItems } : {})
|
|
91
|
+
}
|
|
92
|
+
} else if (kind === 'object') {
|
|
93
|
+
let props = (schema as STObject).props || {}
|
|
94
|
+
let required = Object.entries(props)
|
|
95
|
+
.filter(([_, v]) => !v?.[Optional])
|
|
96
|
+
.map(([k, _]) => k)
|
|
97
|
+
s = {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: Object.fromEntries(Object.entries(props).map(([k, v]) => [k, schemaToOpenapi(v).schema])),
|
|
100
|
+
...(required.length ? { required } : {})
|
|
101
|
+
}
|
|
102
|
+
} else if (kind === 'json') {
|
|
103
|
+
let props = ((schema as STJson).props || {}) as STProps
|
|
104
|
+
let type = (schema as STJson).type
|
|
105
|
+
if (type === 'unknown') type = 'object'
|
|
106
|
+
let required = Object.entries(props)
|
|
107
|
+
.filter(([_, v]) => !v?.[Optional])
|
|
108
|
+
.map(([k, _]) => k)
|
|
109
|
+
isJson = true
|
|
110
|
+
s = {
|
|
111
|
+
type: type,
|
|
112
|
+
...(type === 'object'
|
|
113
|
+
? {
|
|
114
|
+
properties: Object.fromEntries(Object.entries(props).map(([k, v]) => [k, schemaToOpenapi(v).schema])),
|
|
115
|
+
...(required.length ? { required } : {})
|
|
116
|
+
}
|
|
117
|
+
: {})
|
|
118
|
+
}
|
|
119
|
+
} else if (kind === 'union') {
|
|
120
|
+
let anyOf = (schema as STUnion).anyOf
|
|
121
|
+
s = {
|
|
122
|
+
anyOf: anyOf.map(s => schemaToOpenapi(s).schema)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
s = { title: schema.title, description: schema.description, ...s }
|
|
126
|
+
if (components.schemas && schema.id) {
|
|
127
|
+
components.schemas[schema.id] = s
|
|
128
|
+
return { schema: { $ref: `#/components/schemas/${schema.id}` } }
|
|
129
|
+
}
|
|
130
|
+
return { schema: s, isJson }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const resolveRef = (schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject): OpenAPIV3.SchemaObject => {
|
|
134
|
+
if (!(schema as OpenAPIV3.ReferenceObject)?.$ref) return schema as OpenAPIV3.SchemaObject
|
|
135
|
+
let ref = (schema as OpenAPIV3.ReferenceObject)?.$ref
|
|
136
|
+
let match = ref.match('^#/components/(schemas|requestBodies|responses)/(.*)$')
|
|
137
|
+
if (!match) throw new Error(`Invalid schema ref ${ref}`)
|
|
138
|
+
let [_, kind, refPath] = match as [string, keyof typeof components, string]
|
|
139
|
+
//@ts-ignore
|
|
140
|
+
return refPath.split('/').reduce((c, k) => {
|
|
141
|
+
if (c && k in c) return c[k]
|
|
142
|
+
else return undefined
|
|
143
|
+
}, components[kind])
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const parseParam = (key: string, param: STSchema, kind: 'query' | 'header' | 'path' | 'cookie') => {
|
|
147
|
+
let { schema } = schemaToOpenapi({ ...param, [Optional]: false })
|
|
148
|
+
let p: OpenAPIV3.ParameterObject = {
|
|
149
|
+
name: key,
|
|
150
|
+
in: kind,
|
|
151
|
+
description: param?.description,
|
|
152
|
+
required: kind === 'path' ? true : !param[Optional] || undefined,
|
|
153
|
+
deprecated: param.deprecated,
|
|
154
|
+
schema
|
|
155
|
+
}
|
|
156
|
+
if (components.parameters && param.id) components.parameters[param.id] = p
|
|
157
|
+
return p
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const metaRoutes = g.meta?.reduce(
|
|
161
|
+
(routes, c) => ({ ...routes, ...c.routes }),
|
|
162
|
+
{} as Record<string, Record<string, Record<string, any>>>
|
|
163
|
+
)
|
|
164
|
+
walkRoutes(g.router.routes, r => {
|
|
165
|
+
let meta = metaRoutes?.[r.path]?.[r.method]
|
|
166
|
+
let path = r.path.replaceAll(/:([^\/]+)/g, '{$1}')
|
|
167
|
+
if (!(path in paths)) paths[path] = {}
|
|
168
|
+
let tags = [...(meta?.tags?.split(' ')?.map((t: string) => t.trim()) || []), ...(meta?.tag || [])]
|
|
169
|
+
let security: Record<string, any> = []
|
|
170
|
+
|
|
171
|
+
let pathParam = r.schema?.params
|
|
172
|
+
? Object.entries(r.schema?.params as Record<string, STSchema>).map(([k, v]) => parseParam(k, v, 'path'))
|
|
173
|
+
: []
|
|
174
|
+
let queryParam = r.schema?.query
|
|
175
|
+
? Object.entries(r.schema?.query as Record<string, STSchema>).map(([k, v]) => parseParam(k, v, 'query'))
|
|
176
|
+
: []
|
|
177
|
+
let headerParam = r.schema?.headers
|
|
178
|
+
? Object.entries(r.schema?.headers as Record<string, STSchema>)
|
|
179
|
+
.map(([k, v]) => {
|
|
180
|
+
let p = parseParam(k, v, 'header')
|
|
181
|
+
if (k.match(/authorization/i)) {
|
|
182
|
+
// TODO: handle other auth methods
|
|
183
|
+
if (v.pattern && v?.pattern?.toString() === '/^Bearer /') {
|
|
184
|
+
security.push({ bearerAuth: [] })
|
|
185
|
+
components.securitySchemes = { bearerAuth: { type: 'http', scheme: 'bearer' } }
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return p
|
|
190
|
+
})
|
|
191
|
+
.filter(p => p)
|
|
192
|
+
: []
|
|
193
|
+
// TODO cookieParam
|
|
194
|
+
let parameters = [...pathParam, ...queryParam, ...headerParam]
|
|
195
|
+
|
|
196
|
+
let requestBody
|
|
197
|
+
if (r.schema.body) {
|
|
198
|
+
let { schema, isJson } = schemaToOpenapi(r.schema.body)
|
|
199
|
+
let { type, format } = resolveRef(schema)
|
|
200
|
+
let media = schemaToMedia({ type, format, isJson } as SchemaType)
|
|
201
|
+
requestBody = {
|
|
202
|
+
description: r.schema.body.description,
|
|
203
|
+
required: !r.schema.body[Optional],
|
|
204
|
+
content: {
|
|
205
|
+
[media]: { schema }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (r.schema.body.id && components.requestBodies) {
|
|
209
|
+
components.requestBodies[r.schema.body.id] = requestBody
|
|
210
|
+
requestBody = { $ref: `#/components/requestBodies/${r.schema.body.id}` }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
let responses
|
|
214
|
+
if (r.schema.response && Object.keys(r.schema.response).length) {
|
|
215
|
+
responses = Object.fromEntries(
|
|
216
|
+
Object.entries(r.schema.response).map(([status, v]) => {
|
|
217
|
+
let s = Number(status) as keyof typeof HttpStatus
|
|
218
|
+
let { schema, isJson } = schemaToOpenapi(v)
|
|
219
|
+
let { type, format } = resolveRef(schema)
|
|
220
|
+
let media = schemaToMedia({ type, format, isJson } as SchemaType)
|
|
221
|
+
let response: OpenAPIV3.ResponseObject = {
|
|
222
|
+
description: v.description || HttpStatus[Number(s) as keyof typeof HttpStatus] || 'Response',
|
|
223
|
+
content: { [media]: { schema: schema } }
|
|
224
|
+
}
|
|
225
|
+
if (components.responses && r.schema.response?.[s].id) {
|
|
226
|
+
components.responses[r.schema.response?.[s].id as string] = response
|
|
227
|
+
//@ts-ignore
|
|
228
|
+
response = { $ref: `#/components/responses/${r.schema.response?.[s].id}` }
|
|
229
|
+
}
|
|
230
|
+
return [s, response]
|
|
231
|
+
})
|
|
232
|
+
)
|
|
233
|
+
} else {
|
|
234
|
+
responses = {
|
|
235
|
+
default: { description: HttpStatus[200] }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
paths[path][r.method] = {
|
|
239
|
+
tags: tags.length ? tags : undefined,
|
|
240
|
+
summary: meta?.head,
|
|
241
|
+
operationId: meta?.operationId,
|
|
242
|
+
parameters: parameters.length ? parameters : undefined,
|
|
243
|
+
requestBody,
|
|
244
|
+
responses,
|
|
245
|
+
...(security.length ? { security } : {}),
|
|
246
|
+
deprecated: meta?.deprecated ? true : undefined
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
//@ts-ignore
|
|
251
|
+
components = Object.entries(components).reduce((p, [k, v]) => {
|
|
252
|
+
if (Object.keys(v).length) p[k] = v
|
|
253
|
+
return p
|
|
254
|
+
}, {} as Record<string, OpenAPIV3.ComponentsObject>)
|
|
255
|
+
return {
|
|
256
|
+
openapi: version,
|
|
257
|
+
info: {
|
|
258
|
+
title: 'Galbe app',
|
|
259
|
+
version: '0.1.0'
|
|
260
|
+
},
|
|
261
|
+
paths,
|
|
262
|
+
components: Object.keys(components)?.length ? components : undefined
|
|
263
|
+
}
|
|
264
|
+
}
|
package/src/extras.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { OpenAPISerializer } from './extras/spec/openapi.serializer'
|
package/src/index.ts
CHANGED
|
@@ -19,11 +19,10 @@ import type {
|
|
|
19
19
|
|
|
20
20
|
import server from './server'
|
|
21
21
|
import { GalbeRouter } from './router'
|
|
22
|
-
import { defineRoutes } from './routes'
|
|
23
|
-
import { extractMetaRoute, logRoute } from './util'
|
|
24
22
|
import { SchemaType, type STObject, type Static } from './schema'
|
|
25
23
|
|
|
26
24
|
const overloadDiscriminer = <
|
|
25
|
+
M extends Method,
|
|
27
26
|
Path extends string,
|
|
28
27
|
H extends STHeaders,
|
|
29
28
|
P extends Partial<STParams<Path>>,
|
|
@@ -32,14 +31,16 @@ const overloadDiscriminer = <
|
|
|
32
31
|
R extends STResponse
|
|
33
32
|
>(
|
|
34
33
|
galbe: Galbe,
|
|
35
|
-
method:
|
|
34
|
+
method: M,
|
|
36
35
|
path: Path,
|
|
37
36
|
arg2:
|
|
38
|
-
| RequestSchema<Path, H, P, Q, B, R>
|
|
39
|
-
| Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
|
|
40
|
-
| Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
|
|
41
|
-
arg3?:
|
|
42
|
-
|
|
37
|
+
| RequestSchema<M, Path, H, P, Q, B, R>
|
|
38
|
+
| Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[]
|
|
39
|
+
| Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>,
|
|
40
|
+
arg3?:
|
|
41
|
+
| Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[]
|
|
42
|
+
| Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>,
|
|
43
|
+
arg4?: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
43
44
|
) => {
|
|
44
45
|
const defaultSchema = {}
|
|
45
46
|
if (typeof arg2 === 'function') {
|
|
@@ -55,6 +56,7 @@ const overloadDiscriminer = <
|
|
|
55
56
|
throw new Error('Undefined route signature')
|
|
56
57
|
}
|
|
57
58
|
const galbeMethod = <
|
|
59
|
+
M extends Method,
|
|
58
60
|
Path extends string,
|
|
59
61
|
H extends STHeaders,
|
|
60
62
|
P extends Partial<STParams<Path>>,
|
|
@@ -63,19 +65,22 @@ const galbeMethod = <
|
|
|
63
65
|
R extends STResponse
|
|
64
66
|
>(
|
|
65
67
|
_galbe: Galbe,
|
|
66
|
-
method:
|
|
68
|
+
method: M,
|
|
67
69
|
path: Path,
|
|
68
|
-
schema: RequestSchema<Path, H, P, Q, B, R> | undefined,
|
|
69
|
-
hooks: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | undefined,
|
|
70
|
-
handler: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
|
|
70
|
+
schema: RequestSchema<M, Path, H, P, Q, B, R> | undefined,
|
|
71
|
+
hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[] | undefined,
|
|
72
|
+
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
71
73
|
) => {
|
|
72
74
|
schema = schema ?? {}
|
|
73
75
|
hooks = hooks || []
|
|
74
|
-
const context: Context<Path, typeof schema> = {
|
|
76
|
+
const context: Context<M, Path, typeof schema> = {
|
|
75
77
|
headers: {} as Static<STObject<Exclude<(typeof schema)['headers'], undefined>>>,
|
|
76
78
|
params: {} as any,
|
|
77
79
|
query: {} as Static<STObject<Exclude<(typeof schema)['query'], undefined>>>,
|
|
78
|
-
|
|
80
|
+
//@ts-ignore
|
|
81
|
+
body: ['get', 'options', 'head'].includes(method)
|
|
82
|
+
? null
|
|
83
|
+
: ({} as Static<Exclude<(typeof schema)['body'], undefined>>),
|
|
79
84
|
request: {} as Request,
|
|
80
85
|
state: {},
|
|
81
86
|
set: {} as {
|
|
@@ -100,7 +105,6 @@ export const $T = new SchemaType()
|
|
|
100
105
|
|
|
101
106
|
export { RequestError } from './types'
|
|
102
107
|
|
|
103
|
-
const indexRoutes: { method: string; path: string }[] = []
|
|
104
108
|
/**
|
|
105
109
|
* #### Galbe Server
|
|
106
110
|
* Instanciate a Galbe web server
|
|
@@ -120,7 +124,6 @@ export class Galbe {
|
|
|
120
124
|
router: GalbeRouter
|
|
121
125
|
errorHandler?: ErrorHandler
|
|
122
126
|
listening: boolean = false
|
|
123
|
-
#prepare: boolean = false
|
|
124
127
|
server?: Server
|
|
125
128
|
plugins: GalbePlugin[] = []
|
|
126
129
|
constructor(config?: GalbeConfig) {
|
|
@@ -135,32 +138,27 @@ export class Galbe {
|
|
|
135
138
|
}
|
|
136
139
|
private add(route: any) {
|
|
137
140
|
this.router.add(route)
|
|
138
|
-
|
|
139
|
-
if (!this.#prepare) indexRoutes.push({ method: route.method, path: route.path })
|
|
140
|
-
else logRoute(route, extractMetaRoute(route, this.meta))
|
|
141
|
-
}
|
|
141
|
+
return route
|
|
142
142
|
}
|
|
143
143
|
async use(plugin: GalbePlugin) {
|
|
144
144
|
this.plugins.push(plugin)
|
|
145
145
|
}
|
|
146
|
+
async init() {
|
|
147
|
+
for (const p of this.plugins) {
|
|
148
|
+
if (p.init) await p.init(this.config?.plugin?.[p.name] || {}, this)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
146
151
|
async listen(port?: number) {
|
|
147
152
|
port = port || this.config?.port || 3000
|
|
148
153
|
this.config.port = port
|
|
149
154
|
if (this.listening) this.stop()
|
|
155
|
+
await this.init()
|
|
156
|
+
this.server = await server(this, port)
|
|
150
157
|
if (Bun.env.BUN_ENV === 'development') {
|
|
151
|
-
this.#prepare = true
|
|
152
|
-
console.log('🏗️ \x1b[1;30mConstructing routes\x1b[0m')
|
|
153
|
-
for (const r of indexRoutes) logRoute(r, extractMetaRoute(r, this.meta))
|
|
154
|
-
await defineRoutes(this.config || {}, this)
|
|
155
|
-
console.log('\n✅ \x1b[1;30mdone\x1b[0m')
|
|
156
|
-
this.server = await server(this, port)
|
|
157
158
|
const url = `http://localhost:${port}${this.config?.basePath || ''}`
|
|
158
|
-
console.log(`\
|
|
159
|
-
} else {
|
|
160
|
-
this.server = await server(this, port)
|
|
159
|
+
console.log(`\x1b[1;30m🚀 Server running at\x1b[0m \x1b[4;34m${url}\x1b[0m\n`)
|
|
161
160
|
}
|
|
162
161
|
this.listening = true
|
|
163
|
-
this.#prepare = false
|
|
164
162
|
return this.server
|
|
165
163
|
}
|
|
166
164
|
stop() {
|
|
@@ -169,102 +167,134 @@ export class Galbe {
|
|
|
169
167
|
onError(handler: ErrorHandler) {
|
|
170
168
|
this.errorHandler = handler
|
|
171
169
|
}
|
|
172
|
-
get: Endpoint = <
|
|
170
|
+
get: Endpoint<'get'> = <
|
|
173
171
|
Path extends string,
|
|
174
|
-
H extends STHeaders,
|
|
175
172
|
P extends Partial<STParams<Path>>,
|
|
173
|
+
H extends STHeaders,
|
|
176
174
|
Q extends STQuery,
|
|
177
|
-
B extends
|
|
175
|
+
B extends undefined,
|
|
178
176
|
R extends STResponse
|
|
179
177
|
>(
|
|
180
178
|
path: Path,
|
|
181
179
|
arg2:
|
|
182
|
-
| RequestSchema<Path, H, P, Q, B, R>
|
|
183
|
-
| Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
|
|
184
|
-
| Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
|
|
185
|
-
arg3?:
|
|
186
|
-
|
|
180
|
+
| RequestSchema<'get', Path, H, P, Q, B, R>
|
|
181
|
+
| Hook<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>[]
|
|
182
|
+
| Handler<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>,
|
|
183
|
+
arg3?:
|
|
184
|
+
| Hook<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>[]
|
|
185
|
+
| Handler<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>,
|
|
186
|
+
arg4?: Handler<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>
|
|
187
|
+
//@ts-ignore
|
|
187
188
|
) => this.add(overloadDiscriminer(this, 'get', path, arg2, arg3, arg4))
|
|
188
|
-
post: Endpoint = <
|
|
189
|
+
post: Endpoint<'post'> = <
|
|
189
190
|
Path extends string,
|
|
190
|
-
H extends STHeaders,
|
|
191
191
|
P extends Partial<STParams<Path>>,
|
|
192
|
+
H extends STHeaders,
|
|
192
193
|
Q extends STQuery,
|
|
193
194
|
B extends STBody,
|
|
194
195
|
R extends STResponse
|
|
195
196
|
>(
|
|
196
197
|
path: Path,
|
|
197
198
|
arg2:
|
|
198
|
-
| RequestSchema<Path, H, P, Q, B, R>
|
|
199
|
-
| Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
|
|
200
|
-
| Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
|
|
201
|
-
arg3?:
|
|
202
|
-
|
|
199
|
+
| RequestSchema<'post', Path, H, P, Q, B, R>
|
|
200
|
+
| Hook<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>[]
|
|
201
|
+
| Handler<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>,
|
|
202
|
+
arg3?:
|
|
203
|
+
| Hook<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>[]
|
|
204
|
+
| Handler<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>,
|
|
205
|
+
arg4?: Handler<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>
|
|
206
|
+
//@ts-ignore
|
|
203
207
|
) => this.add(overloadDiscriminer(this, 'post', path, arg2, arg3, arg4))
|
|
204
|
-
put: Endpoint = <
|
|
208
|
+
put: Endpoint<'put'> = <
|
|
205
209
|
Path extends string,
|
|
206
|
-
H extends STHeaders,
|
|
207
210
|
P extends Partial<STParams<Path>>,
|
|
211
|
+
H extends STHeaders,
|
|
208
212
|
Q extends STQuery,
|
|
209
213
|
B extends STBody,
|
|
210
214
|
R extends STResponse
|
|
211
215
|
>(
|
|
212
216
|
path: Path,
|
|
213
217
|
arg2:
|
|
214
|
-
| RequestSchema<Path, H, P, Q, B, R>
|
|
215
|
-
| Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
|
|
216
|
-
| Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
|
|
217
|
-
arg3?:
|
|
218
|
-
|
|
218
|
+
| RequestSchema<'put', Path, H, P, Q, B, R>
|
|
219
|
+
| Hook<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>[]
|
|
220
|
+
| Handler<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>,
|
|
221
|
+
arg3?:
|
|
222
|
+
| Hook<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>[]
|
|
223
|
+
| Handler<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>,
|
|
224
|
+
arg4?: Handler<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>
|
|
219
225
|
) => this.add(overloadDiscriminer(this, 'put', path, arg2, arg3, arg4))
|
|
220
|
-
patch: Endpoint = <
|
|
226
|
+
patch: Endpoint<'patch'> = <
|
|
221
227
|
Path extends string,
|
|
222
|
-
H extends STHeaders,
|
|
223
228
|
P extends Partial<STParams<Path>>,
|
|
229
|
+
H extends STHeaders,
|
|
224
230
|
Q extends STQuery,
|
|
225
231
|
B extends STBody,
|
|
226
232
|
R extends STResponse
|
|
227
233
|
>(
|
|
228
234
|
path: Path,
|
|
229
235
|
arg2:
|
|
230
|
-
| RequestSchema<Path, H, P, Q, B, R>
|
|
231
|
-
| Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
|
|
232
|
-
| Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
|
|
233
|
-
arg3?:
|
|
234
|
-
|
|
236
|
+
| RequestSchema<'patch', Path, H, P, Q, B, R>
|
|
237
|
+
| Hook<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>[]
|
|
238
|
+
| Handler<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>,
|
|
239
|
+
arg3?:
|
|
240
|
+
| Hook<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>[]
|
|
241
|
+
| Handler<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>,
|
|
242
|
+
arg4?: Handler<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>
|
|
235
243
|
) => this.add(overloadDiscriminer(this, 'patch', path, arg2, arg3, arg4))
|
|
236
|
-
delete: Endpoint = <
|
|
244
|
+
delete: Endpoint<'delete'> = <
|
|
237
245
|
Path extends string,
|
|
238
|
-
H extends STHeaders,
|
|
239
246
|
P extends Partial<STParams<Path>>,
|
|
247
|
+
H extends STHeaders,
|
|
240
248
|
Q extends STQuery,
|
|
241
249
|
B extends STBody,
|
|
242
250
|
R extends STResponse
|
|
243
251
|
>(
|
|
244
252
|
path: Path,
|
|
245
253
|
arg2:
|
|
246
|
-
| RequestSchema<Path, H, P, Q, B, R>
|
|
247
|
-
| Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
|
|
248
|
-
| Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
|
|
249
|
-
arg3?:
|
|
250
|
-
|
|
254
|
+
| RequestSchema<'delete', Path, H, P, Q, B, R>
|
|
255
|
+
| Hook<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>[]
|
|
256
|
+
| Handler<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>,
|
|
257
|
+
arg3?:
|
|
258
|
+
| Hook<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>[]
|
|
259
|
+
| Handler<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>,
|
|
260
|
+
arg4?: Handler<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>
|
|
251
261
|
) => this.add(overloadDiscriminer(this, 'delete', path, arg2, arg3, arg4))
|
|
252
|
-
options: Endpoint = <
|
|
262
|
+
options: Endpoint<'options'> = <
|
|
253
263
|
Path extends string,
|
|
254
|
-
H extends STHeaders,
|
|
255
264
|
P extends Partial<STParams<Path>>,
|
|
265
|
+
H extends STHeaders,
|
|
256
266
|
Q extends STQuery,
|
|
257
267
|
B extends STBody,
|
|
258
268
|
R extends STResponse
|
|
259
269
|
>(
|
|
260
270
|
path: Path,
|
|
261
271
|
arg2:
|
|
262
|
-
| RequestSchema<Path, H, P, Q, B, R>
|
|
263
|
-
| Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
|
|
264
|
-
| Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
|
|
265
|
-
arg3?:
|
|
266
|
-
|
|
272
|
+
| RequestSchema<'options', Path, H, P, Q, B, R>
|
|
273
|
+
| Hook<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>[]
|
|
274
|
+
| Handler<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>,
|
|
275
|
+
arg3?:
|
|
276
|
+
| Hook<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>[]
|
|
277
|
+
| Handler<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>,
|
|
278
|
+
arg4?: Handler<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>
|
|
267
279
|
) => this.add(overloadDiscriminer(this, 'options', path, arg2, arg3, arg4))
|
|
280
|
+
head: Endpoint<'head'> = <
|
|
281
|
+
Path extends string,
|
|
282
|
+
P extends Partial<STParams<Path>>,
|
|
283
|
+
H extends STHeaders,
|
|
284
|
+
Q extends STQuery,
|
|
285
|
+
B extends STBody,
|
|
286
|
+
R extends STResponse
|
|
287
|
+
>(
|
|
288
|
+
path: Path,
|
|
289
|
+
arg2:
|
|
290
|
+
| RequestSchema<'head', Path, H, P, Q, B, R>
|
|
291
|
+
| Hook<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>[]
|
|
292
|
+
| Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>,
|
|
293
|
+
arg3?:
|
|
294
|
+
| Hook<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>[]
|
|
295
|
+
| Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>,
|
|
296
|
+
arg4?: Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>
|
|
297
|
+
) => this.add(overloadDiscriminer(this, 'head', path, arg2, arg3, arg4))
|
|
268
298
|
}
|
|
269
299
|
|
|
270
300
|
export * from './types'
|
package/src/parser.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MaybeArray, STBody, Context } from './index'
|
|
1
|
+
import type { MaybeArray, STBody, Context, STResponse } from './index'
|
|
2
2
|
import type {
|
|
3
3
|
STStream,
|
|
4
4
|
STUrlForm,
|
|
@@ -283,6 +283,7 @@ const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?: STUrlF
|
|
|
283
283
|
}
|
|
284
284
|
async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundary: string, schema?: STMultipartForm) {
|
|
285
285
|
const bound = textEncoder.encode(boundary)
|
|
286
|
+
const delimiter = textEncoder.encode('\r\n\r\n')
|
|
286
287
|
let rest = new Uint8Array()
|
|
287
288
|
let bK: Uint8Array = new Uint8Array()
|
|
288
289
|
let bV: Uint8Array = new Uint8Array()
|
|
@@ -294,6 +295,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
|
|
|
294
295
|
start = 0
|
|
295
296
|
for (let i = 0; i < chunk.length; i++) {
|
|
296
297
|
let matchBound = true
|
|
298
|
+
let matchDelimiter = true
|
|
297
299
|
for (let b = 0; b < bound.length; b++) {
|
|
298
300
|
if (chunk[i + b] === bound[b]) continue
|
|
299
301
|
else {
|
|
@@ -301,6 +303,15 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
|
|
|
301
303
|
break
|
|
302
304
|
}
|
|
303
305
|
}
|
|
306
|
+
if (!matchBound) {
|
|
307
|
+
for (let b = 0; b < delimiter.length; b++) {
|
|
308
|
+
if (chunk[i + b] === delimiter[b]) continue
|
|
309
|
+
else {
|
|
310
|
+
matchDelimiter = false
|
|
311
|
+
break
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
304
315
|
if (matchBound) {
|
|
305
316
|
bV = new Uint8Array(rest.length + i - start)
|
|
306
317
|
bV.set(rest)
|
|
@@ -324,7 +335,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
|
|
|
324
335
|
start = i + bound.length
|
|
325
336
|
rest = new Uint8Array()
|
|
326
337
|
i = start
|
|
327
|
-
} else if (
|
|
338
|
+
} else if (matchDelimiter) {
|
|
328
339
|
bK = new Uint8Array(rest.length + i - start)
|
|
329
340
|
bK.set(rest)
|
|
330
341
|
bK.set(chunk.slice(start, i), rest.length)
|
|
@@ -630,14 +641,24 @@ export const parseEntry = <T extends STProps>(
|
|
|
630
641
|
return parsedParams as Static<STObject<T>>
|
|
631
642
|
}
|
|
632
643
|
|
|
633
|
-
export const responseParser = (response: any, ctx: Context) => {
|
|
644
|
+
export const responseParser = (response: any, ctx: Context, schema?: STResponse) => {
|
|
634
645
|
const details = {
|
|
635
646
|
status: ctx.set.status || 200,
|
|
636
647
|
headers: new Headers(ctx.set.headers)
|
|
637
648
|
}
|
|
638
649
|
if (response instanceof Response) return response
|
|
639
650
|
else if (typeof response === 'string') {
|
|
640
|
-
if (!details?.headers?.has('content-type'))
|
|
651
|
+
if (!details?.headers?.has('content-type')) {
|
|
652
|
+
if (schema?.[details.status][Kind] === 'json') {
|
|
653
|
+
details?.headers?.set('content-type', 'application/json')
|
|
654
|
+
response = `"${response}"`
|
|
655
|
+
} else details?.headers?.set('content-type', 'text/plain')
|
|
656
|
+
}
|
|
657
|
+
return new Response(response, details)
|
|
658
|
+
} else if (response instanceof Uint8Array) {
|
|
659
|
+
if (!details?.headers?.has('content-type')) {
|
|
660
|
+
details?.headers?.set('content-type', 'application/octet-stream')
|
|
661
|
+
}
|
|
641
662
|
return new Response(response, details)
|
|
642
663
|
}
|
|
643
664
|
if (response instanceof ReadableStream) {
|
|
@@ -666,7 +687,8 @@ export const responseParser = (response: any, ctx: Context) => {
|
|
|
666
687
|
} else {
|
|
667
688
|
try {
|
|
668
689
|
if (!details?.headers?.has('content-type')) details?.headers?.set('content-type', 'application/json')
|
|
669
|
-
|
|
690
|
+
if (details.headers.get('content-type') === 'application/json') response = JSON.stringify(response)
|
|
691
|
+
return new Response(response, details)
|
|
670
692
|
} catch (error) {
|
|
671
693
|
console.error(error)
|
|
672
694
|
throw new InternalError()
|