@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Injectable, Logger, MultipartAdapterToken, StreamAdapterToken, extractControllerMetadata, inject } from "@navios/core";
|
|
1
|
+
import { EndpointAdapterToken, Injectable, Logger, MultipartAdapterToken, StreamAdapterToken, extractControllerMetadata, inject } from "@navios/core";
|
|
2
2
|
import { createSchema } from "zod-openapi";
|
|
3
3
|
|
|
4
4
|
//#region src/tokens/index.mts
|
|
@@ -1340,9 +1340,10 @@ var PathBuilderService = class {
|
|
|
1340
1340
|
/**
|
|
1341
1341
|
* Gets the endpoint type based on the adapter token
|
|
1342
1342
|
*/ getEndpointType(handler) {
|
|
1343
|
+
if (handler.adapterToken === EndpointAdapterToken) return "endpoint";
|
|
1343
1344
|
if (handler.adapterToken === MultipartAdapterToken) return "multipart";
|
|
1344
1345
|
if (handler.adapterToken === StreamAdapterToken) return "stream";
|
|
1345
|
-
return "
|
|
1346
|
+
return "unknown";
|
|
1346
1347
|
}
|
|
1347
1348
|
/**
|
|
1348
1349
|
* Builds OpenAPI parameters from endpoint config
|
|
@@ -1413,33 +1414,64 @@ var PathBuilderService = class {
|
|
|
1413
1414
|
switch (this.getEndpointType(handler)) {
|
|
1414
1415
|
case "stream": return this.buildStreamResponses(endpoint);
|
|
1415
1416
|
case "multipart":
|
|
1416
|
-
case "endpoint":
|
|
1417
|
-
|
|
1417
|
+
case "endpoint": return this.buildJsonResponses(config, handler);
|
|
1418
|
+
case "unknown": return this.buildUnknownResponses(config);
|
|
1419
|
+
default: return this.buildUnknownResponses(config);
|
|
1418
1420
|
}
|
|
1419
1421
|
}
|
|
1420
1422
|
/**
|
|
1423
|
+
* Builds error responses from errorSchema
|
|
1424
|
+
*
|
|
1425
|
+
* @param errorSchema - Optional record mapping status codes to Zod schemas
|
|
1426
|
+
* @returns ResponsesObject with error responses, or empty object if no errorSchema
|
|
1427
|
+
*/ buildErrorResponses(errorSchema) {
|
|
1428
|
+
if (!errorSchema) return {};
|
|
1429
|
+
const errorResponses = {};
|
|
1430
|
+
for (const [statusCode, schema] of Object.entries(errorSchema)) {
|
|
1431
|
+
const { schema: convertedSchema } = this.schemaConverter.convert(schema);
|
|
1432
|
+
errorResponses[statusCode] = {
|
|
1433
|
+
description: `Error response (${statusCode})`,
|
|
1434
|
+
content: { "application/json": { schema: convertedSchema } }
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
return errorResponses;
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1421
1440
|
* Builds responses for JSON endpoints
|
|
1422
1441
|
*/ buildJsonResponses(config, handler) {
|
|
1423
1442
|
const successCode = handler.successStatusCode?.toString() ?? "200";
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1443
|
+
const responses = {};
|
|
1444
|
+
if (!config.responseSchema) responses[successCode] = { description: "Successful response" };
|
|
1445
|
+
else {
|
|
1446
|
+
const { schema } = this.schemaConverter.convert(config.responseSchema);
|
|
1447
|
+
responses[successCode] = {
|
|
1448
|
+
description: "Successful response",
|
|
1449
|
+
content: { "application/json": { schema } }
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
Object.assign(responses, this.buildErrorResponses(config.errorSchema));
|
|
1453
|
+
return responses;
|
|
1430
1454
|
}
|
|
1431
1455
|
/**
|
|
1432
1456
|
* Builds responses for stream endpoints
|
|
1433
1457
|
*/ buildStreamResponses(endpoint) {
|
|
1434
|
-
const { openApiMetadata, handler } = endpoint;
|
|
1458
|
+
const { config, openApiMetadata, handler } = endpoint;
|
|
1435
1459
|
const successCode = handler.successStatusCode?.toString() ?? "200";
|
|
1436
1460
|
const contentType = openApiMetadata.stream?.contentType ?? "application/octet-stream";
|
|
1437
1461
|
const description = openApiMetadata.stream?.description ?? "Stream response";
|
|
1438
1462
|
const content = this.getStreamContent(contentType);
|
|
1439
|
-
|
|
1463
|
+
const responses = { [successCode]: {
|
|
1440
1464
|
description,
|
|
1441
1465
|
content
|
|
1442
1466
|
} };
|
|
1467
|
+
Object.assign(responses, this.buildErrorResponses(config.errorSchema));
|
|
1468
|
+
return responses;
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Builds responses for unknown endpoint types.
|
|
1472
|
+
* Unknown types have no success response but can have error responses.
|
|
1473
|
+
*/ buildUnknownResponses(config) {
|
|
1474
|
+
return this.buildErrorResponses(config.errorSchema);
|
|
1443
1475
|
}
|
|
1444
1476
|
/**
|
|
1445
1477
|
* Gets content object for different stream types
|
|
@@ -1822,4 +1854,4 @@ var OpenApiGeneratorService = class {
|
|
|
1822
1854
|
|
|
1823
1855
|
//#endregion
|
|
1824
1856
|
export { _MetadataExtractorService as a, ApiOperationToken as c, ApiSummaryToken as d, ApiTagToken as f, _EndpointScannerService as i, ApiSecurityToken as l, _PathBuilderService as n, ApiDeprecatedToken as o, _SchemaConverterService as r, ApiExcludeToken as s, _OpenApiGeneratorService as t, ApiStreamToken as u };
|
|
1825
|
-
//# sourceMappingURL=services-
|
|
1857
|
+
//# sourceMappingURL=services-B7UR1D1X.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"services-B7UR1D1X.mjs","names":["ApiTagToken","Symbol","for","ApiOperationToken","ApiSummaryToken","ApiDeprecatedToken","ApiSecurityToken","ApiExcludeToken","ApiStreamToken","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","EndpointAdapterToken","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","buildUnknownResponses","buildErrorResponses","errorSchema","errorResponses","statusCode","convertedSchema","successCode","successStatusCode","toString","responseSchema","assign","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/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 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 { BaseEndpointOptions, EndpointOptions } from '@navios/builder'\nimport type {\n ControllerMetadata,\n HandlerMetadata,\n ModuleMetadata,\n} from '@navios/core'\n\nimport {\n extractControllerMetadata,\n inject,\n Injectable,\n Logger,\n} 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: EndpointOptions | BaseEndpointOptions\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 (\n !moduleMetadata.controllers ||\n moduleMetadata.controllers.size === 0\n ) {\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(`Skipping excluded endpoint: ${handler.classMethod}`)\n continue\n }\n\n endpoints.push({\n module,\n controllerClass,\n controller: controllerMeta,\n handler,\n config: handler.config as EndpointOptions | BaseEndpointOptions,\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 {\n BaseEndpointOptions,\n EndpointOptions,\n ErrorSchemaRecord,\n} 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 as EndpointOptions),\n requestBody: this.buildRequestBody(config as EndpointOptions, 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' | 'unknown' {\n if (handler.adapterToken === EndpointAdapterToken) {\n return 'endpoint'\n }\n if (handler.adapterToken === MultipartAdapterToken) {\n return 'multipart'\n }\n if (handler.adapterToken === StreamAdapterToken) {\n return 'stream'\n }\n return 'unknown'\n }\n\n /**\n * Builds OpenAPI parameters from endpoint config\n */\n private buildParameters(config: EndpointOptions): 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: EndpointOptions,\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: EndpointOptions,\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: EndpointOptions,\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)\n .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 return this.buildJsonResponses(config as EndpointOptions, handler)\n case 'unknown':\n return this.buildUnknownResponses(config)\n default:\n return this.buildUnknownResponses(config)\n }\n }\n\n /**\n * Builds error responses from errorSchema\n *\n * @param errorSchema - Optional record mapping status codes to Zod schemas\n * @returns ResponsesObject with error responses, or empty object if no errorSchema\n */\n private buildErrorResponses(\n errorSchema?: ErrorSchemaRecord,\n ): ResponsesObject {\n if (!errorSchema) {\n return {}\n }\n\n const errorResponses: ResponsesObject = {}\n\n for (const [statusCode, schema] of Object.entries(errorSchema)) {\n const { schema: convertedSchema } = this.schemaConverter.convert(schema)\n errorResponses[statusCode] = {\n description: `Error response (${statusCode})`,\n content: {\n 'application/json': {\n schema: convertedSchema,\n },\n },\n }\n }\n\n return errorResponses\n }\n\n /**\n * Builds responses for JSON endpoints\n */\n private buildJsonResponses(\n config: EndpointOptions,\n handler: HandlerMetadata<any>,\n ): ResponsesObject {\n const successCode = handler.successStatusCode?.toString() ?? '200'\n const responses: ResponsesObject = {}\n\n // Build success response\n if (!config.responseSchema) {\n responses[successCode] = {\n description: 'Successful response',\n }\n } else {\n const { schema } = this.schemaConverter.convert(config.responseSchema)\n responses[successCode] = {\n description: 'Successful response',\n content: {\n 'application/json': {\n schema,\n },\n },\n }\n }\n\n // Add error responses from errorSchema\n Object.assign(responses, this.buildErrorResponses(config.errorSchema))\n\n return responses\n }\n\n /**\n * Builds responses for stream endpoints\n */\n private buildStreamResponses(endpoint: DiscoveredEndpoint): ResponsesObject {\n const { config, openApiMetadata, handler } = endpoint\n const successCode = handler.successStatusCode?.toString() ?? '200'\n\n const contentType =\n openApiMetadata.stream?.contentType ?? 'application/octet-stream'\n const description = openApiMetadata.stream?.description ?? 'Stream response'\n\n const content: ContentObject = this.getStreamContent(contentType)\n\n const responses: ResponsesObject = {\n [successCode]: {\n description,\n content,\n },\n }\n\n // Add error responses from errorSchema\n Object.assign(responses, this.buildErrorResponses(config.errorSchema))\n\n return responses\n }\n\n /**\n * Builds responses for unknown endpoint types.\n * Unknown types have no success response but can have error responses.\n */\n private buildUnknownResponses(\n config: EndpointOptions | BaseEndpointOptions,\n ): ResponsesObject {\n // Only include error responses, no success response\n return this.buildErrorResponses(config.errorSchema)\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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navios/openapi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-alpha.3",
|
|
4
4
|
"author": {
|
|
5
5
|
"name": "Oleksandr Hanzha",
|
|
6
6
|
"email": "alex@granted.name"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"@navios/core": "^0.
|
|
15
|
+
"@navios/core": "^1.0.0-alpha.3",
|
|
16
16
|
"zod": "^3.25.0 || ^4.0.0"
|
|
17
17
|
},
|
|
18
18
|
"typings": "./lib/index.d.mts",
|
|
@@ -41,13 +41,12 @@
|
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@navios/builder": "^0.
|
|
45
|
-
"@navios/core": "^0.
|
|
44
|
+
"@navios/builder": "^1.0.0-alpha.2",
|
|
45
|
+
"@navios/core": "^1.0.0-alpha.3",
|
|
46
46
|
"typescript": "^5.9.3",
|
|
47
47
|
"zod": "^4.2.1"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@navios/di": "^0.9.2",
|
|
51
50
|
"yaml": "^2.8.2",
|
|
52
51
|
"zod-openapi": "^5.4.5"
|
|
53
52
|
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import type { BaseEndpointOptions } from '@navios/builder'
|
|
2
|
+
import type {
|
|
3
|
+
ControllerMetadata,
|
|
4
|
+
HandlerMetadata,
|
|
5
|
+
ModuleMetadata,
|
|
6
|
+
} from '@navios/core'
|
|
7
|
+
|
|
8
|
+
import { Logger } from '@navios/core'
|
|
9
|
+
import { TestContainer } from '@navios/di/testing'
|
|
10
|
+
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
12
|
+
|
|
13
|
+
import { EndpointScannerService } from '../services/endpoint-scanner.service.mjs'
|
|
14
|
+
import { MetadataExtractorService } from '../services/metadata-extractor.service.mjs'
|
|
15
|
+
|
|
16
|
+
// Mock metadata extractor
|
|
17
|
+
const mockMetadataExtractor = {
|
|
18
|
+
extract: vi.fn().mockReturnValue({
|
|
19
|
+
tags: ['default'],
|
|
20
|
+
summary: '',
|
|
21
|
+
description: '',
|
|
22
|
+
operationId: '',
|
|
23
|
+
deprecated: false,
|
|
24
|
+
excluded: false,
|
|
25
|
+
security: [],
|
|
26
|
+
}),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Mock logger
|
|
30
|
+
const mockLogger = {
|
|
31
|
+
debug: vi.fn(),
|
|
32
|
+
warn: vi.fn(),
|
|
33
|
+
error: vi.fn(),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Mock extractControllerMetadata
|
|
37
|
+
vi.mock('@navios/core', async () => {
|
|
38
|
+
const actual = await vi.importActual('@navios/core')
|
|
39
|
+
return {
|
|
40
|
+
...actual,
|
|
41
|
+
extractControllerMetadata: vi.fn((controller) => controller.__metadata),
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const createHandlerMetadata = (
|
|
46
|
+
config: Partial<BaseEndpointOptions> | undefined,
|
|
47
|
+
classMethod = 'test',
|
|
48
|
+
): HandlerMetadata<any> => ({
|
|
49
|
+
classMethod,
|
|
50
|
+
url: config?.url ?? '',
|
|
51
|
+
successStatusCode: 200,
|
|
52
|
+
adapterToken: null,
|
|
53
|
+
headers: {},
|
|
54
|
+
httpMethod: config?.method ?? 'GET',
|
|
55
|
+
config: config as BaseEndpointOptions,
|
|
56
|
+
guards: new Set(),
|
|
57
|
+
customAttributes: new Map(),
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('EndpointScannerService', () => {
|
|
61
|
+
let container: TestContainer
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
container = new TestContainer()
|
|
65
|
+
// Bind required dependencies
|
|
66
|
+
container.bind(Logger).toValue(mockLogger as any)
|
|
67
|
+
container
|
|
68
|
+
.bind(MetadataExtractorService)
|
|
69
|
+
.toValue(mockMetadataExtractor as any)
|
|
70
|
+
vi.clearAllMocks()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
afterEach(async () => {
|
|
74
|
+
await container.dispose()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('scan', () => {
|
|
78
|
+
it('should discover endpoints from modules', async () => {
|
|
79
|
+
const scanner = await container.get(EndpointScannerService)
|
|
80
|
+
|
|
81
|
+
// Create mock handler metadata
|
|
82
|
+
const handler = createHandlerMetadata(
|
|
83
|
+
{
|
|
84
|
+
method: 'GET',
|
|
85
|
+
url: '/users',
|
|
86
|
+
},
|
|
87
|
+
'getUsers',
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
// Create mock controller metadata
|
|
91
|
+
const controllerMeta: ControllerMetadata = {
|
|
92
|
+
endpoints: new Set([handler]),
|
|
93
|
+
guards: new Set(),
|
|
94
|
+
customAttributes: new Map(),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Create mock controller class with attached metadata
|
|
98
|
+
class TestController {
|
|
99
|
+
static __metadata = controllerMeta
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Create mock module metadata
|
|
103
|
+
const moduleMetadata: ModuleMetadata = {
|
|
104
|
+
controllers: new Set([TestController as any]),
|
|
105
|
+
imports: new Set(),
|
|
106
|
+
guards: new Set(),
|
|
107
|
+
overrides: new Set(),
|
|
108
|
+
customAttributes: new Map(),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const modules = new Map([['TestModule', moduleMetadata]])
|
|
112
|
+
|
|
113
|
+
const endpoints = scanner.scan(modules)
|
|
114
|
+
|
|
115
|
+
expect(endpoints).toHaveLength(1)
|
|
116
|
+
expect(endpoints[0].handler).toBe(handler)
|
|
117
|
+
expect(endpoints[0].module).toBe(moduleMetadata)
|
|
118
|
+
expect(mockLogger.debug).toHaveBeenCalled()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should skip modules without controllers', async () => {
|
|
122
|
+
const scanner = await container.get(EndpointScannerService)
|
|
123
|
+
|
|
124
|
+
const moduleMetadata: ModuleMetadata = {
|
|
125
|
+
controllers: new Set(),
|
|
126
|
+
imports: new Set(),
|
|
127
|
+
guards: new Set(),
|
|
128
|
+
overrides: new Set(),
|
|
129
|
+
customAttributes: new Map(),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const modules = new Map([['EmptyModule', moduleMetadata]])
|
|
133
|
+
|
|
134
|
+
const endpoints = scanner.scan(modules)
|
|
135
|
+
|
|
136
|
+
expect(endpoints).toHaveLength(0)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('should skip modules with undefined controllers', async () => {
|
|
140
|
+
const scanner = await container.get(EndpointScannerService)
|
|
141
|
+
|
|
142
|
+
const moduleMetadata: ModuleMetadata = {
|
|
143
|
+
controllers: new Set(),
|
|
144
|
+
imports: new Set(),
|
|
145
|
+
guards: new Set(),
|
|
146
|
+
overrides: new Set(),
|
|
147
|
+
customAttributes: new Map(),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const modules = new Map([['NoControllerModule', moduleMetadata]])
|
|
151
|
+
|
|
152
|
+
const endpoints = scanner.scan(modules)
|
|
153
|
+
|
|
154
|
+
expect(endpoints).toHaveLength(0)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should skip endpoints without config', async () => {
|
|
158
|
+
const scanner = await container.get(EndpointScannerService)
|
|
159
|
+
|
|
160
|
+
// Handler without config
|
|
161
|
+
const handler = createHandlerMetadata(undefined, 'noConfigMethod')
|
|
162
|
+
|
|
163
|
+
const controllerMeta: ControllerMetadata = {
|
|
164
|
+
endpoints: new Set([handler]),
|
|
165
|
+
guards: new Set(),
|
|
166
|
+
customAttributes: new Map(),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
class TestController {
|
|
170
|
+
static __metadata = controllerMeta
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const moduleMetadata: ModuleMetadata = {
|
|
174
|
+
controllers: new Set([TestController as any]),
|
|
175
|
+
imports: new Set(),
|
|
176
|
+
guards: new Set(),
|
|
177
|
+
overrides: new Set(),
|
|
178
|
+
customAttributes: new Map(),
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const modules = new Map([['TestModule', moduleMetadata]])
|
|
182
|
+
|
|
183
|
+
const endpoints = scanner.scan(modules)
|
|
184
|
+
|
|
185
|
+
expect(endpoints).toHaveLength(0)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('should skip excluded endpoints', async () => {
|
|
189
|
+
const excludedExtractor = {
|
|
190
|
+
extract: vi.fn().mockReturnValue({
|
|
191
|
+
tags: [],
|
|
192
|
+
excluded: true, // Excluded
|
|
193
|
+
}),
|
|
194
|
+
}
|
|
195
|
+
await container.invalidate(mockMetadataExtractor as any)
|
|
196
|
+
container.bind(MetadataExtractorService).toValue(excludedExtractor as any)
|
|
197
|
+
|
|
198
|
+
const scanner = await container.get(EndpointScannerService)
|
|
199
|
+
|
|
200
|
+
const handler = createHandlerMetadata(
|
|
201
|
+
{
|
|
202
|
+
method: 'GET',
|
|
203
|
+
url: '/excluded',
|
|
204
|
+
},
|
|
205
|
+
'excludedMethod',
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
const controllerMeta: ControllerMetadata = {
|
|
209
|
+
endpoints: new Set([handler]),
|
|
210
|
+
guards: new Set(),
|
|
211
|
+
customAttributes: new Map(),
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
class TestController {
|
|
215
|
+
static __metadata = controllerMeta
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const moduleMetadata: ModuleMetadata = {
|
|
219
|
+
controllers: new Set([TestController as any]),
|
|
220
|
+
imports: new Set(),
|
|
221
|
+
guards: new Set(),
|
|
222
|
+
overrides: new Set(),
|
|
223
|
+
customAttributes: new Map(),
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const modules = new Map([['TestModule', moduleMetadata]])
|
|
227
|
+
|
|
228
|
+
const endpoints = scanner.scan(modules)
|
|
229
|
+
|
|
230
|
+
expect(endpoints).toHaveLength(0)
|
|
231
|
+
expect(mockLogger.debug).toHaveBeenCalledWith(
|
|
232
|
+
expect.stringContaining('Skipping excluded'),
|
|
233
|
+
)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('should scan multiple modules and controllers', async () => {
|
|
237
|
+
const scanner = await container.get(EndpointScannerService)
|
|
238
|
+
|
|
239
|
+
// Module 1, Controller 1
|
|
240
|
+
const handler1 = createHandlerMetadata(
|
|
241
|
+
{ method: 'GET', url: '/users' },
|
|
242
|
+
'getUsers',
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
const controllerMeta1: ControllerMetadata = {
|
|
246
|
+
endpoints: new Set([handler1]),
|
|
247
|
+
guards: new Set(),
|
|
248
|
+
customAttributes: new Map(),
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
class Controller1 {
|
|
252
|
+
static __metadata = controllerMeta1
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Module 1, Controller 2
|
|
256
|
+
const handler2 = createHandlerMetadata(
|
|
257
|
+
{ method: 'POST', url: '/posts' },
|
|
258
|
+
'createPost',
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
const controllerMeta2: ControllerMetadata = {
|
|
262
|
+
endpoints: new Set([handler2]),
|
|
263
|
+
guards: new Set(),
|
|
264
|
+
customAttributes: new Map(),
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
class Controller2 {
|
|
268
|
+
static __metadata = controllerMeta2
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const module1: ModuleMetadata = {
|
|
272
|
+
controllers: new Set([Controller1 as any, Controller2 as any]),
|
|
273
|
+
imports: new Set(),
|
|
274
|
+
guards: new Set(),
|
|
275
|
+
overrides: new Set(),
|
|
276
|
+
customAttributes: new Map(),
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Module 2
|
|
280
|
+
const handler3 = createHandlerMetadata(
|
|
281
|
+
{ method: 'GET', url: '/orders' },
|
|
282
|
+
'getOrders',
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
const controllerMeta3: ControllerMetadata = {
|
|
286
|
+
endpoints: new Set([handler3]),
|
|
287
|
+
guards: new Set(),
|
|
288
|
+
customAttributes: new Map(),
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
class Controller3 {
|
|
292
|
+
static __metadata = controllerMeta3
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const module2: ModuleMetadata = {
|
|
296
|
+
controllers: new Set([Controller3 as any]),
|
|
297
|
+
imports: new Set(),
|
|
298
|
+
guards: new Set(),
|
|
299
|
+
overrides: new Set(),
|
|
300
|
+
customAttributes: new Map(),
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const modules = new Map([
|
|
304
|
+
['Module1', module1],
|
|
305
|
+
['Module2', module2],
|
|
306
|
+
])
|
|
307
|
+
|
|
308
|
+
const endpoints = scanner.scan(modules)
|
|
309
|
+
|
|
310
|
+
expect(endpoints).toHaveLength(3)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('should extract openApiMetadata for each endpoint', async () => {
|
|
314
|
+
const customMetadata = {
|
|
315
|
+
tags: ['users', 'api'],
|
|
316
|
+
summary: 'Get all users',
|
|
317
|
+
description: 'Returns a list of users',
|
|
318
|
+
operationId: 'getUsers',
|
|
319
|
+
deprecated: false,
|
|
320
|
+
excluded: false,
|
|
321
|
+
security: [{ bearer: [] }],
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const customExtractor = {
|
|
325
|
+
extract: vi.fn().mockReturnValue(customMetadata),
|
|
326
|
+
}
|
|
327
|
+
await container.invalidate(mockMetadataExtractor as any)
|
|
328
|
+
container.bind(MetadataExtractorService).toValue(customExtractor as any)
|
|
329
|
+
|
|
330
|
+
const scanner = await container.get(EndpointScannerService)
|
|
331
|
+
|
|
332
|
+
const handler = createHandlerMetadata(
|
|
333
|
+
{ method: 'GET', url: '/users' },
|
|
334
|
+
'getUsers',
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
const controllerMeta: ControllerMetadata = {
|
|
338
|
+
endpoints: new Set([handler]),
|
|
339
|
+
guards: new Set(),
|
|
340
|
+
customAttributes: new Map(),
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
class TestController {
|
|
344
|
+
static __metadata = controllerMeta
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const moduleMetadata: ModuleMetadata = {
|
|
348
|
+
controllers: new Set([TestController as any]),
|
|
349
|
+
imports: new Set(),
|
|
350
|
+
guards: new Set(),
|
|
351
|
+
overrides: new Set(),
|
|
352
|
+
customAttributes: new Map(),
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const modules = new Map([['TestModule', moduleMetadata]])
|
|
356
|
+
|
|
357
|
+
const endpoints = scanner.scan(modules)
|
|
358
|
+
|
|
359
|
+
expect(endpoints[0].openApiMetadata).toEqual(customMetadata)
|
|
360
|
+
})
|
|
361
|
+
})
|
|
362
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { HandlerMetadata } from '@navios/core'
|
|
2
2
|
|
|
3
3
|
import { Controller, extractControllerMetadata } from '@navios/core'
|
|
4
|
-
import { TestContainer } from '@navios/
|
|
4
|
+
import { TestContainer } from '@navios/core/testing'
|
|
5
5
|
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
7
7
|
|