@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.
- package/LICENSE +8 -0
- package/README.md +260 -0
- package/dist/src/__tests__/decorators.spec.d.mts +2 -0
- package/dist/src/__tests__/decorators.spec.d.mts.map +1 -0
- package/dist/src/__tests__/metadata.spec.d.mts +2 -0
- package/dist/src/__tests__/metadata.spec.d.mts.map +1 -0
- package/dist/src/__tests__/services.spec.d.mts +2 -0
- package/dist/src/__tests__/services.spec.d.mts.map +1 -0
- package/dist/src/decorators/api-deprecated.decorator.d.mts +33 -0
- package/dist/src/decorators/api-deprecated.decorator.d.mts.map +1 -0
- package/dist/src/decorators/api-exclude.decorator.d.mts +26 -0
- package/dist/src/decorators/api-exclude.decorator.d.mts.map +1 -0
- package/dist/src/decorators/api-operation.decorator.d.mts +58 -0
- package/dist/src/decorators/api-operation.decorator.d.mts.map +1 -0
- package/dist/src/decorators/api-security.decorator.d.mts +36 -0
- package/dist/src/decorators/api-security.decorator.d.mts.map +1 -0
- package/dist/src/decorators/api-stream.decorator.d.mts +50 -0
- package/dist/src/decorators/api-stream.decorator.d.mts.map +1 -0
- package/dist/src/decorators/api-summary.decorator.d.mts +24 -0
- package/dist/src/decorators/api-summary.decorator.d.mts.map +1 -0
- package/dist/src/decorators/api-tag.decorator.d.mts +42 -0
- package/dist/src/decorators/api-tag.decorator.d.mts.map +1 -0
- package/dist/src/decorators/index.d.mts +8 -0
- package/dist/src/decorators/index.d.mts.map +1 -0
- package/dist/src/index.d.mts +5 -0
- package/dist/src/index.d.mts.map +1 -0
- package/dist/src/metadata/index.d.mts +2 -0
- package/dist/src/metadata/index.d.mts.map +1 -0
- package/dist/src/metadata/openapi.metadata.d.mts +30 -0
- package/dist/src/metadata/openapi.metadata.d.mts.map +1 -0
- package/dist/src/services/endpoint-scanner.service.d.mts +42 -0
- package/dist/src/services/endpoint-scanner.service.d.mts.map +1 -0
- package/dist/src/services/index.d.mts +6 -0
- package/dist/src/services/index.d.mts.map +1 -0
- package/dist/src/services/metadata-extractor.service.d.mts +19 -0
- package/dist/src/services/metadata-extractor.service.d.mts.map +1 -0
- package/dist/src/services/openapi-generator.service.d.mts +91 -0
- package/dist/src/services/openapi-generator.service.d.mts.map +1 -0
- package/dist/src/services/path-builder.service.d.mts +73 -0
- package/dist/src/services/path-builder.service.d.mts.map +1 -0
- package/dist/src/services/schema-converter.service.d.mts +57 -0
- package/dist/src/services/schema-converter.service.d.mts.map +1 -0
- package/dist/src/tokens/index.d.mts +18 -0
- package/dist/src/tokens/index.d.mts.map +1 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/tsconfig.spec.tsbuildinfo +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/tsdown.config.d.mts +3 -0
- package/dist/tsdown.config.d.mts.map +1 -0
- package/dist/vitest.config.d.mts +3 -0
- package/dist/vitest.config.d.mts.map +1 -0
- package/lib/index.cjs +2120 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.cts +594 -0
- package/lib/index.d.cts.map +1 -0
- package/lib/index.d.mts +594 -0
- package/lib/index.d.mts.map +1 -0
- package/lib/index.mjs +2077 -0
- package/lib/index.mjs.map +1 -0
- package/package.json +44 -0
- package/project.json +66 -0
- package/src/__tests__/decorators.spec.mts +185 -0
- package/src/__tests__/metadata.spec.mts +207 -0
- package/src/__tests__/services.spec.mts +216 -0
- package/src/decorators/api-deprecated.decorator.mts +45 -0
- package/src/decorators/api-exclude.decorator.mts +29 -0
- package/src/decorators/api-operation.decorator.mts +59 -0
- package/src/decorators/api-security.decorator.mts +44 -0
- package/src/decorators/api-stream.decorator.mts +55 -0
- package/src/decorators/api-summary.decorator.mts +33 -0
- package/src/decorators/api-tag.decorator.mts +51 -0
- package/src/decorators/index.mts +7 -0
- package/src/index.mts +42 -0
- package/src/metadata/index.mts +2 -0
- package/src/metadata/openapi.metadata.mts +30 -0
- package/src/services/endpoint-scanner.service.mts +118 -0
- package/src/services/index.mts +21 -0
- package/src/services/metadata-extractor.service.mts +91 -0
- package/src/services/openapi-generator.service.mts +219 -0
- package/src/services/path-builder.service.mts +344 -0
- package/src/services/schema-converter.service.mts +96 -0
- package/src/tokens/index.mts +24 -0
- package/tsconfig.json +24 -0
- package/tsconfig.lib.json +8 -0
- package/tsconfig.spec.json +12 -0
- package/tsdown.config.mts +35 -0
- package/vitest.config.mts +11 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["ApiTagToken","Symbol","for","ApiOperationToken","ApiSummaryToken","ApiDeprecatedToken","ApiSecurityToken","ApiExcludeToken","ApiStreamToken","AttributeFactory","z","ApiTagToken","ApiTagSchema","object","name","string","description","optional","BaseApiTag","createAttribute","ApiTag","AttributeFactory","z","ApiOperationToken","ApiOperationSchema","object","summary","string","optional","description","operationId","deprecated","boolean","externalDocs","url","ApiOperation","createAttribute","AttributeFactory","z","ApiSummaryToken","ApiSummarySchema","string","ApiSummary","createAttribute","AttributeFactory","z","ApiDeprecatedToken","ApiDeprecatedSchema","object","message","string","optional","BaseApiDeprecated","createAttribute","ApiDeprecated","AttributeFactory","z","ApiSecurityToken","ApiSecuritySchema","record","string","array","ApiSecurity","createAttribute","AttributeFactory","ApiExcludeToken","ApiExclude","createAttribute","AttributeFactory","z","ApiStreamToken","ApiStreamSchema","object","contentType","string","description","optional","ApiStream","createAttribute","Injectable","ApiDeprecatedToken","ApiExcludeToken","ApiOperationToken","ApiSecurityToken","ApiStreamToken","ApiSummaryToken","ApiTagToken","MetadataExtractorService","extract","controller","handler","controllerTag","customAttributes","get","handlerTag","operation","summary","deprecated","security","excluded","stream","tags","name","push","description","operationId","undefined","externalDocs","extractControllerMetadata","inject","Injectable","Logger","MetadataExtractorService","EndpointScannerService","logger","context","name","metadataExtractor","scan","modules","endpoints","moduleName","moduleMetadata","controllers","size","debug","controllerClass","controllerMeta","controllerEndpoints","scanController","push","length","module","handler","config","openApiMetadata","extract","excluded","classMethod","controller","Injectable","createSchema","SchemaConverterService","convert","schema","isFileSchema","type","format","transformFileProperties","properties","result","key","prop","Object","entries","description","items","inject","Injectable","MultipartAdapterToken","StreamAdapterToken","SchemaConverterService","PathBuilderService","schemaConverter","build","endpoint","config","handler","openApiMetadata","path","convertUrlParams","url","operation","tags","length","undefined","summary","description","operationId","deprecated","externalDocs","security","parameters","buildParameters","requestBody","buildRequestBody","responses","buildResponses","cleanOperation","Object","fromEntries","entries","filter","v","pathItem","method","toLowerCase","replace","extractUrlParamNames","matches","matchAll","Array","from","m","getEndpointType","adapterToken","params","urlParams","param","push","name","in","required","schema","type","querySchema","convert","schemaObj","properties","includes","buildMultipartRequestBody","buildJsonRequestBody","requestSchema","content","transformFileProperties","buildStreamResponses","buildJsonResponses","successCode","successStatusCode","toString","responseSchema","contentType","stream","getStreamContent","format","inject","Injectable","Logger","EndpointScannerService","PathBuilderService","OpenApiGeneratorService","logger","context","name","scanner","pathBuilder","generate","modules","options","debug","endpoints","scan","paths","buildPaths","discoveredTags","collectTags","tags","mergeTags","document","openapi","info","servers","length","externalDocs","security","securitySchemes","components","Object","keys","endpoint","path","pathItem","build","Set","tag","openApiMetadata","add","configuredTags","tagMap","Map","set","tagName","has","Array","from","values"],"sources":["../src/tokens/index.mts","../src/decorators/api-tag.decorator.mts","../src/decorators/api-operation.decorator.mts","../src/decorators/api-summary.decorator.mts","../src/decorators/api-deprecated.decorator.mts","../src/decorators/api-security.decorator.mts","../src/decorators/api-exclude.decorator.mts","../src/decorators/api-stream.decorator.mts","../src/services/metadata-extractor.service.mts","../src/services/endpoint-scanner.service.mts","../src/services/schema-converter.service.mts","../src/services/path-builder.service.mts","../src/services/openapi-generator.service.mts"],"sourcesContent":["/**\n * Tokens for OpenAPI metadata attributes\n */\n\n/** Token for @ApiTag decorator */\nexport const ApiTagToken = Symbol.for('navios:openapi:tag')\n\n/** Token for @ApiOperation decorator */\nexport const ApiOperationToken = Symbol.for('navios:openapi:operation')\n\n/** Token for @ApiSummary decorator */\nexport const ApiSummaryToken = Symbol.for('navios:openapi:summary')\n\n/** Token for @ApiDeprecated decorator */\nexport const ApiDeprecatedToken = Symbol.for('navios:openapi:deprecated')\n\n/** Token for @ApiSecurity decorator */\nexport const ApiSecurityToken = Symbol.for('navios:openapi:security')\n\n/** Token for @ApiExclude decorator */\nexport const ApiExcludeToken = Symbol.for('navios:openapi:exclude')\n\n/** Token for @ApiStream decorator */\nexport const ApiStreamToken = Symbol.for('navios:openapi:stream')\n","import { AttributeFactory } from '@navios/core'\nimport { z } from 'zod/v4'\n\nimport { ApiTagToken } from '../tokens/index.mjs'\n\nconst ApiTagSchema = z.object({\n name: z.string(),\n description: z.string().optional(),\n})\n\n/** Options for the @ApiTag decorator, inferred from the schema */\nexport type ApiTagOptions = z.infer<typeof ApiTagSchema>\n\nconst BaseApiTag = AttributeFactory.createAttribute(ApiTagToken, ApiTagSchema)\n\n/**\n * Groups endpoints under a specific tag/folder in the documentation.\n *\n * Can be applied to controllers (affects all endpoints) or individual methods.\n * When applied to both, the method-level tag takes precedence.\n *\n * @param name - The tag name\n * @param description - Optional tag description\n *\n * @example\n * ```typescript\n * // Apply to entire controller\n * @Controller()\n * @ApiTag('Users', 'User management operations')\n * export class UserController {\n * @Endpoint(getUser)\n * async getUser() {}\n * }\n *\n * // Apply to individual endpoint\n * @Controller()\n * export class MixedController {\n * @Endpoint(getUser)\n * @ApiTag('Users')\n * async getUser() {}\n *\n * @Endpoint(getOrder)\n * @ApiTag('Orders')\n * async getOrder() {}\n * }\n * ```\n */\nexport function ApiTag(name: string, description?: string) {\n return BaseApiTag({ name, description })\n}\n\n","import { AttributeFactory } from '@navios/core'\nimport { z } from 'zod/v4'\n\nimport { ApiOperationToken } from '../tokens/index.mjs'\n\nconst ApiOperationSchema = z.object({\n summary: z.string().optional(),\n description: z.string().optional(),\n operationId: z.string().optional(),\n deprecated: z.boolean().optional(),\n externalDocs: z\n .object({\n url: z.string(),\n description: z.string().optional(),\n })\n .optional(),\n})\n\n/** Options for the @ApiOperation decorator, inferred from the schema */\nexport type ApiOperationOptions = z.infer<typeof ApiOperationSchema>\n\n/**\n * Provides detailed operation metadata for an endpoint.\n *\n * Use this decorator when you need to specify multiple operation properties.\n * For simple cases, consider using @ApiSummary instead.\n *\n * @param options - Operation configuration options\n *\n * @example\n * ```typescript\n * @Controller()\n * export class UserController {\n * @Endpoint(getUser)\n * @ApiOperation({\n * summary: 'Get user by ID',\n * description: 'Retrieves a user by their unique identifier. Returns 404 if not found.',\n * operationId: 'getUserById',\n * })\n * async getUser(params: EndpointParams<typeof getUser>) {}\n *\n * @Endpoint(legacyGetUser)\n * @ApiOperation({\n * summary: 'Get user (legacy)',\n * deprecated: true,\n * externalDocs: {\n * url: 'https://docs.example.com/migration',\n * description: 'Migration guide'\n * }\n * })\n * async getLegacyUser() {}\n * }\n * ```\n */\nexport const ApiOperation = AttributeFactory.createAttribute(\n ApiOperationToken,\n ApiOperationSchema,\n)\n\n","import { AttributeFactory } from '@navios/core'\nimport { z } from 'zod/v4'\n\nimport { ApiSummaryToken } from '../tokens/index.mjs'\n\nconst ApiSummarySchema = z.string()\n\n/**\n * Shorthand decorator for adding just a summary to an endpoint.\n *\n * This is equivalent to `@ApiOperation({ summary: '...' })` but more concise.\n *\n * @param summary - Short summary text for the endpoint\n *\n * @example\n * ```typescript\n * @Controller()\n * export class UserController {\n * @Endpoint(getUser)\n * @ApiSummary('Get user by ID')\n * async getUser() {}\n *\n * @Endpoint(createUser)\n * @ApiSummary('Create a new user')\n * async createUser() {}\n * }\n * ```\n */\nexport const ApiSummary = AttributeFactory.createAttribute(\n ApiSummaryToken,\n ApiSummarySchema,\n)\n\n","import { AttributeFactory } from '@navios/core'\nimport { z } from 'zod/v4'\n\nimport { ApiDeprecatedToken } from '../tokens/index.mjs'\n\nconst ApiDeprecatedSchema = z.object({\n message: z.string().optional(),\n})\n\n/** Options for the @ApiDeprecated decorator, inferred from the schema */\nexport type ApiDeprecatedOptions = z.infer<typeof ApiDeprecatedSchema>\n\nconst BaseApiDeprecated = AttributeFactory.createAttribute(\n ApiDeprecatedToken,\n ApiDeprecatedSchema,\n)\n\n/**\n * Marks an endpoint as deprecated.\n *\n * Deprecated endpoints are shown with a visual indicator in documentation\n * and may include a migration message.\n *\n * @param message - Optional deprecation message\n *\n * @example\n * ```typescript\n * @Controller()\n * export class UserController {\n * // Simple deprecation\n * @Endpoint(legacyGetUser)\n * @ApiDeprecated()\n * async getLegacyUser() {}\n *\n * // With migration message\n * @Endpoint(oldCreateUser)\n * @ApiDeprecated('Use POST /v2/users instead')\n * async oldCreateUser() {}\n * }\n * ```\n */\nexport function ApiDeprecated(message?: string) {\n return BaseApiDeprecated({ message })\n}\n\n","import { AttributeFactory } from '@navios/core'\nimport { z } from 'zod/v4'\n\nimport { ApiSecurityToken } from '../tokens/index.mjs'\n\nconst ApiSecuritySchema = z.record(z.string(), z.array(z.string()))\n\n/** Security requirement for an endpoint, inferred from the schema */\nexport type ApiSecurityRequirement = z.infer<typeof ApiSecuritySchema>\n\n/**\n * Specifies security requirements for an endpoint.\n *\n * The security requirement object maps security scheme names to their scopes.\n * For schemes that don't use scopes (like API keys), use an empty array.\n *\n * @param requirements - Security requirements object\n *\n * @example\n * ```typescript\n * @Controller()\n * export class UserController {\n * // Require bearer token authentication\n * @Endpoint(getUser)\n * @ApiSecurity({ bearerAuth: [] })\n * async getUser() {}\n *\n * // Require multiple authentication methods\n * @Endpoint(adminEndpoint)\n * @ApiSecurity({ bearerAuth: [], apiKey: [] })\n * async adminAction() {}\n *\n * // OAuth2 with specific scopes\n * @Endpoint(writeUser)\n * @ApiSecurity({ oauth2: ['users:write', 'users:read'] })\n * async writeUser() {}\n * }\n * ```\n */\nexport const ApiSecurity = AttributeFactory.createAttribute(\n ApiSecurityToken,\n ApiSecuritySchema,\n)\n\n","import { AttributeFactory } from '@navios/core'\n\nimport { ApiExcludeToken } from '../tokens/index.mjs'\n\n/**\n * Excludes an endpoint from OpenAPI documentation.\n *\n * Use this decorator for internal endpoints that should not be visible\n * in the public API documentation.\n *\n * @example\n * ```typescript\n * @Controller()\n * export class HealthController {\n * @Endpoint(healthCheck)\n * @ApiExclude()\n * async healthCheck() {\n * return { status: 'ok' }\n * }\n *\n * @Endpoint(internalMetrics)\n * @ApiExclude()\n * async internalMetrics() {\n * return { ... }\n * }\n * }\n * ```\n */\nexport const ApiExclude = AttributeFactory.createAttribute(ApiExcludeToken)\n","import { AttributeFactory } from '@navios/core'\nimport { z } from 'zod/v4'\n\nimport { ApiStreamToken } from '../tokens/index.mjs'\n\nconst ApiStreamSchema = z.object({\n contentType: z.string(),\n description: z.string().optional(),\n})\n\n/** Options for the @ApiStream decorator, inferred from the schema */\nexport type ApiStreamOptions = z.infer<typeof ApiStreamSchema>\n\n/**\n * Specifies content type and description for stream endpoints.\n *\n * Stream endpoints don't have a responseSchema, so this decorator provides\n * the necessary metadata for OpenAPI documentation.\n *\n * @param options - Stream response options\n *\n * @example\n * ```typescript\n * @Controller()\n * export class FileController {\n * // Binary file download\n * @Stream(downloadFile)\n * @ApiStream({\n * contentType: 'application/octet-stream',\n * description: 'Download file as binary stream'\n * })\n * async download(params: StreamParams<typeof downloadFile>, reply: Reply) {\n * // Stream implementation\n * }\n * }\n *\n * @Controller()\n * export class EventController {\n * // Server-Sent Events\n * @Stream(streamEvents)\n * @ApiStream({\n * contentType: 'text/event-stream',\n * description: 'Real-time event stream'\n * })\n * async stream(params: StreamParams<typeof streamEvents>, reply: Reply) {\n * // SSE implementation\n * }\n * }\n * ```\n */\nexport const ApiStream = AttributeFactory.createAttribute(\n ApiStreamToken,\n ApiStreamSchema,\n)\n\n","import type { ControllerMetadata, HandlerMetadata } from '@navios/core'\n\nimport { Injectable } from '@navios/core'\n\nimport type { OpenApiEndpointMetadata } from '../metadata/openapi.metadata.mjs'\n\nimport {\n ApiDeprecatedToken,\n ApiExcludeToken,\n ApiOperationToken,\n ApiSecurityToken,\n ApiStreamToken,\n ApiSummaryToken,\n ApiTagToken,\n} from '../tokens/index.mjs'\n\n/**\n * Service responsible for extracting OpenAPI metadata from decorators.\n *\n * Merges controller-level and handler-level metadata to produce\n * a complete OpenAPI metadata object for each endpoint.\n */\n@Injectable()\nexport class MetadataExtractorService {\n /**\n * Extracts and merges OpenAPI metadata from controller and handler.\n *\n * @param controller - Controller metadata\n * @param handler - Handler metadata\n * @returns Merged OpenAPI metadata\n */\n extract(\n controller: ControllerMetadata,\n handler: HandlerMetadata<any>,\n ): OpenApiEndpointMetadata {\n // Extract controller-level metadata\n const controllerTag = controller.customAttributes.get(ApiTagToken) as\n | { name: string; description?: string }\n | undefined\n\n // Extract handler-level metadata\n const handlerTag = handler.customAttributes.get(ApiTagToken) as\n | { name: string; description?: string }\n | undefined\n const operation = handler.customAttributes.get(ApiOperationToken) as\n | {\n summary?: string\n description?: string\n operationId?: string\n deprecated?: boolean\n externalDocs?: { url: string; description?: string }\n }\n | undefined\n const summary = handler.customAttributes.get(ApiSummaryToken) as\n | string\n | undefined\n const deprecated = handler.customAttributes.get(ApiDeprecatedToken) as\n | { message?: string }\n | undefined\n const security = handler.customAttributes.get(ApiSecurityToken) as\n | Record<string, string[]>\n | undefined\n const excluded = handler.customAttributes.get(ApiExcludeToken) as\n | boolean\n | undefined\n const stream = handler.customAttributes.get(ApiStreamToken) as\n | { contentType: string; description?: string }\n | undefined\n\n // Build tags array (handler tag takes precedence but both are included)\n const tags: string[] = []\n if (controllerTag?.name) {\n tags.push(controllerTag.name)\n }\n if (handlerTag?.name && handlerTag.name !== controllerTag?.name) {\n tags.push(handlerTag.name)\n }\n\n return {\n tags,\n summary: operation?.summary ?? summary,\n description: operation?.description,\n operationId: operation?.operationId,\n deprecated: deprecated !== undefined || operation?.deprecated === true,\n externalDocs: operation?.externalDocs,\n security: security ? [security] : undefined,\n excluded: excluded === true,\n stream,\n }\n }\n}\n","import type {\n ControllerMetadata,\n HandlerMetadata,\n ModuleMetadata,\n} from '@navios/core'\nimport type { BaseEndpointConfig } from '@navios/builder'\n\nimport { extractControllerMetadata, inject, Injectable, Logger } from '@navios/core'\n\nimport type { OpenApiEndpointMetadata } from '../metadata/openapi.metadata.mjs'\n\nimport { MetadataExtractorService } from './metadata-extractor.service.mjs'\n\n/**\n * Represents a discovered endpoint with all its metadata\n */\nexport interface DiscoveredEndpoint {\n /** Module metadata */\n module: ModuleMetadata\n /** Controller class */\n controllerClass: any\n /** Controller metadata */\n controller: ControllerMetadata\n /** Handler (endpoint) metadata */\n handler: HandlerMetadata<any>\n /** Endpoint configuration from @navios/builder */\n config: BaseEndpointConfig\n /** Extracted OpenAPI metadata */\n openApiMetadata: OpenApiEndpointMetadata\n}\n\n/**\n * Service responsible for scanning modules and discovering endpoints.\n *\n * Iterates through all modules, controllers, and endpoints,\n * extracting OpenAPI metadata from decorators.\n */\n@Injectable()\nexport class EndpointScannerService {\n private readonly logger = inject(Logger, {\n context: EndpointScannerService.name,\n })\n\n private readonly metadataExtractor = inject(MetadataExtractorService)\n\n /**\n * Scans all loaded modules and discovers endpoints.\n *\n * @param modules - Map of loaded modules from NaviosApplication\n * @returns Array of discovered endpoints\n */\n scan(modules: Map<string, ModuleMetadata>): DiscoveredEndpoint[] {\n const endpoints: DiscoveredEndpoint[] = []\n\n for (const [moduleName, moduleMetadata] of modules) {\n if (!moduleMetadata.controllers || moduleMetadata.controllers.size === 0) {\n continue\n }\n\n this.logger.debug(`Scanning module: ${moduleName}`)\n\n for (const controllerClass of moduleMetadata.controllers) {\n const controllerMeta = extractControllerMetadata(controllerClass)\n const controllerEndpoints = this.scanController(\n moduleMetadata,\n controllerClass,\n controllerMeta,\n )\n endpoints.push(...controllerEndpoints)\n }\n }\n\n this.logger.debug(`Discovered ${endpoints.length} endpoints`)\n return endpoints\n }\n\n /**\n * Scans a controller and returns its endpoints\n */\n private scanController(\n module: ModuleMetadata,\n controllerClass: any,\n controllerMeta: ControllerMetadata,\n ): DiscoveredEndpoint[] {\n const endpoints: DiscoveredEndpoint[] = []\n\n for (const handler of controllerMeta.endpoints) {\n // Skip endpoints without config (non-builder endpoints)\n if (!handler.config) {\n continue\n }\n\n const openApiMetadata = this.metadataExtractor.extract(\n controllerMeta,\n handler,\n )\n\n // Skip excluded endpoints\n if (openApiMetadata.excluded) {\n this.logger.debug(\n `Skipping excluded endpoint: ${handler.classMethod}`,\n )\n continue\n }\n\n endpoints.push({\n module,\n controllerClass,\n controller: controllerMeta,\n handler,\n config: handler.config as BaseEndpointConfig,\n openApiMetadata,\n })\n }\n\n return endpoints\n }\n}\n","import type { ZodType } from 'zod/v4'\nimport type { oas31 } from 'zod-openapi'\n\nimport { Injectable } from '@navios/core'\nimport { createSchema } from 'zod-openapi'\n\ntype SchemaObject = oas31.SchemaObject\ntype ReferenceObject = oas31.ReferenceObject\n\n/**\n * Result of schema conversion\n */\nexport interface SchemaConversionResult {\n schema: SchemaObject | ReferenceObject\n components: Record<string, SchemaObject>\n}\n\n/**\n * Service responsible for converting Zod schemas to OpenAPI schemas.\n *\n * Uses zod-openapi library which supports Zod 4's native `.meta()` method\n * for OpenAPI-specific metadata.\n */\n@Injectable()\nexport class SchemaConverterService {\n /**\n * Converts a Zod schema to an OpenAPI schema object.\n *\n * @param schema - Zod schema to convert\n * @returns OpenAPI schema object with any component schemas\n *\n * @example\n * ```typescript\n * const userSchema = z.object({\n * id: z.string().meta({ openapi: { example: 'usr_123' } }),\n * name: z.string(),\n * })\n *\n * const result = schemaConverter.convert(userSchema)\n * // { schema: { type: 'object', properties: { ... } }, components: {} }\n * ```\n */\n convert(schema: ZodType): SchemaConversionResult {\n return createSchema(schema)\n }\n\n /**\n * Checks if a schema property represents a File type.\n *\n * Used for multipart form handling to convert File types to binary format.\n *\n * @param schema - Schema object to check\n * @returns true if the schema represents a file\n */\n isFileSchema(schema: SchemaObject): boolean {\n return schema.type === 'string' && schema.format === 'binary'\n }\n\n /**\n * Transforms schema properties to handle File/Blob types for multipart.\n *\n * Converts File types to OpenAPI binary format and handles arrays of files.\n *\n * @param properties - Schema properties object\n * @returns Transformed properties with file types as binary\n */\n transformFileProperties(\n properties: Record<string, SchemaObject>,\n ): Record<string, SchemaObject> {\n const result: Record<string, SchemaObject> = {}\n\n for (const [key, prop] of Object.entries(properties)) {\n if (this.isFileSchema(prop)) {\n result[key] = {\n type: 'string',\n format: 'binary',\n description: prop.description,\n }\n } else if (\n prop.type === 'array' &&\n prop.items &&\n this.isFileSchema(prop.items as SchemaObject)\n ) {\n result[key] = {\n type: 'array',\n items: { type: 'string', format: 'binary' },\n description: prop.description,\n }\n } else {\n result[key] = prop\n }\n }\n\n return result\n }\n}\n","import type { BaseEndpointConfig } from '@navios/builder'\nimport type { HandlerMetadata } from '@navios/core'\nimport type { oas31 } from 'zod-openapi'\n\nimport {\n EndpointAdapterToken,\n inject,\n Injectable,\n MultipartAdapterToken,\n StreamAdapterToken,\n} from '@navios/core'\n\nimport type { DiscoveredEndpoint } from './endpoint-scanner.service.mjs'\n\nimport { SchemaConverterService } from './schema-converter.service.mjs'\n\ntype ContentObject = oas31.ContentObject\ntype OperationObject = oas31.OperationObject\ntype ParameterObject = oas31.ParameterObject\ntype PathItemObject = oas31.PathItemObject\ntype RequestBodyObject = oas31.RequestBodyObject\ntype ResponsesObject = oas31.ResponsesObject\ntype SchemaObject = oas31.SchemaObject\n\n/**\n * Result of path item generation\n */\nexport interface PathItemResult {\n path: string\n pathItem: PathItemObject\n}\n\n/**\n * Service responsible for building OpenAPI path items from endpoints.\n *\n * Handles URL parameter conversion, request body generation,\n * and response schema generation for different endpoint types.\n */\n@Injectable()\nexport class PathBuilderService {\n private readonly schemaConverter = inject(SchemaConverterService)\n\n /**\n * Generates an OpenAPI path item for a discovered endpoint.\n *\n * @param endpoint - Discovered endpoint with metadata\n * @returns Path string and path item object\n */\n build(endpoint: DiscoveredEndpoint): PathItemResult {\n const { config, handler, openApiMetadata } = endpoint\n\n // Convert $param to {param} format for OpenAPI\n const path = this.convertUrlParams(config.url)\n\n const operation: OperationObject = {\n tags: openApiMetadata.tags.length > 0 ? openApiMetadata.tags : undefined,\n summary: openApiMetadata.summary,\n description: openApiMetadata.description,\n operationId: openApiMetadata.operationId,\n deprecated: openApiMetadata.deprecated || undefined,\n externalDocs: openApiMetadata.externalDocs,\n security: openApiMetadata.security,\n parameters: this.buildParameters(config),\n requestBody: this.buildRequestBody(config, handler),\n responses: this.buildResponses(endpoint),\n }\n\n // Remove undefined properties\n const cleanOperation = Object.fromEntries(\n Object.entries(operation).filter(([, v]) => v !== undefined),\n ) as OperationObject\n\n return {\n path,\n pathItem: {\n [config.method.toLowerCase()]: cleanOperation,\n },\n }\n }\n\n /**\n * Converts Navios URL param format ($param) to OpenAPI format ({param})\n */\n convertUrlParams(url: string): string {\n return url.replace(/\\$(\\w+)/g, '{$1}')\n }\n\n /**\n * Extracts URL parameter names from a URL pattern\n */\n extractUrlParamNames(url: string): string[] {\n const matches = url.matchAll(/\\$(\\w+)/g)\n return Array.from(matches, (m) => m[1])\n }\n\n /**\n * Gets the endpoint type based on the adapter token\n */\n getEndpointType(\n handler: HandlerMetadata<any>,\n ): 'endpoint' | 'multipart' | 'stream' {\n if (handler.adapterToken === MultipartAdapterToken) {\n return 'multipart'\n }\n if (handler.adapterToken === StreamAdapterToken) {\n return 'stream'\n }\n return 'endpoint'\n }\n\n /**\n * Builds OpenAPI parameters from endpoint config\n */\n private buildParameters(config: BaseEndpointConfig): ParameterObject[] {\n const params: ParameterObject[] = []\n\n // URL parameters (from $paramName in URL)\n const urlParams = this.extractUrlParamNames(config.url)\n for (const param of urlParams) {\n params.push({\n name: param,\n in: 'path',\n required: true,\n schema: { type: 'string' },\n })\n }\n\n // Query parameters (from querySchema)\n if (config.querySchema) {\n const { schema: querySchema } = this.schemaConverter.convert(\n config.querySchema,\n )\n const schemaObj = querySchema as SchemaObject\n if (schemaObj.properties) {\n for (const [name, schema] of Object.entries(schemaObj.properties)) {\n params.push({\n name,\n in: 'query',\n required: schemaObj.required?.includes(name) ?? false,\n schema: schema as SchemaObject,\n description: (schema as SchemaObject).description,\n })\n }\n }\n }\n\n return params\n }\n\n /**\n * Builds request body based on endpoint type\n */\n private buildRequestBody(\n config: BaseEndpointConfig,\n handler: HandlerMetadata<any>,\n ): RequestBodyObject | undefined {\n const type = this.getEndpointType(handler)\n\n switch (type) {\n case 'multipart':\n return this.buildMultipartRequestBody(config)\n case 'stream':\n return undefined // Streams typically don't have request bodies\n case 'endpoint':\n default:\n return this.buildJsonRequestBody(config)\n }\n }\n\n /**\n * Builds request body for JSON endpoints\n */\n private buildJsonRequestBody(\n config: BaseEndpointConfig,\n ): RequestBodyObject | undefined {\n if (!config.requestSchema) {\n return undefined\n }\n\n const { schema } = this.schemaConverter.convert(config.requestSchema)\n\n return {\n required: true,\n content: {\n 'application/json': {\n schema,\n },\n },\n }\n }\n\n /**\n * Builds request body for multipart endpoints\n */\n private buildMultipartRequestBody(\n config: BaseEndpointConfig,\n ): RequestBodyObject {\n if (!config.requestSchema) {\n return {\n required: true,\n content: {\n 'multipart/form-data': {\n schema: { type: 'object' },\n },\n },\n }\n }\n\n const schema = this.schemaConverter.convert(config.requestSchema).schema as SchemaObject\n\n // Transform schema properties to handle File types\n const properties = this.schemaConverter.transformFileProperties(\n (schema.properties as Record<string, SchemaObject>) || {},\n )\n\n return {\n required: true,\n content: {\n 'multipart/form-data': {\n schema: {\n type: 'object',\n properties,\n required: schema.required,\n },\n },\n },\n }\n }\n\n /**\n * Builds responses based on endpoint type\n */\n private buildResponses(endpoint: DiscoveredEndpoint): ResponsesObject {\n const { config, handler } = endpoint\n const type = this.getEndpointType(handler)\n\n switch (type) {\n case 'stream':\n return this.buildStreamResponses(endpoint)\n case 'multipart':\n case 'endpoint':\n default:\n return this.buildJsonResponses(config, handler)\n }\n }\n\n /**\n * Builds responses for JSON endpoints\n */\n private buildJsonResponses(\n config: BaseEndpointConfig,\n handler: HandlerMetadata<any>,\n ): ResponsesObject {\n const successCode = handler.successStatusCode?.toString() ?? '200'\n\n if (!config.responseSchema) {\n return {\n [successCode]: {\n description: 'Successful response',\n },\n }\n }\n\n const { schema } = this.schemaConverter.convert(config.responseSchema)\n\n return {\n [successCode]: {\n description: 'Successful response',\n content: {\n 'application/json': {\n schema,\n },\n },\n },\n }\n }\n\n /**\n * Builds responses for stream endpoints\n */\n private buildStreamResponses(endpoint: DiscoveredEndpoint): ResponsesObject {\n const { openApiMetadata, handler } = endpoint\n const successCode = handler.successStatusCode?.toString() ?? '200'\n\n const contentType =\n openApiMetadata.stream?.contentType ?? 'application/octet-stream'\n const description =\n openApiMetadata.stream?.description ?? 'Stream response'\n\n const content: ContentObject = this.getStreamContent(contentType)\n\n return {\n [successCode]: {\n description,\n content,\n },\n }\n }\n\n /**\n * Gets content object for different stream types\n */\n private getStreamContent(contentType: string): ContentObject {\n switch (contentType) {\n case 'text/event-stream':\n return {\n 'text/event-stream': {\n schema: {\n type: 'string',\n description: 'Server-Sent Events stream',\n },\n },\n }\n\n case 'application/octet-stream':\n return {\n 'application/octet-stream': {\n schema: {\n type: 'string',\n format: 'binary',\n description: 'Binary file download',\n },\n },\n }\n\n case 'application/json':\n return {\n 'application/json': {\n schema: {\n type: 'string',\n description: 'Newline-delimited JSON stream',\n },\n },\n }\n\n default:\n return {\n [contentType]: {\n schema: { type: 'string', format: 'binary' },\n },\n }\n }\n }\n}\n","import type { ModuleMetadata } from '@navios/core'\nimport type { oas31 } from 'zod-openapi'\n\nimport { inject, Injectable, Logger } from '@navios/core'\n\nimport type { DiscoveredEndpoint } from './endpoint-scanner.service.mjs'\n\nimport { EndpointScannerService } from './endpoint-scanner.service.mjs'\nimport { PathBuilderService } from './path-builder.service.mjs'\n\ntype OpenAPIObject = oas31.OpenAPIObject\ntype PathsObject = oas31.PathsObject\ntype SecuritySchemeObject = oas31.SecuritySchemeObject\ntype TagObject = oas31.TagObject\n\n/**\n * Options for generating the OpenAPI document\n */\nexport interface OpenApiGeneratorOptions {\n /**\n * OpenAPI document info\n */\n info: {\n title: string\n version: string\n description?: string\n termsOfService?: string\n contact?: {\n name?: string\n url?: string\n email?: string\n }\n license?: {\n name: string\n url?: string\n }\n }\n\n /**\n * External documentation\n */\n externalDocs?: {\n url: string\n description?: string\n }\n\n /**\n * Server definitions\n */\n servers?: Array<{\n url: string\n description?: string\n variables?: Record<\n string,\n {\n default: string\n enum?: string[]\n description?: string\n }\n >\n }>\n\n /**\n * Security scheme definitions\n */\n securitySchemes?: Record<string, SecuritySchemeObject>\n\n /**\n * Global security requirements\n */\n security?: Array<Record<string, string[]>>\n\n /**\n * Tag definitions with descriptions\n */\n tags?: TagObject[]\n}\n\n/**\n * Service responsible for generating the complete OpenAPI document.\n *\n * Orchestrates endpoint discovery, path generation, and document assembly.\n */\n@Injectable()\nexport class OpenApiGeneratorService {\n private readonly logger = inject(Logger, {\n context: OpenApiGeneratorService.name,\n })\n\n private readonly scanner = inject(EndpointScannerService)\n private readonly pathBuilder = inject(PathBuilderService)\n\n /**\n * Generates an OpenAPI document from loaded modules.\n *\n * @param modules - Map of loaded modules\n * @param options - OpenAPI generation options\n * @returns Complete OpenAPI document\n */\n generate(\n modules: Map<string, ModuleMetadata>,\n options: OpenApiGeneratorOptions,\n ): OpenAPIObject {\n this.logger.debug('Generating OpenAPI document')\n\n // Discover all endpoints\n const endpoints = this.scanner.scan(modules)\n\n // Generate paths\n const paths = this.buildPaths(endpoints)\n\n // Collect unique tags from endpoints\n const discoveredTags = this.collectTags(endpoints)\n\n // Merge discovered tags with configured tags\n const tags = this.mergeTags(discoveredTags, options.tags)\n\n // Build the OpenAPI document\n const document: OpenAPIObject = {\n openapi: '3.1.0',\n info: options.info,\n paths,\n }\n\n // Add optional fields\n if (options.servers && options.servers.length > 0) {\n document.servers = options.servers\n }\n\n if (options.externalDocs) {\n document.externalDocs = options.externalDocs\n }\n\n if (tags.length > 0) {\n document.tags = tags\n }\n\n if (options.security) {\n document.security = options.security\n }\n\n if (options.securitySchemes) {\n document.components = {\n ...document.components,\n securitySchemes: options.securitySchemes,\n }\n }\n\n this.logger.debug(\n `Generated OpenAPI document with ${Object.keys(paths).length} paths`,\n )\n\n return document\n }\n\n /**\n * Builds paths object from discovered endpoints\n */\n private buildPaths(endpoints: DiscoveredEndpoint[]): PathsObject {\n const paths: PathsObject = {}\n\n for (const endpoint of endpoints) {\n const { path, pathItem } = this.pathBuilder.build(endpoint)\n\n // Merge with existing path if methods differ\n if (paths[path]) {\n paths[path] = {\n ...paths[path],\n ...pathItem,\n }\n } else {\n paths[path] = pathItem\n }\n }\n\n return paths\n }\n\n /**\n * Collects unique tags from endpoints\n */\n private collectTags(endpoints: DiscoveredEndpoint[]): Set<string> {\n const tags = new Set<string>()\n\n for (const endpoint of endpoints) {\n for (const tag of endpoint.openApiMetadata.tags) {\n tags.add(tag)\n }\n }\n\n return tags\n }\n\n /**\n * Merges discovered tags with configured tags\n */\n private mergeTags(\n discoveredTags: Set<string>,\n configuredTags?: TagObject[],\n ): TagObject[] {\n const tagMap = new Map<string, TagObject>()\n\n // Add configured tags first (they have descriptions)\n if (configuredTags) {\n for (const tag of configuredTags) {\n tagMap.set(tag.name, tag)\n }\n }\n\n // Add discovered tags that aren't already configured\n for (const tagName of discoveredTags) {\n if (!tagMap.has(tagName)) {\n tagMap.set(tagName, { name: tagName })\n }\n }\n\n return Array.from(tagMap.values())\n }\n}\n"],"mappings":";;;;;;;;mCAKA,MAAaA,cAAcC,OAAOC,IAAI,qBAAA;yCAGtC,MAAaC,oBAAoBF,OAAOC,IAAI,2BAAA;uCAG5C,MAAaE,kBAAkBH,OAAOC,IAAI,yBAAA;0CAG1C,MAAaG,qBAAqBJ,OAAOC,IAAI,4BAAA;wCAG7C,MAAaI,mBAAmBL,OAAOC,IAAI,0BAAA;uCAG3C,MAAaK,kBAAkBN,OAAOC,IAAI,yBAAA;sCAG1C,MAAaM,iBAAiBP,OAAOC,IAAI,wBAAA;;;;AClBzC,MAAMU,eAAeF,EAAEG,OAAO;CAC5BC,MAAMJ,EAAEK,QAAM;CACdC,aAAaN,EAAEK,QAAM,CAAGE,UAAQ;CAClC,CAAA;AAKA,MAAMC,aAAaT,iBAAiBU,gBAAgBR,aAAaC,aAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCjE,SAAgBQ,OAAON,MAAcE,aAAoB;AACvD,QAAOE,WAAW;EAAEJ;EAAME;EAAY,CAAA;;;;;AC3CxC,MAAMQ,qBAAqBF,EAAEG,OAAO;CAClCC,SAASJ,EAAEK,QAAM,CAAGC,UAAQ;CAC5BC,aAAaP,EAAEK,QAAM,CAAGC,UAAQ;CAChCE,aAAaR,EAAEK,QAAM,CAAGC,UAAQ;CAChCG,YAAYT,EAAEU,SAAO,CAAGJ,UAAQ;CAChCK,cAAcX,EACXG,OAAO;EACNS,KAAKZ,EAAEK,QAAM;EACbE,aAAaP,EAAEK,QAAM,CAAGC,UAAQ;EAClC,CAAA,CACCA,UAAQ;CACb,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCA,MAAaO,eAAed,iBAAiBe,gBAC3Cb,mBACAC,mBAAAA;;;;ACnDF,MAAMgB,mBAAmBF,EAAEG,QAAM;;;;;;;;;;;;;;;;;;;;;GAuBjC,MAAaC,aAAaL,iBAAiBM,gBACzCJ,iBACAC,iBAAAA;;;;ACzBF,MAAMO,sBAAsBF,EAAEG,OAAO,EACnCC,SAASJ,EAAEK,QAAM,CAAGC,UAAQ,EAC9B,CAAA;AAKA,MAAMC,oBAAoBR,iBAAiBS,gBACzCP,oBACAC,oBAAAA;;;;;;;;;;;;;;;;;;;;;;;;GA2BF,SAAgBO,cAAcL,SAAgB;AAC5C,QAAOG,kBAAkB,EAAEH,SAAQ,CAAA;;;;;ACrCrC,MAAMS,oBAAoBF,EAAEG,OAAOH,EAAEI,QAAM,EAAIJ,EAAEK,MAAML,EAAEI,QAAM,CAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkC/D,MAAaE,cAAcP,iBAAiBQ,gBAC1CN,kBACAC,kBAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GCbF,MAAaQ,aAAaF,iBAAiBG,gBAAgBF,gBAAAA;;;;ACvB3D,MAAMM,kBAAkBF,EAAEG,OAAO;CAC/BC,aAAaJ,EAAEK,QAAM;CACrBC,aAAaN,EAAEK,QAAM,CAAGE,UAAQ;CAClC,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CA,MAAaC,YAAYT,iBAAiBU,gBACxCR,gBACAC,gBAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SC9BDQ,YAAAA;AACM,IAAMQ,2BAAN,MAAMA;;;;;;;;;;IAQXC,QACEC,YACAC,SACyB;EAEzB,MAAMC,gBAAgBF,WAAWG,iBAAiBC,IAAIP,YAAAA;EAKtD,MAAMQ,aAAaJ,QAAQE,iBAAiBC,IAAIP,YAAAA;EAGhD,MAAMS,YAAYL,QAAQE,iBAAiBC,IAAIX,kBAAAA;EAS/C,MAAMc,UAAUN,QAAQE,iBAAiBC,IAAIR,gBAAAA;EAG7C,MAAMY,aAAaP,QAAQE,iBAAiBC,IAAIb,mBAAAA;EAGhD,MAAMkB,WAAWR,QAAQE,iBAAiBC,IAAIV,iBAAAA;EAG9C,MAAMgB,WAAWT,QAAQE,iBAAiBC,IAAIZ,gBAAAA;EAG9C,MAAMmB,SAASV,QAAQE,iBAAiBC,IAAIT,eAAAA;EAK5C,MAAMiB,OAAiB,EAAE;AACzB,MAAIV,eAAeW,KACjBD,MAAKE,KAAKZ,cAAcW,KAAI;AAE9B,MAAIR,YAAYQ,QAAQR,WAAWQ,SAASX,eAAeW,KACzDD,MAAKE,KAAKT,WAAWQ,KAAI;AAG3B,SAAO;GACLD;GACAL,SAASD,WAAWC,WAAWA;GAC/BQ,aAAaT,WAAWS;GACxBC,aAAaV,WAAWU;GACxBR,YAAYA,eAAeS,UAAaX,WAAWE,eAAe;GAClEU,cAAcZ,WAAWY;GACzBT,UAAUA,WAAW,CAACA,SAAS,GAAGQ;GAClCP,UAAUA,aAAa;GACvBC;GACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SCnDHU,YAAAA;AACM,IAAMG,yBAAN,MAAMA;;;;CACMC,SAASL,OAAOE,QAAQ,EACvCI,SAASF,wBAAuBG,MAClC,CAAA;CAEiBC,oBAAoBR,OAAOG,0BAAAA;;;;;;IAQ5CM,KAAKC,SAA4D;EAC/D,MAAMC,YAAkC,EAAE;AAE1C,OAAK,MAAM,CAACC,YAAYC,mBAAmBH,SAAS;AAClD,OAAI,CAACG,eAAeC,eAAeD,eAAeC,YAAYC,SAAS,EACrE;AAGF,QAAKV,OAAOW,MAAM,oBAAoBJ,aAAY;AAElD,QAAK,MAAMK,mBAAmBJ,eAAeC,aAAa;IACxD,MAAMI,iBAAiBnB,0BAA0BkB,gBAAAA;IACjD,MAAME,sBAAsB,KAAKC,eAC/BP,gBACAI,iBACAC,eAAAA;AAEFP,cAAUU,KAAI,GAAIF,oBAAAA;;;AAItB,OAAKd,OAAOW,MAAM,cAAcL,UAAUW,OAAO,YAAW;AAC5D,SAAOX;;;;IAMT,eACEY,QACAN,iBACAC,gBACsB;EACtB,MAAMP,YAAkC,EAAE;AAE1C,OAAK,MAAMa,WAAWN,eAAeP,WAAW;AAE9C,OAAI,CAACa,QAAQC,OACX;GAGF,MAAMC,kBAAkB,KAAKlB,kBAAkBmB,QAC7CT,gBACAM,QAAAA;AAIF,OAAIE,gBAAgBE,UAAU;AAC5B,SAAKvB,OAAOW,MACV,+BAA+BQ,QAAQK,cAAa;AAEtD;;AAGFlB,aAAUU,KAAK;IACbE;IACAN;IACAa,YAAYZ;IACZM;IACAC,QAAQD,QAAQC;IAChBC;IACF,CAAA;;AAGF,SAAOf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SC5FVoB,YAAAA;AACM,IAAME,yBAAN,MAAMA;;;;;;;;;;;;;;;;;;;;IAkBXC,QAAQC,QAAyC;AAC/C,SAAOH,aAAaG,OAAAA;;;;;;;;;IAWtBC,aAAaD,QAA+B;AAC1C,SAAOA,OAAOE,SAAS,YAAYF,OAAOG,WAAW;;;;;;;;;IAWvDC,wBACEC,YAC8B;EAC9B,MAAMC,SAAuC,EAAC;AAE9C,OAAK,MAAM,CAACC,KAAKC,SAASC,OAAOC,QAAQL,WAAAA,CACvC,KAAI,KAAKJ,aAAaO,KAAAA,CACpBF,QAAOC,OAAO;GACZL,MAAM;GACNC,QAAQ;GACRQ,aAAaH,KAAKG;GACpB;WAEAH,KAAKN,SAAS,WACdM,KAAKI,SACL,KAAKX,aAAaO,KAAKI,MAAK,CAE5BN,QAAOC,OAAO;GACZL,MAAM;GACNU,OAAO;IAAEV,MAAM;IAAUC,QAAQ;IAAS;GAC1CQ,aAAaH,KAAKG;GACpB;MAEAL,QAAOC,OAAOC;AAIlB,SAAOF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SCvDVQ,YAAAA;AACM,IAAMI,qBAAN,MAAMA;;;;CACMC,kBAAkBN,OAAOI,wBAAAA;;;;;;IAQ1CG,MAAMC,UAA8C;EAClD,MAAM,EAAEC,QAAQC,SAASC,oBAAoBH;EAG7C,MAAMI,OAAO,KAAKC,iBAAiBJ,OAAOK,IAAG;EAE7C,MAAMC,YAA6B;GACjCC,MAAML,gBAAgBK,KAAKC,SAAS,IAAIN,gBAAgBK,OAAOE;GAC/DC,SAASR,gBAAgBQ;GACzBC,aAAaT,gBAAgBS;GAC7BC,aAAaV,gBAAgBU;GAC7BC,YAAYX,gBAAgBW,cAAcJ;GAC1CK,cAAcZ,gBAAgBY;GAC9BC,UAAUb,gBAAgBa;GAC1BC,YAAY,KAAKC,gBAAgBjB,OAAAA;GACjCkB,aAAa,KAAKC,iBAAiBnB,QAAQC,QAAAA;GAC3CmB,WAAW,KAAKC,eAAetB,SAAAA;GACjC;EAGA,MAAMuB,iBAAiBC,OAAOC,YAC5BD,OAAOE,QAAQnB,UAAAA,CAAWoB,QAAQ,GAAGC,OAAOA,MAAMlB,OAAAA,CAAAA;AAGpD,SAAO;GACLN;GACAyB,UAAU,GACP5B,OAAO6B,OAAOC,aAAW,GAAKR,gBACjC;GACF;;;;IAMFlB,iBAAiBC,KAAqB;AACpC,SAAOA,IAAI0B,QAAQ,YAAY,OAAA;;;;IAMjCC,qBAAqB3B,KAAuB;EAC1C,MAAM4B,UAAU5B,IAAI6B,SAAS,WAAA;AAC7B,SAAOC,MAAMC,KAAKH,UAAUI,MAAMA,EAAE,GAAE;;;;IAMxCC,gBACErC,SACqC;AACrC,MAAIA,QAAQsC,iBAAiB9C,sBAC3B,QAAO;AAET,MAAIQ,QAAQsC,iBAAiB7C,mBAC3B,QAAO;AAET,SAAO;;;;IAMT,gBAAwBM,QAA+C;EACrE,MAAMwC,SAA4B,EAAE;EAGpC,MAAMC,YAAY,KAAKT,qBAAqBhC,OAAOK,IAAG;AACtD,OAAK,MAAMqC,SAASD,UAClBD,QAAOG,KAAK;GACVC,MAAMF;GACNG,IAAI;GACJC,UAAU;GACVC,QAAQ,EAAEC,MAAM,UAAS;GAC3B,CAAA;AAIF,MAAIhD,OAAOiD,aAAa;GACtB,MAAM,EAAEF,QAAQE,gBAAgB,KAAKpD,gBAAgBqD,QACnDlD,OAAOiD,YAAW;GAEpB,MAAME,YAAYF;AAClB,OAAIE,UAAUC,WACZ,MAAK,MAAM,CAACR,MAAMG,WAAWxB,OAAOE,QAAQ0B,UAAUC,WAAU,CAC9DZ,QAAOG,KAAK;IACVC;IACAC,IAAI;IACJC,UAAUK,UAAUL,UAAUO,SAAST,KAAAA,IAAS;IACxCG;IACRpC,aAAa,OAAyBA;IACxC,CAAA;;AAKN,SAAO6B;;;;IAMT,iBACExC,QACAC,SAC+B;AAG/B,UAFa,KAAKqC,gBAAgBrC,QAAAA,EAElC;GACE,KAAK,YACH,QAAO,KAAKqD,0BAA0BtD,OAAAA;GACxC,KAAK,SACH;GACF,KAAK;GACL,QACE,QAAO,KAAKuD,qBAAqBvD,OAAAA;;;;;IAOvC,qBACEA,QAC+B;AAC/B,MAAI,CAACA,OAAOwD,cACV;EAGF,MAAM,EAAET,WAAW,KAAKlD,gBAAgBqD,QAAQlD,OAAOwD,cAAa;AAEpE,SAAO;GACLV,UAAU;GACVW,SAAS,EACP,oBAAoB,EAClBV,QACF,EACF;GACF;;;;IAMF,0BACE/C,QACmB;AACnB,MAAI,CAACA,OAAOwD,cACV,QAAO;GACLV,UAAU;GACVW,SAAS,EACP,uBAAuB,EACrBV,QAAQ,EAAEC,MAAM,UAAS,EAC3B,EACF;GACF;EAGF,MAAMD,SAAS,KAAKlD,gBAAgBqD,QAAQlD,OAAOwD,cAAa,CAAET;EAGlE,MAAMK,aAAa,KAAKvD,gBAAgB6D,wBACtC,OAAQN,cAA+C,EAAC,CAAA;AAG1D,SAAO;GACLN,UAAU;GACVW,SAAS,EACP,uBAAuB,EACrBV,QAAQ;IACNC,MAAM;IACNI;IACAN,UAAUC,OAAOD;IACnB,EACF,EACF;GACF;;;;IAMF,eAAuB/C,UAA+C;EACpE,MAAM,EAAEC,QAAQC,YAAYF;AAG5B,UAFa,KAAKuC,gBAAgBrC,QAAAA,EAElC;GACE,KAAK,SACH,QAAO,KAAK0D,qBAAqB5D,SAAAA;GACnC,KAAK;GACL,KAAK;GACL,QACE,QAAO,KAAK6D,mBAAmB5D,QAAQC,QAAAA;;;;;IAO7C,mBACED,QACAC,SACiB;EACjB,MAAM4D,cAAc5D,QAAQ6D,mBAAmBC,UAAAA,IAAc;AAE7D,MAAI,CAAC/D,OAAOgE,eACV,QAAO,GACJH,cAAc,EACblD,aAAa,uBACf,EACF;EAGF,MAAM,EAAEoC,WAAW,KAAKlD,gBAAgBqD,QAAQlD,OAAOgE,eAAc;AAErE,SAAO,GACJH,cAAc;GACblD,aAAa;GACb8C,SAAS,EACP,oBAAoB,EAClBV,QACF,EACF;GACF,EACF;;;;IAMF,qBAA6BhD,UAA+C;EAC1E,MAAM,EAAEG,iBAAiBD,YAAYF;EACrC,MAAM8D,cAAc5D,QAAQ6D,mBAAmBC,UAAAA,IAAc;EAE7D,MAAME,cACJ/D,gBAAgBgE,QAAQD,eAAe;EACzC,MAAMtD,cACJT,gBAAgBgE,QAAQvD,eAAe;EAEzC,MAAM8C,UAAyB,KAAKU,iBAAiBF,YAAAA;AAErD,SAAO,GACJJ,cAAc;GACblD;GACA8C;GACF,EACF;;;;IAMF,iBAAyBQ,aAAoC;AAC3D,UAAQA,aAAR;GACE,KAAK,oBACH,QAAO,EACL,qBAAqB,EACnBlB,QAAQ;IACNC,MAAM;IACNrC,aAAa;IACf,EACF,EACF;GAEF,KAAK,2BACH,QAAO,EACL,4BAA4B,EAC1BoC,QAAQ;IACNC,MAAM;IACNoB,QAAQ;IACRzD,aAAa;IACf,EACF,EACF;GAEF,KAAK,mBACH,QAAO,EACL,oBAAoB,EAClBoC,QAAQ;IACNC,MAAM;IACNrC,aAAa;IACf,EACF,EACF;GAEF,QACE,QAAO,GACJsD,cAAc,EACblB,QAAQ;IAAEC,MAAM;IAAUoB,QAAQ;IAAS,EAC7C,EACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OCjQPE,YAAAA;AACM,IAAMI,0BAAN,MAAMA;;;;CACMC,SAASN,OAAOE,QAAQ,EACvCK,SAASF,yBAAwBG,MACnC,CAAA;CAEiBC,UAAUT,OAAOG,wBAAAA;CACjBO,cAAcV,OAAOI,oBAAAA;;;;;;;IAStCO,SACEC,SACAC,SACe;AACf,OAAKP,OAAOQ,MAAM,8BAAA;EAGlB,MAAMC,YAAY,KAAKN,QAAQO,KAAKJ,QAAAA;EAGpC,MAAMK,QAAQ,KAAKC,WAAWH,UAAAA;EAG9B,MAAMI,iBAAiB,KAAKC,YAAYL,UAAAA;EAGxC,MAAMM,OAAO,KAAKC,UAAUH,gBAAgBN,QAAQQ,KAAI;EAGxD,MAAME,WAA0B;GAC9BC,SAAS;GACTC,MAAMZ,QAAQY;GACdR;GACF;AAGA,MAAIJ,QAAQa,WAAWb,QAAQa,QAAQC,SAAS,EAC9CJ,UAASG,UAAUb,QAAQa;AAG7B,MAAIb,QAAQe,aACVL,UAASK,eAAef,QAAQe;AAGlC,MAAIP,KAAKM,SAAS,EAChBJ,UAASF,OAAOA;AAGlB,MAAIR,QAAQgB,SACVN,UAASM,WAAWhB,QAAQgB;AAG9B,MAAIhB,QAAQiB,gBACVP,UAASQ,aAAa;GACpB,GAAGR,SAASQ;GACZD,iBAAiBjB,QAAQiB;GAC3B;AAGF,OAAKxB,OAAOQ,MACV,mCAAmCkB,OAAOC,KAAKhB,MAAAA,CAAOU,OAAO,QAAO;AAGtE,SAAOJ;;;;IAMT,WAAmBR,WAA8C;EAC/D,MAAME,QAAqB,EAAC;AAE5B,OAAK,MAAMiB,YAAYnB,WAAW;GAChC,MAAM,EAAEoB,MAAMC,aAAa,KAAK1B,YAAY2B,MAAMH,SAAAA;AAGlD,OAAIjB,MAAMkB,MACRlB,OAAMkB,QAAQ;IACZ,GAAGlB,MAAMkB;IACT,GAAGC;IACL;OAEAnB,OAAMkB,QAAQC;;AAIlB,SAAOnB;;;;IAMT,YAAoBF,WAA8C;EAChE,MAAMM,uBAAO,IAAIiB,KAAAA;AAEjB,OAAK,MAAMJ,YAAYnB,UACrB,MAAK,MAAMwB,OAAOL,SAASM,gBAAgBnB,KACzCA,MAAKoB,IAAIF,IAAAA;AAIb,SAAOlB;;;;IAMT,UACEF,gBACAuB,gBACa;EACb,MAAMC,yBAAS,IAAIC,KAAAA;AAGnB,MAAIF,eACF,MAAK,MAAMH,OAAOG,eAChBC,QAAOE,IAAIN,IAAI/B,MAAM+B,IAAAA;AAKzB,OAAK,MAAMO,WAAW3B,eACpB,KAAI,CAACwB,OAAOI,IAAID,QAAAA,CACdH,QAAOE,IAAIC,SAAS,EAAEtC,MAAMsC,SAAQ,CAAA;AAIxC,SAAOE,MAAMC,KAAKN,OAAOO,QAAM,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@navios/openapi",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "Oleksandr Hanzha",
|
|
6
|
+
"email": "alex@granted.name"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"directory": "packages/openapi",
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/Arilas/navios.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"@navios/core": "^0.7.1",
|
|
16
|
+
"zod": "^3.25.0 || ^4.0.0"
|
|
17
|
+
},
|
|
18
|
+
"typings": "./lib/index.d.mts",
|
|
19
|
+
"main": "./lib/index.cjs",
|
|
20
|
+
"module": "./lib/index.mjs",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": {
|
|
24
|
+
"types": "./lib/index.d.mts",
|
|
25
|
+
"default": "./lib/index.mjs"
|
|
26
|
+
},
|
|
27
|
+
"require": {
|
|
28
|
+
"types": "./lib/index.d.cts",
|
|
29
|
+
"default": "./lib/index.cjs"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@navios/builder": "^0.5.1",
|
|
35
|
+
"@navios/core": "^0.7.1",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"zod": "^4.2.1"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@navios/di": "^0.6.1",
|
|
41
|
+
"yaml": "^2.8.2",
|
|
42
|
+
"zod-openapi": "^5.4.5"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@navios/openapi",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "packages/openapi/src",
|
|
5
|
+
"prefix": "openapi",
|
|
6
|
+
"tags": [],
|
|
7
|
+
"projectType": "library",
|
|
8
|
+
"targets": {
|
|
9
|
+
"check": {
|
|
10
|
+
"executor": "nx:run-commands",
|
|
11
|
+
"outputs": ["{projectRoot}/dist"],
|
|
12
|
+
"inputs": [
|
|
13
|
+
"^projectSources",
|
|
14
|
+
"projectSources",
|
|
15
|
+
"{projectRoot}/tsconfig.json",
|
|
16
|
+
"{projectRoot}/tsconfig.lib.json"
|
|
17
|
+
],
|
|
18
|
+
"options": {
|
|
19
|
+
"command": ["tsc -b"],
|
|
20
|
+
"cwd": "packages/openapi"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"lint": {
|
|
24
|
+
"executor": "nx:run-commands",
|
|
25
|
+
"inputs": ["^projectSources", "project"],
|
|
26
|
+
"options": {
|
|
27
|
+
"command": "oxlint --fix",
|
|
28
|
+
"cwd": "packages/openapi"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"test:ci": {
|
|
32
|
+
"executor": "nx:run-commands",
|
|
33
|
+
"inputs": ["^projectSources", "project"],
|
|
34
|
+
"options": {
|
|
35
|
+
"command": "vitest run",
|
|
36
|
+
"cwd": "packages/openapi"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"build": {
|
|
40
|
+
"executor": "nx:run-commands",
|
|
41
|
+
"inputs": ["projectSources", "{projectRoot}/tsdown.config.mts"],
|
|
42
|
+
"outputs": ["{projectRoot}/lib"],
|
|
43
|
+
"dependsOn": ["check", "test:ci", "lint"],
|
|
44
|
+
"options": {
|
|
45
|
+
"command": "tsdown",
|
|
46
|
+
"cwd": "packages/openapi"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"publish": {
|
|
50
|
+
"executor": "nx:run-commands",
|
|
51
|
+
"dependsOn": ["build"],
|
|
52
|
+
"options": {
|
|
53
|
+
"command": "yarn npm publish --access public",
|
|
54
|
+
"cwd": "packages/openapi"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"publish:next": {
|
|
58
|
+
"executor": "nx:run-commands",
|
|
59
|
+
"dependsOn": ["build"],
|
|
60
|
+
"options": {
|
|
61
|
+
"command": "yarn npm publish --access public --tag next",
|
|
62
|
+
"cwd": "packages/openapi"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { Controller, extractControllerMetadata } from '@navios/core'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ApiDeprecated,
|
|
7
|
+
ApiExclude,
|
|
8
|
+
ApiOperation,
|
|
9
|
+
ApiSecurity,
|
|
10
|
+
ApiStream,
|
|
11
|
+
ApiSummary,
|
|
12
|
+
ApiTag,
|
|
13
|
+
} from '../decorators/index.mjs'
|
|
14
|
+
import {
|
|
15
|
+
ApiDeprecatedToken,
|
|
16
|
+
ApiExcludeToken,
|
|
17
|
+
ApiOperationToken,
|
|
18
|
+
ApiSecurityToken,
|
|
19
|
+
ApiStreamToken,
|
|
20
|
+
ApiSummaryToken,
|
|
21
|
+
ApiTagToken,
|
|
22
|
+
} from '../tokens/index.mjs'
|
|
23
|
+
|
|
24
|
+
describe('OpenAPI Decorators', () => {
|
|
25
|
+
describe('@ApiTag', () => {
|
|
26
|
+
it('should store tag metadata on class', () => {
|
|
27
|
+
// @ApiTag must come before @Controller (decorators execute bottom-up)
|
|
28
|
+
@ApiTag('Users', 'User management operations')
|
|
29
|
+
@Controller()
|
|
30
|
+
class UserController {}
|
|
31
|
+
|
|
32
|
+
const meta = extractControllerMetadata(UserController)
|
|
33
|
+
const tagMeta = meta.customAttributes.get(ApiTagToken)
|
|
34
|
+
|
|
35
|
+
expect(tagMeta).toEqual({
|
|
36
|
+
name: 'Users',
|
|
37
|
+
description: 'User management operations',
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should work with just name', () => {
|
|
42
|
+
@ApiTag('Products')
|
|
43
|
+
@Controller()
|
|
44
|
+
class ProductController {}
|
|
45
|
+
|
|
46
|
+
const meta = extractControllerMetadata(ProductController)
|
|
47
|
+
const tagMeta = meta.customAttributes.get(ApiTagToken)
|
|
48
|
+
|
|
49
|
+
expect(tagMeta).toEqual({
|
|
50
|
+
name: 'Products',
|
|
51
|
+
description: undefined,
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('@ApiOperation', () => {
|
|
57
|
+
it('should store operation metadata on method', () => {
|
|
58
|
+
@Controller()
|
|
59
|
+
class TestController {
|
|
60
|
+
@ApiOperation({
|
|
61
|
+
summary: 'Get user by ID',
|
|
62
|
+
description: 'Retrieves a user by their unique identifier',
|
|
63
|
+
operationId: 'getUserById',
|
|
64
|
+
deprecated: false,
|
|
65
|
+
})
|
|
66
|
+
getUser() {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const meta = extractControllerMetadata(TestController)
|
|
70
|
+
const handler = [...meta.endpoints][0]
|
|
71
|
+
const opMeta = handler.customAttributes.get(ApiOperationToken)
|
|
72
|
+
|
|
73
|
+
expect(opMeta).toEqual({
|
|
74
|
+
summary: 'Get user by ID',
|
|
75
|
+
description: 'Retrieves a user by their unique identifier',
|
|
76
|
+
operationId: 'getUserById',
|
|
77
|
+
deprecated: false,
|
|
78
|
+
externalDocs: undefined,
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('@ApiSummary', () => {
|
|
84
|
+
it('should store summary metadata on method', () => {
|
|
85
|
+
@Controller()
|
|
86
|
+
class TestController {
|
|
87
|
+
@ApiSummary('Create a new user')
|
|
88
|
+
createUser() {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const meta = extractControllerMetadata(TestController)
|
|
92
|
+
const handler = [...meta.endpoints][0]
|
|
93
|
+
const summaryMeta = handler.customAttributes.get(ApiSummaryToken)
|
|
94
|
+
|
|
95
|
+
expect(summaryMeta).toBe('Create a new user')
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('@ApiDeprecated', () => {
|
|
100
|
+
it('should store deprecated metadata on method', () => {
|
|
101
|
+
@Controller()
|
|
102
|
+
class TestController {
|
|
103
|
+
@ApiDeprecated('Use v2 instead')
|
|
104
|
+
legacyEndpoint() {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const meta = extractControllerMetadata(TestController)
|
|
108
|
+
const handler = [...meta.endpoints][0]
|
|
109
|
+
const deprecatedMeta = handler.customAttributes.get(ApiDeprecatedToken)
|
|
110
|
+
|
|
111
|
+
expect(deprecatedMeta).toEqual({
|
|
112
|
+
message: 'Use v2 instead',
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should work without message', () => {
|
|
117
|
+
@Controller()
|
|
118
|
+
class TestController {
|
|
119
|
+
@ApiDeprecated()
|
|
120
|
+
oldEndpoint() {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const meta = extractControllerMetadata(TestController)
|
|
124
|
+
const handler = [...meta.endpoints][0]
|
|
125
|
+
const deprecatedMeta = handler.customAttributes.get(ApiDeprecatedToken)
|
|
126
|
+
|
|
127
|
+
// When called without message, stores { message: undefined }
|
|
128
|
+
expect(deprecatedMeta).toMatchObject({})
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('@ApiSecurity', () => {
|
|
133
|
+
it('should store security metadata on method', () => {
|
|
134
|
+
@Controller()
|
|
135
|
+
class TestController {
|
|
136
|
+
@ApiSecurity({ bearerAuth: [] })
|
|
137
|
+
securedEndpoint() {}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const meta = extractControllerMetadata(TestController)
|
|
141
|
+
const handler = [...meta.endpoints][0]
|
|
142
|
+
const securityMeta = handler.customAttributes.get(ApiSecurityToken)
|
|
143
|
+
|
|
144
|
+
expect(securityMeta).toEqual({ bearerAuth: [] })
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('@ApiExclude', () => {
|
|
149
|
+
it('should store exclude metadata on method', () => {
|
|
150
|
+
@Controller()
|
|
151
|
+
class TestController {
|
|
152
|
+
@ApiExclude()
|
|
153
|
+
hiddenEndpoint() {}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const meta = extractControllerMetadata(TestController)
|
|
157
|
+
const handler = [...meta.endpoints][0]
|
|
158
|
+
const excludeMeta = handler.customAttributes.get(ApiExcludeToken)
|
|
159
|
+
|
|
160
|
+
expect(excludeMeta).toBe(true)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
describe('@ApiStream', () => {
|
|
165
|
+
it('should store stream metadata on method', () => {
|
|
166
|
+
@Controller()
|
|
167
|
+
class TestController {
|
|
168
|
+
@ApiStream({
|
|
169
|
+
contentType: 'text/event-stream',
|
|
170
|
+
description: 'Real-time events',
|
|
171
|
+
})
|
|
172
|
+
eventStream() {}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const meta = extractControllerMetadata(TestController)
|
|
176
|
+
const handler = [...meta.endpoints][0]
|
|
177
|
+
const streamMeta = handler.customAttributes.get(ApiStreamToken)
|
|
178
|
+
|
|
179
|
+
expect(streamMeta).toEqual({
|
|
180
|
+
contentType: 'text/event-stream',
|
|
181
|
+
description: 'Real-time events',
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
})
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Controller,
|
|
5
|
+
extractControllerMetadata,
|
|
6
|
+
type HandlerMetadata,
|
|
7
|
+
} from '@navios/core'
|
|
8
|
+
import { TestContainer } from '@navios/di'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
ApiDeprecated,
|
|
12
|
+
ApiExclude,
|
|
13
|
+
ApiOperation,
|
|
14
|
+
ApiSecurity,
|
|
15
|
+
ApiStream,
|
|
16
|
+
ApiSummary,
|
|
17
|
+
ApiTag,
|
|
18
|
+
} from '../decorators/index.mjs'
|
|
19
|
+
import { MetadataExtractorService } from '../services/metadata-extractor.service.mjs'
|
|
20
|
+
|
|
21
|
+
describe('Metadata Extraction', () => {
|
|
22
|
+
let container: TestContainer
|
|
23
|
+
let metadataExtractor: MetadataExtractorService
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
container = new TestContainer()
|
|
27
|
+
metadataExtractor = await container.get(MetadataExtractorService)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
await container.dispose()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('MetadataExtractorService', () => {
|
|
35
|
+
it('should extract tags from controller', () => {
|
|
36
|
+
@ApiTag('Users', 'User management')
|
|
37
|
+
@Controller()
|
|
38
|
+
class UserController {
|
|
39
|
+
@ApiSummary('Get user')
|
|
40
|
+
getUser() {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const controllerMeta = extractControllerMetadata(UserController)
|
|
44
|
+
const handler = [...controllerMeta.endpoints][0] as HandlerMetadata<any>
|
|
45
|
+
const metadata = metadataExtractor.extract(controllerMeta, handler)
|
|
46
|
+
|
|
47
|
+
expect(metadata.tags).toEqual(['Users'])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should extract tags from method (both controller and method tags)', () => {
|
|
51
|
+
@ApiTag('Users')
|
|
52
|
+
@Controller()
|
|
53
|
+
class MixedController {
|
|
54
|
+
@ApiTag('Orders')
|
|
55
|
+
getOrder() {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const controllerMeta = extractControllerMetadata(MixedController)
|
|
59
|
+
const handler = [...controllerMeta.endpoints][0] as HandlerMetadata<any>
|
|
60
|
+
const metadata = metadataExtractor.extract(controllerMeta, handler)
|
|
61
|
+
|
|
62
|
+
// Both tags should be included
|
|
63
|
+
expect(metadata.tags).toContain('Users')
|
|
64
|
+
expect(metadata.tags).toContain('Orders')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should extract summary from @ApiSummary', () => {
|
|
68
|
+
@Controller()
|
|
69
|
+
class TestController {
|
|
70
|
+
@ApiSummary('Get a user by ID')
|
|
71
|
+
getUser() {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const controllerMeta = extractControllerMetadata(TestController)
|
|
75
|
+
const handler = [...controllerMeta.endpoints][0] as HandlerMetadata<any>
|
|
76
|
+
const metadata = metadataExtractor.extract(controllerMeta, handler)
|
|
77
|
+
|
|
78
|
+
expect(metadata.summary).toBe('Get a user by ID')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should extract operation metadata from @ApiOperation', () => {
|
|
82
|
+
@Controller()
|
|
83
|
+
class TestController {
|
|
84
|
+
@ApiOperation({
|
|
85
|
+
summary: 'Create user',
|
|
86
|
+
description: 'Creates a new user in the system',
|
|
87
|
+
operationId: 'createUser',
|
|
88
|
+
deprecated: false,
|
|
89
|
+
})
|
|
90
|
+
createUser() {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const controllerMeta = extractControllerMetadata(TestController)
|
|
94
|
+
const handler = [...controllerMeta.endpoints][0] as HandlerMetadata<any>
|
|
95
|
+
const metadata = metadataExtractor.extract(controllerMeta, handler)
|
|
96
|
+
|
|
97
|
+
expect(metadata.summary).toBe('Create user')
|
|
98
|
+
expect(metadata.description).toBe('Creates a new user in the system')
|
|
99
|
+
expect(metadata.operationId).toBe('createUser')
|
|
100
|
+
expect(metadata.deprecated).toBe(false)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should mark endpoint as deprecated', () => {
|
|
104
|
+
@Controller()
|
|
105
|
+
class TestController {
|
|
106
|
+
@ApiDeprecated('Use v2 API instead')
|
|
107
|
+
legacyEndpoint() {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const controllerMeta = extractControllerMetadata(TestController)
|
|
111
|
+
const handler = [...controllerMeta.endpoints][0] as HandlerMetadata<any>
|
|
112
|
+
const metadata = metadataExtractor.extract(controllerMeta, handler)
|
|
113
|
+
|
|
114
|
+
expect(metadata.deprecated).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should extract security requirements', () => {
|
|
118
|
+
@Controller()
|
|
119
|
+
class TestController {
|
|
120
|
+
@ApiSecurity({ bearerAuth: [] })
|
|
121
|
+
securedEndpoint() {}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const controllerMeta = extractControllerMetadata(TestController)
|
|
125
|
+
const handler = [...controllerMeta.endpoints][0] as HandlerMetadata<any>
|
|
126
|
+
const metadata = metadataExtractor.extract(controllerMeta, handler)
|
|
127
|
+
|
|
128
|
+
expect(metadata.security).toEqual([{ bearerAuth: [] }])
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should mark endpoint as excluded', () => {
|
|
132
|
+
@Controller()
|
|
133
|
+
class TestController {
|
|
134
|
+
@ApiExclude()
|
|
135
|
+
hiddenEndpoint() {}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const controllerMeta = extractControllerMetadata(TestController)
|
|
139
|
+
const handler = [...controllerMeta.endpoints][0] as HandlerMetadata<any>
|
|
140
|
+
const metadata = metadataExtractor.extract(controllerMeta, handler)
|
|
141
|
+
|
|
142
|
+
expect(metadata.excluded).toBe(true)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should extract stream metadata', () => {
|
|
146
|
+
@Controller()
|
|
147
|
+
class TestController {
|
|
148
|
+
@ApiStream({
|
|
149
|
+
contentType: 'text/event-stream',
|
|
150
|
+
description: 'Real-time notifications',
|
|
151
|
+
})
|
|
152
|
+
streamEvents() {}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const controllerMeta = extractControllerMetadata(TestController)
|
|
156
|
+
const handler = [...controllerMeta.endpoints][0] as HandlerMetadata<any>
|
|
157
|
+
const metadata = metadataExtractor.extract(controllerMeta, handler)
|
|
158
|
+
|
|
159
|
+
expect(metadata.stream).toEqual({
|
|
160
|
+
contentType: 'text/event-stream',
|
|
161
|
+
description: 'Real-time notifications',
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('should combine multiple decorators', () => {
|
|
166
|
+
@ApiTag('Files')
|
|
167
|
+
@Controller()
|
|
168
|
+
class FileController {
|
|
169
|
+
@ApiOperation({
|
|
170
|
+
summary: 'Upload file',
|
|
171
|
+
description: 'Uploads a file to storage',
|
|
172
|
+
})
|
|
173
|
+
@ApiSecurity({ bearerAuth: [] })
|
|
174
|
+
uploadFile() {}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const controllerMeta = extractControllerMetadata(FileController)
|
|
178
|
+
const handler = [...controllerMeta.endpoints][0] as HandlerMetadata<any>
|
|
179
|
+
const metadata = metadataExtractor.extract(controllerMeta, handler)
|
|
180
|
+
|
|
181
|
+
expect(metadata.tags).toEqual(['Files'])
|
|
182
|
+
expect(metadata.summary).toBe('Upload file')
|
|
183
|
+
expect(metadata.description).toBe('Uploads a file to storage')
|
|
184
|
+
expect(metadata.security).toEqual([{ bearerAuth: [] }])
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should handle endpoint with no OpenAPI decorators', () => {
|
|
188
|
+
@Controller()
|
|
189
|
+
class PlainController {
|
|
190
|
+
@ApiSummary('Plain endpoint')
|
|
191
|
+
plainEndpoint() {}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const controllerMeta = extractControllerMetadata(PlainController)
|
|
195
|
+
const handler = [...controllerMeta.endpoints][0] as HandlerMetadata<any>
|
|
196
|
+
// Manually remove the summary to test no decorators scenario
|
|
197
|
+
handler.customAttributes.clear()
|
|
198
|
+
const metadata = metadataExtractor.extract(controllerMeta, handler)
|
|
199
|
+
|
|
200
|
+
expect(metadata.tags).toEqual([])
|
|
201
|
+
expect(metadata.summary).toBeUndefined()
|
|
202
|
+
expect(metadata.description).toBeUndefined()
|
|
203
|
+
expect(metadata.deprecated).toBe(false)
|
|
204
|
+
expect(metadata.excluded).toBe(false)
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
})
|