diff --git a/src/openapi.test.ts b/src/openapi.test.ts index 4931d75..933a306 100644 --- a/src/openapi.test.ts +++ b/src/openapi.test.ts @@ -313,4 +313,134 @@ describe('toOpenAPISpec', () => { }, }); }); + test("handles spec with 'allOf' query parameters", () => { + const spec: OneSchemaDefinition = withAssumptions( + { + Resources: { + Post: { + type: 'object', + properties: { + id: { type: 'number' }, + message: { type: 'string' }, + }, + }, + }, + Endpoints: { + 'GET /posts/:id': { + Name: 'getPostByIdWithParams', + Request: { + allOf: [ + { + type: 'object', + properties: { + project: { + type: 'string', + }, + }, + required: ['project'], + }, + { + type: 'object', + properties: { + count: { + type: 'string', + }, + }, + }, + { + type: 'string', // handles a code coverage for a non-object allOf entry + }, + ], + }, + Response: { + $ref: '#/definitions/Post', + }, + }, + }, + }, + { + objectPropertiesRequiredByDefault: false, + noAdditionalPropertiesOnObjects: true, + }, + ); + const result = toOpenAPISpec(spec, { + info: { title: 'test title', version: '1.2.3' }, + }); + + // Ensure result is a valid OpenAPI spec + const { errors } = new OpenAPIValidator({ + version: '3.0.0', + }).validate(result); + + expect(errors).toHaveLength(0); + + expect(result).toStrictEqual({ + openapi: '3.0.0', + info: { + title: 'test title', + version: '1.2.3', + }, + components: { + schemas: { + Post: { + additionalProperties: false, + properties: { + id: { + type: 'number', + }, + message: { + type: 'string', + }, + }, + type: 'object', + }, + }, + }, + paths: { + '/posts/{id}': { + get: { + operationId: 'getPostByIdWithParams', + parameters: [ + { + in: 'path', + name: 'id', + required: true, + schema: { + type: 'string', + }, + }, + { + in: 'query', + name: 'project', + required: true, + schema: { + type: 'string', + }, + }, + { + in: 'query', + name: 'count', + required: false, + schema: { + type: 'string', + }, + }, + ], + responses: { + '200': { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Post', + }, + }, + }, + description: 'A successful response', + }, + }, + }, + }, + }, + }); + }); }); diff --git a/src/openapi.ts b/src/openapi.ts index db71c6f..8a40428 100644 --- a/src/openapi.ts +++ b/src/openapi.ts @@ -2,6 +2,7 @@ import type { OpenAPIV3 } from 'openapi-types'; import type { OneSchemaDefinition } from './types'; import { deepCopy } from './generate-endpoints'; import { validateSchema } from './meta-schema'; +import { JSONSchema4 } from 'json-schema'; /** * Converts e.g. `/users/:id/profile` to `/users/{id}/profile`. @@ -18,6 +19,18 @@ const getPathParameters = (koaPath: string) => .filter((part) => part.startsWith(':')) .map((part) => part.slice(1)); +const toQueryParam = ( + item: JSONSchema4, + name: string, + schema: JSONSchema4, +) => ({ + in: 'query', + name, + description: schema.description, + schema: schema, + required: Array.isArray(item.required) && item.required.includes(name), +}); + export const toOpenAPISpec = ( schema: OneSchemaDefinition, config: { @@ -82,19 +95,21 @@ export const toOpenAPISpec = ( if (Request) { if (['GET', 'DELETE'].includes(method)) { // Add the query parameters for GET/DELETE methods - for (const [name, schema] of Object.entries(Request.properties ?? {})) - parameters.push({ - in: 'query', - name, - description: schema.description, + for (const [name, schema] of Object.entries(Request.properties ?? {})) { + // @ts-expect-error TS detects a mismatch between the JSONSchema types + // between openapi-types and json-schema. Ignore and assume everything + // is cool. + parameters.push(toQueryParam(Request, name, schema)); + } + + for (const item of Request.allOf ?? []) { + for (const [name, schema] of Object.entries(item.properties ?? {})) { // @ts-expect-error TS detects a mismatch between the JSONSchema types // between openapi-types and json-schema. Ignore and assume everything // is cool. - schema: schema, - required: - Array.isArray(Request.required) && - Request.required.includes(name), - }); + parameters.push(toQueryParam(item, name, schema)); + } + } } else { // Add the body spec parameters for non-GET/DELETE methods operation.requestBody = {