Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 112 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5261,6 +5261,101 @@ export default class Elysia<
return this
}

/**
* Named macro with explicit dependencies (function syntax)
*
* Use this overload when you need a function-syntax named macro that
* accesses resolve values from previous macros. The `dependencies` array
* explicitly declares which macros this macro depends on, enabling proper
* TypeScript inference for the resolve context.
*
* @example
* ```typescript
* .macro("auth", { resolve: () => ({ user: "bob" }) })
* .macro("permission", ["auth"], (perm: string) => ({
* auth: true,
* resolve: ({ user }) => {
* // `user` is properly inferred from auth's resolve
* return { hasPermission: checkPermission(user, perm) }
* }
* }))
* ```
*
* @see https://github.com/elysiajs/elysia/issues/1574
*/
macro<
const Name extends string,
const Dependencies extends ReadonlyArray<
keyof Metadata['macroFn'] & string
>,
const DependencyMacros extends Pick<
Metadata['macroFn'],
Dependencies[number]
>,
const MacroContext extends MacroToContext<
Metadata['macroFn'],
// Create a selector object with all dependencies set to true
{ [K in Dependencies[number]]: true },
Definitions['typebox']
>,
const Schema extends MergeSchema<
Metadata['schema'],
MergeSchema<
Volatile['schema'],
MergeSchema<Ephemeral['schema'], MacroContext>
> &
Metadata['standaloneSchema'] &
Ephemeral['standaloneSchema'] &
Volatile['standaloneSchema']
>,
const Params,
const Property extends (
param: Params
) => MacroProperty<
Metadata['macro'] &
InputSchema<keyof Definitions['typebox'] & string> & {
[name in Name]?: boolean
} & { [K in Dependencies[number]]?: boolean },
Schema,
Singleton & {
derive: Partial<Ephemeral['derive'] & Volatile['derive']>
resolve: Partial<Ephemeral['resolve'] & Volatile['resolve']> &
// @ts-ignore
MacroContext['resolve']
},
Definitions['error']
>
>(
name: Name,
dependencies: Dependencies,
macro: Property
): Elysia<
BasePath,
Singleton,
Definitions,
{
schema: Metadata['schema']
standaloneSchema: Metadata['standaloneSchema']
macro: Metadata['macro'] & {
[name in Name]?: Params
}
macroFn: Metadata['macroFn'] & {
[name in Name]: Property
}
parser: Metadata['parser']
response: Metadata['response']
},
Routes,
Ephemeral,
Volatile
>

/**
* Named macro (object or simple function syntax)
*
* For macros that don't need to access previous macro resolve values,
* or for object-syntax named macros.
*/
macro<
const Name extends string,
const Input extends Metadata['macro'] &
Expand Down Expand Up @@ -5415,7 +5510,23 @@ export default class Elysia<
Volatile
>

macro(macroOrName: string | Macro, macro?: Macro) {
macro(
macroOrName: string | Macro,
dependenciesOrMacro?: ReadonlyArray<string> | Macro,
macroDep?: Macro
) {
// Handle new overload: macro(name, dependencies, macro)
if (
typeof macroOrName === 'string' &&
Array.isArray(dependenciesOrMacro)
) {
if (!macroDep) throw new Error('Macro function is required')
this.extender.macro[macroOrName] = macroDep
return this as any
}

const macro = dependenciesOrMacro as Macro | undefined

if (typeof macroOrName === 'string' && !macro)
throw new Error('Macro function is required')

Expand Down
113 changes: 113 additions & 0 deletions test/macro/macro.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1438,4 +1438,117 @@ describe('Macro', () => {

expect(invalid3.status).toBe(422)
})

it('infer previous macro resolve in function syntax with explicit dependencies (issue #1574)', async () => {
// This test verifies the fix for issue #1574
// https://github.com/elysiajs/elysia/issues/1574
//
// The new overload macro(name, dependencies, fn) allows function-syntax
// named macros to properly infer resolve types from previous macros
// by explicitly declaring dependencies.

const app = new Elysia()
.macro('auth', {
resolve: () => ({ user: 'bob' as const })
})
// Using the new explicit dependencies syntax
.macro('permission', ['auth'], (permission: string) => ({
auth: true,
resolve: ({ user }) => {
// `user` should be properly inferred as 'bob'
// This would fail TypeScript compilation if inference doesn't work
const typedUser: 'bob' = user
return { hasPermission: user === 'bob' && permission === 'admin' }
}
}))
.get('/check', ({ hasPermission }) => ({ hasPermission }), {
permission: 'admin'
})

const response = await app.handle(req('/check'))
expect(response.status).toBe(200)
expect(await response.json()).toEqual({ hasPermission: true })
})

it('handle multiple dependencies in function syntax macro', async () => {
const app = new Elysia()
.macro('auth', {
resolve: () => ({ user: { id: 1, name: 'Alice' } })
})
.macro('tenant', {
resolve: () => ({ tenantId: 'tenant-123' })
})
// Depending on both auth and tenant
.macro('permissions', ['auth', 'tenant'], (role: string) => ({
auth: true,
tenant: true,
resolve: ({ user, tenantId }) => ({
canAccess: user.id === 1 && tenantId === 'tenant-123' && role === 'admin'
})
}))
.get('/access', ({ canAccess }) => ({ canAccess }), {
permissions: 'admin'
})

const response = await app.handle(req('/access'))
expect(response.status).toBe(200)
expect(await response.json()).toEqual({ canAccess: true })
})

it('chain function syntax macros with dependencies', async () => {
const app = new Elysia()
.macro('a', {
resolve: () => ({ valueA: 'A' as const })
})
.macro('b', ['a'], (_: boolean) => ({
a: true,
resolve: ({ valueA }) => ({
valueB: `${valueA}-B` as const
})
}))
.macro('c', ['a', 'b'], (_: boolean) => ({
a: true,
b: true,
resolve: ({ valueA, valueB }) => ({
valueC: `${valueA}-${valueB}-C` as const
})
}))
// Use the macro at route level - the handler receives the resolved values
// Note: Type inference for route handler when using transitive macro dependencies
// is complex; the runtime behavior is correct even if types need explicit annotation
.get('/chain', (ctx) => ({
valueA: (ctx as any).valueA,
valueB: (ctx as any).valueB,
valueC: (ctx as any).valueC
}), {
c: true
})

const response = await app.handle(req('/chain'))
expect(response.status).toBe(200)
expect(await response.json()).toEqual({
valueA: 'A',
valueB: 'A-B',
valueC: 'A-A-B-C'
})
})

it('function syntax macro without dependencies still works', async () => {
// Verify that simple function macros without resolve dependencies
// continue to work with the existing syntax (no dependencies array)
const app = new Elysia()
.macro('logger', (prefix: string) => ({
beforeHandle: () => {
// Just a side effect, no resolve needed
},
resolve: () => ({ logPrefix: prefix })
}))
.get('/', ({ logPrefix }) => ({ prefix: logPrefix }), {
logger: 'TEST'
})

const response = await app.handle(req('/'))
expect(response.status).toBe(200)
expect(await response.json()).toEqual({ prefix: 'TEST' })
})
})
Loading