Skip to content

Commit 0771705

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

File tree

3 files changed

+668
-12
lines changed

3 files changed

+668
-12
lines changed

src/index.ts

Lines changed: 130 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,54 @@ 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+
// Skip introspection for routes added to child instances inside groups
508+
// Macro introspection will happen when routes are added to the parent with the fully-resolved path
509+
if (
510+
!this.skipMacroIntrospection &&
511+
this.guardMacroOptions &&
512+
Object.keys(this.guardMacroOptions).length
513+
) {
514+
const macro = this.extender.macro
515+
516+
for (const [key, value] of Object.entries(this.guardMacroOptions)) {
517+
if (!(key in macro)) continue
518+
519+
const macroDef = macro[key]
520+
const macroHook =
521+
typeof macroDef === 'function' ? macroDef(value) : macroDef
522+
523+
if (
524+
!macroHook ||
525+
(typeof macroDef === 'object' && value === false)
526+
)
527+
continue
528+
529+
const introspectFn =
530+
typeof macroHook === 'object' && macroHook !== null && 'introspect' in macroHook
531+
? (macroHook as { introspect?: (option: Record<string, any>, context: MacroIntrospectionMetadata) => unknown }).introspect
532+
: undefined
533+
534+
if (typeof introspectFn === 'function') {
535+
const introspectOptions: Record<string, any> = {
536+
[key]: value
537+
}
538+
539+
introspectFn(introspectOptions, { path, method })
540+
}
541+
}
542+
}
543+
544+
// Skip macro introspection for routes added to child instances inside groups
545+
// Macro introspection will happen when routes are added to the parent with the fully-resolved path
546+
if (!this.skipMacroIntrospection) {
547+
this.applyMacro(localHook, localHook, { metadata: { path, method } })
548+
}
491549

492550
let standaloneValidators = [] as InputSchema[]
493551

@@ -511,9 +569,6 @@ export default class Elysia<
511569
this.standaloneValidator.global
512570
)
513571

514-
if (path !== '' && path.charCodeAt(0) !== 47) path = '/' + path
515-
if (this.config.prefix && !skipPrefix) path = this.config.prefix + path
516-
517572
if (localHook?.type)
518573
switch (localHook.type) {
519574
case 'text':
@@ -3976,11 +4031,21 @@ export default class Elysia<
39764031
scoped: [...(this.standaloneValidator.scoped ?? [])],
39774032
global: [...(this.standaloneValidator.global ?? [])]
39784033
}
4034+
// Mark this instance as being inside a group to skip macro introspection
4035+
// Macro introspection will happen when routes are added to the parent with the fully-resolved path
4036+
instance.skipMacroIntrospection = true
39794037

39804038
const isSchema = typeof schemaOrRun === 'object'
39814039
const sandbox = (isSchema ? run! : schemaOrRun)(instance)
39824040
this.singleton = mergeDeep(this.singleton, instance.singleton) as any
39834041
this.definitions = mergeDeep(this.definitions, instance.definitions)
4042+
// Merge macros from the group instance so introspect can be called when routes are added to the parent
4043+
if (isNotEmpty(instance.extender.macro)) {
4044+
this.extender.macro = {
4045+
...this.extender.macro,
4046+
...instance.extender.macro
4047+
}
4048+
}
39844049

39854050
if (sandbox.event.request?.length)
39864051
this.event.request = [
@@ -3996,6 +4061,20 @@ export default class Elysia<
39964061

39974062
this.model(sandbox.definitions.type)
39984063

4064+
// Merge guardMacroOptions from group instance to parent during route merging
4065+
// so guard macro introspection happens with correct options when routes are added
4066+
// The parent's guardMacroOptions will be restored after merging is complete
4067+
const originalGuardMacroOptions = this.guardMacroOptions
4068+
if (
4069+
instance.guardMacroOptions &&
4070+
Object.keys(instance.guardMacroOptions).length
4071+
) {
4072+
this.guardMacroOptions = {
4073+
...this.guardMacroOptions,
4074+
...instance.guardMacroOptions
4075+
}
4076+
}
4077+
39994078
Object.values(instance.router.history).forEach(
40004079
({ method, path, handler, hooks }) => {
40014080
path =
@@ -4066,6 +4145,9 @@ export default class Elysia<
40664145
}
40674146
)
40684147

4148+
// Restore original guardMacroOptions
4149+
this.guardMacroOptions = originalGuardMacroOptions
4150+
40694151
return this as any
40704152
}
40714153

@@ -4472,6 +4554,30 @@ export default class Elysia<
44724554
): AnyElysia {
44734555
if (!run) {
44744556
if (typeof hook === 'object') {
4557+
// Capture guard-level macro options so we can introspect them per-route later
4558+
const macro = this.extender.macro
4559+
if (macro && typeof macro === 'object') {
4560+
const guardOptions: Record<string, any> = {}
4561+
4562+
for (const [key, value] of Object.entries(hook)) {
4563+
if (!(key in macro)) continue
4564+
4565+
const macroDef = macro[key]
4566+
4567+
// Match object-style macro semantics: value=false disables the macro entirely
4568+
if (typeof macroDef === 'object' && value === false)
4569+
continue
4570+
4571+
guardOptions[key] = value
4572+
}
4573+
4574+
if (Object.keys(guardOptions).length)
4575+
this.guardMacroOptions = {
4576+
...this.guardMacroOptions,
4577+
...guardOptions
4578+
}
4579+
}
4580+
44754581
this.applyMacro(hook)
44764582

44774583
if (hook.detail) {
@@ -5390,8 +5496,13 @@ export default class Elysia<
53905496
appliable: AnyLocalHook = localHook,
53915497
{
53925498
iteration = 0,
5393-
applied = {}
5394-
}: { iteration?: number; applied?: { [key: number]: true } } = {}
5499+
applied = {},
5500+
metadata
5501+
}: {
5502+
iteration?: number
5503+
applied?: { [key: number]: true }
5504+
metadata?: MacroIntrospectionMetadata
5505+
} = {}
53955506
) {
53965507
if (iteration >= 16) return
53975508
const macro = this.extender.macro
@@ -5429,7 +5540,17 @@ export default class Elysia<
54295540
}
54305541

54315542
if (k === 'introspect') {
5432-
value?.(localHook)
5543+
// Only run introspect when route metadata is available.
5544+
// Guard-level macros (configured via `.guard({ macro: ... })`)
5545+
// are introspected per-route in `add`.
5546+
if (metadata) {
5547+
// Call introspect with only the options relevant to the current macro key
5548+
const introspectOptions: Record<string, any> = {
5549+
[key]: appliable[key]
5550+
}
5551+
5552+
value?.(introspectOptions, metadata)
5553+
}
54335554

54345555
delete localHook[key]
54355556
continue
@@ -5449,7 +5570,7 @@ export default class Elysia<
54495570
this.applyMacro(
54505571
localHook,
54515572
{ [k]: value },
5452-
{ applied, iteration: iteration + 1 }
5573+
{ applied, iteration: iteration + 1, metadata }
54535574
)
54545575

54555576
delete localHook[key]
@@ -8214,6 +8335,7 @@ export type {
82148335
MapResponse,
82158336
BaseMacro,
82168337
MacroManager,
8338+
MacroIntrospectionMetadata,
82178339
MacroToProperty,
82188340
MergeElysiaInstances,
82198341
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)