From 4aeb108c55d71dd475e181ca93fb5209acec5f9a Mon Sep 17 00:00:00 2001 From: blas Date: Tue, 24 Mar 2026 19:15:47 +0100 Subject: [PATCH] feat: add base url mechanism --- main.ts | 30 ++++++++- src/generators/clean-arch.generator.ts | 29 +++++++- src/utils/environment-finder.ts | 47 +++++++++++++ src/utils/prompt.ts | 91 ++++++++++++++++++++++++++ templates/api.repository.impl.mustache | 2 +- 5 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 src/utils/environment-finder.ts create mode 100644 src/utils/prompt.ts diff --git a/main.ts b/main.ts index 6379041..b45e938 100755 --- a/main.ts +++ b/main.ts @@ -10,9 +10,12 @@ import { checkOpenApiGenerator, installOpenApiGenerator } from './src/utils/open import { createDirectoryStructure, cleanup } from './src/utils/filesystem'; import { analyzeSwagger } from './src/swagger/analyzer'; import { generateCode, organizeFiles, addDtoImports } from './src/generators/dto.generator'; -import { generateCleanArchitecture } from './src/generators/clean-arch.generator'; +import { generateCleanArchitecture, extractTagsFromAnalysis } from './src/generators/clean-arch.generator'; import { generateReport } from './src/generators/report.generator'; +import { findEnvironmentFile, parseApiKeys } from './src/utils/environment-finder'; +import { askApiKeysForTags } from './src/utils/prompt'; import type { CliOptions } from './src/types'; +import packageJson from './package.json'; // Desactivar escape HTML para que los literales < y > generen tipos genéricos válidos de TS. (mustache as { escape: (text: string) => string }).escape = function (text: string): string { @@ -24,7 +27,7 @@ import type { CliOptions } from './src/types'; program .name('generate-clean-arch') .description('Generador de código Angular con Clean Architecture desde OpenAPI/Swagger') - .version('1.0.0') + .version(packageJson.version) .option('-i, --input ', 'Archivo OpenAPI/Swagger (yaml o json)', 'swagger.yaml') .option('-o, --output ', 'Directorio de salida', './src/app') .option( @@ -82,10 +85,31 @@ async function main(): Promise { createDirectoryStructure(options.output); + // ── ENVIRONMENT API KEY SELECTION ────────────────────────────────────────── + const tags = extractTagsFromAnalysis(analysis); + const envFile = findEnvironmentFile(process.cwd()); + let apiKeys: ReturnType = []; + + if (envFile) { + const envContent = fs.readFileSync(envFile, 'utf8'); + apiKeys = parseApiKeys(envContent); + logSuccess(`environment.ts encontrado: ${colors.cyan}${path.relative(process.cwd(), envFile)}${colors.reset}`); + if (apiKeys.length > 0) { + logInfo(`Claves de API detectadas: ${apiKeys.map((k) => k.key).join(', ')}`); + } else { + logWarning('No se encontraron claves con "api" en environment.ts. Se solicitará manualmente.'); + } + } else { + logWarning('No se encontró environment.ts. Se solicitará la clave manualmente por repositorio.'); + } + + const tagApiKeyMap = await askApiKeysForTags(tags, apiKeys); + // ────────────────────────────────────────────────────────────────────────── + const tempDir = generateCode(options.input, options.templates); organizeFiles(tempDir, options.output); addDtoImports(options.output); - generateCleanArchitecture(analysis, options.output, options.templates); + generateCleanArchitecture(analysis, options.output, options.templates, tagApiKeyMap); cleanup(tempDir); const report = generateReport(options.output, analysis); diff --git a/src/generators/clean-arch.generator.ts b/src/generators/clean-arch.generator.ts index 17c09f2..8347087 100644 --- a/src/generators/clean-arch.generator.ts +++ b/src/generators/clean-arch.generator.ts @@ -12,11 +12,34 @@ import type { GeneratedCount } from '../types'; +/** + * Extracts the unique tags (in order of appearance) from a SwaggerAnalysis. + * Only endpoints that have at least one tag are considered; the first tag is used. + */ +export function extractTagsFromAnalysis(analysis: SwaggerAnalysis): string[] { + const seen = new Set(); + const tags: string[] = []; + Object.values(analysis.paths).forEach((pathObj) => { + Object.values(pathObj as Record).forEach((opRaw) => { + const op = opRaw as OpenApiOperation; + if (op.tags && op.tags.length > 0) { + const tag = op.tags[0]; + if (!seen.has(tag)) { + seen.add(tag); + tags.push(tag); + } + } + }); + }); + return tags; +} + /** Genera todos los artefactos de Clean Architecture (modelos, mappers, repos, use cases, providers) usando Mustache. */ export function generateCleanArchitecture( analysis: SwaggerAnalysis, outputDir: string, - templatesDir: string + templatesDir: string, + tagApiKeyMap: Record = {} ): GeneratedCount { logStep('Generando artefactos de Clean Architecture usando Mustache...'); const generatedCount: GeneratedCount = { @@ -236,7 +259,9 @@ export function generateCleanArchitecture( // Return-type-only imports — for repo impl (Dto + Entity + Mapper) returnImports, // Param-only imports — for repo impl (Entity only, no Dto/Mapper) - paramImports + paramImports, + // Environment API key for the repository base URL (e.g. "aprovalmApi") + environmentApiKey: tagApiKeyMap[tag] || 'apiUrl' } } ] diff --git a/src/utils/environment-finder.ts b/src/utils/environment-finder.ts new file mode 100644 index 0000000..d40f85b --- /dev/null +++ b/src/utils/environment-finder.ts @@ -0,0 +1,47 @@ +import fs from 'fs-extra'; +import path from 'path'; + +export interface ApiKeyInfo { + key: string; + url?: string; +} + +const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.angular', 'coverage', '.cache']); + +export function findEnvironmentFile(dir: string, maxDepth = 8, currentDepth = 0): string | null { + if (currentDepth > maxDepth) return null; + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + if (SKIP_DIRS.has(entry.name)) continue; + const fullPath = path.join(dir, entry.name); + if (entry.isFile() && entry.name === 'environment.ts') { + return fullPath; + } + if (entry.isDirectory()) { + const found = findEnvironmentFile(fullPath, maxDepth, currentDepth + 1); + if (found) return found; + } + } + } catch {} + return null; +} + +/** + * Parses environment.ts content and returns all top-level keys that contain "api" + * (case-insensitive), along with their `url` value if present. + */ +export function parseApiKeys(content: string): ApiKeyInfo[] { + const result: ApiKeyInfo[] = []; + const keyRegex = /^ {2}(\w*[Aa][Pp][Ii]\w*)\s*:/gm; + let match: RegExpExecArray | null; + + while ((match = keyRegex.exec(content)) !== null) { + const key = match[1]; + const afterKey = content.slice(match.index); + const urlMatch = afterKey.match(/url:\s*['"`]([^'"`\n]+)['"`]/); + result.push({ key, url: urlMatch?.[1] }); + } + + return result; +} diff --git a/src/utils/prompt.ts b/src/utils/prompt.ts new file mode 100644 index 0000000..bc86a64 --- /dev/null +++ b/src/utils/prompt.ts @@ -0,0 +1,91 @@ +import readline from 'readline'; +import { ApiKeyInfo } from './environment-finder'; +import { colors } from './logger'; + +function ask(rl: readline.Interface, query: string): Promise { + return new Promise((resolve) => rl.question(query, resolve)); +} + +/** + * Interactively asks the user which environment API key to use for each tag. + * Returns a map of tag → environment key (e.g. { "SupplyingMaintenances": "suppliyingMaintenancesApi" }). + */ +export async function askApiKeysForTags( + tags: string[], + apiKeys: ApiKeyInfo[] +): Promise> { + const result: Record = {}; + + if (tags.length === 0) return result; + + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + + console.log('\n' + '─'.repeat(60)); + console.log(`${colors.bright}🔑 Configuración de URLs base para repositorios${colors.reset}`); + console.log('─'.repeat(60)); + + for (const tag of tags) { + result[tag] = await askApiKeyForTag(rl, tag, apiKeys); + } + + rl.close(); + return result; +} + +async function askApiKeyForTag( + rl: readline.Interface, + tagName: string, + apiKeys: ApiKeyInfo[] +): Promise { + console.log( + `\n Repositorio: ${colors.cyan}${tagName}RepositoryImpl${colors.reset}` + ); + + if (apiKeys.length > 0) { + console.log(` Selecciona la clave de environment para la URL base:\n`); + apiKeys.forEach((k, i) => { + const urlText = k.url ? `\n ${colors.cyan}${k.url}${colors.reset}` : ''; + console.log(` ${colors.bright}${i + 1})${colors.reset} ${k.key}${urlText}`); + }); + console.log(` ${colors.bright}${apiKeys.length + 1})${colors.reset} Escribir manualmente`); + } else { + console.log(` No se encontraron claves de API en environment.ts.`); + console.log(` Escribe la clave manualmente (ej: myApi):\n`); + } + + while (true) { + const answer = (await ask(rl, `\n > `)).trim(); + + if (apiKeys.length > 0) { + const num = parseInt(answer, 10); + if (!isNaN(num) && num >= 1 && num <= apiKeys.length) { + const chosen = apiKeys[num - 1].key; + console.log(` ✅ ${colors.bright}environment.${chosen}.url${colors.reset}`); + return chosen; + } + if (num === apiKeys.length + 1 || answer === '') { + const manual = (await ask(rl, ` Escribe la clave (ej: myApi): `)).trim(); + if (manual) { + console.log(` ✅ ${colors.bright}environment.${manual}.url${colors.reset}`); + return manual; + } + console.log(` ⚠️ La clave no puede estar vacía.`); + continue; + } + + if (answer && isNaN(num)) { + console.log(` ✅ ${colors.bright}environment.${answer}.url${colors.reset}`); + return answer; + } + console.log( + ` ⚠️ Opción inválida. Elige un número del 1 al ${apiKeys.length + 1} o escribe la clave directamente.` + ); + } else { + if (answer) { + console.log(` ✅ ${colors.bright}environment.${answer}.url${colors.reset}`); + return answer; + } + console.log(` ⚠️ La clave no puede estar vacía.`); + } + } +} diff --git a/templates/api.repository.impl.mustache b/templates/api.repository.impl.mustache index 80bc23b..e40cb8a 100644 --- a/templates/api.repository.impl.mustache +++ b/templates/api.repository.impl.mustache @@ -26,7 +26,7 @@ import { {{classname}} } from '@/entities/models/{{classFilename}}.model'; @Injectable() export class {{classname}}RepositoryImpl extends MRepository implements {{classname}}Repository { constructor() { - super(`${environment.modapApi.url}`); + super(`${environment.{{environmentApiKey}}.url}`); } {{#operation}}