cr
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user