Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/serious-pigs-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@eventcatalog/generator-openapi": patch
---

fix(generator-openapi): resolve schemas/params when operationId is missing
32 changes: 32 additions & 0 deletions packages/generator-openapi/src/test/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1424,6 +1424,38 @@ describe('OpenAPI EventCatalog Plugin', () => {
expect(getCommandMessage).toBeDefined();
});

it('when operationIds are missing, schemas and parameters are resolved by path+method (not first undefined operationId)', async () => {
const { getQuery } = utils(catalogDir);

await plugin(config, { services: [{ path: join(openAPIExamples, 'without-operationIds.yml'), id: 'product-api' }] });

const listProducts = await getQuery('product-api_GET');
const getProductById = await getQuery('product-api_GET_{productId}');

const listProductsResponseSchema = await fs.readFile(
join(catalogDir, 'services', 'product-api', 'queries', 'product-api_GET', 'response-200.json'),
'utf8'
);

const getProductByIdResponseSchema = await fs.readFile(
join(catalogDir, 'services', 'product-api', 'queries', 'product-api_GET_{productId}', 'response-200.json'),
'utf8'
);

const getProductByIdMarkdown = await fs.readFile(
join(catalogDir, 'services', 'product-api', 'queries', 'product-api_GET_{productId}', 'index.mdx'),
'utf8'
);

expect(listProducts).toBeDefined();
expect(getProductById).toBeDefined();
expect(listProductsResponseSchema).toContain('"type": "array"');
expect(getProductByIdResponseSchema).not.toContain('"type": "array"');
expect(getProductByIdResponseSchema).toContain('"type": "object"');
expect(getProductByIdMarkdown).toContain('### Parameters');
expect(getProductByIdMarkdown).toContain('productId');
});

it('when the service has owners, the messages are given the same owners', async () => {
const { getCommand } = utils(catalogDir);

Expand Down
2 changes: 1 addition & 1 deletion packages/generator-openapi/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export type Message = {
export type Operation = {
path: string;
method: string;
operationId: string;
operationId?: string;
summary?: string;
description?: string;
type: string;
Expand Down
5 changes: 4 additions & 1 deletion packages/generator-openapi/src/utils/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,10 @@ export const buildMessage = async (
serviceVersion?: string
) => {
// Pass the document to avoid re-parsing (needed for authenticated URLs)
const requestBodiesAndResponses = await getSchemasByOperationId(pathToFile, operation.operationId, document as any);
const requestBodiesAndResponses = await getSchemasByOperationId(pathToFile, operation.operationId, document as any, {
path: operation.path,
method: operation.method,
});
const extensions = operation.extensions || {};

const operationTags = operation.tags.map((badge) => ({
Expand Down
18 changes: 14 additions & 4 deletions packages/generator-openapi/src/utils/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ const DEFAULT_MESSAGE_TYPE = 'query';

export async function getSchemasByOperationId(
filePath: string,
operationId: string,
parsedDocument?: OpenAPIDocument
operationId: string | undefined,
parsedDocument?: OpenAPIDocument,
operationLookup?: { path: string; method: string }
): Promise<OpenAPIOperation | undefined> {
try {
// Use pre-parsed document if provided, otherwise parse from file
Expand All @@ -27,7 +28,12 @@ export async function getSchemasByOperationId(
// Cast operation to OpenAPIOperation type
const typedOperation = operation as OpenAPIOperation;

if (typedOperation.operationId === operationId) {
const matchesByOperationId = operationId ? typedOperation.operationId === operationId : false;
const matchesByPathAndMethod = operationLookup
? path === operationLookup.path && method.toUpperCase() === operationLookup.method.toUpperCase()
: false;

if (matchesByOperationId || matchesByPathAndMethod) {
// Extract query parameters
if (typedOperation.parameters) {
schemas.parameters = typedOperation.parameters;
Expand Down Expand Up @@ -56,7 +62,11 @@ export async function getSchemasByOperationId(
}
}

throw new Error(`Operation with ID "${operationId}" not found.`);
throw new Error(
operationId
? `Operation with ID "${operationId}" not found.`
: `Operation not found for ${operationLookup?.method || 'UNKNOWN'} ${operationLookup?.path || ''}.`
);
} catch (error) {
console.error('Error parsing OpenAPI file or finding operation:', error);
return;
Expand Down
Loading