galbe 0.15.5 → 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 +378 -73
- 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 +368 -92
- 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,5 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { RouteFileMeta } from './routes'
|
|
1
|
+
import type { MiddlewareFileMeta, RouteFileMeta } from './routes'
|
|
3
2
|
import type {
|
|
4
3
|
GalbeConfig,
|
|
5
4
|
Method,
|
|
@@ -8,7 +7,6 @@ import type {
|
|
|
8
7
|
Handler,
|
|
9
8
|
Endpoint,
|
|
10
9
|
Context,
|
|
11
|
-
ContextSet,
|
|
12
10
|
ErrorHandler,
|
|
13
11
|
GalbePlugin,
|
|
14
12
|
STBody,
|
|
@@ -16,17 +14,25 @@ import type {
|
|
|
16
14
|
STParams,
|
|
17
15
|
STHeaders,
|
|
18
16
|
STQuery,
|
|
17
|
+
STCookies,
|
|
19
18
|
StaticEndpoint,
|
|
20
19
|
Route,
|
|
21
20
|
StaticEndpointOptions,
|
|
22
|
-
|
|
21
|
+
GalbeMiddleware,
|
|
22
|
+
MaybeArray,
|
|
23
|
+
MiddlewareDef,
|
|
24
|
+
MiddlewareSchema,
|
|
25
|
+
PreParseHook,
|
|
26
|
+
ResponseHook,
|
|
23
27
|
} from './types'
|
|
24
28
|
|
|
25
|
-
import { readdirSync, statSync } from 'fs'
|
|
29
|
+
import { existsSync, readdirSync, statSync } from 'fs'
|
|
26
30
|
import { resolve as resolvePath } from 'path'
|
|
27
31
|
import server from './server'
|
|
32
|
+
import { joinPath, matchMiddleware, mergeMiddlewareSchema, parseMiddlewarePattern, walkRoutes } from './util'
|
|
28
33
|
import { GalbeRouter } from './router'
|
|
29
|
-
import { SchemaType
|
|
34
|
+
import { SchemaType } from './schema'
|
|
35
|
+
import { compileRoute } from './validator.compile'
|
|
30
36
|
|
|
31
37
|
const overloadDiscriminer = <
|
|
32
38
|
M extends Method,
|
|
@@ -36,19 +42,20 @@ const overloadDiscriminer = <
|
|
|
36
42
|
Q extends STQuery,
|
|
37
43
|
B extends STBody,
|
|
38
44
|
R extends STResponse,
|
|
45
|
+
C extends STCookies,
|
|
39
46
|
>(
|
|
40
47
|
galbe: Galbe,
|
|
41
48
|
method: M,
|
|
42
49
|
path: Path,
|
|
43
50
|
arg2:
|
|
44
|
-
| RequestSchema<M, Path, H, P, Q, B, R>
|
|
45
|
-
| Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[]
|
|
46
|
-
| 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>>,
|
|
47
54
|
arg3?:
|
|
48
|
-
| Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[]
|
|
49
|
-
| Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>,
|
|
50
|
-
arg4?: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
51
|
-
): 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> => {
|
|
52
59
|
const defaultSchema = {}
|
|
53
60
|
if (typeof arg2 === 'function') {
|
|
54
61
|
return galbeMethod(galbe, method, path, defaultSchema, undefined, arg2)
|
|
@@ -62,6 +69,99 @@ const overloadDiscriminer = <
|
|
|
62
69
|
}
|
|
63
70
|
throw new Error('Undefined route signature')
|
|
64
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
|
+
|
|
65
165
|
const galbeMethod = <
|
|
66
166
|
M extends Method,
|
|
67
167
|
Path extends string,
|
|
@@ -70,34 +170,24 @@ const galbeMethod = <
|
|
|
70
170
|
Q extends STQuery,
|
|
71
171
|
B extends STBody,
|
|
72
172
|
R extends STResponse,
|
|
173
|
+
C extends STCookies,
|
|
73
174
|
>(
|
|
74
175
|
_galbe: Galbe,
|
|
75
176
|
method: M,
|
|
76
177
|
path: Path,
|
|
77
|
-
schema: RequestSchema<M, Path, H, P, Q, B, R> | undefined,
|
|
78
|
-
hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[] | undefined,
|
|
79
|
-
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
|
|
80
|
-
): 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> => {
|
|
81
182
|
schema = schema ?? {}
|
|
82
183
|
hooks = hooks || []
|
|
83
|
-
//@ts-ignore
|
|
84
|
-
const context: Context<M, Path, typeof schema> = {
|
|
85
|
-
headers: {} as Static<STObject<Exclude<(typeof schema)['headers'], undefined>>>,
|
|
86
|
-
params: {} as any,
|
|
87
|
-
query: {} as Static<STObject<Exclude<(typeof schema)['query'], undefined>>>,
|
|
88
|
-
body: ['get', 'options', 'head'].includes(method) ? null : ({} as unknown as STBodyValue),
|
|
89
|
-
request: {} as Request,
|
|
90
|
-
cookies: {} as Record<string, string>,
|
|
91
|
-
state: {},
|
|
92
|
-
set: {} as ContextSet,
|
|
93
|
-
}
|
|
94
184
|
return {
|
|
95
185
|
method,
|
|
96
186
|
path,
|
|
97
187
|
schema,
|
|
98
|
-
context,
|
|
99
188
|
hooks,
|
|
100
189
|
handler,
|
|
190
|
+
composed: composeHooks(hooks, handler),
|
|
101
191
|
}
|
|
102
192
|
}
|
|
103
193
|
|
|
@@ -109,6 +199,27 @@ export type { STResponseContent, STResponseBodyKey, STResponseEntry } from './ty
|
|
|
109
199
|
|
|
110
200
|
export const config = (config: GalbeConfig) => config
|
|
111
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
|
+
|
|
112
223
|
/**
|
|
113
224
|
* #### Galbe Server
|
|
114
225
|
* Instanciate a Galbe web server
|
|
@@ -125,28 +236,133 @@ export const config = (config: GalbeConfig) => config
|
|
|
125
236
|
export class Galbe {
|
|
126
237
|
config: GalbeConfig
|
|
127
238
|
meta?: Array<RouteFileMeta> = []
|
|
239
|
+
/** Header metadata of middleware files discovered by the Automatic Route Analyzer. */
|
|
240
|
+
metaMiddleware: Array<MiddlewareFileMeta> = []
|
|
128
241
|
router: GalbeRouter
|
|
129
242
|
startCb: (() => void)[] = []
|
|
130
243
|
stopCb: (() => void)[] = []
|
|
131
244
|
errorCb: ErrorHandler[] = []
|
|
245
|
+
routeAddedCb: ((event: { route: Route }) => void)[] = []
|
|
132
246
|
listening: boolean = false
|
|
133
|
-
server?:
|
|
247
|
+
server?: Awaited<ReturnType<typeof server>>
|
|
134
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 }> = []
|
|
135
252
|
constructor(config?: GalbeConfig) {
|
|
136
253
|
this.config = config ?? {}
|
|
137
254
|
this.router = new GalbeRouter({
|
|
138
255
|
prefix: this.config?.basePath || '',
|
|
139
256
|
cacheEnabled: this.config?.router?.cacheEnabled,
|
|
140
257
|
cacheLimit: this.config?.router?.cacheLimit,
|
|
258
|
+
warn: this.config?.router?.warn,
|
|
141
259
|
})
|
|
142
260
|
}
|
|
143
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)
|
|
144
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 })
|
|
145
268
|
return route
|
|
146
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
|
+
}
|
|
147
286
|
async use(plugin: GalbePlugin) {
|
|
148
287
|
this.plugins.push(plugin)
|
|
149
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
|
+
}
|
|
150
366
|
async init() {
|
|
151
367
|
for (const p of this.plugins) {
|
|
152
368
|
if (p.init) await p.init(this.config?.plugin?.[p.name] || {}, this)
|
|
@@ -181,6 +397,20 @@ export class Galbe {
|
|
|
181
397
|
onError(handler: ErrorHandler) {
|
|
182
398
|
this.errorCb.push(handler)
|
|
183
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
|
+
}
|
|
184
414
|
get: Endpoint<'get'> = <
|
|
185
415
|
Path extends string,
|
|
186
416
|
P extends Partial<STParams<Path>>,
|
|
@@ -188,17 +418,17 @@ export class Galbe {
|
|
|
188
418
|
Q extends STQuery,
|
|
189
419
|
B extends STBody,
|
|
190
420
|
R extends STResponse,
|
|
421
|
+
C extends STCookies,
|
|
191
422
|
>(
|
|
192
423
|
path: Path,
|
|
193
424
|
arg2:
|
|
194
|
-
| RequestSchema<'get', Path, H, P, Q, B, R>
|
|
195
|
-
| Hook<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>[]
|
|
196
|
-
| 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>>,
|
|
197
428
|
arg3?:
|
|
198
|
-
| Hook<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>[]
|
|
199
|
-
| Handler<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>,
|
|
200
|
-
arg4?: Handler<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>
|
|
201
|
-
//@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>>
|
|
202
432
|
) => this.add(overloadDiscriminer(this, 'get', path, arg2, arg3, arg4))
|
|
203
433
|
post: Endpoint<'post'> = <
|
|
204
434
|
Path extends string,
|
|
@@ -207,16 +437,17 @@ export class Galbe {
|
|
|
207
437
|
Q extends STQuery,
|
|
208
438
|
B extends STBody,
|
|
209
439
|
R extends STResponse,
|
|
440
|
+
C extends STCookies,
|
|
210
441
|
>(
|
|
211
442
|
path: Path,
|
|
212
443
|
arg2:
|
|
213
|
-
| RequestSchema<'post', Path, H, P, Q, B, R>
|
|
214
|
-
| Hook<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>[]
|
|
215
|
-
| 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>>,
|
|
216
447
|
arg3?:
|
|
217
|
-
| Hook<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>[]
|
|
218
|
-
| Handler<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>,
|
|
219
|
-
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>>
|
|
220
451
|
) => this.add(overloadDiscriminer(this, 'post', path, arg2, arg3, arg4))
|
|
221
452
|
put: Endpoint<'put'> = <
|
|
222
453
|
Path extends string,
|
|
@@ -225,16 +456,17 @@ export class Galbe {
|
|
|
225
456
|
Q extends STQuery,
|
|
226
457
|
B extends STBody,
|
|
227
458
|
R extends STResponse,
|
|
459
|
+
C extends STCookies,
|
|
228
460
|
>(
|
|
229
461
|
path: Path,
|
|
230
462
|
arg2:
|
|
231
|
-
| RequestSchema<'put', Path, H, P, Q, B, R>
|
|
232
|
-
| Hook<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>[]
|
|
233
|
-
| 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>>,
|
|
234
466
|
arg3?:
|
|
235
|
-
| Hook<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>[]
|
|
236
|
-
| Handler<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>,
|
|
237
|
-
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>>
|
|
238
470
|
) => this.add(overloadDiscriminer(this, 'put', path, arg2, arg3, arg4))
|
|
239
471
|
patch: Endpoint<'patch'> = <
|
|
240
472
|
Path extends string,
|
|
@@ -243,16 +475,17 @@ export class Galbe {
|
|
|
243
475
|
Q extends STQuery,
|
|
244
476
|
B extends STBody,
|
|
245
477
|
R extends STResponse,
|
|
478
|
+
C extends STCookies,
|
|
246
479
|
>(
|
|
247
480
|
path: Path,
|
|
248
481
|
arg2:
|
|
249
|
-
| RequestSchema<'patch', Path, H, P, Q, B, R>
|
|
250
|
-
| Hook<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>[]
|
|
251
|
-
| 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>>,
|
|
252
485
|
arg3?:
|
|
253
|
-
| Hook<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>[]
|
|
254
|
-
| Handler<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>,
|
|
255
|
-
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>>
|
|
256
489
|
) => this.add(overloadDiscriminer(this, 'patch', path, arg2, arg3, arg4))
|
|
257
490
|
delete: Endpoint<'delete'> = <
|
|
258
491
|
Path extends string,
|
|
@@ -261,16 +494,17 @@ export class Galbe {
|
|
|
261
494
|
Q extends STQuery,
|
|
262
495
|
B extends STBody,
|
|
263
496
|
R extends STResponse,
|
|
497
|
+
C extends STCookies,
|
|
264
498
|
>(
|
|
265
499
|
path: Path,
|
|
266
500
|
arg2:
|
|
267
|
-
| RequestSchema<'delete', Path, H, P, Q, B, R>
|
|
268
|
-
| Hook<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>[]
|
|
269
|
-
| 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>>,
|
|
270
504
|
arg3?:
|
|
271
|
-
| Hook<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>[]
|
|
272
|
-
| Handler<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>,
|
|
273
|
-
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>>
|
|
274
508
|
) => this.add(overloadDiscriminer(this, 'delete', path, arg2, arg3, arg4))
|
|
275
509
|
options: Endpoint<'options'> = <
|
|
276
510
|
Path extends string,
|
|
@@ -279,16 +513,17 @@ export class Galbe {
|
|
|
279
513
|
Q extends STQuery,
|
|
280
514
|
B extends STBody,
|
|
281
515
|
R extends STResponse,
|
|
516
|
+
C extends STCookies,
|
|
282
517
|
>(
|
|
283
518
|
path: Path,
|
|
284
519
|
arg2:
|
|
285
|
-
| RequestSchema<'options', Path, H, P, Q, B, R>
|
|
286
|
-
| Hook<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>[]
|
|
287
|
-
| 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>>,
|
|
288
523
|
arg3?:
|
|
289
|
-
| Hook<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>[]
|
|
290
|
-
| Handler<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>,
|
|
291
|
-
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>>
|
|
292
527
|
) => this.add(overloadDiscriminer(this, 'options', path, arg2, arg3, arg4))
|
|
293
528
|
head: Endpoint<'head'> = <
|
|
294
529
|
Path extends string,
|
|
@@ -297,20 +532,23 @@ export class Galbe {
|
|
|
297
532
|
Q extends STQuery,
|
|
298
533
|
B extends STBody,
|
|
299
534
|
R extends STResponse,
|
|
535
|
+
C extends STCookies,
|
|
300
536
|
>(
|
|
301
537
|
path: Path,
|
|
302
538
|
arg2:
|
|
303
|
-
| RequestSchema<'head', Path, H, P, Q, B, R>
|
|
304
|
-
| Hook<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>[]
|
|
305
|
-
| 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>>,
|
|
306
542
|
arg3?:
|
|
307
|
-
| Hook<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>[]
|
|
308
|
-
| Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>,
|
|
309
|
-
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>>
|
|
310
546
|
) => this.add(overloadDiscriminer(this, 'head', path, arg2, arg3, arg4))
|
|
311
547
|
static: StaticEndpoint = (path: string, target: string, options?: StaticEndpointOptions) => {
|
|
312
548
|
let { resolve } = options ?? {}
|
|
313
549
|
const rootPath = path
|
|
550
|
+
const rootTarget = target
|
|
551
|
+
this.staticTargets.push({ path, target })
|
|
314
552
|
|
|
315
553
|
const walkStatic = (path: string, target: string) => {
|
|
316
554
|
path = path?.[0] === '/' ? path : `/${path}`
|
|
@@ -321,6 +559,8 @@ export class Galbe {
|
|
|
321
559
|
t = resolvePath(import.meta.dir, `static-${Bun.env.GALBE_BUILD}/${target}`)
|
|
322
560
|
}
|
|
323
561
|
|
|
562
|
+
if (!existsSync(t)) throw new Error(`galbe.static('${rootPath}', '${rootTarget}'): target does not exist: ${t}`)
|
|
563
|
+
|
|
324
564
|
if (!statSync(t).isDirectory()) {
|
|
325
565
|
let ut: string | null | undefined | void = t
|
|
326
566
|
if (path.endsWith('.html')) path = path.slice(0, -5)
|
|
@@ -350,4 +590,69 @@ export class Galbe {
|
|
|
350
590
|
}
|
|
351
591
|
}
|
|
352
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
|
+
|
|
353
658
|
export * from './types'
|