Skip to content

Commit

Permalink
prefix lazy
Browse files Browse the repository at this point in the history
  • Loading branch information
unnoq committed Jan 30, 2025
1 parent 2b953c8 commit 76fc5ca
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 48 deletions.
99 changes: 99 additions & 0 deletions packages/server/src/hidden.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { oc } from '@orpc/contract'
import { router } from '../tests/shared'
import { deepSetLazyRouterPrefix, getLazyRouterPrefix, getRouterContract, setRouterContract } from './hidden'
import { lazy, unlazy } from './lazy'
import { createAccessibleLazyRouter } from './router-accessible-lazy'

describe('setRouterContract', () => {
const ping = oc.route({})
const baseContract = { ping }
const nestedContract = { ping, nested: { ping } }

it('sets contract on empty object', () => {
const obj = {}
const router = setRouterContract(obj, baseContract)
expect(getRouterContract(router)).toBe(baseContract)
})

it('preserves original object properties', () => {
const obj = { existingProp: 'value' } as any
const router = setRouterContract(obj, baseContract)
expect(router.existingProp).toBe('value')
expect(getRouterContract(router)).toBe(baseContract)
})

it('handles nested contracts', () => {
const obj = { nested: { value: 42 } } as any
const router = setRouterContract(obj, nestedContract)
expect(getRouterContract(router)).toBe(nestedContract)
expect(router.nested.value).toBe(42)
expect(getRouterContract(router.nested)).toBeUndefined()
})

it('allows contract overwriting', () => {
const obj = {}
const router1 = setRouterContract(obj, baseContract)
const router2 = setRouterContract(router1, nestedContract)
expect(getRouterContract(router2)).toBe(nestedContract)
})
})

describe('deepSetLazyRouterPrefix', () => {
it('prefix on root and nested lazy', async () => {
const obj = createAccessibleLazyRouter(lazy(() => Promise.resolve({
default: {
l1: {
l2: {
l3: { value: router.ping },
},
},
},
})))
const prefixed = deepSetLazyRouterPrefix(obj, '/api')
expect(getLazyRouterPrefix(prefixed)).toBe('/api')
expect(getLazyRouterPrefix(prefixed.l1)).toBe('/api')
expect(getLazyRouterPrefix(prefixed.l1.l2)).toBe('/api')
expect(getLazyRouterPrefix(prefixed.l1.l2.l3)).toBe('/api')
expect(getLazyRouterPrefix(prefixed.l1.l2.l3.value)).toBe('/api')
expect(await unlazy(prefixed.l1.l2.l3.value)).toEqual(await unlazy(router.ping))
})

it('can override old prefix', async () => {
const obj = createAccessibleLazyRouter(lazy(() => Promise.resolve({
default: {
l1: {
l2: {
l3: { value: router.ping },
},
},
},
})))

const prefixed1 = deepSetLazyRouterPrefix(obj, '/api')
const prefixed2 = deepSetLazyRouterPrefix(prefixed1, '/v2')

expect(getLazyRouterPrefix(prefixed1)).toBe('/api')
expect(getLazyRouterPrefix(prefixed1.l1.l2.l3.value)).toBe('/api')
expect(getLazyRouterPrefix(prefixed2)).toBe('/v2')
expect(getLazyRouterPrefix(prefixed2.l1.l2.l3.value)).toBe('/v2')

expect(await unlazy(obj)).toEqual(await unlazy(obj))
})

it('not prefix on non-lazy', () => {
const obj = {
l1: {
l2: {
l3: { value: router.ping },
value: 1, // not lazy
},
},
} as any

const prefixed = deepSetLazyRouterPrefix(obj, '/api') as any

expect(getLazyRouterPrefix(prefixed)).toBe('/api')
expect(getLazyRouterPrefix(prefixed.l1)).toBe(undefined)
expect(getLazyRouterPrefix(prefixed.l1.l2.value)).toBe(undefined)
})
})
45 changes: 45 additions & 0 deletions packages/server/src/hidden.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { AnyContractRouter, ContractRouter, HTTPPath } from '@orpc/contract'
import type { Lazy } from './lazy'
import type { AnyRouter } from './router'
import { isLazy } from './lazy'

const ROUTER_CONTRACT_SYMBOL = Symbol('ORPC_ROUTER_CONTRACT')

export function setRouterContract<T extends AnyRouter>(obj: T, contract: AnyContractRouter): T {
return new Proxy(obj, {
get(target, key) {
if (key === ROUTER_CONTRACT_SYMBOL) {
return contract
}

return Reflect.get(target, key)
},
})
}

