@navios/openapi 0.7.0

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 (87) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +260 -0
  3. package/dist/src/__tests__/decorators.spec.d.mts +2 -0
  4. package/dist/src/__tests__/decorators.spec.d.mts.map +1 -0
  5. package/dist/src/__tests__/metadata.spec.d.mts +2 -0
  6. package/dist/src/__tests__/metadata.spec.d.mts.map +1 -0
  7. package/dist/src/__tests__/services.spec.d.mts +2 -0
  8. package/dist/src/__tests__/services.spec.d.mts.map +1 -0
  9. package/dist/src/decorators/api-deprecated.decorator.d.mts +33 -0
  10. package/dist/src/decorators/api-deprecated.decorator.d.mts.map +1 -0
  11. package/dist/src/decorators/api-exclude.decorator.d.mts +26 -0
  12. package/dist/src/decorators/api-exclude.decorator.d.mts.map +1 -0
  13. package/dist/src/decorators/api-operation.decorator.d.mts +58 -0
  14. package/dist/src/decorators/api-operation.decorator.d.mts.map +1 -0
  15. package/dist/src/decorators/api-security.decorator.d.mts +36 -0
  16. package/dist/src/decorators/api-security.decorator.d.mts.map +1 -0
  17. package/dist/src/decorators/api-stream.decorator.d.mts +50 -0
  18. package/dist/src/decorators/api-stream.decorator.d.mts.map +1 -0
  19. package/dist/src/decorators/api-summary.decorator.d.mts +24 -0
  20. package/dist/src/decorators/api-summary.decorator.d.mts.map +1 -0
  21. package/dist/src/decorators/api-tag.decorator.d.mts +42 -0
  22. package/dist/src/decorators/api-tag.decorator.d.mts.map +1 -0
  23. package/dist/src/decorators/index.d.mts +8 -0
  24. package/dist/src/decorators/index.d.mts.map +1 -0
  25. package/dist/src/index.d.mts +5 -0
  26. package/dist/src/index.d.mts.map +1 -0
  27. package/dist/src/metadata/index.d.mts +2 -0
  28. package/dist/src/metadata/index.d.mts.map +1 -0
  29. package/dist/src/metadata/openapi.metadata.d.mts +30 -0
  30. package/dist/src/metadata/openapi.metadata.d.mts.map +1 -0
  31. package/dist/src/services/endpoint-scanner.service.d.mts +42 -0
  32. package/dist/src/services/endpoint-scanner.service.d.mts.map +1 -0
  33. package/dist/src/services/index.d.mts +6 -0
  34. package/dist/src/services/index.d.mts.map +1 -0
  35. package/dist/src/services/metadata-extractor.service.d.mts +19 -0
  36. package/dist/src/services/metadata-extractor.service.d.mts.map +1 -0
  37. package/dist/src/services/openapi-generator.service.d.mts +91 -0
  38. package/dist/src/services/openapi-generator.service.d.mts.map +1 -0
  39. package/dist/src/services/path-builder.service.d.mts +73 -0
  40. package/dist/src/services/path-builder.service.d.mts.map +1 -0
  41. package/dist/src/services/schema-converter.service.d.mts +57 -0
  42. package/dist/src/services/schema-converter.service.d.mts.map +1 -0
  43. package/dist/src/tokens/index.d.mts +18 -0
  44. package/dist/src/tokens/index.d.mts.map +1 -0
  45. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  46. package/dist/tsconfig.spec.tsbuildinfo +1 -0
  47. package/dist/tsconfig.tsbuildinfo +1 -0
  48. package/dist/tsdown.config.d.mts +3 -0
  49. package/dist/tsdown.config.d.mts.map +1 -0
  50. package/dist/vitest.config.d.mts +3 -0
  51. package/dist/vitest.config.d.mts.map +1 -0
  52. package/lib/index.cjs +2120 -0
  53. package/lib/index.cjs.map +1 -0
  54. package/lib/index.d.cts +594 -0
  55. package/lib/index.d.cts.map +1 -0
  56. package/lib/index.d.mts +594 -0
  57. package/lib/index.d.mts.map +1 -0
  58. package/lib/index.mjs +2077 -0
  59. package/lib/index.mjs.map +1 -0
  60. package/package.json +44 -0
  61. package/project.json +66 -0
  62. package/src/__tests__/decorators.spec.mts +185 -0
  63. package/src/__tests__/metadata.spec.mts +207 -0
  64. package/src/__tests__/services.spec.mts +216 -0
  65. package/src/decorators/api-deprecated.decorator.mts +45 -0
  66. package/src/decorators/api-exclude.decorator.mts +29 -0
  67. package/src/decorators/api-operation.decorator.mts +59 -0
  68. package/src/decorators/api-security.decorator.mts +44 -0
  69. package/src/decorators/api-stream.decorator.mts +55 -0
  70. package/src/decorators/api-summary.decorator.mts +33 -0
  71. package/src/decorators/api-tag.decorator.mts +51 -0
  72. package/src/decorators/index.mts +7 -0
  73. package/src/index.mts +42 -0
  74. package/src/metadata/index.mts +2 -0
  75. package/src/metadata/openapi.metadata.mts +30 -0
  76. package/src/services/endpoint-scanner.service.mts +118 -0
  77. package/src/services/index.mts +21 -0
  78. package/src/services/metadata-extractor.service.mts +91 -0
  79. package/src/services/openapi-generator.service.mts +219 -0
  80. package/src/services/path-builder.service.mts +344 -0
  81. package/src/services/schema-converter.service.mts +96 -0
  82. package/src/tokens/index.mts +24 -0
  83. package/tsconfig.json +24 -0
  84. package/tsconfig.lib.json +8 -0
  85. package/tsconfig.spec.json +12 -0
  86. package/tsdown.config.mts +35 -0
  87. package/vitest.config.mts +11 -0
