From 5bb8cf904fdbc68ade3b953d5c1cd7b5c774da71 Mon Sep 17 00:00:00 2001 From: Thor Date: Mon, 16 Feb 2026 14:20:20 +0000 Subject: [PATCH 1/2] fix(generator-openapi): fallback schema lookup when operationId is missing --- .../generator-openapi/src/test/plugin.test.ts | 32 +++++++++++++++++++ packages/generator-openapi/src/types.ts | 2 +- .../generator-openapi/src/utils/messages.ts | 5 ++- .../generator-openapi/src/utils/openapi.ts | 18 ++++++++--- 4 files changed, 51 insertions(+), 6 deletions(-) diff --git a/packages/generator-openapi/src/test/plugin.test.ts b/packages/generator-openapi/src/test/plugin.test.ts index 7382a7e6..5d3f016b 100644 --- a/packages/generator-openapi/src/test/plugin.test.ts +++ b/packages/generator-openapi/src/test/plugin.test.ts @@ -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); diff --git a/packages/generator-openapi/src/types.ts b/packages/generator-openapi/src/types.ts index 3d42f445..75f9dd6e 100644 --- a/packages/generator-openapi/src/types.ts +++ b/packages/generator-openapi/src/types.ts @@ -43,7 +43,7 @@ export type Message = { export type Operation = { path: string; method: string; - operationId: string; + operationId?: string; summary?: string; description?: string; type: string; diff --git a/packages/generator-openapi/src/utils/messages.ts b/packages/generator-openapi/src/utils/messages.ts index d4db6acf..8e798c46 100644 --- a/packages/generator-openapi/src/utils/messages.ts +++ b/packages/generator-openapi/src/utils/messages.ts @@ -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) => ({ diff --git a/packages/generator-openapi/src/utils/openapi.ts b/packages/generator-openapi/src/utils/openapi.ts index 8c372bc3..21586d4f 100644 --- a/packages/generator-openapi/src/utils/openapi.ts +++ b/packages/generator-openapi/src/utils/openapi.ts @@ -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 { try { // Use pre-parsed document if provided, otherwise parse from file @@ -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; @@ -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; From 8f7ba51a63ae6095d95c36a26e15b7ac532e4e86 Mon Sep 17 00:00:00 2001 From: David Boyne Date: Mon, 16 Feb 2026 14:21:55 +0000 Subject: [PATCH 2/2] Create serious-pigs-hunt.md --- .changeset/serious-pigs-hunt.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/serious-pigs-hunt.md diff --git a/.changeset/serious-pigs-hunt.md b/.changeset/serious-pigs-hunt.md new file mode 100644 index 00000000..19c19cdc --- /dev/null +++ b/.changeset/serious-pigs-hunt.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/generator-openapi": patch +--- + +fix(generator-openapi): resolve schemas/params when operationId is missing