@navios/openapi 0.9.1 → 1.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +1 -2
  3. package/dist/legacy-compat/__type-tests__/tsconfig.tsbuildinfo +1 -1
  4. package/dist/src/__tests__/endpoint-scanner.service.spec.d.mts +2 -0
  5. package/dist/src/__tests__/endpoint-scanner.service.spec.d.mts.map +1 -0
  6. package/dist/src/__tests__/openapi-generator.service.spec.d.mts +2 -0
  7. package/dist/src/__tests__/openapi-generator.service.spec.d.mts.map +1 -0
  8. package/dist/src/services/endpoint-scanner.service.d.mts +2 -2
  9. package/dist/src/services/endpoint-scanner.service.d.mts.map +1 -1
  10. package/dist/src/services/path-builder.service.d.mts +13 -1
  11. package/dist/src/services/path-builder.service.d.mts.map +1 -1
  12. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  13. package/dist/tsconfig.spec.tsbuildinfo +1 -1
  14. package/dist/tsconfig.tsbuildinfo +1 -1
  15. package/lib/{index-BYd1gzJQ.d.cts → index-Bzkj5ltS.d.cts} +22 -10
  16. package/lib/{index-BYd1gzJQ.d.cts.map → index-Bzkj5ltS.d.cts.map} +1 -1
  17. package/lib/{index-KzCwlPFD.d.mts → index-CwN9u2YO.d.mts} +22 -10
  18. package/lib/{index-KzCwlPFD.d.mts.map → index-CwN9u2YO.d.mts.map} +1 -1
  19. package/lib/index.cjs +1 -1
  20. package/lib/index.d.cts +1 -1
  21. package/lib/index.d.mts +1 -1
  22. package/lib/index.mjs +1 -1
  23. package/lib/legacy-compat/index.cjs +1 -1
  24. package/lib/legacy-compat/index.d.cts +7 -7
  25. package/lib/legacy-compat/index.d.mts +1 -1
  26. package/lib/legacy-compat/index.mjs +1 -1
  27. package/lib/{services-kEHEZqLZ.cjs → services-B16xN1Kh.cjs} +44 -12
  28. package/lib/services-B16xN1Kh.cjs.map +1 -0
  29. package/lib/{services-MFCyRMd8.mjs → services-B7UR1D1X.mjs} +45 -13
  30. package/lib/services-B7UR1D1X.mjs.map +1 -0
  31. package/package.json +4 -5
  32. package/src/__tests__/endpoint-scanner.service.spec.mts +362 -0
  33. package/src/__tests__/metadata.spec.mts +1 -1
  34. package/src/__tests__/openapi-generator.service.spec.mts +394 -0
  35. package/src/__tests__/services.spec.mts +1 -1
  36. package/src/services/endpoint-scanner.service.mts +14 -8
  37. package/src/services/path-builder.service.mts +85 -27
  38. package/lib/services-MFCyRMd8.mjs.map +0 -1
  39. package/lib/services-kEHEZqLZ.cjs.map +0 -1
