@0kmpo/openapi-clean-arch-generator 1.3.14 → 1.4.0

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.
Files changed (29) hide show
  1. package/README.md +132 -63
  2. package/dist/main.js +31 -3
  3. package/dist/package.json +1 -1
  4. package/dist/src/generators/clean-arch.generator.js +221 -145
  5. package/dist/src/generators/dto.generator.js +48 -24
  6. package/dist/src/generators/report.generator.js +6 -0
  7. package/dist/src/utils/config.js +3 -0
  8. package/dist/src/utils/example-validator.js +86 -0
  9. package/dist/src/utils/mock-value-resolver.js +19 -7
  10. package/dist/src/utils/name-formatter.js +133 -2
  11. package/dist/templates/api.repository.contract.mustache +1 -1
  12. package/dist/templates/api.repository.impl.mock.mustache +2 -2
  13. package/dist/templates/api.repository.impl.mustache +5 -5
  14. package/dist/templates/api.repository.impl.spec.mustache +2 -2
  15. package/dist/templates/api.use-cases.contract.mustache +1 -1
  16. package/dist/templates/api.use-cases.impl.mustache +2 -2
  17. package/dist/templates/api.use-cases.impl.spec.mustache +2 -2
  18. package/dist/templates/api.use-cases.mock.mustache +2 -2
  19. package/dist/templates/dto.mock.mustache +1 -1
  20. package/dist/templates/mapper.mustache +2 -2
  21. package/dist/templates/mapper.spec.mustache +2 -2
  22. package/dist/templates/model-entity.mustache +1 -1
  23. package/dist/templates/model-entity.spec.mustache +4 -0
  24. package/dist/templates/model.mock.mustache +2 -2
  25. package/dist/templates/repository.provider.mock.mustache +2 -2
  26. package/dist/templates/repository.provider.mustache +2 -2
  27. package/dist/templates/use-cases.provider.mock.mustache +2 -2
  28. package/dist/templates/use-cases.provider.mustache +2 -2
  29. package/package.json +1 -1
@@ -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 = {}, baseName = '') {
62
195
  (0, logger_1.logStep)('Generating Clean Architecture artefacts using Mustache...');
63
196
  const generatedCount = {
64
197
  models: 0,
@@ -71,14 +204,23 @@ 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
- const baseName = schemaName.replace(/Dto$/, '');
215
+ const schemaBaseName = schemaName.replace(/Dto$/, '');
216
+ const tagFilename = schemaTagMap[schemaBaseName] || 'shared';
217
+ const tagFolderPath = baseName ? `${baseName}/${tagFilename}` : tagFilename;
77
218
  const schemaObj = schemas[schemaName];
78
219
  const rawProperties = schemaObj.properties || {};
79
220
  const requiredProps = schemaObj.required || [];
80
221
  const varsMap = Object.keys(rawProperties).map((k) => {
81
222
  let tsType = (0, type_mapper_1.mapSwaggerTypeToTs)(rawProperties[k].type);
223
+ const isInlineObject = rawProperties[k].type === 'object' && !rawProperties[k].$ref;
82
224
  if (rawProperties[k].$ref) {
83
225
  tsType = rawProperties[k].$ref.split('/').pop() || 'unknown';
84
226
  }
@@ -86,10 +228,12 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
86
228
  tsType = `${rawProperties[k].items.$ref.split('/').pop()}[]`;
87
229
  }
88
230
  return {
89
- name: k,
231
+ name: (0, name_formatter_1.safePropertyName)(k),
232
+ originalName: k,
90
233
  dataType: tsType,
91
234
  description: rawProperties[k].description || '',
92
- required: requiredProps.includes(k)
235
+ required: requiredProps.includes(k),
236
+ hasMockValue: !isInlineObject
93
237
  };
94
238
  });
95
239
  // Collect imports for types referenced via $ref in properties
@@ -102,16 +246,23 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
102
246
  referencedTypes.add(prop.items.$ref.split('/').pop() || '');
103
247
  }
104
248
  });
