diff --git a/apps/content/docs/openapi/integrations/implement-contract-in-nest.md b/apps/content/docs/openapi/integrations/implement-contract-in-nest.md index 494e688e9..84d6a6f3e 100644 --- a/apps/content/docs/openapi/integrations/implement-contract-in-nest.md +++ b/apps/content/docs/openapi/integrations/implement-contract-in-nest.md @@ -64,8 +64,7 @@ Before implementation, define your oRPC contract. This process is consistent wit ::: details Example Contract ```ts -import { populateContractRouterPaths } from '@orpc/nest' -import { oc } from '@orpc/contract' +import { oc, populateContractRouterPaths } from '@orpc/contract' import * as z from 'zod' export const PlanetSchema = z.object({ diff --git a/packages/contract/src/router-utils.test-d.ts b/packages/contract/src/router-utils.test-d.ts index b06f30931..dc446a71c 100644 --- a/packages/contract/src/router-utils.test-d.ts +++ b/packages/contract/src/router-utils.test-d.ts @@ -1,9 +1,11 @@ -import type { baseErrorMap, BaseMeta, inputSchema, outputSchema, router } from '../tests/shared' +import type { BaseMeta } from '../tests/shared' import type { MergedErrorMap } from './error' import type { Meta } from './meta' import type { ContractProcedure } from './procedure' -import type { EnhancedContractRouter } from './router-utils' +import type { EnhancedContractRouter, PopulatedContractRouterPaths } from './router-utils' import type { Schema } from './schema' +import { baseErrorMap, inputSchema, outputSchema, router } from '../tests/shared' +import { oc } from './builder' it('EnhancedContractRouter', () => { const enhanced = {} as EnhancedContractRouter @@ -44,3 +46,23 @@ it('EnhancedContractRouter', () => { > >() }) + +it('PopulatedContractRouterPaths', () => { + expectTypeOf>().toEqualTypeOf(router) + + const ping = oc + .$meta({ meta: true }) + .input(inputSchema) + .errors(baseErrorMap) + .output(outputSchema) + .route({ path: '/ping' }) + + expectTypeOf>().toEqualTypeOf< + ContractProcedure< + typeof inputSchema, + typeof outputSchema, + typeof baseErrorMap & Record, + { meta: boolean } & Record + > + >() +}) diff --git a/packages/contract/src/router-utils.test.ts b/packages/contract/src/router-utils.test.ts index 5cfa86f77..13487c374 100644 --- a/packages/contract/src/router-utils.test.ts +++ b/packages/contract/src/router-utils.test.ts @@ -1,7 +1,8 @@ -import { ping, pong, router } from '../tests/shared' +import { inputSchema, outputSchema, ping, pong, router } from '../tests/shared' +import { oc } from './builder' import { isContractProcedure } from './procedure' import { enhanceRoute } from './route' -import { enhanceContractRouter, getContractRouter, minifyContractRouter } from './router-utils' +import { enhanceContractRouter, getContractRouter, minifyContractRouter, populateContractRouterPaths } from './router-utils' it('getContractRouter', () => { expect(getContractRouter(router, [])).toEqual(router) @@ -72,3 +73,29 @@ it('minifyContractRouter', () => { expect((minified as any).nested.pong).toSatisfy(isContractProcedure) expect((minified as any).nested.pong).toEqual(minifiedPong) }) + +it('populateContractRouterPaths', () => { + const contract = { + ping: oc.input(inputSchema), + pong: oc.route({ + path: '/pong/{id}', + }), + nested: { + ping: oc.output(outputSchema), + pong: oc.route({ + path: '/pong2/{id}', + }), + }, + } + + const populated = populateContractRouterPaths(contract) + + expect(populated.pong['~orpc'].route.path).toBe('/pong/{id}') + expect(populated.nested.pong['~orpc'].route.path).toBe('/pong2/{id}') + + expect(populated.ping['~orpc'].route.path).toBe('/ping') + expect(populated.ping['~orpc'].inputSchema).toBe(inputSchema) + + expect(populated.nested.ping['~orpc'].route.path).toBe('/nested/ping') + expect(populated.nested.ping['~orpc'].outputSchema).toBe(outputSchema) +}) diff --git a/packages/contract/src/router-utils.ts b/packages/contract/src/router-utils.ts index 1c12763a1..428f1c91e 100644 --- a/packages/contract/src/router-utils.ts +++ b/packages/contract/src/router-utils.ts @@ -2,6 +2,8 @@ import type { ErrorMap, MergedErrorMap } from './error' import type { AnyContractProcedure } from './procedure' import type { EnhanceRouteOptions } from './route' import type { AnyContractRouter } from './router' +import { toHttpPath } from '@orpc/client/standard' +import { toArray } from '@orpc/shared' import { mergeErrorMap } from './error' import { ContractProcedure, isContractProcedure } from './procedure' import { enhanceRoute } from './route' @@ -89,3 +91,48 @@ export function minifyContractRouter(router: AnyContractRouter): AnyContractRout return json } + +export type PopulatedContractRouterPaths + = T extends ContractProcedure + ? ContractProcedure + : { + [K in keyof T]: T[K] extends AnyContractRouter ? PopulatedContractRouterPaths : never + } + +export interface PopulateContractRouterPathsOptions { + path?: readonly string[] +} + +/** + * Automatically populates missing route paths using the router's nested keys. + * + * Constructs paths by joining router keys with `/`. + * Useful for NestJS integration that require explicit route paths. + * + * @see {@link https://orpc.dev/docs/openapi/integrations/implement-contract-in-nest#define-your-contract NestJS Implement Contract Docs} + */ +export function populateContractRouterPaths(router: T, options: PopulateContractRouterPathsOptions = {}): PopulatedContractRouterPaths { + const path = toArray(options.path) + + if (isContractProcedure(router)) { + if (router['~orpc'].route.path === undefined) { + return new ContractProcedure({ + ...router['~orpc'], + route: { + ...router['~orpc'].route, + path: toHttpPath(path), + }, + }) as any + } + + return router as any + } + + const populated: Record = {} + + for (const key in router) { + populated[key] = populateContractRouterPaths(router[key]!, { ...options, path: [...path, key] }) + } + + return populated as any +} diff --git a/packages/nest/src/implement.ts b/packages/nest/src/implement.ts index 32b58804e..584b09b79 100644 --- a/packages/nest/src/implement.ts +++ b/packages/nest/src/implement.ts @@ -49,7 +49,7 @@ export function Implement>( throw new Error(` @Implement decorator requires contract to have a 'path'. Please define one using 'path' property on the '.route' method. - Or use "populateContractRouterPaths" utility to automatically fill in any missing paths. + Or use "populateContractRouterPaths" from "@orpc/contract" utility to automatically fill in any missing paths. `) } diff --git a/packages/nest/src/index.ts b/packages/nest/src/index.ts index 364742e65..49bde505a 100644 --- a/packages/nest/src/index.ts +++ b/packages/nest/src/index.ts @@ -8,6 +8,23 @@ export { Implement as Impl } from './implement' export * from './module' export * from './utils' +export { + /** + * @deprecated Import from `@orpc/contract` instead for better compatibility. + */ + populateContractRouterPaths, +} from '@orpc/contract' +export type { + /** + * @deprecated Import from `@orpc/contract` instead for better compatibility. + */ + PopulateContractRouterPathsOptions, + /** + * @deprecated Import from `@orpc/contract` instead for better compatibility. + */ + PopulatedContractRouterPaths, +} from '@orpc/contract' + export { onError, onFinish, onStart, onSuccess, ORPCError } from '@orpc/server' export type { ImplementedProcedure, diff --git a/packages/nest/src/utils.test-d.ts b/packages/nest/src/utils.test-d.ts deleted file mode 100644 index 555dd69a4..000000000 --- a/packages/nest/src/utils.test-d.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { ContractProcedure } from '@orpc/contract' -import type { PopulatedContractRouterPaths } from './utils' -import { oc } from '@orpc/contract' -import { expectTypeOf } from 'vitest' -import { baseErrorMap, inputSchema, outputSchema, router } from '../../contract/tests/shared' - -it('PopulatedContractRouterPaths', () => { - expectTypeOf>().toEqualTypeOf(router) - - const ping = oc - .$meta({ meta: true }) - .input(inputSchema) - .errors(baseErrorMap) - .output(outputSchema) - .route({ path: '/ping' }) - - expectTypeOf>().toEqualTypeOf< - ContractProcedure< - typeof inputSchema, - typeof outputSchema, - typeof baseErrorMap & Record, - { meta: boolean } & Record - > - >() -}) diff --git a/packages/nest/src/utils.test.ts b/packages/nest/src/utils.test.ts index 99703a863..9cbfbb172 100644 --- a/packages/nest/src/utils.test.ts +++ b/packages/nest/src/utils.test.ts @@ -1,6 +1,4 @@ -import { oc } from '@orpc/contract' -import { inputSchema, outputSchema } from '../../contract/tests/shared' -import { populateContractRouterPaths, toNestPattern } from './utils' +import { toNestPattern } from './utils' it('toNestPattern', () => { expect(toNestPattern('/ping')).toBe('/ping') @@ -10,29 +8,3 @@ it('toNestPattern', () => { expect(toNestPattern('/{id}/name{name}')).toBe('/:id/name{name}') }) - -it('populateContractRouterPaths', () => { - const contract = { - ping: oc.input(inputSchema), - pong: oc.route({ - path: '/pong/{id}', - }), - nested: { - ping: oc.output(outputSchema), - pong: oc.route({ - path: '/pong2/{id}', - }), - }, - } - - const populated = populateContractRouterPaths(contract) - - expect(populated.pong['~orpc'].route.path).toBe('/pong/{id}') - expect(populated.nested.pong['~orpc'].route.path).toBe('/pong2/{id}') - - expect(populated.ping['~orpc'].route.path).toBe('/ping') - expect(populated.ping['~orpc'].inputSchema).toBe(inputSchema) - - expect(populated.nested.ping['~orpc'].route.path).toBe('/nested/ping') - expect(populated.nested.ping['~orpc'].outputSchema).toBe(outputSchema) -}) diff --git a/packages/nest/src/utils.ts b/packages/nest/src/utils.ts index 1d2ac0e88..fb0a90e2d 100644 --- a/packages/nest/src/utils.ts +++ b/packages/nest/src/utils.ts @@ -1,56 +1,8 @@ -import type { AnyContractRouter, HTTPPath } from '@orpc/contract' -import { toHttpPath } from '@orpc/client/standard' -import { ContractProcedure, isContractProcedure } from '@orpc/contract' +import type { HTTPPath } from '@orpc/contract' import { standardizeHTTPPath } from '@orpc/openapi-client/standard' -import { toArray } from '@orpc/shared' export function toNestPattern(path: HTTPPath): string { return standardizeHTTPPath(path) .replace(/\/\{\+([^}]+)\}/g, '/*$1') .replace(/\/\{([^}]+)\}/g, '/:$1') } - -export type PopulatedContractRouterPaths - = T extends ContractProcedure - ? ContractProcedure - : { - [K in keyof T]: T[K] extends AnyContractRouter ? PopulatedContractRouterPaths : never - } - -export interface PopulateContractRouterPathsOptions { - path?: readonly string[] -} - -/** - * populateContractRouterPaths is completely optional, - * because the procedure's path is required for NestJS implementation. - * This utility automatically populates any missing paths - * Using the router's keys + `/`. - * - * @see {@link https://orpc.dev/docs/openapi/integrations/implement-contract-in-nest#define-your-contract NestJS Implement Contract Docs} - */ -export function populateContractRouterPaths(router: T, options: PopulateContractRouterPathsOptions = {}): PopulatedContractRouterPaths { - const path = toArray(options.path) - - if (isContractProcedure(router)) { - if (router['~orpc'].route.path === undefined) { - return new ContractProcedure({ - ...router['~orpc'], - route: { - ...router['~orpc'].route, - path: toHttpPath(path), - }, - }) as any - } - - return router as any - } - - const populated: Record = {} - - for (const key in router) { - populated[key] = populateContractRouterPaths(router[key]!, { ...options, path: [...path, key] }) - } - - return populated as any -}