Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion apps/content/docs/openapi/openapi-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,19 @@ const spec = await generator.generate(router, {
strategy: 'output',
schema: PetSchema,
},
UndefinedError: {
error: 'UndefinedError'
}
},
})
```

:::info
The `strategy` option determines which schema definition to use when input and output types differ (defaults to `input`). This is needed because we cannot use the same `$ref` for both input and output in this case.

- The `strategy` option determines which schema definition to use when input and output types differ (defaults to `input`). This is needed because we cannot use the same `$ref` for both input and output in this case.

- `UndefinedError` is used for undefined errors, which is very useful when using [Type-Safe Error Handling](/docs/error-handling#type‐safe-error-handling).

:::

## Excluding Procedures
Expand Down
38 changes: 18 additions & 20 deletions packages/openapi/src/openapi-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,9 @@ describe('openAPIGenerator', () => {
OutputDetailedStructure: {
schema: OutputDetailedStructure,
},
UndefinedError2: {
error: 'UndefinedError',
},
},
})

Expand Down Expand Up @@ -1155,11 +1158,22 @@ describe('openAPIGenerator', () => {
},
],
},
UndefinedError2: {
type: 'object',
properties: {
defined: { const: false },
code: { type: 'string' },
status: { type: 'number' },
message: { type: 'string' },
data: {},
},
required: ['defined', 'code', 'status', 'message'],
},
},
})
})

it('works with schema that input & output is same', async () => {
it('works with schema that input & output is same + error', async () => {
expect(spec.paths!['/user']).toEqual({
post: {
requestBody: {
Expand Down Expand Up @@ -1197,15 +1211,7 @@ describe('openAPIGenerator', () => {
required: ['defined', 'code', 'status', 'message', 'data'],
},
{
type: 'object',
properties: {
defined: { const: false },
code: { type: 'string' },
status: { type: 'number' },
message: { type: 'string' },
data: {},
},
required: ['defined', 'code', 'status', 'message'],
$ref: '#/components/schemas/UndefinedError2',
},
],
},
Expand All @@ -1218,7 +1224,7 @@ describe('openAPIGenerator', () => {
})
})

it('works with schema that input & output is different', async () => {
it('works with schema that input & output is different + error', async () => {
expect(spec.paths!['/pet']).toEqual({
post: {
operationId: 'pet',
Expand Down Expand Up @@ -1263,15 +1269,7 @@ describe('openAPIGenerator', () => {
required: ['defined', 'code', 'status', 'message', 'data'],
},
{
type: 'object',
properties: {
defined: { const: false },
code: { type: 'string' },
status: { type: 'number' },
message: { type: 'string' },
data: {},
},
required: ['defined', 'code', 'status', 'message'],
$ref: '#/components/schemas/UndefinedError2',
},
],
},
Expand Down
70 changes: 48 additions & 22 deletions packages/openapi/src/openapi-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { checkParamsSchema, resolveOpenAPIJsonSchemaRef, toOpenAPIContent, toOpe
import { CompositeSchemaConverter } from './schema-converter'
import { applySchemaOptionality, expandUnionSchema, isAnySchema, isObjectSchema, separateObjectSchema } from './schema-utils'

class OpenAPIGeneratorError extends Error {}
class OpenAPIGeneratorError extends Error { }

export interface OpenAPIGeneratorOptions extends StandardOpenAPIJsonSerializerOptions {
schemaConverters?: ConditionalSchemaConverter[]
Expand Down Expand Up @@ -55,6 +55,9 @@ export interface OpenAPIGeneratorGenerateOptions extends Partial<Omit<OpenAPI.Do
*/
strategy?: SchemaConvertOptions['strategy']
schema: AnySchema
} | {
error: 'UndefinedError'
schema?: never
}>
}

Expand Down Expand Up @@ -88,7 +91,7 @@ export class OpenAPIGenerator {
commonSchemas: undefined,
} as OpenAPI.Document

const baseSchemaConvertOptions = await this.#resolveCommonSchemas(doc, options.commonSchemas)
const { baseSchemaConvertOptions, undefinedErrorJsonSchema } = await this.#resolveCommonSchemas(doc, options.commonSchemas)

const contracts: { contract: AnyContractProcedure, path: readonly string[] }[] = []

Expand Down Expand Up @@ -125,7 +128,7 @@ export class OpenAPIGenerator {

await this.#request(doc, operationObjectRef, def, baseSchemaConvertOptions)
await this.#successResponse(doc, operationObjectRef, def, baseSchemaConvertOptions)
await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions)
await this.#errorResponse(operationObjectRef, def, baseSchemaConvertOptions, undefinedErrorJsonSchema)
}

doc.paths ??= {}
Expand All @@ -152,14 +155,34 @@ export class OpenAPIGenerator {
return this.serializer.serialize(doc)[0] as OpenAPI.Document
}

async #resolveCommonSchemas(doc: OpenAPI.Document, commonSchemas: OpenAPIGeneratorGenerateOptions['commonSchemas']): Promise<Pick<SchemaConvertOptions, 'components'>> {
const baseOptions: { components?: SchemaConverterComponent[] } = {}
async #resolveCommonSchemas(doc: OpenAPI.Document, commonSchemas: OpenAPIGeneratorGenerateOptions['commonSchemas']): Promise<{
baseSchemaConvertOptions: Pick<SchemaConvertOptions, 'components'>
undefinedErrorJsonSchema: JSONSchema
}> {
let undefinedErrorJsonSchema: JSONSchema = {
type: 'object',
properties: {
defined: { const: false },
code: { type: 'string' },
status: { type: 'number' },
message: { type: 'string' },
data: {},
},
required: ['defined', 'code', 'status', 'message'],
}
const baseSchemaConvertOptions: { components?: SchemaConverterComponent[] } = {}
Comment thread
dinwwwh marked this conversation as resolved.

if (commonSchemas) {
baseOptions.components = []
baseSchemaConvertOptions.components = []

for (const key in commonSchemas) {
const { schema, strategy = 'input' } = commonSchemas[key]!
const options = commonSchemas[key]!

if (options.schema === undefined) {
continue
}

const { schema, strategy = 'input' } = options

const [required, json] = await this.converter.convert(schema, { strategy })

Expand All @@ -180,7 +203,7 @@ export class OpenAPIGenerator {
}
}

baseOptions.components.push({
baseSchemaConvertOptions.components.push({
schema,
required,
ref: `#/components/schemas/${key}`,
Expand All @@ -192,11 +215,23 @@ export class OpenAPIGenerator {
doc.components.schemas ??= {}

for (const key in commonSchemas) {
const { schema, strategy = 'input' } = commonSchemas[key]!
const options = commonSchemas[key]!

if (options.schema === undefined) {
if (options.error === 'UndefinedError') {
doc.components.schemas[key] = toOpenAPISchema(undefinedErrorJsonSchema)
undefinedErrorJsonSchema = { $ref: `#/components/schemas/${key}` }
}

continue
}

const { schema, strategy = 'input' } = options

const [, json] = await this.converter.convert(
schema,
{
...baseOptions,
...baseSchemaConvertOptions,
strategy,
minStructureDepthForRef: 1, // not allow use $ref for root schemas
},
Expand All @@ -205,7 +240,7 @@ export class OpenAPIGenerator {
}
}

return baseOptions
return { baseSchemaConvertOptions, undefinedErrorJsonSchema }
}

async #request(
Expand Down Expand Up @@ -472,6 +507,7 @@ export class OpenAPIGenerator {
ref: OpenAPI.OperationObject,
def: AnyContractProcedure['~orpc'],
baseSchemaConvertOptions: Pick<SchemaConvertOptions, 'components'>,
undefinedErrorSchema: JSONSchema,
): Promise<void> {
const errorMap = def.errorMap as ErrorMap

Expand Down Expand Up @@ -513,17 +549,7 @@ export class OpenAPIGenerator {
content: toOpenAPIContent({
oneOf: [
...schemas,
{
type: 'object',
properties: {
defined: { const: false },
code: { type: 'string' },
status: { type: 'number' },
message: { type: 'string' },
data: {},
},
required: ['defined', 'code', 'status', 'message'],
},
undefinedErrorSchema,
],
}),
}
Expand Down
1 change: 1 addition & 0 deletions playgrounds/astro/src/pages/api/[...rest].ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const handler = new OpenAPIHandler(router, {
NewPlanet: { schema: NewPlanetSchema },
UpdatePlanet: { schema: UpdatePlanetSchema },
Planet: { schema: PlanetSchema },
UndefinedError: { error: 'UndefinedError' },
},
security: [{ bearerAuth: [] }],
components: {
Expand Down
1 change: 1 addition & 0 deletions playgrounds/contract-first/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const openAPIHandler = new OpenAPIHandler(router, {
NewPlanet: { schema: NewPlanetSchema },
UpdatePlanet: { schema: UpdatePlanetSchema },
Planet: { schema: PlanetSchema },
UndefinedError: { error: 'UndefinedError' },
},
security: [{ bearerAuth: [] }],
components: {
Expand Down
1 change: 1 addition & 0 deletions playgrounds/nest/src/reference/reference.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export class ReferenceService {
NewPlanet: { schema: NewPlanetSchema },
UpdatePlanet: { schema: UpdatePlanetSchema },
Planet: { schema: PlanetSchema },
UndefinedError: { error: 'UndefinedError' },
},
servers: [
{ url: 'http://localhost:3000' },
Expand Down
1 change: 1 addition & 0 deletions playgrounds/next/src/app/api/[[...rest]]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const openAPIHandler = new OpenAPIHandler(router, {
NewPlanet: { schema: NewPlanetSchema },
UpdatePlanet: { schema: UpdatePlanetSchema },
Planet: { schema: PlanetSchema },
UndefinedError: { error: 'UndefinedError' },
},
security: [{ bearerAuth: [] }],
components: {
Expand Down
1 change: 1 addition & 0 deletions playgrounds/nuxt/server/routes/api/[...].ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const openAPIHandler = new OpenAPIHandler(router, {
NewPlanet: { schema: NewPlanetSchema },
UpdatePlanet: { schema: UpdatePlanetSchema },
Planet: { schema: PlanetSchema },
UndefinedError: { error: 'UndefinedError' },
},
security: [{ bearerAuth: [] }],
components: {
Expand Down
1 change: 1 addition & 0 deletions playgrounds/solid-start/src/routes/api/[...rest].ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const handler = new OpenAPIHandler(router, {
NewPlanet: { schema: NewPlanetSchema },
UpdatePlanet: { schema: UpdatePlanetSchema },
Planet: { schema: PlanetSchema },
UndefinedError: { error: 'UndefinedError' },
},
security: [{ bearerAuth: [] }],
components: {
Expand Down
1 change: 1 addition & 0 deletions playgrounds/svelte-kit/src/routes/api/[...rest]/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const handler = new OpenAPIHandler(router, {
NewPlanet: { schema: NewPlanetSchema },
UpdatePlanet: { schema: UpdatePlanetSchema },
Planet: { schema: PlanetSchema },
UndefinedError: { error: 'UndefinedError' },
},
security: [{ bearerAuth: [] }],
components: {
Expand Down
1 change: 1 addition & 0 deletions playgrounds/tanstack-start/src/routes/api/$.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const handler = new OpenAPIHandler(router, {
NewPlanet: { schema: NewPlanetSchema },
UpdatePlanet: { schema: UpdatePlanetSchema },
Planet: { schema: PlanetSchema },
UndefinedError: { error: 'UndefinedError' },
},
security: [{ bearerAuth: [] }],
components: {
Expand Down