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
79 changes: 79 additions & 0 deletions packages/contract/src/router-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,85 @@ it('minifyContractRouter', () => {
expect((minified as any).nested.pong).toEqual(minifiedPong)
})

describe('contract modules that export primitives alongside procedures', () => {
// Simulates: import * as userContract from './contracts/user'
// where the module exports contract procedures AND constants like:
// export const getUser = oc.input(userSchema)
// export const listUsers = oc.input(listSchema)
// export const API_VERSION = 'v2'
// export const MAX_PAGE_SIZE = 100
// export const ENABLE_CACHE = true

const moduleWithPrimitives = {
getUser: ping,
listUsers: pong,
API_VERSION: 'v2',
MAX_PAGE_SIZE: 100,
ENABLE_CACHE: true,
DEPRECATED: null,
OPTIONAL_FEATURE: undefined,
} as any

describe('enhanceContractRouter', () => {
const options = { errorMap: {}, prefix: '/api', tags: ['api'] } as const

it('enhances procedures and passes through primitive exports', () => {
const enhanced = enhanceContractRouter(moduleWithPrimitives, options)
expect(isContractProcedure(enhanced.getUser)).toBe(true)
expect(isContractProcedure(enhanced.listUsers)).toBe(true)
expect(enhanced.API_VERSION).toBe('v2')
expect(enhanced.MAX_PAGE_SIZE).toBe(100)
expect(enhanced.ENABLE_CACHE).toBe(true)
})

it('handles single-character string exports without stack overflow', () => {
// Single-char strings are the worst case: for...in on 'v' yields key '0',
// and 'v'[0] === 'v' creates an infinite loop
const moduleWithFlag = { getUser: ping, v: 'v' } as any
expect(() => enhanceContractRouter(moduleWithFlag, options)).not.toThrow()
})
})

describe('minifyContractRouter', () => {
it('minifies procedures and passes through primitive exports', () => {
const minified = minifyContractRouter(moduleWithPrimitives)
expect(isContractProcedure((minified as any).getUser)).toBe(true)
expect(isContractProcedure((minified as any).listUsers)).toBe(true)
expect((minified as any).API_VERSION).toBe('v2')
expect((minified as any).MAX_PAGE_SIZE).toBe(100)
})

it('handles single-character string exports without stack overflow', () => {
const moduleWithFlag = { getUser: ping, v: 'v' } as any
expect(() => minifyContractRouter(moduleWithFlag)).not.toThrow()
})
})

describe('populateContractRouterPaths', () => {
it('populates procedure paths and passes through primitive exports', () => {
const moduleForPaths = {
getUser: oc.input(inputSchema),
listUsers: oc.output(outputSchema),
API_VERSION: 'v2',
MAX_PAGE_SIZE: 100,
ENABLE_CACHE: true,
} as any
const populated = populateContractRouterPaths(moduleForPaths)
expect(isContractProcedure(populated.getUser)).toBe(true)
expect(populated.getUser['~orpc'].route.path).toBe('/getUser')
expect(isContractProcedure(populated.listUsers)).toBe(true)
expect(populated.listUsers['~orpc'].route.path).toBe('/listUsers')
expect(populated.API_VERSION).toBe('v2')
expect(populated.MAX_PAGE_SIZE).toBe(100)
})

it('handles single-character string exports without stack overflow', () => {
const moduleWithFlag = { getUser: oc.input(inputSchema), v: 'v' } as any
expect(() => populateContractRouterPaths(moduleWithFlag)).not.toThrow()
})
})
})

