From c1e97214bbfe14c6d3c9bad9c5e98e425b730bef Mon Sep 17 00:00:00 2001 From: David Boyne Date: Thu, 26 Feb 2026 17:36:12 +0000 Subject: [PATCH] feat(openapi): add operation frontmatter to generated messages Co-Authored-By: Claude Sonnet 4.5 --- .changeset/bright-dolphins-dance.md | 5 + .../generator-openapi/src/test/plugin.test.ts | 114 ++++++++++++++++++ .../generator-openapi/src/utils/messages.ts | 18 +++ 3 files changed, 137 insertions(+) create mode 100644 .changeset/bright-dolphins-dance.md diff --git a/.changeset/bright-dolphins-dance.md b/.changeset/bright-dolphins-dance.md new file mode 100644 index 00000000..0ad9d671 --- /dev/null +++ b/.changeset/bright-dolphins-dance.md @@ -0,0 +1,5 @@ +--- +'@eventcatalog/generator-openapi': minor +--- + +Add operation frontmatter (method, path, statusCodes) to OpenAPI-generated messages diff --git a/packages/generator-openapi/src/test/plugin.test.ts b/packages/generator-openapi/src/test/plugin.test.ts index 7382a7e6..b1b82011 100644 --- a/packages/generator-openapi/src/test/plugin.test.ts +++ b/packages/generator-openapi/src/test/plugin.test.ts @@ -1624,6 +1624,120 @@ describe('OpenAPI EventCatalog Plugin', () => { expect(command.id).toEqual('hello_createPets'); }); }); + + describe('operation frontmatter', () => { + it('messages are generated with the operation frontmatter containing method, path, and statusCodes', async () => { + const { getCommand } = utils(catalogDir); + + await plugin(config, { services: [{ path: join(openAPIExamples, 'petstore.yml'), id: 'swagger-petstore' }] }); + + const command = await getCommand('createPets'); + + expect((command as any).operation).toEqual({ + method: 'POST', + path: '/pets', + }); + }); + + it('messages with response status codes include statusCodes in the operation frontmatter', async () => { + const { getCommand } = utils(catalogDir); + + await plugin(config, { services: [{ path: join(openAPIExamples, 'petstore.yml'), id: 'swagger-petstore' }] }); + + const command = await getCommand('updatePet'); + + expect((command as any).operation).toEqual({ + method: 'PUT', + path: '/pets/{petId}', + statusCodes: ['200', '400', '404'], + }); + }); + + it('GET messages include the operation frontmatter with method and path', async () => { + const { getQuery } = utils(catalogDir); + + await plugin(config, { services: [{ path: join(openAPIExamples, 'petstore.yml'), id: 'swagger-petstore' }] }); + + const query = await getQuery('list-pets'); + + expect((query as any).operation).toEqual({ + method: 'GET', + path: '/pets', + statusCodes: ['200'], + }); + }); + + it('DELETE messages include the operation frontmatter with method and path', async () => { + const { getCommand } = utils(catalogDir); + + await plugin(config, { services: [{ path: join(openAPIExamples, 'petstore.yml'), id: 'swagger-petstore' }] }); + + const command = await getCommand('deletePet'); + + expect((command as any).operation).toEqual({ + method: 'DELETE', + path: '/pets/{petId}', + statusCodes: ['400', '404'], + }); + }); + + it('operation frontmatter is always overridden and not persisted from existing messages', async () => { + const { writeCommand, getCommand } = utils(catalogDir); + + // Write a message with a different operation value + await writeCommand({ + id: 'createPets', + name: 'createPets', + version: '1.0.0', + summary: 'Create a pet', + markdown: '', + operation: { + method: 'GET', + path: '/old-path', + statusCodes: ['999'], + }, + } as any); + + await plugin(config, { services: [{ path: join(openAPIExamples, 'petstore.yml'), id: 'swagger-petstore' }] }); + + const command = await getCommand('createPets', '1.0.0'); + + // Operation should be overridden with the value from the OpenAPI spec, not persisted + expect((command as any).operation).toEqual({ + method: 'POST', + path: '/pets', + }); + }); + + it('event messages include the operation frontmatter', async () => { + const { getEvent } = utils(catalogDir); + + await plugin(config, { services: [{ path: join(openAPIExamples, 'petstore.yml'), id: 'swagger-petstore' }] }); + + const event = await getEvent('petAdopted'); + + expect((event as any).operation).toEqual({ + method: 'POST', + path: '/pets/{petId}/adopted', + }); + }); + + it('statusCodes does not include the "default" response code', async () => { + const { getQuery } = utils(catalogDir); + + await plugin(config, { services: [{ path: join(openAPIExamples, 'petstore.yml'), id: 'swagger-petstore' }] }); + + const query = await getQuery('showPetById'); + + // showPetById has responses: 200 and default - default should be excluded + expect((query as any).operation).toEqual({ + method: 'GET', + path: '/pets/{petId}', + statusCodes: ['200'], + }); + expect((query as any).operation.statusCodes).not.toContain('default'); + }); + }); }); describe('$ref', () => { diff --git a/packages/generator-openapi/src/utils/messages.ts b/packages/generator-openapi/src/utils/messages.ts index d4db6acf..ead5feec 100644 --- a/packages/generator-openapi/src/utils/messages.ts +++ b/packages/generator-openapi/src/utils/messages.ts @@ -206,6 +206,23 @@ export const buildMessage = async ( uniqueIdentifier = [serviceId, uniqueIdentifier].join(separator); } + // Build the operation frontmatter (method, path, statusCodes) + const validOperationMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']; + const operationMethod = operation.method.toUpperCase(); + let operationFrontmatter: { method?: string; path?: string; statusCodes?: string[] } | undefined; + + if (validOperationMethods.includes(operationMethod)) { + const statusCodes = requestBodiesAndResponses?.responses + ? Object.keys(requestBodiesAndResponses.responses).filter((code) => code !== 'default') + : []; + + operationFrontmatter = { + method: operationMethod as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', + path: operation.path, + ...(statusCodes.length > 0 ? { statusCodes } : {}), + }; + } + return { id: extensions['x-eventcatalog-message-id'] || uniqueIdentifier, version: messageVersion, @@ -223,5 +240,6 @@ export const buildMessage = async ( messageName, ...(extensions['x-eventcatalog-draft'] ? { draft: true } : {}), ...(operation.deprecated ? { deprecated: operation.deprecated } : {}), + ...(operationFrontmatter ? { operation: operationFrontmatter } : {}), }; };