Skip to content

Commit

Permalink
fix: body schema not remove params fields
Browse files Browse the repository at this point in the history
fix: required and schema examples not work on query
  • Loading branch information
unnoq committed Nov 20, 2024
1 parent 8e9a954 commit 1c9f9ab
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .changeset/good-ghosts-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@orpc/openapi": patch
---

fix: body schema not remove params fields
5 changes: 5 additions & 0 deletions .changeset/little-sloths-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@orpc/openapi": patch
---

fix: required and schema examples not work on query
178 changes: 178 additions & 0 deletions packages/openapi/src/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,3 +463,181 @@ it('work with example', () => {
},
})
})

it('should remove params on body', () => {
const router = oc.router({
upload: oc.route({ method: 'POST', path: '/upload/{id}' }).input(
oz.openapi(
z.object({
id: z.number(),
file: z.string().url(),
}),
{
examples: [
{
id: 123,
file: 'https://example.com/file.png',
},
],
},
),
),
})

const spec = generateOpenAPI({
router,
info: {
title: 'test',
version: '1.0.0',
},
})

expect(spec).toEqual({
info: { title: 'test', version: '1.0.0' },
openapi: '3.1.0',
paths: {
'/upload/{id}': {
post: {
summary: undefined,
description: undefined,
deprecated: undefined,
tags: undefined,
operationId: 'upload',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { examples: [123], type: 'number' },
example: undefined,
},
],
requestBody: {
required: false,
content: {
'application/json': {
schema: {
type: 'object',
properties: { file: { type: 'string', format: 'uri' } },
required: ['file'],
examples: [{ file: 'https://example.com/file.png' }],
},
example: undefined,
},
},
},
responses: {
'200': {
description: 'OK',
content: {
'application/json': { schema: {}, example: undefined },
},
},
},
},
},
},
})
})

it('should remove params on query', () => {
const router = oc.router({
upload: oc.route({ method: 'GET', path: '/upload/{id}' }).input(
oz.openapi(
z.object({
id: z.number(),
file: z.string().url(),
object: z
.object({
name: z.string(),
})
.optional(),
}),
{
examples: [
{
id: 123,
file: 'https://example.com/file.png',
object: { name: 'test' },
},
{
id: 456,
file: 'https://example.com/file2.png',
},
],
},
),
),
})

const spec = generateOpenAPI({
router,
info: {
title: 'test',
version: '1.0.0',
},
})

expect(spec).toEqual({
info: { title: 'test', version: '1.0.0' },
openapi: '3.1.0',
paths: {
'/upload/{id}': {
get: {
summary: undefined,
description: undefined,
deprecated: undefined,
tags: undefined,
operationId: 'upload',
parameters: [
{
name: 'id',
in: 'path',
required: true,
schema: { examples: [123, 456], type: 'number' },
example: undefined,
},
{
name: 'file',
in: 'query',
style: 'deepObject',
required: true,
schema: {
examples: [
'https://example.com/file.png',
'https://example.com/file2.png',
],
type: 'string',
format: 'uri',
},
example: undefined,
},
{
name: 'object',
in: 'query',
style: 'deepObject',
required: false,
schema: {
examples: [{ name: 'test' }],
anyOf: undefined,
type: 'object',
properties: { name: { type: 'string' } },
required: ['name'],
},
example: undefined,
},
],
requestBody: undefined,
responses: {
'200': {
description: 'OK',
content: {
'application/json': { schema: {}, example: undefined },
},
},
},
},
},
},
})
})
91 changes: 80 additions & 11 deletions packages/openapi/src/generator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type ContractRouter, eachContractRouterLeaf } from '@orpc/contract'
import { type Router, toContractRouter } from '@orpc/server'
import { findDeepMatches, omit } from '@orpc/shared'
import { findDeepMatches, isPlainObject, omit } from '@orpc/shared'
import { preSerialize } from '@orpc/transformer'
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
import {
Expand All @@ -12,7 +12,11 @@ import {
type RequestBodyObject,
type ResponseObject,
} from 'openapi3-ts/oas31'
import { extractJSONSchema, zodToJsonSchema } from './zod-to-json-schema'
import {
UNSUPPORTED_JSON_SCHEMA,
extractJSONSchema,
zodToJsonSchema,
} from './zod-to-json-schema'

// Reference: https://spec.openapis.org/oas/v3.1.0.html#style-values

Expand Down Expand Up @@ -64,7 +68,7 @@ export function generateOpenAPI(
const path = internal.path ?? `/${path_.map(encodeURIComponent).join('/')}`
const method = internal.method ?? 'POST'

const inputSchema = internal.InputSchema
let inputSchema = internal.InputSchema
? zodToJsonSchema(internal.InputSchema, { mode: 'input' })
: {}
const outputSchema = internal.OutputSchema
Expand All @@ -87,10 +91,10 @@ export function generateOpenAPI(
return names
.map((raw) => raw.slice(1, -1))
.map((name) => {
const schema = inputSchema.properties?.[name]
let schema = inputSchema.properties?.[name]
const required = inputSchema.required?.includes(name)

if (!schema) {
if (schema === undefined) {
throw new Error(
`Parameter ${name} is missing in input schema [${path_.join('.')}]`,
)
Expand All @@ -102,6 +106,54 @@ export function generateOpenAPI(
)
}

const examples = inputSchema.examples
?.filter((example) => {
return isPlainObject(example) && name in example
})
.map((example) => {
return example[name]
})

schema = {
examples: examples?.length ? examples : undefined,
...(schema === true
? {}
: schema === false
? UNSUPPORTED_JSON_SCHEMA
: schema),
}

inputSchema = {
...inputSchema,
properties: inputSchema.properties
? Object.entries(inputSchema.properties).reduce(
(acc, [key, value]) => {
if (key !== name) {
acc[key] = value
}

return acc
},
{} as Record<string, JSONSchema>,
)
: undefined,
required: inputSchema.required?.filter((v) => v !== name),
examples: inputSchema.examples?.map((example) => {
if (!isPlainObject(example)) return example

return Object.entries(example).reduce(
(acc, [key, value]) => {
if (key !== name) {
acc[key] = value
}

return acc
},
{} as Record<string, unknown>,
)
}),
}

return {
name,
in: 'path',
Expand All @@ -123,18 +175,35 @@ export function generateOpenAPI(
)
}

return Object.entries(inputSchema.properties ?? {})
.filter(([name]) => !params?.find((param) => param.name === name))
.map(([name, schema]) => {
return Object.entries(inputSchema.properties ?? {}).map(
([name, schema]) => {
const examples = inputSchema.examples
?.filter((example) => {
return isPlainObject(example) && name in example
})
.map((example) => {
return example[name]
})

const schema_ = {
examples: examples?.length ? examples : undefined,
...(schema === true
? {}
: schema === false
? UNSUPPORTED_JSON_SCHEMA
: schema),
}

return {
name,
in: 'query',
style: 'deepObject',
required: true,
schema: schema as any,
required: inputSchema?.required?.includes(name) ?? false,
schema: schema_ as any,
example: internal.inputExample?.[name],
}
})
},
)
})()

const parameters = [...(params ?? []), ...(query ?? [])]
Expand Down

0 comments on commit 1c9f9ab

Please sign in to comment.