Skip to content

Commit 6543156

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

File tree

3 files changed

+839
-39
lines changed

3 files changed

+839
-39
lines changed

src/index.ts

Lines changed: 229 additions & 35 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,65 @@ 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' &&
531+
macroHook !== null &&
532+
'introspect' in macroHook
533+
? (
534+
macroHook as {
535+
introspect?: (
536+
option: Record<string, any>,
537+
context: MacroIntrospectionMetadata
538+
) => unknown
539+
}
540+
).introspect
541+
: undefined
542+
543+
if (typeof introspectFn === 'function') {
544+
const introspectOptions: Record<string, any> = {
545+
[key]: value
546+
}
547+
548+
introspectFn(introspectOptions, { path, method })
549+
}
550+
}
551+
}
552+
553+
// Skip macro introspection for routes added to child instances inside groups
554+
// Macro introspection will happen when routes are added to the parent with the fully-resolved path
555+
if (!this.skipMacroIntrospection) {
556+
this.applyMacro(localHook, localHook, {
557+
metadata: { path, method }
558+
})
559+
}
491560

492561
let standaloneValidators = [] as InputSchema[]
493562

@@ -511,9 +580,6 @@ export default class Elysia<
511580
this.standaloneValidator.global
512581
)
513582

514-
if (path !== '' && path.charCodeAt(0) !== 47) path = '/' + path
515-
if (this.config.prefix && !skipPrefix) path = this.config.prefix + path
516-
517583
if (localHook?.type)
518584
switch (localHook.type) {
519585
case 'text':
@@ -3976,11 +4042,21 @@ export default class Elysia<
39764042
scoped: [...(this.standaloneValidator.scoped ?? [])],
39774043
global: [...(this.standaloneValidator.global ?? [])]
39784044
}
4045+
// Mark this instance as being inside a group to skip macro introspection
4046+
// Macro introspection will happen when routes are added to the parent with the fully-resolved path
4047+
instance.skipMacroIntrospection = true
39794048

39804049
const isSchema = typeof schemaOrRun === 'object'
39814050
const sandbox = (isSchema ? run! : schemaOrRun)(instance)
39824051
this.singleton = mergeDeep(this.singleton, instance.singleton) as any
39834052
this.definitions = mergeDeep(this.definitions, instance.definitions)
4053+
// Merge macros from the group instance so introspect can be called when routes are added to the parent
4054+
if (isNotEmpty(instance.extender.macro)) {
4055+
this.extender.macro = {
4056+
...this.extender.macro,
4057+
...instance.extender.macro
4058+
}
4059+
}
39844060

39854061
if (sandbox.event.request?.length)
39864062
this.event.request = [
@@ -3996,6 +4072,20 @@ export default class Elysia<
39964072

39974073
this.model(sandbox.definitions.type)
39984074

4075+
// Merge guardMacroOptions from group instance to parent during route merging
4076+
// so guard macro introspection happens with correct options when routes are added
4077+
// The parent's guardMacroOptions will be restored after merging is complete
4078+
const originalGuardMacroOptions = this.guardMacroOptions
4079+
if (
4080+
instance.guardMacroOptions &&
4081+
Object.keys(instance.guardMacroOptions).length
4082+
) {
4083+
this.guardMacroOptions = {
4084+
...this.guardMacroOptions,
4085+
...instance.guardMacroOptions
4086+
}
4087+
}
4088+
39994089
Object.values(instance.router.history).forEach(
40004090
({ method, path, handler, hooks }) => {
40014091
path =
@@ -4066,6 +4156,9 @@ export default class Elysia<
40664156
}
40674157
)
40684158

4159+
// Restore original guardMacroOptions
4160+
this.guardMacroOptions = originalGuardMacroOptions
4161+
40694162
return this as any
40704163
}
40714164

