#!/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 { execSync } from 'child_process'; import fs from 'fs/promises'; import path from 'path'; 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...'); // Check if OpenAPI spec exists try { 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'); process.exit(1); } // Ensure output directory exists await fs.mkdir(outputDir, { recursive: true }); try { // 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); } catch (error) { console.error('❌ Failed to generate types:', error); process.exit(1); } } async function generateIndividualDtoFiles(openapiPath: string, outputDir: 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); // Generate individual files for each schema for (const schemaName of schemaNames) { const schema = schemas[schemaName]; const fileName = `${schemaName}.ts`; const filePath = path.join(outputDir, fileName); const fileContent = generateDtoFileContent(schemaName, schema, schemas); await fs.writeFile(filePath, fileContent); console.log(` ✅ Generated ${fileName}`); } // Generate index file that re-exports all DTOs let indexContent = `/** * Auto-generated DTO type exports * This file is generated by scripts/generate-api-types.ts * Do not edit manually - regenerate using: npm run api:sync-types */ // Re-export all schema types from the generated OpenAPI types export type { components, paths, operations } from './api'; // Re-export individual DTO types `; for (const schemaName of schemaNames) { indexContent += `export type { ${schemaName} } from './${schemaName}';\n`; } const indexPath = path.join(outputDir, 'index.ts'); await fs.writeFile(indexPath, indexContent); console.log(`✅ Generated ${schemaNames.length} individual DTO files and index at: ${outputDir}`); } function generateDtoFileContent(schemaName: string, schema: any, allSchemas: Record): string { // Collect dependencies (referenced DTOs) const dependencies = new Set(); collectDependencies(schema, dependencies, allSchemas); dependencies.delete(schemaName); // Remove self-reference let content = `/** * Auto-generated DTO from OpenAPI spec * This file is generated by scripts/generate-api-types.ts * Do not edit manually - regenerate using: npm run api:sync-types */ `; // Add imports for dependencies for (const dep of dependencies) { content += `import type { ${dep} } from './${dep}';\n`; } if (dependencies.size > 0) { content += '\n'; } // Generate interface 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); // 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.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): string { if (!schema) return 'unknown'; if (schema.$ref) { return schema.$ref.split('/').pop() || 'unknown'; } if (schema.type === 'array') { const itemType = schemaToTypeString(schema.items); return `${itemType}[]`; } if (schema.type === 'object') { if (schema.properties) { // Inline object type const props = Object.entries(schema.properties) .map(([key, val]) => `${key}: ${schemaToTypeString(val as any)}`) .join('; '); return `{ ${props} }`; } return 'Record'; } if (schema.oneOf) { return schema.oneOf.map((s: any) => schemaToTypeString(s)).join(' | '); } if (schema.anyOf) { return schema.anyOf.map((s: any) => schemaToTypeString(s)).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);