weaver-frontend-cli
Version:
🕷️ Weaver CLI - Generador completo de arquitectura Clean Architecture con parser OpenAPI avanzado para entidades CRUD y flujos de negocio complejos
2,249 lines • 134 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createBusinessFlow = createBusinessFlow;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const chalk_1 = __importDefault(require("chalk"));
// Ruta base donde se ejecutará el comando
const DEFAULT_BASE_PATH = process.cwd(); // Directorio actual por defecto
const LOCAL_TEST_PATH = './test-output';
/**
* Extrae el nombre de la operación del path, ignorando los parámetros de path (ej: {user_id})
* Para /auth/delete-user-internal/{user_id} retorna "delete-user-internal"
*/
function getOperationNameFromPath(operationPath, fallback) {
const segments = operationPath.split('/').filter(s => s.length > 0);
// Buscar desde el final el primer segmento que NO sea un parámetro (que no tenga {})
for (let i = segments.length - 1; i >= 0; i--) {
const segment = segments[i];
if (!segment.startsWith('{') && !segment.endsWith('}')) {
return segment;
}
}
// Si todos son parámetros, usar el fallback (operationId)
return fallback;
}
async function createBusinessFlow(serviceName, basePath = DEFAULT_BASE_PATH, schema, targetApiName = 'platform') {
console.log(chalk_1.default.blue(`📁 Generando flujo de negocio completo para ${serviceName} en: ${basePath}`));
const serviceNameLower = serviceName.toLowerCase();
const apiPrefix = '';
const paths = {
// DTOs para business
domainModels: path.join(basePath, `${apiPrefix}domain/models/apis/${targetApiName}/business/${serviceNameLower}`),
// Repositories para business
domainRepositories: path.join(basePath, `${apiPrefix}domain/services/repositories/apis/${targetApiName}/business`),
// Use Cases para business
domainUseCases: path.join(basePath, `${apiPrefix}domain/services/use_cases/apis/${targetApiName}/business/${serviceNameLower}`),
// Entities para business
infraEntities: path.join(basePath, `${apiPrefix}infrastructure/entities/apis/${targetApiName}/business/${serviceNameLower}`),
// Mappers para business
infraMappers: path.join(basePath, `${apiPrefix}infrastructure/mappers/apis/${targetApiName}/business/${serviceNameLower}`),
// Injection para mappers de business (ubicación correcta)
injectionMappers: path.join(basePath, `${apiPrefix}infrastructure/mappers/apis/${targetApiName}/injection/business/${serviceNameLower}`),
// Facades para business
facades: path.join(basePath, `${apiPrefix}facade/apis/${targetApiName}/business`),
// Injection para business Use Cases
injectionUseCases: path.join(basePath, `${apiPrefix}domain/services/use_cases/apis/${targetApiName}/injection/business`),
// Injection para business Facades
injectionFacades: path.join(basePath, `${apiPrefix}facade/apis/${targetApiName}/injection/business`),
// Infrastructure repositories para business
infraRepositories: path.join(basePath, `${apiPrefix}infrastructure/repositories/apis/${targetApiName}/repositories/business/${serviceNameLower}`),
// Injection para infrastructure repositories de business
injectionRepositories: path.join(basePath, `${apiPrefix}infrastructure/repositories/apis/${targetApiName}/repositories/injection/business`)
};
try {
// Crear directorios necesarios
await createDirectoriesIfNotExists(paths);
// Generar DTOs por operación
await generateDomainDTOs(serviceName, paths, schema, targetApiName);
// Generar repository interfaces por servicio
await generateDomainRepositoryInterfaces(serviceName, paths, schema, targetApiName);
// Generar use cases por operación
await generateDomainUseCases(serviceName, paths, schema, targetApiName);
// Generar entities por operación
await generateInfrastructureEntities(serviceName, paths, schema, targetApiName);
// Generar mappers por operación
await generateInfrastructureMappers(serviceName, paths, schema, targetApiName);
// Generar injection files por operación (en lugar de por servicio)
await generateMapperInjectionPerOperation(serviceName, paths, schema, targetApiName);
// Generar repositories de implementación por operación
await generateInfrastructureRepositories(serviceName, paths, schema, targetApiName);
// Generar injection para infrastructure repositories
await generateRepositoryInjectionFiles(serviceName, paths, schema, targetApiName);
// Generar facades por servicio
await generateBusinessFacades(serviceName, paths, schema, targetApiName);
// Generar archivos de inyección
await generateBusinessInjectionFiles(serviceName, paths, schema, targetApiName);
console.log(chalk_1.default.green(`✨ Flujo de negocio ${serviceName} generado exitosamente!`));
}
catch (error) {
console.error(chalk_1.default.red('❌ Error generando archivos:'), error);
throw error;
}
}
async function generateInfrastructureEntities(serviceName, paths, schema, apiName = 'platform') {
// Solo generar por operaciones de negocio (nunca legacy)
if (schema?.businessOperations && schema.businessOperations.length > 0) {
let exportStatements = [];
for (const operation of schema.businessOperations) {
const rawOperationName = getOperationNameFromPath(operation.path, operation.operationId.toLowerCase());
const operationName = rawOperationName.replace(/_/g, '-');
const operationFolder = path.join(paths.infraEntities, operationName);
await fs.ensureDir(operationFolder);
// --- Request Entity ---
if (operation.fields && operation.fields.length > 0) {
const requestInterface = generateBusinessEntityInterface(serviceName, operation, 'request');
const requestFileName = `i-${serviceName.toLowerCase()}-${operationName}-request-entity.ts`;
await fs.writeFile(path.join(operationFolder, requestFileName), requestInterface);
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const requestInterfaceName = `I${toPascalCase(serviceName)}${cleanOperationName}RequestEntity`;
exportStatements.push(`export { ${requestInterfaceName} } from './${operationName}/${requestFileName.replace('.ts', '')}';`);
await generateNestedEntitiesForOperation(serviceName, operation, 'request', operationFolder, apiName, exportStatements, operationName);
}
// --- Response Entity ---
if (operation.responseFields && operation.responseFields.length > 0) {
const responseInterface = generateBusinessEntityInterface(serviceName, operation, 'response');
const responseFileName = `i-${serviceName.toLowerCase()}-${operationName}-response-entity.ts`;
await fs.writeFile(path.join(operationFolder, responseFileName), responseInterface);
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const responseInterfaceName = `I${toPascalCase(serviceName)}${cleanOperationName}ResponseEntity`;
exportStatements.push(`export { ${responseInterfaceName} } from './${operationName}/${responseFileName.replace('.ts', '')}';`);
await generateNestedEntitiesForOperation(serviceName, operation, 'response', operationFolder, apiName, exportStatements, operationName);
}
}
// Generar index.ts con export type (simplificado y mejor práctica) - INCREMENTAL
const indexPath = path.join(paths.infraEntities, 'index.ts');
const uniqueExports = new Set();
const finalExports = [];
// Leer exports existentes si el archivo ya existe
if (await fs.pathExists(indexPath)) {
try {
const existingContent = await fs.readFile(indexPath, 'utf-8');
const existingExports = existingContent.split('\n').filter(line => line.trim().startsWith('export'));
existingExports.forEach(statement => {
const match = statement.match(/export (?:type )?\{ (\w+) \}/);
if (match) {
const interfaceName = match[1];
if (!uniqueExports.has(interfaceName)) {
uniqueExports.add(interfaceName);
// Asegurar que sea export type
const typeStatement = statement.includes('export type {') ? statement : statement.replace('export {', 'export type {');
finalExports.push(typeStatement);
}
}
});
}
catch (error) {
console.log(chalk_1.default.yellow(`⚠️ No se pudo leer index.ts existente: ${error}`));
}
}
// Agregar nuevos exports
exportStatements.forEach(statement => {
const match = statement.match(/export \{ (\w+) \}/);
if (match) {
const interfaceName = match[1];
if (!uniqueExports.has(interfaceName)) {
uniqueExports.add(interfaceName);
const typeStatement = statement.replace('export {', 'export type {');
finalExports.push(typeStatement);
}
}
});
const indexContent = finalExports.sort().join('\n') + '\n';
await fs.writeFile(indexPath, indexContent);
console.log(chalk_1.default.green(`✅ Index Entity: index.ts ${await fs.pathExists(indexPath) ? '(actualizado)' : ''}`));
}
else {
// Si no hay operaciones de negocio, NO generar nada (nunca legacy)
console.log(chalk_1.default.yellow('⚠️ No se generaron entities porque no hay operaciones de negocio detectadas.'));
// Eliminar archivos legacy si existen
const legacyFiles = [
`i-${serviceName.toLowerCase()}-entity.ts`,
`i-${serviceName.toLowerCase()}-save-entity.ts`,
`i-${serviceName.toLowerCase()}-read-entity.ts`,
`i-${serviceName.toLowerCase()}-update-entity.ts`,
`i-${serviceName.toLowerCase()}-delete-entity.ts`,
'index.ts'
];
for (const file of legacyFiles) {
const filePath = path.join(paths.infraEntities, file);
if (await fs.pathExists(filePath)) {
await fs.remove(filePath);
}
}
}
}
async function generateInfrastructureMappers(serviceName, paths, schema, apiName = 'platform') {
// Solo generar por operaciones de negocio
if (schema?.businessOperations && schema.businessOperations.length > 0) {
let exportStatements = [];
for (const operation of schema.businessOperations) {
const rawOperationName = getOperationNameFromPath(operation.path, operation.operationId.toLowerCase());
const operationName = rawOperationName.replace(/_/g, '-');
const operationFolder = path.join(paths.infraMappers, operationName);
await fs.ensureDir(operationFolder);
// --- Request Mapper ---
if (operation.fields && operation.fields.length > 0) {
const requestMapper = generateBusinessMapper(serviceName, operation, 'request', apiName);
const requestFileName = `${serviceName.toLowerCase()}-${operationName}-request-mapper.ts`;
await fs.writeFile(path.join(operationFolder, requestFileName), requestMapper);
// Generar mappers para interfaces anidadas del request
await generateNestedMappersForOperation(serviceName, operation, 'request', operationFolder, apiName, exportStatements, operationName);
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const requestMapperName = `${toPascalCase(serviceName)}${cleanOperationName}RequestMapper`;
exportStatements.push(`export { ${requestMapperName} } from './${operationName}/${serviceName.toLowerCase()}-${operationName}-request-mapper';`);
}
// --- Response Mapper ---
if (operation.responseFields && operation.responseFields.length > 0) {
const responseMapper = generateBusinessMapper(serviceName, operation, 'response', apiName);
const responseFileName = `${serviceName.toLowerCase()}-${operationName}-response-mapper.ts`;
await fs.writeFile(path.join(operationFolder, responseFileName), responseMapper);
// Generar mappers para interfaces anidadas
await generateNestedMappersForOperation(serviceName, operation, 'response', operationFolder, apiName, exportStatements, operationName);
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const responseMapperName = `${toPascalCase(serviceName)}${cleanOperationName}ResponseMapper`;
exportStatements.push(`export { ${responseMapperName} } from './${operationName}/${serviceName.toLowerCase()}-${operationName}-response-mapper';`);
}
}
// Generar index.ts para mappers - INCREMENTAL
const indexPath = path.join(paths.infraMappers, 'index.ts');
const uniqueExports = new Set();
const finalExports = [];
// Leer exports existentes si el archivo ya existe
if (await fs.pathExists(indexPath)) {
try {
const existingContent = await fs.readFile(indexPath, 'utf-8');
const existingExports = existingContent.split('\n').filter(line => line.trim().startsWith('export'));
existingExports.forEach(statement => {
const match = statement.match(/export \{ (\w+) \}/);
if (match) {
const mapperName = match[1];
if (!uniqueExports.has(mapperName)) {
uniqueExports.add(mapperName);
finalExports.push(statement);
}
}
});
}
catch (error) {
console.log(chalk_1.default.yellow(`⚠️ No se pudo leer index.ts existente: ${error}`));
}
}
// Agregar nuevos exports
exportStatements.forEach(statement => {
const match = statement.match(/export \{ (\w+) \}/);
if (match) {
const mapperName = match[1];
if (!uniqueExports.has(mapperName)) {
uniqueExports.add(mapperName);
finalExports.push(statement);
}
}
});
const indexContent = finalExports.sort().join('\n') + '\n';
await fs.writeFile(indexPath, indexContent);
console.log(chalk_1.default.green(`✅ Index Mapper: index.ts ${await fs.pathExists(indexPath) ? '(actualizado)' : ''}`));
}
else {
console.log(chalk_1.default.yellow('⚠️ No se generaron mappers porque no hay operaciones de negocio detectadas.'));
}
}
async function generateDomainDTOs(serviceName, paths, schema, apiName = 'platform') {
// Solo generar por operaciones de negocio
if (schema?.businessOperations && schema.businessOperations.length > 0) {
let exportStatements = [];
for (const operation of schema.businessOperations) {
const rawOperationName = getOperationNameFromPath(operation.path, operation.operationId.toLowerCase());
const operationName = rawOperationName.replace(/_/g, '-');
const operationFolder = path.join(paths.domainModels, operationName);
await fs.ensureDir(operationFolder);
// --- Request DTO ---
if (operation.fields && operation.fields.length > 0) {
const requestDTO = generateBusinessDTO(serviceName, operation, 'request', apiName);
const requestFileName = `i-${serviceName.toLowerCase()}-${operationName}-request-dto.ts`;
await fs.writeFile(path.join(operationFolder, requestFileName), requestDTO);
// Generar DTOs para interfaces anidadas de request
await generateNestedDTOsForOperation(serviceName, operation, 'request', operationFolder, apiName, exportStatements, operationName);
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const requestDTOName = `I${toPascalCase(serviceName)}${cleanOperationName}RequestDTO`;
exportStatements.push(`export { ${requestDTOName} } from './${operationName}/${requestFileName.replace('.ts', '')}';`);
}
// --- Response DTO ---
if (operation.responseFields && operation.responseFields.length > 0) {
const responseDTO = generateBusinessDTO(serviceName, operation, 'response', apiName);
const responseFileName = `i-${serviceName.toLowerCase()}-${operationName}-response-dto.ts`;
await fs.writeFile(path.join(operationFolder, responseFileName), responseDTO);
// Generar DTOs para interfaces anidadas con patrón correcto
await generateNestedDTOsForOperation(serviceName, operation, 'response', operationFolder, apiName, exportStatements, operationName);
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const baseResponseDTOName = `I${toPascalCase(serviceName)}${cleanOperationName}ResponseDTO`;
const responseDTOName = operation.isResponseArray ? `${baseResponseDTOName}[]` : baseResponseDTOName;
exportStatements.push(`export { ${baseResponseDTOName} } from './${operationName}/${responseFileName.replace('.ts', '')}';`);
}
}
// Generar index.ts para DTOs con export type (simplificado y mejor práctica) - INCREMENTAL
const indexPath = path.join(paths.domainModels, 'index.ts');
const uniqueExports = new Set();
const finalExports = [];
// Leer exports existentes si el archivo ya existe
if (await fs.pathExists(indexPath)) {
try {
const existingContent = await fs.readFile(indexPath, 'utf-8');
const existingExports = existingContent.split('\n').filter(line => line.trim().startsWith('export'));
existingExports.forEach(statement => {
const match = statement.match(/export (?:type )?\{ (\w+) \}/);
if (match) {
const interfaceName = match[1];
if (!uniqueExports.has(interfaceName)) {
uniqueExports.add(interfaceName);
// Asegurar que sea export type
const typeStatement = statement.includes('export type {') ? statement : statement.replace('export {', 'export type {');
finalExports.push(typeStatement);
}
}
});
}
catch (error) {
console.log(chalk_1.default.yellow(`⚠️ No se pudo leer index.ts existente: ${error}`));
}
}
// Agregar nuevos exports
exportStatements.forEach(statement => {
const match = statement.match(/export \{ (\w+) \}/);
if (match) {
const interfaceName = match[1];
if (!uniqueExports.has(interfaceName)) {
uniqueExports.add(interfaceName);
const typeStatement = statement.replace('export {', 'export type {');
finalExports.push(typeStatement);
}
}
});
const indexContent = finalExports.sort().join('\n') + '\n';
await fs.writeFile(indexPath, indexContent);
console.log(chalk_1.default.green(`✅ Index DTO: index.ts ${await fs.pathExists(indexPath) ? '(actualizado)' : ''}`));
}
else {
console.log(chalk_1.default.yellow('⚠️ No se generaron DTOs porque no hay operaciones de negocio detectadas.'));
}
}
async function generateNestedMappersForOperation(serviceName, operation, type, operationFolder, apiName, exportStatements, operationName) {
const fields = type === 'request' ? operation.fields : operation.responseFields;
const generated = new Set();
function processNestedFields(field) {
// Solo generar mappers para interfaces, NO para enums (los enums se mapean directamente)
if (field.type && !['string', 'number', 'boolean', 'any', 'object', 'array'].includes(field.type) && !field.isEnum && !generated.has(field.type)) {
generated.add(field.type);
// Generar mapper individual para cada interface anidada
const nestedMapper = generateIndividualNestedMapper(field.type, field, apiName, serviceName, operationName, type);
// Aplicar el patrón correcto: <flujo>-<proceso>-<tipo>-<request/response>-mapper.ts
const serviceNameKebab = serviceName.toLowerCase();
const operationKebab = operationName.replace(/_/g, '-');
// Limpiar el tipo para obtener solo el nombre base sin sufijos Login/Response
// pero preservando Response/Request cuando es la operación actual
let cleanType = field.type;
const currentOperationClean = operationName.replace(/_/g, '').toLowerCase();
// Solo remover "Login" si no estamos en la operación login
if (currentOperationClean !== 'login') {
cleanType = cleanType.replace(/LoginResponse$/, ''); // CompanyLoginResponse -> CompanyResponse
cleanType = cleanType.replace(/LoginRequest$/, ''); // CompanyLoginRequest -> CompanyRequest
cleanType = cleanType.replace(/Login$/, ''); // CompanyLogin -> Company
}
// Solo remover Response/Request si el tipo ya los incluye redundantemente
if (type === 'response' && cleanType.endsWith('Response')) {
// No hacer nada - mantener Response
}
else if (type === 'request' && cleanType.endsWith('Request')) {
// No hacer nada - mantener Request
}
else {
// Remover sufijos generales solo si no coinciden con el tipo actual
cleanType = cleanType.replace(/Response$/, '');
cleanType = cleanType.replace(/Request$/, '');
}
let typeKebab = cleanType.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '').replace(/[\[\]]/g, 'array');
const fileSuffix = type === 'request' ? 'request' : 'response';
// Detectar y evitar duplicación de operaciones en el nombre del tipo para archivos
const operationInFileName = operationKebab.toLowerCase();
if (typeKebab.includes(operationInFileName)) {
// Eliminar la operación del tipo: user-login-response -> user-response
typeKebab = typeKebab.replace(new RegExp(`-${operationInFileName}`, 'gi'), '');
}
// Si el tipo ya termina en request o response, no duplicar
const needsSuffix = !field.type.toLowerCase().endsWith('response') && !field.type.toLowerCase().endsWith('request');
const nestedFileName = needsSuffix
? `${serviceNameKebab}-${operationKebab}-${typeKebab}-${fileSuffix}-mapper.ts`
: `${serviceNameKebab}-${operationKebab}-${typeKebab}-mapper.ts`;
fs.writeFileSync(path.join(operationFolder, nestedFileName), nestedMapper);
const formattedFieldType = toPascalCase(field.type);
const formattedServiceName = toPascalCase(serviceName);
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const classSuffix = type === 'request' ? 'Request' : 'Response';
// Aplicar la misma lógica de limpieza contextual que en generateIndividualNestedMapper
let finalFieldType = formattedFieldType;
const currentOperationCleanForExport = operationName.replace(/_/g, '').toLowerCase();
// Solo remover "Login" si no estamos en la operación login
if (currentOperationCleanForExport !== 'login') {
finalFieldType = finalFieldType.replace(/LoginResponse$/, ''); // CompanyLoginResponse -> CompanyResponse
finalFieldType = finalFieldType.replace(/LoginRequest$/, ''); // CompanyLoginRequest -> CompanyRequest
finalFieldType = finalFieldType.replace(/Login$/, ''); // CompanyLogin -> Company
}
// Solo remover Response/Request si el tipo ya los incluye redundantemente
if (type === 'response' && finalFieldType.endsWith('Response')) {
// No hacer nada - mantener Response
}
else if (type === 'request' && finalFieldType.endsWith('Request')) {
// No hacer nada - mantener Request
}
else {
// Remover sufijos generales solo si no coinciden con el tipo actual
finalFieldType = finalFieldType.replace(/Response$/, '');
finalFieldType = finalFieldType.replace(/Request$/, '');
}
// Detectar y evitar duplicación de operaciones en el nombre del tipo
const operationInTypeName = cleanOperationName.toLowerCase();
const typeNameLower = finalFieldType.toLowerCase();
// Si el tipo incluye el nombre de la operación, removerlo para evitar duplicación
if (typeNameLower.includes(operationInTypeName)) {
finalFieldType = finalFieldType.replace(new RegExp(cleanOperationName, 'gi'), '');
}
// Determinar si necesita sufijo después de la limpieza
const finalNeedsSuffix = !finalFieldType.endsWith('Response') && !finalFieldType.endsWith('Request');
// Generar nombre con patrón correcto: <Flujo><Proceso><Tipo><Request/Response>Mapper
const mapperClassName = finalNeedsSuffix
? `${formattedServiceName}${cleanOperationName}${finalFieldType}${classSuffix}Mapper`
: `${formattedServiceName}${cleanOperationName}${finalFieldType}Mapper`;
exportStatements.push(`export { ${mapperClassName} } from './${operationName}/${nestedFileName.replace('.ts', '')}';`);
// Procesar campos anidados recursivamente
if (field.nestedFields && field.nestedFields.length > 0) {
field.nestedFields.forEach(processNestedFields);
}
}
}
if (fields && fields.length > 0) {
fields.forEach(processNestedFields);
}
}
async function createDirectoriesIfNotExists(paths) {
for (const [key, dir] of Object.entries(paths)) {
if (typeof dir === 'string') {
console.log(chalk_1.default.cyan(`📂 Directorio creado/verificado: ${dir}`));
await fs.ensureDir(dir);
}
}
}
// Funciones auxiliares copiadas del generador original
function getDefaultFields() {
return [
{ name: 'name', type: 'string', required: true },
{ name: 'state', type: 'boolean', required: false }
];
}
function toPascalCase(str) {
return str
.replace(/[\[\]]/g, 'Array') // ← Fix: Convert brackets to 'Array' before PascalCase conversion
.replace(/(^|_|-)(\w)/g, (_, __, c) => c ? c.toUpperCase() : '')
.replace(/Dto$/i, 'Entity')
.replace(/EntityEntity$/, 'Entity');
}
function toScreamingSnakeCase(str) {
return str.replace(/([A-Z])/g, '_$1').toUpperCase().replace(/^_/, '');
}
function toSnakeCase(str) {
return str
.replace(/([A-Z])/g, '_$1')
.replace(/^_/, '')
.toLowerCase();
}
function generateBusinessEntityInterface(serviceName, operation, type) {
const operationName = getOperationNameFromPath(operation.path, type);
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const interfaceName = `I${toPascalCase(serviceName)}${cleanOperationName}${type === 'request' ? 'Request' : 'Response'}Entity`;
const fields = type === 'request' ? operation.fields : operation.responseFields;
let imports = [];
let content = '';
if (fields && fields.length > 0) {
fields.forEach((field) => {
const optionalMark = field.required ? '' : '?';
const arrayMark = field.isArray ? '[]' : '';
// Mantener los nombres de campos tal como vienen del swagger (snake_case)
const fieldName = field.name;
let fieldType = field.type;
if (fieldType && !['string', 'number', 'boolean', 'any', 'object', 'array'].includes(fieldType)) {
const typeName = toPascalCase(fieldType);
const suffix = type === 'request' ? 'Request' : 'Response';
// Usar patrón completo: I<Flujo><Proceso><Tipo><Request/Response>Entity
// Limpiar el tipo para obtener solo el nombre base sin sufijos
let cleanTypeName = typeName;
cleanTypeName = cleanTypeName.replace(/LoginResponse$/, '');
cleanTypeName = cleanTypeName.replace(/LoginRequest$/, '');
cleanTypeName = cleanTypeName.replace(/Login$/, '');
cleanTypeName = cleanTypeName.replace(/Response$/, '');
cleanTypeName = cleanTypeName.replace(/Request$/, '');
// Limpiar corchetes para nombres de interfaces
cleanTypeName = cleanTypeName.replace(/[\[\]]/g, 'Array');
fieldType = `I${toPascalCase(serviceName)}${cleanOperationName}${cleanTypeName}${suffix}Entity`;
// Generar nombre de archivo con patrón correcto: i-<flujo>-<proceso>-<tipo>-<request/response>-entity
// Limpiar el tipo para obtener solo el nombre base sin sufijos Login/Response
let cleanType = field.type;
// Remover múltiples sufijos en orden específico
cleanType = cleanType.replace(/LoginResponse$/, ''); // CompanyLoginResponse -> Company
cleanType = cleanType.replace(/LoginRequest$/, ''); // CompanyLoginRequest -> Company
cleanType = cleanType.replace(/Login$/, ''); // CompanyLogin -> Company
cleanType = cleanType.replace(/Response$/, ''); // CompanyResponse -> Company
cleanType = cleanType.replace(/Request$/, ''); // CompanyRequest -> Company
const baseFileName = cleanType.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '').replace(/[\[\]]/g, 'array');
const serviceNameKebab = serviceName.toLowerCase().replace(/([A-Z])/g, '-$1').replace(/^-/, '');
const operationKebab = operationName.replace(/_/g, '-');
const importFileName = `i-${serviceNameKebab}-${operationKebab}-${baseFileName}-${type}-entity`;
imports.push(`import { ${fieldType} } from "./${importFileName}";`);
}
content += ` ${fieldName}${optionalMark}: ${fieldType}${arrayMark};\n`;
});
}
else {
content += ` // Define los campos del ${type} aquí\n`;
}
const importsSection = imports.length > 0 ? imports.join('\n') + '\n\n' : '';
return `${importsSection}export interface ${interfaceName} {
${content}}`;
}
function convertToCamelCase(str) {
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}
function getFieldOptionalStatus(field, type) {
if (type === 'save') {
return !field.required;
}
if (type === 'update') {
return true; // En update, todos los campos son opcionales excepto ID
}
return !field.required;
}
function getTypeScriptType(field) {
switch (field.type) {
case 'string':
return 'string';
case 'number':
case 'integer':
return 'number';
case 'boolean':
return 'boolean';
case 'array':
return 'any[]';
case 'object':
return 'any';
default:
return 'string';
}
}
function generateBusinessMapper(serviceName, operation, type, apiName = 'platform') {
const operationName = getOperationNameFromPath(operation.path, type);
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const serviceNameLower = serviceName.toLowerCase();
const dtoInterfaceName = `I${toPascalCase(serviceName)}${cleanOperationName}${type === 'request' ? 'Request' : 'Response'}DTO`;
const entityInterfaceName = `I${toPascalCase(serviceName)}${cleanOperationName}${type === 'request' ? 'Request' : 'Response'}Entity`;
const mapperClassName = `${toPascalCase(serviceName)}${cleanOperationName}${type === 'request' ? 'Request' : 'Response'}Mapper`;
const fields = type === 'request' ? operation.fields : operation.responseFields;
// Generar campos para mapFrom (Entity -> DTO)
let mapFromFields = '';
// Generar campos para mapTo (DTO -> Entity)
let mapToFields = '';
// Agregar imports e instancias de mappers anidados
let nestedMapperImports = '';
let nestedMapperInstances = '';
if (fields && fields.length > 0) {
const nestedMappers = [];
const nestedMapperReferences = {}; // Para evitar duplicados
const mapFromMappings = fields.map((field) => {
const dtoFieldName = convertToCamelCase(field.name); // DTO usa camelCase
const entityFieldName = field.name; // Entity usa nombres del swagger (snake_case)
// Si es un tipo complejo, usar mapper específico
if (field.type && !['string', 'number', 'boolean', 'any', 'object', 'array'].includes(field.type)) {
const formattedFieldType = toPascalCase(field.type);
const suffix = type === 'request' ? 'Request' : 'Response';
const mapperClassName = formattedFieldType.endsWith('Response') || formattedFieldType.endsWith('Request')
? `${formattedFieldType}Mapper`
: `${formattedFieldType}${suffix}Mapper`;
// Crear un nombre de variable único sin duplicaciones usando camelCase correcto
const cleanFieldType = formattedFieldType.endsWith('Response') || formattedFieldType.endsWith('Request')
? formattedFieldType.replace(/Response$|Request$/, '')
: formattedFieldType;
const nestedMapperName = `${cleanFieldType.charAt(0).toLowerCase() + cleanFieldType.slice(1)}${suffix.charAt(0).toLowerCase() + suffix.slice(1)}Mapper`;
// Generar nombre de método abreviado (sin prefijo de servicio y operación)
const formattedServiceName = toPascalCase(serviceName);
const methodName = mapperClassName.replace(new RegExp(`^${formattedServiceName}${cleanOperationName}`, ''), '');
// Solo agregar si no existe ya
if (!nestedMapperReferences[nestedMapperName]) {
nestedMapperReferences[nestedMapperName] = mapperClassName;
nestedMappers.push(` private ${nestedMapperName} = Injection${toPascalCase(apiName)}Business${toPascalCase(serviceName)}${cleanOperationName}Mapper.${methodName}()`);
}
if (field.isArray) {
return ` ${dtoFieldName}: this.${nestedMapperName}.mapFromList(param.${entityFieldName} ?? [])`;
}
else {
return ` ${dtoFieldName}: this.${nestedMapperName}.mapFrom(param.${entityFieldName})`;
}
}
else {
return ` ${dtoFieldName}: param.${entityFieldName}`;
}
});
const mapToMappings = fields.map((field) => {
const dtoFieldName = convertToCamelCase(field.name); // DTO usa camelCase
const entityFieldName = field.name; // Entity usa nombres del swagger (snake_case)
// Si es un tipo complejo, usar mapper específico
if (field.type && !['string', 'number', 'boolean', 'any', 'object', 'array'].includes(field.type)) {
const formattedFieldType = toPascalCase(field.type);
const suffix = type === 'request' ? 'Request' : 'Response';
const cleanFieldType = formattedFieldType.endsWith('Response') || formattedFieldType.endsWith('Request')
? formattedFieldType.replace(/Response$|Request$/, '')
: formattedFieldType;
const nestedMapperName = `${cleanFieldType.charAt(0).toLowerCase() + cleanFieldType.slice(1)}${suffix.charAt(0).toLowerCase() + suffix.slice(1)}Mapper`;
if (field.isArray) {
return ` ${entityFieldName}: this.${nestedMapperName}.mapToList(param.${dtoFieldName} ?? [])`;
}
else {
return ` ${entityFieldName}: this.${nestedMapperName}.mapTo(param.${dtoFieldName})`;
}
}
else {
return ` ${entityFieldName}: param.${dtoFieldName}`;
}
});
// Agregar imports e instancias de mappers anidados si existen
if (nestedMappers.length > 0) {
nestedMapperImports = `import { Injection${toPascalCase(apiName)}Business${toPascalCase(serviceName)}${cleanOperationName}Mapper } from "@${apiName}/infrastructure/mappers/apis/${apiName}/injection/business/${serviceNameLower}/injection-${apiName}-business-${serviceNameLower}-${operationName.replace(/_/g, '-')}-mapper";\n`;
nestedMapperInstances = nestedMappers.join('\n');
}
mapFromFields = mapFromMappings.join(',\n');
mapToFields = mapToMappings.join(',\n');
}
return `import { Mapper } from "@core/classes";
import { ${dtoInterfaceName} } from "@${apiName}/domain/models/apis/${apiName}/business/${serviceNameLower}";
import { ${entityInterfaceName} } from "@${apiName}/infrastructure/entities/apis/${apiName}/business/${serviceNameLower}";
${nestedMapperImports}
export class ${mapperClassName} extends Mapper<${entityInterfaceName}, ${dtoInterfaceName}> {
private static instance: ${mapperClassName};
${nestedMapperInstances}
public constructor() { super(); }
public static getInstance(): ${mapperClassName} {
if (!${mapperClassName}.instance)
${mapperClassName}.instance = new ${mapperClassName}();
return ${mapperClassName}.instance;
}
public mapFrom(param: ${entityInterfaceName}): ${dtoInterfaceName} {
return {
${mapFromFields}
}
}
public mapFromList(params: ${entityInterfaceName}[]): ${dtoInterfaceName}[] {
return params.map((param: ${entityInterfaceName}) => {
return this.mapFrom(param)
})
}
public mapTo(param: ${dtoInterfaceName}): ${entityInterfaceName} {
return {
${mapToFields}
}
}
public mapToList(params: ${dtoInterfaceName}[]): ${entityInterfaceName}[] {
return params.map((param: ${dtoInterfaceName}) => {
return this.mapTo(param);
})
}
}`;
}
function generateIndividualNestedMapper(typeName, field, apiName, serviceName, operationName, type = 'response') {
const serviceNameLower = serviceName.toLowerCase();
const formattedTypeName = toPascalCase(typeName);
const formattedServiceName = toPascalCase(serviceName);
const suffix = type === 'request' ? 'Request' : 'Response';
// Crear cleanOperationName aquí también
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
// Aplicar el patrón correcto: <Flujo><Proceso><Tipo><Request/Response><DTO/Entity/Mapper>
// Determinar si necesita sufijo basado en el tipo original
const needsSuffix = !formattedTypeName.endsWith('Response') && !formattedTypeName.endsWith('Request');
// Limpiar el tipo para obtener solo el nombre base sin sufijos Login/Response
// pero preservando Response/Request cuando es la operación actual
let cleanTypeName = formattedTypeName;
const currentOperationClean = operationName.replace(/_/g, '').toLowerCase();
// Solo remover "Login" si no estamos en la operación login
if (currentOperationClean !== 'login') {
cleanTypeName = cleanTypeName.replace(/LoginResponse$/, ''); // CompanyLoginResponse -> CompanyResponse
cleanTypeName = cleanTypeName.replace(/LoginRequest$/, ''); // CompanyLoginRequest -> CompanyRequest
cleanTypeName = cleanTypeName.replace(/Login$/, ''); // CompanyLogin -> Company
}
// Solo remover Response/Request si el tipo ya los incluye redundantemente
if (type === 'response' && cleanTypeName.endsWith('Response')) {
// No hacer nada - mantener Response
}
else if (type === 'request' && cleanTypeName.endsWith('Request')) {
// No hacer nada - mantener Request
}
else {
// Remover sufijos generales solo si no coinciden con el tipo actual
cleanTypeName = cleanTypeName.replace(/Response$/, '');
cleanTypeName = cleanTypeName.replace(/Request$/, '');
}
// Limpiar corchetes para nombres de interfaces
cleanTypeName = cleanTypeName.replace(/[\[\]]/g, 'Array');
// Detectar y evitar duplicación de operaciones en el nombre del tipo
// Ejemplo: UserRefreshToken -> User (cuando operationName es "refresh-token")
const operationInTypeName = cleanOperationName.toLowerCase();
const typeNameLower = cleanTypeName.toLowerCase();
// Si el tipo incluye el nombre de la operación, removerlo para evitar duplicación
if (typeNameLower.includes(operationInTypeName)) {
// Eliminar la operación del tipo: UserRefreshToken -> User
cleanTypeName = cleanTypeName.replace(new RegExp(cleanOperationName, 'gi'), '');
}
// Determinar si necesita sufijo después de la limpieza
const finalNeedsSuffix = !cleanTypeName.endsWith('Response') && !cleanTypeName.endsWith('Request');
const dtoInterfaceName = finalNeedsSuffix
? `I${formattedServiceName}${cleanOperationName}${cleanTypeName}${suffix}DTO`
: `I${formattedServiceName}${cleanOperationName}${cleanTypeName}DTO`;
const entityInterfaceName = finalNeedsSuffix
? `I${formattedServiceName}${cleanOperationName}${cleanTypeName}${suffix}Entity`
: `I${formattedServiceName}${cleanOperationName}${cleanTypeName}Entity`;
const mapperClassName = finalNeedsSuffix
? `${formattedServiceName}${cleanOperationName}${cleanTypeName}${suffix}Mapper`
: `${formattedServiceName}${cleanOperationName}${cleanTypeName}Mapper`;
let mapFromFields = '';
let mapToFields = '';
let nestedMapperImports = '';
let nestedMapperInstances = '';
if (field.nestedFields && field.nestedFields.length > 0) {
const nestedMappers = [];
const nestedMapperReferences = {}; // Para evitar duplicados
const mapFromMappings = field.nestedFields.map((nestedField) => {
const dtoFieldName = convertToCamelCase(nestedField.name);
const entityFieldName = nestedField.name;
if (nestedField.type && !['string', 'number', 'boolean', 'any', 'object', 'array'].includes(nestedField.type) && !nestedField.isEnum) {
const nestedFieldTypeName = toPascalCase(nestedField.type);
const nestedSuffix = type === 'request' ? 'Request' : 'Response';
// Para mappers anidados, usar lógica simplificada más coherente con los archivos generados
// Siempre remover sufijos duplicados y mantener solo el nombre base + sufijo correspondiente al tipo
let cleanNestedFieldType = nestedFieldTypeName;
// Remover sufijos redundantes para obtener nombre base limpio
cleanNestedFieldType = cleanNestedFieldType.replace(/LoginResponse$|LoginRequest$|Response$|Request$/, '');
// Generar nombre de clase de mapper basado en las clases reales que se generan
const nestedMapperClassName = `${toPascalCase(serviceName)}${cleanOperationName}${cleanNestedFieldType}${nestedSuffix}Mapper`;
// Crear un nombre de variable único sin duplicaciones usando camelCase correcto
// Para la variable, extraer solo el nombre base sin sufijos y agregar ResponseMapper/RequestMapper
let variableBaseName = nestedFieldTypeName;
// Remover todos los sufijos para obtener el nombre base limpio
variableBaseName = variableBaseName.replace(/LoginResponse$|LoginRequest$|Login$|Response$|Request$/, '');
const nestedMapperName = `${variableBaseName.charAt(0).toLowerCase() + variableBaseName.slice(1)}${nestedSuffix.charAt(0).toLowerCase() + nestedSuffix.slice(1)}Mapper`;
// Generar nombre de método abreviado (sin prefijo de servicio y operación)
const formattedServiceName = toPascalCase(serviceName);
const methodName = nestedMapperClassName.replace(new RegExp(`^${formattedServiceName}${cleanOperationName}`, ''), '');
// Solo agregar si no existe ya
if (!nestedMapperReferences[nestedMapperName]) {
nestedMapperReferences[nestedMapperName] = nestedMapperClassName;
nestedMappers.push(` private ${nestedMapperName} = Injection${toPascalCase(apiName)}Business${toPascalCase(serviceName)}${cleanOperationName}Mapper.${methodName}()`);
}
if (nestedField.isArray) {
return ` ${dtoFieldName}: this.${nestedMapperName}.mapFromList(param.${entityFieldName} ?? [])`;
}
else {
return ` ${dtoFieldName}: this.${nestedMapperName}.mapFrom(param.${entityFieldName})`;
}
}
else {
return ` ${dtoFieldName}: param.${entityFieldName}`;
}
});
const mapToMappings = field.nestedFields.map((nestedField) => {
const dtoFieldName = convertToCamelCase(nestedField.name);
const entityFieldName = nestedField.name;
if (nestedField.type && !['string', 'number', 'boolean', 'any', 'object', 'array'].includes(nestedField.type) && !nestedField.isEnum) {
// Usar el mismo nombre de variable que en mapFromMappings aplicando la misma lógica contextual
const nestedFieldTypeName = toPascalCase(nestedField.type);
const nestedSuffix = type === 'request' ? 'Request' : 'Response';
// Para mappers anidados, usar la misma lógica simplificada
let cleanNestedFieldType = nestedFieldTypeName;
// Remover sufijos redundantes para obtener nombre base limpio
cleanNestedFieldType = cleanNestedFieldType.replace(/LoginResponse$|LoginRequest$|Response$|Request$/, '');
// Para la variable, extraer solo el nombre base sin sufijos y agregar ResponseMapper/RequestMapper
let variableBaseName = nestedFieldTypeName;
// Remover todos los sufijos para obtener el nombre base limpio
variableBaseName = variableBaseName.replace(/LoginResponse$|LoginRequest$|Login$|Response$|Request$/, '');
const nestedMapperName = `${variableBaseName.charAt(0).toLowerCase() + variableBaseName.slice(1)}${nestedSuffix.charAt(0).toLowerCase() + nestedSuffix.slice(1)}Mapper`;
if (nestedField.isArray) {
return ` ${entityFieldName}: this.${nestedMapperName}.mapToList(param.${dtoFieldName} ?? [])`;
}
else {
return ` ${entityFieldName}: this.${nestedMapperName}.mapTo(param.${dtoFieldName})`;
}
}
else {
return ` ${entityFieldName}: param.${dtoFieldName}`;
}
});
if (nestedMappers.length > 0) {
nestedMapperImports = `import { Injection${toPascalCase(apiName)}Business${toPascalCase(serviceName)}${cleanOperationName}Mapper } from "@${apiName}/infrastructure/mappers/apis/${apiName}/injection/business/${serviceNameLower}/injection-${apiName}-business-${serviceNameLower}-${operationName.replace(/_/g, '-')}-mapper";\n`;
nestedMapperInstances = nestedMappers.join('\n');
}
mapFromFields = mapFromMappings.join(',\n');
mapToFields = mapToMappings.join(',\n');
}
return `import { Mapper } from "@core/classes";
import { ${dtoInterfaceName} } from "@${apiName}/domain/models/apis/${apiName}/business/${serviceNameLower}";
import { ${entityInterfaceName} } from "@${apiName}/infrastructure/entities/apis/${apiName}/business/${serviceNameLower}";
${nestedMapperImports}
export class ${mapperClassName} extends Mapper<${entityInterfaceName}, ${dtoInterfaceName}> {
private static instance: ${mapperClassName};
${nestedMapperInstances}
public constructor() { super(); }
public static getInstance(): ${mapperClassName} {
if (!${mapperClassName}.instance)
${mapperClassName}.instance = new ${mapperClassName}();
return ${mapperClassName}.instance;
}
public mapFrom(param: ${entityInterfaceName}): ${dtoInterfaceName} {
return {
${mapFromFields}
}
}
public mapFromList(params: ${entityInterfaceName}[]): ${dtoInterfaceName}[] {
return params.map((param: ${entityInterfaceName}) => {
return this.mapFrom(param)
})
}
public mapTo(param: ${dtoInterfaceName}): ${entityInterfaceName} {
return {
${mapToFields}
}
}
public mapToList(params: ${dtoInterfaceName}[]): ${entityInterfaceName}[] {
return params.map((param: ${dtoInterfaceName}) => {
return this.mapTo(param);
})
}
}`;
}
async function generateNestedDTOsForOperation(serviceName, operation, type, operationFolder, apiName, exportStatements, operationName) {
const fields = type === 'request' ? operation.fields : operation.responseFields;
const generated = new Set();
function processNestedFields(field) {
if (field.type && !['string', 'number', 'boolean', 'any', 'object', 'array'].includes(field.type) && !generated.has(field.type)) {
generated.add(field.type);
let nestedDTO;
// Si es un enum, generar archivo de enum en lugar de interfaz
if (field.isEnum && field.enumValues) {
nestedDTO = generateIndividualEnum(field.type, field, apiName, serviceName, operationName, type);
}
else {
// Generar DTO individual para cada interface anidada con patrón correcto
nestedDTO = generateIndividualNestedDTO(field.type, field, apiName, serviceName, operationName, type);
}
// Patrón correcto: i-<flujo>-<proceso>-<tipo>-<request/response>-dto.ts
// Limpiar el tipo para obtener solo el nombre base sin sufijos Login/Response
let cleanType = field.type;
// Remover múltiples sufijos en orden específico
cleanType = cleanType.replace(/LoginResponse$/, ''); // CompanyLoginResponse -> Company
cleanType = cleanType.replace(/LoginRequest$/, ''); // CompanyLoginRequest -> Company
cleanType = cleanType.replace(/Login$/, ''); // CompanyLogin -> Company
cleanType = cleanType.replace(/Response$/, ''); // CompanyResponse -> Company
cleanType = cleanType.replace(/Request$/, ''); // CompanyRequest -> Company
const baseFileName = cleanType.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '').replace(/[\[\]]/g, 'array');
const serviceNameKebab = serviceName.toLowerCase().replace(/([A-Z])/g, '-$1').replace(/^-/, '');
const operationKebab = operationName.replace(/_/g, '-');
let nestedFileName;
if (field.isEnum && field.enumValues) {
// Para enums, usar el patrón: <flujo>-<proceso>-<tipo>-<request/response>-dto.ts
nestedFileName = `${serviceNameKebab}-${operationKebab}-${baseFileName}-${type}-dto.ts`;
}
else {
// Para interfaces, usar el patrón normal
nestedFileName = `i-${serviceNameKebab}-${operationKebab}-${baseFileName}-${type}-dto.ts`;
}
fs.writeFileSync(path.join(operationFolder, nestedFileName), nestedDTO);
// Generar nombre de clase que coincida con el patrón del archivo
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
// Limpiar el tipo para obtener solo el nombre base
let cleanFieldType = field.type;
cleanFieldType = cleanFieldType.replace(/LoginResponse$/, '');
cleanFieldType = cleanFieldType.replace(/LoginRequest$/, '');
cleanFieldType = cleanFieldType.replace(/Login$/, '');
cleanFieldType = cleanFieldType.replace(/Response$/, '');
cleanFieldType = cleanFieldType.replace(/Request$/, '');
const formattedFieldType = toPascalCase(cleanFieldType);
let exportClassName;
if (field.isEnum && field.enumValues) {
// Para enums, usar el nombre del enum en SCREAMING_SNAKE_CASE (evitar duplicar "Enum")
const enumSuffix = formattedFieldType.endsWith('Enum') ? '' : 'Enum';
const pascalCaseName = `${toPascalCase(serviceName)}${cleanOperationName}${formattedFieldType}${enumSuffix}`;
exportClassName = toScreamingSnakeCase(pascalCaseName);
}
else {
// Para interfaces, usar el patrón normal
const suffix = type === 'request' ? 'Request' : 'Response';
exportClassName = `I${toPascalCase(serviceName)}${cleanOperationName}${formattedFieldType}${suffix}DTO`;
}
// Export statement con el nombre correcto del archivo
const fileNameForExport = nestedFileName.replace('.ts', '');
exportStatements.push(`export { ${exportClassName} } from './${operationName}/${fileNameForExport}';`);
// Procesar campos anidados recursivamente
if (field.nestedFields && field.nestedFields.length > 0) {
field.nestedFields.forEach(processNestedFields);
}
}
}
if (fields && fields.length > 0) {
fields.forEach(processNestedFields);
}
}
function generateBusinessDTO(serviceName, operation, type, apiName = 'platform') {
const operationName = getOperationNameFromPath(operation.path, type);
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const dtoInterfaceName = `I${toPascalCase(serviceName)}${cleanOperationName}${type === 'request' ? 'Request' : 'Response'}DTO`;
const fields = type === 'request' ? operation.fields : operation.responseFields;
let dtoFields = '';
let imports = [];
if (fields && fields.length > 0) {
const fieldMappings = fields.map((field) => {
const fieldName = convertToCamelCase(field.name); // DTO usa camelCase
const optionalMark = field.required ? '' : '?';
const arrayMark = field.isArray ? '[]' : '';
// Si es un tipo complejo, usar el tipo con el sufijo DTO
if (field.type && !['string', 'number', 'boolean', 'any', 'object', 'array'].includes(field.type)) {
const typeName = toPascalCase(field.type);
const suffix = type === 'request' ? 'Request' : 'Response';
// Usar patrón completo: I<Flujo><Proceso><Tipo><Request/Response>DTO
// Limpiar el tipo para obtener solo el nombre base sin sufijos
let cleanTypeName = typeName;
cleanTypeName = cleanTypeName.replace(/LoginResponse$/, '');
cleanTypeName = cleanTypeName.replace(/LoginRequest$/, '');
cleanTypeName = cleanTypeName.replace(/Login$/, '');
cleanTypeName = cleanTypeName.replace(/Response$/, '');
cleanTypeName = cleanTypeName.replace(/Request$/, '');
// Limpiar corchetes para nombres de interfaces
cleanTypeName = cleanTypeName.replace(/[\[\]]/g, 'Array');
const fieldType = `I${toPascalCase(serviceName)}${cleanOperationName}${cleanTypeName}${suffix}DTO`;
// Generar nombre de archivo con patrón correcto: i-<flujo>-<proceso>-<tipo>-<request/response>-dto
// Limpiar el tipo para obtener solo el nombre base sin sufijos Login/Response
let cleanType = field.type;
// Remover múltiples sufijos en orden específico
cleanType = cleanType.replace(/LoginResponse$/, ''); // CompanyLoginResponse -> Company
cleanType = cleanType.replace(/LoginRequest$/, ''); // CompanyLoginRequest -> Company
cleanType = cleanType.replace(/Login$/, ''); // CompanyLogin -> Company
cleanType = cleanType.replace(/Response$/, ''); // CompanyResponse -> Company
cleanType = cleanType.replace(/Request$/, ''); // CompanyRequest -> Company
const baseFileName = cleanType.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '').replace(/[\[\]]/g, 'array');
const serviceNameKebab = serviceName.toLowerCase().replace(/([A-Z])/g, '-$1').replace(/^-/, '');
const operationKebab = operationName.replace(/_/g, '-');
const importFileName = `i-${serviceNameKebab}-${operationKebab}-${baseFileName}-${type}-dto`;
imports.push(`import { ${fieldType} } from "./${importFileName}";`);
return ` ${fieldName}${optionalMark}: ${fieldType}${arrayMark};`;
}
else {
return ` ${fieldName}${optionalMark}: ${field.type}${arrayMark};`;
}
});
dtoFields = fieldMappings.join('\n');
}
const importsSection = imports.length > 0 ? imports.join('\n') + '\n\n' : '';
return `${importsSection}export interface ${dtoInterfaceName} {
${dtoFields}
}`;
}
function generateIndividualEnum(typeName, field, apiName, serviceName, operationName, type = 'response') {
let cleanType = typeName;
// Limpiar el tipo para obtener el nombre base
cleanType = cleanType.replace(/LoginResponse$/, '');
cleanType = cleanType.replace(/LoginRequest$/, '');
cleanType = cleanType.replace(/Login$/, '');
cleanType = cleanType.replace(/Response$/, '');
cleanType = cleanType.replace(/Request$/, '');
const formattedTypeName = toPascalCase(cleanType);
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
// Si el formattedTypeName ya contiene "Enum", no agregarlo de nuevo
const enumSuffix = formattedTypeName.endsWith('Enum') ? '' : 'Enum';
const pascalCaseName = `${toPascalCase(serviceName)}${cleanOperationName}${formattedTypeName}${enumSuffix}`;
// Convertir a SCREAMING_SNAKE_CASE para el nombre del enum
const enumName = toScreamingSnakeCase(pascalCaseName);
// Generar valores del enum
const enumValues = field.enumValues.map((value) => {
// Convertir valores a nombres válidos de enum
let enumKey = value
.replace(/[^a-zA-Z0-9]/g, '_') // Reemplazar caracteres especiales con _
.replace(/^_+|_+$/g, '') // Quitar _ al inicio y final
.replace(/_+/g, '_'); // Consolidar múltiples _
// Si está vacío después de limpiar, usar el valor original como clave
if (!enumKey) {
enumKey = `VALUE_${Math.abs(value.split('').reduce((a, b) => a + b.charCodeAt(0), 0))}`;
}
// Si empieza con número, agregar prefijo
if (/^\d/.test(enumKey)) {
enumKey = `VALUE_${enumKey}`;
}
// Casos especiales para operadores (nombres del backend)
const operatorMappings = {
'==': 'EQUALS',
'>': 'GREATER_THAN',
'<': 'LESS_THAN',
'>=': 'GREATER_THAN_OR_EQUAL_TO',
'<=': 'LESS_THAN_OR_EQUAL_TO',
'!=': 'DIFFERENT_THAN',
'like': 'LIKE',
'in': 'IN',
'between': 'BETWEEN'
};
if (operatorMappings[value]) {
enumKey = operatorMappings[value];
}
else {
// Convertir a UPPER_CASE si no es un operador especial
enumKey = enumKey.toUpperCase();
}
return ` ${enumKey} = "${value}"`;
}).join(',\n');
return `export enum ${enumName} {
${enumValues}
}`;
}
function generateIndividualNestedDTO(typeName, field, apiName, serviceName, operationName, type = 'response') {
// Generar nombre de interface que coincida con el patrón del archivo
// i-<flujo>-<proceso>-<tipo>-<request/response>-dto.ts → I<Flujo><Proceso><Tipo><Request/Response>DTO
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
// Limpiar el tipo para obtener solo el nombre base
let cleanType = typeName;
cleanType = cleanType.replace(/LoginResponse$/, '');
cleanType = cleanType.replace(/LoginRequest$/, '');
cleanType = cleanType.replace(/Login$/, '');
cleanType = cleanType.replace(/Response$/, '');
cleanType = cleanType.replace(/Request$/, '');
const formattedTypeName = toPascalCase(cleanType);
const suffix = type === 'request' ? 'Request' : 'Response';
const dtoInterfaceName = `I${toPascalCase(serviceName)}${cleanOperationName}${formattedTypeName}${suffix}DTO`;
let dtoFields = '';
let imports = [];
if (field.nestedFields && field.nestedFields.length > 0) {
const fieldMappings = field.nestedFields.map((nestedField) => {
const fieldName = convertToCamelCase(nestedField.name); // DTO usa camelCase
const optionalMark = nestedField.required ? '' : '?';
const arrayMark = nestedField.isArray ? '[]' : '';
// Si es un tipo complejo, usar el tipo con el sufijo DTO o Enum
if (nestedField.type && !['string', 'number', 'boolean', 'any', 'object', 'array'].includes(nestedField.type)) {
// Usar el patrón completo: I<Flujo><Proceso><Tipo><Request/Response>DTO o <Flujo><Proceso><Tipo>Enum
let cleanNestedType = nestedField.type;
cleanNestedType = cleanNestedType.replace(/LoginResponse$/, '');
cleanNestedType = cleanNestedType.replace(/LoginRequest$/, '');
cleanNestedType = cleanNestedType.replace(/Login$/, '');
cleanNestedType = cleanNestedType.replace(/Response$/, '');
cleanNestedType = cleanNestedType.replace(/Request$/, '');
const formattedNestedTypeName = toPascalCase(cleanNestedType);
let fieldType;
if (nestedField.isEnum && nestedField.enumValues) {
// Para enums, usar el nombre del enum en SCREAMING_SNAKE_CASE (evitar duplicar "Enum")
const enumSuffix = formattedNestedTypeName.endsWith('Enum') ? '' : 'Enum';
const pascalCaseName = `${toPascalCase(serviceName)}${cleanOperationName}${formattedNestedTypeName}${enumSuffix}`;
fieldType = toScreamingSnakeCase(pascalCaseName);
}
else {
// Para interfaces, usar el patrón normal
const nestedSuffix = type === 'request' ? 'Request' : 'Response';
fieldType = `I${toPascalCase(serviceName)}${cleanOperationName}${formattedNestedTypeName}${nestedSuffix}DTO`;
}
// Agregar import para tipos complejos con patrón completo
const formattedNestedType = cleanNestedType.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
const serviceNameKebab = serviceName.toLowerCase().replace(/([A-Z])/g, '-$1').replace(/^-/, '');
const operationNameKebab = operationName.replace(/_/g, '-');
let importFileName;
if (nestedField.isEnum && nestedField.enumValues) {
// Para enums, usar el patrón: <flujo>-<proceso>-<tipo>-<request/response>-dto
const typeKebab = type === 'request' ? 'request' : 'response';
importFileName = `${serviceNameKebab}-${operationNameKebab}-${formattedNestedType}-${typeKebab}-dto`;
}
else {
// Para interfaces, usar el patrón: i-<flujo>-<proceso>-<tipo>-<request/response>-dto
const typeKebab = type === 'request' ? 'request' : 'response';
importFileName = `i-${serviceNameKebab}-${operationNameKebab}-${formattedNestedType}-${typeKebab}-dto`;
}
imports.push(`import { ${fieldType} } from "./${importFileName}";`);
return ` ${fieldName}${optionalMark}: ${fieldType}${arrayMark};`;
}
else {
return ` ${fieldName}${optionalMark}: ${nestedField.type}${arrayMark};`;
}
});
dtoFields = fieldMappings.join('\n');
}
const importsSection = imports.length > 0 ? imports.join('\n') + '\n\n' : '';
return `${importsSection}export interface ${dtoInterfaceName} {
${dtoFields}
}`;
}
async function generateNestedEntitiesForOperation(serviceName, operation, type, operationFolder, apiName, exportStatements, operationName) {
const fields = type === 'request' ? operation.fields : operation.responseFields;
const generated = new Set();
function processNestedFields(field) {
if (field.type && !['string', 'number', 'boolean', 'any', 'object', 'array'].includes(field.type) && !generated.has(field.type)) {
generated.add(field.type);
let nestedEntity;
// Si es un enum, generar archivo de enum en lugar de interfaz
if (field.isEnum && field.enumValues) {
nestedEntity = generateIndividualEnum(field.type, field, apiName, serviceName, operationName, type);
}
else {
// Generar Entity individual para cada interface anidada con patrón correcto
nestedEntity = generateIndividualNestedEntity(field.type, field, apiName, serviceName, operationName, type);
}
// Patrón correcto: i-<flujo>-<proceso>-<tipo>-<request/response>-entity.ts
// Limpiar el tipo para obtener solo el nombre base sin sufijos Login/Response
let cleanType = field.type;
// Remover múltiples sufijos en orden específico
cleanType = cleanType.replace(/LoginResponse$/, ''); // CompanyLoginResponse -> Company
cleanType = cleanType.replace(/LoginRequest$/, ''); // CompanyLoginRequest -> Company
cleanType = cleanType.replace(/Login$/, ''); // CompanyLogin -> Company
cleanType = cleanType.replace(/Response$/, ''); // CompanyResponse -> Company
cleanType = cleanType.replace(/Request$/, ''); // CompanyRequest -> Company
const baseFileName = cleanType.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '').replace(/[\[\]]/g, 'array');
const serviceNameKebab = serviceName.toLowerCase().replace(/([A-Z])/g, '-$1').replace(/^-/, '');
const operationKebab = operationName.replace(/_/g, '-');
let nestedFileName;
if (field.isEnum && field.enumValues) {
// Para enums, usar el patrón: <flujo>-<proceso>-<tipo>-<request/response>-entity.ts
nestedFileName = `${serviceNameKebab}-${operationKebab}-${baseFileName}-${type}-entity.ts`;
}
else {
// Para interfaces, usar el patrón normal
nestedFileName = `i-${serviceNameKebab}-${operationKebab}-${baseFileName}-${type}-entity.ts`;
}
fs.writeFileSync(path.join(operationFolder, nestedFileName), nestedEntity);
// Generar nombre de clase que coincida con el patrón del archivo
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
// Limpiar el tipo para obtener solo el nombre base
let cleanFieldType = field.type;
cleanFieldType = cleanFieldType.replace(/LoginResponse$/, '');
cleanFieldType = cleanFieldType.replace(/LoginRequest$/, '');
cleanFieldType = cleanFieldType.replace(/Login$/, '');
cleanFieldType = cleanFieldType.replace(/Response$/, '');
cleanFieldType = cleanFieldType.replace(/Request$/, '');
const formattedFieldType = toPascalCase(cleanFieldType);
let exportClassName;
if (field.isEnum && field.enumValues) {
// Para enums, usar el nombre del enum en SCREAMING_SNAKE_CASE
const enumSuffix = formattedFieldType.endsWith('Enum') ? '' : 'Enum';
const pascalCaseName = `${toPascalCase(serviceName)}${cleanOperationName}${formattedFieldType}${enumSuffix}`;
exportClassName = toScreamingSnakeCase(pascalCaseName);
}
else {
// Para interfaces, usar el patrón normal
const suffix = type === 'request' ? 'Request' : 'Response';
exportClassName = `I${toPascalCase(serviceName)}${cleanOperationName}${formattedFieldType}${suffix}Entity`;
}
// Export statement con el nombre correcto del archivo
const fileNameForExport = nestedFileName.replace('.ts', '');
exportStatements.push(`export { ${exportClassName} } from './${operationName}/${fileNameForExport}';`);
// Procesar campos anidados recursivamente
if (field.nestedFields && field.nestedFields.length > 0) {
field.nestedFields.forEach(processNestedFields);
}
}
}
if (fields && fields.length > 0) {
fields.forEach(processNestedFields);
}
}
function generateIndividualNestedEntity(typeName, field, apiName, serviceName, operationName, type = 'response') {
// Generar nombre de interface que coincida con el patrón del archivo
// i-<flujo>-<proceso>-<tipo>-<request/response>-entity.ts → I<Flujo><Proceso><Tipo><Request/Response>Entity
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
// Limpiar el tipo para obtener solo el nombre base
let cleanType = typeName;
cleanType = cleanType.replace(/LoginResponse$/, '');
cleanType = cleanType.replace(/LoginRequest$/, '');
cleanType = cleanType.replace(/Login$/, '');
cleanType = cleanType.replace(/Response$/, '');
cleanType = cleanType.replace(/Request$/, '');
const formattedTypeName = toPascalCase(cleanType);
const suffix = type === 'request' ? 'Request' : 'Response';
const entityInterfaceName = `I${toPascalCase(serviceName)}${cleanOperationName}${formattedTypeName}${suffix}Entity`;
let entityFields = '';
let imports = [];
if (field.nestedFields && field.nestedFields.length > 0) {
const fieldMappings = field.nestedFields.map((nestedField) => {
const fieldName = nestedField.name; // Entity usa snake_case como viene del swagger
const optionalMark = nestedField.required ? '' : '?';
const arrayMark = nestedField.isArray ? '[]' : '';
// Si es un tipo complejo, usar el tipo con el sufijo Entity o Enum
if (nestedField.type && !['string', 'number', 'boolean', 'any', 'object', 'array'].includes(nestedField.type)) {
// Usar el patrón completo: I<Flujo><Proceso><Tipo><Request/Response>Entity o <Flujo><Proceso><Tipo>Enum
let cleanNestedType = nestedField.type;
cleanNestedType = cleanNestedType.replace(/LoginResponse$/, '');
cleanNestedType = cleanNestedType.replace(/LoginRequest$/, '');
cleanNestedType = cleanNestedType.replace(/Login$/, '');
cleanNestedType = cleanNestedType.replace(/Response$/, '');
cleanNestedType = cleanNestedType.replace(/Request$/, '');
const formattedNestedTypeName = toPascalCase(cleanNestedType);
let fieldType, importFileName;
if (nestedField.isEnum && nestedField.enumValues) {
// Para enums, usar el nombre del enum en SCREAMING_SNAKE_CASE
const enumSuffix = formattedNestedTypeName.endsWith('Enum') ? '' : 'Enum';
const pascalCaseName = `${toPascalCase(serviceName)}${cleanOperationName}${formattedNestedTypeName}${enumSuffix}`;
fieldType = toScreamingSnakeCase(pascalCaseName);
// Para enums, usar el patrón: <flujo>-<proceso>-<tipo>-<request/response>-entity
const formattedNestedType = cleanNestedType.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
const serviceNameKebab = serviceName.toLowerCase().replace(/([A-Z])/g, '-$1').replace(/^-/, '');
const operationNameKebab = operationName.replace(/_/g, '-');
const typeKebab = type === 'request' ? 'request' : 'response';
importFileName = `${serviceNameKebab}-${operationNameKebab}-${formattedNestedType}-${typeKebab}-entity`;
}
else {
// Para interfaces, usar el patrón normal
const nestedSuffix = type === 'request' ? 'Request' : 'Response';
fieldType = `I${toPascalCase(serviceName)}${cleanOperationName}${formattedNestedTypeName}${nestedSuffix}Entity`;
// Patron: i-<flujo>-<proceso>-<tipo>-<request/response>-entity.ts
const formattedNestedType = cleanNestedType.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
const serviceNameKebab = serviceName.toLowerCase().replace(/([A-Z])/g, '-$1').replace(/^-/, '');
const operationNameKebab = operationName.replace(/_/g, '-');
const typeKebab = type === 'request' ? 'request' : 'response';
importFileName = `i-${serviceNameKebab}-${operationNameKebab}-${formattedNestedType}-${typeKebab}-entity`;
}
imports.push(`import { ${fieldType} } from "./${importFileName}";`);
return ` ${fieldName}${optionalMark}: ${fieldType}${arrayMark};`;
}
else {
return ` ${fieldName}${optionalMark}: ${nestedField.type}${arrayMark};`;
}
});
entityFields = fieldMappings.join('\n');
}
const importsSection = imports.length > 0 ? imports.join('\n') + '\n\n' : '';
return `${importsSection}export interface ${entityInterfaceName} {
${entityFields}
}`;
}
// ======================================================================
// NUEVAS FUNCIONES PARA COMPLETAR EL FLUJO DE NEGOCIO
// ======================================================================
async function generateDomainRepositoryInterfaces(serviceName, paths, schema, apiName = 'platform') {
if (schema?.businessOperations && schema.businessOperations.length > 0) {
const serviceNameLower = serviceName.toLowerCase();
const serviceNameKebab = serviceName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
const repositoryFilePath = path.join(paths.domainRepositories, `i-${serviceNameKebab}-repository.ts`);
// Recopilar DTOs y Entities necesarias para las nuevas operaciones
const newDtos = new Set();
const newEntities = new Set();
const newMethods = [];
// Generar nuevos métodos abstractos
for (const operation of schema.businessOperations) {
const rawOperationName = getOperationNameFromPath(operation.path, operation.operationId.toLowerCase());
const operationName = rawOperationName.replace(/_/g, '-');
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
// Convertir operationName a camelCase para el nombre del método
const operationCamelCase = operationName
.replace(/_/g, '-')
.split('-')
.map((word, index) => index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const baseResponseType = operation.responseFields && operation.responseFields.length > 0
? `I${toPascalCase(serviceName)}${cleanOperationName}ResponseDTO`
: 'any';
const responseType = operation.isResponseArray
? `${baseResponseType}[] | null`
: `${baseResponseType} | null`;
// Si tiene campos de request, incluir params, sino solo config
const hasRequestFields = operation.fields && operation.fields.length > 0;
const requestType = hasRequestFields ? `I${toPascalCase(serviceName)}${cleanOperationName}RequestEntity` : null;
const params = hasRequestFields ? `params: ${requestType}, ` : '';
// Agregar a los imports
if (operation.responseFields && operation.responseFields.length > 0) {
newDtos.add(`I${toPascalCase(serviceName)}${cleanOperationName}ResponseDTO`);
}
if (hasRequestFields) {
newEntities.add(`I${toPascalCase(serviceName)}${cleanOperationName}RequestEntity`);
}
newMethods.push(` abstract ${operationCamelCase}(${params}config: IConfigDTO): Promise<${responseType}>;`);
}
// Leer archivo existente si existe
let existingMethods = [];
let existingDtos = new Set();
let existingEntities = new Set();
if (await fs.pathExists(repositoryFilePath)) {
try {
const existingContent = await fs.readFile(repositoryFilePath, 'utf-8');
// Extraer métodos abstractos existentes
const newMethodNames = new Set(newMethods.map(m => {
const match = m.match(/abstract (\w+)\(/);
return match ? match[1] : '';
}));
// Extraer métodos usando regex
const methodRegex = /abstract\s+(\w+)\([^)]*\):\s*Promise<[^>]+>;/g;
const methodMatches = existingContent.matchAll(methodRegex);
for (const match of methodMatches) {
const methodName = match[1];
if (!newMethodNames.has(methodName)) {
existingMethods.push(` ${match[0]}`);
}
}
// Extraer DTOs existentes del primer import
const dtoImportMatch = existingContent.match(/import \{([^}]+)\} from "@\w+\/domain\/models\/apis\/\w+\/business\/\w+";/);
if (dtoImportMatch) {
dtoImportMatch[1].split(',').forEach(dto => {
const trimmed = dto.trim();
if (trimmed && !newDtos.has(trimmed)) {
existingDtos.add(trimmed);
}
});
}
// Extraer Entities existentes
const entityImportMatch = existingContent.match(/import \{([^}]+)\} from "@\w+\/infrastructure\/entities\/apis\/\w+\/business\/\w+";/);
if (entityImportMatch) {
entityImportMatch[1].split(',').forEach(entity => {
const trimmed = entity.trim();
if (trimmed && !newEntities.has(trimmed)) {
existingEntities.add(trimmed);
}
});
}
console.log(chalk_1.default.blue(`ℹ️ Archivo existente detectado - Agregando ${newMethods.length} método(s) nuevo(s), manteniendo ${existingMethods.length} método(s) existente(s)`));
}
catch (error) {
console.log(chalk_1.default.yellow(`⚠️ No se pudo leer el archivo existente: ${error}`));
}
}
// Combinar DTOs, Entities y Métodos
const allDtos = [...Array.from(existingDtos), ...Array.from(newDtos)];
const allEntities = [...Array.from(existingEntities), ...Array.from(newEntities)];
const allMethods = [...existingMethods, ...newMethods];
// Generar imports
const dtoImport = allDtos.length > 0
? `import {
${allDtos.join(',\n ')}
} from "@${apiName}/domain/models/apis/${apiName}/business/${serviceNameLower}";`
: '';
const entityImport = allEntities.length > 0
? `import {
${allEntities.join(',\n ')}
} from "@${apiName}/infrastructure/entities/apis/${apiName}/business/${serviceNameLower}";`
: '';
// Generar el archivo completo
const repositoryInterface = `import { IConfigDTO } from "@core/interfaces/i-config-repository-dto";
${dtoImport}
${entityImport}
export abstract class I${toPascalCase(serviceName)}Repository {
${allMethods.join('\n')}
}`;
await fs.writeFile(repositoryFilePath, repositoryInterface);
console.log(chalk_1.default.green(`✅ Repository Interface: i-${serviceNameKebab}-repository.ts ${existingMethods.length > 0 ? '(actualizado)' : ''}`));
}
else {
console.log(chalk_1.default.yellow('⚠️ No se generó repository interface porque no hay operaciones de negocio detectadas.'));
}
}
async function generateDomainUseCases(serviceName, paths, schema, apiName = 'platform') {
if (schema?.businessOperations && schema.businessOperations.length > 0) {
const serviceNameLower = serviceName.toLowerCase();
const serviceNameKebab = serviceName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
for (const operation of schema.businessOperations) {
const rawOperationName = getOperationNameFromPath(operation.path, operation.operationId.toLowerCase());
const operationName = rawOperationName.replace(/_/g, '-');
const operationKebab = operationName.replace(/_/g, '-');
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
// Los casos de uso van directamente en la carpeta del servicio (sin subcarpetas)
await fs.ensureDir(paths.domainUseCases);
// Generar Use Case si tiene responseFields (puede o no tener request)
if (operation.responseFields && operation.responseFields.length > 0) {
const hasRequest = operation.fields && operation.fields.length > 0;
const requestDTOName = `I${toPascalCase(serviceName)}${cleanOperationName}RequestDTO`;
const baseResponseDTOName = `I${toPascalCase(serviceName)}${cleanOperationName}ResponseDTO`;
const responseDTOName = operation.isResponseArray ? `${baseResponseDTOName}[]` : baseResponseDTOName;
const useCaseClassName = `${toPascalCase(serviceName)}${cleanOperationName}UseCase`;
// Convertir operationName a camelCase para el método del repository
const operationCamelCase = operationName
.replace(/_/g, '-')
.split('-')
.map((word, index) => index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1))
.join('');
// Seguir el patrón de entities: usar un solo mapper y estructura simple
const dtoImports = hasRequest ? `${requestDTOName}, ${baseResponseDTOName}` : baseResponseDTOName;
const useCaseInterface = hasRequest ? `UseCase<${requestDTOName}, ${responseDTOName} | null>` : `UseCase<any, ${responseDTOName} | null>`;
// Solo importar mapper si tiene request fields
const mapperImport = hasRequest ? `import { Injection${toPascalCase(apiName)}Business${toPascalCase(serviceName)}${cleanOperationName}Mapper } from "@${apiName}/infrastructure/mappers/apis/${apiName}/injection/business/${serviceNameLower}/injection-${apiName}-business-${serviceNameKebab}-${operationKebab}-mapper";` : '';
// Método execute siguiendo el patrón
const executeParams = hasRequest ? `params: ${requestDTOName}, ` : '';
const repositoryCall = hasRequest ? `this.repository.${operationCamelCase}(paramsEntity, config)` : `this.repository.${operationCamelCase}(config)`;
const mapperLogic = hasRequest
? `const paramsEntity = this.mapper.mapTo(params);
return await ${repositoryCall}.then((data) => data ?? null);`
: `return await ${repositoryCall}.then((data) => data ?? null);`;
const useCase = `import { IConfigDTO } from "@core/interfaces/i-config-repository-dto";
import { UseCase } from "@core/interfaces/use-case";
import { ${dtoImports} } from "@${apiName}/domain/models/apis/${apiName}/business/${serviceNameLower}";
${mapperImport}
import { Injection${toPascalCase(apiName)}BusinessRepository } from "@${apiName}/infrastructure/repositories/apis/${apiName}/repositories/injection/business/injection-${apiName}-business-repository";
export class ${useCaseClassName} implements ${useCaseInterface} {
private static instance: ${useCaseClassName};
private repository = Injection${toPascalCase(apiName)}BusinessRepository.${toPascalCase(serviceName)}Repository();${hasRequest ? `
private mapper = Injection${toPascalCase(apiName)}Business${toPascalCase(serviceName)}${cleanOperationName}Mapper.${toPascalCase(serviceName)}${cleanOperationName}RequestMapper();` : ''}
public static getInstance(): ${useCaseClassName} {
if (!${useCaseClassName}.instance)
${useCaseClassName}.instance = new ${useCaseClassName}();
return ${useCaseClassName}.instance;
}
public async execute(${executeParams}config?: IConfigDTO): Promise<${responseDTOName} | null> {
${mapperLogic}
}
}`;
await fs.writeFile(path.join(paths.domainUseCases, `${serviceNameKebab}-${operationKebab}-use-case.ts`), useCase);
console.log(chalk_1.default.green(`✅ Use Case: ${serviceNameKebab}-${operationKebab}-use-case.ts`));
}
}
}
else {
console.log(chalk_1.default.yellow('⚠️ No se generaron use cases porque no hay operaciones de negocio detectadas.'));
}
}
async function generateBusinessFacades(serviceName, paths, schema, apiName = 'platform') {
if (schema?.businessOperations && schema.businessOperations.length > 0) {
const serviceNameLower = serviceName.toLowerCase();
const serviceNameKebab = serviceName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
const facadeFilePath = path.join(paths.facades, `${serviceNameKebab}-facade.ts`);
const facadeClassName = `${toPascalCase(serviceName)}Facade`;
// Recopilar nuevos DTOs, use case instances y métodos
const newDtos = new Set();
const newUseCaseInstances = [];
const newMethods = [];
schema.businessOperations
.filter(operation => operation.responseFields && operation.responseFields.length > 0)
.forEach(operation => {
const rawOperationName = getOperationNameFromPath(operation.path, operation.operationId.toLowerCase());
const operationName = rawOperationName.replace(/_/g, '-');
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
// Convertir operationName a camelCase
const operationCamelCase = operationName
.replace(/_/g, '-')
.split('-')
.map((word, index) => index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const hasRequest = operation.fields && operation.fields.length > 0;
// Agregar DTOs
if (hasRequest) {
newDtos.add(`I${toPascalCase(serviceName)}${cleanOperationName}RequestDTO`);
}
newDtos.add(`I${toPascalCase(serviceName)}${cleanOperationName}ResponseDTO`);
// Agregar use case instance
newUseCaseInstances.push(` private readonly ${operationCamelCase}UseCase = Injection${toPascalCase(apiName)}Business${toPascalCase(serviceName)}UseCase.${toPascalCase(serviceName)}${cleanOperationName}UseCase();`);
// Agregar método
const params = hasRequest ? `params: I${toPascalCase(serviceName)}${cleanOperationName}RequestDTO, ` : '';
const args = hasRequest ? 'params, config' : 'config';
const baseResponseType = `I${toPascalCase(serviceName)}${cleanOperationName}ResponseDTO`;
const responseType = operation.isResponseArray ? `${baseResponseType}[]` : baseResponseType;
newMethods.push(` public async ${operationCamelCase}(${params}config?: IConfigDTO): Promise<${responseType} | null> {
return await this.${operationCamelCase}UseCase.execute(${args});
}`);
});
// Leer archivo existente si existe
let existingDtos = new Set();
let existingUseCaseInstances = [];
let existingMethods = [];
if (await fs.pathExists(facadeFilePath)) {
try {
const existingContent = await fs.readFile(facadeFilePath, 'utf-8');
// Extraer DTOs existentes
const dtoImportMatch = existingContent.match(/import \{([^}]+)\} from "@\w+\/domain\/models\/apis\/\w+\/business\/\w+";/);
if (dtoImportMatch) {
dtoImportMatch[1].split(',').forEach(dto => {
const trimmed = dto.trim();
if (trimmed && !newDtos.has(trimmed)) {
existingDtos.add(trimmed);
}
});
}
// Extraer use case instances existentes
const useCaseInstanceRegex = /private readonly (\w+UseCase) = Injection[^;]+;/g;
const useCaseMatches = existingContent.matchAll(useCaseInstanceRegex);
const newUseCaseNames = new Set(newUseCaseInstances.map(inst => {
const match = inst.match(/private readonly (\w+UseCase)/);
return match ? match[1] : '';
}));
for (const match of useCaseMatches) {
const useCaseName = match[1];
if (!newUseCaseNames.has(useCaseName)) {
existingUseCaseInstances.push(` ${match[0]}`);
}
}
// IMPORTANTE: Detectar métodos que usan use cases pero no tienen la instancia declarada
// Esto puede pasar si el archivo fue corrompido antes
const methodUseCaseRegex = /this\.(\w+UseCase)\.execute\(/g;
const methodUseCaseMatches = existingContent.matchAll(methodUseCaseRegex);
const declaredUseCases = new Set([
...newUseCaseNames,
...existingUseCaseInstances.map(inst => {
const match = inst.match(/private readonly (\w+UseCase)/);
return match ? match[1] : '';
})
]);
for (const match of methodUseCaseMatches) {
const useCaseName = match[1];
if (!declaredUseCases.has(useCaseName)) {
// Reconstruir la instancia del use case basándose en el nombre del método
// Ejemplo: loginUseCase -> AuthLoginUseCase
const methodName = useCaseName.replace(/UseCase$/, '');
const useCaseClassName = `${toPascalCase(serviceName)}${methodName.charAt(0).toUpperCase() + methodName.slice(1)}UseCase`;
existingUseCaseInstances.push(` private readonly ${useCaseName} = Injection${toPascalCase(apiName)}Business${toPascalCase(serviceName)}UseCase.${useCaseClassName}();`);
declaredUseCases.add(useCaseName);
}
}
// Extraer métodos públicos existentes
const methodRegex = /public async (\w+)\([^)]*\): Promise<[^{]+\{[^}]+\}/g;
const methodMatches = existingContent.matchAll(methodRegex);
const newMethodNames = new Set(newMethods.map(m => {
const match = m.match(/public async (\w+)\(/);
return match ? match[1] : '';
}));
for (const match of methodMatches) {
const methodName = match[1];
if (!newMethodNames.has(methodName)) {
existingMethods.push(` ${match[0]}`);
}
}
console.log(chalk_1.default.blue(`ℹ️ Facade existente detectado - Agregando ${newMethods.length} método(s) nuevo(s), manteniendo ${existingMethods.length} método(s) existente(s)`));
}
catch (error) {
console.log(chalk_1.default.yellow(`⚠️ No se pudo leer el archivo existente: ${error}`));
}
}
// Combinar DTOs, use case instances y métodos
const allDtos = [...Array.from(existingDtos), ...Array.from(newDtos)];
const allUseCaseInstances = [...existingUseCaseInstances, ...newUseCaseInstances];
const allMethods = [...existingMethods, ...newMethods];
// Generar el archivo completo
const dtoImports = allDtos.join(',\n ');
const useCaseInjectionImport = `import { Injection${toPascalCase(apiName)}Business${toPascalCase(serviceName)}UseCase } from "@${apiName}/domain/services/use_cases/apis/${apiName}/injection/business/injection-${apiName}-business-${serviceNameKebab}-use-case";`;
const facade = `import { IConfigDTO } from "@core/interfaces/i-config-repository-dto";
import {
${dtoImports},
} from "@${apiName}/domain/models/apis/${apiName}/business/${serviceNameLower}";
${useCaseInjectionImport}
export class ${facadeClassName} {
private static instance: ${facadeClassName};
${allUseCaseInstances.join('\n')}
public static getInstance(): ${facadeClassName} {
if (!${facadeClassName}.instance)
${facadeClassName}.instance = new ${facadeClassName}();
return ${facadeClassName}.instance;
}
${allMethods.join('\n\n')}
}`;
await fs.writeFile(facadeFilePath, facade);
console.log(chalk_1.default.green(`✅ Facade: ${serviceNameKebab}-facade.ts ${existingMethods.length > 0 ? '(actualizado)' : ''}`));
}
else {
console.log(chalk_1.default.yellow('⚠️ No se generó facade porque no hay operaciones de negocio detectadas.'));
}
}
async function generateMapperInjectionPerOperation(serviceName, paths, schema, apiName = 'platform') {
if (schema?.businessOperations && schema.businessOperations.length > 0) {
const serviceNameLower = serviceName.toLowerCase();
const serviceNameKebab = serviceName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
// Crear directorio injection en la ubicación correcta
await fs.ensureDir(paths.injectionMappers);
for (const operation of schema.businessOperations) {
const rawOperationName = getOperationNameFromPath(operation.path, operation.operationId.toLowerCase());
const operationName = rawOperationName.replace(/_/g, '-');
const operationKebab = operationName.replace(/_/g, '-');
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
// Recopilar todos los mappers de esta operación
const mapperNames = [];
const mapperMethods = [];
// Request mapper (si existe)
if (operation.fields && operation.fields.length > 0) {
const requestMapperName = `${toPascalCase(serviceName)}${cleanOperationName}RequestMapper`;
mapperNames.push(requestMapperName);
mapperMethods.push(` public static ${requestMapperName}(): ${requestMapperName} {
return ${requestMapperName}.getInstance();
}`);
}
// Response mapper (si existe)
if (operation.responseFields && operation.responseFields.length > 0) {
const responseMapperName = `${toPascalCase(serviceName)}${cleanOperationName}ResponseMapper`;
mapperNames.push(responseMapperName);
mapperMethods.push(` public static ${responseMapperName}(): ${responseMapperName} {
return ${responseMapperName}.getInstance();
}`);
// Mappers anidados de response (si existen)
const responseNestedMappers = await collectNestedMappersForOperation(operation, 'response', serviceName, operationName);
for (const nestedMapper of responseNestedMappers) {
const nestedMapperName = nestedMapper.className;
// Generar nombre de método abreviado removiendo el prefijo del servicio y operación
// Ejemplo: AuthLoginPlatformConfigurationResponseMapper -> PlatformConfigurationResponseMapper
const formattedServiceName = toPascalCase(serviceName);
const methodName = nestedMapperName.replace(new RegExp(`^${formattedServiceName}${cleanOperationName}`, ''), '');
mapperNames.push(nestedMapperName);
mapperMethods.push(` public static ${methodName}(): ${nestedMapperName} {
return ${nestedMapperName}.getInstance();
}`);
}
}
// Mappers anidados de request (si existen)
if (operation.fields && operation.fields.length > 0) {
const requestNestedMappers = await collectNestedMappersForOperation(operation, 'request', serviceName, operationName);
for (const nestedMapper of requestNestedMappers) {
const nestedMapperName = nestedMapper.className;
// Generar nombre de método abreviado removiendo el prefijo del servicio y operación
// Ejemplo: AvailabilityAppointmentTableFilterManagerRequestMapper -> FilterManagerRequestMapper
const formattedServiceName = toPascalCase(serviceName);
const methodName = nestedMapperName.replace(new RegExp(`^${formattedServiceName}${cleanOperationName}`, ''), '');
mapperNames.push(nestedMapperName);
mapperMethods.push(` public static ${methodName}(): ${nestedMapperName} {
return ${nestedMapperName}.getInstance();
}`);
}
}
// Solo crear injection si hay mappers
if (mapperNames.length > 0) {
const injectionClassName = `Injection${toPascalCase(apiName)}Business${toPascalCase(serviceName)}${cleanOperationName}Mapper`;
// Generar import unificado usando el index.ts
const importStatement = `import {
${mapperNames.join(',\n ')}
} from "@${apiName}/infrastructure/mappers/apis/${apiName}/business/${serviceNameLower}";`;
const injectionContent = `${importStatement}
export class ${injectionClassName} {
${mapperMethods.join('\n\n')}
}`;
// Generar archivo en la ubicación correcta: /infrastructure/mappers/apis/platform/injection/business/auth/
await fs.writeFile(path.join(paths.injectionMappers, `injection-${apiName}-business-${serviceNameKebab}-${operationKebab}-mapper.ts`), injectionContent);
console.log(chalk_1.default.green(`✅ Injection Mapper (${operationName}): injection-${apiName}-business-${serviceNameKebab}-${operationKebab}-mapper.ts`));
}
}
}
else {
console.log(chalk_1.default.yellow('⚠️ No se generaron injection files porque no hay operaciones de negocio detectadas.'));
}
}
async function collectNestedMappersForOperation(operation, type, serviceName, operationName) {
const fields = type === 'request' ? operation.fields : operation.responseFields;
const nestedMappers = [];
const generated = new Set();
function processNestedFields(field, serviceName, operationName) {
// Solo generar mappers para interfaces, NO para enums (los enums se mapean directamente)
if (field.type && !['string', 'number', 'boolean', 'any', 'object', 'array'].includes(field.type) && !field.isEnum && !generated.has(field.type)) {
generated.add(field.type);
// Aplicar el patrón correcto: <flujo>-<proceso>-<tipo>-<request/response>-mapper
const serviceNameKebab = serviceName.toLowerCase();
const operationKebab = operationName.replace(/_/g, '-');
let typeKebab = field.type.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '').replace(/[\[\]]/g, 'array');
const suffix = type === 'request' ? 'request' : 'response';
// Detectar y evitar duplicación de operaciones en el nombre del tipo para archivos
const operationInFileName = operationKebab.toLowerCase();
if (typeKebab.includes(operationInFileName)) {
// Eliminar la operación del tipo: user-login-response -> user-response
typeKebab = typeKebab.replace(new RegExp(`-${operationInFileName}`, 'gi'), '');
}
const needsSuffix = !field.type.toLowerCase().endsWith('response') && !field.type.toLowerCase().endsWith('request');
const fileName = needsSuffix
? `${serviceNameKebab}-${operationKebab}-${typeKebab}-${suffix}-mapper`
: `${serviceNameKebab}-${operationKebab}-${typeKebab}-mapper`;
// Para el nombre de la clase: <Flujo><Proceso><Tipo><Request/Response>Mapper
const formattedServiceName = toPascalCase(serviceName);
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const formattedFieldType = toPascalCase(field.type);
const classSuffix = type === 'request' ? 'Request' : 'Response';
// Limpiar caracteres especiales para nombres de clase válidos
const cleanFormattedType = formattedFieldType.replace(/[\[\]]/g, 'Array');
// Aplicar la misma lógica de limpieza simplificada que en generateIndividualNestedMapper
let finalTypeName = cleanFormattedType;
// Remover sufijos redundantes para obtener nombre base limpio (igual que en generateIndividualNestedMapper)
finalTypeName = finalTypeName.replace(/LoginResponse$|LoginRequest$|Response$|Request$/, '');
// Generar nombre de clase consistente con la lógica de generateIndividualNestedMapper
const className = `${formattedServiceName}${cleanOperationName}${finalTypeName}${classSuffix}Mapper`;
nestedMappers.push({
className,
fileName
});
// Procesar campos anidados recursivamente
if (field.nestedFields && field.nestedFields.length > 0) {
field.nestedFields.forEach((nestedField) => processNestedFields(nestedField, serviceName, operationName));
}
}
}
if (fields && fields.length > 0) {
fields.forEach((field) => processNestedFields(field, serviceName, operationName));
}
return nestedMappers;
}
async function generateInfrastructureRepositories(serviceName, paths, schema, apiName = 'platform') {
if (schema?.businessOperations && schema.businessOperations.length > 0) {
const serviceNameLower = serviceName.toLowerCase();
const serviceNameKebab = serviceName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
const repositoryClassName = `${toPascalCase(serviceName)}Repository`;
// Generar imports para todas las DTOs y Entities necesarias
const allImports = {
dtos: new Set(),
entities: new Set(),
mapperImports: new Set(),
mapperInstances: []
};
// Generar métodos para todas las operaciones
const methods = [];
for (const operation of schema.businessOperations) {
const rawOperationName = getOperationNameFromPath(operation.path, operation.operationId.toLowerCase());
const operationName = rawOperationName.replace(/_/g, '-');
const operationKebab = operationName.replace(/_/g, '-');
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
// Generar método si tiene responseFields (puede o no tener request)
if (operation.responseFields && operation.responseFields.length > 0) {
const requestDTOName = `I${toPascalCase(serviceName)}${cleanOperationName}RequestDTO`;
const baseResponseDTOName = `I${toPascalCase(serviceName)}${cleanOperationName}ResponseDTO`;
const responseDTOName = operation.isResponseArray ? `${baseResponseDTOName}[]` : baseResponseDTOName;
const requestEntityName = `I${toPascalCase(serviceName)}${cleanOperationName}RequestEntity`;
const responseEntityName = `I${toPascalCase(serviceName)}${cleanOperationName}ResponseEntity`;
// Agregar imports (solo entities de request y DTOs de response)
const hasRequest = operation.fields && operation.fields.length > 0;
if (hasRequest) {
allImports.entities.add(requestEntityName);
}
// Para imports, siempre usar el nombre base sin [], los [] van solo en el tipo de retorno
allImports.dtos.add(baseResponseDTOName);
allImports.entities.add(responseEntityName);
allImports.mapperImports.add(`import { Injection${toPascalCase(apiName)}Business${toPascalCase(serviceName)}${cleanOperationName}Mapper } from "@${apiName}/infrastructure/mappers/apis/${apiName}/injection/business/${serviceNameLower}/injection-${apiName}-business-${serviceNameKebab}-${operationKebab}-mapper";`);
// Agregar instancias de mappers (solo Response mappers)
const operationCamelCaseVar = operationName
.replace(/_/g, '-')
.split('-')
.map((word, index) => index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1))
.join('');
// Solo agregar Response mapper (los Request mappers no se usan en el repository)
allImports.mapperInstances.push(` private ${operationCamelCaseVar}ResponseMapper = Injection${toPascalCase(apiName)}Business${toPascalCase(serviceName)}${cleanOperationName}Mapper.${toPascalCase(serviceName)}${cleanOperationName}ResponseMapper();`);
// Convertir operationName a camelCase para el nombre del método
const operationCamelCase = operationName
.replace(/_/g, '-')
.split('-')
.map((word, index) => index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1))
.join('');
// Generar método
const methodParams = hasRequest ? `params: ${requestEntityName}, ` : '';
// Determinar el método HTTP correcto
const httpMethod = (operation.method || 'post').toLowerCase();
// Verificar si hay path parameters
const hasPathParams = operation.pathParameters && operation.pathParameters.length > 0;
// Construir la URL - si hay path params, interpolarlos
const routeConstant = `CONST_${apiName.toUpperCase()}_API_ROUTES.${serviceName.toUpperCase()}_${operationName.toUpperCase().replace(/-/g, '_')}`;
let urlExpression;
let bodyParams;
if (hasPathParams && operation.pathParameters) {
// Interpolar path params en la URL usando template string
const pathParamsList = operation.pathParameters.map((p) => `params.${p.name}`);
urlExpression = `\`\${${routeConstant}}/${pathParamsList.map((p) => `\${${p}}`).join('/')}\``;
// Para body, excluir los path params (solo enviar los que no son de path)
const bodyFields = operation.fields?.filter((f) => !f.isPathParam) || [];
if (bodyFields.length > 0 && (httpMethod === 'post' || httpMethod === 'put' || httpMethod === 'patch')) {
// Crear objeto solo con los campos que no son path params
const bodyFieldNames = bodyFields.map((f) => f.name);
bodyParams = `{ ${bodyFieldNames.map((n) => `${n}: params.${n}`).join(', ')} }`;
}
else {
bodyParams = '';
}
}
else {
urlExpression = routeConstant;
bodyParams = hasRequest ? 'params' : '{}';
}
// Construir la llamada axios según el método HTTP
let axiosCall;
if (httpMethod === 'get' || httpMethod === 'delete') {
// GET y DELETE normalmente no envían body (o usan query params)
axiosCall = `.${httpMethod}(${urlExpression})`;
}
else {
// POST, PUT, PATCH envían body
axiosCall = `.${httpMethod}(${urlExpression}, ${bodyParams || '{}'})`;
}
// Para ResolveRequest, usar [] solo si es un array
const resolveEntityType = operation.isResponseArray ? `${responseEntityName}[]` : responseEntityName;
const method = ` public async ${operationCamelCase}(
${methodParams}config: IConfigDTO = CONST_CORE_DTO.CONFIG
): Promise<${responseDTOName} | null> {
if (config.loadService)
return ${apiName}Axios
${axiosCall}
.then(({ data }: { data: Response<${resolveEntityType}> }) => {
const entity = this.resolve.ResolveRequest<${resolveEntityType}>(data);
if (entity)
return this.${operationCamelCaseVar}ResponseMapper.${operation.isResponseArray ? 'mapFromList' : 'mapFrom'}(entity);
return null;
});
return null;
}`;
methods.push(method);
}
}
// Verificar si el archivo ya existe
const repositoryFilePath = path.join(paths.infraRepositories, `${serviceNameKebab}-repository.ts`);
let existingMethods = [];
let existingMapperInstances = [];
let existingDtos = new Set();
let existingEntities = new Set();
let existingMapperImports = new Set();
if (await fs.pathExists(repositoryFilePath)) {
try {
const existingContent = await fs.readFile(repositoryFilePath, 'utf-8');
// Extraer métodos existentes usando un enfoque más robusto
// Buscar desde "public async" hasta el siguiente "public async" o hasta el cierre de la clase
const newMethodNames = new Set(methods.map(m => {
const match = m.match(/public async (\w+)\(/);
return match ? match[1] : '';
}));
// Dividir por "public async" para obtener cada método completo
const methodParts = existingContent.split(/\n\s*public async /);
for (let i = 1; i < methodParts.length; i++) {
const methodPart = methodParts[i];
const methodNameMatch = methodPart.match(/^(\w+)\(/);
if (methodNameMatch) {
const methodName = methodNameMatch[1];
// Solo agregar si no está en los nuevos métodos
if (!newMethodNames.has(methodName)) {
// Reconstruir el método completo
const fullMethod = ` public async ${methodPart.split(/\n\s*public async |^\}/m)[0].trimEnd()}`;
existingMethods.push(fullMethod);
}
}
}
// Extraer mapper instances existentes (pueden ser multilínea)
// Buscar patrones como: private xxxResponseMapper = ... ;
const mapperLines = existingContent.split('\n');
const newMapperNames = new Set(allImports.mapperInstances.map(m => {
const match = m.match(/private (\w+ResponseMapper)/);
return match ? match[1] : '';
}));
let inMapper = false;
let currentMapper = '';
let currentMapperName = '';
for (let i = 0; i < mapperLines.length; i++) {
const line = mapperLines[i];
// Detectar inicio de mapper instance
const mapperStartMatch = line.match(/^\s*(private (\w+ResponseMapper) =)/);
if (mapperStartMatch) {
inMapper = true;
currentMapperName = mapperStartMatch[2];
currentMapper = line;
// Si termina en la misma línea
if (line.includes(';')) {
inMapper = false;
if (!newMapperNames.has(currentMapperName)) {
existingMapperInstances.push(currentMapper);
}
currentMapper = '';
currentMapperName = '';
}
}
else if (inMapper) {
// Continuar acumulando líneas del mapper
currentMapper += '\n' + line;
// Si encontramos el final (;)
if (line.includes(';')) {
inMapper = false;
if (!newMapperNames.has(currentMapperName)) {
existingMapperInstances.push(currentMapper);
}
currentMapper = '';
currentMapperName = '';
}
}
}
// Extraer DTOs existentes
const dtoImportMatch = existingContent.match(/import \{([^}]+)\} from "@\w+\/domain\/models\/apis\/\w+\/business\/\w+";/);
if (dtoImportMatch) {
dtoImportMatch[1].split(',').forEach(dto => {
const trimmed = dto.trim();
if (trimmed && !allImports.dtos.has(trimmed)) {
existingDtos.add(trimmed);
}
});
}
// Extraer Entities existentes
const entityImportMatch = existingContent.match(/import \{([^}]+)\} from "@\w+\/infrastructure\/entities\/apis\/\w+\/business\/\w+";/);
if (entityImportMatch) {
entityImportMatch[1].split(',').forEach(entity => {
const trimmed = entity.trim();
if (trimmed && !allImports.entities.has(trimmed)) {
existingEntities.add(trimmed);
}
});
}
// Extraer mapper imports existentes
const mapperImportRegex = /import \{ Injection\w+ \} from "@\w+\/infrastructure\/mappers[^"]+";/g;
const mapperImportMatches = existingContent.matchAll(mapperImportRegex);
const newMapperImportsSet = new Set(Array.from(allImports.mapperImports));
for (const match of mapperImportMatches) {
if (!newMapperImportsSet.has(match[0])) {
existingMapperImports.add(match[0]);
}
}
console.log(chalk_1.default.blue(`ℹ️ Archivo existente detectado - Agregando ${methods.length} método(s) nuevo(s), manteniendo ${existingMethods.length} método(s) existente(s)`));
}
catch (error) {
console.log(chalk_1.default.yellow(`⚠️ No se pudo leer el archivo existente: ${error}`));
}
}
// Combinar DTOs, Entities y Mapper Imports
const allDtos = [...Array.from(existingDtos), ...Array.from(allImports.dtos)];
const allEntities = [...Array.from(existingEntities), ...Array.from(allImports.entities)];
const allMapperImportsList = [...Array.from(existingMapperImports), ...Array.from(allImports.mapperImports)];
const allMapperInstancesList = [...existingMapperInstances, ...allImports.mapperInstances];
const allMethodsList = [...existingMethods, ...methods];
// Generar el archivo completo
const dtoImports = allDtos.join(', ');
const entityImports = allEntities.join(', ');
const mapperImports = allMapperImportsList.join('\n');
const repository = `import { IConfigDTO } from "@core/interfaces/i-config-repository-dto";
import { Response } from "@core/interfaces/response";
import ${apiName}Axios from "@core/axios/${apiName}-axios";
import { CONST_${apiName.toUpperCase()}_API_ROUTES } from "@core/const";
import { CONST_CORE_DTO } from "@core/const/const-core";
import { InjectionCore } from "@core/injection/injection-core";
import { I${toPascalCase(serviceName)}Repository } from "@${apiName}/domain/services/repositories/apis/${apiName}/business/i-${serviceNameKebab}-repository";
import { ${dtoImports} } from "@${apiName}/domain/models/apis/${apiName}/business/${serviceNameLower}";
import { ${entityImports} } from "@${apiName}/infrastructure/entities/apis/${apiName}/business/${serviceNameLower}";
${mapperImports}
export class ${repositoryClassName} extends I${toPascalCase(serviceName)}Repository {
private static instance: ${repositoryClassName};
private readonly resolve = InjectionCore.Resolve();
${allMapperInstancesList.join('\n')}
private constructor() {
super();
}
public static getInstance(): ${repositoryClassName} {
if (!${repositoryClassName}.instance)
${repositoryClassName}.instance = new ${repositoryClassName}();
return ${repositoryClassName}.instance;
}
${allMethodsList.join('\n\n')}
}`;
await fs.writeFile(repositoryFilePath, repository);
console.log(chalk_1.default.green(`✅ Infrastructure Repository: ${serviceNameKebab}-repository.ts ${existingMethods.length > 0 ? '(actualizado)' : ''}`));
}
else {
console.log(chalk_1.default.yellow('⚠️ No se generaron infrastructure repositories porque no hay operaciones de negocio detectadas.'));
}
}
async function generateRepositoryInjectionFiles(serviceName, paths, schema, apiName = 'platform') {
if (schema?.businessOperations && schema.businessOperations.length > 0) {
const serviceNameKebab = serviceName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
const injectionFilePath = path.join(paths.injectionRepositories, `injection-${apiName}-business-repository.ts`);
// Datos del repository actual
const currentRepository = {
serviceName: toPascalCase(serviceName),
import: `import { ${toPascalCase(serviceName)}Repository } from "../../business/${serviceName.toLowerCase()}/${serviceNameKebab}-repository";`,
method: ` public static ${toPascalCase(serviceName)}Repository() { return ${toPascalCase(serviceName)}Repository.getInstance(); }`
};
let existingRepositories = [];
let existingImports = [];
// Leer archivo existente si existe
if (await fs.pathExists(injectionFilePath)) {
try {
const existingContent = await fs.readFile(injectionFilePath, 'utf-8');
// Extraer imports existentes
const importMatches = existingContent.match(/import \{[^}]+\} from "[^"]+";/g);
if (importMatches) {
existingImports = importMatches.filter(imp => !imp.includes(`${toPascalCase(serviceName)}Repository`));
}
// Extraer métodos existentes
const methodMatches = existingContent.match(/public static \w+Repository\(\)[^}]+}/g);
if (methodMatches) {
existingRepositories = methodMatches
.filter(method => !method.includes(`${toPascalCase(serviceName)}Repository`))
.map(method => ({ method: ` ${method}` }));
}
}
catch (error) {
console.log(chalk_1.default.yellow(`⚠️ No se pudo leer el archivo existente: ${error}`));
}
}
// Combinar imports (existentes + actual)
const allImports = [...existingImports, currentRepository.import].join('\n');
// Combinar métodos (existentes + actual)
const allMethods = [...existingRepositories.map(r => r.method), currentRepository.method].join('\n');
// Generar archivo completo
const injectionFile = `${allImports}
export class Injection${toPascalCase(apiName)}BusinessRepository {
${allMethods}
}`;
await fs.writeFile(injectionFilePath, injectionFile);
console.log(chalk_1.default.green(`✅ Repository Injection: injection-${apiName}-business-repository.ts (actualizado)`));
}
else {
console.log(chalk_1.default.yellow('⚠️ No se generó repository injection porque no hay operaciones de negocio detectadas.'));
}
}
async function generateBusinessInjectionFiles(serviceName, paths, schema, apiName = 'platform') {
if (schema?.businessOperations && schema.businessOperations.length > 0) {
const serviceNameKebab = serviceName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
const useCaseInjectionFilePath = path.join(paths.injectionUseCases, `injection-${apiName}-business-${serviceNameKebab}-use-case.ts`);
// 1. Use Case Injection - MERGE INCREMENTAL
const newUseCaseImports = [];
const newUseCaseMethods = [];
schema.businessOperations
.filter(operation => operation.responseFields && operation.responseFields.length > 0)
.forEach(operation => {
const rawOperationName = getOperationNameFromPath(operation.path, operation.operationId.toLowerCase());
const operationName = rawOperationName.replace(/_/g, '-');
const operationKebab = operationName.replace(/_/g, '-');
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const useCaseClassName = `${toPascalCase(serviceName)}${cleanOperationName}UseCase`;
newUseCaseImports.push(`import { ${useCaseClassName} } from "@${apiName}/domain/services/use_cases/apis/${apiName}/business/${serviceName.toLowerCase()}/${serviceNameKebab}-${operationKebab}-use-case";`);
newUseCaseMethods.push(` public static ${useCaseClassName}(): ${useCaseClassName} {
return ${useCaseClassName}.getInstance();
}`);
});
let existingImports = [];
let existingMethods = [];
// Leer archivo existente si existe
if (await fs.pathExists(useCaseInjectionFilePath)) {
try {
const existingContent = await fs.readFile(useCaseInjectionFilePath, 'utf-8');
// Extraer imports existentes
const importMatches = existingContent.match(/import \{[^}]+\} from "[^"]+";/g);
if (importMatches) {
const newUseCaseClassNames = new Set(newUseCaseImports.map(imp => {
const match = imp.match(/import \{ (\w+) \}/);
return match ? match[1] : '';
}));
existingImports = importMatches.filter(imp => {
const match = imp.match(/import \{ (\w+) \}/);
const className = match ? match[1] : '';
return className && !newUseCaseClassNames.has(className);
});
}
// Extraer métodos existentes
const methodRegex = /public static (\w+UseCase)\(\):[^}]+\}/g;
const methodMatches = existingContent.matchAll(methodRegex);
const newMethodNames = new Set(newUseCaseMethods.map(m => {
const match = m.match(/public static (\w+UseCase)\(\)/);
return match ? match[1] : '';
}));
for (const match of methodMatches) {
const methodName = match[1];
if (!newMethodNames.has(methodName)) {
existingMethods.push(` ${match[0]}`);
}
}
console.log(chalk_1.default.blue(`ℹ️ Use Case Injection existente - Agregando ${newUseCaseMethods.length} método(s) nuevo(s), manteniendo ${existingMethods.length} método(s) existente(s)`));
}
catch (error) {
console.log(chalk_1.default.yellow(`⚠️ No se pudo leer el archivo existente: ${error}`));
}
}
// Combinar imports y métodos (existentes + nuevos)
const allImports = [...existingImports, ...newUseCaseImports].join('\n');
const allMethods = [...existingMethods, ...newUseCaseMethods].join('\n\n');
const useCaseInjection = `${allImports}
export class Injection${toPascalCase(apiName)}Business${toPascalCase(serviceName)}UseCase {
${allMethods}
}`;
await fs.writeFile(useCaseInjectionFilePath, useCaseInjection);
console.log(chalk_1.default.green(`✅ Use Case Injection: injection-${apiName}-business-${serviceNameKebab}-use-case.ts ${existingMethods.length > 0 ? '(actualizado)' : ''}`));
// 2. Facade Injection (acumulativo como repositories)
await generateFacadeInjectionFiles(serviceName, paths, apiName);
}
else {
console.log(chalk_1.default.yellow('⚠️ No se generaron archivos de inyección porque no hay operaciones de negocio detectadas.'));
}
}
async function generateFacadeInjectionFiles(serviceName, paths, apiName = 'platform') {
const serviceNameKebab = serviceName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
const injectionFilePath = path.join(paths.injectionFacades, `injection-${apiName}-business-facade.ts`);
// Datos del facade actual
const currentFacade = {
serviceName: toPascalCase(serviceName),
import: `import { ${toPascalCase(serviceName)}Facade } from "@${apiName}/facade/apis/${apiName}/business/${serviceNameKebab}-facade";`,
method: ` public static ${toPascalCase(serviceName)}Facade() { return ${toPascalCase(serviceName)}Facade.getInstance(); }`
};
let existingFacades = [];
let existingImports = [];
// Leer archivo existente si existe
if (await fs.pathExists(injectionFilePath)) {
try {
const existingContent = await fs.readFile(injectionFilePath, 'utf-8');
// Extraer imports existentes
const importMatches = existingContent.match(/import \{[^}]+\} from "[^"]+";/g);
if (importMatches) {
existingImports = importMatches.filter(imp => !imp.includes(`${toPascalCase(serviceName)}Facade`));
}
// Extraer métodos existentes
const methodMatches = existingContent.match(/public static \w+Facade\(\)[^}]+}/g);
if (methodMatches) {
existingFacades = methodMatches
.filter(method => !method.includes(`${toPascalCase(serviceName)}Facade`))
.map(method => ({ method: ` ${method}` }));
}
}
catch (error) {
console.log(chalk_1.default.yellow(`⚠️ No se pudo leer el archivo existente: ${error}`));
}
}
// Combinar imports (existentes + actual)
const allImports = [...existingImports, currentFacade.import].join('\n');
// Combinar métodos (existentes + actual)
const allMethods = [...existingFacades.map(f => f.method), currentFacade.method].join('\n');
// Generar archivo completo
const injectionFile = `${allImports}
export class Injection${toPascalCase(apiName)}BusinessFacade {
${allMethods}
}`;
await fs.writeFile(injectionFilePath, injectionFile);
console.log(chalk_1.default.green(`✅ Facade Injection: injection-${apiName}-business-facade.ts (actualizado)`));
}
//# sourceMappingURL=business-flow-generator.js.map