@@ -0,0 +1,394 @@
1
+ import type { ModuleMetadata } from '@navios/core'
2
+
3
+ import { Logger } from '@navios/core'
4
+ import { TestContainer } from '@navios/di/testing'
5
+
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7
+
8
+ import type { DiscoveredEndpoint } from '../services/endpoint-scanner.service.mjs'
9
+ import type { OpenApiGeneratorOptions } from '../services/openapi-generator.service.mjs'
10
+
11
+ import { EndpointScannerService } from '../services/endpoint-scanner.service.mjs'
12
+ import { OpenApiGeneratorService } from '../services/openapi-generator.service.mjs'
13
+ import { PathBuilderService } from '../services/path-builder.service.mjs'
14
+
15
+ // Mock logger
16
+ const mockLogger = {
17
+ debug: vi.fn(),
18
+ warn: vi.fn(),
19
+ error: vi.fn(),
20
+ }
21
+
22
+ // Mock scanner
23
+ const mockScanner = {
24
+ scan: vi.fn().mockReturnValue([]),
25
+ }
26
+
27
+ // Mock path builder
28
+ const mockPathBuilder = {
29
+ build: vi.fn().mockReturnValue({
30
+ path: '/test',
31
+ pathItem: { get: { responses: { 200: { description: 'OK' } } } },
32
+ }),
33
+ }
34
+
35
+ describe('OpenApiGeneratorService', () => {
36
+ let container: TestContainer
37
+
38
+ beforeEach(() => {
39
+ container = new TestContainer()
40
+ // Bind required dependencies
41
+ container.bind(Logger).toValue(mockLogger as any)
42
+ container.bind(EndpointScannerService).toValue(mockScanner as any)
43
+ container.bind(PathBuilderService).toValue(mockPathBuilder as any)
44
+ vi.clearAllMocks()
45
+ })
46
+
47
+ afterEach(async () => {
48
+ await container.dispose()
49
+ })
50
+
51
+ describe('generate', () => {
52
+ it('should generate basic OpenAPI document', async () => {
53
+ const generator = await container.get(OpenApiGeneratorService)
54
+
55
+ const modules = new Map<string, ModuleMetadata>()
56
+ const options: OpenApiGeneratorOptions = {
57
+ info: {
58
+ title: 'Test API',
59
+ version: '1.0.0',
60
+ },
61
+ }
62
+
63
+ const document = generator.generate(modules, options)
64
+
65
+ expect(document.openapi).toBe('3.1.0')
66
+ expect(document.info.title).toBe('Test API')
67
+ expect(document.info.version).toBe('1.0.0')
68
+ expect(document.paths).toBeDefined()
69
+ })
70
+
71
+ it('should include optional info fields', async () => {
72
+ const generator = await container.get(OpenApiGeneratorService)
73
+
74
+ const modules = new Map<string, ModuleMetadata>()
75
+ const options: OpenApiGeneratorOptions = {
76
+ info: {
77
+ title: 'Test API',
78
+ version: '1.0.0',
79
+ description: 'A test API',
80
+ termsOfService: 'https://example.com/terms',
81
+ contact: {
82
+ name: 'Support',
83
+ email: 'support@example.com',
84
+ url: 'https://example.com',
85
+ },
86
+ license: {
87
+ name: 'MIT',
88
+ url: 'https://opensource.org/licenses/MIT',
89
+ },
90
+ },
91
+ }
92
+
93
+ const document = generator.generate(modules, options)
94
+
95
+ expect(document.info.description).toBe('A test API')
96
+ expect(document.info.termsOfService).toBe('https://example.com/terms')
97
+ expect(document.info.contact?.name).toBe('Support')
98
+ expect(document.info.license?.name).toBe('MIT')
99
+ })
100
+
101
+ it('should include servers when provided', async () => {
102
+ const generator = await container.get(OpenApiGeneratorService)
103
+
104
+ const modules = new Map<string, ModuleMetadata>()
105
+ const options: OpenApiGeneratorOptions = {
106
+ info: {
107
+ title: 'Test API',
108
+ version: '1.0.0',
109
+ },
110
+ servers: [
111
+ { url: 'https://api.example.com', description: 'Production' },
112
+ { url: 'https://staging.example.com', description: 'Staging' },
113
+ ],
114
+ }
115
+
116
+ const document = generator.generate(modules, options)
117
+
118
+ expect(document.servers).toHaveLength(2)
119
+ expect(document.servers?.[0].url).toBe('https://api.example.com')
120
+ expect(document.servers?.[1].description).toBe('Staging')
121
+ })
122
+
123
+ it('should not include servers when empty array', async () => {
124
+ const generator = await container.get(OpenApiGeneratorService)
125
+
126
+ const modules = new Map<string, ModuleMetadata>()
127
+ const options: OpenApiGeneratorOptions = {
128
+ info: {
129
+ title: 'Test API',
130
+ version: '1.0.0',
131
+ },
132
+ servers: [],
133
+ }
134
+
135
+ const document = generator.generate(modules, options)
136
+
137
+ expect(document.servers).toBeUndefined()
138
+ })
139
+
140
+ it('should include external docs when provided', async () => {
141
+ const generator = await container.get(OpenApiGeneratorService)
142
+
143
+ const modules = new Map<string, ModuleMetadata>()
144
+ const options: OpenApiGeneratorOptions = {
145
+ info: {
146
+ title: 'Test API',
147
+ version: '1.0.0',
148
+ },
149
+ externalDocs: {
150
+ url: 'https://docs.example.com',
151
+ description: 'Full documentation',
152
+ },
153
+ }
154
+
155
+ const document = generator.generate(modules, options)
156
+
157
+ expect(document.externalDocs?.url).toBe('https://docs.example.com')
158
+ expect(document.externalDocs?.description).toBe('Full documentation')
159
+ })
160
+
161
+ it('should include security schemes when provided', async () => {
162
+ const generator = await container.get(OpenApiGeneratorService)
163
+
164
+ const modules = new Map<string, ModuleMetadata>()
165
+ const options: OpenApiGeneratorOptions = {
166
+ info: {
167
+ title: 'Test API',
168
+ version: '1.0.0',
169
+ },
170
+ securitySchemes: {
171
+ bearerAuth: {
172
+ type: 'http',
173
+ scheme: 'bearer',
174
+ bearerFormat: 'JWT',
175
+ },
176
+ apiKey: {
177
+ type: 'apiKey',
178
+ in: 'header',
179
+ name: 'X-API-Key',
180
+ },
181
+ },
182
+ }
183
+
184
+ const document = generator.generate(modules, options)
185
+
186
+ expect(document.components?.securitySchemes).toBeDefined()
187
+ const bearerAuth = document.components?.securitySchemes?.bearerAuth
188
+ const apiKey = document.components?.securitySchemes?.apiKey
189
+ expect(
190
+ bearerAuth && 'type' in bearerAuth ? bearerAuth.type : undefined,
191
+ ).toBe('http')
192
+ expect(apiKey && 'type' in apiKey ? apiKey.type : undefined).toBe(
193
+ 'apiKey',
194
+ )
195
+ })
196
+
197
+ it('should include global security requirements', async () => {
198
+ const generator = await container.get(OpenApiGeneratorService)
199
+
200
+ const modules = new Map<string, ModuleMetadata>()
201
+ const options: OpenApiGeneratorOptions = {
202
+ info: {
203
+ title: 'Test API',
204
+ version: '1.0.0',
205
+ },
206
+ security: [{ bearerAuth: [] }],
207
+ }
208
+
209
+ const document = generator.generate(modules, options)
210
+
211
+ expect(document.security).toHaveLength(1)
212
+ expect(document.security?.[0]).toEqual({ bearerAuth: [] })
213
+ })
214
+
215
+ it('should build paths from discovered endpoints', async () => {
216
+ const mockEndpoints: DiscoveredEndpoint[] = [
217
+ {
218
+ module: {} as any,
219
+ controllerClass: class {},
220
+ controller: {} as any,
221
+ handler: { config: { method: 'GET', url: '/users' } } as any,
222
+ config: { method: 'GET', url: '/users' },
223
+ openApiMetadata: {
224
+ tags: ['users'],
225
+ summary: '',
226
+ description: '',
227
+ operationId: '',
228
+ deprecated: false,
229
+ excluded: false,
230
+ security: [],
231
+ },
232
+ },
233
+ ]
234
+
235
+ mockScanner.scan.mockReturnValue(mockEndpoints)
236
+ mockPathBuilder.build.mockReturnValue({
237
+ path: '/users',
238
+ pathItem: {
239
+ get: { tags: ['users'], responses: { 200: { description: 'OK' } } },
240
+ },
241
+ })
242
+
243
+ const generator = await container.get(OpenApiGeneratorService)
244
+
245
+ const modules = new Map<string, ModuleMetadata>()
246
+ const options: OpenApiGeneratorOptions = {
247
+ info: {
248
+ title: 'Test API',
249
+ version: '1.0.0',
250
+ },
251
+ }
252
+
253
+ const document = generator.generate(modules, options)
254
+
255
+ expect(mockScanner.scan).toHaveBeenCalledWith(modules)
256
+ expect(mockPathBuilder.build).toHaveBeenCalledWith(mockEndpoints[0])
257
+ expect(document.paths?.['/users']).toBeDefined()
258
+ })
259
+
260
+ it('should merge paths with different methods', async () => {
261
+ const mockEndpoints: DiscoveredEndpoint[] = [
262
+ {
263
+ module: {} as any,
264
+ controllerClass: class {},
265
+ controller: {} as any,
266
+ handler: { config: { method: 'GET', url: '/users' } } as any,
267
+ config: { method: 'GET', url: '/users' },
268
+ openApiMetadata: { tags: ['users'], excluded: false } as any,
269
+ },
270
+ {
271
+ module: {} as any,
272
+ controllerClass: class {},
273
+ controller: {} as any,
274
+ handler: { config: { method: 'POST', url: '/users' } } as any,
275
+ config: { method: 'POST', url: '/users' },
276
+ openApiMetadata: { tags: ['users'], excluded: false } as any,
277
+ },
278
+ ]
279
+
280
+ mockScanner.scan.mockReturnValue(mockEndpoints)
281
+ mockPathBuilder.build
282
+ .mockReturnValueOnce({
283
+ path: '/users',
284
+ pathItem: { get: { responses: { 200: { description: 'OK' } } } },
285
+ })
286
+ .mockReturnValueOnce({
287
+ path: '/users',
288
+ pathItem: {
289
+ post: { responses: { 201: { description: 'Created' } } },
290
+ },
291
+ })
292
+
293
+ const generator = await container.get(OpenApiGeneratorService)
294
+
295
+ const modules = new Map<string, ModuleMetadata>()
296
+ const options: OpenApiGeneratorOptions = {
297
+ info: {
298
+ title: 'Test API',
299
+ version: '1.0.0',
300
+ },
301
+ }
302
+
303
+ const document = generator.generate(modules, options)
304
+
305
+ expect(document.paths?.['/users']?.get).toBeDefined()
306
+ expect(document.paths?.['/users']?.post).toBeDefined()
307
+ })
308
+
309
+ it('should collect and merge tags', async () => {
310
+ const mockEndpoints: DiscoveredEndpoint[] = [
311
+ {
312
+ module: {} as any,
313
+ controllerClass: class {},
314
+ controller: {} as any,
315
+ handler: {} as any,
316
+ config: {} as any,
317
+ openApiMetadata: { tags: ['users', 'api'], excluded: false } as any,
318
+ },
319
+ {
320
+ module: {} as any,
321
+ controllerClass: class {},
322
+ controller: {} as any,
323
+ handler: {} as any,
324
+ config: {} as any,
325
+ openApiMetadata: { tags: ['orders'], excluded: false } as any,
326
+ },
327
+ ]
328
+
329
+ mockScanner.scan.mockReturnValue(mockEndpoints)
330
+
331
+ const generator = await container.get(OpenApiGeneratorService)
332
+
333
+ const modules = new Map<string, ModuleMetadata>()
334
+ const options: OpenApiGeneratorOptions = {
335
+ info: {
336
+ title: 'Test API',
337
+ version: '1.0.0',
338
+ },
339
+ tags: [{ name: 'users', description: 'User operations' }],
340
+ }
341
+
342
+ const document = generator.generate(modules, options)
343
+
344
+ expect(document.tags).toBeDefined()
345
+ expect(document.tags).toContainEqual({
346
+ name: 'users',
347
+ description: 'User operations',
348
+ })
349
+ expect(document.tags).toContainEqual({ name: 'api' })
350
+ expect(document.tags).toContainEqual({ name: 'orders' })
351
+ })
352
+
353
+ it('should not include tags when none discovered and none configured', async () => {
354
+ mockScanner.scan.mockReturnValue([])
355
+
356
+ const generator = await container.get(OpenApiGeneratorService)
357
+
358
+ const modules = new Map<string, ModuleMetadata>()
359
+ const options: OpenApiGeneratorOptions = {
360
+ info: {
361
+ title: 'Test API',
362
+ version: '1.0.0',
363
+ },
364
+ }
365
+
366
+ const document = generator.generate(modules, options)
367
+
368
+ expect(document.tags).toBeUndefined()
369
+ })
370
+
371
+ it('should log generation info', async () => {
372
+ mockScanner.scan.mockReturnValue([])
373
+
374
+ const generator = await container.get(OpenApiGeneratorService)
375
+
376
+ const modules = new Map<string, ModuleMetadata>()
377
+ const options: OpenApiGeneratorOptions = {
378
+ info: {
379
+ title: 'Test API',
380
+ version: '1.0.0',
381
+ },
382
+ }
383
+
384
+ generator.generate(modules, options)
385
+
386
+ expect(mockLogger.debug).toHaveBeenCalledWith(
387
+ 'Generating OpenAPI document',
388
+ )
389
+ expect(mockLogger.debug).toHaveBeenCalledWith(
390
+ expect.stringContaining('Generated OpenAPI document'),
391
+ )
392
+ })
393
+ })
394
+ })
@@ -1,4 +1,4 @@
1
- import { TestContainer } from '@navios/di/testing'
1
+ import { TestContainer } from '@navios/core/testing'
2
2
 
