Compare commits
19 Commits
v0.0.1-alp
...
v1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cd25d2b90 | |||
| bada7ba0e9 | |||
| b8d2fd8582 | |||
| a90f7ba078 | |||
| 4aeb108c55 | |||
| a9bbf21317 | |||
| 324e4f458c | |||
|
|
5229a3ad45 | ||
|
|
ca63b85086 | ||
|
|
e008144813 | ||
|
|
73dcb6f701 | ||
| ac9d4ceb99 | |||
|
|
e8c919ee76 | ||
|
|
a97c3b22fa | ||
|
|
031bdb4b3a | ||
|
|
82b734bc55 | ||
|
|
f4d9340137 | ||
|
|
fc3d95052c | ||
|
|
3fe2333a03 |
43
main.ts
43
main.ts
@@ -10,9 +10,13 @@ import { checkOpenApiGenerator, installOpenApiGenerator } from './src/utils/open
|
|||||||
import { createDirectoryStructure, cleanup } from './src/utils/filesystem';
|
import { createDirectoryStructure, cleanup } from './src/utils/filesystem';
|
||||||
import { analyzeSwagger } from './src/swagger/analyzer';
|
import { analyzeSwagger } from './src/swagger/analyzer';
|
||||||
import { generateCode, organizeFiles, addDtoImports } from './src/generators/dto.generator';
|
import { generateCode, organizeFiles, addDtoImports } from './src/generators/dto.generator';
|
||||||
import { generateCleanArchitecture } from './src/generators/clean-arch.generator';
|
import { generateCleanArchitecture, extractTagsWithOperations } from './src/generators/clean-arch.generator';
|
||||||
import { generateReport } from './src/generators/report.generator';
|
import { generateReport } from './src/generators/report.generator';
|
||||||
|
import { findEnvironmentFile, parseApiKeys } from './src/utils/environment-finder';
|
||||||
|
import { askApiKeysForTags, askSelectionFilter } from './src/utils/prompt';
|
||||||
|
import type { SelectionFilter } from './src/types';
|
||||||
import type { CliOptions } from './src/types';
|
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.
|
// 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 {
|
(mustache as { escape: (text: string) => string }).escape = function (text: string): string {
|
||||||
@@ -24,7 +28,7 @@ import type { CliOptions } from './src/types';
|
|||||||
program
|
program
|
||||||
.name('generate-clean-arch')
|
.name('generate-clean-arch')
|
||||||
.description('Generador de código Angular con Clean Architecture desde OpenAPI/Swagger')
|
.description('Generador de código Angular con Clean Architecture desde OpenAPI/Swagger')
|
||||||
.version('1.0.0')
|
.version(packageJson.version)
|
||||||
.option('-i, --input <file>', 'Archivo OpenAPI/Swagger (yaml o json)', 'swagger.yaml')
|
.option('-i, --input <file>', 'Archivo OpenAPI/Swagger (yaml o json)', 'swagger.yaml')
|
||||||
.option('-o, --output <dir>', 'Directorio de salida', './src/app')
|
.option('-o, --output <dir>', 'Directorio de salida', './src/app')
|
||||||
.option(
|
.option(
|
||||||
@@ -34,6 +38,7 @@ program
|
|||||||
)
|
)
|
||||||
.option('--skip-install', 'No instalar dependencias')
|
.option('--skip-install', 'No instalar dependencias')
|
||||||
.option('--dry-run', 'Simular sin generar archivos')
|
.option('--dry-run', 'Simular sin generar archivos')
|
||||||
|
.option('-s, --select-endpoints', 'Seleccionar interactivamente qué tags y endpoints generar')
|
||||||
.parse(process.argv);
|
.parse(process.argv);
|
||||||
|
|
||||||
const options = program.opts<CliOptions>();
|
const options = program.opts<CliOptions>();
|
||||||
@@ -82,10 +87,42 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
createDirectoryStructure(options.output);
|
createDirectoryStructure(options.output);
|
||||||
|
|
||||||
|
// ── SELECTION: tags and endpoints ─────────────────────────────────────────
|
||||||
|
const tagSummaries = extractTagsWithOperations(analysis);
|
||||||
|
let selectionFilter: SelectionFilter = {};
|
||||||
|
|
||||||
|
if (options.selectEndpoints) {
|
||||||
|
selectionFilter = await askSelectionFilter(tagSummaries);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedTags = options.selectEndpoints
|
||||||
|
? Object.keys(selectionFilter)
|
||||||
|
: tagSummaries.map((t) => t.tag);
|
||||||
|
|
||||||
|
// ── ENVIRONMENT API KEY SELECTION ──────────────────────────────────────────
|
||||||
|
const envFile = findEnvironmentFile(process.cwd());
|
||||||
|
let apiKeys: ReturnType<typeof parseApiKeys> = [];
|
||||||
|
|
||||||
|
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(selectedTags, apiKeys);
|
||||||
|
// ──────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const tempDir = generateCode(options.input, options.templates);
|
const tempDir = generateCode(options.input, options.templates);
|
||||||
organizeFiles(tempDir, options.output);
|
organizeFiles(tempDir, options.output);
|
||||||
addDtoImports(options.output);
|
addDtoImports(options.output);
|
||||||
generateCleanArchitecture(analysis, options.output, options.templates);
|
generateCleanArchitecture(analysis, options.output, options.templates, tagApiKeyMap, selectionFilter);
|
||||||
cleanup(tempDir);
|
cleanup(tempDir);
|
||||||
|
|
||||||
const report = generateReport(options.output, analysis);
|
const report = generateReport(options.output, analysis);
|
||||||
|
|||||||
49
package-lock.json
generated
49
package-lock.json
generated
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "openapi-clean-arch-generator",
|
"name": "@blas/openapi-clean-arch-generator",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "openapi-clean-arch-generator",
|
"name": "@blas/openapi-clean-arch-generator",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -13,10 +13,11 @@
|
|||||||
"commander": "^11.1.0",
|
"commander": "^11.1.0",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"mustache": "^4.2.0"
|
"mustache": "^4.2.0",
|
||||||
|
"prompts": "^2.4.2"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"generate-clean-arch": "dist/generate.js"
|
"generate-clean-arch": "dist/main.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/mustache": "^4.2.6",
|
"@types/mustache": "^4.2.6",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
|
"@types/prompts": "^2.4.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
||||||
"@typescript-eslint/parser": "^8.57.1",
|
"@typescript-eslint/parser": "^8.57.1",
|
||||||
"eslint": "^10.1.0",
|
"eslint": "^10.1.0",
|
||||||
@@ -354,6 +356,17 @@
|
|||||||
"undici-types": "~7.18.0"
|
"undici-types": "~7.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/prompts": {
|
||||||
|
"version": "2.4.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz",
|
||||||
|
"integrity": "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"kleur": "^3.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.57.2",
|
"version": "8.57.2",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz",
|
||||||
@@ -1293,6 +1306,15 @@
|
|||||||
"json-buffer": "3.0.1"
|
"json-buffer": "3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/kleur": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
@@ -1491,6 +1513,19 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prompts": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"kleur": "^3.0.3",
|
||||||
|
"sisteransi": "^1.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -1537,6 +1572,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sisteransi": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/supports-color": {
|
"node_modules/supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@blas/openapi-clean-arch-generator",
|
"name": "@blas/openapi-clean-arch-generator",
|
||||||
"version": "0.0.1-alpha",
|
"version": "1.0.0",
|
||||||
"description": "Generador de código Angular con Clean Architecture desde OpenAPI/Swagger",
|
"description": "Generador de código Angular con Clean Architecture desde OpenAPI/Swagger",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -41,7 +41,8 @@
|
|||||||
"commander": "^11.1.0",
|
"commander": "^11.1.0",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"mustache": "^4.2.0"
|
"mustache": "^4.2.0",
|
||||||
|
"prompts": "^2.4.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/mustache": "^4.2.6",
|
"@types/mustache": "^4.2.6",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
|
"@types/prompts": "^2.4.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
||||||
"@typescript-eslint/parser": "^8.57.1",
|
"@typescript-eslint/parser": "^8.57.1",
|
||||||
"eslint": "^10.1.0",
|
"eslint": "^10.1.0",
|
||||||
|
|||||||
@@ -3,19 +3,68 @@ import path from 'path';
|
|||||||
import mustache from 'mustache';
|
import mustache from 'mustache';
|
||||||
import { logStep, logSuccess, logInfo } from '../utils/logger';
|
import { logStep, logSuccess, logInfo } from '../utils/logger';
|
||||||
import { mapSwaggerTypeToTs } from '../utils/type-mapper';
|
import { mapSwaggerTypeToTs } from '../utils/type-mapper';
|
||||||
|
import { toCamelCase } from '../utils/name-formatter';
|
||||||
import type {
|
import type {
|
||||||
SwaggerAnalysis,
|
SwaggerAnalysis,
|
||||||
OpenApiSchema,
|
OpenApiSchema,
|
||||||
OpenApiOperation,
|
OpenApiOperation,
|
||||||
TagOperation,
|
TagOperation,
|
||||||
|
TagSummary,
|
||||||
|
SelectionFilter,
|
||||||
GeneratedCount
|
GeneratedCount
|
||||||
} from '../types';
|
} 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<string>();
|
||||||
|
const tags: string[] = [];
|
||||||
|
Object.values(analysis.paths).forEach((pathObj) => {
|
||||||
|
Object.values(pathObj as Record<string, unknown>).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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts all tags with their operations summary for the interactive selection screen.
|
||||||
|
*/
|
||||||
|
export function extractTagsWithOperations(analysis: SwaggerAnalysis): TagSummary[] {
|
||||||
|
const map = new Map<string, TagSummary>();
|
||||||
|
Object.entries(analysis.paths).forEach(([pathKey, pathObj]) => {
|
||||||
|
Object.entries(pathObj as Record<string, unknown>).forEach(([method, opRaw]) => {
|
||||||
|
const op = opRaw as OpenApiOperation;
|
||||||
|
if (!op.tags?.length) return;
|
||||||
|
const tag = op.tags[0];
|
||||||
|
if (!map.has(tag)) map.set(tag, { tag, operations: [] });
|
||||||
|
map.get(tag)!.operations.push({
|
||||||
|
nickname: op.operationId || `${method}${pathKey.replace(/\//g, '_')}`,
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
path: pathKey,
|
||||||
|
summary: op.summary || ''
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return [...map.values()];
|
||||||
|
}
|
||||||
|
|
||||||
/** Genera todos los artefactos de Clean Architecture (modelos, mappers, repos, use cases, providers) usando Mustache. */
|
/** Genera todos los artefactos de Clean Architecture (modelos, mappers, repos, use cases, providers) usando Mustache. */
|
||||||
export function generateCleanArchitecture(
|
export function generateCleanArchitecture(
|
||||||
analysis: SwaggerAnalysis,
|
analysis: SwaggerAnalysis,
|
||||||
outputDir: string,
|
outputDir: string,
|
||||||
templatesDir: string
|
templatesDir: string,
|
||||||
|
tagApiKeyMap: Record<string, string> = {},
|
||||||
|
selectionFilter: SelectionFilter = {}
|
||||||
): GeneratedCount {
|
): GeneratedCount {
|
||||||
logStep('Generando artefactos de Clean Architecture usando Mustache...');
|
logStep('Generando artefactos de Clean Architecture usando Mustache...');
|
||||||
const generatedCount: GeneratedCount = {
|
const generatedCount: GeneratedCount = {
|
||||||
@@ -43,7 +92,7 @@ export function generateCleanArchitecture(
|
|||||||
if (rawProperties[k].$ref) {
|
if (rawProperties[k].$ref) {
|
||||||
tsType = rawProperties[k].$ref.split('/').pop() || 'unknown';
|
tsType = rawProperties[k].$ref.split('/').pop() || 'unknown';
|
||||||
} else if (rawProperties[k].type === 'array' && rawProperties[k].items?.$ref) {
|
} else if (rawProperties[k].type === 'array' && rawProperties[k].items?.$ref) {
|
||||||
tsType = `Array<${rawProperties[k].items.$ref.split('/').pop()}>`;
|
tsType = `${rawProperties[k].items.$ref.split('/').pop()}[]`;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
name: k,
|
name: k,
|
||||||
@@ -53,14 +102,28 @@ export function generateCleanArchitecture(
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Collect imports for types referenced via $ref in properties
|
||||||
|
const referencedTypes = new Set<string>();
|
||||||
|
Object.values(rawProperties).forEach((prop) => {
|
||||||
|
if (prop.$ref) {
|
||||||
|
referencedTypes.add(prop.$ref.split('/').pop() || '');
|
||||||
|
} else if (prop.type === 'array' && prop.items?.$ref) {
|
||||||
|
referencedTypes.add(prop.items.$ref.split('/').pop() || '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const modelImports = [...referencedTypes]
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((name) => ({ classname: name, classFilename: toCamelCase(name) }));
|
||||||
|
|
||||||
const modelViewData = {
|
const modelViewData = {
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
model: {
|
model: {
|
||||||
classname: baseName,
|
classname: baseName,
|
||||||
classFilename: baseName.toLowerCase(),
|
classFilename: toCamelCase(baseName),
|
||||||
classVarName: baseName.charAt(0).toLowerCase() + baseName.slice(1),
|
classVarName: toCamelCase(baseName),
|
||||||
description: schemaObj.description || '',
|
description: schemaObj.description || '',
|
||||||
|
imports: modelImports,
|
||||||
vars: varsMap
|
vars: varsMap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,8 +138,8 @@ export function generateCleanArchitecture(
|
|||||||
{
|
{
|
||||||
operations: {
|
operations: {
|
||||||
classname: baseName,
|
classname: baseName,
|
||||||
classFilename: baseName.toLowerCase(),
|
classFilename: toCamelCase(baseName),
|
||||||
classVarName: baseName.charAt(0).toLowerCase() + baseName.slice(1)
|
classVarName: toCamelCase(baseName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -88,14 +151,10 @@ export function generateCleanArchitecture(
|
|||||||
if (fs.existsSync(modelTemplatePath)) {
|
if (fs.existsSync(modelTemplatePath)) {
|
||||||
const template = fs.readFileSync(modelTemplatePath, 'utf8');
|
const template = fs.readFileSync(modelTemplatePath, 'utf8');
|
||||||
const output = mustache.render(template, modelViewData);
|
const output = mustache.render(template, modelViewData);
|
||||||
const destPath = path.join(
|
const destPath = path.join(outputDir, 'entities/models', `${toCamelCase(baseName)}.model.ts`);
|
||||||
outputDir,
|
|
||||||
'entities/models',
|
|
||||||
`${baseName.toLowerCase()}.model.ts`
|
|
||||||
);
|
|
||||||
fs.writeFileSync(destPath, output);
|
fs.writeFileSync(destPath, output);
|
||||||
generatedCount.models++;
|
generatedCount.models++;
|
||||||
logInfo(` ${baseName.toLowerCase()}.model.ts → ${path.relative(process.cwd(), destPath)}`);
|
logInfo(` ${toCamelCase(baseName)}.model.ts → ${path.relative(process.cwd(), destPath)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mapper
|
// Mapper
|
||||||
@@ -103,7 +162,7 @@ export function generateCleanArchitecture(
|
|||||||
if (fs.existsSync(mapperTemplatePath)) {
|
if (fs.existsSync(mapperTemplatePath)) {
|
||||||
const template = fs.readFileSync(mapperTemplatePath, 'utf8');
|
const template = fs.readFileSync(mapperTemplatePath, 'utf8');
|
||||||
const output = mustache.render(template, mapperViewData);
|
const output = mustache.render(template, mapperViewData);
|
||||||
const destPath = path.join(outputDir, 'data/mappers', `${baseName.toLowerCase()}.mapper.ts`);
|
const destPath = path.join(outputDir, 'data/mappers', `${toCamelCase(baseName)}.mapper.ts`);
|
||||||
fs.writeFileSync(destPath, output);
|
fs.writeFileSync(destPath, output);
|
||||||
generatedCount.mappers++;
|
generatedCount.mappers++;
|
||||||
}
|
}
|
||||||
@@ -152,7 +211,7 @@ export function generateCleanArchitecture(
|
|||||||
returnBaseType = returnType;
|
returnBaseType = returnType;
|
||||||
} else if (responseSchema.type === 'array' && responseSchema.items?.$ref) {
|
} else if (responseSchema.type === 'array' && responseSchema.items?.$ref) {
|
||||||
returnBaseType = responseSchema.items.$ref.split('/').pop() || 'unknown';
|
returnBaseType = responseSchema.items.$ref.split('/').pop() || 'unknown';
|
||||||
returnType = `Array<${returnBaseType}>`;
|
returnType = `${returnBaseType}[]`;
|
||||||
isListContainer = true;
|
isListContainer = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,6 +237,8 @@ export function generateCleanArchitecture(
|
|||||||
bodyParam: 'body',
|
bodyParam: 'body',
|
||||||
returnType: returnType !== 'void' ? returnType : false,
|
returnType: returnType !== 'void' ? returnType : false,
|
||||||
returnBaseType: returnBaseType !== 'void' ? returnBaseType : false,
|
returnBaseType: returnBaseType !== 'void' ? returnBaseType : false,
|
||||||
|
returnTypeVarName: returnType !== 'void' ? toCamelCase(returnType) : false,
|
||||||
|
returnBaseTypeVarName: returnBaseType !== 'void' ? toCamelCase(returnBaseType) : false,
|
||||||
isListContainer: isListContainer,
|
isListContainer: isListContainer,
|
||||||
vendorExtensions: {}
|
vendorExtensions: {}
|
||||||
});
|
});
|
||||||
@@ -185,16 +246,37 @@ export function generateCleanArchitecture(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (Object.keys(selectionFilter).length > 0) {
|
||||||
|
Object.keys(tagsMap).forEach((tag) => {
|
||||||
|
if (!selectionFilter[tag]) {
|
||||||
|
delete tagsMap[tag];
|
||||||
|
} else {
|
||||||
|
tagsMap[tag] = tagsMap[tag].filter((op) => selectionFilter[tag].includes(op.nickname));
|
||||||
|
if (tagsMap[tag].length === 0) delete tagsMap[tag];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Generar por cada Tag
|
// Generar por cada Tag
|
||||||
Object.keys(tagsMap).forEach((tag) => {
|
Object.keys(tagsMap).forEach((tag) => {
|
||||||
const imports: { classname: string; classFilename: string; classVarName: string }[] = [];
|
const returnImports: { classname: string; classFilename: string; classVarName: string }[] = [];
|
||||||
|
const paramImports: { classname: string; classFilename: string; classVarName: string }[] = [];
|
||||||
|
|
||||||
Object.keys(schemas).forEach((s) => {
|
Object.keys(schemas).forEach((s) => {
|
||||||
if (tagsMap[tag].some((op) => op.returnType === s || op.returnType === `Array<${s}>`)) {
|
const usedAsReturn = tagsMap[tag].some(
|
||||||
imports.push({
|
(op) => op.returnType === s || op.returnType === `${s}[]`
|
||||||
classname: s,
|
);
|
||||||
classFilename: s.toLowerCase(),
|
const usedAsParam = tagsMap[tag].some((op) =>
|
||||||
classVarName: s.charAt(0).toLowerCase() + s.slice(1)
|
op.allParams.some((p) => p.dataType === s || p.dataType === `${s}[]`)
|
||||||
});
|
);
|
||||||
|
|
||||||
|
const entry = { classname: s, classFilename: toCamelCase(s), classVarName: toCamelCase(s) };
|
||||||
|
|
||||||
|
if (usedAsReturn) {
|
||||||
|
returnImports.push(entry);
|
||||||
|
} else if (usedAsParam) {
|
||||||
|
// Param-only types: entity import needed for method signatures, but no Dto or Mapper
|
||||||
|
paramImports.push(entry);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -204,10 +286,18 @@ export function generateCleanArchitecture(
|
|||||||
{
|
{
|
||||||
operations: {
|
operations: {
|
||||||
classname: tag,
|
classname: tag,
|
||||||
classFilename: tag.toLowerCase(),
|
classFilename: toCamelCase(tag),
|
||||||
|
classVarName: toCamelCase(tag),
|
||||||
constantName: tag.toUpperCase().replace(/[^A-Z0-9]/g, '_'),
|
constantName: tag.toUpperCase().replace(/[^A-Z0-9]/g, '_'),
|
||||||
operation: tagsMap[tag],
|
operation: tagsMap[tag],
|
||||||
imports: imports
|
// All entity imports (return + param) — for contracts and use-cases
|
||||||
|
imports: [...returnImports, ...paramImports],
|
||||||
|
// Return-type-only imports — for repo impl (Dto + Entity + Mapper)
|
||||||
|
returnImports,
|
||||||
|
// Param-only imports — for repo impl (Entity only, no Dto/Mapper)
|
||||||
|
paramImports,
|
||||||
|
// Environment API key for the repository base URL (e.g. "aprovalmApi")
|
||||||
|
environmentApiKey: tagApiKeyMap[tag] || 'apiUrl'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -218,7 +308,7 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'api.use-cases.contract.mustache',
|
'api.use-cases.contract.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'domain/use-cases', `${tag.toLowerCase()}.use-cases.contract.ts`),
|
path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.contract.ts`),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'useCases'
|
'useCases'
|
||||||
);
|
);
|
||||||
@@ -227,7 +317,7 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'api.use-cases.impl.mustache',
|
'api.use-cases.impl.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'domain/use-cases', `${tag.toLowerCase()}.use-cases.impl.ts`),
|
path.join(outputDir, 'domain/use-cases', `${toCamelCase(tag)}.use-cases.impl.ts`),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'useCases'
|
'useCases'
|
||||||
);
|
);
|
||||||
@@ -236,7 +326,7 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'api.repository.contract.mustache',
|
'api.repository.contract.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'domain/repositories', `${tag.toLowerCase()}.repository.contract.ts`),
|
path.join(outputDir, 'domain/repositories', `${toCamelCase(tag)}.repository.contract.ts`),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'repositories'
|
'repositories'
|
||||||
);
|
);
|
||||||
@@ -245,7 +335,7 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'api.repository.impl.mustache',
|
'api.repository.impl.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'data/repositories', `${tag.toLowerCase()}.repository.impl.ts`),
|
path.join(outputDir, 'data/repositories', `${toCamelCase(tag)}.repository.impl.ts`),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'repositories'
|
'repositories'
|
||||||
);
|
);
|
||||||
@@ -254,7 +344,7 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'use-cases.provider.mustache',
|
'use-cases.provider.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'di/use-cases', `${tag.toLowerCase()}.use-cases.provider.ts`),
|
path.join(outputDir, 'di/use-cases', `${toCamelCase(tag)}.use-cases.provider.ts`),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'providers'
|
'providers'
|
||||||
);
|
);
|
||||||
@@ -263,7 +353,7 @@ export function generateCleanArchitecture(
|
|||||||
templatesDir,
|
templatesDir,
|
||||||
'repository.provider.mustache',
|
'repository.provider.mustache',
|
||||||
apiViewData,
|
apiViewData,
|
||||||
path.join(outputDir, 'di/repositories', `${tag.toLowerCase()}.repository.provider.ts`),
|
path.join(outputDir, 'di/repositories', `${toCamelCase(tag)}.repository.provider.ts`),
|
||||||
generatedCount,
|
generatedCount,
|
||||||
'providers'
|
'providers'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function organizeFiles(tempDir: string, outputDir: string): void {
|
|||||||
logSuccess(`${filesMoved} DTOs movidos correctamente`);
|
logSuccess(`${filesMoved} DTOs movidos correctamente`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Post-procesa los DTOs generados añadiendo los imports de tipos referenciados. */
|
/** Post-procesa los DTOs generados añadiendo imports y normalizando Array<T> → T[]. */
|
||||||
export function addDtoImports(outputDir: string): void {
|
export function addDtoImports(outputDir: string): void {
|
||||||
logStep('Añadiendo imports a los DTOs generados...');
|
logStep('Añadiendo imports a los DTOs generados...');
|
||||||
|
|
||||||
@@ -85,11 +85,15 @@ export function addDtoImports(outputDir: string): void {
|
|||||||
|
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
const filePath = path.join(dtosDir, file);
|
const filePath = path.join(dtosDir, file);
|
||||||
let content = fs.readFileSync(filePath, 'utf8');
|
const originalContent = fs.readFileSync(filePath, 'utf8');
|
||||||
|
let content = originalContent;
|
||||||
|
|
||||||
const selfMatch = content.match(/export interface (\w+)/);
|
const selfMatch = content.match(/export interface (\w+)/);
|
||||||
const selfName = selfMatch ? selfMatch[1] : '';
|
const selfName = selfMatch ? selfMatch[1] : '';
|
||||||
|
|
||||||
|
// Normalize Array<T> → T[] (openapi-generator-cli always outputs Array<T>)
|
||||||
|
content = content.replace(/Array<(\w+)>/g, '$1[]');
|
||||||
|
|
||||||
// Find all Dto type references in the file body (excluding the interface name itself)
|
// Find all Dto type references in the file body (excluding the interface name itself)
|
||||||
const references = new Set<string>();
|
const references = new Set<string>();
|
||||||
const typeRegex = /\b(\w+Dto)\b/g;
|
const typeRegex = /\b(\w+Dto)\b/g;
|
||||||
@@ -100,8 +104,6 @@ export function addDtoImports(outputDir: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (references.size === 0) return;
|
|
||||||
|
|
||||||
// Build import lines for each referenced type that exists in the dtoMap
|
// Build import lines for each referenced type that exists in the dtoMap
|
||||||
const imports: string[] = [];
|
const imports: string[] = [];
|
||||||
references.forEach((ref) => {
|
references.forEach((ref) => {
|
||||||
@@ -112,9 +114,12 @@ export function addDtoImports(outputDir: string): void {
|
|||||||
|
|
||||||
if (imports.length > 0) {
|
if (imports.length > 0) {
|
||||||
content = imports.join('\n') + '\n' + content;
|
content = imports.join('\n') + '\n' + content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content !== originalContent) {
|
||||||
fs.writeFileSync(filePath, content);
|
fs.writeFileSync(filePath, content);
|
||||||
filesProcessed++;
|
filesProcessed++;
|
||||||
logInfo(` Imports añadidos a ${file}`);
|
logInfo(` Procesado ${file}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ export interface CliOptions {
|
|||||||
templates: string;
|
templates: string;
|
||||||
skipInstall?: boolean;
|
skipInstall?: boolean;
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
|
selectEndpoints?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Resumen de un endpoint para mostrar en la pantalla de selección interactiva.
|
||||||
|
*/
|
||||||
|
export interface OperationSummary {
|
||||||
|
nickname: string;
|
||||||
|
method: string;
|
||||||
|
path: string;
|
||||||
|
summary: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag con sus endpoints resumidos, para la pantalla de selección interactiva.
|
||||||
|
*/
|
||||||
|
export interface TagSummary {
|
||||||
|
tag: string;
|
||||||
|
operations: OperationSummary[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapa de filtro de selección: tag → array de nicknames de operaciones seleccionadas.
|
||||||
|
*/
|
||||||
|
export type SelectionFilter = Record<string, string[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representación simplificada de un schema de componente OpenAPI.
|
* Representación simplificada de un schema de componente OpenAPI.
|
||||||
* Se utiliza para generar modelos (entidades) y mappers.
|
* Se utiliza para generar modelos (entidades) y mappers.
|
||||||
@@ -65,19 +88,29 @@ export interface OpenApiOperation {
|
|||||||
* Operación normalizada y lista para ser consumida por los templates Mustache.
|
* Operación normalizada y lista para ser consumida por los templates Mustache.
|
||||||
* Cada instancia representa un endpoint agrupado bajo un tag del API.
|
* Cada instancia representa un endpoint agrupado bajo un tag del API.
|
||||||
*/
|
*/
|
||||||
|
export interface TagOperationParam {
|
||||||
|
paramName: string;
|
||||||
|
dataType: string;
|
||||||
|
description: string;
|
||||||
|
required: boolean;
|
||||||
|
'-last': boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TagOperation {
|
export interface TagOperation {
|
||||||
nickname: string;
|
nickname: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
notes: string;
|
notes: string;
|
||||||
httpMethod: string;
|
httpMethod: string;
|
||||||
path: string;
|
path: string;
|
||||||
allParams: unknown[];
|
allParams: TagOperationParam[];
|
||||||
hasQueryParams: boolean;
|
hasQueryParams: boolean;
|
||||||
queryParams: unknown[];
|
queryParams: unknown[];
|
||||||
hasBodyParam: boolean;
|
hasBodyParam: boolean;
|
||||||
bodyParam: string;
|
bodyParam: string;
|
||||||
returnType: string | boolean;
|
returnType: string | boolean;
|
||||||
returnBaseType: string | boolean;
|
returnBaseType: string | boolean;
|
||||||
|
returnTypeVarName: string | boolean;
|
||||||
|
returnBaseTypeVarName: string | boolean;
|
||||||
isListContainer: boolean;
|
isListContainer: boolean;
|
||||||
vendorExtensions: Record<string, unknown>;
|
vendorExtensions: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|||||||
47
src/utils/environment-finder.ts
Normal file
47
src/utils/environment-finder.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
12
src/utils/name-formatter.ts
Normal file
12
src/utils/name-formatter.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Converts a PascalCase name to camelCase by lowercasing the first character.
|
||||||
|
* Used to derive class filenames and variable names from schema/tag names.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* toCamelCase('ProductResponse') // 'productResponse'
|
||||||
|
* toCamelCase('UserSchema') // 'userSchema'
|
||||||
|
*/
|
||||||
|
export function toCamelCase(name: string): string {
|
||||||
|
if (!name) return name;
|
||||||
|
return name.charAt(0).toLowerCase() + name.slice(1);
|
||||||
|
}
|
||||||
183
src/utils/prompt.ts
Normal file
183
src/utils/prompt.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import prompts from 'prompts';
|
||||||
|
import { ApiKeyInfo } from './environment-finder';
|
||||||
|
import { colors } from './logger';
|
||||||
|
import type { TagSummary, SelectionFilter } from '../types';
|
||||||
|
|
||||||
|
function clearScreen(): void {
|
||||||
|
process.stdout.write('\x1Bc');
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHeader(current?: number, total?: number): void {
|
||||||
|
const stepText =
|
||||||
|
current !== undefined && total !== undefined
|
||||||
|
? ` [${colors.cyan}${current}${colors.reset} de ${colors.cyan}${total}${colors.reset}]`
|
||||||
|
: '';
|
||||||
|
console.log(`\n ${colors.bright}🔑 Configuración de URLs base${colors.reset}${stepText}`);
|
||||||
|
console.log(` ${'─'.repeat(54)}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function printSummary(tags: string[], result: Record<string, string>): void {
|
||||||
|
clearScreen();
|
||||||
|
console.log(`\n ${colors.bright}✅ Configuración completada${colors.reset}`);
|
||||||
|
console.log(` ${'─'.repeat(54)}\n`);
|
||||||
|
tags.forEach((tag) => {
|
||||||
|
console.log(` ${colors.bright}${tag}${colors.reset}`);
|
||||||
|
console.log(` ${colors.cyan}environment.${result[tag]}.url${colors.reset}\n`);
|
||||||
|
});
|
||||||
|
console.log(` ${'─'.repeat(54)}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interactively asks the user which tags and endpoints to generate.
|
||||||
|
* Returns a SelectionFilter map of tag → selected operation nicknames.
|
||||||
|
*/
|
||||||
|
export async function askSelectionFilter(tagSummaries: TagSummary[]): Promise<SelectionFilter> {
|
||||||
|
if (tagSummaries.length === 0) return {};
|
||||||
|
|
||||||
|
clearScreen();
|
||||||
|
console.log(`\n ${colors.bright}📋 Selección de tags y endpoints${colors.reset}`);
|
||||||
|
console.log(` ${'─'.repeat(54)}\n`);
|
||||||
|
|
||||||
|
// Step 1: select tags
|
||||||
|
const tagResponse = await prompts({
|
||||||
|
type: 'multiselect',
|
||||||
|
name: 'tags',
|
||||||
|
message: 'Tags a generar',
|
||||||
|
choices: tagSummaries.map((t) => ({
|
||||||
|
title: `${colors.bright}${t.tag}${colors.reset} ${colors.cyan}(${t.operations.length} endpoint${t.operations.length !== 1 ? 's' : ''})${colors.reset}`,
|
||||||
|
value: t.tag,
|
||||||
|
selected: true
|
||||||
|
})),
|
||||||
|
min: 1,
|
||||||
|
hint: 'Espacio para marcar/desmarcar, Enter para confirmar'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tagResponse.tags?.length) process.exit(0);
|
||||||
|
|
||||||
|
const selectedTags: string[] = tagResponse.tags;
|
||||||
|
const filter: SelectionFilter = {};
|
||||||
|
|
||||||
|
// Step 2: for each selected tag, select endpoints
|
||||||
|
for (let i = 0; i < selectedTags.length; i++) {
|
||||||
|
const tag = selectedTags[i];
|
||||||
|
const summary = tagSummaries.find((t) => t.tag === tag)!;
|
||||||
|
|
||||||
|
clearScreen();
|
||||||
|
console.log(
|
||||||
|
`\n ${colors.bright}📋 Endpoints a generar${colors.reset} [${colors.cyan}${i + 1}${colors.reset} de ${colors.cyan}${selectedTags.length}${colors.reset}]`
|
||||||
|
);
|
||||||
|
console.log(` ${'─'.repeat(54)}\n`);
|
||||||
|
|
||||||
|
const opResponse = await prompts({
|
||||||
|
type: 'multiselect',
|
||||||
|
name: 'ops',
|
||||||
|
message: `Tag ${colors.bright}${tag}${colors.reset}`,
|
||||||
|
choices: summary.operations.map((op) => ({
|
||||||
|
title:
|
||||||
|
`${colors.bright}${op.method.padEnd(6)}${colors.reset} ${op.path}` +
|
||||||
|
(op.summary ? ` ${colors.cyan}${op.summary}${colors.reset}` : ''),
|
||||||
|
value: op.nickname,
|
||||||
|
selected: true
|
||||||
|
})),
|
||||||
|
min: 1,
|
||||||
|
hint: 'Espacio para marcar/desmarcar, Enter para confirmar'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!opResponse.ops?.length) process.exit(0);
|
||||||
|
|
||||||
|
filter[tag] = opResponse.ops;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interactively asks the user which environment API key to use for each tag,
|
||||||
|
* using arrow-key selection. The last option always allows typing manually.
|
||||||
|
* Returns a map of tag → environment key (e.g. { "SupplyingMaintenances": "suppliyingMaintenancesApi" }).
|
||||||
|
*/
|
||||||
|
export async function askApiKeysForTags(
|
||||||
|
tags: string[],
|
||||||
|
apiKeys: ApiKeyInfo[]
|
||||||
|
): Promise<Record<string, string>> {
|
||||||
|
if (tags.length === 0) return {};
|
||||||
|
|
||||||
|
clearScreen();
|
||||||
|
printHeader();
|
||||||
|
|
||||||
|
const modeResponse = await prompts({
|
||||||
|
type: 'select',
|
||||||
|
name: 'mode',
|
||||||
|
message: 'URL base para los repositorios',
|
||||||
|
choices: [
|
||||||
|
{ title: `${colors.bright}La misma para todos${colors.reset}`, value: 'all' },
|
||||||
|
{ title: `${colors.bright}Configurar individualmente${colors.reset}`, value: 'individual' }
|
||||||
|
],
|
||||||
|
hint: ' '
|
||||||
|
});
|
||||||
|
|
||||||
|
if (modeResponse.mode === undefined) process.exit(0);
|
||||||
|
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (modeResponse.mode === 'all') {
|
||||||
|
clearScreen();
|
||||||
|
printHeader();
|
||||||
|
const sharedKey = await askApiKeyForTag('todos los repositorios', apiKeys);
|
||||||
|
tags.forEach((tag) => (result[tag] = sharedKey));
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < tags.length; i++) {
|
||||||
|
clearScreen();
|
||||||
|
printHeader(i + 1, tags.length);
|
||||||
|
result[tags[i]] = await askApiKeyForTag(tags[i], apiKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printSummary(tags, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function askApiKeyForTag(tagName: string, apiKeys: ApiKeyInfo[]): Promise<string> {
|
||||||
|
const MANUAL_VALUE = '__manual__';
|
||||||
|
|
||||||
|
const choices = [
|
||||||
|
...apiKeys.map((k) => ({
|
||||||
|
title: k.url
|
||||||
|
? `${colors.bright}${k.key}${colors.reset}\n ${colors.cyan}↳ ${k.url}${colors.reset}`
|
||||||
|
: `${colors.bright}${k.key}${colors.reset}`,
|
||||||
|
value: k.key
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
title: `${colors.bright}Escribir manualmente${colors.reset}`,
|
||||||
|
value: MANUAL_VALUE
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectResponse = await prompts({
|
||||||
|
type: 'select',
|
||||||
|
name: 'key',
|
||||||
|
message: `Repositorio ${colors.bright}${tagName}${colors.reset}`,
|
||||||
|
choices,
|
||||||
|
hint: ' '
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectResponse.key === undefined) process.exit(0);
|
||||||
|
|
||||||
|
if (selectResponse.key !== MANUAL_VALUE) {
|
||||||
|
return selectResponse.key as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
const textResponse = await prompts({
|
||||||
|
type: 'text',
|
||||||
|
name: 'key',
|
||||||
|
message: `Clave de environment`,
|
||||||
|
hint: 'ej: aprovalmApi',
|
||||||
|
validate: (v: string) => v.trim().length > 0 || 'La clave no puede estar vacía'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (textResponse.key === undefined) process.exit(0);
|
||||||
|
|
||||||
|
return (textResponse.key as string).trim();
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ export function mapSwaggerTypeToTs(type?: string): string {
|
|||||||
string: 'string',
|
string: 'string',
|
||||||
boolean: 'boolean',
|
boolean: 'boolean',
|
||||||
number: 'number',
|
number: 'number',
|
||||||
array: 'Array<unknown>',
|
array: 'unknown[]',
|
||||||
object: 'unknown'
|
object: 'unknown'
|
||||||
};
|
};
|
||||||
return typeMap[type] || 'unknown';
|
return typeMap[type] || 'unknown';
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ import { environment } from '@environment';
|
|||||||
|
|
||||||
import { MRepository } from '@mercadona/core/utils/repository';
|
import { MRepository } from '@mercadona/core/utils/repository';
|
||||||
|
|
||||||
import { {{classname}}Repository } from '../../../domain/repositories/{{classFilename}}.repository.contract';
|
import { {{classname}}Repository } from '@/domain/repositories/{{classFilename}}.repository.contract';
|
||||||
{{#imports}}
|
{{#returnImports}}
|
||||||
import { {{classname}}Dto } from '@/dtos/{{classFilename}}/{{classFilename}}.dto';
|
import { {{classname}}Dto } from '@/dtos/{{classFilename}}.dto';
|
||||||
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
|
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
|
||||||
import { {{classVarName}}Mapper } from '@/mappers/{{classFilename}}/{{classFilename}}.mapper';
|
import { {{classVarName}}Mapper } from '@/mappers/{{classFilename}}.mapper';
|
||||||
{{/imports}}
|
{{/returnImports}}
|
||||||
|
{{#paramImports}}
|
||||||
|
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
|
||||||
|
{{/paramImports}}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {{classname}} Repository Implementation
|
* {{classname}} Repository Implementation
|
||||||
@@ -23,7 +26,7 @@ import { {{classVarName}}Mapper } from '@/mappers/{{classFilename}}/{{classFilen
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class {{classname}}RepositoryImpl extends MRepository implements {{classname}}Repository {
|
export class {{classname}}RepositoryImpl extends MRepository implements {{classname}}Repository {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(`${environment.modapApi.url}`);
|
super(`${environment.{{environmentApiKey}}.url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
{{#operation}}
|
{{#operation}}
|
||||||
@@ -33,7 +36,7 @@ export class {{classname}}RepositoryImpl extends MRepository implements {{classn
|
|||||||
params: { {{#queryParams}}{{paramName}}{{^-last}}, {{/-last}}{{/queryParams}} }
|
params: { {{#queryParams}}{{paramName}}{{^-last}}, {{/-last}}{{/queryParams}} }
|
||||||
}{{/hasQueryParams}}{{#hasBodyParam}}, {{bodyParam}}{{/hasBodyParam}})
|
}{{/hasQueryParams}}{{#hasBodyParam}}, {{bodyParam}}{{/hasBodyParam}})
|
||||||
.pipe(
|
.pipe(
|
||||||
map((response) => response.{{#vendorExtensions}}{{x-response-property}}{{/vendorExtensions}}{{^vendorExtensions}}items{{/vendorExtensions}}.map({{{returnBaseType}}}Mapper))
|
map((response) => response.{{#vendorExtensions}}{{x-response-property}}{{/vendorExtensions}}{{^vendorExtensions}}items{{/vendorExtensions}}.map({{{returnBaseTypeVarName}}}Mapper))
|
||||||
);
|
);
|
||||||
{{/isListContainer}}
|
{{/isListContainer}}
|
||||||
{{^isListContainer}}
|
{{^isListContainer}}
|
||||||
@@ -42,7 +45,7 @@ export class {{classname}}RepositoryImpl extends MRepository implements {{classn
|
|||||||
params: { {{#queryParams}}{{paramName}}{{^-last}}, {{/-last}}{{/queryParams}} }
|
params: { {{#queryParams}}{{paramName}}{{^-last}}, {{/-last}}{{/queryParams}} }
|
||||||
}{{/hasQueryParams}}{{#hasBodyParam}}, {{bodyParam}}{{/hasBodyParam}})
|
}{{/hasQueryParams}}{{#hasBodyParam}}, {{bodyParam}}{{/hasBodyParam}})
|
||||||
.pipe(
|
.pipe(
|
||||||
map({{{returnType}}}Mapper)
|
map({{{returnTypeVarName}}}Mapper)
|
||||||
);
|
);
|
||||||
{{/returnType}}
|
{{/returnType}}
|
||||||
{{^returnType}}
|
{{^returnType}}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { MapFromFn } from '@mercadona/common/public';
|
import { MapFromFn } from '@mercadona/common/public';
|
||||||
import { Builder } from '@mercadona/common/utils';
|
import { Builder } from '@mercadona/common/utils';
|
||||||
|
|
||||||
import { {{classname}}Dto } from '@/dtos/{{classFilename}}/{{classFilename}}.dto';
|
import { {{classname}}Dto } from '@/dtos/{{classFilename}}.dto';
|
||||||
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
|
import { {{classname}} } from '@/entities/models/{{classFilename}}.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
{{#operations}}
|
{{#operations}}
|
||||||
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
|
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
|
||||||
|
|
||||||
import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.contract';
|
import { {{constantName}}_USE_CASES } from '@/domain/use-cases/{{classFilename}}.use-cases.contract';
|
||||||
import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}/{{classFilename}}.use-cases.impl';
|
import { {{classname}}UseCasesImpl } from '@/domain/use-cases/{{classFilename}}.use-cases.impl';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {{classname}} Use Cases Provider
|
* {{classname}} Use Cases Provider
|
||||||
|
|||||||
Reference in New Issue
Block a user