@currentjs/gen 0.3.2 → 0.5.1
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 +18 -609
- 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/generators/controllerGenerator.d.ts +50 -19
- package/dist/generators/controllerGenerator.js +588 -331
- package/dist/generators/domainLayerGenerator.d.ts +21 -0
- package/dist/generators/domainLayerGenerator.js +286 -0
- package/dist/generators/dtoGenerator.d.ts +21 -0
- package/dist/generators/dtoGenerator.js +523 -0
- package/dist/generators/serviceGenerator.d.ts +22 -51
- package/dist/generators/serviceGenerator.js +345 -568
- package/dist/generators/storeGenerator.d.ts +39 -32
- package/dist/generators/storeGenerator.js +396 -236
- 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 +16 -11
- package/dist/generators/templates/data/appYamlTemplate +5 -2
- package/dist/generators/templates/data/cursorRulesTemplate +315 -221
- package/dist/generators/templates/data/frontendScriptTemplate +56 -15
- package/dist/generators/templates/data/mainViewTemplate +2 -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/storeTemplates.d.ts +1 -5
- package/dist/generators/templates/storeTemplates.js +84 -224
- package/dist/generators/useCaseGenerator.d.ts +13 -0
- package/dist/generators/useCaseGenerator.js +191 -0
- package/dist/types/configTypes.d.ts +149 -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/typeUtils.d.ts +23 -0
- package/dist/utils/typeUtils.js +77 -0
- package/package.json +7 -3
- package/dist/generators/domainModelGenerator.d.ts +0 -41
- package/dist/generators/domainModelGenerator.js +0 -242
- package/dist/generators/templates/controllerTemplates.d.ts +0 -43
- package/dist/generators/templates/controllerTemplates.js +0 -82
- package/dist/generators/templates/serviceTemplates.d.ts +0 -16
- package/dist/generators/templates/serviceTemplates.js +0 -59
- package/dist/generators/templates/validationTemplates.d.ts +0 -25
- package/dist/generators/templates/validationTemplates.js +0 -66
- package/dist/generators/templates/viewTemplates.d.ts +0 -25
- package/dist/generators/templates/viewTemplates.js +0 -491
- package/dist/generators/validationGenerator.d.ts +0 -29
- package/dist/generators/validationGenerator.js +0 -250
|
@@ -37,295 +37,455 @@ exports.StoreGenerator = void 0;
|
|
|
37
37
|
const yaml_1 = require("yaml");
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
|
-
const storeTemplates_1 = require("./templates/storeTemplates");
|
|
41
40
|
const generationRegistry_1 = require("../utils/generationRegistry");
|
|
42
41
|
const colors_1 = require("../utils/colors");
|
|
43
|
-
const
|
|
42
|
+
const configTypes_1 = require("../types/configTypes");
|
|
43
|
+
const childEntityUtils_1 = require("../utils/childEntityUtils");
|
|
44
|
+
const storeTemplates_1 = require("./templates/storeTemplates");
|
|
45
|
+
const typeUtils_1 = require("../utils/typeUtils");
|
|
44
46
|
class StoreGenerator {
|
|
45
47
|
constructor() {
|
|
46
|
-
this.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
this.availableValueObjects = new Map();
|
|
49
|
+
this.availableAggregates = new Set();
|
|
50
|
+
}
|
|
51
|
+
isAggregateField(fieldConfig) {
|
|
52
|
+
return (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
|
|
53
|
+
}
|
|
54
|
+
isValueObjectType(fieldType) {
|
|
55
|
+
const capitalizedType = (0, typeUtils_1.capitalize)(fieldType);
|
|
56
|
+
return this.availableValueObjects.has(capitalizedType);
|
|
57
|
+
}
|
|
58
|
+
getValueObjectName(fieldType) {
|
|
59
|
+
return (0, typeUtils_1.capitalize)(fieldType);
|
|
60
|
+
}
|
|
61
|
+
getValueObjectConfig(fieldType) {
|
|
62
|
+
return this.availableValueObjects.get(this.getValueObjectName(fieldType));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Sort fields the same way as the domain layer generator:
|
|
66
|
+
* Required fields first, then optional fields.
|
|
67
|
+
* This ensures rowToModel parameter order matches entity constructor.
|
|
68
|
+
*/
|
|
69
|
+
sortFieldsForConstructor(fields) {
|
|
70
|
+
return [...fields].sort((a, b) => {
|
|
71
|
+
const aRequired = a[1].required !== false && !a[1].auto && !this.isAggregateField(a[1]);
|
|
72
|
+
const bRequired = b[1].required !== false && !b[1].auto && !this.isAggregateField(b[1]);
|
|
73
|
+
if (aRequired === bRequired)
|
|
74
|
+
return 0;
|
|
75
|
+
return aRequired ? -1 : 1;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Gets the type name for a value object field (for casting).
|
|
80
|
+
* For enum fields, returns the generated type name (e.g., BreedName).
|
|
81
|
+
* For other fields, returns the basic TypeScript type.
|
|
82
|
+
*/
|
|
83
|
+
getValueObjectFieldTypeName(voName, voConfig) {
|
|
84
|
+
const fields = Object.entries(voConfig.fields);
|
|
85
|
+
// Look for enum fields - they get generated type names
|
|
86
|
+
for (const [fieldName, fieldConfig] of fields) {
|
|
87
|
+
if (typeof fieldConfig === 'object' && 'values' in fieldConfig) {
|
|
88
|
+
// This is an enum field, return the generated type name
|
|
89
|
+
return `${voName}${(0, typeUtils_1.capitalize)(fieldName)}`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
56
93
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
94
|
+
mapTypeToRowType(yamlType) {
|
|
95
|
+
return (0, typeUtils_1.mapRowType)(yamlType, this.availableValueObjects);
|
|
96
|
+
}
|
|
97
|
+
/** Single line for serializing a value object field to DB (insert/update). */
|
|
98
|
+
generateValueObjectSerialization(fieldName, voName, voConfig) {
|
|
99
|
+
const voFields = Object.keys(voConfig.fields);
|
|
100
|
+
if (voFields.length > 1) {
|
|
101
|
+
return ` ${fieldName}: entity.${fieldName} ? JSON.stringify(entity.${fieldName}) : undefined`;
|
|
61
102
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return 'number'; // Foreign keys are numbers
|
|
103
|
+
if (voFields.length === 1) {
|
|
104
|
+
return ` ${fieldName}: entity.${fieldName}?.${voFields[0]}`;
|
|
65
105
|
}
|
|
66
|
-
return
|
|
106
|
+
return ` ${fieldName}: entity.${fieldName} ? JSON.stringify(entity.${fieldName}) : undefined`;
|
|
67
107
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
108
|
+
/** Single line for deserializing a value object from row (rowToModel). */
|
|
109
|
+
generateValueObjectDeserialization(fieldName, voName, voConfig) {
|
|
110
|
+
const voFields = Object.keys(voConfig.fields);
|
|
111
|
+
if (voFields.length > 1) {
|
|
112
|
+
const voArgs = voFields.map(f => `parsed.${f}`).join(', ');
|
|
113
|
+
return ` row.${fieldName} ? (() => { const parsed = JSON.parse(row.${fieldName}); return new ${voName}(${voArgs}); })() : undefined`;
|
|
114
|
+
}
|
|
115
|
+
if (voFields.length === 1) {
|
|
116
|
+
const singleFieldType = voConfig.fields[voFields[0]];
|
|
117
|
+
const hasEnumValues = typeof singleFieldType === 'object' && 'values' in singleFieldType;
|
|
118
|
+
if (hasEnumValues) {
|
|
119
|
+
const enumTypeName = this.getValueObjectFieldTypeName(voName, voConfig);
|
|
120
|
+
return ` row.${fieldName} ? new ${voName}(row.${fieldName} as ${enumTypeName}) : undefined`;
|
|
121
|
+
}
|
|
122
|
+
return ` row.${fieldName} ? new ${voName}(row.${fieldName}) : undefined`;
|
|
123
|
+
}
|
|
124
|
+
return ` row.${fieldName} ? new ${voName}(...Object.values(JSON.parse(row.${fieldName}))) : undefined`;
|
|
125
|
+
}
|
|
126
|
+
/** Single line for datetime conversion: toDate (row->model) or toMySQL (entity->row). */
|
|
127
|
+
generateDatetimeConversion(fieldName, direction) {
|
|
128
|
+
if (direction === 'toDate') {
|
|
129
|
+
return ` row.${fieldName} ? new Date(row.${fieldName}) : undefined`;
|
|
130
|
+
}
|
|
131
|
+
return ` ${fieldName}: entity.${fieldName} ? this.toMySQLDatetime(entity.${fieldName}) : undefined`;
|
|
132
|
+
}
|
|
133
|
+
replaceTemplateVars(template, variables) {
|
|
134
|
+
let result = template;
|
|
135
|
+
Object.entries(variables).forEach(([key, value]) => {
|
|
136
|
+
const regex = new RegExp(`{{${key}}}`, 'g');
|
|
137
|
+
result = result.replace(regex, value);
|
|
72
138
|
});
|
|
139
|
+
return result;
|
|
73
140
|
}
|
|
74
|
-
|
|
75
|
-
|
|
141
|
+
generateRowFields(fields, childInfo) {
|
|
142
|
+
const result = [];
|
|
143
|
+
const ownerOrParentField = childInfo ? childInfo.parentIdField : 'ownerId';
|
|
144
|
+
result.push(` ${ownerOrParentField}: number;`);
|
|
145
|
+
fields.forEach(([fieldName, fieldConfig]) => {
|
|
146
|
+
if (this.isAggregateField(fieldConfig)) {
|
|
147
|
+
result.push(` ${fieldName}_id?: number;`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const tsType = this.mapTypeToRowType(fieldConfig.type);
|
|
151
|
+
const isOptional = !fieldConfig.required;
|
|
152
|
+
result.push(` ${fieldName}${isOptional ? '?' : ''}: ${tsType};`);
|
|
153
|
+
});
|
|
154
|
+
return result.join('\n');
|
|
76
155
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
156
|
+
generateFieldNamesStr(fields, childInfo) {
|
|
157
|
+
const fieldNames = ['id'];
|
|
158
|
+
fieldNames.push(childInfo ? childInfo.parentIdField : 'ownerId');
|
|
159
|
+
fieldNames.push(...fields.map(([name, config]) => this.isAggregateField(config) ? `${name}_id` : name));
|
|
160
|
+
return fieldNames.map(f => `\\\`${f}\\\``).join(', ');
|
|
80
161
|
}
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
162
|
+
generateRowToModelMapping(modelName, fields, childInfo) {
|
|
163
|
+
const result = [];
|
|
164
|
+
const ownerOrParentField = childInfo ? childInfo.parentIdField : 'ownerId';
|
|
165
|
+
result.push(` row.${ownerOrParentField}`);
|
|
166
|
+
fields.forEach(([fieldName, fieldConfig]) => {
|
|
167
|
+
const fieldType = fieldConfig.type;
|
|
168
|
+
// Handle enum type - cast string to the generated union type
|
|
169
|
+
if (fieldType === 'enum' && fieldConfig.values && fieldConfig.values.length > 0) {
|
|
170
|
+
const enumTypeName = `${modelName}${(0, typeUtils_1.capitalize)(fieldName)}`;
|
|
171
|
+
result.push(` row.${fieldName} as ${enumTypeName}`);
|
|
85
172
|
return;
|
|
86
173
|
}
|
|
87
|
-
//
|
|
88
|
-
if (this.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const isOptional = !field.required && !field.auto;
|
|
92
|
-
const fieldDef = ` ${foreignKeyName}${isOptional ? '?' : ''}: ${tsType};`;
|
|
93
|
-
fields.push(fieldDef);
|
|
174
|
+
// Handle aggregate reference - create stub from FK
|
|
175
|
+
if (this.isAggregateField(fieldConfig)) {
|
|
176
|
+
result.push(` row.${fieldName}_id != null ? ({ id: row.${fieldName}_id } as unknown as ${fieldType}) : undefined`);
|
|
177
|
+
return;
|
|
94
178
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
fields.push(fieldDef);
|
|
179
|
+
// Handle datetime/date conversion
|
|
180
|
+
if (fieldType === 'datetime' || fieldType === 'date') {
|
|
181
|
+
result.push(this.generateDatetimeConversion(fieldName, 'toDate'));
|
|
182
|
+
return;
|
|
100
183
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const updatableFields = modelConfig.fields
|
|
118
|
-
.filter(field => field.name !== 'id' && field.name !== 'createdAt')
|
|
119
|
-
.map(field => `'${field.name}'`);
|
|
120
|
-
return updatableFields.join(', ');
|
|
121
|
-
}
|
|
122
|
-
sortFieldsByRequired(fields) {
|
|
123
|
-
// Sort fields: required fields first, then optional fields
|
|
124
|
-
// This must match the order used in domainModelGenerator
|
|
125
|
-
return [...fields].sort((a, b) => {
|
|
126
|
-
const aRequired = a.required !== false && !a.auto;
|
|
127
|
-
const bRequired = b.required !== false && !b.auto;
|
|
128
|
-
if (aRequired === bRequired) {
|
|
129
|
-
return 0; // Keep original order if both have same required status
|
|
184
|
+
// Handle boolean conversion
|
|
185
|
+
if (fieldType === 'boolean') {
|
|
186
|
+
result.push(` Boolean(row.${fieldName})`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Handle value object conversion - deserialize from JSON
|
|
190
|
+
if (this.isValueObjectType(fieldType)) {
|
|
191
|
+
const voName = this.getValueObjectName(fieldType);
|
|
192
|
+
const voConfig = this.getValueObjectConfig(fieldType);
|
|
193
|
+
if (voConfig) {
|
|
194
|
+
result.push(this.generateValueObjectDeserialization(fieldName, voName, voConfig));
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
result.push(` row.${fieldName} ? new ${voName}(...Object.values(JSON.parse(row.${fieldName}))) : undefined`);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
130
200
|
}
|
|
131
|
-
|
|
201
|
+
result.push(` row.${fieldName}`);
|
|
132
202
|
});
|
|
203
|
+
return result.join(',\n');
|
|
133
204
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
205
|
+
generateInsertDataMapping(fields, childInfo) {
|
|
206
|
+
const result = [];
|
|
207
|
+
const ownerOrParentField = childInfo ? childInfo.parentIdField : 'ownerId';
|
|
208
|
+
result.push(` ${ownerOrParentField}: entity.${ownerOrParentField}`);
|
|
209
|
+
fields.forEach(([fieldName, fieldConfig]) => {
|
|
210
|
+
const fieldType = fieldConfig.type;
|
|
211
|
+
// Handle aggregate reference - extract FK id
|
|
212
|
+
if (this.isAggregateField(fieldConfig)) {
|
|
213
|
+
result.push(` ${fieldName}_id: entity.${fieldName}?.id`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// Handle datetime/date - convert Date to MySQL DATETIME format
|
|
217
|
+
if (fieldType === 'datetime' || fieldType === 'date') {
|
|
218
|
+
result.push(this.generateDatetimeConversion(fieldName, 'toMySQL'));
|
|
219
|
+
return;
|
|
140
220
|
}
|
|
141
|
-
//
|
|
142
|
-
if (this.
|
|
143
|
-
|
|
221
|
+
// Handle value object - serialize to JSON
|
|
222
|
+
if (this.isValueObjectType(fieldType)) {
|
|
223
|
+
const voConfig = this.getValueObjectConfig(fieldType);
|
|
224
|
+
if (voConfig) {
|
|
225
|
+
result.push(this.generateValueObjectSerialization(fieldName, this.getValueObjectName(fieldType), voConfig));
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
result.push(` ${fieldName}: entity.${fieldName} ? JSON.stringify(entity.${fieldName}) : undefined`);
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
144
231
|
}
|
|
145
|
-
|
|
232
|
+
result.push(` ${fieldName}: entity.${fieldName}`);
|
|
146
233
|
});
|
|
147
|
-
return
|
|
234
|
+
return result.join(',\n');
|
|
148
235
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
236
|
+
generateUpdateDataMapping(fields) {
|
|
237
|
+
return fields
|
|
238
|
+
.map(([fieldName, fieldConfig]) => {
|
|
239
|
+
const fieldType = fieldConfig.type;
|
|
240
|
+
if (this.isAggregateField(fieldConfig)) {
|
|
241
|
+
return ` ${fieldName}_id: entity.${fieldName}?.id`;
|
|
153
242
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const foreignKeyName = this.getForeignKeyFieldName(field);
|
|
157
|
-
return ` ${foreignKeyName}: model.${field.name}?.id`;
|
|
243
|
+
if (fieldType === 'datetime' || fieldType === 'date') {
|
|
244
|
+
return this.generateDatetimeConversion(fieldName, 'toMySQL');
|
|
158
245
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return result;
|
|
246
|
+
if (this.isValueObjectType(fieldType)) {
|
|
247
|
+
const voConfig = this.getValueObjectConfig(fieldType);
|
|
248
|
+
if (voConfig) {
|
|
249
|
+
return this.generateValueObjectSerialization(fieldName, this.getValueObjectName(fieldType), voConfig);
|
|
250
|
+
}
|
|
251
|
+
return ` ${fieldName}: entity.${fieldName} ? JSON.stringify(entity.${fieldName}) : undefined`;
|
|
252
|
+
}
|
|
253
|
+
return ` ${fieldName}: entity.${fieldName}`;
|
|
254
|
+
})
|
|
255
|
+
.join(',\n');
|
|
170
256
|
}
|
|
171
|
-
|
|
172
|
-
return
|
|
257
|
+
generateUpdateFieldsArray(fields) {
|
|
258
|
+
return JSON.stringify(fields.map(([name, config]) => this.isAggregateField(config) ? `${name}_id` : name));
|
|
173
259
|
}
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
260
|
+
generateValueObjectImports(fields) {
|
|
261
|
+
const imports = [];
|
|
262
|
+
fields.forEach(([, fieldConfig]) => {
|
|
263
|
+
if (this.isValueObjectType(fieldConfig.type)) {
|
|
264
|
+
const voName = this.getValueObjectName(fieldConfig.type);
|
|
265
|
+
const voConfig = this.getValueObjectConfig(fieldConfig.type);
|
|
266
|
+
// Import the class
|
|
267
|
+
const importItems = [voName];
|
|
268
|
+
// Also import the type if it's an enum
|
|
269
|
+
if (voConfig) {
|
|
270
|
+
const enumTypeName = this.getValueObjectFieldTypeName(voName, voConfig);
|
|
271
|
+
if (enumTypeName) {
|
|
272
|
+
importItems.push(enumTypeName);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
imports.push(`import { ${importItems.join(', ')} } from '../../domain/valueObjects/${voName}';`);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
// Dedupe imports
|
|
279
|
+
const uniqueImports = [...new Set(imports)];
|
|
280
|
+
if (uniqueImports.length === 0) {
|
|
177
281
|
return '';
|
|
178
282
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const ${field.name} = await this.${relatedModelLower}Store.getById(row.${foreignKeyName});
|
|
188
|
-
if (${field.name}) {
|
|
189
|
-
entity.set${field.name.charAt(0).toUpperCase() + field.name.slice(1)}(${field.name});
|
|
190
|
-
}
|
|
191
|
-
}`;
|
|
192
|
-
}).join('\n');
|
|
193
|
-
methods.push(`
|
|
194
|
-
async loadRelationships(entity: ${entityName}, row: ${entityName}Row): Promise<${entityName}> {
|
|
195
|
-
${loadCalls}
|
|
196
|
-
return entity;
|
|
197
|
-
}`);
|
|
198
|
-
// Generate getByIdWithRelationships method
|
|
199
|
-
methods.push(`
|
|
200
|
-
async getByIdWithRelationships(id: number): Promise<${entityName} | null> {
|
|
201
|
-
try {
|
|
202
|
-
const query = 'SELECT * FROM ${entityName.toLowerCase()}s WHERE id = :id AND deleted_at IS NULL';
|
|
203
|
-
const result = await this.db.query(query, { id });
|
|
204
|
-
|
|
205
|
-
if (!result.success || result.data.length === 0) {
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const row = result.data[0] as ${entityName}Row;
|
|
210
|
-
const entity = ${entityName}Store.rowToModel(row);
|
|
211
|
-
return await this.loadRelationships(entity, row);
|
|
212
|
-
} catch (error) {
|
|
213
|
-
if (error instanceof MySQLConnectionError) {
|
|
214
|
-
throw new Error(\`Database connection error while fetching ${entityName} with id \${id}: \${error.message}\`);
|
|
215
|
-
} else if (error instanceof MySQLQueryError) {
|
|
216
|
-
throw new Error(\`Query error while fetching ${entityName} with id \${id}: \${error.message}\`);
|
|
217
|
-
}
|
|
218
|
-
throw error;
|
|
219
|
-
}
|
|
220
|
-
}`);
|
|
221
|
-
return methods.join('\n');
|
|
222
|
-
}
|
|
223
|
-
generateStoreConstructorParams(modelConfig) {
|
|
224
|
-
const relationshipFields = modelConfig.fields.filter(f => this.isRelationshipField(f));
|
|
225
|
-
const params = ['private db: ISqlProvider'];
|
|
226
|
-
relationshipFields.forEach(field => {
|
|
227
|
-
const relatedModel = field.type;
|
|
228
|
-
const relatedModelLower = relatedModel.toLowerCase();
|
|
229
|
-
params.push(`private ${relatedModelLower}Store: ${relatedModel}Store`);
|
|
283
|
+
return '\n' + uniqueImports.join('\n');
|
|
284
|
+
}
|
|
285
|
+
generateAggregateRefImports(modelName, fields) {
|
|
286
|
+
const imports = [];
|
|
287
|
+
fields.forEach(([, fieldConfig]) => {
|
|
288
|
+
if (this.isAggregateField(fieldConfig) && fieldConfig.type !== modelName) {
|
|
289
|
+
imports.push(`import { ${fieldConfig.type} } from '../../domain/entities/${fieldConfig.type}';`);
|
|
290
|
+
}
|
|
230
291
|
});
|
|
231
|
-
|
|
292
|
+
const uniqueImports = [...new Set(imports)];
|
|
293
|
+
if (uniqueImports.length === 0)
|
|
294
|
+
return '';
|
|
295
|
+
return '\n' + uniqueImports.join('\n');
|
|
296
|
+
}
|
|
297
|
+
generateListMethods(modelName, fieldNamesStr, childInfo) {
|
|
298
|
+
const isRoot = !childInfo;
|
|
299
|
+
const ownerParam = isRoot ? ', ownerId?: number' : '';
|
|
300
|
+
const ownerFilter = isRoot
|
|
301
|
+
? `\n const ownerFilter = ownerId != null ? ' AND \\\`ownerId\\\` = :ownerId' : '';`
|
|
302
|
+
: '';
|
|
303
|
+
const ownerFilterRef = isRoot ? '\${ownerFilter}' : '';
|
|
304
|
+
const ownerParamsSetup = isRoot
|
|
305
|
+
? `\n if (ownerId != null) params.ownerId = ownerId;`
|
|
306
|
+
: '';
|
|
307
|
+
const getPaginated = ` async getPaginated(page: number = 1, limit: number = 20${ownerParam}): Promise<${modelName}[]> {
|
|
308
|
+
const offset = (page - 1) * limit;${ownerFilter}
|
|
309
|
+
const params: Record<string, any> = { limit: String(limit), offset: String(offset) };${ownerParamsSetup}
|
|
310
|
+
const result = await this.db.query(
|
|
311
|
+
\`SELECT ${fieldNamesStr} FROM \\\`\${this.tableName}\\\` WHERE deleted_at IS NULL${ownerFilterRef} LIMIT :limit OFFSET :offset\`,
|
|
312
|
+
params
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
if (result.success && result.data) {
|
|
316
|
+
return result.data.map((row: ${modelName}Row) => this.rowToModel(row));
|
|
317
|
+
}
|
|
318
|
+
return [];
|
|
319
|
+
}`;
|
|
320
|
+
const getAll = ` async getAll(${isRoot ? 'ownerId?: number' : ''}): Promise<${modelName}[]> {${ownerFilter}
|
|
321
|
+
const params: Record<string, any> = {};${ownerParamsSetup}
|
|
322
|
+
const result = await this.db.query(
|
|
323
|
+
\`SELECT ${fieldNamesStr} FROM \\\`\${this.tableName}\\\` WHERE deleted_at IS NULL${ownerFilterRef}\`,
|
|
324
|
+
params
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
if (result.success && result.data) {
|
|
328
|
+
return result.data.map((row: ${modelName}Row) => this.rowToModel(row));
|
|
329
|
+
}
|
|
330
|
+
return [];
|
|
331
|
+
}`;
|
|
332
|
+
const count = ` async count(${isRoot ? 'ownerId?: number' : ''}): Promise<number> {${ownerFilter}
|
|
333
|
+
const params: Record<string, any> = {};${ownerParamsSetup}
|
|
334
|
+
const result = await this.db.query(
|
|
335
|
+
\`SELECT COUNT(*) as count FROM \\\`\${this.tableName}\\\` WHERE deleted_at IS NULL${ownerFilterRef}\`,
|
|
336
|
+
params
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
if (result.success && result.data && result.data.length > 0) {
|
|
340
|
+
return parseInt(result.data[0].count, 10);
|
|
232
341
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
342
|
+
return 0;
|
|
343
|
+
}`;
|
|
344
|
+
return `${getPaginated}\n\n${getAll}\n\n${count}`;
|
|
345
|
+
}
|
|
346
|
+
generateGetByParentIdMethod(modelName, fields, childInfo) {
|
|
347
|
+
if (!childInfo)
|
|
236
348
|
return '';
|
|
349
|
+
const fieldList = ['id', childInfo.parentIdField, ...fields.map(([name, config]) => this.isAggregateField(config) ? `${name}_id` : name)].map(f => '\\`' + f + '\\`').join(', ');
|
|
350
|
+
const parentIdField = childInfo.parentIdField;
|
|
351
|
+
return `
|
|
352
|
+
|
|
353
|
+
async getByParentId(parentId: number): Promise<${modelName}[]> {
|
|
354
|
+
const result = await this.db.query(
|
|
355
|
+
\`SELECT ${fieldList} FROM \\\`\${this.tableName}\\\` WHERE \\\`${parentIdField}\\\` = :parentId AND deleted_at IS NULL\`,
|
|
356
|
+
{ parentId }
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
if (result.success && result.data) {
|
|
360
|
+
return result.data.map((row: ${modelName}Row) => this.rowToModel(row));
|
|
361
|
+
}
|
|
362
|
+
return [];
|
|
363
|
+
}`;
|
|
364
|
+
}
|
|
365
|
+
generateGetResourceOwnerMethod(childInfo) {
|
|
366
|
+
if (childInfo) {
|
|
367
|
+
const parentTable = childInfo.parentTableName;
|
|
368
|
+
const parentIdField = childInfo.parentIdField;
|
|
369
|
+
return `
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Get the owner ID of a resource by its ID (via parent entity).
|
|
373
|
+
* Used for pre-mutation authorization checks.
|
|
374
|
+
*/
|
|
375
|
+
async getResourceOwner(id: number): Promise<number | null> {
|
|
376
|
+
const result = await this.db.query(
|
|
377
|
+
\`SELECT p.ownerId FROM \\\`\${this.tableName}\\\` c INNER JOIN \\\`${parentTable}\\\` p ON p.id = c.\\\`${parentIdField}\\\` WHERE c.id = :id AND c.deleted_at IS NULL\`,
|
|
378
|
+
{ id }
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
if (result.success && result.data && result.data.length > 0) {
|
|
382
|
+
return result.data[0].ownerId as number;
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}`;
|
|
237
386
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
387
|
+
return `
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Get the owner ID of a resource by its ID.
|
|
391
|
+
* Used for pre-mutation authorization checks.
|
|
392
|
+
*/
|
|
393
|
+
async getResourceOwner(id: number): Promise<number | null> {
|
|
394
|
+
const result = await this.db.query(
|
|
395
|
+
\`SELECT ownerId FROM \\\`\${this.tableName}\\\` WHERE id = :id AND deleted_at IS NULL\`,
|
|
396
|
+
{ id }
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
if (result.success && result.data && result.data.length > 0) {
|
|
400
|
+
return result.data[0].ownerId as number;
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
}`;
|
|
243
404
|
}
|
|
244
|
-
generateStore(
|
|
245
|
-
const
|
|
246
|
-
const
|
|
405
|
+
generateStore(modelName, aggregateConfig, childInfo) {
|
|
406
|
+
const tableName = modelName.toLowerCase();
|
|
407
|
+
const fields = Object.entries(aggregateConfig.fields);
|
|
408
|
+
// Sort fields for rowToModel to match entity constructor order (required first, optional second)
|
|
409
|
+
const sortedFields = this.sortFieldsForConstructor(fields);
|
|
410
|
+
const fieldNamesStr = this.generateFieldNamesStr(fields, childInfo);
|
|
247
411
|
const variables = {
|
|
248
|
-
ENTITY_NAME:
|
|
412
|
+
ENTITY_NAME: modelName,
|
|
249
413
|
TABLE_NAME: tableName,
|
|
250
|
-
ROW_FIELDS: this.generateRowFields(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
414
|
+
ROW_FIELDS: this.generateRowFields(fields, childInfo),
|
|
415
|
+
FIELD_NAMES: fieldNamesStr,
|
|
416
|
+
ROW_TO_MODEL_MAPPING: this.generateRowToModelMapping(modelName, sortedFields, childInfo),
|
|
417
|
+
INSERT_DATA_MAPPING: this.generateInsertDataMapping(fields, childInfo),
|
|
418
|
+
UPDATE_DATA_MAPPING: this.generateUpdateDataMapping(fields),
|
|
419
|
+
UPDATE_FIELDS_ARRAY: this.generateUpdateFieldsArray(fields),
|
|
420
|
+
VALUE_OBJECT_IMPORTS: this.generateValueObjectImports(fields),
|
|
421
|
+
AGGREGATE_REF_IMPORTS: this.generateAggregateRefImports(modelName, fields),
|
|
422
|
+
LIST_METHODS: this.generateListMethods(modelName, fieldNamesStr, childInfo),
|
|
423
|
+
GET_BY_PARENT_ID_METHOD: this.generateGetByParentIdMethod(modelName, fields, childInfo),
|
|
424
|
+
GET_RESOURCE_OWNER_METHOD: this.generateGetResourceOwnerMethod(childInfo)
|
|
256
425
|
};
|
|
257
426
|
const rowInterface = this.replaceTemplateVars(storeTemplates_1.storeTemplates.rowInterface, variables);
|
|
258
|
-
const
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
427
|
+
const storeClass = this.replaceTemplateVars(storeTemplates_1.storeTemplates.storeClass, variables);
|
|
428
|
+
// Build entity import items: entity name + any enum type names
|
|
429
|
+
const entityImportItems = [modelName];
|
|
430
|
+
fields.forEach(([fieldName, fieldConfig]) => {
|
|
431
|
+
if (fieldConfig.type === 'enum' && fieldConfig.values && fieldConfig.values.length > 0) {
|
|
432
|
+
entityImportItems.push(`${modelName}${(0, typeUtils_1.capitalize)(fieldName)}`);
|
|
433
|
+
}
|
|
263
434
|
});
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
// Add relationship methods before the closing brace
|
|
268
|
-
const relationshipMethods = this.generateRelationshipMethods(modelConfig);
|
|
269
|
-
if (relationshipMethods) {
|
|
270
|
-
storeClass = storeClass.replace(/}$/, `${relationshipMethods}\n}`);
|
|
271
|
-
}
|
|
272
|
-
// Add relationship store imports
|
|
273
|
-
const relationshipImports = this.generateRelationshipImports(modelConfig);
|
|
274
|
-
return this.replaceTemplateVars(storeTemplates_1.fileTemplates.storeFile, {
|
|
275
|
-
ENTITY_NAME: entityName,
|
|
435
|
+
return this.replaceTemplateVars(storeTemplates_1.storeFileTemplate, {
|
|
436
|
+
ENTITY_NAME: modelName,
|
|
437
|
+
ENTITY_IMPORT_ITEMS: entityImportItems.join(', '),
|
|
276
438
|
ROW_INTERFACE: rowInterface,
|
|
277
|
-
STORE_CLASS: storeClass
|
|
278
|
-
|
|
439
|
+
STORE_CLASS: storeClass,
|
|
440
|
+
VALUE_OBJECT_IMPORTS: variables.VALUE_OBJECT_IMPORTS,
|
|
441
|
+
AGGREGATE_REF_IMPORTS: variables.AGGREGATE_REF_IMPORTS
|
|
442
|
+
});
|
|
279
443
|
}
|
|
280
|
-
|
|
444
|
+
generateFromConfig(config) {
|
|
281
445
|
const result = {};
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
446
|
+
// First, collect all value object names and configs
|
|
447
|
+
this.availableValueObjects.clear();
|
|
448
|
+
if (config.domain.valueObjects) {
|
|
449
|
+
Object.entries(config.domain.valueObjects).forEach(([name, voConfig]) => {
|
|
450
|
+
this.availableValueObjects.set(name, voConfig);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
// Collect all aggregate names for detecting entity references
|
|
454
|
+
this.availableAggregates.clear();
|
|
455
|
+
if (config.domain.aggregates) {
|
|
456
|
+
Object.keys(config.domain.aggregates).forEach(name => {
|
|
457
|
+
this.availableAggregates.add(name);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
// Generate a store for each aggregate
|
|
461
|
+
const childEntityMap = (0, childEntityUtils_1.buildChildEntityMap)(config);
|
|
462
|
+
if (config.domain.aggregates) {
|
|
463
|
+
Object.entries(config.domain.aggregates).forEach(([modelName, aggregateConfig]) => {
|
|
464
|
+
const childInfo = childEntityMap.get(modelName);
|
|
465
|
+
result[modelName] = this.generateStore(modelName, aggregateConfig, childInfo);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
285
468
|
return result;
|
|
286
469
|
}
|
|
287
470
|
generateFromYamlFile(yamlFilePath) {
|
|
288
471
|
const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
|
|
289
472
|
const config = (0, yaml_1.parse)(yamlContent);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
Object.values(config.modules).forEach(moduleConfig => {
|
|
293
|
-
if (moduleConfig.models && moduleConfig.models.length > 0) {
|
|
294
|
-
// Set available models for relationship detection
|
|
295
|
-
this.setAvailableModels(moduleConfig.models);
|
|
296
|
-
const stores = this.generateStores(moduleConfig.models);
|
|
297
|
-
Object.assign(result, stores);
|
|
298
|
-
}
|
|
299
|
-
});
|
|
473
|
+
if (!(0, configTypes_1.isValidModuleConfig)(config)) {
|
|
474
|
+
throw new Error('Configuration does not match new module format. Expected domain.aggregates structure.');
|
|
300
475
|
}
|
|
301
|
-
|
|
302
|
-
const module = config;
|
|
303
|
-
if (module.models && module.models.length > 0) {
|
|
304
|
-
// Set available models for relationship detection
|
|
305
|
-
this.setAvailableModels(module.models);
|
|
306
|
-
const stores = this.generateStores(module.models);
|
|
307
|
-
Object.assign(result, stores);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
return result;
|
|
476
|
+
return this.generateFromConfig(config);
|
|
311
477
|
}
|
|
312
|
-
async generateAndSaveFiles(yamlFilePath
|
|
313
|
-
const
|
|
314
|
-
const storesDir = path.join(
|
|
315
|
-
const interfacesDir = path.join(outputDir, 'interfaces');
|
|
478
|
+
async generateAndSaveFiles(yamlFilePath, moduleDir, opts) {
|
|
479
|
+
const storesByModel = this.generateFromYamlFile(yamlFilePath);
|
|
480
|
+
const storesDir = path.join(moduleDir, 'infrastructure', 'stores');
|
|
316
481
|
fs.mkdirSync(storesDir, { recursive: true });
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const interfaceFilePath = path.join(interfacesDir, constants_1.COMMON_FILES.STORE_INTERFACE);
|
|
320
|
-
await (0, generationRegistry_1.writeGeneratedFile)(interfaceFilePath, storeInterface, { force: !!(opts === null || opts === void 0 ? void 0 : opts.force), skipOnConflict: !!(opts === null || opts === void 0 ? void 0 : opts.skipOnConflict) });
|
|
321
|
-
for (const [entityName, storeCode] of Object.entries(stores)) {
|
|
322
|
-
const fileName = `${entityName}Store.ts`;
|
|
323
|
-
const filePath = path.join(storesDir, fileName);
|
|
482
|
+
for (const [modelName, code] of Object.entries(storesByModel)) {
|
|
483
|
+
const filePath = path.join(storesDir, `${modelName}Store.ts`);
|
|
324
484
|
// eslint-disable-next-line no-await-in-loop
|
|
325
|
-
await (0, generationRegistry_1.writeGeneratedFile)(filePath,
|
|
485
|
+
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) });
|
|
326
486
|
}
|
|
327
487
|
// eslint-disable-next-line no-console
|
|
328
|
-
console.log('\n' + colors_1.colors.green('
|
|
488
|
+
console.log('\n' + colors_1.colors.green('Store files generated successfully!') + '\n');
|
|
329
489
|
}
|
|
330
490
|
}
|
|
331
491
|
exports.StoreGenerator = StoreGenerator;
|