@@ -4472,6 +4565,38 @@ export default class Elysia<
44724565
): AnyElysia {
44734566
if (!run) {
44744567
if (typeof hook === 'object') {
4568+
// Capture guard-level macro options so we can introspect them per-route later
4569+
const macro = this.extender.macro
4570+
if (macro && typeof macro === 'object') {
4571+
const updatedGuardMacroOptions = {
4572+
...this.guardMacroOptions
4573+
}
4574+
let guardMacroOptionsChanged = false
4575+
4576+
for (const [key, value] of Object.entries(hook)) {
4577+
if (!(key in macro)) continue
4578+
4579+
const macroDef = macro[key]
4580+
4581+
// Match object-style macro semantics: value=false disables the macro entirely
4582+
if (typeof macroDef === 'object' && value === false) {
4583+
if (key in updatedGuardMacroOptions) {
4584+
delete updatedGuardMacroOptions[key]
4585+
guardMacroOptionsChanged = true
4586+
}
4587+
continue
4588+
}
4589+
4590+
if (updatedGuardMacroOptions[key] !== value) {
4591+
updatedGuardMacroOptions[key] = value
4592+
guardMacroOptionsChanged = true
4593+
}
4594+
}
4595+
4596+
if (guardMacroOptionsChanged)
4597+
this.guardMacroOptions = updatedGuardMacroOptions
4598+
}
4599+
44754600
this.applyMacro(hook)
44764601

44774602
if (hook.detail) {
@@ -4558,6 +4683,41 @@ export default class Elysia<
45584683
instance.definitions = { ...this.definitions }
45594684
instance.inference = cloneInference(this.inference)
45604685
instance.extender = { ...this.extender }
4686+
instance.guardMacroOptions = { ...this.guardMacroOptions }
4687+
// Apply guard hook's macro options to child instance so routes are introspected
4688+
// with the correct options when added (not when merged)
4689+
if (typeof hook === 'object') {
4690+
const macro = this.extender.macro
4691+
if (macro && typeof macro === 'object') {
4692+
const updatedGuardMacroOptions = {
4693+
...instance.guardMacroOptions
4694+
}
4695+
let guardMacroOptionsChanged = false
4696+
4697+
for (const [key, value] of Object.entries(hook)) {
4698+
if (!(key in macro)) continue
4699+
4700+
const macroDef = macro[key]
4701+
4702+
// Match object-style macro semantics: value=false disables the macro entirely
4703+
if (typeof macroDef === 'object' && value === false) {
4704+
if (key in updatedGuardMacroOptions) {
4705+
delete updatedGuardMacroOptions[key]
4706+
guardMacroOptionsChanged = true
4707+
}
4708+
continue
4709+
}
4710+
4711+
if (updatedGuardMacroOptions[key] !== value) {
4712+
updatedGuardMacroOptions[key] = value
4713+
guardMacroOptionsChanged = true
4714+
}
4715+
}
4716+
4717+
if (guardMacroOptionsChanged)
4718+
instance.guardMacroOptions = updatedGuardMacroOptions
4719+
}
4720+
}
45614721
instance.getServer = () => this.getServer()
45624722

45634723
const sandbox = run(instance)
@@ -4581,6 +4741,15 @@ export default class Elysia<
45814741

45824742
this.model(sandbox.definitions.type)
45834743

4744+
// Routes are already introspected for guard-level macros when added to child instances
4745+
// so we don't need to introspect guard-level macros again when merging routes back
4746+
// Temporarily skip macro introspection and set guardMacroOptions to empty
4747+
// Then manually apply macros without introspection to ensure macros are still applied
4748+
const originalGuardMacroOptions = this.guardMacroOptions
4749+
const originalSkipMacroIntrospection = this.skipMacroIntrospection
4750+
this.skipMacroIntrospection = true
4751+
this.guardMacroOptions = {}
4752+
45844753
Object.values(instance.router.history).forEach(
45854754
({ method, path, handler, hooks: localHook }) => {
45864755
const {
@@ -4596,41 +4765,50 @@ export default class Elysia<
45964765
const hasStandaloneSchema =
45974766
body || headers || query || params || cookie || response
45984767

4768+
const mergedHook = mergeHook(guardHook as AnyLocalHook, {
4769+
...((localHook || {}) as AnyLocalHook),
4770+
error: !localHook.error
4771+
? sandbox.event.error
4772+
: Array.isArray(localHook.error)
4773+
? [
4774+
...(localHook.error ?? []),
4775+
...(sandbox.event.error ?? [])
4776+
]
4777+
: [
4778+
localHook.error,
4779+
...(sandbox.event.error ?? [])
4780+
],
4781+
standaloneValidator: !hasStandaloneSchema
4782+
? localHook.standaloneValidator
4783+
: [
4784+
...(localHook.standaloneValidator ?? []),
4785+
{
4786+
body,
4787+
headers,
4788+
query,
4789+
params,
4790+
cookie,
4791+
response
4792+
}
4793+
]
4794+
})
4795+
4796+
// Apply macros without introspection (no metadata passed) before adding route
4797+
this.applyMacro(mergedHook, mergedHook)
4798+
45994799
this.add(
46004800
method,
46014801
path,
46024802
handler,
4603-
mergeHook(guardHook as AnyLocalHook, {
4604-
...((localHook || {}) as AnyLocalHook),
4605-
error: !localHook.error
4606-
? sandbox.event.error
4607-
: Array.isArray(localHook.error)
4608-
? [
4609-
...(localHook.error ?? []),
4610-
...(sandbox.event.error ?? [])
4611-
]
4612-
: [
4613-
localHook.error,
4614-
...(sandbox.event.error ?? [])
4615-
],
4616-
standaloneValidator: !hasStandaloneSchema
4617-
? localHook.standaloneValidator
4618-
: [
4619-
...(localHook.standaloneValidator ?? []),
4620-
{
4621-
body,
4622-
headers,
4623-
query,
4624-
params,
4625-
cookie,
4626-
response
4627-
}
4628-
]
4629-
})
4803+
mergedHook
46304804
)
46314805
}
46324806
)
46334807

4808+
// Restore original guardMacroOptions and skipMacroIntrospection
4809+
this.guardMacroOptions = originalGuardMacroOptions
4810+
this.skipMacroIntrospection = originalSkipMacroIntrospection
4811+
46344812
return this as any
46354813
}
46364814

@@ -5390,8 +5568,13 @@ export default class Elysia<
53905568
appliable: AnyLocalHook = localHook,
53915569
{
53925570
iteration = 0,
5393-
applied = {}
5394-
}: { iteration?: number; applied?: { [key: number]: true } } = {}
5571+
applied = {},
5572+
metadata
5573+
}: {
5574+
iteration?: number
5575+
applied?: { [key: number]: true }
5576+
metadata?: MacroIntrospectionMetadata
5577+
} = {}
53955578
) {
53965579
if (iteration >= 16) return
53975580
const macro = this.extender.macro
@@ -5429,7 +5612,17 @@ export default class Elysia<
54295612
}
54305613

54315614
if (k === 'introspect') {
5432-
value?.(localHook)
5615+
// Only run introspect when route metadata is available.
5616+
// Guard-level macros (configured via `.guard({ macro: ... })`)
5617+
// are introspected per-route in `add`.
5618+
if (metadata) {
5619+
// Call introspect with only the options relevant to the current macro key
5620+
const introspectOptions: Record<string, any> = {
5621+
[key]: appliable[key]
5622+
}
5623+
5624+
value?.(introspectOptions, metadata)
5625+
}
54335626

54345627
delete localHook[key]
54355628
continue
@@ -5449,7 +5642,7 @@ export default class Elysia<
54495642
this.applyMacro(
54505643
localHook,
54515644
{ [k]: value },
5452-
{ applied, iteration: iteration + 1 }
5645+
{ applied, iteration: iteration + 1, metadata }
54535646
)
54545647

54555648
delete localHook[key]
@@ -8214,6 +8407,7 @@ export type {
82148407
MapResponse,
82158408
BaseMacro,
82168409
MacroManager,
8410+
MacroIntrospectionMetadata,
82178411
MacroToProperty,
82188412
MergeElysiaInstances,
82198413
MaybeArray,

0 commit comments

Comments
 (0)