Skip to content

Commit 3f106f9

Browse files
committed
feat: full path and method for macro introspection metadata
1 parent 78f4828 commit 3f106f9

File tree

3 files changed

+618
-10
lines changed

3 files changed

+618
-10
lines changed

src/index.ts

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ import type {
117117
MapResponse,
118118
Checksum,
119119
MacroManager,
120+
MacroIntrospectionMetadata,
120121
MacroToProperty,
121122
TransformHandler,
122123
MetadataBase,
@@ -281,6 +282,16 @@ export default class Elysia<
281282
}
282283
}
283284

285+
// Stores macro options applied via `.guard({ ...macros })`
286+
// so we can run macro `introspect` once per route with full metadata.
287+
// Is there a way to do this without a separate store?
288+
protected guardMacroOptions: Record<string, any> = {}
289+
290+
// Flag to skip macro introspection for routes added to child instances inside groups
291+
// Macro introspection will happen when routes are added to the parent with the fully-resolved path
292+
// Is there a way to do this without a flag?
293+
protected skipMacroIntrospection?: boolean
294+
284295
protected standaloneValidator: StandaloneValidator = {
285296
global: null,
286297
scoped: null,
@@ -487,7 +498,51 @@ export default class Elysia<
487498

488499
localHook ??= {}
489500

490-
this.applyMacro(localHook)
501+
// Normalize path before macro introspection to ensure metadata has the correct path
502+
if (path !== '' && path.charCodeAt(0) !== 47) path = '/' + path
503+
if (this.config.prefix && !skipPrefix) path = this.config.prefix + path
504+
505+
// Run macro introspection for guard-level macros (configured via `.guard({ macro: ... })`)
506+
// once per route with the fully-resolved path.
507+
if (
508+
this.guardMacroOptions &&
509+
Object.keys(this.guardMacroOptions).length
510+
) {
511+
const macro = this.extender.macro
512+
513+
for (const [key, value] of Object.entries(this.guardMacroOptions)) {
514+
if (!(key in macro)) continue
515+
516+
const macroDef = macro[key]
517+
const macroHook =
518+
typeof macroDef === 'function' ? macroDef(value) : macroDef
519+
520+
if (
521+
!macroHook ||
522+
(typeof macroDef === 'object' && value === false)
523+
)
524+
continue
525+
526+
const introspectFn =
527+
typeof macroHook === 'object' && macroHook !== null && 'introspect' in macroHook
528+
? (macroHook as { introspect?: (option: Record<string, any>, context: MacroIntrospectionMetadata) => unknown }).introspect
529+
: undefined
530+
531+
if (typeof introspectFn === 'function') {
532+
const introspectOptions: Record<string, any> = {
533+
[key]: value
534+
}
535+
536+
introspectFn(introspectOptions, { path, method })
537+
}
538+
}
539+
}
540+
541+
// Skip macro introspection for routes added to child instances inside groups
542+
// Macro introspection will happen when routes are added to the parent with the fully-resolved path
543+
if (!this.skipMacroIntrospection) {
544+
this.applyMacro(localHook, localHook, { metadata: { path, method } })
545+
}
491546

492547
let standaloneValidators = [] as InputSchema[]
493548

@@ -511,9 +566,6 @@ export default class Elysia<
511566
this.standaloneValidator.global
512567
)
513568

514-
if (path !== '' && path.charCodeAt(0) !== 47) path = '/' + path
515-
if (this.config.prefix && !skipPrefix) path = this.config.prefix + path
516-
517569
if (localHook?.type)
518570
switch (localHook.type) {
519571
case 'text':
@@ -3976,11 +4028,21 @@ export default class Elysia<
39764028
scoped: [...(this.standaloneValidator.scoped ?? [])],
39774029
global: [...(this.standaloneValidator.global ?? [])]
39784030
}
4031+
// Mark this instance as being inside a group to skip macro introspection
4032+
// Macro introspection will happen when routes are added to the parent with the fully-resolved path
4033+
instance.skipMacroIntrospection = true
39794034

39804035
const isSchema = typeof schemaOrRun === 'object'
39814036
const sandbox = (isSchema ? run! : schemaOrRun)(instance)
39824037
this.singleton = mergeDeep(this.singleton, instance.singleton) as any
39834038
this.definitions = mergeDeep(this.definitions, instance.definitions)
4039+
// Merge macros from the group instance so introspect can be called when routes are added to the parent
4040+
if (isNotEmpty(instance.extender.macro)) {
4041+
this.extender.macro = {
4042+
...this.extender.macro,
4043+
...instance.extender.macro
4044+
}
4045+
}
39844046

