Skip to content

Commit 544b973

Browse files
authored
feat(nest, contract): prefer import populateContractRouterPaths from @orpc/contract (#1312)
Fixes #1311 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added automatic path population for nested contract router structures, simplifying route configuration without manual path setup. * **Improvements** * Enhanced error messaging with clearer guidance on using available utilities. * **Documentation** * Updated code examples with optimized import patterns. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent cc1a1aa commit 544b973

File tree

9 files changed

+121
-110
lines changed

9 files changed

+121
-110
lines changed

apps/content/docs/openapi/integrations/implement-contract-in-nest.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@ Before implementation, define your oRPC contract. This process is consistent wit
6464
::: details Example Contract
6565

6666
```ts
67-
import { populateContractRouterPaths } from '@orpc/nest'
68-
import { oc } from '@orpc/contract'
67+
import { oc, populateContractRouterPaths } from '@orpc/contract'
6968
import * as z from 'zod'
7069

7170
export const PlanetSchema = z.object({

packages/contract/src/router-utils.test-d.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import type { baseErrorMap, BaseMeta, inputSchema, outputSchema, router } from '../tests/shared'
1+
import type { BaseMeta } from '../tests/shared'
22
import type { MergedErrorMap } from './error'
33
import type { Meta } from './meta'
44
import type { ContractProcedure } from './procedure'
5-
import type { EnhancedContractRouter } from './router-utils'
5+
import type { EnhancedContractRouter, PopulatedContractRouterPaths } from './router-utils'
66
import type { Schema } from './schema'
7+
import { baseErrorMap, inputSchema, outputSchema, router } from '../tests/shared'
8+
import { oc } from './builder'
79

810
it('EnhancedContractRouter', () => {
911
const enhanced = {} as EnhancedContractRouter<typeof router, { INVALID: { status: number }, BASE2: { message: string } }>
@@ -44,3 +46,23 @@ it('EnhancedContractRouter', () => {
4446
>
4547
>()
4648
})
49+
50+
it('PopulatedContractRouterPaths', () => {
51+
expectTypeOf<PopulatedContractRouterPaths<typeof router>>().toEqualTypeOf(router)
52+
53+
const ping = oc
54+
.$meta({ meta: true })
55+
.input(inputSchema)
56+
.errors(baseErrorMap)
57+
.output(outputSchema)
58+
.route({ path: '/ping' })
59+
60+
expectTypeOf<PopulatedContractRouterPaths<typeof ping>>().toEqualTypeOf<
61+
ContractProcedure<
62+
typeof inputSchema,
63+
typeof outputSchema,
64+
typeof baseErrorMap & Record<never, never>,
65+
{ meta: boolean } & Record<never, never>
66+
>
67+
>()
68+
})

packages/contract/src/router-utils.test.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { ping, pong, router } from '../tests/shared'
1+
import { inputSchema, outputSchema, ping, pong, router } from '../tests/shared'
2+
import { oc } from './builder'
23
import { isContractProcedure } from './procedure'
34
import { enhanceRoute } from './route'
4-
import { enhanceContractRouter, getContractRouter, minifyContractRouter } from './router-utils'
5+
import { enhanceContractRouter, getContractRouter, minifyContractRouter, populateContractRouterPaths } from './router-utils'
56

67
it('getContractRouter', () => {
78
expect(getContractRouter(router, [])).toEqual(router)
@@ -72,3 +73,29 @@ it('minifyContractRouter', () => {
7273
expect((minified as any).nested.pong).toSatisfy(isContractProcedure)
7374
expect((minified as any).nested.pong).toEqual(minifiedPong)
7475
})
76+
77+
it('populateContractRouterPaths', () => {
78+
const contract = {
79+
ping: oc.input(inputSchema),
80+
pong: oc.route({
81+
path: '/pong/{id}',
82+
}),
83+
nested: {
84+
ping: oc.output(outputSchema),
85+
pong: oc.route({
86+
path: '/pong2/{id}',
87+
}),
88+
},
89+
}
90+
91+
const populated = populateContractRouterPaths(contract)
92+
93+
expect(populated.pong['~orpc'].route.path).toBe('/pong/{id}')
94+
expect(populated.nested.pong['~orpc'].route.path).toBe('/pong2/{id}')
95+
96+
expect(populated.ping['~orpc'].route.path).toBe('/ping')
97+
expect(populated.ping['~orpc'].inputSchema).toBe(inputSchema)
98+
99+
expect(populated.nested.ping['~orpc'].route.path).toBe('/nested/ping')
100+
expect(populated.nested.ping['~orpc'].outputSchema).toBe(outputSchema)
101+
})

packages/contract/src/router-utils.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { ErrorMap, MergedErrorMap } from './error'
22
import type { AnyContractProcedure } from './procedure'
33
import type { EnhanceRouteOptions } from './route'
44
import type { AnyContractRouter } from './router'
5+
import { toHttpPath } from '@orpc/client/standard'
6+
import { toArray } from '@orpc/shared'
57
import { mergeErrorMap } from './error'
68
import { ContractProcedure, isContractProcedure } from './procedure'
79
import { enhanceRoute } from './route'
@@ -89,3 +91,48 @@ export function minifyContractRouter(router: AnyContractRouter): AnyContractRout
8991

9092
return json
9193
}
94+
95+
export type PopulatedContractRouterPaths<T extends AnyContractRouter>
96+
= T extends ContractProcedure<infer UInputSchema, infer UOutputSchema, infer UErrors, infer UMeta>
97+
? ContractProcedure<UInputSchema, UOutputSchema, UErrors, UMeta>
98+
: {
99+
[K in keyof T]: T[K] extends AnyContractRouter ? PopulatedContractRouterPaths<T[K]> : never
100+
}
101+
102+
export interface PopulateContractRouterPathsOptions {
103+
path?: readonly string[]
104+
}
105+
106+
/**
107+
* Automatically populates missing route paths using the router's nested keys.
108+
*
109+
* Constructs paths by joining router keys with `/`.
110+
* Useful for NestJS integration that require explicit route paths.
111+
*
112+
* @see {@link https://orpc.dev/docs/openapi/integrations/implement-contract-in-nest#define-your-contract NestJS Implement Contract Docs}
113+
*/
114+
export function populateContractRouterPaths<T extends AnyContractRouter>(router: T, options: PopulateContractRouterPathsOptions = {}): PopulatedContractRouterPaths<T> {
115+
const path = toArray(options.path)
116+
117+
if (isContractProcedure(router)) {
118+
if (router['~orpc'].route.path === undefined) {
119+
return new ContractProcedure({
120+
...router['~orpc'],
121+
route: {
122+
...router['~orpc'].route,
123+
path: toHttpPath(path),
124+
},
125+
}) as any
126+
}
127+
128+
return router as any
129+
}
130+
131+
const populated: Record<string, any> = {}
132+
133+
for (const key in router) {
134+
populated[key] = populateContractRouterPaths(router[key]!, { ...options, path: [...path, key] })
135+
}
136+
137+
return populated as any
138+
}

packages/nest/src/implement.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export function Implement<T extends ContractRouter<any>>(
4949
throw new Error(`
5050
@Implement decorator requires contract to have a 'path'.
5151
Please define one using 'path' property on the '.route' method.
52-
Or use "populateContractRouterPaths" utility to automatically fill in any missing paths.
52+
Or use "populateContractRouterPaths" from "@orpc/contract" utility to automatically fill in any missing paths.
5353
`)
5454
}
5555

packages/nest/src/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@ export { Implement as Impl } from './implement'
88
export * from './module'
99
export * from './utils'
1010

11+
export {
12+
/**
13+
* @deprecated Import from `@orpc/contract` instead for better compatibility.
14+
*/
15+
populateContractRouterPaths,
16+
} from '@orpc/contract'
17+
export type {
18+
/**
19+
* @deprecated Import from `@orpc/contract` instead for better compatibility.
20+
*/
21+
PopulateContractRouterPathsOptions,
22+
/**
23+
* @deprecated Import from `@orpc/contract` instead for better compatibility.
24+
*/
25+
PopulatedContractRouterPaths,
26+
} from '@orpc/contract'
27+
1128
export { onError, onFinish, onStart, onSuccess, ORPCError } from '@orpc/server'
1229
export type {
1330
ImplementedProcedure,

packages/nest/src/utils.test-d.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

packages/nest/src/utils.test.ts

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { oc } from '@orpc/contract'
2-
import { inputSchema, outputSchema } from '../../contract/tests/shared'
3-
import { populateContractRouterPaths, toNestPattern } from './utils'
1+
import { toNestPattern } from './utils'
42

53
it('toNestPattern', () => {
64
expect(toNestPattern('/ping')).toBe('/ping')
@@ -10,29 +8,3 @@ it('toNestPattern', () => {
108

119
expect(toNestPattern('/{id}/name{name}')).toBe('/:id/name{name}')
1210
})
13-
14-
it('populateContractRouterPaths', () => {
15-
const contract = {
16-
ping: oc.input(inputSchema),
17-
pong: oc.route({
18-
path: '/pong/{id}',
19-
}),
20-
nested: {
21-
ping: oc.output(outputSchema),
22-
pong: oc.route({
23-
path: '/pong2/{id}',
24-
}),
25-
},
26-
}
27-
28-
const populated = populateContractRouterPaths(contract)
29-
30-
expect(populated.pong['~orpc'].route.path).toBe('/pong/{id}')
31-
expect(populated.nested.pong['~orpc'].route.path).toBe('/pong2/{id}')
32-
33-
expect(populated.ping['~orpc'].route.path).toBe('/ping')
34-
expect(populated.ping['~orpc'].inputSchema).toBe(inputSchema)
35-
36-
expect(populated.nested.ping['~orpc'].route.path).toBe('/nested/ping')
37-
expect(populated.nested.ping['~orpc'].outputSchema).toBe(outputSchema)
38-
})

packages/nest/src/utils.ts

Lines changed: 1 addition & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,8 @@
1-
import type { AnyContractRouter, HTTPPath } from '@orpc/contract'
2-
import { toHttpPath } from '@orpc/client/standard'
3-
import { ContractProcedure, isContractProcedure } from '@orpc/contract'
1+
import type { HTTPPath } from '@orpc/contract'
42
import { standardizeHTTPPath } from '@orpc/openapi-client/standard'
5-
import { toArray } from '@orpc/shared'
63

74
export function toNestPattern(path: HTTPPath): string {
85
return standardizeHTTPPath(path)
96
.replace(/\/\{\+([^}]+)\}/g, '/*$1')
107
.replace(/\/\{([^}]+)\}/g, '/:$1')
118
}
12-
13-
export type PopulatedContractRouterPaths<T extends AnyContractRouter>
14-
= T extends ContractProcedure<infer UInputSchema, infer UOutputSchema, infer UErrors, infer UMeta>
15-
? ContractProcedure<UInputSchema, UOutputSchema, UErrors, UMeta>
16-
: {
17-
[K in keyof T]: T[K] extends AnyContractRouter ? PopulatedContractRouterPaths<T[K]> : never
18-
}
19-
20-
export interface PopulateContractRouterPathsOptions {
21-
path?: readonly string[]
22-
}
23-
24-
/**
25-
* populateContractRouterPaths is completely optional,
26-
* because the procedure's path is required for NestJS implementation.
27-
* This utility automatically populates any missing paths
28-
* Using the router's keys + `/`.
29-
*
30-
* @see {@link https://orpc.dev/docs/openapi/integrations/implement-contract-in-nest#define-your-contract NestJS Implement Contract Docs}
31-
*/
32-
export function populateContractRouterPaths<T extends AnyContractRouter>(router: T, options: PopulateContractRouterPathsOptions = {}): PopulatedContractRouterPaths<T> {
33-
const path = toArray(options.path)
34-
35-
if (isContractProcedure(router)) {
36-
if (router['~orpc'].route.path === undefined) {
37-
return new ContractProcedure({
38-
...router['~orpc'],
39-
route: {
40-
...router['~orpc'].route,
41-
path: toHttpPath(path),
42-
},
43-
}) as any
44-
}
45-
46-
return router as any
47-
}
48-
49-
const populated: Record<string, any> = {}
50-
51-
for (const key in router) {
52-
populated[key] = populateContractRouterPaths(router[key]!, { ...options, path: [...path, key] })
53-
}
54-
55-
return populated as any
56-
}

0 commit comments

Comments
 (0)