@currentjs/gen 0.3.1 → 0.5.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.
- package/CHANGELOG.md +8 -289
- package/README.md +623 -427
- package/dist/cli.js +2 -1
- package/dist/commands/commit.js +25 -42
- package/dist/commands/createApp.js +1 -0
- package/dist/commands/createModule.js +151 -45
- package/dist/commands/diff.js +27 -40
- package/dist/commands/generateAll.js +141 -291
- package/dist/commands/migrateCommit.js +6 -18
- package/dist/commands/migratePush.d.ts +1 -0
- package/dist/commands/migratePush.js +135 -0
- package/dist/commands/migrateUpdate.d.ts +1 -0
- package/dist/commands/migrateUpdate.js +147 -0
- package/dist/commands/newGenerateAll.d.ts +4 -0
- package/dist/commands/newGenerateAll.js +336 -0
- package/dist/generators/controllerGenerator.d.ts +43 -19
- package/dist/generators/controllerGenerator.js +547 -329
- package/dist/generators/domainLayerGenerator.d.ts +21 -0
- package/dist/generators/domainLayerGenerator.js +276 -0
- package/dist/generators/dtoGenerator.d.ts +21 -0
- package/dist/generators/dtoGenerator.js +518 -0
- package/dist/generators/newControllerGenerator.d.ts +55 -0
- package/dist/generators/newControllerGenerator.js +644 -0
- package/dist/generators/newServiceGenerator.d.ts +19 -0
- package/dist/generators/newServiceGenerator.js +266 -0
- package/dist/generators/newStoreGenerator.d.ts +39 -0
- package/dist/generators/newStoreGenerator.js +408 -0
- package/dist/generators/newTemplateGenerator.d.ts +29 -0
- package/dist/generators/newTemplateGenerator.js +510 -0
- package/dist/generators/serviceGenerator.d.ts +16 -51
- package/dist/generators/serviceGenerator.js +167 -586
- package/dist/generators/storeGenerator.d.ts +35 -32
- package/dist/generators/storeGenerator.js +291 -238
- package/dist/generators/storeGeneratorV2.d.ts +31 -0
- package/dist/generators/storeGeneratorV2.js +190 -0
- package/dist/generators/templateGenerator.d.ts +21 -21
- package/dist/generators/templateGenerator.js +393 -268
- package/dist/generators/templates/appTemplates.d.ts +3 -1
- package/dist/generators/templates/appTemplates.js +15 -10
- package/dist/generators/templates/data/appYamlTemplate +5 -2
- package/dist/generators/templates/data/cursorRulesTemplate +315 -221
- package/dist/generators/templates/data/frontendScriptTemplate +76 -47
- package/dist/generators/templates/data/mainViewTemplate +1 -1
- package/dist/generators/templates/data/systemTsTemplate +5 -0
- package/dist/generators/templates/index.d.ts +0 -3
- package/dist/generators/templates/index.js +0 -3
- package/dist/generators/templates/newStoreTemplates.d.ts +5 -0
- package/dist/generators/templates/newStoreTemplates.js +141 -0
- package/dist/generators/templates/storeTemplates.d.ts +1 -5
- package/dist/generators/templates/storeTemplates.js +102 -219
- package/dist/generators/templates/viewTemplates.js +1 -1
- package/dist/generators/useCaseGenerator.d.ts +13 -0
- package/dist/generators/useCaseGenerator.js +188 -0
- package/dist/types/configTypes.d.ts +148 -0
- package/dist/types/configTypes.js +10 -0
- package/dist/utils/childEntityUtils.d.ts +18 -0
- package/dist/utils/childEntityUtils.js +78 -0
- package/dist/utils/commandUtils.d.ts +43 -0
- package/dist/utils/commandUtils.js +124 -0
- package/dist/utils/commitUtils.d.ts +4 -1
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/constants.js +13 -1
- package/dist/utils/diResolver.d.ts +32 -0
- package/dist/utils/diResolver.js +204 -0
- package/dist/utils/new_parts_of_migrationUtils.d.ts +0 -0
- package/dist/utils/new_parts_of_migrationUtils.js +164 -0
- package/dist/utils/typeUtils.d.ts +19 -0
- package/dist/utils/typeUtils.js +70 -0
- package/package.json +7 -3
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DtoGenerator = void 0;
|
|
37
|
+
const yaml_1 = require("yaml");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const generationRegistry_1 = require("../utils/generationRegistry");
|
|
41
|
+
const colors_1 = require("../utils/colors");
|
|
42
|
+
const configTypes_1 = require("../types/configTypes");
|
|
43
|
+
const childEntityUtils_1 = require("../utils/childEntityUtils");
|
|
44
|
+
const typeUtils_1 = require("../utils/typeUtils");
|
|
45
|
+
class DtoGenerator {
|
|
46
|
+
constructor() {
|
|
47
|
+
this.availableAggregates = new Map();
|
|
48
|
+
this.availableValueObjects = new Map();
|
|
49
|
+
}
|
|
50
|
+
mapType(yamlType) {
|
|
51
|
+
return (0, typeUtils_1.mapType)(yamlType, this.availableAggregates, this.availableValueObjects);
|
|
52
|
+
}
|
|
53
|
+
isValueObjectType(yamlType) {
|
|
54
|
+
const capitalizedType = (0, typeUtils_1.capitalize)(yamlType);
|
|
55
|
+
return this.availableValueObjects.has(capitalizedType);
|
|
56
|
+
}
|
|
57
|
+
getValidationCode(fieldName, fieldType, isRequired) {
|
|
58
|
+
const checks = [];
|
|
59
|
+
if (isRequired) {
|
|
60
|
+
if (fieldType === 'string') {
|
|
61
|
+
checks.push(` if (typeof b.${fieldName} !== 'string' || !b.${fieldName}) {
|
|
62
|
+
throw new Error('${fieldName} is required');
|
|
63
|
+
}`);
|
|
64
|
+
}
|
|
65
|
+
else if (fieldType === 'number' || fieldType === 'integer' || fieldType === 'decimal' || fieldType === 'id') {
|
|
66
|
+
checks.push(` if (b.${fieldName} === undefined || b.${fieldName} === null) {
|
|
67
|
+
throw new Error('${fieldName} is required');
|
|
68
|
+
}`);
|
|
69
|
+
}
|
|
70
|
+
else if (fieldType === 'boolean') {
|
|
71
|
+
checks.push(` if (typeof b.${fieldName} !== 'boolean') {
|
|
72
|
+
throw new Error('${fieldName} is required');
|
|
73
|
+
}`);
|
|
74
|
+
}
|
|
75
|
+
else if (fieldType === 'datetime' || fieldType === 'date') {
|
|
76
|
+
checks.push(` if (!b.${fieldName}) {
|
|
77
|
+
throw new Error('${fieldName} is required');
|
|
78
|
+
}`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
checks.push(` if (b.${fieldName} === undefined) {
|
|
82
|
+
throw new Error('${fieldName} is required');
|
|
83
|
+
}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return checks;
|
|
87
|
+
}
|
|
88
|
+
getTransformCode(fieldName, fieldType) {
|
|
89
|
+
if (fieldType === 'datetime' || fieldType === 'date') {
|
|
90
|
+
return `b.${fieldName} ? new Date(b.${fieldName} as string) : undefined`;
|
|
91
|
+
}
|
|
92
|
+
else if (fieldType === 'number' || fieldType === 'integer' || fieldType === 'decimal' || fieldType === 'id') {
|
|
93
|
+
return `typeof b.${fieldName} === 'string' ? parseFloat(b.${fieldName}) : b.${fieldName} as number`;
|
|
94
|
+
}
|
|
95
|
+
else if (fieldType === 'boolean') {
|
|
96
|
+
return `Boolean(b.${fieldName})`;
|
|
97
|
+
}
|
|
98
|
+
return `b.${fieldName} as ${this.mapType(fieldType)}`;
|
|
99
|
+
}
|
|
100
|
+
generateInputDto(modelName, actionName, inputConfig, aggregateConfig, childInfo) {
|
|
101
|
+
var _a, _b, _c, _d;
|
|
102
|
+
const className = `${modelName}${(0, typeUtils_1.capitalize)(actionName)}Input`;
|
|
103
|
+
if (!inputConfig) {
|
|
104
|
+
return `export class ${className} {
|
|
105
|
+
private constructor() {}
|
|
106
|
+
|
|
107
|
+
static parse(body: unknown): ${className} {
|
|
108
|
+
return new ${className}();
|
|
109
|
+
}
|
|
110
|
+
}`;
|
|
111
|
+
}
|
|
112
|
+
const fieldDeclarations = [];
|
|
113
|
+
const constructorParams = [];
|
|
114
|
+
const constructorAssignments = [];
|
|
115
|
+
const validationChecks = [];
|
|
116
|
+
const fieldTransforms = [];
|
|
117
|
+
// Handle identifier (for get, update, delete)
|
|
118
|
+
if (inputConfig.identifier) {
|
|
119
|
+
const fieldName = inputConfig.identifier;
|
|
120
|
+
fieldDeclarations.push(` readonly ${fieldName}: number;`);
|
|
121
|
+
constructorParams.push(`${fieldName}: number`);
|
|
122
|
+
constructorAssignments.push(` this.${fieldName} = ${fieldName};`);
|
|
123
|
+
validationChecks.push(` if (b.${fieldName} === undefined || b.${fieldName} === null) {
|
|
124
|
+
throw new Error('${fieldName} is required');
|
|
125
|
+
}`);
|
|
126
|
+
fieldTransforms.push(` ${fieldName}: typeof b.${fieldName} === 'string' ? parseInt(b.${fieldName}, 10) : b.${fieldName} as number`);
|
|
127
|
+
}
|
|
128
|
+
// Handle pagination
|
|
129
|
+
if (inputConfig.pagination) {
|
|
130
|
+
fieldDeclarations.push(` readonly page: number;`);
|
|
131
|
+
fieldDeclarations.push(` readonly limit: number;`);
|
|
132
|
+
constructorParams.push(`page: number`);
|
|
133
|
+
constructorParams.push(`limit: number`);
|
|
134
|
+
constructorAssignments.push(` this.page = page;`);
|
|
135
|
+
constructorAssignments.push(` this.limit = limit;`);
|
|
136
|
+
const defaultLimit = ((_a = inputConfig.pagination.defaults) === null || _a === void 0 ? void 0 : _a.limit) || 20;
|
|
137
|
+
const maxLimit = ((_b = inputConfig.pagination.defaults) === null || _b === void 0 ? void 0 : _b.maxLimit) || 100;
|
|
138
|
+
fieldTransforms.push(` page: typeof b.page === 'string' ? parseInt(b.page, 10) : (b.page as number || 1)`);
|
|
139
|
+
fieldTransforms.push(` limit: Math.min(typeof b.limit === 'string' ? parseInt(b.limit, 10) : (b.limit as number || ${defaultLimit}), ${maxLimit})`);
|
|
140
|
+
if (inputConfig.pagination.type === 'cursor') {
|
|
141
|
+
fieldDeclarations.push(` readonly cursor?: string;`);
|
|
142
|
+
constructorParams.push(`cursor?: string`);
|
|
143
|
+
constructorAssignments.push(` this.cursor = cursor;`);
|
|
144
|
+
fieldTransforms.push(` cursor: b.cursor as string | undefined`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Handle fields from aggregate (pick/omit/add)
|
|
148
|
+
if (inputConfig.from) {
|
|
149
|
+
const aggregateFields = Object.entries(aggregateConfig.fields);
|
|
150
|
+
let fieldsToInclude = aggregateFields;
|
|
151
|
+
// Apply pick
|
|
152
|
+
if (inputConfig.pick && inputConfig.pick.length > 0) {
|
|
153
|
+
fieldsToInclude = fieldsToInclude.filter(([fieldName]) => inputConfig.pick.includes(fieldName));
|
|
154
|
+
}
|
|
155
|
+
// Apply omit
|
|
156
|
+
if (inputConfig.omit && inputConfig.omit.length > 0) {
|
|
157
|
+
fieldsToInclude = fieldsToInclude.filter(([fieldName]) => !inputConfig.omit.includes(fieldName));
|
|
158
|
+
}
|
|
159
|
+
const isCreateAction = !inputConfig.identifier && !inputConfig.partial;
|
|
160
|
+
if (isCreateAction) {
|
|
161
|
+
const ownerOrParentField = childInfo ? childInfo.parentIdField : 'ownerId';
|
|
162
|
+
fieldDeclarations.push(` readonly ${ownerOrParentField}: number;`);
|
|
163
|
+
constructorParams.push(`${ownerOrParentField}: number`);
|
|
164
|
+
constructorAssignments.push(` this.${ownerOrParentField} = ${ownerOrParentField};`);
|
|
165
|
+
validationChecks.push(` if (b.${ownerOrParentField} === undefined || b.${ownerOrParentField} === null) {
|
|
166
|
+
throw new Error('${ownerOrParentField} is required');
|
|
167
|
+
}`);
|
|
168
|
+
fieldTransforms.push(` ${ownerOrParentField}: typeof b.${ownerOrParentField} === 'string' ? parseInt(b.${ownerOrParentField}, 10) : b.${ownerOrParentField} as number`);
|
|
169
|
+
}
|
|
170
|
+
// Add fields
|
|
171
|
+
fieldsToInclude.forEach(([fieldName, fieldConfig]) => {
|
|
172
|
+
if (fieldName === 'id' || fieldConfig.auto)
|
|
173
|
+
return;
|
|
174
|
+
const tsType = this.mapType(fieldConfig.type);
|
|
175
|
+
// Fields are required by default unless partial or required: false
|
|
176
|
+
const isRequired = !inputConfig.partial && fieldConfig.required !== false;
|
|
177
|
+
const optional = isRequired ? '' : '?';
|
|
178
|
+
fieldDeclarations.push(` readonly ${fieldName}${optional}: ${tsType};`);
|
|
179
|
+
constructorParams.push(`${fieldName}${optional}: ${tsType}`);
|
|
180
|
+
constructorAssignments.push(` this.${fieldName} = ${fieldName};`);
|
|
181
|
+
validationChecks.push(...this.getValidationCode(fieldName, fieldConfig.type, isRequired));
|
|
182
|
+
fieldTransforms.push(` ${fieldName}: ${this.getTransformCode(fieldName, fieldConfig.type)}`);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// Handle filters
|
|
186
|
+
if (inputConfig.filters) {
|
|
187
|
+
Object.entries(inputConfig.filters).forEach(([filterName, filterConfig]) => {
|
|
188
|
+
const tsType = this.mapType(filterConfig.type);
|
|
189
|
+
const isRequired = !filterConfig.optional;
|
|
190
|
+
const optional = isRequired ? '' : '?';
|
|
191
|
+
fieldDeclarations.push(` readonly ${filterName}${optional}: ${tsType};`);
|
|
192
|
+
constructorParams.push(`${filterName}${optional}: ${tsType}`);
|
|
193
|
+
constructorAssignments.push(` this.${filterName} = ${filterName};`);
|
|
194
|
+
if (isRequired) {
|
|
195
|
+
validationChecks.push(...this.getValidationCode(filterName, filterConfig.type, true));
|
|
196
|
+
}
|
|
197
|
+
fieldTransforms.push(` ${filterName}: ${this.getTransformCode(filterName, filterConfig.type)}`);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// Handle sorting
|
|
201
|
+
if (inputConfig.sorting) {
|
|
202
|
+
fieldDeclarations.push(` readonly sortBy?: string;`);
|
|
203
|
+
fieldDeclarations.push(` readonly sortOrder?: 'asc' | 'desc';`);
|
|
204
|
+
constructorParams.push(`sortBy?: string`);
|
|
205
|
+
constructorParams.push(`sortOrder?: 'asc' | 'desc'`);
|
|
206
|
+
constructorAssignments.push(` this.sortBy = sortBy;`);
|
|
207
|
+
constructorAssignments.push(` this.sortOrder = sortOrder;`);
|
|
208
|
+
const allowedFields = inputConfig.sorting.allow.map(f => `'${f}'`).join(', ');
|
|
209
|
+
const defaultField = ((_c = inputConfig.sorting.default) === null || _c === void 0 ? void 0 : _c.field) || inputConfig.sorting.allow[0];
|
|
210
|
+
const defaultOrder = ((_d = inputConfig.sorting.default) === null || _d === void 0 ? void 0 : _d.order) || 'asc';
|
|
211
|
+
fieldTransforms.push(` sortBy: [${allowedFields}].includes(b.sortBy as string) ? b.sortBy as string : '${defaultField}'`);
|
|
212
|
+
fieldTransforms.push(` sortOrder: b.sortOrder === 'desc' ? 'desc' : '${defaultOrder}'`);
|
|
213
|
+
}
|
|
214
|
+
const fieldsStr = fieldDeclarations.length > 0 ? fieldDeclarations.join('\n') : ' // No fields';
|
|
215
|
+
const paramsStr = constructorParams.join(', ');
|
|
216
|
+
const assignmentsStr = constructorAssignments.join('\n');
|
|
217
|
+
const validationsStr = validationChecks.length > 0 ? '\n' + validationChecks.join('\n') + '\n' : '';
|
|
218
|
+
const transformsStr = fieldTransforms.join(',\n');
|
|
219
|
+
return `export class ${className} {
|
|
220
|
+
${fieldsStr}
|
|
221
|
+
|
|
222
|
+
private constructor(data: { ${constructorParams.map(p => p.replace('readonly ', '')).join('; ')} }) {
|
|
223
|
+
${constructorAssignments.map(a => a.replace('this.', 'this.').replace(' = ', ' = data.')).join('\n').replace(/= data\./g, '= data.').split('\n').map(line => {
|
|
224
|
+
const match = line.match(/this\.(\w+) = data\.(\w+)/);
|
|
225
|
+
if (match) {
|
|
226
|
+
return ` this.${match[1]} = data.${match[1]};`;
|
|
227
|
+
}
|
|
228
|
+
return line;
|
|
229
|
+
}).join('\n')}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
static parse(body: unknown): ${className} {
|
|
233
|
+
if (!body || typeof body !== 'object') {
|
|
234
|
+
throw new Error('Invalid request body');
|
|
235
|
+
}
|
|
236
|
+
const b = body as Record<string, unknown>;
|
|
237
|
+
${validationsStr}
|
|
238
|
+
return new ${className}({
|
|
239
|
+
${transformsStr}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}`;
|
|
243
|
+
}
|
|
244
|
+
generateOutputDto(modelName, actionName, outputConfig, aggregateConfig, allAggregates) {
|
|
245
|
+
const className = `${modelName}${(0, typeUtils_1.capitalize)(actionName)}Output`;
|
|
246
|
+
if (outputConfig === 'void') {
|
|
247
|
+
return `export type ${className} = void;`;
|
|
248
|
+
}
|
|
249
|
+
const fieldDeclarations = [];
|
|
250
|
+
const constructorParams = [];
|
|
251
|
+
const fromMappings = [];
|
|
252
|
+
// Always include id for entity outputs
|
|
253
|
+
if (outputConfig.from) {
|
|
254
|
+
fieldDeclarations.push(` readonly id: number;`);
|
|
255
|
+
constructorParams.push(`id: number`);
|
|
256
|
+
fromMappings.push(` id: entity.id`);
|
|
257
|
+
}
|
|
258
|
+
// Handle fields from aggregate (pick)
|
|
259
|
+
if (outputConfig.from) {
|
|
260
|
+
const aggregateFields = Object.entries(aggregateConfig.fields);
|
|
261
|
+
let fieldsToInclude = aggregateFields;
|
|
262
|
+
// Apply pick
|
|
263
|
+
if (outputConfig.pick && outputConfig.pick.length > 0) {
|
|
264
|
+
fieldsToInclude = fieldsToInclude.filter(([fieldName]) => outputConfig.pick.includes(fieldName));
|
|
265
|
+
}
|
|
266
|
+
// Add fields
|
|
267
|
+
fieldsToInclude.forEach(([fieldName, fieldConfig]) => {
|
|
268
|
+
if (fieldName === 'id')
|
|
269
|
+
return;
|
|
270
|
+
const tsType = this.mapType(fieldConfig.type);
|
|
271
|
+
const isOptional = fieldConfig.required === false;
|
|
272
|
+
const optional = isOptional ? '?' : '';
|
|
273
|
+
fieldDeclarations.push(` readonly ${fieldName}${optional}: ${tsType};`);
|
|
274
|
+
constructorParams.push(`${fieldName}${optional}: ${tsType}`);
|
|
275
|
+
fromMappings.push(` ${fieldName}: entity.${fieldName}`);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
// Handle includes (nested objects)
|
|
279
|
+
if (outputConfig.include) {
|
|
280
|
+
Object.entries(outputConfig.include).forEach(([includeName, includeConfig]) => {
|
|
281
|
+
const relatedAggregate = allAggregates.get(includeConfig.from);
|
|
282
|
+
if (relatedAggregate && includeConfig.pick && includeConfig.pick.length > 0) {
|
|
283
|
+
// Filter out 'id' from picked fields since we add it separately
|
|
284
|
+
const pickedFieldsFiltered = includeConfig.pick.filter(f => f !== 'id');
|
|
285
|
+
const pickedFields = pickedFieldsFiltered.map(fieldName => {
|
|
286
|
+
const fieldConfig = relatedAggregate.fields[fieldName];
|
|
287
|
+
if (fieldConfig) {
|
|
288
|
+
const tsType = this.mapType(fieldConfig.type);
|
|
289
|
+
return `${fieldName}: ${tsType}`;
|
|
290
|
+
}
|
|
291
|
+
return `${fieldName}: any`;
|
|
292
|
+
}).join('; ');
|
|
293
|
+
const fieldsStr = pickedFields ? `id: number; ${pickedFields}` : 'id: number';
|
|
294
|
+
fieldDeclarations.push(` readonly ${includeName}?: { ${fieldsStr} };`);
|
|
295
|
+
constructorParams.push(`${includeName}?: { ${fieldsStr} }`);
|
|
296
|
+
fromMappings.push(` ${includeName}: (entity as any).${includeName}`);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
fieldDeclarations.push(` readonly ${includeName}?: ${includeConfig.from};`);
|
|
300
|
+
constructorParams.push(`${includeName}?: ${includeConfig.from}`);
|
|
301
|
+
fromMappings.push(` ${includeName}: (entity as any).${includeName}`);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
// Handle additional fields
|
|
306
|
+
if (outputConfig.add) {
|
|
307
|
+
Object.entries(outputConfig.add).forEach(([fieldName, fieldDef]) => {
|
|
308
|
+
const tsType = this.mapType(fieldDef.type);
|
|
309
|
+
fieldDeclarations.push(` readonly ${fieldName}: ${tsType};`);
|
|
310
|
+
constructorParams.push(`${fieldName}: ${tsType}`);
|
|
311
|
+
fromMappings.push(` ${fieldName}: (entity as any).${fieldName}`);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const fieldsStr = fieldDeclarations.length > 0 ? fieldDeclarations.join('\n') : ' // No fields';
|
|
315
|
+
const paramsStr = constructorParams.map(p => p.replace('readonly ', '')).join('; ');
|
|
316
|
+
const mappingsStr = fromMappings.join(',\n');
|
|
317
|
+
// Wrap in pagination if needed
|
|
318
|
+
if (outputConfig.pagination) {
|
|
319
|
+
return `export class ${className}Item {
|
|
320
|
+
${fieldsStr}
|
|
321
|
+
|
|
322
|
+
private constructor(data: { ${paramsStr} }) {
|
|
323
|
+
${fieldDeclarations.map(d => {
|
|
324
|
+
const match = d.match(/readonly (\w+)/);
|
|
325
|
+
if (match)
|
|
326
|
+
return ` this.${match[1]} = data.${match[1]};`;
|
|
327
|
+
return '';
|
|
328
|
+
}).filter(Boolean).join('\n')}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
static from(entity: ${modelName}): ${className}Item {
|
|
332
|
+
return new ${className}Item({
|
|
333
|
+
${mappingsStr}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export class ${className} {
|
|
339
|
+
readonly items: ${className}Item[];
|
|
340
|
+
readonly total: number;
|
|
341
|
+
readonly page?: number;
|
|
342
|
+
readonly limit?: number;
|
|
343
|
+
|
|
344
|
+
private constructor(data: { items: ${className}Item[]; total: number; page?: number; limit?: number }) {
|
|
345
|
+
this.items = data.items;
|
|
346
|
+
this.total = data.total;
|
|
347
|
+
this.page = data.page;
|
|
348
|
+
this.limit = data.limit;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
static from(data: { items: ${modelName}[]; total: number; page?: number; limit?: number }): ${className} {
|
|
352
|
+
return new ${className}({
|
|
353
|
+
items: data.items.map(item => ${className}Item.from(item)),
|
|
354
|
+
total: data.total,
|
|
355
|
+
page: data.page,
|
|
356
|
+
limit: data.limit
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}`;
|
|
360
|
+
}
|
|
361
|
+
// List action without pagination: still wrap items in a list container
|
|
362
|
+
if (actionName === 'list') {
|
|
363
|
+
return `export class ${className}Item {
|
|
364
|
+
${fieldsStr}
|
|
365
|
+
|
|
366
|
+
private constructor(data: { ${paramsStr} }) {
|
|
367
|
+
${fieldDeclarations.map(d => {
|
|
368
|
+
const match = d.match(/readonly (\w+)/);
|
|
369
|
+
if (match)
|
|
370
|
+
return ` this.${match[1]} = data.${match[1]};`;
|
|
371
|
+
return '';
|
|
372
|
+
}).filter(Boolean).join('\n')}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
static from(entity: ${modelName}): ${className}Item {
|
|
376
|
+
return new ${className}Item({
|
|
377
|
+
${mappingsStr}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export class ${className} {
|
|
383
|
+
readonly items: ${className}Item[];
|
|
384
|
+
|
|
385
|
+
private constructor(data: { items: ${className}Item[] }) {
|
|
386
|
+
this.items = data.items;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
static from(data: { items: ${modelName}[] }): ${className} {
|
|
390
|
+
return new ${className}({
|
|
391
|
+
items: data.items.map(item => ${className}Item.from(item))
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}`;
|
|
395
|
+
}
|
|
396
|
+
return `export class ${className} {
|
|
397
|
+
${fieldsStr}
|
|
398
|
+
|
|
399
|
+
private constructor(data: { ${paramsStr} }) {
|
|
400
|
+
${fieldDeclarations.map(d => {
|
|
401
|
+
const match = d.match(/readonly (\w+)/);
|
|
402
|
+
if (match)
|
|
403
|
+
return ` this.${match[1]} = data.${match[1]};`;
|
|
404
|
+
return '';
|
|
405
|
+
}).filter(Boolean).join('\n')}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
static from(entity: ${modelName}): ${className} {
|
|
409
|
+
return new ${className}({
|
|
410
|
+
${mappingsStr}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}`;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Collect types that need to be imported for a use case DTO.
|
|
417
|
+
*/
|
|
418
|
+
collectRequiredImports(modelName, aggregateConfig, outputConfig) {
|
|
419
|
+
const valueObjects = new Set();
|
|
420
|
+
const entities = new Set();
|
|
421
|
+
// Check aggregate fields for value object types
|
|
422
|
+
if (outputConfig !== 'void' && outputConfig.from) {
|
|
423
|
+
const aggregateFields = Object.entries(aggregateConfig.fields);
|
|
424
|
+
let fieldsToCheck = aggregateFields;
|
|
425
|
+
// Apply pick filter if specified
|
|
426
|
+
if (outputConfig.pick && outputConfig.pick.length > 0) {
|
|
427
|
+
fieldsToCheck = fieldsToCheck.filter(([fieldName]) => outputConfig.pick.includes(fieldName));
|
|
428
|
+
}
|
|
429
|
+
// Check each field for value object types
|
|
430
|
+
fieldsToCheck.forEach(([, fieldConfig]) => {
|
|
431
|
+
if (this.isValueObjectType(fieldConfig.type)) {
|
|
432
|
+
valueObjects.add((0, typeUtils_1.capitalize)(fieldConfig.type));
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
// Check include for child entities
|
|
437
|
+
if (outputConfig !== 'void' && outputConfig.include) {
|
|
438
|
+
Object.entries(outputConfig.include).forEach(([, includeConfig]) => {
|
|
439
|
+
if (includeConfig.from && includeConfig.from !== modelName) {
|
|
440
|
+
entities.add(includeConfig.from);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
return { valueObjects, entities };
|
|
445
|
+
}
|
|
446
|
+
generateFromConfig(config) {
|
|
447
|
+
const result = {};
|
|
448
|
+
// Collect all aggregates
|
|
449
|
+
if (config.domain.aggregates) {
|
|
450
|
+
Object.entries(config.domain.aggregates).forEach(([name, aggConfig]) => {
|
|
451
|
+
this.availableAggregates.set(name, aggConfig);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
// Collect all value objects
|
|
455
|
+
if (config.domain.valueObjects) {
|
|
456
|
+
Object.entries(config.domain.valueObjects).forEach(([name, voConfig]) => {
|
|
457
|
+
this.availableValueObjects.set(name, voConfig);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
const childEntityMap = (0, childEntityUtils_1.buildChildEntityMap)(config);
|
|
461
|
+
// Generate DTOs for each use case
|
|
462
|
+
Object.entries(config.useCases).forEach(([modelName, useCases]) => {
|
|
463
|
+
const aggregateConfig = this.availableAggregates.get(modelName);
|
|
464
|
+
if (!aggregateConfig) {
|
|
465
|
+
console.warn(`Warning: No aggregate found for model ${modelName}`);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const childInfo = childEntityMap.get(modelName);
|
|
469
|
+
Object.entries(useCases).forEach(([actionName, useCaseConfig]) => {
|
|
470
|
+
// Generate Input DTO
|
|
471
|
+
const inputDto = this.generateInputDto(modelName, actionName, useCaseConfig.input, aggregateConfig, childInfo);
|
|
472
|
+
// Generate Output DTO
|
|
473
|
+
const outputDto = this.generateOutputDto(modelName, actionName, useCaseConfig.output || 'void', aggregateConfig, this.availableAggregates);
|
|
474
|
+
// Collect required imports
|
|
475
|
+
const imports = [];
|
|
476
|
+
const needsModelImport = useCaseConfig.output !== 'void';
|
|
477
|
+
if (needsModelImport) {
|
|
478
|
+
imports.push(`import { ${modelName} } from '../../domain/entities/${modelName}';`);
|
|
479
|
+
}
|
|
480
|
+
// Collect value objects and entities needed
|
|
481
|
+
const { valueObjects, entities } = this.collectRequiredImports(modelName, aggregateConfig, useCaseConfig.output || 'void');
|
|
482
|
+
// Add value object imports
|
|
483
|
+
valueObjects.forEach(voName => {
|
|
484
|
+
imports.push(`import { ${voName} } from '../../domain/valueObjects/${voName}';`);
|
|
485
|
+
});
|
|
486
|
+
// Add entity imports for includes
|
|
487
|
+
entities.forEach(entityName => {
|
|
488
|
+
imports.push(`import { ${entityName} } from '../../domain/entities/${entityName}';`);
|
|
489
|
+
});
|
|
490
|
+
const importStatement = imports.length > 0 ? imports.join('\n') + '\n\n' : '';
|
|
491
|
+
const dtoFileName = `${modelName}${(0, typeUtils_1.capitalize)(actionName)}`;
|
|
492
|
+
result[dtoFileName] = `${importStatement}${inputDto}\n\n${outputDto}`;
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
return result;
|
|
496
|
+
}
|
|
497
|
+
generateFromYamlFile(yamlFilePath) {
|
|
498
|
+
const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
|
|
499
|
+
const config = (0, yaml_1.parse)(yamlContent);
|
|
500
|
+
if (!(0, configTypes_1.isValidModuleConfig)(config)) {
|
|
501
|
+
throw new Error('Configuration does not match new module format. Expected useCases structure.');
|
|
502
|
+
}
|
|
503
|
+
return this.generateFromConfig(config);
|
|
504
|
+
}
|
|
505
|
+
async generateAndSaveFiles(yamlFilePath, moduleDir, opts) {
|
|
506
|
+
const dtosByName = this.generateFromYamlFile(yamlFilePath);
|
|
507
|
+
const dtoDir = path.join(moduleDir, 'application', 'dto');
|
|
508
|
+
fs.mkdirSync(dtoDir, { recursive: true });
|
|
509
|
+
for (const [name, code] of Object.entries(dtosByName)) {
|
|
510
|
+
const filePath = path.join(dtoDir, `${name}.ts`);
|
|
511
|
+
// eslint-disable-next-line no-await-in-loop
|
|
512
|
+
await (0, generationRegistry_1.writeGeneratedFile)(filePath, code, { force: !!(opts === null || opts === void 0 ? void 0 : opts.force), skipOnConflict: !!(opts === null || opts === void 0 ? void 0 : opts.skipOnConflict) });
|
|
513
|
+
}
|
|
514
|
+
// eslint-disable-next-line no-console
|
|
515
|
+
console.log('\n' + colors_1.colors.green('DTO files generated successfully!') + '\n');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
exports.DtoGenerator = DtoGenerator;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NewModuleConfig } from '../types/configTypes';
|
|
2
|
+
export declare class NewControllerGenerator {
|
|
3
|
+
private capitalize;
|
|
4
|
+
private getHttpDecorator;
|
|
5
|
+
private parseUseCase;
|
|
6
|
+
/**
|
|
7
|
+
* Normalize auth config to an array of roles
|
|
8
|
+
*/
|
|
9
|
+
private normalizeAuth;
|
|
10
|
+
/**
|
|
11
|
+
* Check if auth config includes owner permission
|
|
12
|
+
*/
|
|
13
|
+
private hasOwnerAuth;
|
|
14
|
+
/**
|
|
15
|
+
* Generate pre-fetch authentication/authorization check code.
|
|
16
|
+
* This runs before fetching the entity and validates authentication and role-based access.
|
|
17
|
+
* @param auth - The auth requirement: 'all', 'authenticated', 'owner', role names, or array of roles
|
|
18
|
+
* @returns Code string for the auth check, or empty string if no check needed
|
|
19
|
+
*/
|
|
20
|
+
private generateAuthCheck;
|
|
21
|
+
/**
|
|
22
|
+
* Generate post-fetch authorization check for owner validation.
|
|
23
|
+
* This runs after fetching the entity and validates ownership.
|
|
24
|
+
* Used for READ operations (get, list) where we check after fetch.
|
|
25
|
+
* For child entities, uses getResourceOwner() since result has no ownerId.
|
|
26
|
+
*/
|
|
27
|
+
private generatePostFetchOwnerCheck;
|
|
28
|
+
/**
|
|
29
|
+
* Generate pre-mutation authorization check for owner validation.
|
|
30
|
+
* This runs BEFORE the mutation to prevent unauthorized changes.
|
|
31
|
+
* Used for WRITE operations (update, delete).
|
|
32
|
+
* @param auth - The auth requirement
|
|
33
|
+
* @param useCaseVar - The use case variable name
|
|
34
|
+
* @returns Code string for the pre-mutation owner check, or empty string if not needed
|
|
35
|
+
*/
|
|
36
|
+
private generatePreMutationOwnerCheck;
|
|
37
|
+
private generateApiEndpointMethod;
|
|
38
|
+
private generateWebPageMethod;
|
|
39
|
+
private generateOnSuccessHandler;
|
|
40
|
+
private generateOnErrorHandler;
|
|
41
|
+
/**
|
|
42
|
+
* Sort routes so static paths are registered before parameterized ones.
|
|
43
|
+
* This prevents parameterized routes (e.g. /:id) from catching requests
|
|
44
|
+
* meant for static routes (e.g. /create).
|
|
45
|
+
*/
|
|
46
|
+
private sortRoutesBySpecificity;
|
|
47
|
+
private generateApiController;
|
|
48
|
+
private generateWebController;
|
|
49
|
+
generateFromConfig(config: NewModuleConfig): Record<string, string>;
|
|
50
|
+
generateFromYamlFile(yamlFilePath: string): Record<string, string>;
|
|
51
|
+
generateAndSaveFiles(yamlFilePath: string, moduleDir: string, opts?: {
|
|
52
|
+
force?: boolean;
|
|
53
|
+
skipOnConflict?: boolean;
|
|
54
|
+
}): Promise<string[]>;
|
|
55
|
+
}
|