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

@@ -12,17 +12,21 @@
* Or use: npm run api:sync-types (runs both)
*/
import { execSync } from 'child_process';
import { createHash } from 'crypto';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
async function sha256OfFile(filePath: string): Promise<string> {
const buffer = await fs.readFile(filePath);
return createHash('sha256').update(buffer).digest('hex');
}
async function generateTypes() {
const openapiPath = path.join(__dirname, '../apps/api/openapi.json');
const outputDir = path.join(__dirname, '../apps/website/lib/types/generated');
const outputFile = path.join(outputDir, 'api.ts');
console.log('🔄 Generating TypeScript types from OpenAPI spec...');
@@ -31,7 +35,7 @@ async function generateTypes() {
await fs.access(openapiPath);
} catch {
console.error(`❌ OpenAPI spec not found at: ${openapiPath}`);
console.error('Run "npm run api:generate-spec" first to generate the OpenAPI spec from NestJS');
console.error('Restore the committed OpenAPI contract or run "npm run api:generate-spec" to regenerate it.');
process.exit(1);
}
@@ -39,33 +43,24 @@ async function generateTypes() {
await fs.mkdir(outputDir, { recursive: true });
try {
// Skip generating monolithic api.ts file
// Use openapi-typescript to generate types
// console.log('📝 Running openapi-typescript...');
// execSync(`npx openapi-typescript "${openapiPath}" -o "${outputFile}"`, {
// stdio: 'inherit',
// cwd: path.join(__dirname, '..')
// });
// console.log(`✅ TypeScript types generated at: ${outputFile}`);
// Generate individual DTO files
await generateIndividualDtoFiles(openapiPath, outputDir);
const specSha256 = await sha256OfFile(openapiPath);
// Generate individual DTO files + barrel index for deterministic imports
await generateIndividualDtoFiles(openapiPath, outputDir, specSha256);
} catch (error) {
console.error('❌ Failed to generate types:', error);
process.exit(1);
}
}
async function generateIndividualDtoFiles(openapiPath: string, outputDir: string) {
async function generateIndividualDtoFiles(openapiPath: string, outputDir: string, specSha256: string) {
console.log('📝 Generating individual DTO files...');
const specContent = await fs.readFile(openapiPath, 'utf-8');
const spec = JSON.parse(specContent);
const schemas = spec.components?.schemas || {};
const schemaNames = Object.keys(schemas);
const schemaNames = Object.keys(schemas).sort((a: string, b: string) => a.localeCompare(b));
// Get existing files in output directory
let existingFiles: string[] = [];
@@ -80,17 +75,24 @@ async function generateIndividualDtoFiles(openapiPath: string, outputDir: string
const generatedFileNames: string[] = [];
for (const schemaName of schemaNames) {
const schema = schemas[schemaName];
// File name should match the schema name exactly
const fileName = `${schemaName}.ts`;
const filePath = path.join(outputDir, fileName);
const fileContent = generateDtoFileContent(schemaName, schema, schemas);
const fileContent = generateDtoFileContent(schemaName, schema, schemas, specSha256);
await fs.writeFile(filePath, fileContent);
console.log(` ✅ Generated ${fileName}`);
generatedFileNames.push(fileName);
}
const indexFileName = 'index.ts';
const indexFilePath = path.join(outputDir, indexFileName);
const indexFileContent = generateIndexFileContent(schemaNames, specSha256);
await fs.writeFile(indexFilePath, indexFileContent);
console.log(` ✅ Generated ${indexFileName}`);
generatedFileNames.push(indexFileName);
// Clean up files that are no longer in the spec
const filesToRemove = existingFiles.filter(f => !generatedFileNames.includes(f));
for (const file of filesToRemove) {
@@ -105,26 +107,44 @@ async function generateIndividualDtoFiles(openapiPath: string, outputDir: string
}
}
function generateDtoFileContent(schemaName: string, schema: any, allSchemas: Record<string, any>): string {
function generateIndexFileContent(schemaNames: string[], specSha256: string): string {
let content = `/**
* Auto-generated barrel for API DTO types.
* Spec SHA256: ${specSha256}
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:generate-types
*/
`;
for (const schemaName of schemaNames) {
content += `\nexport type { ${schemaName} } from './${schemaName}';`;
}
content += '\n';
return content;
}
function generateDtoFileContent(schemaName: string, schema: any, allSchemas: Record<string, any>, specSha256: string): string {
// Collect dependencies (referenced DTOs)
const dependencies = new Set<string>();
collectDependencies(schema, dependencies, allSchemas);
dependencies.delete(schemaName); // Remove self-reference
const sortedDependencies = Array.from(dependencies).sort((a, b) => a.localeCompare(b));
let content = `/**
* Auto-generated DTO from OpenAPI spec
* Spec SHA256: ${specSha256}
* This file is generated by scripts/generate-api-types.ts
* Do not edit manually - regenerate using: npm run api:sync-types
* Do not edit manually - regenerate using: npm run api:generate-types
*/
`;
// Add imports for dependencies
for (const dep of dependencies) {
// Add imports for dependencies (sorted for deterministic output)
for (const dep of sortedDependencies) {
content += `import type { ${dep} } from './${dep}';\n`;
}
if (dependencies.size > 0) {
if (sortedDependencies.length > 0) {
content += '\n';
}
@@ -137,7 +157,7 @@ function generateDtoFileContent(schemaName: string, schema: any, allSchemas: Rec
for (const [propName, propSchema] of Object.entries(properties)) {
const isRequired = required.has(propName);
const optionalMark = isRequired ? '' : '?';
const typeStr = schemaToTypeString(propSchema as any);
const typeStr = schemaToTypeString(propSchema as any, allSchemas);
// Add JSDoc comment for format
if ((propSchema as any).format) {
@@ -197,42 +217,44 @@ function collectDependencies(schema: any, deps: Set<string>, allSchemas: Record<
}
}
function schemaToTypeString(schema: any): string {
function schemaToTypeString(schema: any, allSchemas: Record<string, any>): string {
if (!schema) return 'unknown';
if (schema.$ref) {
return schema.$ref.split('/').pop() || 'unknown';
const refName = schema.$ref.split('/').pop();
if (!refName) return 'unknown';
return allSchemas[refName] ? refName : 'unknown';
}
if (schema.type === 'array') {
const itemType = schemaToTypeString(schema.items);
const itemType = schemaToTypeString(schema.items, allSchemas);
return `${itemType}[]`;
}
if (schema.type === 'object') {
if (schema.additionalProperties) {
const valueType = schemaToTypeString(schema.additionalProperties);
const valueType = schemaToTypeString(schema.additionalProperties, allSchemas);
return `Record<string, ${valueType}>`;
}
if (schema.properties) {
// Inline object type
const props = Object.entries(schema.properties)
.map(([key, val]) => `${key}: ${schemaToTypeString(val as any)}`)
.map(([key, val]) => `${key}: ${schemaToTypeString(val as any, allSchemas)}`)
.join('; ');
return `{ ${props} }`;
}
return 'Record<string, unknown>';
}
if (schema.oneOf) {
return schema.oneOf.map((s: any) => schemaToTypeString(s)).join(' | ');
return schema.oneOf.map((s: any) => schemaToTypeString(s, allSchemas)).join(' | ');
}
if (schema.anyOf) {
return schema.anyOf.map((s: any) => schemaToTypeString(s)).join(' | ');
return schema.anyOf.map((s: any) => schemaToTypeString(s, allSchemas)).join(' | ');
}
if (schema.enum) {
return schema.enum.map((v: any) => JSON.stringify(v)).join(' | ');
}