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
1 change: 0 additions & 1 deletion apps/content/docs/openapi/plugins/zod-smart-coercion.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ Supported formats:

- Full ISO date-time (e.g., `2024-11-27T00:00:00.000Z`)
- Date only (e.g., `2024-11-27`)
- Time only (e.g., `19:00:00`)

#### RegExp

Expand Down
2 changes: 1 addition & 1 deletion apps/content/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
},
"devDependencies": {
"@shikijs/vitepress-twoslash": "^2.5.0",
"@types/node": "^22.10.0",
"@types/node": "^22.13.1",
"vitepress": "1.6.3",
"vitepress-plugin-group-icons": "^1.3.6",
"vitepress-plugin-shiki-twoslash": "^0.0.6",
Expand Down
6 changes: 6 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export default antfu({
message: 'JSON.stringify can return undefined, use stringifyJSON instead',
},
],
'no-restricted-imports': ['error', {
patterns: [{
group: ['json-schema-typed', 'json-schema-typed/*'],
message: 'Please import from @orpc/openapi instead',
}],
}],
},
}, {
files: ['**/*.test.ts', '**/*.test.tsx', '**/*.test-d.ts', '**/*.test-d.tsx', 'apps/content/shared/**', 'playgrounds/**', 'packages/*/playground/**'],
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"@eslint-react/eslint-plugin": "^1.16.2",
"@testing-library/jest-dom": "^6.6.2",
"@testing-library/react": "^16.0.1",
"@types/node": "^22.10.0",
"@types/node": "^22.13.1",
"@vitest/coverage-v8": "^3.0.4",
"@vue/test-utils": "^2.4.6",
"eslint": "^9.15.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@
"@orpc/standard-server-fetch": "workspace:*"
},
"devDependencies": {
"zod": "^3.24.1"
"zod": "^3.24.2"
}
}
2 changes: 1 addition & 1 deletion packages/contract/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,6 @@
"devDependencies": {
"arktype": "2.0.0-rc.26",
"valibot": "1.0.0-beta.9",
"zod": "^3.24.1"
"zod": "^3.24.2"
}
}
2 changes: 1 addition & 1 deletion packages/openapi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,6 @@
"rou3": "^0.5.1"
},
"devDependencies": {
"zod": "^3.24.1"
"zod": "^3.24.2"
}
}
4 changes: 2 additions & 2 deletions packages/openapi/src/openapi-content-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ export class OpenAPIContentBuilder {
private readonly schemaUtils: PublicSchemaUtils,
) {}

