diff --git a/packages/app/api-contracts/src/sse/dualModeContracts.ts b/packages/app/api-contracts/src/sse/dualModeContracts.ts index aaa7235dd..cf070d5f7 100644 --- a/packages/app/api-contracts/src/sse/dualModeContracts.ts +++ b/packages/app/api-contracts/src/sse/dualModeContracts.ts @@ -1,5 +1,5 @@ import type { z } from 'zod/v4' -import type { RoutePathResolver } from '../apiContracts.ts' +import type { CommonRouteDefinitionMetadata, RoutePathResolver } from '../apiContracts.ts' import type { HttpStatusCode } from '../HttpStatusCodes.ts' import type { SSEMethod } from './sseContracts.ts' import type { SSEEventSchemas } from './sseTypes.ts' @@ -55,6 +55,10 @@ export type DualModeContractDefinition< responseBodySchemasByStatusCode?: ResponseSchemasByStatusCode serverSentEventSchemas: Events isDualMode: true + metadata?: CommonRouteDefinitionMetadata + description?: string + summary?: string + tags?: readonly string[] } /** @@ -75,4 +79,8 @@ export type AnyDualModeContractDefinition = { responseBodySchemasByStatusCode?: Partial> serverSentEventSchemas: SSEEventSchemas isDualMode: true + metadata?: CommonRouteDefinitionMetadata + description?: string + summary?: string + tags?: readonly string[] } diff --git a/packages/app/api-contracts/src/sse/sseContractBuilders.metadata.spec.ts b/packages/app/api-contracts/src/sse/sseContractBuilders.metadata.spec.ts new file mode 100644 index 000000000..6b0f068ca --- /dev/null +++ b/packages/app/api-contracts/src/sse/sseContractBuilders.metadata.spec.ts @@ -0,0 +1,173 @@ +import { describe, expect, expectTypeOf, it } from 'vitest' +import { z } from 'zod/v4' +import { buildSseContract } from './sseContractBuilders.ts' + +const SCHEMA = z.object({}) +const EVENTS = { data: z.object({ value: z.string() }) } + +type Metadata = { + myTestProp?: string[] + mySecondTestProp?: number +} + +declare module '../apiContracts.ts' { + interface CommonRouteDefinitionMetadata extends Metadata {} +} + +describe('buildSseContract metadata augmentation', () => { + describe('SSE GET route', () => { + it('should respect metadata type and reflect it on contract', () => { + const contract = buildSseContract({ + method: 'get', + pathResolver: () => '/', + serverSentEventSchemas: EVENTS, + metadata: { + myTestProp: ['test'], + mySecondTestProp: 1, + extra: 'extra field', + }, + }) + + expectTypeOf(contract.metadata).toMatchTypeOf() + expect(contract.metadata).toEqual({ + myTestProp: ['test'], + mySecondTestProp: 1, + extra: 'extra field', + }) + }) + + it('should reflect description, summary, and tags on contract', () => { + const contract = buildSseContract({ + method: 'get', + pathResolver: () => '/', + serverSentEventSchemas: EVENTS, + description: 'Stream events', + summary: 'Event stream', + tags: ['streaming', 'sse'], + }) + + expect(contract.description).toBe('Stream events') + expect(contract.summary).toBe('Event stream') + expect(contract.tags).toEqual(['streaming', 'sse']) + }) + }) + + describe('SSE POST route', () => { + it('should respect metadata type and reflect it on contract', () => { + const contract = buildSseContract({ + method: 'post', + pathResolver: () => '/', + requestBodySchema: SCHEMA, + serverSentEventSchemas: EVENTS, + metadata: { + myTestProp: ['test2'], + mySecondTestProp: 2, + extra: 'extra field2', + }, + }) + + expectTypeOf(contract.metadata).toMatchTypeOf() + expect(contract.metadata).toEqual({ + myTestProp: ['test2'], + mySecondTestProp: 2, + extra: 'extra field2', + }) + }) + + it('should reflect description, summary, and tags on contract', () => { + const contract = buildSseContract({ + method: 'post', + pathResolver: () => '/', + requestBodySchema: SCHEMA, + serverSentEventSchemas: EVENTS, + description: 'Stream with body', + summary: 'Body stream', + tags: ['streaming'], + }) + + expect(contract.description).toBe('Stream with body') + expect(contract.summary).toBe('Body stream') + expect(contract.tags).toEqual(['streaming']) + }) + }) + + describe('Dual-mode GET route', () => { + it('should respect metadata type and reflect it on contract', () => { + const contract = buildSseContract({ + method: 'get', + pathResolver: () => '/', + successResponseBodySchema: SCHEMA, + serverSentEventSchemas: EVENTS, + metadata: { + myTestProp: ['test3'], + mySecondTestProp: 3, + extra: 'extra field3', + }, + }) + + expectTypeOf(contract.metadata).toMatchTypeOf() + expect(contract.metadata).toEqual({ + myTestProp: ['test3'], + mySecondTestProp: 3, + extra: 'extra field3', + }) + }) + + it('should reflect description, summary, and tags on contract', () => { + const contract = buildSseContract({ + method: 'get', + pathResolver: () => '/', + successResponseBodySchema: SCHEMA, + serverSentEventSchemas: EVENTS, + description: 'Dual-mode GET', + summary: 'GET dual', + tags: ['dual-mode', 'sse'], + }) + + expect(contract.description).toBe('Dual-mode GET') + expect(contract.summary).toBe('GET dual') + expect(contract.tags).toEqual(['dual-mode', 'sse']) + }) + }) + + describe('Dual-mode POST route', () => { + it('should respect metadata type and reflect it on contract', () => { + const contract = buildSseContract({ + method: 'post', + pathResolver: () => '/', + requestBodySchema: SCHEMA, + successResponseBodySchema: SCHEMA, + serverSentEventSchemas: EVENTS, + metadata: { + myTestProp: ['test4'], + mySecondTestProp: 4, + extra: 'extra field4', + }, + }) + + expectTypeOf(contract.metadata).toMatchTypeOf() + expect(contract.metadata).toEqual({ + myTestProp: ['test4'], + mySecondTestProp: 4, + extra: 'extra field4', + }) + }) + + it('should reflect description, summary, and tags on contract', () => { + const contract = buildSseContract({ + method: 'post', + pathResolver: () => '/', + requestBodySchema: SCHEMA, + successResponseBodySchema: SCHEMA, + serverSentEventSchemas: EVENTS, + description: 'Dual-mode POST', + summary: 'POST dual', + tags: ['dual-mode'], + }) + + expect(contract.description).toBe('Dual-mode POST') + expect(contract.summary).toBe('POST dual') + expect(contract.tags).toEqual(['dual-mode']) + }) + }) +}) diff --git a/packages/app/api-contracts/src/sse/sseContractBuilders.ts b/packages/app/api-contracts/src/sse/sseContractBuilders.ts index 1a3608aba..c76123fb3 100644 --- a/packages/app/api-contracts/src/sse/sseContractBuilders.ts +++ b/packages/app/api-contracts/src/sse/sseContractBuilders.ts @@ -1,5 +1,5 @@ import type { z } from 'zod/v4' -import type { RoutePathResolver } from '../apiContracts.ts' +import type { CommonRouteDefinitionMetadata, RoutePathResolver } from '../apiContracts.ts' import type { HttpStatusCode } from '../HttpStatusCodes.ts' import type { DualModeContractDefinition } from './dualModeContracts.ts' import type { SSEContractDefinition } from './sseContracts.ts' @@ -40,6 +40,10 @@ export type SSEGetContractConfig< responseBodySchemasByStatusCode?: ResponseSchemasByStatusCode requestBodySchema?: never successResponseBodySchema?: never + metadata?: CommonRouteDefinitionMetadata + description?: string + summary?: string + tags?: readonly string[] } /** @@ -78,6 +82,10 @@ export type SSEPayloadContractConfig< */ responseBodySchemasByStatusCode?: ResponseSchemasByStatusCode successResponseBodySchema?: never + metadata?: CommonRouteDefinitionMetadata + description?: string + summary?: string + tags?: readonly string[] } /** @@ -130,6 +138,10 @@ export type DualModeGetContractConfig< responseBodySchemasByStatusCode?: ResponseSchemasByStatusCode serverSentEventSchemas: Events requestBodySchema?: never + metadata?: CommonRouteDefinitionMetadata + description?: string + summary?: string + tags?: readonly string[] } /** @@ -183,6 +195,10 @@ export type DualModePayloadContractConfig< */ responseBodySchemasByStatusCode?: ResponseSchemasByStatusCode serverSentEventSchemas: Events + metadata?: CommonRouteDefinitionMetadata + description?: string + summary?: string + tags?: readonly string[] } /** @@ -250,6 +266,10 @@ function buildBaseFields(config: any, hasBody: boolean) { requestHeaderSchema: config.requestHeaderSchema, requestBodySchema: hasBody ? config.requestBodySchema : undefined, serverSentEventSchemas: config.serverSentEventSchemas, + metadata: config.metadata, + description: config.description, + summary: config.summary, + tags: config.tags, } } diff --git a/packages/app/api-contracts/src/sse/sseContracts.ts b/packages/app/api-contracts/src/sse/sseContracts.ts index 41d41011c..20d8af6f5 100644 --- a/packages/app/api-contracts/src/sse/sseContracts.ts +++ b/packages/app/api-contracts/src/sse/sseContracts.ts @@ -1,5 +1,5 @@ import type { z } from 'zod/v4' -import type { RoutePathResolver } from '../apiContracts.ts' +import type { CommonRouteDefinitionMetadata, RoutePathResolver } from '../apiContracts.ts' import type { HttpStatusCode } from '../HttpStatusCodes.ts' import type { SSEEventSchemas } from './sseTypes.ts' @@ -58,6 +58,10 @@ export type SSEContractDefinition< */ responseBodySchemasByStatusCode?: ResponseSchemasByStatusCode isSSE: true + metadata?: CommonRouteDefinitionMetadata + description?: string + summary?: string + tags?: readonly string[] } /** @@ -75,4 +79,8 @@ export type AnySSEContractDefinition = { serverSentEventSchemas: SSEEventSchemas responseBodySchemasByStatusCode?: Partial> isSSE: true + metadata?: CommonRouteDefinitionMetadata + description?: string + summary?: string + tags?: readonly string[] }