galbe 0.15.6 → 0.16.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/README.md +3 -0
- package/bin/commands/build.ts +30 -19
- package/bin/commands/dev.ts +53 -5
- package/bin/commands/generate/cli/index.ts +4 -1
- package/bin/commands/generate/client.ts +61 -30
- package/bin/commands/generate/code/openapi.parser.ts +440 -163
- package/bin/commands/generate/code/route-merge.ts +26 -21
- package/bin/commands/generate/code.ts +15 -1
- package/bin/commands/generate/model.ts +4 -1
- package/bin/commands/generate/spec.ts +3 -1
- package/bin/res/client.runtime.ts +5 -0
- package/bin/util.ts +36 -90
- package/package.json +34 -9
- package/src/cookies.ts +29 -8
- package/src/extras/spec/openapi.serializer.ts +287 -99
- package/src/extras.ts +1 -1
- package/src/index.ts +377 -71
- package/src/middlewares/_auth.ts +178 -0
- package/src/middlewares/apiKey.ts +139 -0
- package/src/middlewares/basicAuth.ts +151 -0
- package/src/middlewares/bearer.ts +136 -0
- package/src/middlewares/jwt.ts +455 -0
- package/src/middlewares/logger.ts +120 -0
- package/src/middlewares/rateLimit.ts +153 -0
- package/src/middlewares/requestId.ts +94 -0
- package/src/middlewares/timing.ts +86 -0
- package/src/middlewares.ts +53 -0
- package/src/parser.ts +279 -133
- package/src/router.ts +74 -51
- package/src/routes.ts +220 -136
- package/src/schema.ts +123 -31
- package/src/server.ts +130 -70
- package/src/types.ts +366 -90
- package/src/util.ts +271 -5
- package/src/validator.compile.ts +343 -0
- package/src/validator.ts +64 -18
- package/bin/res/client.template.ts +0 -200
- package/scripts/release.ts +0 -196
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RouteFileMeta } from './routes'
|
|
1
|
+
import type { MiddlewareFileMeta, RouteFileMeta } from './routes'
|
|
2
2
|
import type {
|
|
3
3
|
GalbeConfig,
|
|
4
4
|
Method,
|
|
@@ -7,7 +7,6 @@ import type {
|
|
|
7
7
|
Handler,
|
|
8
8
|
Endpoint,
|
|
9
9
|
Context,
|
|
10
|
-
ContextSet,
|
|
11
10
|
ErrorHandler,
|
|
12
11
|
GalbePlugin,
|
|
13
12
|
STBody,
|
|
@@ -15,17 +14,25 @@ import type {
|
|
|
15
14
|
STParams,
|
|
16
15
|
STHeaders,
|
|
17
16
|
STQuery,
|
|
17
|
+
STCookies,
|
|
18
18
|
StaticEndpoint,
|
|
19
19
|
Route,
|
|
20
20
|
StaticEndpointOptions,
|
|
21
|
-
|
|
21
|
+
GalbeMiddleware,
|
|
22
|
+
MaybeArray,
|
|
23
|
+
MiddlewareDef,
|
|
24
|
+
MiddlewareSchema,
|
|
25
|
+
PreParseHook,
|
|
26
|
+
ResponseHook,
|
|
22
27
|
} from './types'
|
|
23
28
|
|
|
24
|
-
import { readdirSync, statSync } from 'fs'
|
|
29
|
+
import { existsSync, readdirSync, statSync } from 'fs'
|
|
25
30
|
import { resolve as resolvePath } from 'path'
|
|
26
31
|
import server from './server'
|
|
32
|
+
import { joinPath, matchMiddleware, mergeMiddlewareSchema, parseMiddlewarePattern, walkRoutes } from './util'
|
|
27
33
|
import { GalbeRouter } from './router'
|
|
28
|
-
import { SchemaType
|
|
34
|
+
import { SchemaType } from './schema'
|
|
35
|
+
import { compileRoute } from './validator.compile'
|
|
29
36
|
|
|
30
37
|
const overloadDiscriminer = <
|
|
31
38
|
M extends Method,
|
|
@@ -35,19 +42,20 @@ const overloadDiscriminer = <
|
|
|
35
42
|
Q extends STQuery,
|
|
36
43
|
B extends STBody,
|
|
37
44
|
R extends STResponse,
|
|
45
|
+
C extends STCookies,
|
|
38
46
|
>(
|
|
39
47
|
galbe: Galbe,
|
|
40
48
|
method: M,
|
|
41
49
|
path: Path,
|
|
42
50
|
arg2:
|
|
43
|
-
| RequestSchema<M, Path, H, P, Q, B, R>
|
|
44
|
-
| Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[]
|
|
45
|
-
| Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>,
|
|
51
|
+
| RequestSchema<M, Path, H, P, Q, B, R, C>
|
|
52
|
+
| Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R, C>>[]
|
|
53
|
+
| Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R, C>>,
|
|
46
54
|
arg3?:
|
|
47
|
-
| Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[]
|
|
48
|
-
| Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>,
|
|
49
|
-
arg4?: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
50
|
-
): Route<M, Path, P, H, Q, B, R> => {
|
|
55
|
+
| Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R, C>>[]
|
|
56
|
+
| Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R, C>>,
|
|
57
|
+
arg4?: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R, C>>
|
|
58
|
+
): Route<M, Path, P, H, Q, B, R, C> => {
|
|
51
59
|
const defaultSchema = {}
|
|
52
60
|
if (typeof arg2 === 'function') {
|
|
53
61
|
return galbeMethod(galbe, method, path, defaultSchema, undefined, arg2)
|
|
@@ -61,6 +69,99 @@ const overloadDiscriminer = <
|
|
|
61
69
|
}
|
|
62
70
|
throw new Error('Undefined route signature')
|
|
63
71
|
}
|
|
72
|
+
type HookChainState = { handlerCalled: boolean; response: any }
|
|
73
|
+
|
|
74
|
+
// Compose the hook/handler chain once, at registration: a route's chain is
|
|
75
|
+
// immutable after `add()`, so rebuilding it per request is pure allocation
|
|
76
|
+
// churn. Per-request semantics are unchanged: fresh `handlerCalled`/`nextCalled`
|
|
77
|
+
// state per invocation, `Hook already called - ignored` on a double-next(), a
|
|
78
|
+
// truthy hook return short-circuits and becomes the response, and a hook that
|
|
79
|
+
// neither called next() nor returned a value triggers an implicit next().
|
|
80
|
+
// The composed function resolves to the handler's response (or the
|
|
81
|
+
// short-circuit value), starting from '' exactly like the historical chain.
|
|
82
|
+
const composeHooks = <M extends Method, Path extends string, S extends RequestSchema>(
|
|
83
|
+
hooks: Hook<M, Path, S>[],
|
|
84
|
+
handler: Handler<M, Path, S>
|
|
85
|
+
): ((context: Context<M, Path, S>) => Promise<any>) => {
|
|
86
|
+
// terminal entry: run the handler and settle the response status
|
|
87
|
+
let downstream: (context: Context<M, Path, S>, state: HookChainState) => Promise<any> = async (context, state) => {
|
|
88
|
+
state.handlerCalled = true
|
|
89
|
+
state.response = await handler(context)
|
|
90
|
+
context.set.status = state.response instanceof Response ? state.response.status : context.set.status || 200
|
|
91
|
+
}
|
|
92
|
+
for (let i = hooks.length - 1; i >= 0; i--) {
|
|
93
|
+
const hook = hooks[i]!
|
|
94
|
+
const next = downstream
|
|
95
|
+
downstream = async (context, state) => {
|
|
96
|
+
let nextCalled = false
|
|
97
|
+
const nextFn = async () => {
|
|
98
|
+
if (nextCalled) console.error('Hook already called - ignored')
|
|
99
|
+
else {
|
|
100
|
+
nextCalled = true
|
|
101
|
+
return await next(context, state)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const r = await hook(context, nextFn)
|
|
105
|
+
if (r) return r
|
|
106
|
+
if (!nextCalled && !state.handlerCalled) return await nextFn()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const chain = downstream
|
|
110
|
+
return async context => {
|
|
111
|
+
const state: HookChainState = { handlerCalled: false, response: '' }
|
|
112
|
+
const r = await chain(context, state)
|
|
113
|
+
if (r) state.response = r
|
|
114
|
+
return state.response
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The pre-parse slot is a sequence, not an onion: no next(), each hook either
|
|
119
|
+
// returns a Response — which short-circuits the request before a byte of body
|
|
120
|
+
// is read — or falls through to the next one. Composed once at registration,
|
|
121
|
+
// alongside the hook chain; undefined when the slot is empty so the request
|
|
122
|
+
// path can skip it outright.
|
|
123
|
+
const composePreParse = (hooks: PreParseHook[]): Route['composedPre'] => {
|
|
124
|
+
if (!hooks.length) return undefined
|
|
125
|
+
return async context => {
|
|
126
|
+
for (const hook of hooks) {
|
|
127
|
+
const r = await hook(context)
|
|
128
|
+
if (r) return r
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The post-parse slot is a transform, not an onion: by the time a Response
|
|
134
|
+
// exists the hook chain has unwound, so a hook either returns a replacement
|
|
135
|
+
// Response or keeps the one it was handed. Composed once at registration, and
|
|
136
|
+
// undefined when the slot is empty so the request path can skip it outright.
|
|
137
|
+
const composePost = (hooks: ResponseHook[]): Route['composedPost'] => {
|
|
138
|
+
if (!hooks.length) return undefined
|
|
139
|
+
return async (response, context, error) => {
|
|
140
|
+
for (const hook of hooks) {
|
|
141
|
+
const r = await hook(response, context, error)
|
|
142
|
+
if (r) response = r
|
|
143
|
+
}
|
|
144
|
+
return response
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// group prefixes may contain ':params', middleware patterns may not: a param
|
|
149
|
+
// segment matches like '*'
|
|
150
|
+
const patternFromPath = (path: string) => path.replace(/:[^/]+/g, '*')
|
|
151
|
+
|
|
152
|
+
// a bare hook (or hook array) is sugar for { hooks }
|
|
153
|
+
const toMiddlewareDef = (arg?: MaybeArray<Hook> | MiddlewareDef): Omit<GalbeMiddleware, 'pattern' | 'segments'> => {
|
|
154
|
+
const def: MiddlewareDef = typeof arg === 'function' || Array.isArray(arg) ? { hooks: arg } : (arg ?? {})
|
|
155
|
+
return {
|
|
156
|
+
beforeParse: def.beforeParse ? [def.beforeParse].flat() : [],
|
|
157
|
+
hooks: def.hooks ? [def.hooks].flat() : [],
|
|
158
|
+
afterHandle: def.afterHandle ? [def.afterHandle].flat() : [],
|
|
159
|
+
schema: def.schema,
|
|
160
|
+
security: def.security,
|
|
161
|
+
securitySchemes: def.securitySchemes,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
64
165
|
const galbeMethod = <
|
|
65
166
|
M extends Method,
|
|
66
167
|
Path extends string,
|
|
@@ -69,34 +170,24 @@ const galbeMethod = <
|
|
|
69
170
|
Q extends STQuery,
|
|
70
171
|
B extends STBody,
|
|
71
172
|
R extends STResponse,
|
|
173
|
+
C extends STCookies,
|
|
72
174
|
>(
|
|
73
175
|
_galbe: Galbe,
|
|
74
176
|
method: M,
|
|
75
177
|
path: Path,
|
|
76
|
-
schema: RequestSchema<M, Path, H, P, Q, B, R> | undefined,
|
|
77
|
-
hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[] | undefined,
|
|
78
|
-
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
79
|
-
): Route<M, Path, P, H, Q, B, R> => {
|
|
178
|
+
schema: RequestSchema<M, Path, H, P, Q, B, R, C> | undefined,
|
|
179
|
+
hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R, C>>[] | undefined,
|
|
180
|
+
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R, C>>
|
|
181
|
+
): Route<M, Path, P, H, Q, B, R, C> => {
|
|
80
182
|
schema = schema ?? {}
|
|
81
183
|
hooks = hooks || []
|
|
82
|
-
//@ts-ignore
|
|
83
|
-
const context: Context<M, Path, typeof schema> = {
|
|
84
|
-
headers: {} as Static<STObject<Exclude<(typeof schema)['headers'], undefined>>>,
|
|
85
|
-
params: {} as any,
|
|
86
|
-
query: {} as Static<STObject<Exclude<(typeof schema)['query'], undefined>>>,
|
|
87
|
-
body: ['get', 'options', 'head'].includes(method) ? null : ({} as unknown as STBodyValue),
|
|
88
|
-
request: {} as Request,
|
|
89
|
-
cookies: {} as Record<string, string>,
|
|
90
|
-
state: {},
|
|
91
|
-
set: {} as ContextSet,
|
|
92
|
-
}
|
|
93
184
|
return {
|
|
94
185
|
method,
|
|
95
186
|
path,
|
|
96
187
|
schema,
|
|
97
|
-
context,
|
|
98
188
|
hooks,
|
|
99
189
|
handler,
|
|
190
|
+
composed: composeHooks(hooks, handler),
|
|
100
191
|
}
|
|
101
192
|
}
|
|
102
193
|
|
|
@@ -108,6 +199,27 @@ export type { STResponseContent, STResponseBodyKey, STResponseEntry } from './ty
|
|
|
108
199
|
|
|
109
200
|
export const config = (config: GalbeConfig) => config
|
|
110
201
|
|
|
202
|
+
/**
|
|
203
|
+
* #### Middleware
|
|
204
|
+
* Define a middleware as a value: hooks, the request contract they impose, and
|
|
205
|
+
* the security scheme they enforce, in one exportable thing. Identity at
|
|
206
|
+
* runtime — it exists so the `schema` fragment types the def's own handlers,
|
|
207
|
+
* which a bare object literal cannot do.
|
|
208
|
+
*
|
|
209
|
+
* ---
|
|
210
|
+
* @example
|
|
211
|
+
* ```typescript
|
|
212
|
+
* // src/api/tenant.middleware.ts — the directory is the scope
|
|
213
|
+
* export default middleware({
|
|
214
|
+
* schema: { headers: { 'x-tenant-id': $T.string() } },
|
|
215
|
+
* hooks: ctx => {
|
|
216
|
+
* ctx.state.tenant = ctx.headers['x-tenant-id'] // string
|
|
217
|
+
* }
|
|
218
|
+
* })
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
export const middleware = <F extends MiddlewareSchema>(def: MiddlewareDef<F>): MiddlewareDef<F> => def
|
|
222
|
+
|
|
111
223
|
/**
|
|
112
224
|
* #### Galbe Server
|
|
113
225
|
* Instanciate a Galbe web server
|
|
@@ -124,28 +236,133 @@ export const config = (config: GalbeConfig) => config
|
|
|
124
236
|
export class Galbe {
|
|
125
237
|
config: GalbeConfig
|
|
126
238
|
meta?: Array<RouteFileMeta> = []
|
|
239
|
+
/** Header metadata of middleware files discovered by the Automatic Route Analyzer. */
|
|
240
|
+
metaMiddleware: Array<MiddlewareFileMeta> = []
|
|
127
241
|
router: GalbeRouter
|
|
128
242
|
startCb: (() => void)[] = []
|
|
129
243
|
stopCb: (() => void)[] = []
|
|
130
244
|
errorCb: ErrorHandler[] = []
|
|
245
|
+
routeAddedCb: ((event: { route: Route }) => void)[] = []
|
|
131
246
|
listening: boolean = false
|
|
132
247
|
server?: Awaited<ReturnType<typeof server>>
|
|
133
248
|
plugins: GalbePlugin[] = []
|
|
249
|
+
middlewares: GalbeMiddleware[] = []
|
|
250
|
+
/** User-supplied `static(path, target)` pairs, recorded so `galbe build` can copy the assets next to the bundle. */
|
|
251
|
+
staticTargets: Array<{ path: string; target: string }> = []
|
|
134
252
|
constructor(config?: GalbeConfig) {
|
|
135
253
|
this.config = config ?? {}
|
|
136
254
|
this.router = new GalbeRouter({
|
|
137
255
|
prefix: this.config?.basePath || '',
|
|
138
256
|
cacheEnabled: this.config?.router?.cacheEnabled,
|
|
139
257
|
cacheLimit: this.config?.router?.cacheLimit,
|
|
258
|
+
warn: this.config?.router?.warn,
|
|
140
259
|
})
|
|
141
260
|
}
|
|
142
261
|
private add(route: any) {
|
|
262
|
+
// schemas are immutable once the route is added: compile their validators now
|
|
263
|
+
if (route?.schema) compileRoute(route.schema)
|
|
143
264
|
this.router.add(route)
|
|
265
|
+
const segments = this.routeSegments(route)
|
|
266
|
+
if (this.middlewares.some(m => matchMiddleware(m.segments, segments))) this.composeMiddleware(route)
|
|
267
|
+
for (const cb of this.routeAddedCb) cb({ route })
|
|
144
268
|
return route
|
|
145
269
|
}
|
|
270
|
+
// route.path carries the basePath prefix once registered: strip it, patterns
|
|
271
|
+
// are written relative to basePath like route paths
|
|
272
|
+
private routeSegments(route: Route): string[] {
|
|
273
|
+
const path = this.router.prefix ? route.path.slice(this.router.prefix.length) : route.path
|
|
274
|
+
return path.split('/').filter(s => s !== '')
|
|
275
|
+
}
|
|
276
|
+
private composeMiddleware(route: Route) {
|
|
277
|
+
const segments = this.routeSegments(route)
|
|
278
|
+
const matched = this.middlewares.filter(m => matchMiddleware(m.segments, segments))
|
|
279
|
+
// merged fragments bring in schemas that were never compiled; compile()
|
|
280
|
+
// caches per schema object, so only the new ones are built
|
|
281
|
+
if (mergeMiddlewareSchema(route, matched)) compileRoute(route.schema)
|
|
282
|
+
route.composed = composeHooks([...matched.flatMap(m => m.hooks), ...route.hooks], route.handler)
|
|
283
|
+
route.composedPre = composePreParse(matched.flatMap(m => m.beforeParse))
|
|
284
|
+
route.composedPost = composePost(matched.flatMap(m => m.afterHandle))
|
|
285
|
+
}
|
|
146
286
|
async use(plugin: GalbePlugin) {
|
|
147
287
|
this.plugins.push(plugin)
|
|
148
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* #### Middleware
|
|
291
|
+
* Register hooks — or a {@link MiddlewareDef} — that apply to every route
|
|
292
|
+
* whose path matches the given pattern, ahead of the route's own hooks.
|
|
293
|
+
* Pattern segments are literals or `*` (any single segment); a trailing `*`
|
|
294
|
+
* matches the whole subtree, including the prefix itself. Patterns match
|
|
295
|
+
* registered route paths (not request URLs) and are resolved at registration
|
|
296
|
+
* time: matched hooks are composed into the route chain and the def's schema
|
|
297
|
+
* fragment is merged into the route schema, adding no per-request cost.
|
|
298
|
+
*
|
|
299
|
+
* ---
|
|
300
|
+
* @example
|
|
301
|
+
* ```typescript
|
|
302
|
+
* galbe.middleware(logger) // every route
|
|
303
|
+
* galbe.middleware('/api/*', authHook) // the /api subtree
|
|
304
|
+
* galbe.middleware('/api/*', middleware({ schema: { headers: { authorization: $T.string() } }, hooks: authHook }))
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
middleware(hooks: MaybeArray<Hook>): void
|
|
308
|
+
middleware<F extends MiddlewareSchema>(def: MiddlewareDef<F>): void
|
|
309
|
+
middleware(pattern: string, hooks: MaybeArray<Hook>): void
|
|
310
|
+
middleware<F extends MiddlewareSchema>(pattern: string, def: MiddlewareDef<F>): void
|
|
311
|
+
middleware(arg1: string | MaybeArray<Hook> | MiddlewareDef, arg2?: MaybeArray<Hook> | MiddlewareDef): void {
|
|
312
|
+
const pattern = typeof arg1 === 'string' ? arg1 : '*'
|
|
313
|
+
const def = toMiddlewareDef(typeof arg1 === 'string' ? arg2 : arg1)
|
|
314
|
+
if (
|
|
315
|
+
!def.hooks.length &&
|
|
316
|
+
!def.beforeParse.length &&
|
|
317
|
+
!def.afterHandle.length &&
|
|
318
|
+
!def.schema &&
|
|
319
|
+
!def.security &&
|
|
320
|
+
!def.securitySchemes
|
|
321
|
+
)
|
|
322
|
+
return
|
|
323
|
+
const entry = { pattern, segments: parseMiddlewarePattern(pattern), ...def }
|
|
324
|
+
this.middlewares.push(entry)
|
|
325
|
+
// routes registered before this call: recompose the ones the new entry matches
|
|
326
|
+
walkRoutes(this.router.routes, route => {
|
|
327
|
+
if (matchMiddleware(entry.segments, this.routeSegments(route))) this.composeMiddleware(route)
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* #### Route group
|
|
332
|
+
* Register routes under a shared path prefix. Optional hooks — or a
|
|
333
|
+
* middleware def — apply to the whole `<prefix>/*` subtree: they are prefix
|
|
334
|
+
* middleware, so they also cover matching routes registered outside the
|
|
335
|
+
* group. A def's schema fragment types the routes registered through the
|
|
336
|
+
* group registrar, on top of merging into their schemas.
|
|
337
|
+
*
|
|
338
|
+
* ---
|
|
339
|
+
* @example
|
|
340
|
+
* ```typescript
|
|
341
|
+
* galbe.group('/v1', [authHook], g => {
|
|
342
|
+
* g.get('/users', listUsers) // GET /v1/users
|
|
343
|
+
* g.group('/admin', a => { ... }) // /v1/admin/...
|
|
344
|
+
* })
|
|
345
|
+
*
|
|
346
|
+
* galbe.group('/v1', middleware({ schema: { headers: { authorization: $T.string() } } }), g => {
|
|
347
|
+
* g.get('/users', ctx => ctx.headers.authorization) // string
|
|
348
|
+
* })
|
|
349
|
+
* ```
|
|
350
|
+
*/
|
|
351
|
+
group<P extends string>(prefix: P, cb: (group: GalbeGroup<P>) => void): GalbeGroup<P>
|
|
352
|
+
group<P extends string>(prefix: P, hooks: Hook[], cb: (group: GalbeGroup<P>) => void): GalbeGroup<P>
|
|
353
|
+
group<P extends string, F extends MiddlewareSchema>(
|
|
354
|
+
prefix: P,
|
|
355
|
+
def: MiddlewareDef<F>,
|
|
356
|
+
cb: (group: GalbeGroup<P, F>) => void
|
|
357
|
+
): GalbeGroup<P, F>
|
|
358
|
+
group(prefix: string, arg2: any, arg3?: any): any {
|
|
359
|
+
const cb = typeof arg2 === 'function' ? arg2 : arg3
|
|
360
|
+
const scoped = Array.isArray(arg2) ? arg2.length > 0 : !!arg2 && typeof arg2 === 'object'
|
|
361
|
+
if (scoped) this.middleware(patternFromPath(joinPath(prefix, '/*')), arg2)
|
|
362
|
+
const group = new GalbeGroup(this, prefix)
|
|
363
|
+
cb?.(group)
|
|
364
|
+
return group
|
|
365
|
+
}
|
|
149
366
|
async init() {
|
|
150
367
|
for (const p of this.plugins) {
|
|
151
368
|
if (p.init) await p.init(this.config?.plugin?.[p.name] || {}, this)
|
|
@@ -180,6 +397,20 @@ export class Galbe {
|
|
|
180
397
|
onError(handler: ErrorHandler) {
|
|
181
398
|
this.errorCb.push(handler)
|
|
182
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* #### Route registration event
|
|
402
|
+
* Subscribe to route registrations: the callback fires synchronously for
|
|
403
|
+
* every route added to the router, right after its hook chain is composed,
|
|
404
|
+
* with the final (prefixed) path. Listener errors propagate to the
|
|
405
|
+
* registration site. Returns an unsubscribe function.
|
|
406
|
+
*/
|
|
407
|
+
onRouteAdded(callback: (event: { route: Route }) => void): () => void {
|
|
408
|
+
this.routeAddedCb.push(callback)
|
|
409
|
+
return () => {
|
|
410
|
+
const idx = this.routeAddedCb.indexOf(callback)
|
|
411
|
+
if (idx >= 0) this.routeAddedCb.splice(idx, 1)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
183
414
|
get: Endpoint<'get'> = <
|
|
184
415
|
Path extends string,
|
|
185
416
|
P extends Partial<STParams<Path>>,
|
|
@@ -187,17 +418,17 @@ export class Galbe {
|
|
|
187
418
|
Q extends STQuery,
|
|
188
419
|
B extends STBody,
|
|
189
420
|
R extends STResponse,
|
|
421
|
+
C extends STCookies,
|
|
190
422
|
>(
|
|
191
423
|
path: Path,
|
|
192
424
|
arg2:
|
|
193
|
-
| RequestSchema<'get', Path, H, P, Q, B, R>
|
|
194
|
-
| Hook<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>[]
|
|
195
|
-
| Handler<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>,
|
|
425
|
+
| RequestSchema<'get', Path, H, P, Q, B, R, C>
|
|
426
|
+
| Hook<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R, C>>[]
|
|
427
|
+
| Handler<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R, C>>,
|
|
196
428
|
arg3?:
|
|
197
|
-
| Hook<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>[]
|
|
198
|
-
| Handler<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>,
|
|
199
|
-
arg4?: Handler<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>
|
|
200
|
-
//@ts-ignore
|
|
429
|
+
| Hook<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R, C>>[]
|
|
430
|
+
| Handler<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R, C>>,
|
|
431
|
+
arg4?: Handler<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R, C>>
|
|
201
432
|
) => this.add(overloadDiscriminer(this, 'get', path, arg2, arg3, arg4))
|
|
202
433
|
post: Endpoint<'post'> = <
|
|
203
434
|
Path extends string,
|
|
@@ -206,16 +437,17 @@ export class Galbe {
|
|
|
206
437
|
Q extends STQuery,
|
|
207
438
|
B extends STBody,
|
|
208
439
|
R extends STResponse,
|
|
440
|
+
C extends STCookies,
|
|
209
441
|
>(
|
|
210
442
|
path: Path,
|
|
211
443
|
arg2:
|
|
212
|
-
| RequestSchema<'post', Path, H, P, Q, B, R>
|
|
213
|
-
| Hook<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>[]
|
|
214
|
-
| Handler<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>,
|
|
444
|
+
| RequestSchema<'post', Path, H, P, Q, B, R, C>
|
|
445
|
+
| Hook<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R, C>>[]
|
|
446
|
+
| Handler<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R, C>>,
|
|
215
447
|
arg3?:
|
|
216
|
-
| Hook<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>[]
|
|
217
|
-
| Handler<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>,
|
|
218
|
-
arg4?: Handler<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>
|
|
448
|
+
| Hook<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R, C>>[]
|
|
449
|
+
| Handler<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R, C>>,
|
|
450
|
+
arg4?: Handler<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R, C>>
|
|
219
451
|
) => this.add(overloadDiscriminer(this, 'post', path, arg2, arg3, arg4))
|
|
220
452
|
put: Endpoint<'put'> = <
|
|
221
453
|
Path extends string,
|
|
@@ -224,16 +456,17 @@ export class Galbe {
|
|
|
224
456
|
Q extends STQuery,
|
|
225
457
|
B extends STBody,
|
|
226
458
|
R extends STResponse,
|
|
459
|
+
C extends STCookies,
|
|
227
460
|
>(
|
|
228
461
|
path: Path,
|
|
229
462
|
arg2:
|
|
230
|
-
| RequestSchema<'put', Path, H, P, Q, B, R>
|
|
231
|
-
| Hook<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>[]
|
|
232
|
-
| Handler<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>,
|
|
463
|
+
| RequestSchema<'put', Path, H, P, Q, B, R, C>
|
|
464
|
+
| Hook<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R, C>>[]
|
|
465
|
+
| Handler<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R, C>>,
|
|
233
466
|
arg3?:
|
|
234
|
-
| Hook<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>[]
|
|
235
|
-
| Handler<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>,
|
|
236
|
-
arg4?: Handler<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>
|
|
467
|
+
| Hook<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R, C>>[]
|
|
468
|
+
| Handler<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R, C>>,
|
|
469
|
+
arg4?: Handler<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R, C>>
|
|
237
470
|
) => this.add(overloadDiscriminer(this, 'put', path, arg2, arg3, arg4))
|
|
238
471
|
patch: Endpoint<'patch'> = <
|
|
239
472
|
Path extends string,
|
|
@@ -242,16 +475,17 @@ export class Galbe {
|
|
|
242
475
|
Q extends STQuery,
|
|
243
476
|
B extends STBody,
|
|
244
477
|
R extends STResponse,
|
|
478
|
+
C extends STCookies,
|
|
245
479
|
>(
|
|
246
480
|
path: Path,
|
|
247
481
|
arg2:
|
|
248
|
-
| RequestSchema<'patch', Path, H, P, Q, B, R>
|
|
249
|
-
| Hook<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>[]
|
|
250
|
-
| Handler<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>,
|
|
482
|
+
| RequestSchema<'patch', Path, H, P, Q, B, R, C>
|
|
483
|
+
| Hook<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R, C>>[]
|
|
484
|
+
| Handler<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R, C>>,
|
|
251
485
|
arg3?:
|
|
252
|
-
| Hook<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>[]
|
|
253
|
-
| Handler<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>,
|
|
254
|
-
arg4?: Handler<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>
|
|
486
|
+
| Hook<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R, C>>[]
|
|
487
|
+
| Handler<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R, C>>,
|
|
488
|
+
arg4?: Handler<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R, C>>
|
|
255
489
|
) => this.add(overloadDiscriminer(this, 'patch', path, arg2, arg3, arg4))
|
|
256
490
|
delete: Endpoint<'delete'> = <
|
|
257
491
|
Path extends string,
|
|
@@ -260,16 +494,17 @@ export class Galbe {
|
|
|
260
494
|
Q extends STQuery,
|
|
261
495
|
B extends STBody,
|
|
262
496
|
R extends STResponse,
|
|
497
|
+
C extends STCookies,
|
|
263
498
|
>(
|
|
264
499
|
path: Path,
|
|
265
500
|
arg2:
|
|
266
|
-
| RequestSchema<'delete', Path, H, P, Q, B, R>
|
|
267
|
-
| Hook<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>[]
|
|
268
|
-
| Handler<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>,
|
|
501
|
+
| RequestSchema<'delete', Path, H, P, Q, B, R, C>
|
|
502
|
+
| Hook<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R, C>>[]
|
|
503
|
+
| Handler<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R, C>>,
|
|
269
504
|
arg3?:
|
|
270
|
-
| Hook<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>[]
|
|
271
|
-
| Handler<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>,
|
|
272
|
-
arg4?: Handler<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>
|
|
505
|
+
| Hook<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R, C>>[]
|
|
506
|
+
| Handler<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R, C>>,
|
|
507
|
+
arg4?: Handler<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R, C>>
|
|
273
508
|
) => this.add(overloadDiscriminer(this, 'delete', path, arg2, arg3, arg4))
|
|
274
509
|
options: Endpoint<'options'> = <
|
|
275
510
|
Path extends string,
|
|
@@ -278,16 +513,17 @@ export class Galbe {
|
|
|
278
513
|
Q extends STQuery,
|
|
279
514
|
B extends STBody,
|
|
280
515
|
R extends STResponse,
|
|
516
|
+
C extends STCookies,
|
|
281
517
|
>(
|
|
282
518
|
path: Path,
|
|
283
519
|
arg2:
|
|
284
|
-
| RequestSchema<'options', Path, H, P, Q, B, R>
|
|
285
|
-
| Hook<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>[]
|
|
286
|
-
| Handler<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>,
|
|
520
|
+
| RequestSchema<'options', Path, H, P, Q, B, R, C>
|
|
521
|
+
| Hook<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R, C>>[]
|
|
522
|
+
| Handler<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R, C>>,
|
|
287
523
|
arg3?:
|
|
288
|
-
| Hook<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>[]
|
|
289
|
-
| Handler<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>,
|
|
290
|
-
arg4?: Handler<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>
|
|
524
|
+
| Hook<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R, C>>[]
|
|
525
|
+
| Handler<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R, C>>,
|
|
526
|
+
arg4?: Handler<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R, C>>
|
|
291
527
|
) => this.add(overloadDiscriminer(this, 'options', path, arg2, arg3, arg4))
|
|
292
528
|
head: Endpoint<'head'> = <
|
|
293
529
|
Path extends string,
|
|
@@ -296,20 +532,23 @@ export class Galbe {
|
|
|
296
532
|
Q extends STQuery,
|
|
297
533
|
B extends STBody,
|
|
298
534
|
R extends STResponse,
|
|
535
|
+
C extends STCookies,
|
|
299
536
|
>(
|
|
300
537
|
path: Path,
|
|
301
538
|
arg2:
|
|
302
|
-
| RequestSchema<'head', Path, H, P, Q, B, R>
|
|
303
|
-
| Hook<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>[]
|
|
304
|
-
| Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>,
|
|
539
|
+
| RequestSchema<'head', Path, H, P, Q, B, R, C>
|
|
540
|
+
| Hook<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R, C>>[]
|
|
541
|
+
| Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R, C>>,
|
|
305
542
|
arg3?:
|
|
306
|
-
| Hook<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>[]
|
|
307
|
-
| Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>,
|
|
308
|
-
arg4?: Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>
|
|
543
|
+
| Hook<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R, C>>[]
|
|
544
|
+
| Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R, C>>,
|
|
545
|
+
arg4?: Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R, C>>
|
|
309
546
|
) => this.add(overloadDiscriminer(this, 'head', path, arg2, arg3, arg4))
|
|
310
547
|
static: StaticEndpoint = (path: string, target: string, options?: StaticEndpointOptions) => {
|
|
311
548
|
let { resolve } = options ?? {}
|
|
312
549
|
const rootPath = path
|
|
550
|
+
const rootTarget = target
|
|
551
|
+
this.staticTargets.push({ path, target })
|
|
313
552
|
|
|
314
553
|
const walkStatic = (path: string, target: string) => {
|
|
315
554
|
path = path?.[0] === '/' ? path : `/${path}`
|
|
@@ -320,6 +559,8 @@ export class Galbe {
|
|
|
320
559
|
t = resolvePath(import.meta.dir, `static-${Bun.env.GALBE_BUILD}/${target}`)
|
|
321
560
|
}
|
|
322
561
|
|
|
562
|
+
if (!existsSync(t)) throw new Error(`galbe.static('${rootPath}', '${rootTarget}'): target does not exist: ${t}`)
|
|
563
|
+
|
|
323
564
|
if (!statSync(t).isDirectory()) {
|
|
324
565
|
let ut: string | null | undefined | void = t
|
|
325
566
|
if (path.endsWith('.html')) path = path.slice(0, -5)
|
|
@@ -349,4 +590,69 @@ export class Galbe {
|
|
|
349
590
|
}
|
|
350
591
|
}
|
|
351
592
|
|
|
593
|
+
/**
|
|
594
|
+
* #### GalbeGroup
|
|
595
|
+
* Route sub-registrar created by {@link Galbe.group}. Paths are prefixed at
|
|
596
|
+
* registration time: router matching and precedence are unchanged, and the
|
|
597
|
+
* prefixed paths flow as-is into the generated OpenAPI spec. `F` carries the
|
|
598
|
+
* schema fragment of the group's middleware def, so routes registered here are
|
|
599
|
+
* typed with it — route-declared keys win, as they do at runtime.
|
|
600
|
+
*/
|
|
601
|
+
export class GalbeGroup<Prefix extends string = string, F extends MiddlewareSchema = {}> {
|
|
602
|
+
#galbe: Galbe
|
|
603
|
+
#prefix: string
|
|
604
|
+
constructor(galbe: Galbe, prefix: string) {
|
|
605
|
+
this.#galbe = galbe
|
|
606
|
+
this.#prefix = joinPath('', prefix).replace(/\/+$/, '')
|
|
607
|
+
}
|
|
608
|
+
#route(method: Method, path: string, args: any[]): any {
|
|
609
|
+
// indexing by a Method union yields a union of Endpoint overload sets, which
|
|
610
|
+
// has no common call signature — the dispatch is checked at the call sites
|
|
611
|
+
return (this.#galbe[method] as (path: string, ...args: any[]) => any)(joinPath(this.#prefix, path), ...args)
|
|
612
|
+
}
|
|
613
|
+
get: Endpoint<'get', Prefix, F> = (path: any, ...args: any[]): any => this.#route('get', path, args)
|
|
614
|
+
post: Endpoint<'post', Prefix, F> = (path: any, ...args: any[]): any => this.#route('post', path, args)
|
|
615
|
+
put: Endpoint<'put', Prefix, F> = (path: any, ...args: any[]): any => this.#route('put', path, args)
|
|
616
|
+
patch: Endpoint<'patch', Prefix, F> = (path: any, ...args: any[]): any => this.#route('patch', path, args)
|
|
617
|
+
delete: Endpoint<'delete', Prefix, F> = (path: any, ...args: any[]): any => this.#route('delete', path, args)
|
|
618
|
+
options: Endpoint<'options', Prefix, F> = (path: any, ...args: any[]): any => this.#route('options', path, args)
|
|
619
|
+
head: Endpoint<'head', Prefix, F> = (path: any, ...args: any[]): any => this.#route('head', path, args)
|
|
620
|
+
static: StaticEndpoint = (path: any, target: any, options?: any): any =>
|
|
621
|
+
this.#galbe.static(joinPath(this.#prefix, path), target, options)
|
|
622
|
+
/**
|
|
623
|
+
* Register middleware scoped to the group: bare hooks or a def cover the
|
|
624
|
+
* group subtree, patterns are relative to the group prefix. The fragment is
|
|
625
|
+
* merged and validated, but only `group(prefix, def, cb)` can type the
|
|
626
|
+
* routes — a mutating call has no value to carry the type on.
|
|
627
|
+
*/
|
|
628
|
+
middleware(hooks: MaybeArray<Hook>): void
|
|
629
|
+
middleware<G extends MiddlewareSchema>(def: MiddlewareDef<G>): void
|
|
630
|
+
middleware(pattern: string, hooks: MaybeArray<Hook>): void
|
|
631
|
+
middleware<G extends MiddlewareSchema>(pattern: string, def: MiddlewareDef<G>): void
|
|
632
|
+
middleware(arg1: string | MaybeArray<Hook> | MiddlewareDef, arg2?: MaybeArray<Hook> | MiddlewareDef): void {
|
|
633
|
+
const prefix = patternFromPath(this.#prefix)
|
|
634
|
+
// the overloads discriminate hooks from defs; the implementation forwards the union
|
|
635
|
+
const scope = typeof arg1 === 'string' ? joinPath(prefix, arg1) : joinPath(prefix, '/*')
|
|
636
|
+
this.#galbe.middleware(scope, (typeof arg1 === 'string' ? arg2! : arg1) as MaybeArray<Hook>)
|
|
637
|
+
}
|
|
638
|
+
group<P extends string>(
|
|
639
|
+
prefix: P,
|
|
640
|
+
cb: (group: GalbeGroup<`${Prefix}${P}`, F>) => void
|
|
641
|
+
): GalbeGroup<`${Prefix}${P}`, F>
|
|
642
|
+
group<P extends string>(
|
|
643
|
+
prefix: P,
|
|
644
|
+
hooks: Hook[],
|
|
645
|
+
cb: (group: GalbeGroup<`${Prefix}${P}`, F>) => void
|
|
646
|
+
): GalbeGroup<`${Prefix}${P}`, F>
|
|
647
|
+
// nested defs stack: the inner fragment merges over the outer one
|
|
648
|
+
group<P extends string, G extends MiddlewareSchema>(
|
|
649
|
+
prefix: P,
|
|
650
|
+
def: MiddlewareDef<G>,
|
|
651
|
+
cb: (group: GalbeGroup<`${Prefix}${P}`, F & G>) => void
|
|
652
|
+
): GalbeGroup<`${Prefix}${P}`, F & G>
|
|
653
|
+
group(prefix: string, arg2: any, arg3?: any): any {
|
|
654
|
+
return this.#galbe.group(joinPath(this.#prefix, prefix), arg2, arg3)
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
352
658
|
export * from './types'
|