@@ -0,0 +1,344 @@
1
+ import type { BaseEndpointConfig } from '@navios/builder'
2
+ import type { HandlerMetadata } from '@navios/core'
3
+ import type { oas31 } from 'zod-openapi'
4
+
5
+ import {
6
+ EndpointAdapterToken,
7
+ inject,
8
+ Injectable,
9
+ MultipartAdapterToken,
10
+ StreamAdapterToken,
11
+ } from '@navios/core'
12
+
13
+ import type { DiscoveredEndpoint } from './endpoint-scanner.service.mjs'
14
+
15
+ import { SchemaConverterService } from './schema-converter.service.mjs'
16
+
17
+ type ContentObject = oas31.ContentObject
18
+ type OperationObject = oas31.OperationObject
19
+ type ParameterObject = oas31.ParameterObject
20
+ type PathItemObject = oas31.PathItemObject
21
+ type RequestBodyObject = oas31.RequestBodyObject
22
+ type ResponsesObject = oas31.ResponsesObject
23
+ type SchemaObject = oas31.SchemaObject
24
+
25
+ /**
26
+ * Result of path item generation
27
+ */
28
+ export interface PathItemResult {
29
+ path: string
30
+ pathItem: PathItemObject
31
+ }
32
+
33
+ /**
34
+ * Service responsible for building OpenAPI path items from endpoints.
35
+ *
36
+ * Handles URL parameter conversion, request body generation,
37
+ * and response schema generation for different endpoint types.
38
+ */
39
+ @Injectable()
40
+ export class PathBuilderService {
41
+ private readonly schemaConverter = inject(SchemaConverterService)
42
+
43
+ /**
44
+ * Generates an OpenAPI path item for a discovered endpoint.
45
+ *
46
+ * @param endpoint - Discovered endpoint with metadata
47
+ * @returns Path string and path item object
48
+ */
49
+ build(endpoint: DiscoveredEndpoint): PathItemResult {
50
+ const { config, handler, openApiMetadata } = endpoint
51
+
52
+ // Convert $param to {param} format for OpenAPI
53
+ const path = this.convertUrlParams(config.url)
54
+
55
+ const operation: OperationObject = {
56
+ tags: openApiMetadata.tags.length > 0 ? openApiMetadata.tags : undefined,
57
+ summary: openApiMetadata.summary,
58
+ description: openApiMetadata.description,
59
+ operationId: openApiMetadata.operationId,
60
+ deprecated: openApiMetadata.deprecated || undefined,
61
+ externalDocs: openApiMetadata.externalDocs,
62
+ security: openApiMetadata.security,
63
+ parameters: this.buildParameters(config),
64
+ requestBody: this.buildRequestBody(config, handler),
65
+ responses: this.buildResponses(endpoint),
66
+ }
67
+
68
+ // Remove undefined properties
69
+ const cleanOperation = Object.fromEntries(
70
+ Object.entries(operation).filter(([, v]) => v !== undefined),
71
+ ) as OperationObject
72
+
73
+ return {
74
+ path,
75
+ pathItem: {
76
+ [config.method.toLowerCase()]: cleanOperation,
77
+ },
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Converts Navios URL param format ($param) to OpenAPI format ({param})
83
+ */
84
+ convertUrlParams(url: string): string {
85
+ return url.replace(/\$(\w+)/g, '{$1}')
86
+ }
87
+
88
+ /**
89
+ * Extracts URL parameter names from a URL pattern
90
+ */
91
+ extractUrlParamNames(url: string): string[] {
92
+ const matches = url.matchAll(/\$(\w+)/g)
93
+ return Array.from(matches, (m) => m[1])
94
+ }
95
+
96
+ /**
97
+ * Gets the endpoint type based on the adapter token
98
+ */
99
+ getEndpointType(
100
+ handler: HandlerMetadata<any>,
101
+ ): 'endpoint' | 'multipart' | 'stream' {
102
+ if (handler.adapterToken === MultipartAdapterToken) {
103
+ return 'multipart'
104
+ }
105
+ if (handler.adapterToken === StreamAdapterToken) {
106
+ return 'stream'
107
+ }
108
+ return 'endpoint'
109
+ }
110
+
111
+ /**
112
+ * Builds OpenAPI parameters from endpoint config
113
+ */
114
+ private buildParameters(config: BaseEndpointConfig): ParameterObject[] {
115
+ const params: ParameterObject[] = []
116
+
117
+ // URL parameters (from $paramName in URL)
118
+ const urlParams = this.extractUrlParamNames(config.url)
119
+ for (const param of urlParams) {
120
+ params.push({
121
+ name: param,
122
+ in: 'path',
123
+ required: true,
124
+ schema: { type: 'string' },
125
+ })
126
+ }
127
+
128
+ // Query parameters (from querySchema)
129
+ if (config.querySchema) {
130
+ const { schema: querySchema } = this.schemaConverter.convert(
131
+ config.querySchema,
132
+ )
133
+ const schemaObj = querySchema as SchemaObject
134
+ if (schemaObj.properties) {
135
+ for (const [name, schema] of Object.entries(schemaObj.properties)) {
136
+ params.push({
137
+ name,
138
+ in: 'query',
139
+ required: schemaObj.required?.includes(name) ?? false,
140
+ schema: schema as SchemaObject,
141
+ description: (schema as SchemaObject).description,
142
+ })
143
+ }
144
+ }
145
+ }
146
+
147
+ return params
148
+ }
149
+
150
+ /**
151
+ * Builds request body based on endpoint type
152
+ */
153
+ private buildRequestBody(
154
+ config: BaseEndpointConfig,
155
+ handler: HandlerMetadata<any>,
156
+ ): RequestBodyObject | undefined {
157
+ const type = this.getEndpointType(handler)
158
+
159
+ switch (type) {
160
+ case 'multipart':
161
+ return this.buildMultipartRequestBody(config)
162
+ case 'stream':
163
+ return undefined // Streams typically don't have request bodies
164
+ case 'endpoint':
165
+ default:
166
+ return this.buildJsonRequestBody(config)
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Builds request body for JSON endpoints
172
+ */
173
+ private buildJsonRequestBody(
174
+ config: BaseEndpointConfig,
175
+ ): RequestBodyObject | undefined {
176
+ if (!config.requestSchema) {
177
+ return undefined
178
+ }
179
+
180
+ const { schema } = this.schemaConverter.convert(config.requestSchema)
181
+
182
+ return {
183
+ required: true,
184
+ content: {
185
+ 'application/json': {
186
+ schema,
187
+ },
188
+ },
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Builds request body for multipart endpoints
194
+ */
195
+ private buildMultipartRequestBody(
196
+ config: BaseEndpointConfig,
197
+ ): RequestBodyObject {
198
+ if (!config.requestSchema) {
199
+ return {
200
+ required: true,
201
+ content: {
202
+ 'multipart/form-data': {
203
+ schema: { type: 'object' },
204
+ },
205
+ },
206
+ }
207
+ }
208
+
209
+ const schema = this.schemaConverter.convert(config.requestSchema).schema as SchemaObject
210
+
211
+ // Transform schema properties to handle File types
212
+ const properties = this.schemaConverter.transformFileProperties(
213
+ (schema.properties as Record<string, SchemaObject>) || {},
214
+ )
215
+
216
+ return {
217
+ required: true,
218
+ content: {
219
+ 'multipart/form-data': {
220
+ schema: {
221
+ type: 'object',
222
+ properties,
223
+ required: schema.required,
224
+ },
225
+ },
226
+ },
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Builds responses based on endpoint type
232
+ */
233
+ private buildResponses(endpoint: DiscoveredEndpoint): ResponsesObject {
234
+ const { config, handler } = endpoint
235
+ const type = this.getEndpointType(handler)
236
+
237
+ switch (type) {
238
+ case 'stream':
239
+ return this.buildStreamResponses(endpoint)
240
+ case 'multipart':
241
+ case 'endpoint':
242
+ default:
243
+ return this.buildJsonResponses(config, handler)
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Builds responses for JSON endpoints
249
+ */
250
+ private buildJsonResponses(
251
+ config: BaseEndpointConfig,
252
+ handler: HandlerMetadata<any>,
253
+ ): ResponsesObject {
254
+ const successCode = handler.successStatusCode?.toString() ?? '200'
255
+
256
+ if (!config.responseSchema) {
257
+ return {
258
+ [successCode]: {
259
+ description: 'Successful response',
260
+ },
261
+ }
262
+ }
263
+
264
+ const { schema } = this.schemaConverter.convert(config.responseSchema)
265
+
266
+ return {
267
+ [successCode]: {
268
+ description: 'Successful response',
269
+ content: {
270
+ 'application/json': {
271
+ schema,
272
+ },
273
+ },
274
+ },
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Builds responses for stream endpoints
280
+ */
281
+ private buildStreamResponses(endpoint: DiscoveredEndpoint): ResponsesObject {
282
+ const { openApiMetadata, handler } = endpoint
283
+ const successCode = handler.successStatusCode?.toString() ?? '200'
284
+
285
+ const contentType =
286
+ openApiMetadata.stream?.contentType ?? 'application/octet-stream'
287
+ const description =
288
+ openApiMetadata.stream?.description ?? 'Stream response'
289
+
290
+ const content: ContentObject = this.getStreamContent(contentType)
291
+
292
+ return {
293
+ [successCode]: {
294
+ description,
295
+ content,
296
+ },
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Gets content object for different stream types
302
+ */
303
+ private getStreamContent(contentType: string): ContentObject {
304
+ switch (contentType) {
305
+ case 'text/event-stream':
306
+ return {
307
+ 'text/event-stream': {
308
+ schema: {
309
+ type: 'string',
310
+ description: 'Server-Sent Events stream',
311
+ },
312
+ },
313
+ }
314
+
315
+ case 'application/octet-stream':
316
+ return {
317
+ 'application/octet-stream': {
318
+ schema: {
319
+ type: 'string',
320
+ format: 'binary',
321
+ description: 'Binary file download',
322
+ },
323
+ },
324
+ }
325
+
326
+ case 'application/json':
327
+ return {
328
+ 'application/json': {
329
+ schema: {
330
+ type: 'string',
331
+ description: 'Newline-delimited JSON stream',
332
+ },
333
+ },
334
+ }
335
+
336
+ default:
337
+ return {
338
+ [contentType]: {
339
+ schema: { type: 'string', format: 'binary' },
340
+ },
341
+ }
342
+ }
343
+ }
344
+ }
@@ -0,0 +1,96 @@
1
+ import type { ZodType } from 'zod/v4'
2
+ import type { oas31 } from 'zod-openapi'
3
+
4
+ import { Injectable } from '@navios/core'
5
+ import { createSchema } from 'zod-openapi'
6
+
7
+ type SchemaObject = oas31.SchemaObject
8
+ type ReferenceObject = oas31.ReferenceObject
9
+
10
+ /**
11
+ * Result of schema conversion
12
+ */
13
+ export interface SchemaConversionResult {
14
+ schema: SchemaObject | ReferenceObject
15
+ components: Record<string, SchemaObject>
16
+ }
17
+
18
+ /**
19
+ * Service responsible for converting Zod schemas to OpenAPI schemas.
20
+ *
21
+ * Uses zod-openapi library which supports Zod 4's native `.meta()` method
22
+ * for OpenAPI-specific metadata.
23
+ */
24
+ @Injectable()
25
+ export class SchemaConverterService {
26
+ /**
27
+ * Converts a Zod schema to an OpenAPI schema object.
28
+ *
29
+ * @param schema - Zod schema to convert
30
+ * @returns OpenAPI schema object with any component schemas
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * const userSchema = z.object({
35
+ * id: z.string().meta({ openapi: { example: 'usr_123' } }),
36
+ * name: z.string(),
37
+ * })
38
+ *
39
+ * const result = schemaConverter.convert(userSchema)
40
+ * // { schema: { type: 'object', properties: { ... } }, components: {} }
41
+ * ```
42
+ */
43
+ convert(schema: ZodType): SchemaConversionResult {
44
+ return createSchema(schema)
45
+ }
46
+
47
+ /**
48
+ * Checks if a schema property represents a File type.
49
+ *
50
+ * Used for multipart form handling to convert File types to binary format.
51
+ *
52
+ * @param schema - Schema object to check
53
+ * @returns true if the schema represents a file
54
+ */
55
+ isFileSchema(schema: SchemaObject): boolean {
56
+ return schema.type === 'string' && schema.format === 'binary'
57
+ }
58
+
59
+ /**
60
+ * Transforms schema properties to handle File/Blob types for multipart.
61
+ *
62
+ * Converts File types to OpenAPI binary format and handles arrays of files.
63
+ *
64
+ * @param properties - Schema properties object
65
+ * @returns Transformed properties with file types as binary
66
+ */
67
+ transformFileProperties(
68
+ properties: Record<string, SchemaObject>,
69
+ ): Record<string, SchemaObject> {
70
+ const result: Record<string, SchemaObject> = {}
71
+
72
+ for (const [key, prop] of Object.entries(properties)) {
73
+ if (this.isFileSchema(prop)) {
74
+ result[key] = {
75
+ type: 'string',
76
+ format: 'binary',
77
+ description: prop.description,
78
+ }
79
+ } else if (
80
+ prop.type === 'array' &&
81
+ prop.items &&
82
+ this.isFileSchema(prop.items as SchemaObject)
83
+ ) {
84
+ result[key] = {
85
+ type: 'array',
86
+ items: { type: 'string', format: 'binary' },
87
+ description: prop.description,
88
+ }
89
+ } else {
90
+ result[key] = prop
91
+ }
92
+ }
93
+
94
+ return result
95
+ }
96
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Tokens for OpenAPI metadata attributes
3
+ */
4
+
5
+ /** Token for @ApiTag decorator */
6
+ export const ApiTagToken = Symbol.for('navios:openapi:tag')
7
+
8
+ /** Token for @ApiOperation decorator */
9
+ export const ApiOperationToken = Symbol.for('navios:openapi:operation')
10
+
11
+ /** Token for @ApiSummary decorator */
12
+ export const ApiSummaryToken = Symbol.for('navios:openapi:summary')
13
+
14
+ /** Token for @ApiDeprecated decorator */
15
+ export const ApiDeprecatedToken = Symbol.for('navios:openapi:deprecated')
16
+
17
+ /** Token for @ApiSecurity decorator */
18
+ export const ApiSecurityToken = Symbol.for('navios:openapi:security')
19
+
20
+ /** Token for @ApiExclude decorator */
21
+ export const ApiExcludeToken = Symbol.for('navios:openapi:exclude')
22
+
23
+ /** Token for @ApiStream decorator */
24
+ export const ApiStreamToken = Symbol.for('navios:openapi:stream')
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "module": "Node18",
5
+ "outDir": "dist"
6
+ },
7
+ "references": [
8
+ {
9
+ "path": "./tsconfig.lib.json"
10
+ },
11
+ {
12
+ "path": "./tsconfig.spec.json"
13
+ },
14
+ {
15
+ "path": "../core"
16
+ },
17
+ {
18
+ "path": "../di"
19
+ },
20
+ {
21
+ "path": "../builder"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist"
5
+ },
6
+ "include": ["src/**/*"],
7
+ "exclude": ["src/**/*.spec.mts", "src/**/*.spec-d.mts"]
8
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist"
5
+ },
6
+ "include": ["src/**/*.spec.mts", "src/**/*.spec-d.mts"],
7
+ "references": [
8
+ {
9
+ "path": "./tsconfig.lib.json"
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1,35 @@
1
+ import { withFilter } from 'rolldown/filter'
2
+ import { defineConfig } from 'tsdown'
3
+ import swc from 'unplugin-swc'
4
+
5
+ export default defineConfig({
6
+ entry: ['src/index.mts'],
7
+ outDir: 'lib',
8
+ format: ['esm', 'cjs'],
9
+ clean: true,
10
+ tsconfig: 'tsconfig.lib.json',
11
+ treeshake: true,
12
+ sourcemap: true,
13
+ platform: 'node',
14
+ external: ['@navios/core', '@navios/di'],
15
+ dts: true,
16
+ target: 'es2022',
17
+ plugins: [
18
+ withFilter(
19
+ swc.rolldown({
20
+ jsc: {
21
+ target: 'es2022',
22
+ parser: {
23
+ syntax: 'typescript',
24
+ decorators: true,
25
+ },
26
+ transform: {
27
+ decoratorVersion: '2022-03',
28
+ },
29
+ },
30
+ }),
31
+ // Only run this transform if the file contains a decorator.
32
+ { transform: { code: '@' } },
33
+ ),
34
+ ],
35
+ })
@@ -0,0 +1,11 @@
1
+ import { defineProject } from 'vitest/config'
2
+
3
+ export default defineProject({
4
+ test: {
5
+ typecheck: {
6
+ enabled: true,
7
+ tsconfig: './tsconfig.lib.json',
8
+ },
9
+ include: ['src/**/*.spec.mts', 'src/**/*.spec-d.mts'],
10
+ },
11
+ })