#!/usr/bin/env tsx /** * Generate TypeScript types from OpenAPI spec * * This script uses openapi-typescript to generate proper TypeScript types * from the OpenAPI spec that NestJS/Swagger generates. * * Usage: * 1. First generate the OpenAPI spec: npm run api:generate-spec * 2. Then generate types: npm run api:generate-types * * Or use: npm run api:sync-types (runs both) */ 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 { 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'); console.log('๐Ÿ”„ Generating TypeScript types from OpenAPI spec...'); // Check if OpenAPI spec exists try { await fs.access(openapiPath); } catch { console.error(`โŒ OpenAPI spec not found at: ${openapiPath}`); console.error('Restore the committed OpenAPI contract or run "npm run api:generate-spec" to regenerate it.'); process.exit(1); } // Ensure output directory exists await fs.mkdir(outputDir, { recursive: true }); try { 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, 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).sort((a: string, b: string) => a.localeCompare(b)); // Get existing files in output directory let existingFiles: string[] = []; try { existingFiles = await fs.readdir(outputDir); existingFiles = existingFiles.filter(f => f.endsWith('.ts')); } catch (error) { // Directory doesn't exist yet } // Generate individual files for each schema 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, 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) { const filePath = path.join(outputDir, file); await fs.unlink(filePath); console.log(` ๐Ÿ—‘๏ธ Removed obsolete file: ${file}`); } console.log(`โœ… Generated ${schemaNames.length} individual DTO files at: ${outputDir}`); if (filesToRemove.length > 0) { console.log(`๐Ÿงน Cleaned up ${filesToRemove.length} obsolete files`); } } 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, specSha256: string): string { // Collect dependencies (referenced DTOs) const dependencies = new Set(); 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:generate-types */ `; // Add imports for dependencies (sorted for deterministic output) for (const dep of sortedDependencies) { content += `import type { ${dep} } from './${dep}';\n`; } if (sortedDependencies.length > 0) { content += '\n'; } // Generate interface - use the schema name directly content += `export interface ${schemaName} {\n`; const properties = schema.properties || {}; const required = new Set(schema.required || []); for (const [propName, propSchema] of Object.entries(properties)) { const isRequired = required.has(propName); const optionalMark = isRequired ? '' : '?'; const typeStr = schemaToTypeString(propSchema as any, allSchemas); // Add JSDoc comment for format if ((propSchema as any).format) { content += ` /** Format: ${(propSchema as any).format} */\n`; } content += ` ${propName}${optionalMark}: ${typeStr};\n`; } content += '}\n'; return content; } function collectDependencies(schema: any, deps: Set, allSchemas: Record): void { if (!schema) return; if (schema.$ref) { const refName = schema.$ref.split('/').pop(); if (refName && allSchemas[refName]) { deps.add(refName); } return; } if (schema.type === 'array' && schema.items) { collectDependencies(schema.items, deps, allSchemas); return; } if (schema.properties) { for (const propSchema of Object.values(schema.properties)) { collectDependencies(propSchema, deps, allSchemas); } } if (schema.additionalProperties) { collectDependencies(schema.additionalProperties, deps, allSchemas); } if (schema.oneOf) { for (const subSchema of schema.oneOf) { collectDependencies(subSchema, deps, allSchemas); } } if (schema.anyOf) { for (const subSchema of schema.anyOf) { collectDependencies(subSchema, deps, allSchemas); } } if (schema.allOf) { for (const subSchema of schema.allOf) { collectDependencies(subSchema, deps, allSchemas); } } } function schemaToTypeString(schema: any, allSchemas: Record): string { if (!schema) return 'unknown'; if (schema.$ref) { const refName = schema.$ref.split('/').pop(); if (!refName) return 'unknown'; return allSchemas[refName] ? refName : 'unknown'; } if (schema.type === 'array') { const itemType = schemaToTypeString(schema.items, allSchemas); return `${itemType}[]`; } if (schema.type === 'object') { if (schema.additionalProperties) { const valueType = schemaToTypeString(schema.additionalProperties, allSchemas); return `Record`; } if (schema.properties) { // Inline object type const props = Object.entries(schema.properties) .map(([key, val]) => `${key}: ${schemaToTypeString(val as any, allSchemas)}`) .join('; '); return `{ ${props} }`; } return 'Record'; } if (schema.oneOf) { return schema.oneOf.map((s: any) => schemaToTypeString(s, allSchemas)).join(' | '); } if (schema.anyOf) { return schema.anyOf.map((s: any) => schemaToTypeString(s, allSchemas)).join(' | '); } if (schema.enum) { return schema.enum.map((v: any) => JSON.stringify(v)).join(' | '); } switch (schema.type) { case 'string': return 'string'; case 'number': case 'integer': return 'number'; case 'boolean': return 'boolean'; case 'null': return 'null'; default: return 'unknown'; } } generateTypes().catch(console.error);