#!/usr/bin/env tsx import fs from 'fs/promises'; import path from 'path'; import { glob } from 'glob'; // Comprehensive OpenAPI spec generator that scans all DTO files async function generateComprehensiveOpenAPISpec() { console.log('🔄 Generating comprehensive OpenAPI spec from all DTO files...'); const schemas: Record = {}; // Find all DTO files in the API const dtoFiles = await glob('apps/api/src/domain/*/dtos/**/*.ts', { cwd: path.join(__dirname, '..') }); console.log(`📁 Found ${dtoFiles.length} DTO files to process`); for (const dtoFile of dtoFiles) { const filePath = path.join(__dirname, '..', dtoFile); await processDTOFile(filePath, schemas); } // Also check for DTOs in other locations const additionalDtoFiles = await glob('apps/api/src/domain/*/*.ts', { cwd: path.join(__dirname, '..') }); for (const dtoFile of additionalDtoFiles) { if (dtoFile.includes('dto') || dtoFile.includes('DTO')) { const filePath = path.join(__dirname, '..', dtoFile); await processDTOFile(filePath, schemas); } } const spec = { openapi: '3.0.0', info: { title: 'GridPilot API', description: 'GridPilot API documentation', version: '1.0.0' }, paths: {}, components: { schemas } }; const outputPath = path.join(__dirname, '../apps/api/openapi.json'); await fs.writeFile(outputPath, JSON.stringify(spec, null, 2)); console.log(`✅ Comprehensive OpenAPI spec generated with ${Object.keys(schemas).length} schemas at: ${outputPath}`); } async function processDTOFile(filePath: string, schemas: Record) { try { const content = await fs.readFile(filePath, 'utf-8'); // Extract all class and interface definitions const classMatches = content.match(/export (?:class|interface) (\w+(?:DTO|Dto))/g); if (!classMatches) { // Debug: check if file has any export statements if (content.includes('export')) { console.log(`📄 ${filePath} has exports but no DTO classes`); } return; } console.log(`📄 Processing ${filePath} - found ${classMatches.length} DTO classes`); for (const classMatch of classMatches) { const classNameMatch = classMatch.match(/export (?:class|interface) (\w+(?:DTO|Dto))/); if (classNameMatch) { const className = classNameMatch[1]; console.log(` 🔍 Extracting schema for ${className}`); const schema = extractSchemaFromClass(content, className); if (schema && Object.keys(schema.properties || {}).length > 0) { schemas[className] = schema; console.log(` ✅ Added schema for ${className} with ${Object.keys(schema.properties).length} properties`); } else { console.log(` ⚠️ No schema generated for ${className}`); } } } } catch (error) { // File can't be read, continue console.warn(`⚠️ Could not process ${filePath}:`, error.message); } } function extractSchemaFromClass(content: string, className: string): any | null { const properties: Record = {}; const required: string[] = []; // Extract @ApiProperty decorated properties from NestJS DTOs // Pattern: @ApiProperty(...) followed by property declaration const lines = content.split('\n'); let i = 0; while (i < lines.length) { const line = lines[i].trim(); // Look for @ApiProperty decorator if (line.startsWith('@ApiProperty(')) { const decoratorMatch = line.match(/@ApiProperty\(([^)]*)\)/); if (decoratorMatch) { const decoratorContent = decoratorMatch[1]; // Find the property declaration (could be on next line) let propertyLine = line; if (!line.includes(';')) { i++; if (i < lines.length) { propertyLine = lines[i].trim(); } } // Extract property name and type from declaration const propertyMatch = propertyLine.match(/(\w+)\s*\??:\s*([^;]+);/); if (propertyMatch) { const propertyName = propertyMatch[1]; const propertyType = propertyMatch[2].trim(); // Check if property is required const isOptional = propertyName.includes('?') || decoratorContent.includes('required: false') || decoratorContent.includes('nullable: true') || propertyLine.includes('@IsOptional()'); if (!isOptional) { required.push(propertyName); } // Try to extract type from decorator first, then fall back to property type let schemaType = extractTypeFromDecorator(decoratorContent); if (!schemaType) { schemaType = mapTypeToSchema(propertyType); } properties[propertyName] = schemaType; } } } i++; } // Also extract interface properties (for existing interfaces) const interfacePropertyRegex = /(\w+)\s*\??:\s*([^;]+);/g; let match; while ((match = interfacePropertyRegex.exec(content)) !== null) { const propertyName = match[1]; const propertyType = match[2].trim(); if (!properties[propertyName]) { // Check if property is required if (!propertyName.includes('?')) { required.push(propertyName); } properties[propertyName] = mapTypeToSchema(propertyType); } } if (Object.keys(properties).length === 0) { return null; } const schema: any = { type: 'object', properties }; if (required.length > 0) { schema.required = required; } return schema; } function extractTypeFromDecorator(decoratorContent: string): any | null { // Extract type information from @ApiProperty decorator // Examples: // @ApiProperty({ type: String }) // @ApiProperty({ type: [SomeDTO] }) // @ApiProperty({ type: () => SomeDTO }) if (decoratorContent.includes('type:')) { // Simple type extraction - this is a simplified version // In a real implementation, you'd want proper AST parsing if (decoratorContent.includes('[String]') || decoratorContent.includes('[string]')) { return { type: 'array', items: { type: 'string' } }; } if (decoratorContent.includes('[Number]') || decoratorContent.includes('[number]')) { return { type: 'array', items: { type: 'number' } }; } if (decoratorContent.includes('String') || decoratorContent.includes('string')) { return { type: 'string' }; } if (decoratorContent.includes('Number') || decoratorContent.includes('number')) { return { type: 'number' }; } if (decoratorContent.includes('Boolean') || decoratorContent.includes('boolean')) { return { type: 'boolean' }; } // For complex types with references const refMatch = decoratorContent.match(/type:\s*\[?(\w+DTO)\]?/); if (refMatch) { const refType = refMatch[1]; if (decoratorContent.includes('[')) { return { type: 'array', items: { $ref: `#/components/schemas/${refType}` } }; } else { return { $ref: `#/components/schemas/${refType}` }; } } // For function types like type: () => SomeDTO const funcMatch = decoratorContent.match(/type:\s*\(\)\s*=>\s*(\w+DTO)/); if (funcMatch) { return { $ref: `#/components/schemas/${funcMatch[1]}` }; } } return null; } function mapTypeToSchema(type: string): any { // Clean up the type type = type.replace(/[|;]/g, '').trim(); // Handle array types if (type.endsWith('[]')) { const itemType = type.slice(0, -2); return { type: 'array', items: mapTypeToSchema(itemType) }; } // Handle union types (simplified) if (type.includes('|')) { const types = type.split('|').map(t => t.trim()); if (types.length === 2 && types.includes('null')) { // Nullable type const nonNullType = types.find(t => t !== 'null'); return mapTypeToSchema(nonNullType!); } return { oneOf: types.map(t => mapTypeToSchema(t)) }; } // Handle basic types switch (type.toLowerCase()) { case 'string': return { type: 'string' }; case 'number': case 'bigint': return { type: 'number' }; case 'boolean': return { type: 'boolean' }; case 'date': return { type: 'string', format: 'date-time' }; case 'any': case 'unknown': return {}; default: // For complex types, assume they're other DTOs if (type.includes('DTO') || type.includes('Dto')) { return { $ref: `#/components/schemas/${type}` }; } // For other types, use string as fallback return { type: 'string' }; } } generateComprehensiveOpenAPISpec().catch(console.error);