3
3
  import { afterEach, beforeEach, describe, expect, it } from 'vitest'
4
4
  import { z } from 'zod/v4'
@@ -1,11 +1,16 @@
1
+ import type { BaseEndpointOptions, EndpointOptions } from '@navios/builder'
1
2
  import type {
2
3
  ControllerMetadata,
3
4
  HandlerMetadata,
4
5
  ModuleMetadata,
5
6
  } from '@navios/core'
6
- import type { BaseEndpointConfig } from '@navios/builder'
7
7
 
8
- import { extractControllerMetadata, inject, Injectable, Logger } from '@navios/core'
8
+ import {
9
+ extractControllerMetadata,
10
+ inject,
11
+ Injectable,
12
+ Logger,
13
+ } from '@navios/core'
9
14
 
10
15
  import type { OpenApiEndpointMetadata } from '../metadata/openapi.metadata.mjs'
11
16
 
@@ -24,7 +29,7 @@ export interface DiscoveredEndpoint {
24
29
  /** Handler (endpoint) metadata */
25
30
  handler: HandlerMetadata<any>
26
31
  /** Endpoint configuration from @navios/builder */
27
- config: BaseEndpointConfig
32
+ config: EndpointOptions | BaseEndpointOptions
28
33
  /** Extracted OpenAPI metadata */
29
34
  openApiMetadata: OpenApiEndpointMetadata
30
35
  }