build(jsonSchema: JSONSchema.JSONSchema, options?: Partial<OpenAPI.MediaTypeObject>): OpenAPI.ContentObject {
build(jsonSchema: JSONSchema, options?: Partial<OpenAPI.MediaTypeObject>): OpenAPI.ContentObject {
const isFileSchema = this.schemaUtils.isFileSchema.bind(this.schemaUtils)

const [matches, schema] = this.schemaUtils.filterSchemaBranches(jsonSchema, isFileSchema)

const files = matches as (JSONSchema.JSONSchema & {
const files = matches as (JSONSchema & {
type: 'string'
contentMediaType: string
})[]
Expand Down
16 changes: 8 additions & 8 deletions packages/openapi/src/openapi-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('openapi generator', () => {
.input(defaultSchema)
.output(defaultSchema)

mockConverter.convert.mockReturnValue({
mockConverter.convert.mockReturnValue([true, {
type: 'object',
properties: {
name: {
Expand All @@ -36,7 +36,7 @@ describe('openapi generator', () => {
type: 'string',
},
},
})
}])

const spec = await generator.generate(router, defaultDoc)

Expand Down Expand Up @@ -100,7 +100,7 @@ describe('openapi generator', () => {

mockConverter.convert.mockImplementation((schema) => {
if (schema === inputSchema) {
return {
return [true, {
type: 'object',
properties: {
params: {
Expand Down Expand Up @@ -147,10 +147,10 @@ describe('openapi generator', () => {
},
},
},
} satisfies JSONSchema.JSONSchema
} satisfies JSONSchema]
}

return {
return [true, {
type: 'object',
properties: {
headers: {
Expand All @@ -170,7 +170,7 @@ describe('openapi generator', () => {
},
},
},
}
}]
})

const spec = await generator.generate(router, defaultDoc)
Expand Down Expand Up @@ -269,7 +269,7 @@ describe('openapi generator', () => {
}).route({})

it('strictErrorResponses=true', async () => {
mockConverter.convert.mockReturnValue({ description: '__mocked__' })
mockConverter.convert.mockReturnValue([true, { description: '__mocked__' }])

const spec = await generator.generate(contract, defaultDoc)

Expand Down Expand Up @@ -324,7 +324,7 @@ describe('openapi generator', () => {
strictErrorResponses: false,
})

mockConverter.convert.mockReturnValue({ description: '__mocked__' })
mockConverter.convert.mockReturnValue([true, { description: '__mocked__' }])

const spec = await generator.generate(contract, defaultDoc)

Expand Down
18 changes: 9 additions & 9 deletions packages/openapi/src/openapi-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { PublicOpenAPIInputStructureParser } from './openapi-input-structur
import type { PublicOpenAPIOutputStructureParser } from './openapi-output-structure-parser'
import type { PublicOpenAPIPathParser } from './openapi-path-parser'
import type { JSONSchema } from './schema'
import type { SchemaConverter } from './schema-converter'
import type { ConditionalSchemaConverter } from './schema-converter'
import { fallbackORPCErrorStatus } from '@orpc/client'
import { type ContractRouter, fallbackContractConfig, getEventIteratorSchemaDetails } from '@orpc/contract'
import { OpenAPIJsonSerializer } from '@orpc/openapi-client/standard'
Expand All @@ -25,7 +25,7 @@ type ErrorHandlerStrategy = 'throw' | 'log' | 'ignore'
export interface OpenAPIGeneratorOptions {
contentBuilder?: PublicOpenAPIContentBuilder
parametersBuilder?: PublicOpenAPIParametersBuilder
schemaConverters?: SchemaConverter[]
schemaConverters?: ConditionalSchemaConverter[]
schemaUtils?: PublicSchemaUtils
jsonSerializer?: OpenAPIJsonSerializer
pathParser?: PublicOpenAPIPathParser
Expand Down Expand Up @@ -132,7 +132,7 @@ export class OpenAPIGenerator {
type: 'object',
properties: {
event: { type: 'string', const: 'message' },
data: this.schemaConverter.convert(eventIteratorSchemaDetails.yields, { strategy: 'input' }) as any,
data: this.schemaConverter.convert(eventIteratorSchemaDetails.yields, 'input')[1] as any,
id: { type: 'string' },
retry: { type: 'number' },
},
Expand All @@ -142,7 +142,7 @@ export class OpenAPIGenerator {
type: 'object',
properties: {
event: { type: 'string', const: 'done' },
data: this.schemaConverter.convert(eventIteratorSchemaDetails.returns, { strategy: 'input' }) as any,
data: this.schemaConverter.convert(eventIteratorSchemaDetails.returns, 'input')[1] as any,
id: { type: 'string' },
retry: { type: 'number' },
},
Expand Down Expand Up @@ -213,7 +213,7 @@ export class OpenAPIGenerator {
type: 'object',
properties: {
event: { type: 'string', const: 'message' },
data: this.schemaConverter.convert(eventIteratorSchemaDetails.yields, { strategy: 'input' }) as any,
data: this.schemaConverter.convert(eventIteratorSchemaDetails.yields, 'input')[1] as any,
id: { type: 'string' },
retry: { type: 'number' },
},
Expand All @@ -223,7 +223,7 @@ export class OpenAPIGenerator {
type: 'object',
properties: {
event: { type: 'string', const: 'done' },
data: this.schemaConverter.convert(eventIteratorSchemaDetails.returns, { strategy: 'input' }) as any,
data: this.schemaConverter.convert(eventIteratorSchemaDetails.returns, 'input')[1] as any,
id: { type: 'string' },
retry: { type: 'number' },
},
Expand Down Expand Up @@ -281,7 +281,7 @@ export class OpenAPIGenerator {
continue
}

const schemas: JSONSchema.JSONSchema[] = configs
const schemas: JSONSchema[] = configs
.map(({ data, code, message }) => {
const json = {
type: 'object',
Expand All @@ -293,10 +293,10 @@ export class OpenAPIGenerator {
data: {},
},
required: ['defined', 'code', 'status', 'message'],
} satisfies JSONSchema.JSONSchema
} satisfies JSONSchema

if (data) {
const dataJson = this.schemaConverter.convert(data, { strategy: 'output' })
const dataJson = this.schemaConverter.convert(data, 'output')[1]

json.properties.data = dataJson

Expand Down
10 changes: 5 additions & 5 deletions packages/openapi/src/openapi-input-structure-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface OpenAPIInputStructureParseResult {
paramsSchema: ObjectSchema | undefined
querySchema: ObjectSchema | undefined
headersSchema: ObjectSchema | undefined
bodySchema: JSONSchema.JSONSchema | undefined
bodySchema: JSONSchema | undefined
}

export class OpenAPIInputStructureParser {
Expand All @@ -21,7 +21,7 @@ export class OpenAPIInputStructureParser {
) { }

parse(contract: AnyContractProcedure, structure: 'compact' | 'detailed'): OpenAPIInputStructureParseResult {
const inputSchema = this.schemaConverter.convert(contract['~orpc'].inputSchema, { strategy: 'input' })
const [_, inputSchema] = this.schemaConverter.convert(contract['~orpc'].inputSchema, 'input')
const method = fallbackContractConfig('defaultMethod', contract['~orpc'].route?.method)
const httpPath = contract['~orpc'].route?.path

Expand All @@ -42,7 +42,7 @@ export class OpenAPIInputStructureParser {
}
}

private parseDetailedSchema(inputSchema: JSONSchema.JSONSchema): OpenAPIInputStructureParseResult {
private parseDetailedSchema(inputSchema: JSONSchema): OpenAPIInputStructureParseResult {
if (!this.schemaUtils.isObjectSchema(inputSchema)) {
throw new OpenAPIError(`When input structure is 'detailed', input schema must be an object.`)
}
Expand Down Expand Up @@ -83,12 +83,12 @@ export class OpenAPIInputStructureParser {
return { paramsSchema, querySchema, headersSchema, bodySchema }
}

private parseCompactSchema(inputSchema: JSONSchema.JSONSchema, method: string, httpPath: string | undefined): OpenAPIInputStructureParseResult {
private parseCompactSchema(inputSchema: JSONSchema, method: string, httpPath: string | undefined): OpenAPIInputStructureParseResult {
const dynamic = httpPath ? this.pathParser.parseDynamicParams(httpPath) : []

if (dynamic.length === 0) {
if (method === 'GET') {
let querySchema: JSONSchema.JSONSchema | undefined = inputSchema
let querySchema: JSONSchema | undefined = inputSchema
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

Add test coverage for this line

This line appears to be flagged by code coverage tools as not covered by tests.


🏁 Script executed:

#!/bin/bash
# Check if there are any existing tests covering this code path
rg -A 5 -B 5 "parseCompactSchema.*querySchema.*method.*GET" --glob "**/*test*.ts" packages/openapi/

Length of output: 101


Action Required: Add Test Coverage for Query Schema Assignment

It appears that the initialization of querySchema at line 91 in packages/openapi/src/openapi-input-structure-parser.ts is currently not exercised by any existing tests. A search for test references (using patterns like "parseCompactSchema.*querySchema.*method.*GET" and a more general search for "querySchema") returned no findings in our openapi-related test files.

  • Location: packages/openapi/src/openapi-input-structure-parser.ts at line 91
            let querySchema: JSONSchema | undefined = inputSchema
  • Recommendation: Please add unit tests to cover this code path. The tests should verify:
    • The scenario where inputSchema correctly initializes querySchema.
    • Any edge cases, such as handling when inputSchema is undefined, if applicable.

These changes are required to ensure full code coverage and to validate the intended behavior of the parser.

🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 91-91: packages/openapi/src/openapi-input-structure-parser.ts#L91
Added line #L91 was not covered by tests


if (querySchema !== undefined && this.schemaUtils.isAnySchema(querySchema)) {
querySchema = undefined
Expand Down
8 changes: 4 additions & 4 deletions packages/openapi/src/openapi-output-structure-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { OpenAPIError } from './openapi-error'

export interface OpenAPIOutputStructureParseResult {
headersSchema: ObjectSchema | undefined
bodySchema: JSONSchema.JSONSchema | undefined
bodySchema: JSONSchema | undefined
}

export class OpenAPIOutputStructureParser {
Expand All @@ -16,7 +16,7 @@ export class OpenAPIOutputStructureParser {
) { }

parse(contract: AnyContractProcedure, structure: 'compact' | 'detailed'): OpenAPIOutputStructureParseResult {
const outputSchema = this.schemaConverter.convert(contract['~orpc'].outputSchema, { strategy: 'output' })
const [_, outputSchema] = this.schemaConverter.convert(contract['~orpc'].outputSchema, 'output')

// TODO: refactor and remove this logic
if (this.schemaUtils.isAnySchema(outputSchema)) {
Expand All @@ -34,7 +34,7 @@ export class OpenAPIOutputStructureParser {
}
}

private parseDetailedSchema(outputSchema: JSONSchema.JSONSchema): OpenAPIOutputStructureParseResult {
private parseDetailedSchema(outputSchema: JSONSchema): OpenAPIOutputStructureParseResult {
if (!this.schemaUtils.isObjectSchema(outputSchema)) {
throw new OpenAPIError(`When output structure is 'detailed', output schema must be an object.`)
}
Expand All @@ -57,7 +57,7 @@ export class OpenAPIOutputStructureParser {
return { headersSchema, bodySchema }
}

private parseCompactSchema(outputSchema: JSONSchema.JSONSchema): OpenAPIOutputStructureParseResult {
private parseCompactSchema(outputSchema: JSONSchema): OpenAPIOutputStructureParseResult {
return {
headersSchema: undefined,
bodySchema: outputSchema,
Expand Down
4 changes: 2 additions & 2 deletions packages/openapi/src/openapi-parameters-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { get, isObject, omit } from '@orpc/shared'
export class OpenAPIParametersBuilder {
build(
paramIn: OpenAPI.ParameterObject['in'],
jsonSchema: JSONSchema.JSONSchema & { type: 'object' } & object,
jsonSchema: JSONSchema & { type: 'object' } & object,
options?: Pick<OpenAPI.ParameterObject, 'example' | 'style' | 'required'>,
): OpenAPI.ParameterObject[] {
const parameters: OpenAPI.ParameterObject[] = []
Expand Down Expand Up @@ -46,7 +46,7 @@ export class OpenAPIParametersBuilder {
}

buildHeadersObject(
jsonSchema: JSONSchema.JSONSchema & { type: 'object' } & object,
jsonSchema: JSONSchema & { type: 'object' } & object,
options?: Pick<OpenAPI.ParameterObject, 'example' | 'style' | 'required'>,
): OpenAPI.HeadersObject {
const parameters = this.build('header', jsonSchema, options)
Expand Down
26 changes: 11 additions & 15 deletions packages/openapi/src/schema-converter.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
import type { Schema } from '@orpc/contract'
import type { JSONSchema } from './schema'

export interface SchemaConvertOptions {
strategy: 'input' | 'output'
}
export type SchemaConvertStrategy = 'input' | 'output'

export interface SchemaConverter {
condition(schema: Schema, options: SchemaConvertOptions): boolean
convert(schema: Schema, strategy: SchemaConvertStrategy): [required: boolean, jsonSchema: JSONSchema]
}

convert(schema: Schema, options: SchemaConvertOptions): JSONSchema.JSONSchema
export interface ConditionalSchemaConverter extends SchemaConverter {
condition(schema: Schema, strategy: SchemaConvertStrategy): boolean
}

export class CompositeSchemaConverter implements SchemaConverter {
private readonly converters: SchemaConverter[]
private readonly converters: ConditionalSchemaConverter[]

constructor(converters: SchemaConverter[]) {
constructor(converters: ConditionalSchemaConverter[]) {
this.converters = converters
}

condition(): boolean {
return true
}

convert(schema: Schema, options: SchemaConvertOptions): JSONSchema.JSONSchema {
convert(schema: Schema, strategy: SchemaConvertStrategy): [required: boolean, jsonSchema: JSONSchema] {
for (const converter of this.converters) {
if (converter.condition(schema, options)) {
return converter.convert(schema, options)
if (converter.condition(schema, strategy)) {
return converter.convert(schema, strategy)
}
}

return {} // ANY SCHEMA
return [false, {}]
}
}
Loading