weaver-frontend-cli
Version:
🕷️ Weaver CLI - Generador completo de arquitectura Clean Architecture para entidades CRUD y flujos de negocio desde OpenAPI/Swagger
1,692 lines • 103 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';
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 = operation.path.split('/').pop() || 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)
const uniqueExports = new Set();
const finalExports = [];
exportStatements.forEach(statement => {
// Extraer el nombre de la interface del export
const match = statement.match(/export \{ (\w+) \}/);
if (match) {
const interfaceName = match[1];
if (!uniqueExports.has(interfaceName)) {
uniqueExports.add(interfaceName);
// Convertir todos a export type (mejor práctica para interfaces)
const typeStatement = statement.replace('export {', 'export type {');
finalExports.push(typeStatement);
}
}
});
const indexContent = finalExports.join('\n') + '\n';
await fs.writeFile(path.join(paths.infraEntities, 'index.ts'), indexContent);
console.log(chalk_1.default.green(`✅ Index Entity: index.ts`));
}
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 = operation.path.split('/').pop() || 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);
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
const indexContent = exportStatements.join('\n') + '\n';
await fs.writeFile(path.join(paths.infraMappers, 'index.ts'), indexContent);
console.log(chalk_1.default.green(`✅ Index Mapper: index.ts`));
}
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 = operation.path.split('/').pop() || 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);
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 responseDTOName = `I${toPascalCase(serviceName)}${cleanOperationName}ResponseDTO`;
exportStatements.push(`export { ${responseDTOName} } from './${operationName}/${responseFileName.replace('.ts', '')}';`);
}
}
// Generar index.ts para DTOs con export type (simplificado y mejor práctica)
const uniqueExports = new Set();
const finalExports = [];
exportStatements.forEach(statement => {
// Extraer el nombre de la interface del export
const match = statement.match(/export \{ (\w+) \}/);
if (match) {
const interfaceName = match[1];
if (!uniqueExports.has(interfaceName)) {
uniqueExports.add(interfaceName);
// Convertir todos a export type (mejor práctica para interfaces)
const typeStatement = statement.replace('export {', 'export type {');
finalExports.push(typeStatement);
}
}
});
const indexContent = finalExports.join('\n') + '\n';
await fs.writeFile(path.join(paths.domainModels, 'index.ts'), indexContent);
console.log(chalk_1.default.green(`✅ Index DTO: index.ts`));
}
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) {
if (field.type && !['string', 'number', 'boolean', 'any', 'object', 'array'].includes(field.type) && !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(/(^|_|-)(\w)/g, (_, __, c) => c ? c.toUpperCase() : '')
.replace(/Dto$/i, 'Entity')
.replace(/EntityEntity$/, 'Entity');
}
function toSnakeCase(str) {
return str
.replace(/([A-Z])/g, '_$1')
.replace(/^_/, '')
.toLowerCase();
}
function generateBusinessEntityInterface(serviceName, operation, type) {
const operationName = operation.path.split('/').pop() || 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$/, '');
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 = operation.path.split('/').pop() || 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} = InjectionPlatformBusiness${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 { InjectionPlatformBusiness${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 "@bus/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$/, '');
}
// 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)) {
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} = InjectionPlatformBusiness${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)) {
// 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 { InjectionPlatformBusiness${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 "@bus/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);
// Generar DTO individual para cada interface anidada con patrón correcto
const 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, '-');
const 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);
const suffix = type === 'request' ? 'Request' : 'Response';
const dtoClassName = `I${toPascalCase(serviceName)}${cleanOperationName}${formattedFieldType}${suffix}DTO`;
// Export statement con el nombre correcto del archivo
const fileNameForExport = nestedFileName.replace('.ts', '');
exportStatements.push(`export { ${dtoClassName} } 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 = operation.path.split('/').pop() || 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$/, '');
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 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
if (nestedField.type && !['string', 'number', 'boolean', 'any', 'object', 'array'].includes(nestedField.type)) {
// Usar el patrón completo: I<Flujo><Proceso><Tipo><Request/Response>DTO
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);
const nestedSuffix = type === 'request' ? 'Request' : 'Response';
const fieldType = `I${toPascalCase(serviceName)}${cleanOperationName}${formattedNestedTypeName}${nestedSuffix}DTO`;
// Agregar import para tipos complejos con patrón completo
// Patron: i-<flujo>-<proceso>-<tipo>-<request/response>-dto.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';
const 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);
// Generar Entity individual para cada interface anidada con patrón correcto
const 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, '-');
const 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);
const suffix = type === 'request' ? 'Request' : 'Response';
const entityClassName = `I${toPascalCase(serviceName)}${cleanOperationName}${formattedFieldType}${suffix}Entity`;
// Export statement con el nombre correcto del archivo
const fileNameForExport = nestedFileName.replace('.ts', '');
exportStatements.push(`export { ${entityClassName} } 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
if (nestedField.type && !['string', 'number', 'boolean', 'any', 'object', 'array'].includes(nestedField.type)) {
// Usar el patrón completo: I<Flujo><Proceso><Tipo><Request/Response>Entity
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);
const nestedSuffix = type === 'request' ? 'Request' : 'Response';
const fieldType = `I${toPascalCase(serviceName)}${cleanOperationName}${formattedNestedTypeName}${nestedSuffix}Entity`;
// Agregar import para tipos complejos con patrón completo
// 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';
const 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(/^-/, '');
// Generar interface del repositorio para el servicio
const repositoryInterface = `import { IConfigDTO } from "@bus/core/interfaces";
import {
${schema.businessOperations.map(operation => {
const rawOperationName = operation.path.split('/').pop() || operation.operationId.toLowerCase();
const operationName = rawOperationName.replace(/_/g, '-');
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const lines = [];
if (operation.responseFields && operation.responseFields.length > 0) {
lines.push(` I${toPascalCase(serviceName)}${cleanOperationName}ResponseDTO`);
}
return lines.join(',\n');
}).filter(line => line).join(',\n')}
} from "@${apiName}/domain/models/apis/${apiName}/business/${serviceNameLower}";
${schema.businessOperations.some(op => op.fields && op.fields.length > 0) ?
`import {
${schema.businessOperations.map(operation => {
const rawOperationName = operation.path.split('/').pop() || operation.operationId.toLowerCase();
const operationName = rawOperationName.replace(/_/g, '-');
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const lines = [];
if (operation.fields && operation.fields.length > 0) {
lines.push(` I${toPascalCase(serviceName)}${cleanOperationName}RequestEntity`);
}
return lines.join(',\n');
}).filter(line => line).join(',\n')}
} from "@${apiName}/infrastructure/entities/apis/${apiName}/business/${serviceNameLower}";`
: ''}
export abstract class I${toPascalCase(serviceName)}Repository {
${schema.businessOperations.map(operation => {
const rawOperationName = operation.path.split('/').pop() || 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 responseType = operation.responseFields && operation.responseFields.length > 0
? `I${toPascalCase(serviceName)}${cleanOperationName}ResponseDTO | null`
: 'any';
// 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}, ` : '';
return ` abstract ${operationCamelCase}(${params}config: IConfigDTO): Promise<${responseType}>;`;
}).join('\n')}
}`;
await fs.writeFile(path.join(paths.domainRepositories, `i-${serviceNameKebab}-repository.ts`), repositoryInterface);
console.log(chalk_1.default.green(`✅ Repository Interface: i-${serviceNameKebab}-repository.ts`));
}
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 = operation.path.split('/').pop() || 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 responseDTOName = `I${toPascalCase(serviceName)}${cleanOperationName}ResponseDTO`;
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}, ${responseDTOName}` : responseDTOName;
const useCaseInterface = hasRequest ? `UseCase<${requestDTOName}, ${responseDTOName} | null>` : `UseCase<any, ${responseDTOName} | null>`;
// Solo importar mapper si tiene request fields
const mapperImport = hasRequest ? `import { InjectionPlatformBusiness${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 "@bus/core/interfaces";
import { UseCase } from "@bus/core/interfaces/use-case";
import { ${dtoImports} } from "@${apiName}/domain/models/apis/${apiName}/business/${serviceNameLower}";
${mapperImport}
import { InjectionPlatformBusinessRepository } from "@${apiName}/infrastructure/repositories/apis/${apiName}/repositories/injection/business/injection-${apiName}-business-repository";
export class ${useCaseClassName} implements ${useCaseInterface} {
private static instance: ${useCaseClassName};
private repository = InjectionPlatformBusinessRepository.${toPascalCase(serviceName)}Repository();${hasRequest ? `
private mapper = InjectionPlatformBusiness${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(/^-/, '');
// Generar facade para el servicio completo
const facadeClassName = `${toPascalCase(serviceName)}Facade`;
const imports = schema.businessOperations
.filter(operation => operation.responseFields && operation.responseFields.length > 0)
.map(operation => {
const rawOperationName = operation.path.split('/').pop() || operation.operationId.toLowerCase();
const operationName = rawOperationName.replace(/_/g, '-');
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
const hasRequest = operation.fields && operation.fields.length > 0;
if (hasRequest) {
return `I${toPascalCase(serviceName)}${cleanOperationName}RequestDTO,\n I${toPascalCase(serviceName)}${cleanOperationName}ResponseDTO`;
}
else {
return `I${toPascalCase(serviceName)}${cleanOperationName}ResponseDTO`;
}
}).join(',\n ');
// Import del injection de use cases (como en entities)
const useCaseInjectionImport = `import { InjectionPlatformBusiness${toPascalCase(serviceName)}UseCase } from "@${apiName}/domain/services/use_cases/apis/${apiName}/injection/business/injection-${apiName}-business-${serviceNameKebab}-use-case";`;
// Use case instances (como en entities)
const useCaseInstances = schema.businessOperations
.filter(operation => operation.responseFields && operation.responseFields.length > 0)
.map(operation => {
const rawOperationName = operation.path.split('/').pop() || 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 las variables
const operationCamelCase = operationName
.replace(/_/g, '-')
.split('-')
.map((word, index) => index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1))
.join('');
return ` private readonly ${operationCamelCase}UseCase = InjectionPlatformBusiness${toPascalCase(serviceName)}UseCase.${toPascalCase(serviceName)}${cleanOperationName}UseCase();`;
}).join('\n');
// Métodos del facade (como en entities)
const facadeMethods = schema.businessOperations
.filter(operation => operation.responseFields && operation.responseFields.length > 0)
.map(operation => {
const rawOperationName = operation.path.split('/').pop() || 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 métodos y variables
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;
const params = hasRequest ? `params: I${toPascalCase(serviceName)}${cleanOperationName}RequestDTO, ` : '';
const args = hasRequest ? 'params, config' : 'config';
return ` public async ${operationCamelCase}(${params}config?: IConfigDTO): Promise<I${toPascalCase(serviceName)}${cleanOperationName}ResponseDTO | null> {
return await this.${operationCamelCase}UseCase.execute(${args});
}`;
}).join('\n\n');
const facade = `import { IConfigDTO } from "@bus/core/interfaces";
import {
${imports},
} from "@${apiName}/domain/models/apis/${apiName}/business/${serviceNameLower}";
${useCaseInjectionImport}
export class ${facadeClassName} {
private static instance: ${facadeClassName};
${useCaseInstances}
public static getInstance(): ${facadeClassName} {
if (!${facadeClassName}.instance)
${facadeClassName}.instance = new ${facadeClassName}();
return ${facadeClassName}.instance;
}
${facadeMethods}
}`;
await fs.writeFile(path.join(paths.facades, `${serviceNameKebab}-facade.ts`), facade);
console.log(chalk_1.default.green(`✅ Facade: ${serviceNameKebab}-facade.ts`));
}
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 = operation.path.split('/').pop() || 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 (si existen)
const nestedMappers = await collectNestedMappersForOperation(operation, 'response', serviceName, operationName);
for (const nestedMapper of nestedMappers) {
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();
}`);
}
}
// Solo crear injection si hay mappers
if (mapperNames.length > 0) {
const injectionClassName = `InjectionPlatformBusiness${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) {
if (field.type && !['string', 'number', 'boolean', 'any', 'object', 'array'].includes(field.type) && !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 = operation.path.split('/').pop() || 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 responseDTOName = `I${toPascalCase(serviceName)}${cleanOperationName}ResponseDTO`;
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);
}
allImports.dtos.add(responseDTOName);
allImports.entities.add(responseEntityName);
allImports.mapperImports.add(`import { InjectionPlatformBusiness${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 = InjectionPlatformBusiness${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}, ` : '';
const methodCall = hasRequest ? 'params' : '{}';
const method = ` public async ${operationCamelCase}(
${methodParams}config: IConfigDTO = CONST_CORE_DTO.CONFIG
): Promise<${responseDTOName} | null> {
if (config.loadService)
return platformAxios
.post(CONST_PLATFORM_API_ROUTES.${serviceName.toUpperCase()}_${operationName.toUpperCase().replace(/-/g, '_')}, ${methodCall})
.then(({ data }) => {
const entity = this.resolve.ResolveRequest<${responseEntityName}>(data);
if (entity)
return this.${operationCamelCaseVar}ResponseMapper.mapFrom(entity);
return null;
});
return null;
}`;
methods.push(method);
}
}
// Generar el archivo completo
const dtoImports = Array.from(allImports.dtos).join(', ');
const entityImports = Array.from(allImports.entities).join(', ');
const mapperImports = Array.from(allImports.mapperImports).join('\n');
const repository = `import { IConfigDTO } from "@bus/core/interfaces";
import platformAxios from "@bus/core/axios/platform-axios";
import { CONST_PLATFORM_API_ROUTES } from "@bus/core/const";
import { CONST_CORE_DTO } from "@bus/core/const/const-core";
import { InjectionCore } from "@bus/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();
${allImports.mapperInstances.join('\n')}
private constructor() {
super();
}
public static getInstance(): ${repositoryClassName} {
if (!${repositoryClassName}.instance)
${repositoryClassName}.instance = new ${repositoryClassName}();
return ${repositoryClassName}.instance;
}
${methods.join('\n\n')}
}`;
await fs.writeFile(path.join(paths.infraRepositories, `${serviceNameKebab}-repository.ts`), repository);
console.log(chalk_1.default.green(`✅ Infrastructure Repository: ${serviceNameKebab}-repository.ts`));
}
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 InjectionPlatformBusinessRepository {
${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(/^-/, '');
// 1. Use Case Injection
const useCaseImports = schema.businessOperations
.filter(operation => operation.responseFields && operation.responseFields.length > 0)
.map(operation => {
const rawOperationName = operation.path.split('/').pop() || 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('');
return `import { ${toPascalCase(serviceName)}${cleanOperationName}UseCase } from "@${apiName}/domain/services/use_cases/apis/${apiName}/business/${serviceName.toLowerCase()}/${serviceNameKebab}-${operationKebab}-use-case";`;
}).join('\n');
const useCaseMethods = schema.businessOperations
.filter(operation => operation.responseFields && operation.responseFields.length > 0)
.map(operation => {
const rawOperationName = operation.path.split('/').pop() || operation.operationId.toLowerCase();
const operationName = rawOperationName.replace(/_/g, '-');
const cleanOperationName = operationName
.replace(/_/g, '-')
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
return ` public static ${toPascalCase(serviceName)}${cleanOperationName}UseCase(): ${toPascalCase(serviceName)}${cleanOperationName}UseCase {
return ${toPascalCase(serviceName)}${cleanOperationName}UseCase.getInstance();
}`;
}).join('\n\n');
const useCaseInjection = `${useCaseImports}
export class InjectionPlatformBusiness${toPascalCase(serviceName)}UseCase {
${useCaseMethods}
}`;
await fs.writeFile(path.join(paths.injectionUseCases, `injection-${apiName}-business-${serviceNameKebab}-use-case.ts`), useCaseInjection);
console.log(chalk_1.default.green(`✅ Use Case Injection: injection-${apiName}-business-${serviceNameKebab}-use-case.ts`));
// 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 InjectionPlatformBusinessFacade {
${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