@@ -53,7 +58,10 @@ export class EndpointScannerService {
53
58
  const endpoints: DiscoveredEndpoint[] = []
54
59
 
55
60
  for (const [moduleName, moduleMetadata] of modules) {
56
- if (!moduleMetadata.controllers || moduleMetadata.controllers.size === 0) {
61
+ if (
62
+ !moduleMetadata.controllers ||
63
+ moduleMetadata.controllers.size === 0
64
+ ) {
57
65
  continue
58
66
  }
59
67
 
@@ -97,9 +105,7 @@ export class EndpointScannerService {
97
105
 
98
106
  // Skip excluded endpoints
99
107
  if (openApiMetadata.excluded) {
100
- this.logger.debug(
101
- `Skipping excluded endpoint: ${handler.classMethod}`,
102
- )
108
+ this.logger.debug(`Skipping excluded endpoint: ${handler.classMethod}`)
103
109
  continue
104
110
  }
105
111
 
@@ -108,7 +114,7 @@ export class EndpointScannerService {
108
114
  controllerClass,
109
115
  controller: controllerMeta,
110
116
  handler,
111
- config: handler.config as BaseEndpointConfig,
117
+ config: handler.config as EndpointOptions | BaseEndpointOptions,
112
118
  openApiMetadata,
113
119
  })
114
120
  }
@@ -1,4 +1,8 @@
1
- import type { BaseEndpointConfig } from '@navios/builder'
1
+ import type {
2
+ BaseEndpointOptions,
3
+ EndpointOptions,
4
+ ErrorSchemaRecord,
5
+ } from '@navios/builder'
2
6
  import type { HandlerMetadata } from '@navios/core'
3
7
  import type { oas31 } from 'zod-openapi'
4
8
 
@@ -60,8 +64,8 @@ export class PathBuilderService {
60
64
  deprecated: openApiMetadata.deprecated || undefined,
61
65
  externalDocs: openApiMetadata.externalDocs,
62
66
  security: openApiMetadata.security,
63
- parameters: this.buildParameters(config),
64
- requestBody: this.buildRequestBody(config, handler),
67
+ parameters: this.buildParameters(config as EndpointOptions),
68
+ requestBody: this.buildRequestBody(config as EndpointOptions, handler),
65
69
  responses: this.buildResponses(endpoint),
66
70
  }
67
71
 
@@ -98,20 +102,23 @@ export class PathBuilderService {
98
102
  */
99
103
  getEndpointType(
100
104
  handler: HandlerMetadata<any>,
101
- ): 'endpoint' | 'multipart' | 'stream' {
105
+ ): 'endpoint' | 'multipart' | 'stream' | 'unknown' {
106
+ if (handler.adapterToken === EndpointAdapterToken) {
107
+ return 'endpoint'
108
+ }
102
109
  if (handler.adapterToken === MultipartAdapterToken) {
103
110
  return 'multipart'
104
111
  }
105
112
  if (handler.adapterToken === StreamAdapterToken) {
106
113
  return 'stream'
107
114
  }
108
- return 'endpoint'
115
+ return 'unknown'
109
116
  }
110
117
 
111
118
  /**
112
119
  * Builds OpenAPI parameters from endpoint config
113
120
  */
114
- private buildParameters(config: BaseEndpointConfig): ParameterObject[] {
121
+ private buildParameters(config: EndpointOptions): ParameterObject[] {
115
122
  const params: ParameterObject[] = []
116
123
 
117
124
  // URL parameters (from $paramName in URL)
@@ -151,7 +158,7 @@ export class PathBuilderService {
151
158
  * Builds request body based on endpoint type
152
159
  */
153
160
  private buildRequestBody(
154
- config: BaseEndpointConfig,
161
+ config: EndpointOptions,
155
162
  handler: HandlerMetadata<any>,
156
163
  ): RequestBodyObject | undefined {
157
164
  const type = this.getEndpointType(handler)
@@ -171,7 +178,7 @@ export class PathBuilderService {
171
178
  * Builds request body for JSON endpoints
172
179
  */
173
180
  private buildJsonRequestBody(
174
- config: BaseEndpointConfig,
181
+ config: EndpointOptions,
175
182
  ): RequestBodyObject | undefined {
176
183
  if (!config.requestSchema) {
177
184
  return undefined
@@ -193,7 +200,7 @@ export class PathBuilderService {
193
200
  * Builds request body for multipart endpoints
194
201
  */
195
202
  private buildMultipartRequestBody(
196
- config: BaseEndpointConfig,
203
+ config: EndpointOptions,
197
204
  ): RequestBodyObject {
198
205
  if (!config.requestSchema) {
199
206
  return {
@@ -206,7 +213,8 @@ export class PathBuilderService {
206
213
  }
207
214
  }
208
215
 
209
- const schema = this.schemaConverter.convert(config.requestSchema).schema as SchemaObject
216
+ const schema = this.schemaConverter.convert(config.requestSchema)
217
+ .schema as SchemaObject
210
218
 
211
219
  // Transform schema properties to handle File types
212
220
  const properties = this.schemaConverter.transformFileProperties(
@@ -239,62 +247,112 @@ export class PathBuilderService {
239
247
  return this.buildStreamResponses(endpoint)
240
248
  case 'multipart':
241
249
  case 'endpoint':
250
+ return this.buildJsonResponses(config as EndpointOptions, handler)
251
+ case 'unknown':
252
+ return this.buildUnknownResponses(config)
242
253
  default:
243
- return this.buildJsonResponses(config, handler)
254
+ return this.buildUnknownResponses(config)
244
255
  }
245
256
  }
246
257
 
258
+ /**
259
+ * Builds error responses from errorSchema
260
+ *
261
+ * @param errorSchema - Optional record mapping status codes to Zod schemas
262
+ * @returns ResponsesObject with error responses, or empty object if no errorSchema
263
+ */
264
+ private buildErrorResponses(
265
+ errorSchema?: ErrorSchemaRecord,
266
+ ): ResponsesObject {
267
+ if (!errorSchema) {
268
+ return {}
269
+ }
270
+
271
+ const errorResponses: ResponsesObject = {}
272
+
273
+ for (const [statusCode, schema] of Object.entries(errorSchema)) {
274
+ const { schema: convertedSchema } = this.schemaConverter.convert(schema)
275
+ errorResponses[statusCode] = {
276
+ description: `Error response (${statusCode})`,
277
+ content: {
278
+ 'application/json': {
279
+ schema: convertedSchema,
280
+ },
281
+ },
282
+ }
283
+ }
284
+
285
+ return errorResponses
286
+ }
287
+
247
288
  /**
248
289
  * Builds responses for JSON endpoints
249
290
  */
250
291
  private buildJsonResponses(
251
- config: BaseEndpointConfig,
292
+ config: EndpointOptions,
252
293
  handler: HandlerMetadata<any>,
253
294
  ): ResponsesObject {
254
295
  const successCode = handler.successStatusCode?.toString() ?? '200'
296
+ const responses: ResponsesObject = {}
255
297
 
298
+ // Build success response
256
299
  if (!config.responseSchema) {
257
- return {
258
- [successCode]: {
259
- description: 'Successful response',
260
- },
300
+ responses[successCode] = {
301
+ description: 'Successful response',
261
302
  }
262
- }
263
-
264
- const { schema } = this.schemaConverter.convert(config.responseSchema)
265
-
266
- return {
267
- [successCode]: {
303
+ } else {
304
+ const { schema } = this.schemaConverter.convert(config.responseSchema)
305
+ responses[successCode] = {
268
306
  description: 'Successful response',
269
307
  content: {
270
308
  'application/json': {
271
309
  schema,
272
310
  },
273
311
  },
274
- },
312
+ }
275
313
  }
314
+
315
+ // Add error responses from errorSchema
316
+ Object.assign(responses, this.buildErrorResponses(config.errorSchema))
317
+
318
+ return responses
276
319
  }
277
320
 
278
321
  /**
279
322
  * Builds responses for stream endpoints
280
323
  */
281
324
  private buildStreamResponses(endpoint: DiscoveredEndpoint): ResponsesObject {
282
- const { openApiMetadata, handler } = endpoint
325
+ const { config, openApiMetadata, handler } = endpoint
283
326
  const successCode = handler.successStatusCode?.toString() ?? '200'
284
327
 
285
328
  const contentType =
286
329
  openApiMetadata.stream?.contentType ?? 'application/octet-stream'
287
- const description =
288
- openApiMetadata.stream?.description ?? 'Stream response'
330
+ const description = openApiMetadata.stream?.description ?? 'Stream response'
289
331
 
290
332
  const content: ContentObject = this.getStreamContent(contentType)
291
333
 
292
- return {
334
+ const responses: ResponsesObject = {
293
335
  [successCode]: {
294
336
  description,
295
337
  content,
296
338
  },
297
339
  }
340
+
341
+ // Add error responses from errorSchema
342
+ Object.assign(responses, this.buildErrorResponses(config.errorSchema))
343
+
344
+ return responses
345
+ }
346
+
347
+ /**
348
+ * Builds responses for unknown endpoint types.
349
+ * Unknown types have no success response but can have error responses.
350
+ */
351
+ private buildUnknownResponses(
352
+ config: EndpointOptions | BaseEndpointOptions,
353
+ ): ResponsesObject {
354
+ // Only include error responses, no success response
355
+ return this.buildErrorResponses(config.errorSchema)
298
356
  }
299
357
 
300
358
  /**