This commit is contained in:
2026-04-16 17:21:48 +03:00
parent c8fa4c442d
commit c8e7e4e927
116 changed files with 3720 additions and 4197 deletions
+88 -88
View File
@@ -1,20 +1,20 @@
import 'reflect-metadata';
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import * as path from 'node:path';
import ts from 'typescript';
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from '../app.module';
import "reflect-metadata";
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import * as path from "node:path";
import ts from "typescript";
import { NestFactory } from "@nestjs/core";
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { AppModule } from "../app.module";
type HttpMethod =
| 'get'
| 'post'
| 'put'
| 'patch'
| 'delete'
| 'options'
| 'head'
| 'all';
| "get"
| "post"
| "put"
| "patch"
| "delete"
| "options"
| "head"
| "all";
interface TsDecoratorMeta {
name: string;
@@ -42,14 +42,14 @@ interface TsMethodMeta {
}
const HTTP_DECORATOR_TO_METHOD: Record<string, HttpMethod> = {
Get: 'get',
Post: 'post',
Put: 'put',
Patch: 'patch',
Delete: 'delete',
Options: 'options',
Head: 'head',
All: 'all',
Get: "get",
Post: "post",
Put: "put",
Patch: "patch",
Delete: "delete",
Options: "options",
Head: "head",
All: "all",
};
function getDecorators(node: ts.Node): readonly ts.Decorator[] {
@@ -105,7 +105,7 @@ function collectControllerFiles(dirPath: string): string[] {
continue;
}
if (entry.isFile() && entry.name.endsWith('.controller.ts')) {
if (entry.isFile() && entry.name.endsWith(".controller.ts")) {
files.push(absolutePath);
}
}
@@ -115,9 +115,9 @@ function collectControllerFiles(dirPath: string): string[] {
function normalizeRoutePart(value: string | undefined): string {
if (!value || value === "''" || value === '""') {
return '';
return "";
}
return value.trim().replace(/^\/+|\/+$/g, '');
return value.trim().replace(/^\/+|\/+$/g, "");
}
function buildSwaggerPath(
@@ -131,18 +131,18 @@ function buildSwaggerPath(
normalizeRoutePart(routePath),
].filter(Boolean);
return `/${parts.join('/')}`;
return `/${parts.join("/")}`;
}
function collectTsEndpointMetadata(
projectRoot: string,
): Map<string, TsMethodMeta> {
const modulesDir = path.join(projectRoot, 'src', 'modules');
const modulesDir = path.join(projectRoot, "src", "modules");
const controllerFiles = collectControllerFiles(modulesDir);
const metadataByOperationId = new Map<string, TsMethodMeta>();
for (const filePath of controllerFiles) {
const sourceText = readFileSync(filePath, 'utf8');
const sourceText = readFileSync(filePath, "utf8");
const sourceFile = ts.createSourceFile(
filePath,
sourceText,
@@ -164,7 +164,7 @@ function collectTsEndpointMetadata(
);
const controllerDecorator = classDecorators.find(
(decorator) => decorator.name === 'Controller',
(decorator) => decorator.name === "Controller",
);
if (!controllerDecorator) {
return;
@@ -221,7 +221,7 @@ function collectTsEndpointMetadata(
routePath,
returnType,
hasPublicDecorator: methodDecorators.some(
(decorator) => decorator.name === 'Public',
(decorator) => decorator.name === "Public",
),
methodDecorators: methodDecorators.map((decorator) => decorator.name),
params,
@@ -235,10 +235,10 @@ function collectTsEndpointMetadata(
}
function refName(ref?: string): string | null {
if (!ref || typeof ref !== 'string') {
if (!ref || typeof ref !== "string") {
return null;
}
const parts = ref.split('/');
const parts = ref.split("/");
return parts[parts.length - 1] ?? null;
}
@@ -246,13 +246,13 @@ function collectSchemaRefs(
value: unknown,
refs = new Set<string>(),
): Set<string> {
if (!value || typeof value !== 'object') {
if (!value || typeof value !== "object") {
return refs;
}
const recordValue = value as Record<string, unknown>;
const maybeRef = recordValue.$ref;
if (typeof maybeRef === 'string') {
if (typeof maybeRef === "string") {
const name = refName(maybeRef);
if (name) {
refs.add(name);
@@ -267,22 +267,22 @@ function collectSchemaRefs(
}
function schemaTypeSummary(schema: unknown): string {
if (!schema || typeof schema !== 'object') {
return 'unknown';
if (!schema || typeof schema !== "object") {
return "unknown";
}
const schemaObj = schema as Record<string, unknown>;
if (typeof schemaObj.$ref === 'string') {
return refName(schemaObj.$ref) ?? 'unknown';
if (typeof schemaObj.$ref === "string") {
return refName(schemaObj.$ref) ?? "unknown";
}
const type = typeof schemaObj.type === 'string' ? schemaObj.type : 'object';
if (type === 'array') {
const type = typeof schemaObj.type === "string" ? schemaObj.type : "object";
if (type === "array") {
return `array<${schemaTypeSummary(schemaObj.items)}>`;
}
if (Array.isArray(schemaObj.enum) && schemaObj.enum.length > 0) {
return `${type}(${schemaObj.enum.join(' | ')})`;
return `${type}(${schemaObj.enum.join(" | ")})`;
}
return type;
@@ -295,35 +295,35 @@ function normalizeParameters(parameters: unknown[] = []) {
.map((parameter) => {
const schema = (parameter.schema ?? {}) as Record<string, unknown>;
return {
name: typeof parameter.name === 'string' ? parameter.name : '',
in: typeof parameter.in === 'string' ? parameter.in : '',
name: typeof parameter.name === "string" ? parameter.name : "",
in: typeof parameter.in === "string" ? parameter.in : "",
required: Boolean(parameter.required),
description:
typeof parameter.description === 'string'
typeof parameter.description === "string"
? parameter.description
: null,
type: schemaTypeSummary(schema),
enum: Array.isArray(schema.enum) ? schema.enum : [],
default: schema.default ?? null,
format: typeof schema.format === 'string' ? schema.format : null,
format: typeof schema.format === "string" ? schema.format : null,
};
});
return {
path: parsed.filter((item) => item.in === 'path'),
query: parsed.filter((item) => item.in === 'query'),
header: parsed.filter((item) => item.in === 'header'),
cookie: parsed.filter((item) => item.in === 'cookie'),
path: parsed.filter((item) => item.in === "path"),
query: parsed.filter((item) => item.in === "query"),
header: parsed.filter((item) => item.in === "header"),
cookie: parsed.filter((item) => item.in === "cookie"),
};
}
function normalizeRequestBody(requestBody: unknown) {
if (!requestBody || typeof requestBody !== 'object') {
if (!requestBody || typeof requestBody !== "object") {
return null;
}
const requestBodyObj = requestBody as Record<string, unknown>;
if (typeof requestBodyObj.$ref === 'string') {
if (typeof requestBodyObj.$ref === "string") {
return {
required: false,
contentTypes: [],
@@ -378,9 +378,9 @@ function normalizeResponses(responses: Record<string, unknown>) {
return {
status: Number(statusCode),
description:
typeof responseObj.description === 'string'
typeof responseObj.description === "string"
? responseObj.description
: '',
: "",
contentTypes,
schemaTypes,
schemaRefs: [...refs].sort(),
@@ -392,25 +392,25 @@ function normalizeResponses(responses: Record<string, unknown>) {
async function run() {
const projectRoot = process.cwd();
const outputDir = path.join(projectRoot, 'mds');
const outputDir = path.join(projectRoot, "mds");
const outputFile = path.join(
outputDir,
'backend_endpoints_swagger_summary.json',
"backend_endpoints_swagger_summary.json",
);
// Predictions module is conditionally loaded with REDIS_ENABLED in AppModule.
// Force-enable here to include all backend endpoints in one Swagger export.
process.env.REDIS_ENABLED = 'true';
process.env.REDIS_ENABLED = "true";
const tsMetadata = collectTsEndpointMetadata(projectRoot);
const app = await NestFactory.create(AppModule, { logger: false });
app.setGlobalPrefix('api');
app.setGlobalPrefix("api");
const swaggerConfig = new DocumentBuilder()
.setTitle('Suggest Bet Backend API')
.setDescription('Auto-generated endpoint summary from Swagger document')
.setVersion('1.0')
.setTitle("Suggest Bet Backend API")
.setDescription("Auto-generated endpoint summary from Swagger document")
.setVersion("1.0")
.addBearerAuth()
.build();
@@ -419,7 +419,7 @@ async function run() {
const endpoints: Array<Record<string, unknown>> = [];
const seenOperationIds = new Set<string>();
const globalPrefix = 'api';
const globalPrefix = "api";
const sortedPaths = Object.keys(paths).sort((a, b) => a.localeCompare(b));
for (const endpointPath of sortedPaths) {
@@ -427,7 +427,7 @@ async function run() {
const methods = Object.keys(pathItem)
.filter((method) =>
['get', 'post', 'put', 'patch', 'delete', 'options', 'head'].includes(
["get", "post", "put", "patch", "delete", "options", "head"].includes(
method,
),
)
@@ -436,7 +436,7 @@ async function run() {
for (const method of methods) {
const operation = pathItem[method] as Record<string, unknown>;
const operationId =
typeof operation.operationId === 'string' ? operation.operationId : '';
typeof operation.operationId === "string" ? operation.operationId : "";
if (operationId) {
seenOperationIds.add(operationId);
@@ -464,13 +464,13 @@ async function run() {
const tsBodyParams =
tsMeta?.params
.filter((param) =>
param.decorators.some((decorator) => decorator.name === 'Body'),
param.decorators.some((decorator) => decorator.name === "Body"),
)
.map((param) => ({
name: param.name,
type: param.type,
bodyKey:
param.decorators.find((decorator) => decorator.name === 'Body')
param.decorators.find((decorator) => decorator.name === "Body")
?.firstArg ?? null,
})) ?? [];
@@ -482,9 +482,9 @@ async function run() {
tag: tags[0] ?? null,
tags,
summary:
typeof operation.summary === 'string' ? operation.summary : null,
typeof operation.summary === "string" ? operation.summary : null,
description:
typeof operation.description === 'string'
typeof operation.description === "string"
? operation.description
: null,
auth: {
@@ -527,10 +527,10 @@ async function run() {
tsMeta.controllerRoute,
tsMeta.routePath,
),
tag: tsMeta.controller.replace(/Controller$/, ''),
tags: [tsMeta.controller.replace(/Controller$/, '')],
tag: tsMeta.controller.replace(/Controller$/, ""),
tags: [tsMeta.controller.replace(/Controller$/, "")],
summary: null,
description: 'Not present in generated Swagger document',
description: "Not present in generated Swagger document",
auth: {
swaggerSecurityRequired: null,
swaggerSecuritySchemes: [],
@@ -546,13 +546,13 @@ async function run() {
body: null,
tsBodyParams: tsMeta.params
.filter((param) =>
param.decorators.some((decorator) => decorator.name === 'Body'),
param.decorators.some((decorator) => decorator.name === "Body"),
)
.map((param) => ({
name: param.name,
type: param.type,
bodyKey:
param.decorators.find((decorator) => decorator.name === 'Body')
param.decorators.find((decorator) => decorator.name === "Body")
?.firstArg ?? null,
})),
},
@@ -569,19 +569,19 @@ async function run() {
}
endpoints.sort((a, b) => {
const pathA = typeof a.path === 'string' ? a.path : '';
const pathB = typeof b.path === 'string' ? b.path : '';
const pathA = typeof a.path === "string" ? a.path : "";
const pathB = typeof b.path === "string" ? b.path : "";
if (pathA !== pathB) {
return pathA.localeCompare(pathB);
}
return (typeof a.method === 'string' ? a.method : '').localeCompare(
typeof b.method === 'string' ? b.method : '',
return (typeof a.method === "string" ? a.method : "").localeCompare(
typeof b.method === "string" ? b.method : "",
);
});
const tagStats = new Map<string, number>();
for (const endpoint of endpoints) {
const tag = typeof endpoint.tag === 'string' ? endpoint.tag : 'Unknown';
const tag = typeof endpoint.tag === "string" ? endpoint.tag : "Unknown";
tagStats.set(tag, (tagStats.get(tag) ?? 0) + 1);
}
@@ -591,7 +591,7 @@ async function run() {
.body as Record<string, unknown> | null;
if (requestBody && Array.isArray(requestBody.schemaRefs)) {
for (const schemaName of requestBody.schemaRefs) {
if (typeof schemaName === 'string') {
if (typeof schemaName === "string") {
referencedSchemas.add(schemaName);
}
}
@@ -604,7 +604,7 @@ async function run() {
continue;
}
for (const schemaName of status.schemaRefs) {
if (typeof schemaName === 'string') {
if (typeof schemaName === "string") {
referencedSchemas.add(schemaName);
}
}
@@ -626,16 +626,16 @@ async function run() {
const summary = {
generatedAt: new Date().toISOString(),
generatedBy: 'src/scripts/export-swagger-endpoints-summary.ts',
project: 'Suggest-Bet-BE',
generatedBy: "src/scripts/export-swagger-endpoints-summary.ts",
project: "Suggest-Bet-BE",
swagger: {
docsPath: '/api/docs',
globalPrefix: '/api',
docsPath: "/api/docs",
globalPrefix: "/api",
endpointCountInSwagger: endpoints.filter((item) => item.inSwagger).length,
endpointCountTotal: endpoints.length,
warnings: [
'Swagger output reflects loaded modules for current environment.',
'This export forces REDIS_ENABLED=true to include conditional Prediction endpoints.',
"Swagger output reflects loaded modules for current environment.",
"This export forces REDIS_ENABLED=true to include conditional Prediction endpoints.",
],
},
stats: {
@@ -668,7 +668,7 @@ async function run() {
};
mkdirSync(outputDir, { recursive: true });
writeFileSync(outputFile, JSON.stringify(summary, null, 2), 'utf8');
writeFileSync(outputFile, JSON.stringify(summary, null, 2), "utf8");
await app.close();
@@ -680,7 +680,7 @@ async function run() {
}
void run().catch((error: unknown) => {
console.error('❌ Failed to export Swagger endpoint summary');
console.error("❌ Failed to export Swagger endpoint summary");
console.error(error);
process.exit(1);