export function getRouterContract(obj: object): ContractRouter<any> | undefined {
return (obj as any)[ROUTER_CONTRACT_SYMBOL]
}

const LAZY_ROUTER_PREFIX_SYMBOL = Symbol('ORPC_LAZY_ROUTER_PREFIX')

export function deepSetLazyRouterPrefix<T extends Lazy<any>>(router: T, prefix: HTTPPath): T {
return new Proxy(router, {
get(target, key) {
if (key !== LAZY_ROUTER_PREFIX_SYMBOL) {
const val = Reflect.get(target, key)
if (isLazy(val)) {
return deepSetLazyRouterPrefix(val, prefix)
}

return val
}

return prefix
},
})
}

export function getLazyRouterPrefix(obj: Lazy<any>): HTTPPath | undefined {
return (obj as any)[LAZY_ROUTER_PREFIX_SYMBOL]
}
1 change: 1 addition & 0 deletions packages/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './builder'
export * from './builder-variants'
export * from './config'
export * from './context'
export * from './hidden'
export * from './lazy'
export * from './lazy-utils'
export * from './middleware'
Expand Down
9 changes: 2 additions & 7 deletions packages/server/src/lazy-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { ping } from '../tests/shared'
import { isLazy, lazy, unlazy } from './lazy'
import { createLazyProcedureFormAnyLazy, flatLazy, prefixLazyMeta } from './lazy-utils'
import { createLazyProcedureFormAnyLazy, flatLazy } from './lazy-utils'

it('flatLazy', () => {
const lazied = lazy(() => Promise.resolve({
default: lazy(() => Promise.resolve({
default: lazy(() => Promise.resolve({ default: ping })),
})),
}), { prefix: '/test' })
}))

const flatten = flatLazy(lazied)
expect(flatten).toSatisfy(isLazy)
Expand Down Expand Up @@ -38,8 +38,3 @@ describe('createLazyProcedureFormAnyLazy', () => {
expect(unlazy(lazyProcedure)).resolves.toEqual({ default: ping })
})
})

it('prefixLazyMeta', () => {
expect(prefixLazyMeta({}, '/test')).toEqual({ prefix: '/test' })
expect(prefixLazyMeta({ prefix: '/test1' }, '/test2')).toEqual({ prefix: '/test2/test1' })
})
16 changes: 3 additions & 13 deletions packages/server/src/lazy-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { HTTPPath } from '@orpc/contract'
import type { Lazy, LazyMeta } from './lazy'
import { getLazyMeta, isLazy, lazy, unlazy } from './lazy'
import type { Lazy } from './lazy'
import { isLazy, lazy, unlazy } from './lazy'
import { type AnyProcedure, isProcedure } from './procedure'

export type FlattenLazy<T> = T extends Lazy<infer U>
Expand All @@ -22,7 +21,7 @@ export function flatLazy<T extends Lazy<any>>(lazied: T): FlattenLazy<T> {
return current
}

return lazy(flattenLoader, getLazyMeta(lazied)) as any
return lazy(flattenLoader) as any
}

export function createLazyProcedureFormAnyLazy(lazied: Lazy<any>): Lazy<AnyProcedure> {
Expand All @@ -42,12 +41,3 @@ export function createLazyProcedureFormAnyLazy(lazied: Lazy<any>): Lazy<AnyProce

return lazyProcedure
}

export function prefixLazyMeta(meta: LazyMeta, prefix: HTTPPath): LazyMeta {
return {
...meta,
prefix: meta.prefix
? `${prefix}${meta.prefix}`
: prefix,
}
}
27 changes: 7 additions & 20 deletions packages/server/src/lazy.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import type { HTTPPath } from '@orpc/contract'

export const LAZY_SYMBOL: unique symbol = Symbol('ORPC_LAZY')
export const LAZY_LOADER_SYMBOL: unique symbol = Symbol('ORPC_LAZY_LOADER')

export interface LazyMeta {
prefix?: HTTPPath
}

export interface Lazy<T> {
[LAZY_SYMBOL]: {
loader(): Promise<{ default: T }>
meta: LazyMeta
}
[LAZY_LOADER_SYMBOL](): Promise<{ default: T }>
}

export type Lazyable<T> = T | Lazy<T>
Expand All @@ -19,31 +16,21 @@ export interface LazyOptions {
prefix?: HTTPPath
}