it('populateContractRouterPaths', () => {
const contract = {
ping: oc.input(inputSchema),
Expand Down
12 changes: 12 additions & 0 deletions packages/contract/src/router-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export function enhanceContractRouter<T extends AnyContractRouter, TErrorMap ext
return enhanced as any
}

if (typeof router !== 'object' || router === null) {
return router as any
}

const enhanced: Record<string, any> = {}

for (const key in router) {
Expand Down Expand Up @@ -83,6 +87,10 @@ export function minifyContractRouter(router: AnyContractRouter): AnyContractRout
return procedure
}

if (typeof router !== 'object' || router === null) {
return router as any
}

const json: Record<string, AnyContractRouter> = {}

for (const key in router) {
Expand Down Expand Up @@ -128,6 +136,10 @@ export function populateContractRouterPaths<T extends AnyContractRouter>(router:
return router as any
}

if (typeof router !== 'object' || router === null) {
return router as any
}

const populated: Record<string, any> = {}

for (const key in router) {
Expand Down
75 changes: 75 additions & 0 deletions packages/server/src/router-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,78 @@ it('unlazyRouter', async () => {
},
})
})

describe('router modules that export primitives alongside procedures', () => {
// Simulates: import * as userRouter from './routes/user'
// where the module exports procedures AND constants like:
// export const getUser = os.handler(...)
// export const listUsers = os.handler(...)
// export const API_VERSION = 'v2'
// export const MAX_PAGE_SIZE = 100
// export const ENABLE_CACHE = true

const moduleWithPrimitives = {
getUser: pong,
listUsers: pong,
API_VERSION: 'v2',
MAX_PAGE_SIZE: 100,
ENABLE_CACHE: true,
DEPRECATED: null,
OPTIONAL_FEATURE: undefined,
} as any

const defaultOptions = {
errorMap: {},
middlewares: [],
prefix: undefined,
tags: [],
dedupeLeadingMiddlewares: false,
} as const

describe('enhanceRouter', () => {
it('enhances procedures and passes through primitive exports', () => {
const enhanced = enhanceRouter(moduleWithPrimitives, defaultOptions)
expect(enhanced.getUser['~orpc']).toBeDefined()
expect(enhanced.listUsers['~orpc']).toBeDefined()
expect(enhanced.API_VERSION).toBe('v2')
expect(enhanced.MAX_PAGE_SIZE).toBe(100)
expect(enhanced.ENABLE_CACHE).toBe(true)
})

it('handles single-character string exports without stack overflow', () => {
// Single-char strings are the worst case: for...in on 'v' yields key '0',
// and 'v'[0] === 'v' creates an infinite loop
const moduleWithFlag = { getUser: pong, v: 'v' } as any
expect(() => enhanceRouter(moduleWithFlag, defaultOptions)).not.toThrow()
})
})

describe('traverseContractProcedures', () => {
it('traverses procedures and skips primitive exports', () => {
// null/undefined are excluded here because getHiddenRouterContract
// is called before the type guard and does not handle null
const moduleWithStringExports = {
getUser: pong,
listUsers: pong,
API_VERSION: 'v2',
MAX_PAGE_SIZE: 100,
ENABLE_CACHE: true,
} as any
const callback = vi.fn()
traverseContractProcedures({ router: moduleWithStringExports, path: [] }, callback)
expect(callback).toHaveBeenCalledTimes(2)
expect(callback).toHaveBeenCalledWith({ contract: pong, path: ['getUser'] })
expect(callback).toHaveBeenCalledWith({ contract: pong, path: ['listUsers'] })
})
})

describe('unlazyRouter', () => {
it('resolves procedures and preserves primitive exports', async () => {
const result = await unlazyRouter(moduleWithPrimitives)
expect(result.getUser).toEqual(pong)
expect(result.listUsers).toEqual(pong)
expect(result.API_VERSION).toBe('v2')
expect(result.MAX_PAGE_SIZE).toBe(100)
})
})
})
10 changes: 9 additions & 1 deletion packages/server/src/router-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ export function enhanceRouter<
return enhanced as any
}

if (typeof router !== 'object' || router === null) {
return router as any
}

const enhanced = {} as Record<string, any>

for (const key in router) {
Expand Down Expand Up @@ -206,7 +210,7 @@ export function traverseContractProcedures(
})
}

else {
else if (typeof currentRouter === 'object' && currentRouter !== null) {
for (const key in currentRouter) {
traverseContractProcedures(
{
Expand Down Expand Up @@ -254,6 +258,10 @@ export async function unlazyRouter<T extends AnyRouter>(router: T): Promise<Unla
return router as any
}

if (typeof router !== 'object' || router === null) {
return router as any
}

const unlazied = {} as Record<string, any>

for (const key in router) {
Expand Down
Loading