@znight/http 0.1.2 → 0.1.4
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 +154 -35
- package/dist/compiler.d.mts +77 -2
- package/dist/compiler.mjs +259 -101
- package/dist/compiler.mjs.map +1 -1
- package/dist/express.mjs +3 -2
- package/dist/express.mjs.map +1 -1
- package/dist/fastify.mjs +200 -23
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.mjs +174 -43
- package/dist/hono.mjs.map +1 -1
- package/dist/index.d.mts +36 -4
- package/dist/index.mjs +1005 -257
- package/dist/index.mjs.map +1 -1
- package/dist/{node-Clm1Ia7T.mjs → node-DnYv_AMI.mjs} +4 -3
- package/dist/{node-Clm1Ia7T.mjs.map → node-DnYv_AMI.mjs.map} +1 -1
- package/dist/{node-io-CVnqpVNS.mjs → node-io-BkdRSQeg.mjs} +99 -60
- package/dist/{node-io-CVnqpVNS.mjs.map → node-io-BkdRSQeg.mjs.map} +1 -1
- package/dist/node.mjs +1 -1
- package/dist/utils-DIWdzVht.mjs +46 -0
- package/dist/utils-DIWdzVht.mjs.map +1 -0
- package/package.json +4 -4
- package/src/adapters/express.ts +4 -1
- package/src/adapters/fastify.ts +265 -25
- package/src/adapters/hono.ts +225 -58
- package/src/adapters/node-io.ts +128 -58
- package/src/adapters/node.ts +4 -1
- package/src/body.ts +31 -2
- package/src/coerce.ts +31 -0
- package/src/compiler/analyze.ts +3 -3
- package/src/compiler/generate.ts +127 -22
- package/src/compiler/index.ts +13 -0
- package/src/compiler/route-decorators.ts +45 -2
- package/src/compiler/route-groups.ts +216 -0
- package/src/compiler/validate.ts +15 -126
- package/src/context.ts +148 -78
- package/src/decorators.ts +19 -3
- package/src/index.ts +2 -1
- package/src/pipeline.ts +936 -177
- package/src/plugin-routes.ts +19 -0
- package/src/plugin.ts +7 -3
- package/src/response.ts +6 -8
- package/src/serialize.ts +48 -21
- package/src/utils.ts +25 -2
- package/src/validate.ts +28 -12
- package/dist/utils-DCbu3a4w.mjs +0 -27
- package/dist/utils-DCbu3a4w.mjs.map +0 -1
package/src/context.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { HeaderValue } from './exceptions.ts'
|
|
|
6
6
|
import type { MultipartResult } from './multipart.ts'
|
|
7
7
|
import type { BodyPlan } from './uploads.ts'
|
|
8
8
|
|
|
9
|
-
import { checkContentEncoding, hasBody,
|
|
9
|
+
import { checkContentEncoding, hasBody, parseBodyBytes, parseMediaType } from './body.ts'
|
|
10
10
|
import { DEFAULT_COOKIE_SETTINGS, missingSecrets, serializeWith, verifySigned } from './cookie-settings.ts'
|
|
11
11
|
import { parseCookieHeader } from './cookies.ts'
|
|
12
12
|
import { UnsupportedMediaTypeException } from './exceptions.ts'
|
|
@@ -182,6 +182,47 @@ export interface HttpContext {
|
|
|
182
182
|
bodyStream(): ReadableStream<Uint8Array>
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/** What {@link RequestContext.of} needs to create a context. */
|
|
186
|
+
export interface RequestContextInit {
|
|
187
|
+
readonly requestId: RequestId
|
|
188
|
+
readonly adapter: string
|
|
189
|
+
/** `source.url` split (default: split here). */
|
|
190
|
+
readonly url?: RequestTarget
|
|
191
|
+
readonly route: HttpRouteInfo | undefined
|
|
192
|
+
readonly params: Readonly<Record<string, string>>
|
|
193
|
+
readonly body: BodyPlan
|
|
194
|
+
/** Client address and protocol (`trustProxy`); default: the adapter's `ip` and `encrypted`. */
|
|
195
|
+
readonly client?: RequestClient | undefined
|
|
196
|
+
/** Cookie defaults and signing key (plugin option `cookies`); default: secure defaults, no signing. */
|
|
197
|
+
readonly cookies?: CookieSettings
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The client of a request behind trusted proxies (`trustProxy`). */
|
|
201
|
+
export interface RequestClient {
|
|
202
|
+
readonly ip: string | undefined
|
|
203
|
+
readonly protocol: 'http' | 'https'
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The state of a request context that most requests never need (parsed query, `ctx.state`, body,
|
|
208
|
+
* request cookies, abort signal), created on first use: a context is created for every request.
|
|
209
|
+
*/
|
|
210
|
+
class ContextState {
|
|
211
|
+
query: QueryRecord | undefined = undefined
|
|
212
|
+
state: Map<string | symbol, unknown> | undefined = undefined
|
|
213
|
+
body: unknown = undefined
|
|
214
|
+
files: Readonly<Record<string, readonly File[]>> | undefined = undefined
|
|
215
|
+
raw: Promise<Uint8Array> | undefined = undefined
|
|
216
|
+
streamed = false
|
|
217
|
+
/** Stops the body stream opened for the request, unless it was read to its end or cancelled. */
|
|
218
|
+
stopBody: (() => void) | undefined = undefined
|
|
219
|
+
parsed: Promise<unknown> | undefined = undefined
|
|
220
|
+
cookies: Readonly<Record<string, string>> | undefined = undefined
|
|
221
|
+
signed: Map<string, string | undefined> | undefined = undefined
|
|
222
|
+
abortReason: RequestAbortedError | undefined = undefined
|
|
223
|
+
abortController: AbortController | undefined = undefined
|
|
224
|
+
}
|
|
225
|
+
|
|
185
226
|
/** Implementation of {@link HttpContext} used by the pipeline (internal). */
|
|
186
227
|
export class RequestContext implements HttpContext {
|
|
187
228
|
readonly requestId: RequestId
|
|
@@ -199,80 +240,93 @@ export class RequestContext implements HttpContext {
|
|
|
199
240
|
readonly #source: AdapterRequest
|
|
200
241
|
readonly #plan: BodyPlan
|
|
201
242
|
readonly #rawQuery: string
|
|
202
|
-
#query: QueryRecord | undefined
|
|
203
|
-
#state: Map<string | symbol, unknown> | undefined
|
|
204
|
-
#responseHeaders: Record<string, HeaderValue> | undefined
|
|
205
|
-
#body: unknown
|
|
206
|
-
#files: Readonly<Record<string, readonly File[]>> | undefined
|
|
207
|
-
#raw: Promise<Uint8Array> | undefined
|
|
208
|
-
#streamed = false
|
|
209
|
-
/** Stops the body stream opened for the request, unless it was read to its end or cancelled. */
|
|
210
|
-
#stopBody: (() => void) | undefined
|
|
211
|
-
#parsed: Promise<unknown> | undefined
|
|
212
243
|
readonly #cookieSettings: CookieSettings
|
|
213
|
-
#
|
|
214
|
-
#signed: Map<string, string | undefined> | undefined
|
|
244
|
+
#responseHeaders: Record<string, HeaderValue> | undefined = undefined
|
|
215
245
|
/** Cookies set on the response, by key (name, domain, path), with their `Set-Cookie` line. */
|
|
216
|
-
#jar: Map<string, { readonly cookie: ResponseCookie; readonly line: string }> | undefined
|
|
246
|
+
#jar: Map<string, { readonly cookie: ResponseCookie; readonly line: string }> | undefined = undefined
|
|
217
247
|
/** The response headers were handed to the adapter (cookies can no longer be set). */
|
|
218
248
|
#committed = false
|
|
219
249
|
/** The request is over: later aborts are ignored. */
|
|
220
250
|
#completed = false
|
|
221
|
-
#
|
|
222
|
-
#abortController: AbortController | undefined
|
|
251
|
+
#more: ContextState | undefined = undefined
|
|
223
252
|
|
|
253
|
+
/** `path` and `query`: `source.url` split; `client`: see {@link RequestContextInit.client}. */
|
|
224
254
|
constructor(
|
|
225
255
|
source: AdapterRequest,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
readonly client?: { readonly ip: string | undefined; readonly protocol: 'http' | 'https' } | undefined
|
|
236
|
-
/** Cookie defaults and signing key (plugin option `cookies`); default: secure defaults, no signing. */
|
|
237
|
-
readonly cookies?: CookieSettings
|
|
238
|
-
},
|
|
256
|
+
requestId: RequestId,
|
|
257
|
+
adapter: string,
|
|
258
|
+
path: string,
|
|
259
|
+
query: string,
|
|
260
|
+
route: HttpRouteInfo | undefined,
|
|
261
|
+
params: Readonly<Record<string, string>>,
|
|
262
|
+
body: BodyPlan,
|
|
263
|
+
cookies: CookieSettings,
|
|
264
|
+
client: RequestClient | undefined,
|
|
239
265
|
) {
|
|
240
266
|
this.#source = source
|
|
241
|
-
this.#plan =
|
|
242
|
-
this.#cookieSettings =
|
|
243
|
-
const { path, query } = init.url ?? splitUrl(source.url)
|
|
267
|
+
this.#plan = body
|
|
268
|
+
this.#cookieSettings = cookies
|
|
244
269
|
this.#rawQuery = query
|
|
245
|
-
this.requestId =
|
|
270
|
+
this.requestId = requestId
|
|
246
271
|
this.method = source.method
|
|
247
272
|
this.url = source.url
|
|
248
273
|
this.path = path
|
|
249
|
-
this.params =
|
|
274
|
+
this.params = params
|
|
250
275
|
this.headers = source.headers
|
|
251
|
-
this.route =
|
|
276
|
+
this.route = route
|
|
252
277
|
this.request = source.raw
|
|
253
278
|
this.response = source.rawResponse
|
|
254
|
-
this.adapter =
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
279
|
+
this.adapter = adapter
|
|
280
|
+
if (client === undefined) {
|
|
281
|
+
this.ip = source.ip
|
|
282
|
+
this.protocol = source.encrypted === true ? 'https' : 'http'
|
|
283
|
+
} else {
|
|
284
|
+
this.ip = client.ip
|
|
285
|
+
this.protocol = client.protocol
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** A context from named settings (defaults: `source.url` split, secure cookie defaults, the adapter's client). */
|
|
290
|
+
static of(source: AdapterRequest, init: RequestContextInit): RequestContext {
|
|
291
|
+
const { path, query } = init.url ?? splitUrl(source.url)
|
|
292
|
+
return new RequestContext(
|
|
293
|
+
source,
|
|
294
|
+
init.requestId,
|
|
295
|
+
init.adapter,
|
|
296
|
+
path,
|
|
297
|
+
query,
|
|
298
|
+
init.route,
|
|
299
|
+
init.params,
|
|
300
|
+
init.body,
|
|
301
|
+
init.cookies ?? DEFAULT_COOKIE_SETTINGS,
|
|
302
|
+
init.client,
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** The state created on first use. */
|
|
307
|
+
get #extra(): ContextState {
|
|
308
|
+
this.#more ??= new ContextState()
|
|
309
|
+
return this.#more
|
|
258
310
|
}
|
|
259
311
|
|
|
260
312
|
get query(): Readonly<QueryRecord> {
|
|
261
|
-
|
|
262
|
-
|
|
313
|
+
const state = this.#extra
|
|
314
|
+
state.query ??= parseQuery(this.#rawQuery)
|
|
315
|
+
return state.query
|
|
263
316
|
}
|
|
264
317
|
|
|
265
318
|
get state(): Map<string | symbol, unknown> {
|
|
266
|
-
|
|
267
|
-
|
|
319
|
+
const state = this.#extra
|
|
320
|
+
state.state ??= new Map()
|
|
321
|
+
return state.state
|
|
268
322
|
}
|
|
269
323
|
|
|
270
324
|
get body(): unknown {
|
|
271
|
-
return this.#body
|
|
325
|
+
return this.#more?.body
|
|
272
326
|
}
|
|
273
327
|
|
|
274
328
|
get files(): Readonly<Record<string, readonly File[]>> {
|
|
275
|
-
return this.#files ?? NO_FILES
|
|
329
|
+
return this.#more?.files ?? NO_FILES
|
|
276
330
|
}
|
|
277
331
|
|
|
278
332
|
/** Maximum body size of the route, in bytes. */
|
|
@@ -307,17 +361,19 @@ export class RequestContext implements HttpContext {
|
|
|
307
361
|
}
|
|
308
362
|
|
|
309
363
|
get cookies(): Readonly<Record<string, string>> {
|
|
310
|
-
|
|
311
|
-
|
|
364
|
+
const state = this.#extra
|
|
365
|
+
state.cookies ??= parseCookieHeader(this.headers['cookie'])
|
|
366
|
+
return state.cookies
|
|
312
367
|
}
|
|
313
368
|
|
|
314
369
|
signedCookie(name: string): string | undefined {
|
|
315
370
|
const signer = this.#cookieSettings.signer
|
|
316
371
|
if (signer === undefined) throw missingSecrets(`Cannot read the signed cookie ${JSON.stringify(String(name))}`)
|
|
317
|
-
|
|
318
|
-
|
|
372
|
+
const state = this.#extra
|
|
373
|
+
state.signed ??= new Map()
|
|
374
|
+
if (state.signed.has(name)) return state.signed.get(name)
|
|
319
375
|
const value = verifySigned(signer, name, this.cookies[name])
|
|
320
|
-
|
|
376
|
+
state.signed.set(name, value)
|
|
321
377
|
return value
|
|
322
378
|
}
|
|
323
379
|
|
|
@@ -333,6 +389,11 @@ export class RequestContext implements HttpContext {
|
|
|
333
389
|
return this.#jar === undefined ? NO_COOKIES : [...this.#jar.values()].map(entry => entry.cookie)
|
|
334
390
|
}
|
|
335
391
|
|
|
392
|
+
/** Whether {@link setCookie} / {@link clearCookie} were called (internal). */
|
|
393
|
+
get hasCookies(): boolean {
|
|
394
|
+
return this.#jar !== undefined
|
|
395
|
+
}
|
|
396
|
+
|
|
336
397
|
/** The `Set-Cookie` lines of {@link setCookie} / {@link clearCookie}, with their keys, in order (internal). */
|
|
337
398
|
get cookieLines(): readonly SerializedCookie[] {
|
|
338
399
|
return this.#jar === undefined ? NO_LINES : [...this.#jar].map(([key, entry]) => ({ key, line: entry.line }))
|
|
@@ -359,18 +420,19 @@ export class RequestContext implements HttpContext {
|
|
|
359
420
|
}
|
|
360
421
|
|
|
361
422
|
get signal(): AbortSignal {
|
|
362
|
-
|
|
423
|
+
const state = this.#extra
|
|
424
|
+
if (state.abortController === undefined) {
|
|
363
425
|
const controller = new AbortController()
|
|
364
|
-
|
|
365
|
-
if (
|
|
426
|
+
state.abortController = controller
|
|
427
|
+
if (state.abortReason !== undefined) controller.abort(state.abortReason)
|
|
366
428
|
else if (!this.#completed) this.#source.onAbort?.(() => this.abort('client'))
|
|
367
429
|
}
|
|
368
|
-
return
|
|
430
|
+
return state.abortController.signal
|
|
369
431
|
}
|
|
370
432
|
|
|
371
433
|
/** Whether {@link signal} aborted (or will be created aborted). */
|
|
372
434
|
get aborted(): boolean {
|
|
373
|
-
return this.#abortReason !== undefined
|
|
435
|
+
return this.#more?.abortReason !== undefined
|
|
374
436
|
}
|
|
375
437
|
|
|
376
438
|
/**
|
|
@@ -378,10 +440,11 @@ export class RequestContext implements HttpContext {
|
|
|
378
440
|
* never once the request is over ({@link complete}).
|
|
379
441
|
*/
|
|
380
442
|
abort(kind: RequestAbortKind): void {
|
|
381
|
-
if (this.#abortReason !== undefined || this.#completed) return
|
|
443
|
+
if (this.#more?.abortReason !== undefined || this.#completed) return
|
|
382
444
|
const reason = new RequestAbortedError(kind)
|
|
383
|
-
|
|
384
|
-
|
|
445
|
+
const state = this.#extra
|
|
446
|
+
state.abortReason = reason
|
|
447
|
+
state.abortController?.abort(reason)
|
|
385
448
|
}
|
|
386
449
|
|
|
387
450
|
/** The response headers are handed to the adapter: cookies can no longer be set (internal). */
|
|
@@ -395,16 +458,18 @@ export class RequestContext implements HttpContext {
|
|
|
395
458
|
}
|
|
396
459
|
|
|
397
460
|
rawBody(): Promise<Uint8Array> {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
461
|
+
const state = this.#extra
|
|
462
|
+
if (state.raw === undefined) {
|
|
463
|
+
if (state.streamed) return Promise.reject(alreadyStreamed())
|
|
464
|
+
state.raw = this.#source.readBody(this.#plan.limit)
|
|
401
465
|
}
|
|
402
|
-
return
|
|
466
|
+
return state.raw
|
|
403
467
|
}
|
|
404
468
|
|
|
405
469
|
bodyStream(): ReadableStream<Uint8Array> {
|
|
406
|
-
|
|
407
|
-
|
|
470
|
+
const state = this.#extra
|
|
471
|
+
if (state.raw !== undefined || state.streamed) throw alreadyStreamed()
|
|
472
|
+
state.streamed = true
|
|
408
473
|
return this.#openBodyStream()
|
|
409
474
|
}
|
|
410
475
|
|
|
@@ -414,19 +479,22 @@ export class RequestContext implements HttpContext {
|
|
|
414
479
|
* adapter discard the rest of the body, so that the connection can serve the next request.
|
|
415
480
|
*/
|
|
416
481
|
releaseBody(): void {
|
|
417
|
-
const
|
|
418
|
-
|
|
482
|
+
const state = this.#more
|
|
483
|
+
if (state === undefined) return
|
|
484
|
+
const stop = state.stopBody
|
|
485
|
+
state.stopBody = undefined
|
|
419
486
|
stop?.()
|
|
420
487
|
}
|
|
421
488
|
|
|
422
489
|
/** The body as a stream (see {@link releaseBody}). */
|
|
423
490
|
#openBodyStream(): ReadableStream<Uint8Array> {
|
|
424
491
|
const reader = openBodyStream(this.#source, this.#plan.limit).getReader()
|
|
492
|
+
const state = this.#extra
|
|
425
493
|
let controller: ReadableStreamDefaultController<Uint8Array> | undefined
|
|
426
494
|
const settle = (): void => {
|
|
427
|
-
|
|
495
|
+
state.stopBody = undefined
|
|
428
496
|
}
|
|
429
|
-
|
|
497
|
+
state.stopBody = (): void => {
|
|
430
498
|
reader.cancel().catch(() => undefined)
|
|
431
499
|
try {
|
|
432
500
|
controller?.error(new Error('The request is over: its body can no longer be read'))
|
|
@@ -464,27 +532,29 @@ export class RequestContext implements HttpContext {
|
|
|
464
532
|
}
|
|
465
533
|
|
|
466
534
|
parseBody(): Promise<unknown> {
|
|
467
|
-
|
|
468
|
-
|
|
535
|
+
const state = this.#extra
|
|
536
|
+
state.parsed ??= this.#parse()
|
|
537
|
+
return state.parsed
|
|
469
538
|
}
|
|
470
539
|
|
|
471
540
|
async #parse(): Promise<unknown> {
|
|
472
541
|
const headers = this.headers
|
|
473
542
|
const contentType = headers['content-type']
|
|
474
543
|
const media = parseMediaType(contentType)
|
|
544
|
+
const state = this.#extra
|
|
475
545
|
if (media?.type === 'multipart/form-data') {
|
|
476
546
|
checkContentEncoding(headers['content-encoding'])
|
|
477
547
|
const policy = this.#plan.multipart
|
|
478
548
|
let result: MultipartResult
|
|
479
|
-
if (
|
|
480
|
-
if (
|
|
481
|
-
|
|
549
|
+
if (state.raw === undefined) {
|
|
550
|
+
if (state.streamed) throw alreadyStreamed()
|
|
551
|
+
state.streamed = true
|
|
482
552
|
result = await readMultipart(this.#openBodyStream(), contentType as string, policy)
|
|
483
553
|
} else {
|
|
484
|
-
result = parseMultipart(await
|
|
554
|
+
result = parseMultipart(await state.raw, contentType as string, policy)
|
|
485
555
|
}
|
|
486
|
-
|
|
487
|
-
|
|
556
|
+
state.files = result.files
|
|
557
|
+
state.body = result.fields
|
|
488
558
|
return result.fields
|
|
489
559
|
}
|
|
490
560
|
if (this.#plan.upload) {
|
|
@@ -497,8 +567,8 @@ export class RequestContext implements HttpContext {
|
|
|
497
567
|
)
|
|
498
568
|
}
|
|
499
569
|
const bytes = await this.rawBody()
|
|
500
|
-
const body =
|
|
501
|
-
|
|
570
|
+
const body = parseBodyBytes(bytes, contentType, headers['content-encoding'])
|
|
571
|
+
state.body = body
|
|
502
572
|
return body
|
|
503
573
|
}
|
|
504
574
|
}
|
package/src/decorators.ts
CHANGED
|
@@ -398,14 +398,30 @@ export interface RouteMetaEntry<T = unknown> {
|
|
|
398
398
|
readonly required?: boolean
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
+
/**
|
|
402
|
+
* Anything that carries effective route metadata: the {@link HttpContext} of a request, and the
|
|
403
|
+
* contexts of other transports that reuse route decorators (a GraphQL resolver context, a WebSocket
|
|
404
|
+
* gateway context). {@link RouteDecorator.get} reads `route.meta[key]`; nothing else is required.
|
|
405
|
+
*
|
|
406
|
+
* ```ts
|
|
407
|
+
* const carrier: RouteMetaCarrier = { route: { meta: { roles: ['admin'] } } }
|
|
408
|
+
* Roles.get(carrier) // ['admin']
|
|
409
|
+
* ```
|
|
410
|
+
*/
|
|
411
|
+
export interface RouteMetaCarrier {
|
|
412
|
+
/** The matched route (or field, or handler), `undefined` when there is none. */
|
|
413
|
+
readonly route?: { readonly meta: Readonly<Record<string, unknown>> } | undefined
|
|
414
|
+
}
|
|
415
|
+
|
|
401
416
|
/** A user-defined route metadata decorator created with {@link createRouteDecorator}. */
|
|
402
417
|
export interface RouteDecorator<T> extends RouteDecoratorInfo {
|
|
403
418
|
(...args: RouteDecoratorArgs<T>): ClassMarker & MethodMarker
|
|
404
419
|
/**
|
|
405
420
|
* The value of the current route, if set. Reads the effective metadata: the route's decision of
|
|
406
|
-
* a group wins over the controller's.
|
|
421
|
+
* a group wins over the controller's. Accepts any {@link RouteMetaCarrier}: an
|
|
422
|
+
* {@link HttpContext}, or the context of another transport exposing `route.meta`.
|
|
407
423
|
*/
|
|
408
|
-
get(ctx:
|
|
424
|
+
get(ctx: RouteMetaCarrier): T | undefined
|
|
409
425
|
/**
|
|
410
426
|
* The same metadata as data (`{ key, value, group, required }`), for routes that are not compiled
|
|
411
427
|
* controllers (plugin routes: `OpenApiPlugin.configure({ meta: [Public.meta()] })`). The value is
|
|
@@ -479,7 +495,7 @@ export function createRouteDecorator<T = true>(key: string, options?: RouteDecor
|
|
|
479
495
|
key,
|
|
480
496
|
group,
|
|
481
497
|
required,
|
|
482
|
-
get: (ctx:
|
|
498
|
+
get: (ctx: RouteMetaCarrier): T | undefined => ctx.route?.meta[key] as T | undefined,
|
|
483
499
|
meta: (...args: RouteDecoratorArgs<T>): RouteMetaEntry<T> =>
|
|
484
500
|
Object.freeze({ key, value: (args.length === 0 ? true : args[0]) as T, group, required }),
|
|
485
501
|
})
|
package/src/index.ts
CHANGED
|
@@ -92,6 +92,7 @@ export {
|
|
|
92
92
|
type RouteDecoratorArgs,
|
|
93
93
|
type RouteDecoratorInfo,
|
|
94
94
|
type RouteDecoratorOptions,
|
|
95
|
+
type RouteMetaCarrier,
|
|
95
96
|
type RouteMetaEntry,
|
|
96
97
|
SignedCookie,
|
|
97
98
|
Sse,
|
|
@@ -182,4 +183,4 @@ export {
|
|
|
182
183
|
type Uploads,
|
|
183
184
|
uploads,
|
|
184
185
|
} from './uploads.ts'
|
|
185
|
-
export { validate } from './validate.ts'
|
|
186
|
+
export { isThenable, validate } from './validate.ts'
|