export function lazy<T>(loader: () => Promise<{ default: T }>, meta: LazyMeta = {}): Lazy<T> {
export function lazy<T>(loader: () => Promise<{ default: T }>): Lazy<T> {
return {
[LAZY_SYMBOL]: {
loader,
meta,
},
[LAZY_LOADER_SYMBOL]: loader,
}
}

export function isLazy(item: unknown): item is Lazy<any> {
return (
(typeof item === 'object' || typeof item === 'function')
&& item !== null
&& LAZY_SYMBOL in item
&& typeof item[LAZY_SYMBOL] === 'object'
&& item[LAZY_SYMBOL] !== null
&& 'loader' in item[LAZY_SYMBOL]
&& 'meta' in item[LAZY_SYMBOL]
&& LAZY_LOADER_SYMBOL in item
&& typeof item[LAZY_LOADER_SYMBOL] === 'function'
)
}

export function unlazy<T extends Lazyable<any>>(lazied: T): Promise<{ default: T extends Lazy<infer U> ? U : T }> {
return isLazy(lazied) ? lazied[LAZY_SYMBOL].loader() : Promise.resolve({ default: lazied })
}

export function getLazyMeta(lazy: Lazy<any>): LazyMeta {
return lazy[LAZY_SYMBOL].meta
return isLazy(lazied) ? lazied[LAZY_LOADER_SYMBOL]() : Promise.resolve({ default: lazied })
}
7 changes: 7 additions & 0 deletions packages/server/src/router.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ping, pingMiddleware, pong, router } from '../tests/shared'
import { getLazyRouterPrefix } from './hidden'
import { isLazy, unlazy } from './lazy'
import { isProcedure } from './procedure'
import { adaptRouter, getRouterChild } from './router'
Expand Down Expand Up @@ -52,14 +53,20 @@ it('adaptRouter', () => {

expect(adapted.ping).toSatisfy(isLazy)
expect(unlazy(adapted.ping)).resolves.toSatisfy(satisfyAdaptedPing)
expect(getLazyRouterPrefix(adapted.ping)).toBe('/adapt')

expect(adapted.nested).toSatisfy(isLazy)
expect(getLazyRouterPrefix(adapted.nested)).toBe('/adapt')

expect(adapted.nested.ping).toSatisfy(isLazy)
expect(unlazy(adapted.nested.ping)).resolves.toSatisfy(satisfyAdaptedPing)
expect(getLazyRouterPrefix(adapted.nested.ping)).toBe('/adapt')

expect({ default: adapted.pong }).toSatisfy(satisfyAdaptedPong)

expect(adapted.nested.pong).toSatisfy(isLazy)
expect(unlazy(adapted.nested.pong)).resolves.toSatisfy(satisfyAdaptedPong)
expect(getLazyRouterPrefix(adapted.nested.pong)).toBe('/adapt')
})

it('getRouterChild', () => {
Expand Down
20 changes: 12 additions & 8 deletions packages/server/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import type { Context } from './context'
import type { Lazy, Lazyable } from './lazy'
import type { AnyMiddleware } from './middleware'
import type { AnyProcedure } from './procedure'
import { adaptRoute, mergeErrorMap } from '@orpc/contract'
import { getLazyMeta, isLazy, lazy, unlazy } from './lazy'
import { flatLazy, prefixLazyMeta } from './lazy-utils'
import { adaptRoute, mergeErrorMap, mergePrefix } from '@orpc/contract'
import { deepSetLazyRouterPrefix, getLazyRouterPrefix } from './hidden'
import { isLazy, lazy, unlazy } from './lazy'
import { flatLazy } from './lazy-utils'
import { mergeMiddlewares } from './middleware-utils'
import { isProcedure, Procedure } from './procedure'
import { type AccessibleLazyRouter, createAccessibleLazyRouter } from './router-accessible-lazy'
Expand Down Expand Up @@ -83,18 +84,21 @@ export function adaptRouter<
options: AdaptRouterOptions<TErrorMap>,
): AdaptedRouter<TRouter, TInitialContext, TErrorMap> {
if (isLazy(router)) {
const lazyMeta = options.prefix
? prefixLazyMeta(getLazyMeta(router), options.prefix)
: getLazyMeta(router)

const adapted = lazy(async () => {
const unlaziedRouter = (await unlazy(router)).default
const adapted = adaptRouter(unlaziedRouter, options)
return { default: adapted }
}, lazyMeta)
})

const accessible = createAccessibleLazyRouter(adapted)

const currentPrefix = getLazyRouterPrefix(router)
const prefix = currentPrefix ? mergePrefix(options.prefix, currentPrefix) : options.prefix

if (prefix) {
return deepSetLazyRouterPrefix(accessible, prefix) as any
}

return accessible as any
}

Expand Down

0 comments on commit 76fc5ca

Please sign in to comment.