Skip to content

Commit 0787cc6

Browse files
authored
feat(openapi): .spec for customizing openapi spec (#131)
1 parent 7b09958 commit 0787cc6

5 files changed

Lines changed: 128 additions & 36 deletions

File tree

packages/openapi/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
/** unnoq */
22

3+
import { setOperationExtender } from './openapi-operation-extender'
4+
35
export * from './json-serializer'
46
export * from './openapi'
57
export * from './openapi-content-builder'
68
export * from './openapi-generator'
9+
export * from './openapi-operation-extender'
710
export * from './openapi-parameters-builder'
811
export * from './openapi-path-parser'
912
export * from './schema'
1013
export * from './schema-converter'
1114
export * from './schema-utils'
1215
export * from './utils'
16+
17+
export const oo = {
18+
spec: setOperationExtender,
19+
}

packages/openapi/src/openapi-generator.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { type OpenAPI, OpenApiBuilder } from './openapi'
1111
import { OpenAPIContentBuilder, type PublicOpenAPIContentBuilder } from './openapi-content-builder'
1212
import { OpenAPIError } from './openapi-error'
1313
import { OpenAPIInputStructureParser } from './openapi-input-structure-parser'
14+
import { extendOperation } from './openapi-operation-extender'
1415
import { OpenAPIOutputStructureParser } from './openapi-output-structure-parser'
1516
import { OpenAPIParametersBuilder, type PublicOpenAPIParametersBuilder } from './openapi-parameters-builder'
1617
import { OpenAPIPathParser } from './openapi-path-parser'
@@ -245,8 +246,10 @@ export class OpenAPIGenerator {
245246
responses,
246247
}
247248

249+
const extendedOperation = extendOperation(operation, contract)
250+
248251
builder.addPath(httpPath, {
249-
[method.toLocaleLowerCase()]: operation,
252+
[method.toLocaleLowerCase()]: extendedOperation,
250253
})
251254
}
252255
catch (e) {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { AnyContractProcedure } from '@orpc/contract'
2+
import type { OpenAPI } from './openapi'
3+
import { isProcedure } from '@orpc/server'
4+
5+
const OPERATION_EXTENDER_SYMBOL = Symbol('ORPC_OPERATION_EXTENDER')
6+
7+
export type OverrideOperationValue = OpenAPI.OperationObject
8+
| ((current: OpenAPI.OperationObject, procedure: AnyContractProcedure) => OpenAPI.OperationObject)
9+
10+
export function setOperationExtender<T extends object>(o: T, extend: OverrideOperationValue): T {
11+
return new Proxy(o, {
12+
get(target, prop, receiver) {
13+
if (prop === OPERATION_EXTENDER_SYMBOL) {
14+
return extend
15+
}
16+
17+
return Reflect.get(target, prop, receiver)
18+
},
19+
})
20+
}
21+
22+
export function getOperationExtender(o: object): OverrideOperationValue | undefined {
23+
return (o as any)[OPERATION_EXTENDER_SYMBOL] as OverrideOperationValue
24+
}
25+
26+
export function extendOperation(operation: OpenAPI.OperationObject, procedure: AnyContractProcedure): OpenAPI.OperationObject {
27+
const operationExtenders: OverrideOperationValue[] = []
28+
29+
for (const errorItem of Object.values(procedure['~orpc'].errorMap)) {
30+
const maybeExtender = getOperationExtender(errorItem as any)
31+
32+
if (maybeExtender) {
33+
operationExtenders.push(maybeExtender)
34+
}
35+
}
36+
37+
if (isProcedure(procedure)) {
38+
for (const middleware of procedure['~orpc'].middlewares) {
39+
const maybeExtender = getOperationExtender(middleware)
40+
41+
if (maybeExtender) {
42+
operationExtenders.push(maybeExtender)
43+
}
44+
}
45+
}
46+
47+
let currentOperation = operation
48+
49+
for (const extender of operationExtenders) {
50+
if (typeof extender === 'function') {
51+
currentOperation = extender(currentOperation, procedure)
52+
}
53+
else {
54+
currentOperation = {
55+
...currentOperation,
56+
...extender,
57+
}
58+
}
59+
}
60+
61+
return currentOperation
62+
}

playgrounds/expressjs/src/orpc.ts

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,45 @@
1+
import type { ResponseHeadersPluginContext } from '@orpc/server/plugins'
12
import type { z } from 'zod'
23
import type { UserSchema } from './schemas/user'
4+
import { oo } from '@orpc/openapi'
35
import { ORPCError, os } from '@orpc/server'
46

5-
export interface ORPCContext {
7+
export interface ORPCContext extends ResponseHeadersPluginContext {
68
user?: z.infer<typeof UserSchema>
79
db?: any
810
}
911

10-
export const pub = os
11-
.$context<ORPCContext>()
12-
.use(async ({ context, path, next }, input) => {
13-
const start = Date.now()
12+
export const base = os.$context<ORPCContext>()
1413

15-
try {
16-
return await next({})
17-
}
18-
finally {
14+
export const pub = base.use(async ({ context, path, next }, input) => {
15+
const start = Date.now()
16+
17+
try {
18+
return await next({})
19+
}
20+
finally {
1921
// eslint-disable-next-line no-console
20-
console.log(`[${path.join('/')}] ${Date.now() - start}ms`)
22+
console.log(`[${path.join('/')}] ${Date.now() - start}ms`)
23+
}
24+
})
25+
26+
const authMid = oo.spec( // this line is optional, just for customize openapi spec
27+
base.middleware(({ context, path, next }, input) => {
28+
if (!context.user) {
29+
throw new ORPCError('UNAUTHORIZED')
2130
}
22-
})
2331

24-
export const authed = pub.use(({ context, path, next }, input) => {
25-
if (!context.user) {
26-
throw new ORPCError('UNAUTHORIZED')
27-
}
32+
return next({
33+
context: {
34+
user: context.user,
35+
},
36+
})
37+
}),
38+
{
39+
security: [
40+
{ bearerAuth: [] },
41+
],
42+
},
43+
)
2844

29-
return next({
30-
context: {
31-
user: context.user,
32-
},
33-
})
34-
})
45+
export const authed = base.use(authMid)

playgrounds/openapi/src/orpc.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
11
import type { ResponseHeadersPluginContext } from '@orpc/server/plugins'
22
import type { z } from 'zod'
33
import type { UserSchema } from './schemas/user'
4+
import { oo } from '@orpc/openapi'
45
import { ORPCError, os } from '@orpc/server'
56

67
export interface ORPCContext extends ResponseHeadersPluginContext {
78
user?: z.infer<typeof UserSchema>
89
db?: any
910
}
1011

11-
export const pub = os
12-
.$context<ORPCContext>()
13-
.use(async ({ context, path, next }, input) => {
14-
const start = Date.now()
12+
export const base = os.$context<ORPCContext>()
1513

16-
try {
17-
return await next({})
18-
}
19-
finally {
14+
export const pub = base.use(async ({ context, path, next }, input) => {
15+
const start = Date.now()
16+
17+
try {
18+
return await next({})
19+
}
20+
finally {
2021
// eslint-disable-next-line no-console
21-
console.log(`[${path.join('/')}] ${Date.now() - start}ms`)
22-
}
23-
})
22+
console.log(`[${path.join('/')}] ${Date.now() - start}ms`)
23+
}
24+
})
2425

25-
export const authed = pub
26-
.use(({ context, path, next }, input) => {
26+
const authMid = oo.spec( // this line is optional, just for customize openapi spec
27+
base.middleware(({ context, path, next }, input) => {
2728
if (!context.user) {
2829
throw new ORPCError('UNAUTHORIZED')
2930
}
@@ -33,4 +34,12 @@ export const authed = pub
3334
user: context.user,
3435
},
3536
})
36-
})
37+
}),
38+
{
39+
security: [
40+
{ bearerAuth: [] },
41+
],
42+
},
43+
)
44+
45+
export const authed = base.use(authMid)

0 commit comments

Comments
 (0)