283 lines
8.6 KiB
TypeScript
283 lines
8.6 KiB
TypeScript
#!/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<string, any> = {};
|
|
|
|
// 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<string, any>) {
|
|
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<string, any> = {};
|
|
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); |