105
- const modelImports = [...referencedTypes]
106
- .filter(Boolean)
107
- .map((name) => ({ classname: name, classFilename: (0, name_formatter_1.toCamelCase)(name) }));
249
+ const modelImports = [...referencedTypes].filter(Boolean).map((name) => ({
250
+ classname: name,
251
+ classFilename: (0, name_formatter_1.toCamelCase)(name),
252
+ tagFilename: schemaTagMap[name] || 'shared',
253
+ tagFolderPath: baseName
254
+ ? `${baseName}/${schemaTagMap[name] || 'shared'}`
255
+ : schemaTagMap[name] || 'shared'
256
+ }));
108
257
  const modelViewData = {
258
+ tagFilename,
259
+ tagFolderPath,
109
260
  models: [
110
261
  {
111
262
  model: {
112
- classname: baseName,
113
- classFilename: (0, name_formatter_1.toCamelCase)(baseName),
114
- classVarName: (0, name_formatter_1.toCamelCase)(baseName),
263
+ classname: schemaBaseName,
264
+ classFilename: (0, name_formatter_1.toCamelCase)(schemaBaseName),
265
+ classVarName: (0, name_formatter_1.toCamelCase)(schemaBaseName),
115
266
  description: schemaObj.description || '',
116
267
  imports: modelImports,
117
268
  vars: varsMap
@@ -126,9 +277,11 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
126
277
  apis: [
127
278
  {
128
279
  operations: {
129
- classname: baseName,
130
- classFilename: (0, name_formatter_1.toCamelCase)(baseName),
131
- classVarName: (0, name_formatter_1.toCamelCase)(baseName)
280
+ classname: schemaBaseName,
281
+ classFilename: (0, name_formatter_1.toCamelCase)(schemaBaseName),
282
+ classVarName: (0, name_formatter_1.toCamelCase)(schemaBaseName),
283
+ tagFilename,
284
+ tagFolderPath
132
285
  }
133
286
  }
134
287
  ]
@@ -139,7 +292,8 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
139
292
  if (fs_extra_1.default.existsSync(modelTemplatePath)) {
140
293
  const template = fs_extra_1.default.readFileSync(modelTemplatePath, 'utf8');
141
294
  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`);
295
+ const destPath = path_1.default.join(outputDir, 'entities/models', tagFolderPath, `${(0, name_formatter_1.toCamelCase)(schemaBaseName)}.model.ts`);
296
+ fs_extra_1.default.ensureDirSync(path_1.default.dirname(destPath));
143
297
  fs_extra_1.default.writeFileSync(destPath, output);
144
298
  generatedCount.models++;
145
299
  (0, logger_1.logDetail)('generate', `model-entity → ${path_1.default.relative(process.cwd(), destPath)}`);
@@ -149,145 +303,69 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
149
303
  if (fs_extra_1.default.existsSync(mapperTemplatePath)) {
150
304
  const template = fs_extra_1.default.readFileSync(mapperTemplatePath, 'utf8');
151
305
  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`);
306
+ const destPath = path_1.default.join(outputDir, 'data/mappers', tagFolderPath, `${(0, name_formatter_1.toCamelCase)(schemaBaseName)}.mapper.ts`);
307
+ fs_extra_1.default.ensureDirSync(path_1.default.dirname(destPath));
153
308
  fs_extra_1.default.writeFileSync(destPath, output);
154
309
  generatedCount.mappers++;
155
310
  }
156
311
  // DTO mock — values resolved from raw schema (example, format, type)
157
312
  const dtoMockVarsMap = Object.keys(rawProperties).map((k) => ({
158
- name: k,
159
- mockValue: (0, mock_value_resolver_1.resolveMockValue)(k, rawProperties[k], 'dto')
313
+ name: (0, name_formatter_1.safePropertyName)(k),
314
+ mockValue: (0, mock_value_resolver_1.resolveMockValue)(k, rawProperties[k], 'dto', schemaName)
160
315
  }));
161
- const dtoMockImports = [...referencedTypes]
162
- .filter(Boolean)
163
- .map((name) => ({ classname: name, classFilename: (0, name_formatter_1.toCamelCase)(name) }));
316
+ const dtoMockImports = [...referencedTypes].filter(Boolean).map((name) => {
317
+ const targetTag = schemaTagMap[name] || 'shared';
318
+ const targetFile = `${(0, name_formatter_1.toCamelCase)(name)}.dto.mock`;
319
+ const importPath = targetTag === tagFilename ? `./${targetFile}` : `../${targetTag}/${targetFile}`;
320
+ return {
321
+ classname: name,
322
+ classFilename: (0, name_formatter_1.toCamelCase)(name),
323
+ tagFilename: targetTag,
324
+ tagFolderPath: baseName ? `${baseName}/${targetTag}` : targetTag,
325
+ importPath
326
+ };
327
+ });
164
328
  const dtoMockViewData = {
329
+ tagFilename,
330
+ tagFolderPath,
165
331
  models: [
166
332
  {
167
333
  model: {
168
- classname: baseName,
169
- classFilename: (0, name_formatter_1.toCamelCase)(baseName),
170
- classVarName: (0, name_formatter_1.toCamelCase)(baseName),
334
+ classname: schemaBaseName,
335
+ classFilename: (0, name_formatter_1.toCamelCase)(schemaBaseName),
336
+ classVarName: (0, name_formatter_1.toCamelCase)(schemaBaseName),
171
337
  mockImports: dtoMockImports,
172
338
  vars: dtoMockVarsMap
173
339
  }
174
340
  }
175
341
  ]
176
342
  };
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');
343
+ renderTemplate(templatesDir, 'dto.mock.mustache', dtoMockViewData, path_1.default.join(outputDir, 'data/dtos', tagFolderPath, `${(0, name_formatter_1.toCamelCase)(schemaBaseName)}.dto.mock.ts`), generatedCount, 'mocks');
178
344
  // 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');
345
+ renderTemplate(templatesDir, 'model.mock.mustache', modelViewData, path_1.default.join(outputDir, 'entities/models', tagFolderPath, `${(0, name_formatter_1.toCamelCase)(schemaBaseName)}.model.mock.ts`), generatedCount, 'mocks');
180
346
  // 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');
347
+ renderTemplate(templatesDir, 'model-entity.spec.mustache', modelViewData, path_1.default.join(outputDir, 'entities/models', tagFolderPath, `${(0, name_formatter_1.toCamelCase)(schemaBaseName)}.model.spec.ts`), generatedCount, 'specs');
182
348
  // 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');
349
+ renderTemplate(templatesDir, 'mapper.spec.mustache', mapperViewData, path_1.default.join(outputDir, 'data/mappers', tagFolderPath, `${(0, name_formatter_1.toCamelCase)(schemaBaseName)}.mapper.spec.ts`), generatedCount, 'specs');
184
350
  });
185
351
  // 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
352
  // Generate per tag
284
353
  Object.keys(tagsMap).forEach((tag) => {
354
+ const tagFilename = (0, name_formatter_1.toCamelCase)(tag);
355
+ const tagFolderPath = baseName ? `${baseName}/${tagFilename}` : tagFilename;
285
356
  const returnImports = [];
286
357
  const paramImports = [];
287
358
  Object.keys(schemas).forEach((s) => {
288
359
  const usedAsReturn = tagsMap[tag].some((op) => op.returnType === s || op.returnType === `${s}[]`);
289
360
  const usedAsParam = tagsMap[tag].some((op) => op.allParams.some((p) => p.dataType === s || p.dataType === `${s}[]`));
290
- const entry = { classname: s, classFilename: (0, name_formatter_1.toCamelCase)(s), classVarName: (0, name_formatter_1.toCamelCase)(s) };
361
+ const schemaTag = schemaTagMap[s] || 'shared';
362
+ const entry = {
363
+ classname: s,
364
+ classFilename: (0, name_formatter_1.toCamelCase)(s),
365
+ classVarName: (0, name_formatter_1.toCamelCase)(s),
366
+ tagFilename: schemaTag,
367
+ tagFolderPath: baseName ? `${baseName}/${schemaTag}` : schemaTag
368
+ };
291
369
  if (usedAsReturn) {
292
370
  returnImports.push(entry);
293
371
  }
@@ -301,39 +379,36 @@ function generateCleanArchitecture(analysis, outputDir, templatesDir, tagApiKeyM
301
379
  apis: [
302
380
  {
303
381
  operations: {
304
- classname: tag,
305
- classFilename: (0, name_formatter_1.toCamelCase)(tag),
306
- classVarName: (0, name_formatter_1.toCamelCase)(tag),
382
+ classname: (0, name_formatter_1.toPascalCase)(tag),
383
+ classFilename: tagFilename,
384
+ classVarName: tagFilename,
385
+ tagFolderPath,
307
386
  constantName: tag.toUpperCase().replace(/[^A-Z0-9]/g, '_'),
308
387
  operation: tagsMap[tag],
309
- // All entity imports (return + param) — for contracts and use-cases
310
388
  imports: [...returnImports, ...paramImports],
311
- // Return-type-only imports — for repo impl (Dto + Entity + Mapper)
312
389
  returnImports,
313
- // Param-only imports — for repo impl (Entity only, no Dto/Mapper)
314
390
  paramImports,
315
- // Environment API key for the repository base URL (e.g. "aprovalmApi")
316
391
  environmentApiKey: tagApiKeyMap[tag] || 'apiUrl'
317
392
  }
318
393
  }
319
394
  ]
320
395
  }
321
396
  };
322
- renderTemplate(templatesDir, 'api.use-cases.contract.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', `${(0, name_formatter_1.toCamelCase)(tag)}.use-cases.contract.ts`), generatedCount, 'useCases');
323
- renderTemplate(templatesDir, 'api.use-cases.impl.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', `${(0, name_formatter_1.toCamelCase)(tag)}.use-cases.impl.ts`), generatedCount, 'useCases');
324
- renderTemplate(templatesDir, 'api.repository.contract.mustache', apiViewData, path_1.default.join(outputDir, 'domain/repositories', `${(0, name_formatter_1.toCamelCase)(tag)}.repository.contract.ts`), generatedCount, 'repositories');
325
- renderTemplate(templatesDir, 'api.repository.impl.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', `${(0, name_formatter_1.toCamelCase)(tag)}.repository.impl.ts`), generatedCount, 'repositories');
326
- renderTemplate(templatesDir, 'use-cases.provider.mustache', apiViewData, path_1.default.join(outputDir, 'di/use-cases', `${(0, name_formatter_1.toCamelCase)(tag)}.use-cases.provider.ts`), generatedCount, 'providers');
327
- renderTemplate(templatesDir, 'repository.provider.mustache', apiViewData, path_1.default.join(outputDir, 'di/repositories', `${(0, name_formatter_1.toCamelCase)(tag)}.repository.provider.ts`), generatedCount, 'providers');
328
- // Mocks — repository impl, use-cases impl, repository provider, use-cases provider
329
- renderTemplate(templatesDir, 'api.repository.impl.mock.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', `${(0, name_formatter_1.toCamelCase)(tag)}.repository.impl.mock.ts`), generatedCount, 'mocks');
330
- renderTemplate(templatesDir, 'api.use-cases.mock.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', `${(0, name_formatter_1.toCamelCase)(tag)}.use-cases.mock.ts`), generatedCount, 'mocks');
331
- renderTemplate(templatesDir, 'repository.provider.mock.mustache', apiViewData, path_1.default.join(outputDir, 'di/repositories', `${(0, name_formatter_1.toCamelCase)(tag)}.repository.provider.mock.ts`), generatedCount, 'mocks');
332
- renderTemplate(templatesDir, 'use-cases.provider.mock.mustache', apiViewData, path_1.default.join(outputDir, 'di/use-cases', `${(0, name_formatter_1.toCamelCase)(tag)}.use-cases.provider.mock.ts`), generatedCount, 'mocks');
397
+ renderTemplate(templatesDir, 'api.use-cases.contract.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', tagFolderPath, `${tagFilename}.use-cases.contract.ts`), generatedCount, 'useCases');
398
+ renderTemplate(templatesDir, 'api.use-cases.impl.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', tagFolderPath, `${tagFilename}.use-cases.impl.ts`), generatedCount, 'useCases');
399
+ renderTemplate(templatesDir, 'api.repository.contract.mustache', apiViewData, path_1.default.join(outputDir, 'domain/repositories', tagFolderPath, `${tagFilename}.repository.contract.ts`), generatedCount, 'repositories');
400
+ renderTemplate(templatesDir, 'api.repository.impl.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', tagFolderPath, `${tagFilename}.repository.impl.ts`), generatedCount, 'repositories');
401
+ renderTemplate(templatesDir, 'use-cases.provider.mustache', apiViewData, path_1.default.join(outputDir, 'di/use-cases', tagFolderPath, `${tagFilename}.use-cases.provider.ts`), generatedCount, 'providers');
402
+ renderTemplate(templatesDir, 'repository.provider.mustache', apiViewData, path_1.default.join(outputDir, 'di/repositories', tagFolderPath, `${tagFilename}.repository.provider.ts`), generatedCount, 'providers');
403
+ // Mocks
404
+ renderTemplate(templatesDir, 'api.repository.impl.mock.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', tagFolderPath, `${tagFilename}.repository.impl.mock.ts`), generatedCount, 'mocks');
405
+ renderTemplate(templatesDir, 'api.use-cases.mock.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', tagFolderPath, `${tagFilename}.use-cases.mock.ts`), generatedCount, 'mocks');
406
+ renderTemplate(templatesDir, 'repository.provider.mock.mustache', apiViewData, path_1.default.join(outputDir, 'di/repositories', tagFolderPath, `${tagFilename}.repository.provider.mock.ts`), generatedCount, 'mocks');
407
+ renderTemplate(templatesDir, 'use-cases.provider.mock.mustache', apiViewData, path_1.default.join(outputDir, 'di/use-cases', tagFolderPath, `${tagFilename}.use-cases.provider.mock.ts`), generatedCount, 'mocks');
333
408
  // Repository impl spec