39854047
if (sandbox.event.request?.length)
39864048
this.event.request = [
@@ -4472,6 +4534,30 @@ export default class Elysia<
44724534
): AnyElysia {
44734535
if (!run) {
44744536
if (typeof hook === 'object') {
4537+
// Capture guard-level macro options so we can introspect them per-route later
4538+
const macro = this.extender.macro
4539+
if (macro && typeof macro === 'object') {
4540+
const guardOptions: Record<string, any> = {}
4541+
4542+
for (const [key, value] of Object.entries(hook)) {
4543+
if (!(key in macro)) continue
4544+
4545+
const macroDef = macro[key]
4546+
4547+
// Match object-style macro semantics: value=false disables the macro entirely
4548+
if (typeof macroDef === 'object' && value === false)
4549+
continue
4550+
4551+
guardOptions[key] = value
4552+
}
4553+
4554+
if (Object.keys(guardOptions).length)
4555+
this.guardMacroOptions = {
4556+
...this.guardMacroOptions,
4557+
...guardOptions
4558+
}
4559+
}
4560+
44754561
this.applyMacro(hook)
44764562

44774563
if (hook.detail) {
@@ -5390,8 +5476,13 @@ export default class Elysia<
53905476
appliable: AnyLocalHook = localHook,
53915477
{
53925478
iteration = 0,
5393-
applied = {}
5394-
}: { iteration?: number; applied?: { [key: number]: true } } = {}
5479+
applied = {},
5480+
metadata
5481+
}: {
5482+
iteration?: number
5483+
applied?: { [key: number]: true }
5484+
metadata?: MacroIntrospectionMetadata
5485+
} = {}
53955486
) {
53965487
if (iteration >= 16) return
53975488
const macro = this.extender.macro
@@ -5429,7 +5520,17 @@ export default class Elysia<
54295520
}
54305521

54315522
if (k === 'introspect') {
5432-
value?.(localHook)
5523+
// Only run introspect when route metadata is available.
5524+
// Guard-level macros (configured via `.guard({ macro: ... })`)
5525+
// are introspected per-route in `add`.
5526+
if (metadata) {
5527+
// Call introspect with only the options relevant to the current macro key
5528+
const introspectOptions: Record<string, any> = {
5529+
[key]: appliable[key]
5530+
}
5531+
5532+
value?.(introspectOptions, metadata)
5533+
}
54335534

54345535
delete localHook[key]
54355536
continue
@@ -5449,7 +5550,7 @@ export default class Elysia<
54495550
this.applyMacro(
54505551
localHook,
54515552
{ [k]: value },
5452-
{ applied, iteration: iteration + 1 }
5553+
{ applied, iteration: iteration + 1, metadata }
54535554
)
54545555

54555556
delete localHook[key]
@@ -8214,6 +8315,7 @@ export type {
82148315
MapResponse,
82158316
BaseMacro,
82168317
MacroManager,
8318+
MacroIntrospectionMetadata,
82178319
MacroToProperty,
82188320
MergeElysiaInstances,
82198321
MaybeArray,

src/types.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1943,6 +1943,23 @@ export type BaseMacro = Record<
19431943

19441944
export type MaybeValueOrVoidFunction<T> = T | ((...a: any) => void | T)
19451945

1946+
export interface MacroIntrospectionMetadata {
1947+
/**
1948+
* Metadata of the unresolved route being introspected.
1949+
*
1950+
* @example
1951+
* '/route/:id'
1952+
*/
1953+
path: string
1954+
/**
1955+
* HTTP method of the unresolved route being introspected.
1956+
*
1957+
* @example
1958+
* 'GET'
1959+
*/
1960+
method: HTTPMethod
1961+
}
1962+
19461963
export interface MacroProperty<
19471964
in out Macro extends BaseMacro = {},
19481965
in out TypedRoute extends RouteSchema = {},
@@ -1970,9 +1987,13 @@ export interface MacroProperty<
19701987
/**
19711988
* Introspect hook option for documentation generation or analysis
19721989
*
1973-
* @param option
1990+
* @param option The options passed to the macro
1991+
* @param context The metadata of the introspection.
19741992
*/
1975-
introspect?(option: Prettify<Macro>): unknown
1993+
introspect?(
1994+
option: Record<string, any>,
1995+
context: MacroIntrospectionMetadata
1996+
): unknown
19761997
}
19771998

19781999
export interface Macro<

0 commit comments

Comments
 (0)