This commit is contained in:
+687
@@ -0,0 +1,687 @@
|
||||
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';
|
||||
|
||||
interface TsDecoratorMeta {
|
||||
name: string;
|
||||
firstArg?: string;
|
||||
}
|
||||
|
||||
interface TsParameterMeta {
|
||||
name: string;
|
||||
type: string | null;
|
||||
decorators: TsDecoratorMeta[];
|
||||
}
|
||||
|
||||
interface TsMethodMeta {
|
||||
operationId: string;
|
||||
controller: string;
|
||||
controllerRoute: string;
|
||||
methodName: string;
|
||||
httpMethod: HttpMethod;
|
||||
routePath: string;
|
||||
returnType: string | null;
|
||||
hasPublicDecorator: boolean;
|
||||
methodDecorators: string[];
|
||||
params: TsParameterMeta[];
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
const HTTP_DECORATOR_TO_METHOD: Record<string, HttpMethod> = {
|
||||
Get: 'get',
|
||||
Post: 'post',
|
||||
Put: 'put',
|
||||
Patch: 'patch',
|
||||
Delete: 'delete',
|
||||
Options: 'options',
|
||||
Head: 'head',
|
||||
All: 'all',
|
||||
};
|
||||
|
||||
function getDecorators(node: ts.Node): readonly ts.Decorator[] {
|
||||
return ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : [];
|
||||
}
|
||||
|
||||
function parseDecorator(
|
||||
decorator: ts.Decorator,
|
||||
sourceFile: ts.SourceFile,
|
||||
): TsDecoratorMeta | null {
|
||||
const expression = decorator.expression;
|
||||
|
||||
if (ts.isIdentifier(expression)) {
|
||||
return { name: expression.text };
|
||||
}
|
||||
|
||||
if (ts.isCallExpression(expression)) {
|
||||
const called = expression.expression;
|
||||
if (!ts.isIdentifier(called)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstArg = expression.arguments[0];
|
||||
let firstArgText: string | undefined;
|
||||
if (firstArg) {
|
||||
if (
|
||||
ts.isStringLiteral(firstArg) ||
|
||||
ts.isNoSubstitutionTemplateLiteral(firstArg)
|
||||
) {
|
||||
firstArgText = firstArg.text;
|
||||
} else {
|
||||
firstArgText = firstArg.getText(sourceFile);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: called.text,
|
||||
firstArg: firstArgText,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectControllerFiles(dirPath: string): string[] {
|
||||
const files: string[] = [];
|
||||
const entries = readdirSync(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const absolutePath = path.join(dirPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...collectControllerFiles(absolutePath));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isFile() && entry.name.endsWith('.controller.ts')) {
|
||||
files.push(absolutePath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function normalizeRoutePart(value: string | undefined): string {
|
||||
if (!value || value === "''" || value === '""') {
|
||||
return '';
|
||||
}
|
||||
return value.trim().replace(/^\/+|\/+$/g, '');
|
||||
}
|
||||
|
||||
function buildSwaggerPath(
|
||||
globalPrefix: string,
|
||||
controllerRoute: string,
|
||||
routePath: string,
|
||||
): string {
|
||||
const parts = [
|
||||
normalizeRoutePart(globalPrefix),
|
||||
normalizeRoutePart(controllerRoute),
|
||||
normalizeRoutePart(routePath),
|
||||
].filter(Boolean);
|
||||
|
||||
return `/${parts.join('/')}`;
|
||||
}
|
||||
|
||||
function collectTsEndpointMetadata(
|
||||
projectRoot: string,
|
||||
): Map<string, TsMethodMeta> {
|
||||
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 sourceFile = ts.createSourceFile(
|
||||
filePath,
|
||||
sourceText,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TS,
|
||||
);
|
||||
|
||||
ts.forEachChild(sourceFile, (node) => {
|
||||
if (!ts.isClassDeclaration(node) || !node.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const className = node.name.text;
|
||||
const classDecorators = getDecorators(node)
|
||||
.map((decorator) => parseDecorator(decorator, sourceFile))
|
||||
.filter((decorator): decorator is TsDecoratorMeta =>
|
||||
Boolean(decorator),
|
||||
);
|
||||
|
||||
const controllerDecorator = classDecorators.find(
|
||||
(decorator) => decorator.name === 'Controller',
|
||||
);
|
||||
if (!controllerDecorator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const controllerRoute = normalizeRoutePart(controllerDecorator.firstArg);
|
||||
|
||||
for (const member of node.members) {
|
||||
if (!ts.isMethodDeclaration(member) || !member.name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const methodDecorators = getDecorators(member)
|
||||
.map((decorator) => parseDecorator(decorator, sourceFile))
|
||||
.filter((decorator): decorator is TsDecoratorMeta =>
|
||||
Boolean(decorator),
|
||||
);
|
||||
|
||||
const httpDecorator = methodDecorators.find(
|
||||
(decorator) => decorator.name in HTTP_DECORATOR_TO_METHOD,
|
||||
);
|
||||
if (!httpDecorator) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const methodName = member.name.getText(sourceFile);
|
||||
const httpMethod = HTTP_DECORATOR_TO_METHOD[httpDecorator.name];
|
||||
const routePath = normalizeRoutePart(httpDecorator.firstArg);
|
||||
const returnType = member.type
|
||||
? member.type.getText(sourceFile).trim()
|
||||
: null;
|
||||
|
||||
const params: TsParameterMeta[] = member.parameters.map((param) => {
|
||||
const paramDecorators = getDecorators(param)
|
||||
.map((decorator) => parseDecorator(decorator, sourceFile))
|
||||
.filter((decorator): decorator is TsDecoratorMeta =>
|
||||
Boolean(decorator),
|
||||
);
|
||||
|
||||
return {
|
||||
name: param.name.getText(sourceFile),
|
||||
type: param.type ? param.type.getText(sourceFile).trim() : null,
|
||||
decorators: paramDecorators,
|
||||
};
|
||||
});
|
||||
|
||||
const operationId = `${className}_${methodName}`;
|
||||
metadataByOperationId.set(operationId, {
|
||||
operationId,
|
||||
controller: className,
|
||||
controllerRoute,
|
||||
methodName,
|
||||
httpMethod,
|
||||
routePath,
|
||||
returnType,
|
||||
hasPublicDecorator: methodDecorators.some(
|
||||
(decorator) => decorator.name === 'Public',
|
||||
),
|
||||
methodDecorators: methodDecorators.map((decorator) => decorator.name),
|
||||
params,
|
||||
filePath,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return metadataByOperationId;
|
||||
}
|
||||
|
||||
function refName(ref?: string): string | null {
|
||||
if (!ref || typeof ref !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const parts = ref.split('/');
|
||||
return parts[parts.length - 1] ?? null;
|
||||
}
|
||||
|
||||
function collectSchemaRefs(
|
||||
value: unknown,
|
||||
refs = new Set<string>(),
|
||||
): Set<string> {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return refs;
|
||||
}
|
||||
|
||||
const recordValue = value as Record<string, unknown>;
|
||||
const maybeRef = recordValue.$ref;
|
||||
if (typeof maybeRef === 'string') {
|
||||
const name = refName(maybeRef);
|
||||
if (name) {
|
||||
refs.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
for (const nested of Object.values(recordValue)) {
|
||||
collectSchemaRefs(nested, refs);
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
function schemaTypeSummary(schema: unknown): string {
|
||||
if (!schema || typeof schema !== 'object') {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
const schemaObj = schema as Record<string, unknown>;
|
||||
if (typeof schemaObj.$ref === 'string') {
|
||||
return refName(schemaObj.$ref) ?? 'unknown';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function normalizeParameters(parameters: unknown[] = []) {
|
||||
const parsed = parameters
|
||||
.map((parameter) => parameter as Record<string, unknown>)
|
||||
.filter(Boolean)
|
||||
.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 : '',
|
||||
required: Boolean(parameter.required),
|
||||
description:
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
||||
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'),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRequestBody(requestBody: unknown) {
|
||||
if (!requestBody || typeof requestBody !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const requestBodyObj = requestBody as Record<string, unknown>;
|
||||
if (typeof requestBodyObj.$ref === 'string') {
|
||||
return {
|
||||
required: false,
|
||||
contentTypes: [],
|
||||
schemaTypes: [],
|
||||
schemaRefs: [refName(requestBodyObj.$ref)].filter(Boolean),
|
||||
raw: requestBodyObj,
|
||||
};
|
||||
}
|
||||
|
||||
const content = (requestBodyObj.content ?? {}) as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>;
|
||||
const contentTypes = Object.keys(content);
|
||||
const schemaTypes: string[] = [];
|
||||
const refs = new Set<string>();
|
||||
|
||||
for (const mediaType of Object.values(content)) {
|
||||
const schema = mediaType.schema;
|
||||
schemaTypes.push(schemaTypeSummary(schema));
|
||||
collectSchemaRefs(schema, refs);
|
||||
}
|
||||
|
||||
return {
|
||||
required: Boolean(requestBodyObj.required),
|
||||
contentTypes,
|
||||
schemaTypes,
|
||||
schemaRefs: [...refs].sort(),
|
||||
raw: requestBodyObj,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeResponses(responses: Record<string, unknown>) {
|
||||
return Object.entries(responses)
|
||||
.sort((a, b) => Number(a[0]) - Number(b[0]))
|
||||
.map(([statusCode, response]) => {
|
||||
const responseObj = response as Record<string, unknown>;
|
||||
const content = (responseObj.content ?? {}) as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>;
|
||||
const contentTypes = Object.keys(content);
|
||||
const refs = new Set<string>();
|
||||
const schemaTypes: string[] = [];
|
||||
|
||||
for (const mediaType of Object.values(content)) {
|
||||
const schema = mediaType.schema;
|
||||
schemaTypes.push(schemaTypeSummary(schema));
|
||||
collectSchemaRefs(schema, refs);
|
||||
}
|
||||
|
||||
return {
|
||||
status: Number(statusCode),
|
||||
description:
|
||||
typeof responseObj.description === 'string'
|
||||
? responseObj.description
|
||||
: '',
|
||||
contentTypes,
|
||||
schemaTypes,
|
||||
schemaRefs: [...refs].sort(),
|
||||
hasSchema: contentTypes.length > 0,
|
||||
raw: responseObj,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const projectRoot = process.cwd();
|
||||
const outputDir = path.join(projectRoot, 'mds');
|
||||
const outputFile = path.join(
|
||||
outputDir,
|
||||
'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';
|
||||
|
||||
const tsMetadata = collectTsEndpointMetadata(projectRoot);
|
||||
|
||||
const app = await NestFactory.create(AppModule, { logger: false });
|
||||
app.setGlobalPrefix('api');
|
||||
|
||||
const swaggerConfig = new DocumentBuilder()
|
||||
.setTitle('Suggest Bet Backend API')
|
||||
.setDescription('Auto-generated endpoint summary from Swagger document')
|
||||
.setVersion('1.0')
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
||||
const paths = document.paths ?? {};
|
||||
|
||||
const endpoints: Array<Record<string, unknown>> = [];
|
||||
const seenOperationIds = new Set<string>();
|
||||
const globalPrefix = 'api';
|
||||
|
||||
const sortedPaths = Object.keys(paths).sort((a, b) => a.localeCompare(b));
|
||||
for (const endpointPath of sortedPaths) {
|
||||
const pathItem = paths[endpointPath] as Record<string, unknown>;
|
||||
|
||||
const methods = Object.keys(pathItem)
|
||||
.filter((method) =>
|
||||
['get', 'post', 'put', 'patch', 'delete', 'options', 'head'].includes(
|
||||
method,
|
||||
),
|
||||
)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
for (const method of methods) {
|
||||
const operation = pathItem[method] as Record<string, unknown>;
|
||||
const operationId =
|
||||
typeof operation.operationId === 'string' ? operation.operationId : '';
|
||||
|
||||
if (operationId) {
|
||||
seenOperationIds.add(operationId);
|
||||
}
|
||||
|
||||
const tsMeta = operationId ? tsMetadata.get(operationId) : undefined;
|
||||
const tags = Array.isArray(operation.tags)
|
||||
? operation.tags.map((tag) => String(tag))
|
||||
: [];
|
||||
|
||||
const parameters = normalizeParameters(
|
||||
Array.isArray(operation.parameters) ? operation.parameters : [],
|
||||
);
|
||||
const requestBody = normalizeRequestBody(operation.requestBody);
|
||||
const responses = normalizeResponses(
|
||||
(operation.responses ?? {}) as Record<string, unknown>,
|
||||
);
|
||||
const security = Array.isArray(operation.security)
|
||||
? operation.security
|
||||
: [];
|
||||
const securitySchemes = security.flatMap((rule) =>
|
||||
Object.keys((rule ?? {}) as Record<string, unknown>),
|
||||
);
|
||||
|
||||
const tsBodyParams =
|
||||
tsMeta?.params
|
||||
.filter((param) =>
|
||||
param.decorators.some((decorator) => decorator.name === 'Body'),
|
||||
)
|
||||
.map((param) => ({
|
||||
name: param.name,
|
||||
type: param.type,
|
||||
bodyKey:
|
||||
param.decorators.find((decorator) => decorator.name === 'Body')
|
||||
?.firstArg ?? null,
|
||||
})) ?? [];
|
||||
|
||||
endpoints.push({
|
||||
inSwagger: true,
|
||||
operationId,
|
||||
method: method.toUpperCase(),
|
||||
path: endpointPath,
|
||||
tag: tags[0] ?? null,
|
||||
tags,
|
||||
summary:
|
||||
typeof operation.summary === 'string' ? operation.summary : null,
|
||||
description:
|
||||
typeof operation.description === 'string'
|
||||
? operation.description
|
||||
: null,
|
||||
auth: {
|
||||
swaggerSecurityRequired: security.length > 0,
|
||||
swaggerSecuritySchemes: [...new Set(securitySchemes)].sort(),
|
||||
hasPublicDecorator: tsMeta?.hasPublicDecorator ?? false,
|
||||
},
|
||||
request: {
|
||||
parameters,
|
||||
body: requestBody,
|
||||
tsBodyParams,
|
||||
},
|
||||
response: {
|
||||
tsReturnType: tsMeta?.returnType ?? null,
|
||||
statuses: responses,
|
||||
},
|
||||
source: tsMeta
|
||||
? {
|
||||
controller: tsMeta.controller,
|
||||
methodName: tsMeta.methodName,
|
||||
filePath: path.relative(projectRoot, tsMeta.filePath),
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add controller methods that are not present in Swagger document.
|
||||
for (const [operationId, tsMeta] of tsMetadata.entries()) {
|
||||
if (seenOperationIds.has(operationId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
endpoints.push({
|
||||
inSwagger: false,
|
||||
operationId,
|
||||
method: tsMeta.httpMethod.toUpperCase(),
|
||||
path: buildSwaggerPath(
|
||||
globalPrefix,
|
||||
tsMeta.controllerRoute,
|
||||
tsMeta.routePath,
|
||||
),
|
||||
tag: tsMeta.controller.replace(/Controller$/, ''),
|
||||
tags: [tsMeta.controller.replace(/Controller$/, '')],
|
||||
summary: null,
|
||||
description: 'Not present in generated Swagger document',
|
||||
auth: {
|
||||
swaggerSecurityRequired: null,
|
||||
swaggerSecuritySchemes: [],
|
||||
hasPublicDecorator: tsMeta.hasPublicDecorator,
|
||||
},
|
||||
request: {
|
||||
parameters: {
|
||||
path: [],
|
||||
query: [],
|
||||
header: [],
|
||||
cookie: [],
|
||||
},
|
||||
body: null,
|
||||
tsBodyParams: tsMeta.params
|
||||
.filter((param) =>
|
||||
param.decorators.some((decorator) => decorator.name === 'Body'),
|
||||
)
|
||||
.map((param) => ({
|
||||
name: param.name,
|
||||
type: param.type,
|
||||
bodyKey:
|
||||
param.decorators.find((decorator) => decorator.name === 'Body')
|
||||
?.firstArg ?? null,
|
||||
})),
|
||||
},
|
||||
response: {
|
||||
tsReturnType: tsMeta.returnType,
|
||||
statuses: [],
|
||||
},
|
||||
source: {
|
||||
controller: tsMeta.controller,
|
||||
methodName: tsMeta.methodName,
|
||||
filePath: path.relative(projectRoot, tsMeta.filePath),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
endpoints.sort((a, b) => {
|
||||
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 : '',
|
||||
);
|
||||
});
|
||||
|
||||
const tagStats = new Map<string, number>();
|
||||
for (const endpoint of endpoints) {
|
||||
const tag = typeof endpoint.tag === 'string' ? endpoint.tag : 'Unknown';
|
||||
tagStats.set(tag, (tagStats.get(tag) ?? 0) + 1);
|
||||
}
|
||||
|
||||
const referencedSchemas = new Set<string>();
|
||||
for (const endpoint of endpoints) {
|
||||
const requestBody = (endpoint.request as Record<string, unknown>)
|
||||
.body as Record<string, unknown> | null;
|
||||
if (requestBody && Array.isArray(requestBody.schemaRefs)) {
|
||||
for (const schemaName of requestBody.schemaRefs) {
|
||||
if (typeof schemaName === 'string') {
|
||||
referencedSchemas.add(schemaName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const statuses = (endpoint.response as Record<string, unknown>)
|
||||
.statuses as Array<Record<string, unknown>>;
|
||||
for (const status of statuses ?? []) {
|
||||
if (!Array.isArray(status.schemaRefs)) {
|
||||
continue;
|
||||
}
|
||||
for (const schemaName of status.schemaRefs) {
|
||||
if (typeof schemaName === 'string') {
|
||||
referencedSchemas.add(schemaName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allSchemas = (document.components?.schemas ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const schemaSnapshots: Record<string, unknown> = {};
|
||||
for (const schemaName of [...referencedSchemas].sort((a, b) =>
|
||||
a.localeCompare(b),
|
||||
)) {
|
||||
if (allSchemas[schemaName]) {
|
||||
schemaSnapshots[schemaName] = allSchemas[schemaName];
|
||||
}
|
||||
}
|
||||
|
||||
const summary = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
generatedBy: 'src/scripts/export-swagger-endpoints-summary.ts',
|
||||
project: 'Suggest-Bet-BE',
|
||||
swagger: {
|
||||
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.',
|
||||
],
|
||||
},
|
||||
stats: {
|
||||
byTag: [...tagStats.entries()]
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([tag, count]) => ({ tag, count })),
|
||||
endpointsWithoutSummary: endpoints
|
||||
.filter((endpoint) => !endpoint.summary)
|
||||
.map((endpoint) => ({
|
||||
operationId: endpoint.operationId,
|
||||
method: endpoint.method,
|
||||
path: endpoint.path,
|
||||
})),
|
||||
endpointsWithoutResponseSchema: endpoints
|
||||
.filter((endpoint) =>
|
||||
(
|
||||
(endpoint.response as Record<string, unknown>).statuses as Array<
|
||||
Record<string, unknown>
|
||||
>
|
||||
).some((status) => status.hasSchema === false),
|
||||
)
|
||||
.map((endpoint) => ({
|
||||
operationId: endpoint.operationId,
|
||||
method: endpoint.method,
|
||||
path: endpoint.path,
|
||||
})),
|
||||
},
|
||||
endpoints,
|
||||
referencedSchemas: schemaSnapshots,
|
||||
};
|
||||
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
writeFileSync(outputFile, JSON.stringify(summary, null, 2), 'utf8');
|
||||
|
||||
await app.close();
|
||||
|
||||
console.log(`✅ Swagger endpoint summary exported: ${outputFile}`);
|
||||
|
||||
console.log(
|
||||
` Endpoints in swagger: ${summary.swagger.endpointCountInSwagger}, total (with TS scan): ${summary.swagger.endpointCountTotal}`,
|
||||
);
|
||||
}
|
||||
|
||||
void run().catch((error: unknown) => {
|
||||
console.error('❌ Failed to export Swagger endpoint summary');
|
||||
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user