wip league admin tools

This commit is contained in:
2025-12-28 12:04:12 +01:00
parent 5dc8c2399c
commit 6edf12fda8
401 changed files with 15365 additions and 6047 deletions

View File

@@ -38,15 +38,34 @@ interface OpenAPISpec {
};
}
function getCliArgValue(flag: string): string | undefined {
const index = process.argv.indexOf(flag);
if (index === -1) return undefined;
return process.argv[index + 1];
}
function resolveOutputPath(): string {
const cliOutput = getCliArgValue('--output') ?? getCliArgValue('-o');
const envOutput = process.env.OPENAPI_OUTPUT_PATH;
const configured = cliOutput ?? envOutput;
if (!configured) {
return path.join(process.cwd(), 'apps/api/openapi.json');
}
return path.resolve(process.cwd(), configured);
}
async function generateSpec() {
console.log('🔄 Generating OpenAPI spec from DTO files...');
const schemas: Record<string, OpenAPISchema> = {};
// Find all DTO files
// Find all DTO files (sorted for deterministic output)
const dtoFiles = await glob('apps/api/src/domain/*/dtos/**/*.ts', {
cwd: process.cwd()
});
dtoFiles.sort((a, b) => a.localeCompare(b));
console.log(`📁 Found ${dtoFiles.length} DTO files to process`);
@@ -58,6 +77,8 @@ async function generateSpec() {
}
}
const paths = await extractPathsFromControllers();
const spec: OpenAPISpec = {
openapi: '3.0.0',
info: {
@@ -65,17 +86,73 @@ async function generateSpec() {
description: 'GridPilot API documentation',
version: '1.0.0'
},
paths: {},
paths,
components: {
schemas
schemas: sortRecordKeys(schemas)
}
};
const outputPath = path.join(process.cwd(), 'apps/api/openapi.json');
const outputPath = resolveOutputPath();
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, JSON.stringify(spec, null, 2));
console.log(`✅ OpenAPI spec generated with ${Object.keys(schemas).length} schemas at: ${outputPath}`);
}
function sortRecordKeys<T>(record: Record<string, T>): Record<string, T> {
return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
}
function joinRouteParts(controllerPath: string, methodPath: string): string {
const base = controllerPath.replace(/^\/+|\/+$/g, '');
const sub = methodPath.replace(/^\/+|\/+$/g, '');
const joined = [base, sub].filter(Boolean).join('/');
return `/${joined}`;
}
function toOpenApiPath(route: string): string {
// Convert Nest-style ":param" to OpenAPI "{param}"
return route.replace(/(^|\/):([^/]+)/g, '$1{$2}');
}
async function extractPathsFromControllers(): Promise<Record<string, any>> {
const controllerFiles = await glob('apps/api/src/domain/**/*Controller.ts', {
cwd: process.cwd(),
});
controllerFiles.sort((a, b) => a.localeCompare(b));
const paths: Record<string, any> = {};
for (const controllerFile of controllerFiles) {
const filePath = path.join(process.cwd(), controllerFile);
const content = await fs.readFile(filePath, 'utf-8');
const controllerMatch = content.match(/@Controller\(\s*['"]([^'"]+)['"]\s*\)/);
if (!controllerMatch) continue;
const controllerPath = controllerMatch[1] ?? '';
const methodRegex = /@(Get|Post|Put|Patch|Delete)\(\s*(?:['"]([^'"]*)['"])?\s*\)/g;
let match: RegExpExecArray | null;
while ((match = methodRegex.exec(content)) !== null) {
const httpMethod = match[1]?.toLowerCase();
if (!httpMethod) continue;
const methodPath = match[2] ?? '';
const route = joinRouteParts(controllerPath, methodPath);
const openapiPath = toOpenApiPath(route);
paths[openapiPath] ??= {};
paths[openapiPath][httpMethod] ??= {
responses: {
'200': { description: 'OK' },
},
};
}
}
return sortRecordKeys(paths);
}
async function processDTOFile(filePath: string, schemas: Record<string, OpenAPISchema>) {
const content = await fs.readFile(filePath, 'utf-8');
@@ -160,9 +237,17 @@ function extractSchemaFromClassBody(classBody: string, fullContent: string): Ope
continue;
}
// Collect decorators
// Collect decorators (support multi-line decorator calls like @ApiProperty({ ... }))
if (line.startsWith('@')) {
currentDecorators.push(line);
let decorator = line;
while (!decorator.includes(')') && i + 1 < lines.length) {
const nextLine = lines[i + 1]?.trim() ?? '';
decorator = `${decorator} ${nextLine}`.trim();
i++;
}
currentDecorators.push(decorator);
continue;
}
@@ -183,10 +268,11 @@ function extractSchemaFromClassBody(classBody: string, fullContent: string): Ope
}
// Determine if required
const hasApiProperty = currentDecorators.some(d => d.includes('@ApiProperty'));
const isOptional = !!optional ||
currentDecorators.some(d => d.includes('required: false') || d.includes('@IsOptional'));
const isNullable = currentDecorators.some(d => d.includes('nullable: true'));
const isOptional =
!!optional || currentDecorators.some(d => d.includes('required: false') || d.includes('@IsOptional'));
const isNullableFromDecorator = currentDecorators.some(d => d.includes('nullable: true'));
const isNullableFromType = cleanedType.includes('| null') || cleanedType.includes('null |');
const isNullable = isNullableFromDecorator || isNullableFromType;
if (!isOptional && !isNullable) {
required.push(propName);