wip league admin tools
This commit is contained in:
@@ -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(' | ');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user