@0kmpo/openapi-clean-arch-generator 1.3.13 → 1.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/main.js +24 -2
- package/dist/package.json +78 -0
- package/dist/src/generators/clean-arch.generator.js +200 -136
- package/dist/src/generators/dto.generator.js +42 -22
- package/dist/src/generators/report.generator.js +6 -0
- package/dist/src/utils/example-validator.js +86 -0
- package/dist/src/utils/mock-value-resolver.js +19 -7
- package/dist/src/utils/name-formatter.js +99 -2
- package/dist/templates/api.repository.contract.mustache +1 -1
- package/dist/templates/api.repository.impl.mock.mustache +2 -2
- package/dist/templates/api.repository.impl.mustache +5 -5
- package/dist/templates/api.repository.impl.spec.mustache +2 -2
- package/dist/templates/api.use-cases.contract.mustache +1 -1
- package/dist/templates/api.use-cases.impl.mustache +2 -2
- package/dist/templates/api.use-cases.impl.spec.mustache +2 -2
- package/dist/templates/api.use-cases.mock.mustache +2 -2
- package/dist/templates/dto.mock.mustache +1 -1
- package/dist/templates/mapper.mustache +2 -2
- package/dist/templates/mapper.spec.mustache +2 -2
- package/dist/templates/model-entity.mustache +1 -1
- package/dist/templates/model-entity.spec.mustache +4 -0
- package/dist/templates/model.mock.mustache +2 -2
- package/dist/templates/repository.provider.mock.mustache +2 -2
- package/dist/templates/repository.provider.mustache +2 -2
- package/dist/templates/use-cases.provider.mock.mustache +2 -2
- package/dist/templates/use-cases.provider.mustache +2 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -76,7 +76,10 @@ Options:
|
|
|
76
76
|
-o, --output <dir> Output directory [default: ./src/app]
|
|
77
77
|
-t, --templates <dir> Custom templates directory [default: ./templates]
|
|
78
78
|
-s, --select-endpoints Interactively select tags and endpoints to generate
|
|
79
|
+
-c, --config <file> Use a JSON configuration file (skips interactive prompts)
|
|
80
|
+
--init-config [file] Generate a JSON configuration file instead of generating code
|
|
79
81
|
--skip-install Skip dependency installation
|
|
82
|
+
--skip-lint Skip post-generation linting and formatting
|
|
80
83
|
--dry-run Simulate without writing files
|
|
81
84
|
-h, --help Show help
|
|
82
85
|
```
|
|
@@ -96,6 +99,15 @@ generate-clean-arch -i api.yaml -t ./my-templates
|
|
|
96
99
|
# Dry run (no files written)
|
|
97
100
|
generate-clean-arch -i swagger.yaml --dry-run
|
|
98
101
|
|
|
102
|
+
# Skip linting after generation
|
|
103
|
+
generate-clean-arch -i swagger.yaml --skip-lint
|
|
104
|
+
|
|
105
|
+
# Generate a config file to reuse later
|
|
106
|
+
generate-clean-arch --init-config generation-config.json
|
|
107
|
+
|
|
108
|
+
# Run using a config file (no interactive prompts)
|
|
109
|
+
generate-clean-arch -c generation-config.json
|
|
110
|
+
|
|
99
111
|
# Full example with all options
|
|
100
112
|
generate-clean-arch -i ./docs/api.yaml -o ./frontend/src/app -t ./custom-templates
|
|
101
113
|
```
|
package/dist/main.js
CHANGED
|
@@ -17,6 +17,7 @@ const clean_arch_generator_1 = require("./src/generators/clean-arch.generator");
|
|
|
17
17
|
const report_generator_1 = require("./src/generators/report.generator");
|
|
18
18
|
const lint_generator_1 = require("./src/generators/lint.generator");
|
|
19
19
|
const environment_finder_1 = require("./src/utils/environment-finder");
|
|
20
|
+
const example_validator_1 = require("./src/utils/example-validator");
|
|
20
21
|
const prompt_1 = require("./src/utils/prompt");
|
|
21
22
|
const config_1 = require("./src/utils/config");
|
|
22
23
|
const package_json_1 = __importDefault(require("./package.json"));
|
|
@@ -109,6 +110,7 @@ async function main() {
|
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
111
112
|
(0, filesystem_1.createDirectoryStructure)(options.output);
|
|
113
|
+
(0, example_validator_1.clearExampleMismatches)();
|
|
112
114
|
// ── SELECTION: tags and endpoints ─────────────────────────────────────────
|
|
113
115
|
let selectionFilter = {};
|
|
114
116
|
let tagApiKeyMap;
|
|
@@ -150,10 +152,27 @@ async function main() {
|
|
|
150
152
|
}
|
|
151
153
|
// ──────────────────────────────────────────────────────────────────────────
|
|
152
154
|
const tempDir = (0, dto_generator_1.generateCode)(options.input, options.templates);
|
|
153
|
-
|
|
155
|
+
// Compute schema→tag map before organizeFiles so DTOs land in the right subfolder
|
|
156
|
+
const tagsMapForSchema = (0, clean_arch_generator_1.buildTagsMapFromAnalysis)(analysis, selectionFilter);
|
|
157
|
+
const schemaTagMap = (0, clean_arch_generator_1.buildSchemaTagMap)(analysis.swagger.components
|
|
158
|
+
?.schemas || {}, tagsMapForSchema);
|
|
159
|
+
(0, dto_generator_1.organizeFiles)(tempDir, options.output, schemaTagMap);
|
|
154
160
|
(0, dto_generator_1.addDtoImports)(options.output);
|
|
155
|
-
(0, clean_arch_generator_1.generateCleanArchitecture)(analysis, options.output, options.templates, tagApiKeyMap, selectionFilter);
|
|
161
|
+
(0, clean_arch_generator_1.generateCleanArchitecture)(analysis, options.output, options.templates, tagApiKeyMap, selectionFilter, schemaTagMap);
|
|
156
162
|
(0, filesystem_1.cleanup)(tempDir);
|
|
163
|
+
// ── EXAMPLE/TYPE MISMATCH WARNINGS ─────────────────────────────────────────
|
|
164
|
+
const mismatches = (0, example_validator_1.getExampleMismatches)();
|
|
165
|
+
if (mismatches.length > 0) {
|
|
166
|
+
console.log('');
|
|
167
|
+
(0, logger_1.logWarning)(`${mismatches.length} example/type mismatch(es) detected in OpenAPI schemas:`);
|
|
168
|
+
for (const m of mismatches) {
|
|
169
|
+
const action = m.action === 'coerced'
|
|
170
|
+
? `→ coerced to ${JSON.stringify(m.coercedValue)}`
|
|
171
|
+
: '→ example ignored, using type default';
|
|
172
|
+
(0, logger_1.logWarning)(` ${m.schemaName}.${m.propertyName}: type '${m.declaredType}' but example is ${m.exampleJsType} (${JSON.stringify(m.exampleValue)}) ${action}`);
|
|
173
|
+
(0, logger_1.logDetail)('VALIDATE', `${m.schemaName}.${m.propertyName}: declared=${m.declaredType} example=${JSON.stringify(m.exampleValue)} (${m.exampleJsType}) action=${m.action}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
157
176
|
const noLintResult = {
|
|
158
177
|
prettier: { ran: false, filesFormatted: 0 },
|
|
159
178
|
eslint: { ran: false, filesFixed: 0 }
|
|
@@ -170,6 +189,9 @@ async function main() {
|
|
|
170
189
|
console.log(` - Use Cases: ${report.structure.useCases}`);
|
|
171
190
|
console.log(` - Providers: ${report.structure.providers}`);
|
|
172
191
|
console.log(` - Mocks: ${report.structure.mocks}`);
|
|
192
|
+
if (report.warnings.total > 0) {
|
|
193
|
+
console.log(`\n ${logger_1.colors.yellow}⚠️ ${report.warnings.total} example/type mismatch(es) (see above)${logger_1.colors.reset}`);
|
|
194
|
+
}
|
|
173
195
|
console.log(`\n📁 Files generated in: ${logger_1.colors.cyan}${options.output}${logger_1.colors.reset}\n`);
|
|
174
196
|
}
|
|
175
197
|
main().catch((error) => {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@0kmpo/openapi-clean-arch-generator",
|
|
3
|
+
"version": "1.3.15",
|
|
4
|
+
"description": "Angular Clean Architecture generator from OpenAPI/Swagger",
|
|
5
|
+
"main": "dist/main.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"generate-clean-arch": "./dist/main.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc && cp -r templates dist/",
|
|
11
|
+
"postbuild": "bun -e \"const fs=require('fs'); const f='dist/main.js'; const c=fs.readFileSync(f,'utf8'); if(!c.startsWith('#!/usr/bin/env node')) fs.writeFileSync(f,'#!/usr/bin/env node\\n'+c); fs.chmodSync(f, '755');\"",
|
|
12
|
+
"prepublishOnly": "bun run build",
|
|
13
|
+
"generate": "bun dist/main.js",
|
|
14
|
+
"generate:dev": "bun main.ts",
|
|
15
|
+
"binaries": "bun run binary:mac-arm64 && bun run binary:mac-x64 && bun run binary:linux-x64 && bun run binary:linux-arm64 && bun run binary:windows",
|
|
16
|
+
"binary:mac-arm64": "bun build --compile --target=bun-darwin-arm64 --outfile dist/bin/generate-clean-arch-macos-arm64 main.ts",
|
|
17
|
+
"binary:mac-x64": "bun build --compile --target=bun-darwin-x64 --outfile dist/bin/generate-clean-arch-macos-x64 main.ts",
|
|
18
|
+
"binary:linux-x64": "bun build --compile --target=bun-linux-x64 --outfile dist/bin/generate-clean-arch-linux-x64 main.ts",
|
|
19
|
+
"binary:linux-arm64": "bun build --compile --target=bun-linux-arm64 --outfile dist/bin/generate-clean-arch-linux-arm64 main.ts",
|
|
20
|
+
"binary:windows": "bun build --compile --target=bun-windows-x64 --outfile dist/bin/generate-clean-arch-windows-x64.exe main.ts",
|
|
21
|
+
"lint": "bunx --bun eslint 'main.ts' 'src/**/*.ts' -f unix",
|
|
22
|
+
"lint:fix": "bunx --bun eslint 'main.ts' 'src/**/*.ts' --fix -f unix",
|
|
23
|
+
"format": "prettier --write .",
|
|
24
|
+
"setup": "bun add -g @openapitools/openapi-generator-cli"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"openapi",
|
|
28
|
+
"swagger",
|
|
29
|
+
"angular",
|
|
30
|
+
"clean-architecture",
|
|
31
|
+
"code-generator"
|
|
32
|
+
],
|
|
33
|
+
"author": "Blas Santomé Ocampo",
|
|
34
|
+
"contributors": [
|
|
35
|
+
{
|
|
36
|
+
"name": "Diego Davila Freitas",
|
|
37
|
+
"email": "diego.davilafreitas@gmail.com",
|
|
38
|
+
"url": "https://www.linkedin.com/in/diegodavilafreitas"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"files": [
|
|
43
|
+
"dist/main.js",
|
|
44
|
+
"dist/package.json",
|
|
45
|
+
"dist/src/",
|
|
46
|
+
"dist/templates/",
|
|
47
|
+
"README.md",
|
|
48
|
+
"LICENSE"
|
|
49
|
+
],
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"chalk": "^4.1.2",
|
|
52
|
+
"commander": "^11.1.0",
|
|
53
|
+
"fs-extra": "^11.2.0",
|
|
54
|
+
"js-yaml": "^4.1.0",
|
|
55
|
+
"mustache": "^4.2.0",
|
|
56
|
+
"prompts": "^2.4.2"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"bun": ">=1.0.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@eslint/js": "^10.0.1",
|
|
63
|
+
"@types/fs-extra": "^11.0.4",
|
|
64
|
+
"@types/js-yaml": "^4.0.9",
|
|
65
|
+
"@types/mustache": "^4.2.6",
|
|
66
|
+
"@types/node": "^25.5.0",
|
|
67
|
+
"@types/prompts": "^2.4.9",
|
|
68
|
+
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
|
69
|
+
"@typescript-eslint/parser": "^8.57.1",
|
|
70
|
+
"eslint": "^10.1.0",
|
|
71
|
+
"eslint-config-prettier": "^10.1.8",
|
|
72
|
+
"eslint-formatter-unix": "^9.0.1",
|
|
73
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
74
|
+
"prettier": "^3.8.1",
|
|
75
|
+
"typescript": "^5.9.3",
|
|
76
|
+
"typescript-eslint": "^8.57.1"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.extractTagsFromAnalysis = extractTagsFromAnalysis;
|
|
7
7
|
exports.extractTagsWithOperations = extractTagsWithOperations;
|
|
8
|
+
exports.buildTagsMapFromAnalysis = buildTagsMapFromAnalysis;
|
|
9
|
+
exports.buildSchemaTagMap = buildSchemaTagMap;
|
|
8
10
|
exports.generateCleanArchitecture = generateCleanArchitecture;
|
|
9
11
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
12
|
const path_1 = __importDefault(require("path"));
|
|
@@ -57,8 +59,139 @@ function extractTagsWithOperations(analysis) {
|
|
|
57
59
|
});
|
|
58
60
|
return [...map.values()];
|
|
59
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Builds and returns the tagsMap from the swagger analysis, applying the optional selection filter.
|
|
64
|
+
* Exported so callers (e.g. main.ts) can compute it before organizeFiles runs.
|
|
65
|
+
*/
|
|
66
|
+
function buildTagsMapFromAnalysis(analysis, selectionFilter = {}) {
|
|
67
|
+
const tagsMap = {};
|
|
68
|
+
Object.keys(analysis.paths).forEach((pathKey) => {
|
|
69
|
+
const pathObj = analysis.paths[pathKey];
|
|
70
|
+
Object.keys(pathObj).forEach((method) => {
|
|
71
|
+
const op = pathObj[method];
|
|
72
|
+
if (op.tags && op.tags.length > 0) {
|
|
73
|
+
const tag = op.tags[0];
|
|
74
|
+
if (!tagsMap[tag])
|
|
75
|
+
tagsMap[tag] = [];
|
|
76
|
+
const allParams = (op.parameters || []).map((p) => ({
|
|
77
|
+
paramName: p.name,
|
|
78
|
+
dataType: (0, type_mapper_1.mapSwaggerTypeToTs)(p.schema?.type || ''),
|
|
79
|
+
description: p.description || '',
|
|
80
|
+
required: p.required,
|
|
81
|
+
testValue: resolveTestParamValue((0, type_mapper_1.mapSwaggerTypeToTs)(p.schema?.type || ''))
|
|
82
|
+
}));
|
|
83
|
+
if (op.requestBody) {
|
|
84
|
+
let bodyType = 'unknown';
|
|
85
|
+
const content = op.requestBody.content?.['application/json']?.schema;
|
|
86
|
+
if (content) {
|
|
87
|
+
if (content.$ref)
|
|
88
|
+
bodyType = content.$ref.split('/').pop() || 'unknown';
|
|
89
|
+
else if (content.type)
|
|
90
|
+
bodyType = (0, type_mapper_1.mapSwaggerTypeToTs)(content.type);
|
|
91
|
+
}
|
|
92
|
+
allParams.push({
|
|
93
|
+
paramName: 'body',
|
|
94
|
+
dataType: bodyType,
|
|
95
|
+
description: op.requestBody.description || '',
|
|
96
|
+
required: true,
|
|
97
|
+
testValue: resolveTestParamValue(bodyType)
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
let returnType = 'void';
|
|
101
|
+
let returnBaseType = 'void';
|
|
102
|
+
let isListContainer = false;
|
|
103
|
+
const successCode = ['200', '201', '202', '203'].find((code) => op.responses?.[code]);
|
|
104
|
+
const responseSchema = successCode !== undefined
|
|
105
|
+
? op.responses?.[successCode]?.content?.['application/json']?.schema
|
|
106
|
+
: undefined;
|
|
107
|
+
if (responseSchema) {
|
|
108
|
+
if (responseSchema.$ref) {
|
|
109
|
+
returnType = responseSchema.$ref.split('/').pop() || 'unknown';
|
|
110
|
+
returnBaseType = returnType;
|
|
111
|
+
}
|
|
112
|
+
else if (responseSchema.type === 'array' && responseSchema.items?.$ref) {
|
|
113
|
+
returnBaseType = responseSchema.items.$ref.split('/').pop() || 'unknown';
|
|
114
|
+
returnType = `${returnBaseType}[]`;
|
|
115
|
+
isListContainer = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const hasQueryParams = (op.parameters || []).some((p) => p.in === 'query');
|
|
119
|
+
const hasBodyParam = !!op.requestBody;
|
|
120
|
+
// Sort: required params first, optional params last (TypeScript requirement)
|
|
121
|
+
allParams.sort((a, b) => {
|
|
122
|
+
if (a.required === b.required)
|
|
123
|
+
return 0;
|
|
124
|
+
return a.required ? -1 : 1;
|
|
125
|
+
});
|
|
126
|
+
tagsMap[tag].push({
|
|
127
|
+
nickname: op.operationId || `${method}${pathKey.replace(/\//g, '_')}`,
|
|
128
|
+
summary: op.summary || '',
|
|
129
|
+
notes: op.description || '',
|
|
130
|
+
httpMethod: method.toLowerCase(),
|
|
131
|
+
uppercaseHttpMethod: method.toUpperCase(),
|
|
132
|
+
path: pathKey,
|
|
133
|
+
allParams: allParams.map((p, i) => ({
|
|
134
|
+
...p,
|
|
135
|
+
'-last': i === allParams.length - 1
|
|
136
|
+
})),
|
|
137
|
+
hasQueryParams,
|
|
138
|
+
queryParams: (op.parameters || [])
|
|
139
|
+
.filter((p) => p.in === 'query')
|
|
140
|
+
.map((p, i, arr) => ({
|
|
141
|
+
paramName: p.name,
|
|
142
|
+
'-last': i === arr.length - 1
|
|
143
|
+
})),
|
|
144
|
+
hasBodyParam,
|
|
145
|
+
bodyParam: 'body',
|
|
146
|
+
hasOptions: hasQueryParams || hasBodyParam,
|
|
147
|
+
hasBothParamsAndBody: hasQueryParams && hasBodyParam,
|
|
148
|
+
returnType: returnType !== 'void' ? returnType : false,
|
|
149
|
+
returnBaseType: returnBaseType !== 'void' ? returnBaseType : false,
|
|
150
|
+
returnTypeVarName: returnType !== 'void' ? (0, name_formatter_1.toCamelCase)(returnType) : false,
|
|
151
|
+
returnBaseTypeVarName: returnBaseType !== 'void' ? (0, name_formatter_1.toCamelCase)(returnBaseType) : false,
|
|
152
|
+
isListContainer: isListContainer,
|
|
153
|
+
vendorExtensions: {}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
if (Object.keys(selectionFilter).length > 0) {
|
|
159
|
+
Object.keys(tagsMap).forEach((tag) => {
|
|
160
|
+
if (!selectionFilter[tag]) {
|
|
161
|
+
delete tagsMap[tag];
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
tagsMap[tag] = tagsMap[tag].filter((op) => selectionFilter[tag].includes(op.nickname));
|
|
165
|
+
if (tagsMap[tag].length === 0)
|
|
166
|
+
delete tagsMap[tag];
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return tagsMap;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Maps each schema basename to the tag subfolder it belongs to.
|
|
174
|
+
* Schemas used by exactly one tag → that tag's camelCase name.
|
|
175
|
+
* Schemas used by 0 or multiple tags → 'shared'.
|
|
176
|
+
*/
|
|
177
|
+
function buildSchemaTagMap(schemas, tagsMap) {
|
|
178
|
+
const result = {};
|
|
179
|
+
Object.keys(schemas).forEach((schemaName) => {
|
|
180
|
+
const baseName = schemaName.replace(/Dto$/, '');
|
|
181
|
+
const tagsUsing = [];
|
|
182
|
+
Object.keys(tagsMap).forEach((tag) => {
|
|
183
|
+
const used = tagsMap[tag].some((op) => op.returnType === baseName ||
|
|
184
|
+
op.returnType === `${baseName}[]` ||
|
|
185
|
+
op.allParams.some((p) => p.dataType === baseName || p.dataType === `${baseName}[]`));
|
|
186
|
+
if (used)
|
|
187
|
+
tagsUsing.push(tag);
|
|
188
|
+
});
|
|
189
|
+
result[baseName] = tagsUsing.length === 1 ? (0, name_formatter_1.toCamelCase)(tagsUsing[0]) : 'shared';
|
|
190
|
+
});
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
60
193
|
/** Generates all Clean Architecture artefacts (models, mappers, repos, use cases, providers) using Mustache. */
|
|
61
|
-
function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyMap = {}, selectionFilter = {}) {
|
|
194
|
+
function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyMap = {}, selectionFilter = {}, precomputedSchemaTagMap = {}) {
|
|
62
195
|
(0, logger_1.logStep)('Generating Clean Architecture artefacts using Mustache...');
|
|
63
196
|
const generatedCount = {
|
|
64
197
|
models: 0,
|
|
@@ -71,14 +204,22 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
|
|
|
71
204
|
};
|
|
72
205
|
const schemas = analysis.swagger.components
|
|
73
206
|
?.schemas || {};
|
|
207
|
+
// Build tagsMap first — needed to compute schemaTagMap before the schema loop
|
|
208
|
+
const tagsMap = buildTagsMapFromAnalysis(analysis, selectionFilter);
|
|
209
|
+
// Map each schema basename → tag subfolder ('shared' if used by 0 or >1 tags)
|
|
210
|
+
const schemaTagMap = Object.keys(precomputedSchemaTagMap).length > 0
|
|
211
|
+
? precomputedSchemaTagMap
|
|
212
|
+
: buildSchemaTagMap(schemas, tagsMap);
|
|
74
213
|
// 1. Generate Models, Entities and Mappers from Schemas
|
|
75
214
|
Object.keys(schemas).forEach((schemaName) => {
|
|
76
215
|
const baseName = schemaName.replace(/Dto$/, '');
|
|
216
|
+
const tagFilename = schemaTagMap[baseName] || 'shared';
|
|
77
217
|
const schemaObj = schemas[schemaName];
|
|
78
218
|
const rawProperties = schemaObj.properties || {};
|
|
79
219
|
const requiredProps = schemaObj.required || [];
|
|
80
220
|
const varsMap = Object.keys(rawProperties).map((k) => {
|
|
81
221
|
let tsType = (0, type_mapper_1.mapSwaggerTypeToTs)(rawProperties[k].type);
|
|
222
|
+
const isInlineObject = rawProperties[k].type === 'object' && !rawProperties[k].$ref;
|
|
82
223
|
if (rawProperties[k].$ref) {
|
|
83
224
|
tsType = rawProperties[k].$ref.split('/').pop() || 'unknown';
|
|
84
225
|
}
|
|
@@ -86,10 +227,12 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
|
|
|
86
227
|
tsType = `${rawProperties[k].items.$ref.split('/').pop()}[]`;
|
|
87
228
|
}
|
|
88
229
|
return {
|
|
89
|
-
name: k,
|
|
230
|
+
name: (0, name_formatter_1.safePropertyName)(k),
|
|
231
|
+
originalName: k,
|
|
90
232
|
dataType: tsType,
|
|
91
233
|
description: rawProperties[k].description || '',
|
|
92
|
-
required: requiredProps.includes(k)
|
|
234
|
+
required: requiredProps.includes(k),
|
|
235
|
+
hasMockValue: !isInlineObject
|
|
93
236
|
};
|
|
94
237
|
});
|
|
95
238
|
// Collect imports for types referenced via $ref in properties
|
|
@@ -102,10 +245,13 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
|
|
|
102
245
|
referencedTypes.add(prop.items.$ref.split('/').pop() || '');
|
|
103
246
|
}
|
|
104
247
|
});
|
|
105
|
-
const modelImports = [...referencedTypes]
|
|
106
|
-
|
|
107
|
-
|
|
248
|
+
const modelImports = [...referencedTypes].filter(Boolean).map((name) => ({
|
|
249
|
+
classname: name,
|
|
250
|
+
classFilename: (0, name_formatter_1.toCamelCase)(name),
|
|
251
|
+
tagFilename: schemaTagMap[name] || 'shared'
|
|
252
|
+
}));
|
|
108
253
|
const modelViewData = {
|
|
254
|
+
tagFilename,
|
|
109
255
|
models: [
|
|
110
256
|
{
|
|
111
257
|
model: {
|
|
@@ -128,7 +274,8 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
|
|
|
128
274
|
operations: {
|
|
129
275
|
classname: baseName,
|
|
130
276
|
classFilename: (0, name_formatter_1.toCamelCase)(baseName),
|
|
131
|
-
classVarName: (0, name_formatter_1.toCamelCase)(baseName)
|
|
277
|
+
classVarName: (0, name_formatter_1.toCamelCase)(baseName),
|
|
278
|
+
tagFilename
|
|
132
279
|
}
|
|
133
280
|
}
|
|
134
281
|
]
|
|
@@ -139,7 +286,8 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
|
|
|
139
286
|
if (fs_extra_1.default.existsSync(modelTemplatePath)) {
|
|
140
287
|
const template = fs_extra_1.default.readFileSync(modelTemplatePath, 'utf8');
|
|
141
288
|
const output = mustache_1.default.render(template, modelViewData);
|
|
142
|
-
const destPath = path_1.default.join(outputDir, 'entities/models', `${(0, name_formatter_1.toCamelCase)(baseName)}.model.ts`);
|
|
289
|
+
const destPath = path_1.default.join(outputDir, 'entities/models', tagFilename, `${(0, name_formatter_1.toCamelCase)(baseName)}.model.ts`);
|
|
290
|
+
fs_extra_1.default.ensureDirSync(path_1.default.dirname(destPath));
|
|
143
291
|
fs_extra_1.default.writeFileSync(destPath, output);
|
|
144
292
|
generatedCount.models++;
|
|
145
293
|
(0, logger_1.logDetail)('generate', `model-entity → ${path_1.default.relative(process.cwd(), destPath)}`);
|
|
@@ -149,19 +297,29 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
|
|
|
149
297
|
if (fs_extra_1.default.existsSync(mapperTemplatePath)) {
|
|
150
298
|
const template = fs_extra_1.default.readFileSync(mapperTemplatePath, 'utf8');
|
|
151
299
|
const output = mustache_1.default.render(template, mapperViewData);
|
|
152
|
-
const destPath = path_1.default.join(outputDir, 'data/mappers', `${(0, name_formatter_1.toCamelCase)(baseName)}.mapper.ts`);
|
|
300
|
+
const destPath = path_1.default.join(outputDir, 'data/mappers', tagFilename, `${(0, name_formatter_1.toCamelCase)(baseName)}.mapper.ts`);
|
|
301
|
+
fs_extra_1.default.ensureDirSync(path_1.default.dirname(destPath));
|
|
153
302
|
fs_extra_1.default.writeFileSync(destPath, output);
|
|
154
303
|
generatedCount.mappers++;
|
|
155
304
|
}
|
|
156
305
|
// DTO mock — values resolved from raw schema (example, format, type)
|
|
157
306
|
const dtoMockVarsMap = Object.keys(rawProperties).map((k) => ({
|
|
158
|
-
name: k,
|
|
159
|
-
mockValue: (0, mock_value_resolver_1.resolveMockValue)(k, rawProperties[k], 'dto')
|
|
307
|
+
name: (0, name_formatter_1.safePropertyName)(k),
|
|
308
|
+
mockValue: (0, mock_value_resolver_1.resolveMockValue)(k, rawProperties[k], 'dto', schemaName)
|
|
160
309
|
}));
|
|
161
|
-
const dtoMockImports = [...referencedTypes]
|
|
162
|
-
|
|
163
|
-
|
|
310
|
+
const dtoMockImports = [...referencedTypes].filter(Boolean).map((name) => {
|
|
311
|
+
const targetTag = schemaTagMap[name] || 'shared';
|
|
312
|
+
const targetFile = `${(0, name_formatter_1.toCamelCase)(name)}.dto.mock`;
|
|
313
|
+
const importPath = targetTag === tagFilename ? `./${targetFile}` : `../${targetTag}/${targetFile}`;
|
|
314
|
+
return {
|
|
315
|
+
classname: name,
|
|
316
|
+
classFilename: (0, name_formatter_1.toCamelCase)(name),
|
|
317
|
+
tagFilename: targetTag,
|
|
318
|
+
importPath
|
|
319
|
+
};
|
|
320
|
+
});
|
|
164
321
|
const dtoMockViewData = {
|
|
322
|
+
tagFilename,
|
|
165
323
|
models: [
|
|
166
324
|
{
|
|
167
325
|
model: {
|
|
@@ -174,120 +332,29 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
|
|
|
174
332
|
}
|
|
175
333
|
]
|
|
176
334
|
};
|
|
177
|
-
renderTemplate(templatesDir, 'dto.mock.mustache', dtoMockViewData, path_1.default.join(outputDir, 'data/dtos', `${(0, name_formatter_1.toCamelCase)(baseName)}.dto.mock.ts`), generatedCount, 'mocks');
|
|
335
|
+
renderTemplate(templatesDir, 'dto.mock.mustache', dtoMockViewData, path_1.default.join(outputDir, 'data/dtos', tagFilename, `${(0, name_formatter_1.toCamelCase)(baseName)}.dto.mock.ts`), generatedCount, 'mocks');
|
|
178
336
|
// Model mock — delegates to mapper + DTO mock (no property values needed)
|
|
179
|
-
renderTemplate(templatesDir, 'model.mock.mustache', modelViewData, path_1.default.join(outputDir, 'entities/models', `${(0, name_formatter_1.toCamelCase)(baseName)}.model.mock.ts`), generatedCount, 'mocks');
|
|
337
|
+
renderTemplate(templatesDir, 'model.mock.mustache', modelViewData, path_1.default.join(outputDir, 'entities/models', tagFilename, `${(0, name_formatter_1.toCamelCase)(baseName)}.model.mock.ts`), generatedCount, 'mocks');
|
|
180
338
|
// Model spec
|
|
181
|
-
renderTemplate(templatesDir, 'model-entity.spec.mustache', modelViewData, path_1.default.join(outputDir, 'entities/models', `${(0, name_formatter_1.toCamelCase)(baseName)}.model.spec.ts`), generatedCount, 'specs');
|
|
339
|
+
renderTemplate(templatesDir, 'model-entity.spec.mustache', modelViewData, path_1.default.join(outputDir, 'entities/models', tagFilename, `${(0, name_formatter_1.toCamelCase)(baseName)}.model.spec.ts`), generatedCount, 'specs');
|
|
182
340
|
// Mapper spec
|
|
183
|
-
renderTemplate(templatesDir, 'mapper.spec.mustache', mapperViewData, path_1.default.join(outputDir, 'data/mappers', `${(0, name_formatter_1.toCamelCase)(baseName)}.mapper.spec.ts`), generatedCount, 'specs');
|
|
341
|
+
renderTemplate(templatesDir, 'mapper.spec.mustache', mapperViewData, path_1.default.join(outputDir, 'data/mappers', tagFilename, `${(0, name_formatter_1.toCamelCase)(baseName)}.mapper.spec.ts`), generatedCount, 'specs');
|
|
184
342
|
});
|
|
185
343
|
// 2. Generate Use Cases and Repositories from Paths/Tags
|
|
186
|
-
const tagsMap = {};
|
|
187
|
-
Object.keys(analysis.paths).forEach((pathKey) => {
|
|
188
|
-
const pathObj = analysis.paths[pathKey];
|
|
189
|
-
Object.keys(pathObj).forEach((method) => {
|
|
190
|
-
const op = pathObj[method];
|
|
191
|
-
if (op.tags && op.tags.length > 0) {
|
|
192
|
-
const tag = op.tags[0];
|
|
193
|
-
if (!tagsMap[tag])
|
|
194
|
-
tagsMap[tag] = [];
|
|
195
|
-
const allParams = (op.parameters || []).map((p) => ({
|
|
196
|
-
paramName: p.name,
|
|
197
|
-
dataType: (0, type_mapper_1.mapSwaggerTypeToTs)(p.schema?.type || ''),
|
|
198
|
-
description: p.description || '',
|
|
199
|
-
required: p.required,
|
|
200
|
-
testValue: resolveTestParamValue((0, type_mapper_1.mapSwaggerTypeToTs)(p.schema?.type || ''))
|
|
201
|
-
}));
|
|
202
|
-
if (op.requestBody) {
|
|
203
|
-
let bodyType = 'unknown';
|
|
204
|
-
const content = op.requestBody.content?.['application/json']?.schema;
|
|
205
|
-
if (content) {
|
|
206
|
-
if (content.$ref)
|
|
207
|
-
bodyType = content.$ref.split('/').pop() || 'unknown';
|
|
208
|
-
else if (content.type)
|
|
209
|
-
bodyType = (0, type_mapper_1.mapSwaggerTypeToTs)(content.type);
|
|
210
|
-
}
|
|
211
|
-
allParams.push({
|
|
212
|
-
paramName: 'body',
|
|
213
|
-
dataType: bodyType,
|
|
214
|
-
description: op.requestBody.description || '',
|
|
215
|
-
required: true,
|
|
216
|
-
testValue: resolveTestParamValue(bodyType)
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
let returnType = 'void';
|
|
220
|
-
let returnBaseType = 'void';
|
|
221
|
-
let isListContainer = false;
|
|
222
|
-
const successCode = ['200', '201', '202', '203'].find((code) => op.responses?.[code]);
|
|
223
|
-
const responseSchema = successCode !== undefined
|
|
224
|
-
? op.responses?.[successCode]?.content?.['application/json']?.schema
|
|
225
|
-
: undefined;
|
|
226
|
-
if (responseSchema) {
|
|
227
|
-
if (responseSchema.$ref) {
|
|
228
|
-
returnType = responseSchema.$ref.split('/').pop() || 'unknown';
|
|
229
|
-
returnBaseType = returnType;
|
|
230
|
-
}
|
|
231
|
-
else if (responseSchema.type === 'array' && responseSchema.items?.$ref) {
|
|
232
|
-
returnBaseType = responseSchema.items.$ref.split('/').pop() || 'unknown';
|
|
233
|
-
returnType = `${returnBaseType}[]`;
|
|
234
|
-
isListContainer = true;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
const hasQueryParams = (op.parameters || []).some((p) => p.in === 'query');
|
|
238
|
-
const hasBodyParam = !!op.requestBody;
|
|
239
|
-
tagsMap[tag].push({
|
|
240
|
-
nickname: op.operationId || `${method}${pathKey.replace(/\//g, '_')}`,
|
|
241
|
-
summary: op.summary || '',
|
|
242
|
-
notes: op.description || '',
|
|
243
|
-
httpMethod: method.toLowerCase(),
|
|
244
|
-
uppercaseHttpMethod: method.toUpperCase(),
|
|
245
|
-
path: pathKey,
|
|
246
|
-
allParams: allParams.map((p, i) => ({
|
|
247
|
-
...p,
|
|
248
|
-
'-last': i === allParams.length - 1
|
|
249
|
-
})),
|
|
250
|
-
hasQueryParams,
|
|
251
|
-
queryParams: (op.parameters || [])
|
|
252
|
-
.filter((p) => p.in === 'query')
|
|
253
|
-
.map((p, i, arr) => ({
|
|
254
|
-
paramName: p.name,
|
|
255
|
-
'-last': i === arr.length - 1
|
|
256
|
-
})),
|
|
257
|
-
hasBodyParam,
|
|
258
|
-
bodyParam: 'body',
|
|
259
|
-
hasOptions: hasQueryParams || hasBodyParam,
|
|
260
|
-
hasBothParamsAndBody: hasQueryParams && hasBodyParam,
|
|
261
|
-
returnType: returnType !== 'void' ? returnType : false,
|
|
262
|
-
returnBaseType: returnBaseType !== 'void' ? returnBaseType : false,
|
|
263
|
-
returnTypeVarName: returnType !== 'void' ? (0, name_formatter_1.toCamelCase)(returnType) : false,
|
|
264
|
-
returnBaseTypeVarName: returnBaseType !== 'void' ? (0, name_formatter_1.toCamelCase)(returnBaseType) : false,
|
|
265
|
-
isListContainer: isListContainer,
|
|
266
|
-
vendorExtensions: {}
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
if (Object.keys(selectionFilter).length > 0) {
|
|
272
|
-
Object.keys(tagsMap).forEach((tag) => {
|
|
273
|
-
if (!selectionFilter[tag]) {
|
|
274
|
-
delete tagsMap[tag];
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
tagsMap[tag] = tagsMap[tag].filter((op) => selectionFilter[tag].includes(op.nickname));
|
|
278
|
-
if (tagsMap[tag].length === 0)
|
|
279
|
-
delete tagsMap[tag];
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
344
|
// Generate per tag
|
|
284
345
|
Object.keys(tagsMap).forEach((tag) => {
|
|
346
|
+
const tagFilename = (0, name_formatter_1.toCamelCase)(tag);
|
|
285
347
|
const returnImports = [];
|
|
286
348
|
const paramImports = [];
|
|
287
349
|
Object.keys(schemas).forEach((s) => {
|
|
288
350
|
const usedAsReturn = tagsMap[tag].some((op) => op.returnType === s || op.returnType === `${s}[]`);
|
|
289
351
|
const usedAsParam = tagsMap[tag].some((op) => op.allParams.some((p) => p.dataType === s || p.dataType === `${s}[]`));
|
|
290
|
-
const entry = {
|
|
352
|
+
const entry = {
|
|
353
|
+
classname: s,
|
|
354
|
+
classFilename: (0, name_formatter_1.toCamelCase)(s),
|
|
355
|
+
classVarName: (0, name_formatter_1.toCamelCase)(s),
|
|
356
|
+
tagFilename: schemaTagMap[s] || 'shared'
|
|
357
|
+
};
|
|
291
358
|
if (usedAsReturn) {
|
|
292
359
|
returnImports.push(entry);
|
|
293
360
|
}
|
|
@@ -301,39 +368,35 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
|
|
|
301
368
|
apis: [
|
|
302
369
|
{
|
|
303
370
|
operations: {
|
|
304
|
-
classname: tag,
|
|
305
|
-
classFilename:
|
|
306
|
-
classVarName:
|
|
371
|
+
classname: (0, name_formatter_1.toPascalCase)(tag),
|
|
372
|
+
classFilename: tagFilename,
|
|
373
|
+
classVarName: tagFilename,
|
|
307
374
|
constantName: tag.toUpperCase().replace(/[^A-Z0-9]/g, '_'),
|
|
308
375
|
operation: tagsMap[tag],
|
|
309
|
-
// All entity imports (return + param) — for contracts and use-cases
|
|
310
376
|
imports: [...returnImports, ...paramImports],
|
|
311
|
-
// Return-type-only imports — for repo impl (Dto + Entity + Mapper)
|
|
312
377
|
returnImports,
|
|
313
|
-
// Param-only imports — for repo impl (Entity only, no Dto/Mapper)
|
|
314
378
|
paramImports,
|
|
315
|
-
// Environment API key for the repository base URL (e.g. "aprovalmApi")
|
|
316
379
|
environmentApiKey: tagApiKeyMap[tag] || 'apiUrl'
|
|
317
380
|
}
|
|
318
381
|
}
|
|
319
382
|
]
|
|
320
383
|
}
|
|
321
384
|
};
|
|
322
|
-
renderTemplate(templatesDir, 'api.use-cases.contract.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', `${
|
|
323
|
-
renderTemplate(templatesDir, 'api.use-cases.impl.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', `${
|
|
324
|
-
renderTemplate(templatesDir, 'api.repository.contract.mustache', apiViewData, path_1.default.join(outputDir, 'domain/repositories', `${
|
|
325
|
-
renderTemplate(templatesDir, 'api.repository.impl.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', `${
|
|
326
|
-
renderTemplate(templatesDir, 'use-cases.provider.mustache', apiViewData, path_1.default.join(outputDir, 'di/use-cases', `${
|
|
327
|
-
renderTemplate(templatesDir, 'repository.provider.mustache', apiViewData, path_1.default.join(outputDir, 'di/repositories', `${
|
|
328
|
-
// Mocks
|
|
329
|
-
renderTemplate(templatesDir, 'api.repository.impl.mock.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', `${
|
|
330
|
-
renderTemplate(templatesDir, 'api.use-cases.mock.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', `${
|
|
331
|
-
renderTemplate(templatesDir, 'repository.provider.mock.mustache', apiViewData, path_1.default.join(outputDir, 'di/repositories', `${
|
|
332
|
-
renderTemplate(templatesDir, 'use-cases.provider.mock.mustache', apiViewData, path_1.default.join(outputDir, 'di/use-cases', `${
|
|
385
|
+
renderTemplate(templatesDir, 'api.use-cases.contract.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', tagFilename, `${tagFilename}.use-cases.contract.ts`), generatedCount, 'useCases');
|
|
386
|
+
renderTemplate(templatesDir, 'api.use-cases.impl.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', tagFilename, `${tagFilename}.use-cases.impl.ts`), generatedCount, 'useCases');
|
|
387
|
+
renderTemplate(templatesDir, 'api.repository.contract.mustache', apiViewData, path_1.default.join(outputDir, 'domain/repositories', tagFilename, `${tagFilename}.repository.contract.ts`), generatedCount, 'repositories');
|
|
388
|
+
renderTemplate(templatesDir, 'api.repository.impl.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', tagFilename, `${tagFilename}.repository.impl.ts`), generatedCount, 'repositories');
|
|
389
|
+
renderTemplate(templatesDir, 'use-cases.provider.mustache', apiViewData, path_1.default.join(outputDir, 'di/use-cases', tagFilename, `${tagFilename}.use-cases.provider.ts`), generatedCount, 'providers');
|
|
390
|
+
renderTemplate(templatesDir, 'repository.provider.mustache', apiViewData, path_1.default.join(outputDir, 'di/repositories', tagFilename, `${tagFilename}.repository.provider.ts`), generatedCount, 'providers');
|
|
391
|
+
// Mocks
|
|
392
|
+
renderTemplate(templatesDir, 'api.repository.impl.mock.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', tagFilename, `${tagFilename}.repository.impl.mock.ts`), generatedCount, 'mocks');
|
|
393
|
+
renderTemplate(templatesDir, 'api.use-cases.mock.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', tagFilename, `${tagFilename}.use-cases.mock.ts`), generatedCount, 'mocks');
|
|
394
|
+
renderTemplate(templatesDir, 'repository.provider.mock.mustache', apiViewData, path_1.default.join(outputDir, 'di/repositories', tagFilename, `${tagFilename}.repository.provider.mock.ts`), generatedCount, 'mocks');
|
|
395
|
+
renderTemplate(templatesDir, 'use-cases.provider.mock.mustache', apiViewData, path_1.default.join(outputDir, 'di/use-cases', tagFilename, `${tagFilename}.use-cases.provider.mock.ts`), generatedCount, 'mocks');
|
|
333
396
|
// Repository impl spec
|
|
334
|
-
renderTemplate(templatesDir, 'api.repository.impl.spec.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', `${
|
|
397
|
+
renderTemplate(templatesDir, 'api.repository.impl.spec.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', tagFilename, `${tagFilename}.repository.impl.spec.ts`), generatedCount, 'specs');
|
|
335
398
|
// Use-cases impl spec
|
|
336
|
-
renderTemplate(templatesDir, 'api.use-cases.impl.spec.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', `${
|
|
399
|
+
renderTemplate(templatesDir, 'api.use-cases.impl.spec.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', tagFilename, `${tagFilename}.use-cases.impl.spec.ts`), generatedCount, 'specs');
|
|
337
400
|
});
|
|
338
401
|
(0, logger_1.logSuccess)(`${generatedCount.models} Models, ${generatedCount.repositories} Repos, ${generatedCount.useCases} Use Cases, ${generatedCount.mappers} Mappers, ${generatedCount.providers} Providers, ${generatedCount.mocks} Mocks, ${generatedCount.specs} Specs generated`);
|
|
339
402
|
return generatedCount;
|
|
@@ -344,6 +407,7 @@ function renderTemplate(templatesDir, templateName, viewData, destPath, counter,
|
|
|
344
407
|
if (fs_extra_1.default.existsSync(templatePath)) {
|
|
345
408
|
const template = fs_extra_1.default.readFileSync(templatePath, 'utf8');
|
|
346
409
|
const output = mustache_1.default.render(template, viewData);
|
|
410
|
+
fs_extra_1.default.ensureDirSync(path_1.default.dirname(destPath));
|
|
347
411
|
fs_extra_1.default.writeFileSync(destPath, output);
|
|
348
412
|
counter[key]++;
|
|
349
413
|
(0, logger_1.logDetail)('generate', `${templateName.replace('.mustache', '')} → ${path_1.default.relative(process.cwd(), destPath)}`);
|