@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.
- package/CHANGELOG.md +38 -0
- package/README.md +1 -2
- package/dist/legacy-compat/__type-tests__/tsconfig.tsbuildinfo +1 -1
- package/dist/src/__tests__/endpoint-scanner.service.spec.d.mts +2 -0
- package/dist/src/__tests__/endpoint-scanner.service.spec.d.mts.map +1 -0
- package/dist/src/__tests__/openapi-generator.service.spec.d.mts +2 -0
- package/dist/src/__tests__/openapi-generator.service.spec.d.mts.map +1 -0
- package/dist/src/services/endpoint-scanner.service.d.mts +2 -2
- package/dist/src/services/endpoint-scanner.service.d.mts.map +1 -1
- package/dist/src/services/path-builder.service.d.mts +13 -1
- package/dist/src/services/path-builder.service.d.mts.map +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/tsconfig.spec.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/lib/{index-BYd1gzJQ.d.cts → index-Bzkj5ltS.d.cts} +22 -10
- package/lib/{index-BYd1gzJQ.d.cts.map → index-Bzkj5ltS.d.cts.map} +1 -1
- package/lib/{index-KzCwlPFD.d.mts → index-CwN9u2YO.d.mts} +22 -10
- package/lib/{index-KzCwlPFD.d.mts.map → index-CwN9u2YO.d.mts.map} +1 -1
- package/lib/index.cjs +1 -1
- package/lib/index.d.cts +1 -1
- package/lib/index.d.mts +1 -1
- package/lib/index.mjs +1 -1
- package/lib/legacy-compat/index.cjs +1 -1
- package/lib/legacy-compat/index.d.cts +7 -7
- package/lib/legacy-compat/index.d.mts +1 -1
- package/lib/legacy-compat/index.mjs +1 -1
- package/lib/{services-kEHEZqLZ.cjs → services-B16xN1Kh.cjs} +44 -12
- package/lib/services-B16xN1Kh.cjs.map +1 -0
- package/lib/{services-MFCyRMd8.mjs → services-B7UR1D1X.mjs} +45 -13
- package/lib/services-B7UR1D1X.mjs.map +1 -0
- package/package.json +4 -5
- package/src/__tests__/endpoint-scanner.service.spec.mts +362 -0
- package/src/__tests__/metadata.spec.mts +1 -1
- package/src/__tests__/openapi-generator.service.spec.mts +394 -0
- package/src/__tests__/services.spec.mts +1 -1
- package/src/services/endpoint-scanner.service.mts +14 -8
- package/src/services/path-builder.service.mts +85 -27
- package/lib/services-MFCyRMd8.mjs.map +0 -1
- 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,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 {
|
|
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:
|
|
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 (
|
|
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
|
|
117
|
+
config: handler.config as EndpointOptions | BaseEndpointOptions,
|
|
112
118
|
openApiMetadata,
|
|
113
119
|
})
|
|
114
120
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
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 '
|
|
115
|
+
return 'unknown'
|
|
109
116
|
}
|
|
110
117
|
|
|
111
118
|
/**
|
|
112
119
|
* Builds OpenAPI parameters from endpoint config
|
|
113
120
|
*/
|
|
114
|
-
private buildParameters(config:
|
|
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:
|
|
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:
|
|
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:
|
|
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)
|
|
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.
|
|
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:
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
description: 'Successful response',
|
|
260
|
-
},
|
|
300
|
+
responses[successCode] = {
|
|
301
|
+
description: 'Successful response',
|
|
261
302
|
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
/**
|