services refactor

This commit is contained in:
2025-12-17 23:37:51 +01:00
parent 1eaa338e86
commit f7a56a92ce
94 changed files with 1894 additions and 3194 deletions

View File

@@ -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);