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/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SocketAddress, TLSOptions } from 'bun'
|
|
2
|
+
import type { OpenAPIV3 } from 'openapi-types'
|
|
2
3
|
import type {
|
|
3
4
|
STAny,
|
|
4
5
|
STArray,
|
|
@@ -59,6 +60,24 @@ export type STBodyValue =
|
|
|
59
60
|
|
|
60
61
|
export type STBodyContent = Partial<Record<MediaType, STBodyValue>>
|
|
61
62
|
export type STBody = STNull | STBodyContent
|
|
63
|
+
/**
|
|
64
|
+
* Metadata a request body may carry beside its media types — describing the
|
|
65
|
+
* body itself rather than any one of its schemas. Kept out of `STBodyContent`
|
|
66
|
+
* on purpose: `Context` maps over the body's keys to derive `contentType`, so
|
|
67
|
+
* anything intersected there would surface as a bogus content type. `MediaType`
|
|
68
|
+
* is a `${string}/${string}` pattern, so these keys never collide with a body.
|
|
69
|
+
*/
|
|
70
|
+
export type STBodyMeta = {
|
|
71
|
+
/** Describes the request body itself, as opposed to any one of its schemas. */
|
|
72
|
+
description?: string
|
|
73
|
+
/** Whether the request body is required. Inferred from the schemas when unset. */
|
|
74
|
+
required?: boolean
|
|
75
|
+
/**
|
|
76
|
+
* Names the `components.requestBodies` entry this body came from, so spec
|
|
77
|
+
* generators can emit it once and `$ref` it. Set by `galbe generate code`.
|
|
78
|
+
*/
|
|
79
|
+
_requestBodyId?: string
|
|
80
|
+
}
|
|
62
81
|
export type STBodyType = MediaType
|
|
63
82
|
|
|
64
83
|
export type STResponseBodyValue =
|
|
@@ -80,10 +99,28 @@ export type STResponseBodyValue =
|
|
|
80
99
|
export type STResponseContent = Partial<Record<MediaType, STResponseBodyValue>> & {
|
|
81
100
|
description?: string
|
|
82
101
|
responseHeaders?: Record<string, STSchema>
|
|
102
|
+
/** The OpenAPI `links` object for this response, carried verbatim. Documentation only. */
|
|
103
|
+
responseLinks?: Record<string, any>
|
|
104
|
+
/** A single example, applied to every media type this response offers. */
|
|
105
|
+
example?: any
|
|
106
|
+
/** Named examples (OpenAPI `examples`), applied to every media type offered. */
|
|
107
|
+
examples?: Record<string, any>
|
|
108
|
+
/**
|
|
109
|
+
* Names the `components.responses` entry this response came from, so spec
|
|
110
|
+
* generators can emit it once and `$ref` it. Set by `galbe generate code`.
|
|
111
|
+
*/
|
|
112
|
+
_responseId?: string
|
|
83
113
|
}
|
|
84
114
|
export type STResponseBodyKey = MediaType
|
|
85
115
|
export type STResponseEntry = STResponseValue | STResponseContent
|
|
86
|
-
|
|
116
|
+
/**
|
|
117
|
+
* A wildcard status range, spelled as in OpenAPI: `'4XX'` covers every 4xx
|
|
118
|
+
* status. An exact status always wins over the range that contains it, which
|
|
119
|
+
* in turn wins over `'default'` — for response validation, for the content
|
|
120
|
+
* type inferred on a string response, and for the generated spec alike.
|
|
121
|
+
*/
|
|
122
|
+
export type STResponseRange = '1XX' | '2XX' | '3XX' | '4XX' | '5XX'
|
|
123
|
+
export type STResponse = Partial<Record<number | STResponseRange | 'default', STResponseEntry>>
|
|
87
124
|
|
|
88
125
|
export type MaybeArray<T> = T | T[]
|
|
89
126
|
export type MaybeSTArray<T extends STSchema> = T | STArray<T>
|
|
@@ -109,9 +146,17 @@ type STParamsValue = MaybeSTUnion<STParamsPrimaryValue>
|
|
|
109
146
|
export type STParams<Path extends string = string> = Record<ExtractParams<Path>, STParamsValue>
|
|
110
147
|
|
|
111
148
|
type STQueryPrimaryValue = STString | STBoolean | STNumber | STInteger | STLiteral
|
|
112
|
-
|
|
149
|
+
// An object query parameter is OpenAPI's `deepObject`: `?filter[lat]=1&filter[lon]=2`.
|
|
150
|
+
// A JSON-encoded value is accepted for it too.
|
|
151
|
+
type STQueryValue = MaybeSTArray<MaybeSTUnion<STQueryPrimaryValue>> | STObject
|
|
113
152
|
export type STQuery = Record<string, STQueryValue>
|
|
114
153
|
|
|
154
|
+
// A cookie arrives as one string; like headers and query params it is parsed
|
|
155
|
+
// into the declared primitive, so the same value shapes apply.
|
|
156
|
+
type STCookiesPrimaryValue = STString | STBoolean | STNumber | STInteger | STLiteral
|
|
157
|
+
type STCookiesValue = MaybeSTUnion<STCookiesPrimaryValue>
|
|
158
|
+
export type STCookies = Record<string, STCookiesValue>
|
|
159
|
+
|
|
115
160
|
/**
|
|
116
161
|
* #### GalbeConfig
|
|
117
162
|
* Instanciate a Galbe web server
|
|
@@ -129,6 +174,42 @@ export type STQuery = Record<string, STQueryValue>
|
|
|
129
174
|
* export default new Galbe(config)
|
|
130
175
|
* ```
|
|
131
176
|
*/
|
|
177
|
+
/**
|
|
178
|
+
* #### OpenAPIConfig
|
|
179
|
+
* Customize the top-level document fields (`info`, `servers`) of the OpenAPI
|
|
180
|
+
* specification produced by `OpenAPISerializer` (`galbe/extras`).
|
|
181
|
+
*
|
|
182
|
+
* Explicit values set here take precedence over the package.json inference
|
|
183
|
+
* applied by `galbe generate spec`, which itself takes precedence over the
|
|
184
|
+
* built-in defaults (`title: 'Galbe app'`, `version: '0.1.0'`).
|
|
185
|
+
*/
|
|
186
|
+
export type OpenAPIConfig = {
|
|
187
|
+
/** Overrides for the spec's `info` object (`title`, `version`, `description`, `contact`, `license`, `termsOfService`). Defaults to `{ title: 'Galbe app', version: '0.1.0' }`. */
|
|
188
|
+
info?: Partial<OpenAPIV3.InfoObject>
|
|
189
|
+
/** The spec's `servers` list. Unset by default. */
|
|
190
|
+
servers?: OpenAPIV3.ServerObject[]
|
|
191
|
+
/**
|
|
192
|
+
* The spec's `components.securitySchemes`. Route-level `@security <name>` tags
|
|
193
|
+
* name a scheme; this is where the scheme itself is defined. A scheme declared
|
|
194
|
+
* here always wins over the `bearerAuth` the serializer infers from an
|
|
195
|
+
* `Authorization: Bearer` header.
|
|
196
|
+
*/
|
|
197
|
+
securitySchemes?: Record<string, OpenAPIV3.SecuritySchemeObject | OpenAPIV3.ReferenceObject>
|
|
198
|
+
/** The document's `externalDocs`. Route-level docs come from the `@externalDocs` tag instead. */
|
|
199
|
+
externalDocs?: OpenAPIV3.ExternalDocumentationObject
|
|
200
|
+
/**
|
|
201
|
+
* The document's `tags` list — the descriptions behind the names operations
|
|
202
|
+
* use. Operations name their tags through `@tags`; this is where a tag is
|
|
203
|
+
* described.
|
|
204
|
+
*/
|
|
205
|
+
tags?: OpenAPIV3.TagObject[]
|
|
206
|
+
/**
|
|
207
|
+
* The document-level `security` requirement, applied to every operation that
|
|
208
|
+
* does not declare its own through `@security`.
|
|
209
|
+
*/
|
|
210
|
+
security?: OpenAPIV3.SecurityRequirementObject[]
|
|
211
|
+
}
|
|
212
|
+
|
|
132
213
|
export type GalbeConfig = {
|
|
133
214
|
/** The port number that the server will be listening on. */
|
|
134
215
|
port?: number
|
|
@@ -140,17 +221,39 @@ export type GalbeConfig = {
|
|
|
140
221
|
basePath?: string
|
|
141
222
|
/** Enable or disable TLS support. */
|
|
142
223
|
tls?: TLSOptions
|
|
224
|
+
/**
|
|
225
|
+
* How many hops in front of the app are yours, so `ctx.clientAddress` can be resolved from
|
|
226
|
+
* `X-Forwarded-For`: a hop count, or the addresses/CIDR ranges your proxies connect from.
|
|
227
|
+
* Default `false` — the header is ignored entirely and the client is the socket peer. Set it
|
|
228
|
+
* to match the real topology: too high is a spoofing hole, too low buckets every client together.
|
|
229
|
+
*/
|
|
230
|
+
trustProxy?: false | number | string[]
|
|
143
231
|
/** Extra options passed through to `Bun.serve` (e.g. `maxRequestBodySize`, `idleTimeout`). `port`, `fetch` and `error` are ignored, and the dedicated `hostname`, `reusePort` and `tls` config keys take precedence. */
|
|
144
232
|
server?: Partial<Omit<Parameters<typeof Bun.serve>[0], 'port' | 'fetch' | 'error'>> | TLSOptions
|
|
145
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
233
|
+
/**
|
|
234
|
+
* Route files picked up by the Automatic Route Analyzer: a glob pattern (or list of), `false` to
|
|
235
|
+
* disable the analyzer, or an object form to also control directory groups. By default a route
|
|
236
|
+
* file's directory relative to its glob's static base becomes its path prefix
|
|
237
|
+
* (`src/api/users.route.ts` → `/api`); set `dirPrefix: false` to opt out.
|
|
238
|
+
*/
|
|
239
|
+
routes?: boolean | string | string[] | { pattern?: string | string[]; dirPrefix?: boolean }
|
|
240
|
+
/**
|
|
241
|
+
* Middleware files discovered by the Automatic Route Analyzer (default `src/**/*.middleware.{js,ts}`).
|
|
242
|
+
* Files default-export `Hook | Hook[] | MiddlewareDef`, scoped to their directory subtree. `false`
|
|
243
|
+
* disables middleware discovery only; `routes: false` disables the whole analyzer.
|
|
244
|
+
*/
|
|
245
|
+
middleware?: boolean | string | string[]
|
|
246
|
+
router?: { cacheEnabled: boolean; cacheLimit?: number; warn?: (message: string) => void }
|
|
148
247
|
/** A property that can be used by plugins to add plugin's specific configuration. */
|
|
149
248
|
plugin?: Record<string, any>
|
|
150
249
|
/** Enable or disable the request schema validation.*/
|
|
151
250
|
requestValidator?: { enabled: boolean }
|
|
152
251
|
/** Enable or disable the response schema validation.*/
|
|
153
252
|
responseValidator?: { enabled: boolean }
|
|
253
|
+
/** Maximum request body size in bytes; larger bodies are rejected with a 413 error. Can be overridden per route with the schema's `bodyLimit`. Unset by default: only Bun's `maxRequestBodySize` (128 MB, see `server`) applies. */
|
|
254
|
+
bodyLimit?: number
|
|
255
|
+
/** Customize the document-level blocks (`info`, `servers`, `tags`, `security`, `externalDocs`, `securitySchemes`) of the OpenAPI spec generated by `OpenAPISerializer` (`galbe/extras`). */
|
|
256
|
+
openapi?: OpenAPIConfig
|
|
154
257
|
}
|
|
155
258
|
/**
|
|
156
259
|
* #### Schema
|
|
@@ -179,21 +282,38 @@ export type RequestSchema<
|
|
|
179
282
|
Q extends STQuery = STQuery,
|
|
180
283
|
B extends STBody = STBody,
|
|
181
284
|
R extends Partial<STResponse> = STResponse,
|
|
285
|
+
C extends STCookies = STCookies,
|
|
182
286
|
> = {
|
|
183
287
|
headers?: H
|
|
184
288
|
params?: P
|
|
185
289
|
query?: Q
|
|
290
|
+
/**
|
|
291
|
+
* Declares the request cookies. Each one is parsed out of the `Cookie` header
|
|
292
|
+
* and validated like a query parameter, so `ctx.cookies` comes back typed and
|
|
293
|
+
* coerced. Undeclared cookies are still present, as strings.
|
|
294
|
+
*/
|
|
295
|
+
cookies?: C
|
|
186
296
|
body?: B
|
|
187
297
|
response?: R
|
|
298
|
+
/** Maximum request body size in bytes for this route; overrides the global `bodyLimit` config. Larger bodies are rejected with a 413 error. */
|
|
299
|
+
bodyLimit?: number
|
|
188
300
|
}
|
|
189
301
|
|
|
302
|
+
/** The route's declared path params, with the `params?:` optionality peeled off. */
|
|
303
|
+
type STParamsOf<S extends RequestSchema> = Exclude<S['params'], undefined>
|
|
304
|
+
/** What `params` infers to, before the not-declared keys are dropped. */
|
|
305
|
+
type StaticParams<S extends RequestSchema> = Static<STObject<STParamsOf<S>>>
|
|
306
|
+
/**
|
|
307
|
+
* The declared params, minus the ones the schema itself marks optional
|
|
308
|
+
* (`params: { id?: $T.integer() }`) — those fall back to `string` in the
|
|
309
|
+
* context, like an undeclared param. `keyof StaticParams<S>` is `keyof
|
|
310
|
+
* STParamsOf<S>` by construction, so the `K extends keyof` guard on the value
|
|
311
|
+
* only exists to keep the indexed access provable while `S` is still generic.
|
|
312
|
+
*/
|
|
190
313
|
type OmitNotDefined<S extends RequestSchema> = {
|
|
191
|
-
[
|
|
192
|
-
|
|
193
|
-
>[K]
|
|
194
|
-
? K
|
|
195
|
-
: //@ts-ignore
|
|
196
|
-
never]: Static<STObject<Exclude<S['params'], undefined>>>[K]
|
|
314
|
+
[
|
|
315
|
+
K in keyof STParamsOf<S> as STParamsOf<S>[K] extends Required<STParamsOf<S>>[K] ? K : never
|
|
316
|
+
]: K extends keyof StaticParams<S> ? StaticParams<S>[K] : never
|
|
197
317
|
}
|
|
198
318
|
type StaticBody<T extends STSchema> = T extends STOptional<STSchema> ? Static<T> | null : Static<T>
|
|
199
319
|
export type ContextSet = {
|
|
@@ -204,67 +324,66 @@ export type ContextSet = {
|
|
|
204
324
|
status?: number
|
|
205
325
|
cookie: (name: string, value: string, opt?: CookieOptions) => void
|
|
206
326
|
}
|
|
327
|
+
/** Methods whose request carries no body: `contentType` and `body` collapse regardless of the schema. */
|
|
328
|
+
type EmptyBodyMethod = 'get' | 'options' | 'head'
|
|
329
|
+
/**
|
|
330
|
+
* `Fallback` when `T` is `any`, `T` otherwise — the `0 extends 1 & T` trick,
|
|
331
|
+
* which only ever holds for `any`. A route registered without a body schema
|
|
332
|
+
* leaves `B` at its `any` default, and `keyof any` is `string | number |
|
|
333
|
+
* symbol`: mapping over it would type `ctx.contentType` as `string` instead of
|
|
334
|
+
* a media type. Falling back to the `STBody` constraint types such a route
|
|
335
|
+
* exactly like the unparameterized {@link Context}, which is also what keeps a
|
|
336
|
+
* shared `(ctx: Context) => …` handler assignable to every route.
|
|
337
|
+
*/
|
|
338
|
+
type IfAny<T, Fallback> = 0 extends 1 & T ? Fallback : T
|
|
339
|
+
/** The declared body's media-type map. `never` when the route declares no body, or declares `$T.null()`. */
|
|
340
|
+
type STBodyOf<S extends RequestSchema> = Exclude<IfAny<S['body'], STBody>, undefined | STNull>
|
|
341
|
+
/**
|
|
342
|
+
* `contentType` and `body` are the only two context fields the request's media
|
|
343
|
+
* type reaches, so they are the only ones derived per media type: one member
|
|
344
|
+
* per key of the body map, which is what makes `ctx.contentType` a discriminant
|
|
345
|
+
* for `ctx.body`. Everything else lives in the single object literal below.
|
|
346
|
+
*/
|
|
347
|
+
type ContextBody<M extends Method, S extends RequestSchema> = [STBodyOf<S>] extends [never]
|
|
348
|
+
? { contentType: undefined; body: null }
|
|
349
|
+
: {
|
|
350
|
+
[K in keyof STBodyOf<S>]: {
|
|
351
|
+
contentType: M extends EmptyBodyMethod ? undefined : K
|
|
352
|
+
body: M extends EmptyBodyMethod ? null : StaticBody<Extract<Exclude<STBodyOf<S>[K], undefined>, STSchema>>
|
|
353
|
+
}
|
|
354
|
+
}[keyof STBodyOf<S>]
|
|
355
|
+
/**
|
|
356
|
+
* The context shape, written once. `B` is a naked type parameter so the
|
|
357
|
+
* conditional distributes over {@link ContextBody}'s union — one context per
|
|
358
|
+
* media type — and resolves to a bare object literal, which is what keeps
|
|
359
|
+
* `ctx` hovering as its expanded shape rather than as an alias reference.
|
|
360
|
+
*/
|
|
361
|
+
type ContextOf<Path extends string, S extends RequestSchema, B> = B extends {
|
|
362
|
+
contentType: infer CT
|
|
363
|
+
body: infer Body
|
|
364
|
+
}
|
|
365
|
+
? {
|
|
366
|
+
headers: Static<STObject<Exclude<S['headers'], undefined>>>
|
|
367
|
+
params: {
|
|
368
|
+
[P in ExtractParams<Path>]: P extends keyof OmitNotDefined<S> ? OmitNotDefined<S>[P] : string
|
|
369
|
+
}
|
|
370
|
+
query: Static<STObject<Exclude<S['query'], undefined>>>
|
|
371
|
+
contentType: CT
|
|
372
|
+
body: Body
|
|
373
|
+
request: Request
|
|
374
|
+
remoteAddress: SocketAddress | null
|
|
375
|
+
clientAddress: string | null
|
|
376
|
+
route?: Route
|
|
377
|
+
state: Record<string, any>
|
|
378
|
+
set: ContextSet
|
|
379
|
+
cookies: Static<STObject<Exclude<S['cookies'], undefined>>>
|
|
380
|
+
}
|
|
381
|
+
: never
|
|
207
382
|
export type Context<
|
|
208
383
|
M extends Method = Method,
|
|
209
384
|
Path extends string = string,
|
|
210
385
|
S extends RequestSchema = RequestSchema,
|
|
211
|
-
> =
|
|
212
|
-
? {
|
|
213
|
-
[K in keyof Exclude<S['body'], undefined | STNull>]: {
|
|
214
|
-
headers: Static<STObject<Exclude<S['headers'], undefined>>>
|
|
215
|
-
params: {
|
|
216
|
-
[P in ExtractParams<Path>]: P extends keyof OmitNotDefined<S> ? OmitNotDefined<S>[P] : string
|
|
217
|
-
}
|
|
218
|
-
query: Static<STObject<Exclude<S['query'], undefined>>>
|
|
219
|
-
contentType: M extends 'get' | 'options' | 'head' ? undefined : K
|
|
220
|
-
body: M extends 'get' | 'options' | 'head'
|
|
221
|
-
? null
|
|
222
|
-
: Exclude<S['body'], undefined> extends STNull
|
|
223
|
-
? null
|
|
224
|
-
: StaticBody<Extract<Exclude<Exclude<S['body'], undefined | STNull>[K], undefined>, STSchema>>
|
|
225
|
-
request: Request
|
|
226
|
-
remoteAddress: SocketAddress | null
|
|
227
|
-
route?: Route
|
|
228
|
-
state: Record<string, any>
|
|
229
|
-
set: ContextSet
|
|
230
|
-
cookies: Record<string, string>
|
|
231
|
-
}
|
|
232
|
-
}[keyof Exclude<S['body'], undefined | STNull>]
|
|
233
|
-
: [Exclude<S['body'], undefined | STNull>] extends [never]
|
|
234
|
-
? {
|
|
235
|
-
headers: Static<STObject<Exclude<S['headers'], undefined>>>
|
|
236
|
-
params: {
|
|
237
|
-
[P in ExtractParams<Path>]: P extends keyof OmitNotDefined<S> ? OmitNotDefined<S>[P] : string
|
|
238
|
-
}
|
|
239
|
-
query: Static<STObject<Exclude<S['query'], undefined>>>
|
|
240
|
-
contentType: undefined
|
|
241
|
-
body: null
|
|
242
|
-
request: Request
|
|
243
|
-
remoteAddress: SocketAddress | null
|
|
244
|
-
route?: Route
|
|
245
|
-
state: Record<string, any>
|
|
246
|
-
set: ContextSet
|
|
247
|
-
cookies: Record<string, string>
|
|
248
|
-
}
|
|
249
|
-
: {
|
|
250
|
-
[K in keyof Exclude<S['body'], undefined | STNull>]: {
|
|
251
|
-
headers: Static<STObject<Exclude<S['headers'], undefined>>>
|
|
252
|
-
params: {
|
|
253
|
-
[P in ExtractParams<Path>]: P extends keyof OmitNotDefined<S> ? OmitNotDefined<S>[P] : string
|
|
254
|
-
}
|
|
255
|
-
query: Static<STObject<Exclude<S['query'], undefined>>>
|
|
256
|
-
contentType: M extends 'get' | 'options' | 'head' ? undefined : K
|
|
257
|
-
body: M extends 'get' | 'options' | 'head'
|
|
258
|
-
? null
|
|
259
|
-
: StaticBody<Extract<Exclude<Exclude<S['body'], undefined | STNull>[K], undefined>, STSchema>>
|
|
260
|
-
request: Request
|
|
261
|
-
remoteAddress: SocketAddress | null
|
|
262
|
-
route?: Route
|
|
263
|
-
state: Record<string, any>
|
|
264
|
-
set: ContextSet
|
|
265
|
-
cookies: Record<string, string>
|
|
266
|
-
}
|
|
267
|
-
}[keyof Exclude<S['body'], undefined | STNull>]
|
|
386
|
+
> = ContextOf<Path, S, ContextBody<M, S>>
|
|
268
387
|
export type Next = () => void | Promise<any>
|
|
269
388
|
export type Hook<M extends Method = Method, Path extends string = string, S extends RequestSchema = RequestSchema> = (
|
|
270
389
|
ctx: Context<M, Path, S>,
|
|
@@ -275,55 +394,187 @@ export type Handler<
|
|
|
275
394
|
Path extends string = string,
|
|
276
395
|
S extends RequestSchema = RequestSchema,
|
|
277
396
|
> = (ctx: Context<M, Path, S>) => any
|
|
278
|
-
|
|
397
|
+
/** Request contract a middleware imposes, merged into the schema of every route it matches. */
|
|
398
|
+
export type MiddlewareSchema = Pick<RequestSchema, 'headers' | 'query' | 'params'>
|
|
399
|
+
type IsAny<T> = 0 extends 1 & T ? true : false
|
|
400
|
+
type FragHeaders<F extends MiddlewareSchema> = F['headers'] extends STHeaders ? F['headers'] : {}
|
|
401
|
+
type FragQuery<F extends MiddlewareSchema> = F['query'] extends STQuery ? F['query'] : {}
|
|
402
|
+
/**
|
|
403
|
+
* Fragment keys merged under route-declared ones — route wins, as at runtime.
|
|
404
|
+
* A route that declares no schema of its own keeps today's permissive `any`
|
|
405
|
+
* when there is no fragment, and gets exactly the fragment's keys when there is.
|
|
406
|
+
*/
|
|
407
|
+
type MergeFragment<Frag, Declared> = [keyof Frag] extends [never]
|
|
408
|
+
? Declared
|
|
409
|
+
: IsAny<Declared> extends true
|
|
410
|
+
? Frag
|
|
411
|
+
: Omit<Frag, keyof Declared> & Declared
|
|
412
|
+
/** The request schema a fragment implies for the middleware's own hooks. */
|
|
413
|
+
type FragmentRequest<F extends MiddlewareSchema> = RequestSchema<Method, string, FragHeaders<F>, {}, FragQuery<F>>
|
|
414
|
+
/**
|
|
415
|
+
* The context a {@link PreParseHook} receives. It deliberately lacks `body`,
|
|
416
|
+
* `params` and the parsed `query`: none of them exist yet at that point. Raw
|
|
417
|
+
* headers and search params remain reachable through `ctx.request`.
|
|
418
|
+
*/
|
|
419
|
+
export type PreParseContext = Pick<
|
|
420
|
+
Context,
|
|
421
|
+
'request' | 'set' | 'state' | 'route' | 'cookies' | 'remoteAddress' | 'clientAddress'
|
|
422
|
+
>
|
|
423
|
+
/**
|
|
424
|
+
* A `beforeParse` hook: runs once the route is known but before the body is
|
|
425
|
+
* read or the request validated. No `next()` — hooks run sequentially and
|
|
426
|
+
* short-circuit by returning a `Response`, like a plugin's `onRoute`.
|
|
427
|
+
*/
|
|
428
|
+
export type PreParseHook = (ctx: PreParseContext) => MaybePromise<Response | void>
|
|
429
|
+
/**
|
|
430
|
+
* A route-scoped response hook — the `afterHandle` slot. It runs once the
|
|
431
|
+
* request has a `Response`, whatever it ended on, and transforms it: return a
|
|
432
|
+
* `Response` to replace it, return nothing to keep it. Not an onion — by the
|
|
433
|
+
* time a `Response` exists the hook chain has unwound, so there is nothing left
|
|
434
|
+
* to wrap. `error` is what the request ended on, and is `undefined` on success.
|
|
435
|
+
*/
|
|
436
|
+
export type ResponseHook = (response: Response, ctx: Context, error?: unknown) => MaybePromise<Response | void>
|
|
437
|
+
/**
|
|
438
|
+
* #### MiddlewareDef
|
|
439
|
+
* Middleware as a value: the hooks to run, plus the request contract they
|
|
440
|
+
* impose. Accepted everywhere a hook is — `galbe.middleware`, `group.middleware`
|
|
441
|
+
* and middleware files — so a packaged middleware is one exportable thing.
|
|
442
|
+
*
|
|
443
|
+
* The `schema` fragment types the def's own `hooks`: declaring a header means
|
|
444
|
+
* reading it back typed, with no annotation. Wrap the def in `middleware()` to
|
|
445
|
+
* get that inference — a bare object literal has nothing to contextually type
|
|
446
|
+
* its handlers against.
|
|
447
|
+
*
|
|
448
|
+
* ---
|
|
449
|
+
* @example
|
|
450
|
+
* ```typescript
|
|
451
|
+
* galbe.middleware('/api/*', middleware({
|
|
452
|
+
* schema: { headers: { authorization: $T.string() } },
|
|
453
|
+
* hooks: ctx => { ctx.headers.authorization }, // string
|
|
454
|
+
* security: 'bearerAuth',
|
|
455
|
+
* securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer' } }
|
|
456
|
+
* }))
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
459
|
+
export type MiddlewareDef<F extends MiddlewareSchema = any> = {
|
|
460
|
+
/**
|
|
461
|
+
* Hooks run on every matched route after routing and before the body is
|
|
462
|
+
* parsed — the point where auth or rate limiting can reject a request
|
|
463
|
+
* without reading it. See {@link PreParseHook}.
|
|
464
|
+
*/
|
|
465
|
+
beforeParse?: MaybeArray<PreParseHook>
|
|
466
|
+
/** Hooks composed into the chain of every matched route, ahead of the route's own hooks. */
|
|
467
|
+
hooks?: MaybeArray<Hook<Method, string, FragmentRequest<F>>>
|
|
468
|
+
/**
|
|
469
|
+
* Hooks run on every matched route once its response is parsed — on success
|
|
470
|
+
* and on failure alike, the error response included — ahead of the plugins'
|
|
471
|
+
* `afterHandle`. They transform the `Response`. See {@link ResponseHook}.
|
|
472
|
+
*/
|
|
473
|
+
afterHandle?: MaybeArray<ResponseHook>
|
|
474
|
+
/**
|
|
475
|
+
* Headers, query and params the hooks require, merged into matched routes.
|
|
476
|
+
* Route-declared keys win. `params` is merged and validated at runtime but
|
|
477
|
+
* cannot type the hooks: a middleware pattern is not a typed route path.
|
|
478
|
+
*/
|
|
479
|
+
schema?: F
|
|
480
|
+
/**
|
|
481
|
+
* OpenAPI security scheme name(s) enforced by the hooks, applied to every
|
|
482
|
+
* matched operation exactly like a middleware file's `@security` header.
|
|
483
|
+
* Scopes follow the name, space-separated (`'oauth2 read write'`); `'none'`
|
|
484
|
+
* documents the scope as public. Metadata only — never affects runtime.
|
|
485
|
+
*/
|
|
486
|
+
security?: string | string[]
|
|
487
|
+
/**
|
|
488
|
+
* Definitions for the schemes {@link MiddlewareDef.security} names, merged
|
|
489
|
+
* into `components.securitySchemes`. A packaged middleware that is not bearer
|
|
490
|
+
* auth needs this: `apiKey` and `basic` cannot be expressed by a name alone.
|
|
491
|
+
* Same shape as `config.openapi.securitySchemes`, which wins on conflict.
|
|
492
|
+
*/
|
|
493
|
+
securitySchemes?: Record<string, OpenAPIV3.SecuritySchemeObject | OpenAPIV3.ReferenceObject>
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Prefix middleware entry registered via `galbe.middleware`. Patterns match
|
|
497
|
+
* registered route paths (not request URLs) and are resolved at registration:
|
|
498
|
+
* matched hooks are composed into the route's hook chain and the schema
|
|
499
|
+
* fragment is merged into the route's schema.
|
|
500
|
+
*/
|
|
501
|
+
export type GalbeMiddleware = {
|
|
502
|
+
/** the pattern as registered, e.g. `/api/*` */
|
|
503
|
+
pattern: string
|
|
504
|
+
/** pattern split into segments, precomputed at registration */
|
|
505
|
+
segments: string[]
|
|
506
|
+
beforeParse: PreParseHook[]
|
|
507
|
+
hooks: Hook[]
|
|
508
|
+
afterHandle: ResponseHook[]
|
|
509
|
+
schema?: MiddlewareSchema
|
|
510
|
+
security?: string | string[]
|
|
511
|
+
securitySchemes?: Record<string, OpenAPIV3.SecuritySchemeObject | OpenAPIV3.ReferenceObject>
|
|
512
|
+
}
|
|
513
|
+
// Prefix is prepended to Path at the type level (route groups): context params,
|
|
514
|
+
// schemas and the returned Route are typed against the full, joined path. F is
|
|
515
|
+
// the schema fragment of the group's middleware def, merged under the route's
|
|
516
|
+
// own declarations so handlers read fragment-declared entries typed.
|
|
517
|
+
export type Endpoint<M extends Method, Prefix extends string = '', F extends MiddlewareSchema = {}> = {
|
|
279
518
|
<
|
|
280
519
|
Path extends string,
|
|
281
|
-
P extends Partial<STParams
|
|
520
|
+
P extends Partial<STParams<`${Prefix}${Path}`>>,
|
|
282
521
|
H extends STHeaders = any,
|
|
283
522
|
Q extends STQuery = any,
|
|
284
523
|
B extends STBody = any,
|
|
285
524
|
R extends STResponse = STResponse,
|
|
525
|
+
C extends STCookies = any,
|
|
526
|
+
MH extends STHeaders = MergeFragment<FragHeaders<F>, H>,
|
|
527
|
+
MQ extends STQuery = MergeFragment<FragQuery<F>, Q>,
|
|
286
528
|
>(
|
|
287
529
|
path: Path,
|
|
288
|
-
schema: RequestSchema<M, Path
|
|
289
|
-
hooks: Hook<M, Path
|
|
290
|
-
handler: Handler<M, Path
|
|
291
|
-
): Route<M, Path
|
|
530
|
+
schema: RequestSchema<M, `${Prefix}${Path}`, H, P, Q, B, R, C>,
|
|
531
|
+
hooks: Hook<M, `${Prefix}${Path}`, RequestSchema<M, `${Prefix}${Path}`, MH, P, MQ, B, R, C>>[],
|
|
532
|
+
handler: Handler<M, `${Prefix}${Path}`, RequestSchema<M, `${Prefix}${Path}`, MH, P, MQ, B, R, C>>
|
|
533
|
+
): Route<M, `${Prefix}${Path}`, P, MH, MQ, B, R, C>
|
|
292
534
|
<
|
|
293
535
|
Path extends string,
|
|
294
|
-
P extends Partial<STParams
|
|
536
|
+
P extends Partial<STParams<`${Prefix}${Path}`>>,
|
|
295
537
|
H extends STHeaders = any,
|
|
296
538
|
Q extends STQuery = any,
|
|
297
539
|
B extends STBody = any,
|
|
298
540
|
R extends STResponse = STResponse,
|
|
541
|
+
C extends STCookies = any,
|
|
542
|
+
MH extends STHeaders = MergeFragment<FragHeaders<F>, H>,
|
|
543
|
+
MQ extends STQuery = MergeFragment<FragQuery<F>, Q>,
|
|
299
544
|
>(
|
|
300
545
|
path: Path,
|
|
301
|
-
schema: RequestSchema<M, Path
|
|
302
|
-
handler: Handler<M, Path
|
|
303
|
-
): Route<M, Path
|
|
546
|
+
schema: RequestSchema<M, `${Prefix}${Path}`, H, P, Q, B, R, C>,
|
|
547
|
+
handler: Handler<M, `${Prefix}${Path}`, RequestSchema<M, `${Prefix}${Path}`, MH, P, MQ, B, R, C>>
|
|
548
|
+
): Route<M, `${Prefix}${Path}`, P, MH, MQ, B, R, C>
|
|
304
549
|
<
|
|
305
550
|
Path extends string,
|
|
306
|
-
P extends Partial<STParams
|
|
551
|
+
P extends Partial<STParams<`${Prefix}${Path}`>>,
|
|
307
552
|
H extends STHeaders = any,
|
|
308
553
|
Q extends STQuery = any,
|
|
309
554
|
B extends STBody = any,
|
|
310
555
|
R extends STResponse = STResponse,
|
|
556
|
+
C extends STCookies = any,
|
|
557
|
+
MH extends STHeaders = MergeFragment<FragHeaders<F>, H>,
|
|
558
|
+
MQ extends STQuery = MergeFragment<FragQuery<F>, Q>,
|
|
311
559
|
>(
|
|
312
560
|
path: Path,
|
|
313
|
-
hooks: Hook<M, Path
|
|
314
|
-
handler: Handler<M, Path
|
|
315
|
-
): Route<M, Path
|
|
561
|
+
hooks: Hook<M, `${Prefix}${Path}`, RequestSchema<M, `${Prefix}${Path}`, MH, P, MQ, B, R, C>>[],
|
|
562
|
+
handler: Handler<M, `${Prefix}${Path}`, RequestSchema<M, `${Prefix}${Path}`, MH, P, MQ, B, R, C>>
|
|
563
|
+
): Route<M, `${Prefix}${Path}`, P, MH, MQ, B, R, C>
|
|
316
564
|
<
|
|
317
565
|
Path extends string,
|
|
318
|
-
P extends Partial<STParams
|
|
566
|
+
P extends Partial<STParams<`${Prefix}${Path}`>>,
|
|
319
567
|
H extends STHeaders = any,
|
|
320
568
|
Q extends STQuery = any,
|
|
321
569
|
B extends STBody = any,
|
|
322
570
|
R extends STResponse = STResponse,
|
|
571
|
+
C extends STCookies = any,
|
|
572
|
+
MH extends STHeaders = MergeFragment<FragHeaders<F>, H>,
|
|
573
|
+
MQ extends STQuery = MergeFragment<FragQuery<F>, Q>,
|
|
323
574
|
>(
|
|
324
575
|
path: Path,
|
|
325
|
-
handler: Handler<M, Path
|
|
326
|
-
): Route<M, Path
|
|
576
|
+
handler: Handler<M, `${Prefix}${Path}`, RequestSchema<M, `${Prefix}${Path}`, MH, P, MQ, B, R, C>>
|
|
577
|
+
): Route<M, `${Prefix}${Path}`, P, MH, MQ, B, R, C>
|
|
327
578
|
}
|
|
328
579
|
|
|
329
580
|
export type StaticEndpointOptions = {
|
|
@@ -333,7 +584,7 @@ export type StaticEndpoint<P extends string = string, T extends string = string>
|
|
|
333
584
|
path: P,
|
|
334
585
|
target: T,
|
|
335
586
|
options?: StaticEndpointOptions
|
|
336
|
-
) => Route<'get', P, {}, {}, {}, STBody, STResponse, T>
|
|
587
|
+
) => Route<'get', P, {}, {}, {}, STBody, STResponse, {}, T>
|
|
337
588
|
|
|
338
589
|
export class RequestError extends Error {
|
|
339
590
|
status: number
|
|
@@ -358,6 +609,8 @@ export type ErrorHandler = (error: any, context: Context) => any
|
|
|
358
609
|
export type RouteNode = {
|
|
359
610
|
routes: { [K in Method]?: Route }
|
|
360
611
|
param?: RouteNode
|
|
612
|
+
/** name of the first param registered on this node, e.g. 'id' for /user/:id */
|
|
613
|
+
paramName?: string
|
|
361
614
|
children?: Record<string, RouteNode>
|
|
362
615
|
}
|
|
363
616
|
|
|
@@ -369,15 +622,38 @@ export type Route<
|
|
|
369
622
|
Q extends STQuery = STQuery,
|
|
370
623
|
B extends STBody = STBody,
|
|
371
624
|
R extends STResponse = STResponse,
|
|
625
|
+
C extends STCookies = STCookies,
|
|
372
626
|
SP extends string = string,
|
|
373
627
|
SR extends string = string,
|
|
374
628
|
> = {
|
|
375
629
|
method: M
|
|
376
630
|
path: Path
|
|
377
|
-
schema: RequestSchema<M, Path, H, P, Q, B, R>
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
631
|
+
schema: RequestSchema<M, Path, H, P, Q, B, R, C>
|
|
632
|
+
hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R, C>>[]
|
|
633
|
+
handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R, C>>
|
|
634
|
+
/**
|
|
635
|
+
* Hook/handler chain composed at registration — and recomposed if a later
|
|
636
|
+
* `middleware()` call matches the route — never per request. Runs matched
|
|
637
|
+
* middleware, then the hooks, then the handler and resolves to the handler's
|
|
638
|
+
* response — or a hook's short-circuit value.
|
|
639
|
+
*/
|
|
640
|
+
composed: (context: Context<M, Path, RequestSchema<M, Path, H, P, Q, B, R, C>>) => Promise<any>
|
|
641
|
+
/**
|
|
642
|
+
* Matched middleware `beforeParse` hooks, composed at registration like
|
|
643
|
+
* {@link Route.composed}. Left `undefined` when no matched middleware fills
|
|
644
|
+
* the slot, so the request path skips the stage with a single check. Runs
|
|
645
|
+
* after the plugins' `onRoute` and before the body is read; a returned
|
|
646
|
+
* `Response` short-circuits the request.
|
|
647
|
+
*/
|
|
648
|
+
composedPre?: (context: PreParseContext) => Promise<Response | void>
|
|
649
|
+
/**
|
|
650
|
+
* Matched middleware `afterHandle` hooks, composed at registration like
|
|
651
|
+
* {@link Route.composedPre}, and left `undefined` when no matched middleware
|
|
652
|
+
* fills the slot. Runs on the parsed `Response` — the error response
|
|
653
|
+
* included — ahead of the plugins' `afterHandle`; a returned `Response`
|
|
654
|
+
* replaces it.
|
|
655
|
+
*/
|
|
656
|
+
composedPost?: (response: Response, context: Context, error?: unknown) => Promise<Response>
|
|
381
657
|
static?: { path: SP; root: SR }
|
|
382
658
|
}
|
|
383
659
|
|