Skip to content

Commit 9840356

Browse files
authored
feat(openapi): override operationId and spec callback for extending operations (#818)
- Allow overriding auto-generated operationId in route configuration - Accept route.spec as callback function for extending operation objects - Enable dynamic modification of OpenAPI operation specifications <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for explicitly setting an operation ID for endpoints in OpenAPI integration. * Enabled dynamic extension of OpenAPI operation objects using a callback function for advanced customization. * **Documentation** * Enhanced OpenAPI documentation with clearer explanations and new examples for setting operation IDs and extending operation objects. * **Tests** * Added new tests to verify dynamic extension and customization of OpenAPI operation objects. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 8646d1e commit 9840356

4 files changed

Lines changed: 62 additions & 10 deletions

File tree

apps/content/docs/openapi/openapi-specification.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,13 @@ You can enrich your API documentation by specifying operation metadata using the
168168
```ts
169169
const ping = os
170170
.route({
171+
operationId: 'ping', // override auto-generated operationId
171172
summary: 'the summary',
172173
description: 'the description',
173174
deprecated: false,
174175
tags: ['tag'],
175176
successDescription: 'the success description',
176-
spec: { // override entire auto-generated operation object
177+
spec: { // override entire auto-generated operation object, can also be a callback for extending
177178
operationId: 'customOperationId',
178179
tags: ['tag'],
179180
summary: 'the summary',
@@ -204,11 +205,22 @@ const router = os.tag('planets').router({
204205

205206
### Customizing Operation Objects
206207

207-
You can also extend the operation object using the `.spec` helper for an `error` or `middleware`:
208+
You can also extend the operation object by defining `route.spec` as a callback, or by using `oo.spec` in errors or middleware:
208209

209210
```ts
210211
import { oo } from '@orpc/openapi'
211212

213+
// Using `route.spec` as a callback
214+
const procedure = os
215+
.route({
216+
spec: spec => ({
217+
...spec,
218+
security: [{ 'api-key': [] }],
219+
}),
220+
})
221+
.handler(() => 'Hello, World!')
222+
223+
// With errors
212224
const base = os.errors({
213225
UNAUTHORIZED: oo.spec({
214226
data: z.any(),
@@ -217,15 +229,14 @@ const base = os.errors({
217229
})
218230
})
219231

220-
// OR in middleware
221-
232+
// With middleware
222233
const requireAuth = oo.spec(
223234
os.middleware(async ({ next, errors }) => {
224235
throw new ORPCError('UNAUTHORIZED')
225236
return next()
226237
}),
227238
{
228-
security: [{ 'api-key': [] }]
239+
security: [{ 'api-key': [] }],
229240
}
230241
)
231242
```

packages/contract/src/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ export interface Route {
2121
*/
2222
path?: HTTPPath
2323

24+
/**
25+
* The operation ID of the endpoint.
26+
* This option is typically relevant when integrating with OpenAPI.
27+
*
28+
* @default Concatenation of router segments
29+
*/
30+
operationId?: string
31+
2432
/**
2533
* The summary of the procedure.
2634
* This option is typically relevant when integrating with OpenAPI.
@@ -127,7 +135,7 @@ export interface Route {
127135
*
128136
* @see {@link https://orpc.unnoq.com/docs/openapi/openapi-specification#operation-metadata Operation Metadata Docs}
129137
*/
130-
spec?: OpenAPI.OperationObject
138+
spec?: OpenAPI.OperationObject | ((current: OpenAPI.OperationObject) => OpenAPI.OperationObject)
131139
}
132140

133141
export function mergeRoute(a: Route, b: Route): Route {

packages/openapi/src/openapi-generator.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const routeTests: TestCase[] = [
5757
{
5858
name: 'metadata',
5959
contract: oc.route({
60+
operationId: 'customOperationId',
6061
tags: ['planets'],
6162
summary: 'the summary',
6263
description: 'the description',
@@ -67,6 +68,7 @@ const routeTests: TestCase[] = [
6768
expected: {
6869
'/': {
6970
post: expect.objectContaining({
71+
operationId: 'customOperationId',
7072
tags: ['planets'],
7173
summary: 'the summary',
7274
description: 'the description',
@@ -896,6 +898,33 @@ const customOperationTests: TestCase[] = [
896898
},
897899
},
898900
},
901+
{
902+
name: 'extend operation object',
903+
contract: oc
904+
.route({
905+
spec: spec => ({
906+
...spec,
907+
operationId: 'customOperationId',
908+
summary: '__OVERRIDE__',
909+
}),
910+
})
911+
.errors({
912+
TEST: customOpenAPIOperation({}, { security: [{ bearerAuth: [] }] }),
913+
})
914+
.input(z.object({ id: z.string() }))
915+
.output(z.object({ name: z.string() })),
916+
expected: {
917+
'/': {
918+
post: {
919+
operationId: 'customOperationId',
920+
summary: '__OVERRIDE__',
921+
security: [{ bearerAuth: [] }],
922+
requestBody: expect.any(Object),
923+
responses: expect.any(Object),
924+
},
925+
},
926+
},
927+
},
899928
]
900929

901930
it.each([

packages/openapi/src/openapi-generator.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export class OpenAPIGenerator {
119119
const errors: string[] = []
120120

121121
for (const { contract, path } of contracts) {
122-
const operationId = path.join('.')
122+
const stringPath = path.join('.')
123123

124124
try {
125125
const def = contract['~orpc']
@@ -129,12 +129,12 @@ export class OpenAPIGenerator {
129129

130130
let operationObjectRef: OpenAPI.OperationObject
131131

132-
if (def.route.spec !== undefined) {
132+
if (def.route.spec !== undefined && typeof def.route.spec !== 'function') {
133133
operationObjectRef = def.route.spec
134134
}
135135
else {
136136
operationObjectRef = {
137-
operationId,
137+
operationId: def.route.operationId ?? stringPath,
138138
summary: def.route.summary,
139139
description: def.route.description,
140140
deprecated: def.route.deprecated,
@@ -146,6 +146,10 @@ export class OpenAPIGenerator {
146146
await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema)
147147
}
148148

149+
if (typeof def.route.spec === 'function') {
150+
operationObjectRef = def.route.spec(operationObjectRef)
151+
}
152+
149153
doc.paths ??= {}
150154
doc.paths[httpPath] ??= {}
151155
doc.paths[httpPath][method] = applyCustomOpenAPIOperation(operationObjectRef, contract) as any
@@ -156,7 +160,7 @@ export class OpenAPIGenerator {
156160
}
157161

158162
errors.push(
159-
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${operationId}\n${e.message}`,
163+
`[OpenAPIGenerator] Error occurred while generating OpenAPI for procedure at path: ${stringPath}\n${e.message}`,
160164
)
161165
}
162166
}

0 commit comments

Comments
 (0)