334
- renderTemplate(templatesDir, 'api.repository.impl.spec.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', `${(0, name_formatter_1.toCamelCase)(tag)}.repository.impl.spec.ts`), generatedCount, 'specs');
409
+ renderTemplate(templatesDir, 'api.repository.impl.spec.mustache', apiViewData, path_1.default.join(outputDir, 'data/repositories', tagFolderPath, `${tagFilename}.repository.impl.spec.ts`), generatedCount, 'specs');
335
410
  // Use-cases impl spec
336
- renderTemplate(templatesDir, 'api.use-cases.impl.spec.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', `${(0, name_formatter_1.toCamelCase)(tag)}.use-cases.impl.spec.ts`), generatedCount, 'specs');
411
+ renderTemplate(templatesDir, 'api.use-cases.impl.spec.mustache', apiViewData, path_1.default.join(outputDir, 'domain/use-cases', tagFolderPath, `${tagFilename}.use-cases.impl.spec.ts`), generatedCount, 'specs');
337
412
  });
338
413
  (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
414
  return generatedCount;
@@ -344,6 +419,7 @@ function renderTemplate(templatesDir, templateName, viewData, destPath, counter,
344
419
  if (fs_extra_1.default.existsSync(templatePath)) {
345
420
  const template = fs_extra_1.default.readFileSync(templatePath, 'utf8');
346
421
  const output = mustache_1.default.render(template, viewData);
422
+ fs_extra_1.default.ensureDirSync(path_1.default.dirname(destPath));
347
423
  fs_extra_1.default.writeFileSync(destPath, output);
348
424
  counter[key]++;
349
425
  (0, logger_1.logDetail)('generate', `${templateName.replace('.mustache', '')} → ${path_1.default.relative(process.cwd(), destPath)}`);
@@ -10,6 +10,7 @@ const child_process_1 = require("child_process");
10
10
  const fs_extra_1 = __importDefault(require("fs-extra"));
11
11
  const path_1 = __importDefault(require("path"));
12
12
  const logger_1 = require("../utils/logger");
13
+ const name_formatter_1 = require("../utils/name-formatter");
13
14
  /** Invokes `openapi-generator-cli` to generate DTOs into a temporary directory. */
14
15
  function generateCode(swaggerFile, templatesDir) {
15
16
  (0, logger_1.logStep)('Generating code from OpenAPI spec...');
@@ -37,8 +38,8 @@ function generateCode(swaggerFile, templatesDir) {
37
38
  process.exit(1);
38
39
  }
39
40
  }
40
- /** Copies the generated DTOs from the temporary directory to the output directory. */
41
- function organizeFiles(tempDir, outputDir) {
41
+ /** Copies the generated DTOs from the temporary directory to the output directory, organised by tag subfolder. */
42
+ function organizeFiles(tempDir, outputDir, schemaTagMap = {}, baseName = '') {
42
43
  (0, logger_1.logStep)('Organising generated DTO files...');
43
44
  const sourceDir = path_1.default.join(tempDir, 'model');
44
45
  const destDir = path_1.default.join(outputDir, 'data/dtos');
@@ -47,8 +48,15 @@ function organizeFiles(tempDir, outputDir) {
47
48
  fs_extra_1.default.ensureDirSync(destDir);
48
49
  const files = fs_extra_1.default.readdirSync(sourceDir).filter((file) => file.endsWith('.dto.ts'));
49
50
  files.forEach((file) => {
51
+ // file is like "userResponse.dto.ts" → derive PascalCase schema name to look up tag
52
+ const camelName = file.replace('.dto.ts', '');
53
+ const pascalName = (0, name_formatter_1.toPascalCase)(camelName);
54
+ const tagFolder = schemaTagMap[pascalName] || 'shared';
50
55
  const sourcePath = path_1.default.join(sourceDir, file);
51
- const destPath = path_1.default.join(destDir, file);
56
+ const destPath = baseName
57
+ ? path_1.default.join(destDir, baseName, tagFolder, file)
58
+ : path_1.default.join(destDir, tagFolder, file);
59
+ fs_extra_1.default.ensureDirSync(path_1.default.dirname(destPath));
52
60
  fs_extra_1.default.copySync(sourcePath, destPath);
53
61
  filesMoved++;
54
62
  (0, logger_1.logDetail)('dto', `${file} → ${path_1.default.relative(process.cwd(), destPath)}`);
@@ -57,51 +65,67 @@ function organizeFiles(tempDir, outputDir) {
57
65
  (0, logger_1.logSuccess)(`${filesMoved} DTOs moved successfully`);
58
66
  }
59
67
  /** Post-processes the generated DTOs: adds cross-DTO imports and normalises Array<T> → T[]. */
60
- function addDtoImports(outputDir) {
68
+ function addDtoImports(outputDir, baseName = '') {
61
69
  (0, logger_1.logStep)('Post-processing generated DTOs...');
62
- const dtosDir = path_1.default.join(outputDir, 'data/dtos');
70
+ const dtosDir = baseName
71
+ ? path_1.default.join(outputDir, 'data/dtos', baseName)
72
+ : path_1.default.join(outputDir, 'data/dtos');
63
73
  if (!fs_extra_1.default.existsSync(dtosDir))
64
74
  return;
65
- const files = fs_extra_1.default.readdirSync(dtosDir).filter((f) => f.endsWith('.dto.ts'));
66
- // Build a map of DTO classname → file base name (without .ts)
75
+ // Collect all .dto.ts files from all subfolders (1 level deep)
76
+ const allFiles = [];
77
+ const entries = fs_extra_1.default.readdirSync(dtosDir);
78
+ entries.forEach((entry) => {
79
+ const entryPath = path_1.default.join(dtosDir, entry);
80
+ if (fs_extra_1.default.statSync(entryPath).isDirectory()) {
81
+ fs_extra_1.default.readdirSync(entryPath)
82
+ .filter((f) => f.endsWith('.dto.ts'))
83
+ .forEach((file) => allFiles.push({ subfolder: entry, file, fullPath: path_1.default.join(entryPath, file) }));
84
+ }
85
+ else if (entry.endsWith('.dto.ts')) {
86
+ allFiles.push({ subfolder: '', file: entry, fullPath: entryPath });
87
+ }
88
+ });
89
+ // Build map: ClassName → { subfolder, fileBase }
67
90
  const dtoMap = {};
68
- files.forEach((file) => {
69
- const content = fs_extra_1.default.readFileSync(path_1.default.join(dtosDir, file), 'utf8');
91
+ allFiles.forEach(({ subfolder, file, fullPath }) => {
92
+ const content = fs_extra_1.default.readFileSync(fullPath, 'utf8');
70
93
  const match = content.match(/export interface (\w+)/);
71
- if (match) {
72
- dtoMap[match[1]] = file.replace('.ts', '');
73
- }
94
+ if (match)
95
+ dtoMap[match[1]] = { subfolder, fileBase: file.replace('.ts', '') };
74
96
  });
75
97
  let filesProcessed = 0;
76
- files.forEach((file) => {
77
- const filePath = path_1.default.join(dtosDir, file);
78
- const originalContent = fs_extra_1.default.readFileSync(filePath, 'utf8');
98
+ allFiles.forEach(({ subfolder, file, fullPath }) => {
99
+ const originalContent = fs_extra_1.default.readFileSync(fullPath, 'utf8');
79
100
  let content = originalContent;
80
101
  const selfMatch = content.match(/export interface (\w+)/);
81
102
  const selfName = selfMatch ? selfMatch[1] : '';
82
- // Normalize Array<T> → T[] (openapi-generator-cli always outputs Array<T>)
83
103
  content = content.replace(/Array<(\w+)>/g, '$1[]');
84
- // Find all Dto type references in the file body (excluding the interface name itself)
85
104
  const references = new Set();
86
105
  const typeRegex = /\b(\w+Dto)\b/g;
87
106
  let match;
88
107
  while ((match = typeRegex.exec(content)) !== null) {
89
- if (match[1] !== selfName) {
108
+ if (match[1] !== selfName)
90
109
  references.add(match[1]);
91
- }
92
110
  }
93
- // Build import lines for each referenced type that exists in the dtoMap
94
111
  const imports = [];
95
112
  references.forEach((ref) => {
96
113
  if (dtoMap[ref]) {
97
- imports.push(`import { ${ref} } from './${dtoMap[ref]}';`);
114
+ const { subfolder: refSubfolder, fileBase: refFileBase } = dtoMap[ref];
115
+ const fromDir = subfolder ? path_1.default.join(dtosDir, subfolder) : dtosDir;
116
+ const toFile = refSubfolder
117
+ ? path_1.default.join(dtosDir, refSubfolder, refFileBase)
118
+ : path_1.default.join(dtosDir, refFileBase);
119
+ let relPath = path_1.default.relative(fromDir, toFile).replace(/\\/g, '/');
120
+ if (!relPath.startsWith('.'))
121
+ relPath = './' + relPath;
122
+ imports.push(`import { ${ref} } from '${relPath}';`);
98
123
  }
99
124
  });
100
- if (imports.length > 0) {
125
+ if (imports.length > 0)
101
126
  content = imports.join('\n') + '\n' + content;
102
- }
103
127
  if (content !== originalContent) {
104
- fs_extra_1.default.writeFileSync(filePath, content);
128
+ fs_extra_1.default.writeFileSync(fullPath, content);
105
129
  filesProcessed++;
106
130
  (0, logger_1.logDetail)('dto', `Post-processed ${file} (added ${imports.length} import(s))`);
107
131
  }
@@ -7,6 +7,7 @@ exports.generateReport = generateReport;
7
7
  const fs_extra_1 = __importDefault(require("fs-extra"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const logger_1 = require("../utils/logger");
10
+ const example_validator_1 = require("../utils/example-validator");
10
11
  /** Counts files ending with `.mock.ts` in a directory (returns 0 if directory does not exist). */
11
12
  function countMockFiles(dir) {
12
13
  try {
@@ -37,6 +38,7 @@ function generateReport(outputDir, analysis, lintResult) {
37
38
  })).length;
38
39
  return { name: t.name, description: t.description || '', endpoints: endpointCount };
39
40
  });
41
+ const exampleMismatches = (0, example_validator_1.getExampleMismatches)();
40
42
  const report = {
41
43
  timestamp: new Date().toISOString(),
42
44
  tags: analysis.tags.length,
@@ -44,6 +46,10 @@ function generateReport(outputDir, analysis, lintResult) {
44
46
  tagDetails,
45
47
  outputDirectory: outputDir,
46
48
  linting: lintResult,
49
+ warnings: {
50
+ exampleMismatches: exampleMismatches.map((m) => ({ ...m })),
51
+ total: exampleMismatches.length
52
+ },
47
53
  structure: {
48
54
  dtos: fs_extra_1.default.readdirSync(path_1.default.join(outputDir, 'data/dtos')).length,
49
55
  repositories: fs_extra_1.default.readdirSync(path_1.default.join(outputDir, 'data/repositories')).length,