services refactor
This commit is contained in:
@@ -1,113 +1,238 @@
|
||||
#!/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 generateIndividualDTOs() {
|
||||
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 individual DTO files from OpenAPI spec...');
|
||||
console.log('🔄 Generating TypeScript types from OpenAPI spec...');
|
||||
|
||||
// Check if OpenAPI spec exists
|
||||
try {
|
||||
// Check if OpenAPI spec exists
|
||||
await fs.access(openapiPath);
|
||||
} catch {
|
||||
console.error(`❌ OpenAPI spec not found at: ${openapiPath}`);
|
||||
console.error('Run "npm run api:generate-spec" first');
|
||||
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 {
|
||||
// Read the OpenAPI spec
|
||||
const specContent = await fs.readFile(openapiPath, 'utf-8');
|
||||
const spec = JSON.parse(specContent);
|
||||
|
||||
// Ensure output directory exists
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
// Extract schemas from the spec
|
||||
const schemas = spec.components?.schemas || {};
|
||||
|
||||
console.log(`📝 Found ${Object.keys(schemas).length} schemas to generate`);
|
||||
|
||||
// Generate individual files for each schema
|
||||
for (const [schemaName, schema] of Object.entries(schemas)) {
|
||||
if (typeof schema === 'object' && schema !== null) {
|
||||
const fileName = `${schemaName}.ts`;
|
||||
const filePath = path.join(outputDir, fileName);
|
||||
|
||||
// Convert OpenAPI schema to TypeScript interface
|
||||
const tsInterface = generateTypeScriptInterface(schemaName, schema);
|
||||
|
||||
await fs.writeFile(filePath, tsInterface);
|
||||
console.log(`✅ Generated ${fileName}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🎉 Generated ${Object.keys(schemas).length} DTO files in ${outputDir}`);
|
||||
// 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 DTOs:', error);
|
||||
console.error('❌ Failed to generate types:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function generateTypeScriptInterface(name: string, schema: any): string {
|
||||
const properties = schema.properties || {};
|
||||
const required = schema.required || [];
|
||||
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
|
||||
*/
|
||||
|
||||
let interfaceContent = `/**
|
||||
// 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, any>): string {
|
||||
// Collect dependencies (referenced DTOs)
|
||||
const dependencies = new Set<string>();
|
||||
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
|
||||
*/
|
||||
|
||||
export interface ${name} {\n`;
|
||||
`;
|
||||
|
||||
for (const [propName, propSchema] of Object.entries(properties)) {
|
||||
const isRequired = required.includes(propName);
|
||||
const optionalMark = isRequired ? '' : '?';
|
||||
const type = openApiTypeToTypeScript(propSchema);
|
||||
|
||||
interfaceContent += ` ${propName}${optionalMark}: ${type};\n`;
|
||||
// Add imports for dependencies
|
||||
for (const dep of dependencies) {
|
||||
content += `import type { ${dep} } from './${dep}';\n`;
|
||||
}
|
||||
|
||||
if (dependencies.size > 0) {
|
||||
content += '\n';
|
||||
}
|
||||
|
||||
interfaceContent += '}\n';
|
||||
|
||||
return interfaceContent;
|
||||
// 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 openApiTypeToTypeScript(schema: any): string {
|
||||
function collectDependencies(schema: any, deps: Set<string>, allSchemas: Record<string, any>): void {
|
||||
if (!schema) return;
|
||||
|
||||
if (schema.$ref) {
|
||||
// Handle references
|
||||
const refName = schema.$ref.split('/').pop();
|
||||
return refName;
|
||||
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 = openApiTypeToTypeScript(schema.items);
|
||||
const itemType = schemaToTypeString(schema.items);
|
||||
return `${itemType}[]`;
|
||||
}
|
||||
|
||||
|
||||
if (schema.type === 'object') {
|
||||
// For complex objects, we'll use a generic Record for now
|
||||
return 'Record<string, any>';
|
||||
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<string, unknown>';
|
||||
}
|
||||
|
||||
|
||||
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':
|
||||
if (schema.format === 'date-time') {
|
||||
return 'string'; // Keep as string for now, could be Date
|
||||
}
|
||||
return 'string';
|
||||
case 'number':
|
||||
case 'integer':
|
||||
return 'number';
|
||||
case 'boolean':
|
||||
return 'boolean';
|
||||
case 'null':
|
||||
return 'null';
|
||||
default:
|
||||
return 'any';
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
generateIndividualDTOs().catch(console.error);
|
||||
generateTypes().catch(console.error);
|
||||
@@ -1,283 +0,0 @@
|
||||
#!/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);
|
||||
@@ -1,39 +1,63 @@
|
||||
#!/usr/bin/env tsx
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
/**
|
||||
* Generate OpenAPI spec from NestJS DTO classes
|
||||
*
|
||||
* This script scans all DTO files and extracts OpenAPI schema definitions
|
||||
* using the @nestjs/swagger metadata.
|
||||
*/
|
||||
|
||||
import 'reflect-metadata';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as 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...');
|
||||
// OpenAPI schema types
|
||||
interface OpenAPISchema {
|
||||
type?: string;
|
||||
format?: string;
|
||||
$ref?: string;
|
||||
items?: OpenAPISchema;
|
||||
properties?: Record<string, OpenAPISchema>;
|
||||
required?: string[];
|
||||
enum?: string[];
|
||||
nullable?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const schemas: Record<string, any> = {};
|
||||
interface OpenAPISpec {
|
||||
openapi: string;
|
||||
info: {
|
||||
title: string;
|
||||
description: string;
|
||||
version: string;
|
||||
};
|
||||
paths: Record<string, any>;
|
||||
components: {
|
||||
schemas: Record<string, OpenAPISchema>;
|
||||
};
|
||||
}
|
||||
|
||||
// Find all DTO files in the API
|
||||
async function generateSpec() {
|
||||
console.log('🔄 Generating OpenAPI spec from DTO files...');
|
||||
|
||||
const schemas: Record<string, OpenAPISchema> = {};
|
||||
|
||||
// Find all DTO files
|
||||
const dtoFiles = await glob('apps/api/src/domain/*/dtos/**/*.ts', {
|
||||
cwd: path.join(__dirname, '..')
|
||||
cwd: process.cwd()
|
||||
});
|
||||
|
||||
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);
|
||||
try {
|
||||
await processDTOFile(path.join(process.cwd(), dtoFile), schemas);
|
||||
} catch (e: any) {
|
||||
console.warn(`⚠️ Could not process ${dtoFile}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const spec = {
|
||||
const spec: OpenAPISpec = {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'GridPilot API',
|
||||
@@ -46,218 +70,183 @@ async function generateComprehensiveOpenAPISpec() {
|
||||
}
|
||||
};
|
||||
|
||||
const outputPath = path.join(__dirname, '../apps/api/openapi.json');
|
||||
const outputPath = path.join(process.cwd(), '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}`);
|
||||
console.log(`✅ 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');
|
||||
async function processDTOFile(filePath: string, schemas: Record<string, OpenAPISchema>) {
|
||||
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;
|
||||
// Find class definitions with DTO suffix
|
||||
const classRegex = /export\s+class\s+(\w+(?:DTO|Dto))\s*(?:extends\s+\w+)?\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/gs;
|
||||
let classMatch;
|
||||
|
||||
while ((classMatch = classRegex.exec(content)) !== null) {
|
||||
const className = classMatch[1];
|
||||
const classBody = classMatch[2];
|
||||
|
||||
console.log(` 📝 Processing ${className}`);
|
||||
|
||||
const schema = extractSchemaFromClassBody(classBody, content);
|
||||
if (schema && Object.keys(schema.properties || {}).length > 0) {
|
||||
schemas[className] = schema;
|
||||
console.log(` ✅ Added ${className} with ${Object.keys(schema.properties || {}).length} properties`);
|
||||
}
|
||||
|
||||
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> = {};
|
||||
function extractSchemaFromClassBody(classBody: string, fullContent: string): OpenAPISchema {
|
||||
const properties: Record<string, OpenAPISchema> = {};
|
||||
const required: string[] = [];
|
||||
|
||||
// Extract @ApiProperty decorated properties from NestJS DTOs
|
||||
// Pattern: @ApiProperty(...) followed by property declaration
|
||||
const lines = content.split('\n');
|
||||
let i = 0;
|
||||
// Split by lines and process each property
|
||||
const lines = classBody.split('\n');
|
||||
let currentDecorators: string[] = [];
|
||||
|
||||
while (i < lines.length) {
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Skip empty lines and comments
|
||||
if (!line || line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
// Collect decorators
|
||||
if (line.startsWith('@')) {
|
||||
currentDecorators.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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();
|
||||
// Check if this is a property declaration
|
||||
const propertyMatch = line.match(/^(\w+)(\?)?[!]?\s*:\s*(.+?)\s*;?\s*$/);
|
||||
if (propertyMatch) {
|
||||
const [, propName, optional, propType] = propertyMatch;
|
||||
|
||||
if (!properties[propertyName]) {
|
||||
// Check if property is required
|
||||
if (!propertyName.includes('?')) {
|
||||
required.push(propertyName);
|
||||
// Skip if propName is a TypeScript/decorator keyword
|
||||
if (['constructor', 'private', 'public', 'protected', 'static', 'readonly'].includes(propName)) {
|
||||
currentDecorators = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
properties[propertyName] = mapTypeToSchema(propertyType);
|
||||
// Determine if required
|
||||
const hasApiProperty = currentDecorators.some(d => d.includes('@ApiProperty'));
|
||||
const isOptional = !!optional ||
|
||||
currentDecorators.some(d => d.includes('required: false') || d.includes('@IsOptional'));
|
||||
const isNullable = currentDecorators.some(d => d.includes('nullable: true'));
|
||||
|
||||
if (!isOptional && !isNullable) {
|
||||
required.push(propName);
|
||||
}
|
||||
|
||||
// Extract type from @ApiProperty decorator if present
|
||||
let schema = extractTypeFromDecorators(currentDecorators, propType);
|
||||
|
||||
properties[propName] = schema;
|
||||
currentDecorators = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(properties).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const schema: any = {
|
||||
const result: OpenAPISchema = {
|
||||
type: 'object',
|
||||
properties
|
||||
};
|
||||
|
||||
if (required.length > 0) {
|
||||
schema.required = required;
|
||||
result.required = required;
|
||||
}
|
||||
|
||||
return schema;
|
||||
return result;
|
||||
}
|
||||
|
||||
function extractTypeFromDecorator(decoratorContent: string): any | null {
|
||||
// Extract type information from @ApiProperty decorator
|
||||
// Examples:
|
||||
// @ApiProperty({ type: String })
|
||||
// @ApiProperty({ type: [SomeDTO] })
|
||||
// @ApiProperty({ type: () => SomeDTO })
|
||||
function extractTypeFromDecorators(decorators: string[], tsType: string): OpenAPISchema {
|
||||
// Join all decorators to search across them
|
||||
const decoratorStr = decorators.join(' ');
|
||||
|
||||
if (decoratorContent.includes('type:')) {
|
||||
// Simple type extraction - this is a simplified version
|
||||
// In a real implementation, you'd want proper AST parsing
|
||||
// Check for @ApiProperty type specification
|
||||
const apiPropertyMatch = decoratorStr.match(/@ApiProperty\s*\(\s*\{([^}]*)\}\s*\)/s);
|
||||
|
||||
if (apiPropertyMatch) {
|
||||
const apiPropertyContent = apiPropertyMatch[1];
|
||||
|
||||
if (decoratorContent.includes('[String]') || decoratorContent.includes('[string]')) {
|
||||
return { type: 'array', items: { type: 'string' } };
|
||||
// Check for array type: type: [SomeDTO]
|
||||
const arrayTypeMatch = apiPropertyContent.match(/type:\s*\[\s*(\w+)\s*\]/);
|
||||
if (arrayTypeMatch) {
|
||||
const itemType = arrayTypeMatch[1];
|
||||
return {
|
||||
type: 'array',
|
||||
items: mapTypeToSchema(itemType)
|
||||
};
|
||||
}
|
||||
|
||||
if (decoratorContent.includes('[Number]') || decoratorContent.includes('[number]')) {
|
||||
return { type: 'array', items: { type: 'number' } };
|
||||
// Check for function type: type: () => SomeDTO
|
||||
const funcTypeMatch = apiPropertyContent.match(/type:\s*\(\)\s*=>\s*(\w+)/);
|
||||
if (funcTypeMatch) {
|
||||
return mapTypeToSchema(funcTypeMatch[1]);
|
||||
}
|
||||
|
||||
if (decoratorContent.includes('String') || decoratorContent.includes('string')) {
|
||||
return { type: 'string' };
|
||||
// Check for direct type reference: type: SomeDTO
|
||||
const directTypeMatch = apiPropertyContent.match(/type:\s*(\w+)(?:\s*[,}])/);
|
||||
if (directTypeMatch && !['String', 'Number', 'Boolean', 'string', 'number', 'boolean'].includes(directTypeMatch[1])) {
|
||||
return mapTypeToSchema(directTypeMatch[1]);
|
||||
}
|
||||
|
||||
if (decoratorContent.includes('Number') || decoratorContent.includes('number')) {
|
||||
return { type: 'number' };
|
||||
// Check for enum
|
||||
const enumMatch = apiPropertyContent.match(/enum:\s*(\w+)/);
|
||||
if (enumMatch) {
|
||||
return { type: 'string' }; // Simplify enum to string
|
||||
}
|
||||
|
||||
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]}` };
|
||||
// Check for nullable
|
||||
if (apiPropertyContent.includes('nullable: true')) {
|
||||
const baseSchema = mapTypeToSchema(tsType);
|
||||
baseSchema.nullable = true;
|
||||
return baseSchema;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
// Fall back to TypeScript type
|
||||
return mapTypeToSchema(tsType);
|
||||
}
|
||||
|
||||
function mapTypeToSchema(type: string): any {
|
||||
function mapTypeToSchema(type: string): OpenAPISchema {
|
||||
// Clean up the type
|
||||
type = type.replace(/[|;]/g, '').trim();
|
||||
type = type.replace(/[;!]/g, '').trim();
|
||||
|
||||
// Handle union with null
|
||||
if (type.includes('| null') || type.includes('null |')) {
|
||||
const baseType = type.replace(/\|\s*null/g, '').replace(/null\s*\|/g, '').trim();
|
||||
const schema = mapTypeToSchema(baseType);
|
||||
schema.nullable = true;
|
||||
return schema;
|
||||
}
|
||||
|
||||
// Handle array types
|
||||
if (type.endsWith('[]')) {
|
||||
const itemType = type.slice(0, -2);
|
||||
const itemType = type.slice(0, -2).trim();
|
||||
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!);
|
||||
}
|
||||
// Handle Array<T> syntax
|
||||
const arrayGenericMatch = type.match(/^Array<(.+)>$/);
|
||||
if (arrayGenericMatch) {
|
||||
return {
|
||||
oneOf: types.map(t => mapTypeToSchema(t))
|
||||
type: 'array',
|
||||
items: mapTypeToSchema(arrayGenericMatch[1])
|
||||
};
|
||||
}
|
||||
|
||||
// Handle basic types
|
||||
switch (type.toLowerCase()) {
|
||||
// Handle object literal types (inline objects)
|
||||
if (type.startsWith('{')) {
|
||||
return { type: 'object' };
|
||||
}
|
||||
|
||||
// Handle primitive types
|
||||
const lowerType = type.toLowerCase();
|
||||
switch (lowerType) {
|
||||
case 'string':
|
||||
return { type: 'string' };
|
||||
case 'number':
|
||||
@@ -269,15 +258,17 @@ function mapTypeToSchema(type: string): any {
|
||||
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' };
|
||||
case 'object':
|
||||
return { type: 'object' };
|
||||
}
|
||||
|
||||
// Handle DTO references
|
||||
if (type.endsWith('DTO') || type.endsWith('Dto')) {
|
||||
return { $ref: `#/components/schemas/${type}` };
|
||||
}
|
||||
|
||||
// Default to string for unknown types
|
||||
return { type: 'string' };
|
||||
}
|
||||
|
||||
generateComprehensiveOpenAPISpec().catch(console.error);
|
||||
generateSpec().catch(console.error);
|
||||
Reference in New Issue
Block a user