@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.
Files changed (60) hide show
  1. package/CHANGELOG.md +18 -609
  2. package/README.md +623 -427
  3. package/dist/cli.js +2 -1
  4. package/dist/commands/commit.js +25 -42
  5. package/dist/commands/createApp.js +1 -0
  6. package/dist/commands/createModule.js +151 -45
  7. package/dist/commands/diff.js +27 -40
  8. package/dist/commands/generateAll.js +141 -291
  9. package/dist/commands/migrateCommit.js +6 -18
  10. package/dist/generators/controllerGenerator.d.ts +50 -19
  11. package/dist/generators/controllerGenerator.js +588 -331
  12. package/dist/generators/domainLayerGenerator.d.ts +21 -0
  13. package/dist/generators/domainLayerGenerator.js +286 -0
  14. package/dist/generators/dtoGenerator.d.ts +21 -0
  15. package/dist/generators/dtoGenerator.js +523 -0
  16. package/dist/generators/serviceGenerator.d.ts +22 -51
  17. package/dist/generators/serviceGenerator.js +345 -568
  18. package/dist/generators/storeGenerator.d.ts +39 -32
  19. package/dist/generators/storeGenerator.js +396 -236
  20. package/dist/generators/templateGenerator.d.ts +21 -21
  21. package/dist/generators/templateGenerator.js +393 -268
  22. package/dist/generators/templates/appTemplates.d.ts +3 -1
  23. package/dist/generators/templates/appTemplates.js +16 -11
  24. package/dist/generators/templates/data/appYamlTemplate +5 -2
  25. package/dist/generators/templates/data/cursorRulesTemplate +315 -221
  26. package/dist/generators/templates/data/frontendScriptTemplate +56 -15
  27. package/dist/generators/templates/data/mainViewTemplate +2 -1
  28. package/dist/generators/templates/data/systemTsTemplate +5 -0
  29. package/dist/generators/templates/index.d.ts +0 -3
  30. package/dist/generators/templates/index.js +0 -3
  31. package/dist/generators/templates/storeTemplates.d.ts +1 -5
  32. package/dist/generators/templates/storeTemplates.js +84 -224
  33. package/dist/generators/useCaseGenerator.d.ts +13 -0
  34. package/dist/generators/useCaseGenerator.js +191 -0
  35. package/dist/types/configTypes.d.ts +149 -0
  36. package/dist/types/configTypes.js +10 -0
  37. package/dist/utils/childEntityUtils.d.ts +18 -0
  38. package/dist/utils/childEntityUtils.js +78 -0
  39. package/dist/utils/commandUtils.d.ts +43 -0
  40. package/dist/utils/commandUtils.js +124 -0
  41. package/dist/utils/commitUtils.d.ts +4 -1
  42. package/dist/utils/constants.d.ts +10 -0
  43. package/dist/utils/constants.js +13 -1
  44. package/dist/utils/diResolver.d.ts +32 -0
  45. package/dist/utils/diResolver.js +204 -0
  46. package/dist/utils/typeUtils.d.ts +23 -0
  47. package/dist/utils/typeUtils.js +77 -0
  48. package/package.json +7 -3
  49. package/dist/generators/domainModelGenerator.d.ts +0 -41
  50. package/dist/generators/domainModelGenerator.js +0 -242
  51. package/dist/generators/templates/controllerTemplates.d.ts +0 -43
  52. package/dist/generators/templates/controllerTemplates.js +0 -82
  53. package/dist/generators/templates/serviceTemplates.d.ts +0 -16
  54. package/dist/generators/templates/serviceTemplates.js +0 -59
  55. package/dist/generators/templates/validationTemplates.d.ts +0 -25
  56. package/dist/generators/templates/validationTemplates.js +0 -66
  57. package/dist/generators/templates/viewTemplates.d.ts +0 -25
  58. package/dist/generators/templates/viewTemplates.js +0 -491
  59. package/dist/generators/validationGenerator.d.ts +0 -29
  60. 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 constants_1 = require("../utils/constants");
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.typeMapping = {
47
- string: 'string',
48
- number: 'number',
49
- boolean: 'boolean',
50
- datetime: 'Date',
51
- json: 'any',
52
- array: 'any[]',
53
- object: 'object'
54
- };
55
- this.availableModels = new Set();
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
- mapType(yamlType, isRelationship = false) {
58
- // For relationships, we store the foreign key (number) in the database
59
- if (isRelationship) {
60
- return 'number';
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
- // Check if this is a known model (relationship) - should use foreign key type
63
- if (this.availableModels.has(yamlType)) {
64
- return 'number'; // Foreign keys are numbers
103
+ if (voFields.length === 1) {
104
+ return ` ${fieldName}: entity.${fieldName}?.${voFields[0]}`;
65
105
  }
66
- return this.typeMapping[yamlType] || 'any';
106
+ return ` ${fieldName}: entity.${fieldName} ? JSON.stringify(entity.${fieldName}) : undefined`;
67
107
  }
68
- setAvailableModels(models) {
69
- this.availableModels.clear();
70
- models.forEach(model => {
71
- this.availableModels.add(model.name);
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
- isRelationshipField(field) {
75
- return this.availableModels.has(field.type);
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
- getForeignKeyFieldName(field) {
78
- // Convention: fieldName + 'Id' (e.g., owner -> ownerId)
79
- return field.name + 'Id';
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
- generateRowFields(modelConfig) {
82
- const fields = [];
83
- modelConfig.fields.forEach(field => {
84
- if (field.name === 'createdAt') {
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
- // For relationship fields, store the foreign key instead
88
- if (this.isRelationshipField(field)) {
89
- const foreignKeyName = this.getForeignKeyFieldName(field);
90
- const tsType = 'number'; // Foreign keys are always numbers
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
- else {
96
- const tsType = this.mapType(field.type);
97
- const isOptional = !field.required && !field.auto;
98
- const fieldDef = ` ${field.name}${isOptional ? '?' : ''}: ${tsType};`;
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
- return fields.join('\n');
103
- }
104
- generateFilterableFields(modelConfig) {
105
- const filterableFields = modelConfig.fields
106
- .filter(field => ['string', 'number', 'boolean'].includes(field.type))
107
- .map(field => `'${field.name}'`);
108
- return filterableFields.join(' | ');
109
- }
110
- generateFilterableFieldsArray(modelConfig) {
111
- const filterableFields = modelConfig.fields
112
- .filter(field => ['string', 'number', 'boolean'].includes(field.type))
113
- .map(field => `'${field.name}'`);
114
- return filterableFields.join(', ');
115
- }
116
- generateUpdatableFieldsArray(modelConfig) {
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
- return aRequired ? -1 : 1; // Required fields come first
201
+ result.push(` row.${fieldName}`);
132
202
  });
203
+ return result.join(',\n');
133
204
  }
134
- generateRowToModelMapping(modelConfig) {
135
- // Sort fields to match the constructor parameter order
136
- const sortedFields = this.sortFieldsByRequired(modelConfig.fields);
137
- const mappings = sortedFields.map(field => {
138
- if (field.name === 'createdAt') {
139
- return ' row.created_at';
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
- // For relationship fields, we pass null - will be loaded separately
142
- if (this.isRelationshipField(field)) {
143
- return ' null as any'; // Placeholder, loaded via loadRelationships
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
- return ` row.${field.name}`;
232
+ result.push(` ${fieldName}: entity.${fieldName}`);
146
233
  });
147
- return mappings.join(',\n');
234
+ return result.join(',\n');
148
235
  }
149
- generateModelToRowMapping(modelConfig) {
150
- const mappings = modelConfig.fields.map(field => {
151
- if (field.name === 'createdAt') {
152
- return ' created_at: model.createdAt';
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
- // For relationship fields, extract ID from the object to store as FK
155
- if (this.isRelationshipField(field)) {
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
- return ` ${field.name}: model.${field.name}`;
160
- });
161
- return mappings.join(',\n');
162
- }
163
- replaceTemplateVars(template, variables) {
164
- let result = template;
165
- Object.entries(variables).forEach(([key, value]) => {
166
- const regex = new RegExp(`{{${key}}}`, 'g');
167
- result = result.replace(regex, value);
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
- generateStoreInterface() {
172
- return storeTemplates_1.fileTemplates.storeInterface;
257
+ generateUpdateFieldsArray(fields) {
258
+ return JSON.stringify(fields.map(([name, config]) => this.isAggregateField(config) ? `${name}_id` : name));
173
259
  }
174
- generateRelationshipMethods(modelConfig) {
175
- const relationshipFields = modelConfig.fields.filter(f => this.isRelationshipField(f));
176
- if (relationshipFields.length === 0) {
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
- const entityName = modelConfig.name;
180
- const methods = [];
181
- // Generate loadRelationships method
182
- const loadCalls = relationshipFields.map(field => {
183
- const foreignKeyName = this.getForeignKeyFieldName(field);
184
- const relatedModel = field.type;
185
- const relatedModelLower = relatedModel.toLowerCase();
186
- return ` if (entity.${field.name} === null && row.${foreignKeyName}) {
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
- return params.join(', ');
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
- generateRelationshipImports(modelConfig) {
234
- const relationshipFields = modelConfig.fields.filter(f => this.isRelationshipField(f));
235
- if (relationshipFields.length === 0) {
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
- const imports = relationshipFields.map(field => {
239
- const relatedModel = field.type;
240
- return `import { ${relatedModel}Store } from './${relatedModel}Store';`;
241
- });
242
- return '\n' + imports.join('\n');
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(modelConfig) {
245
- const entityName = modelConfig.name;
246
- const tableName = entityName.toLowerCase() + 's';
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: entityName,
412
+ ENTITY_NAME: modelName,
249
413
  TABLE_NAME: tableName,
250
- ROW_FIELDS: this.generateRowFields(modelConfig),
251
- FILTERABLE_FIELDS: this.generateFilterableFields(modelConfig),
252
- FILTERABLE_FIELDS_ARRAY: this.generateFilterableFieldsArray(modelConfig),
253
- UPDATABLE_FIELDS_ARRAY: this.generateUpdatableFieldsArray(modelConfig),
254
- ROW_TO_MODEL_MAPPING: this.generateRowToModelMapping(modelConfig),
255
- MODEL_TO_ROW_MAPPING: this.generateModelToRowMapping(modelConfig)
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 conversionMethods = this.replaceTemplateVars(storeTemplates_1.storeTemplates.conversionMethods, variables);
259
- // Replace constructor in storeClass template
260
- let storeClass = this.replaceTemplateVars(storeTemplates_1.storeTemplates.storeClass, {
261
- ...variables,
262
- CONVERSION_METHODS: conversionMethods
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
- // Update constructor to include foreign store dependencies
265
- const constructorParams = this.generateStoreConstructorParams(modelConfig);
266
- storeClass = storeClass.replace('constructor(private db: ISqlProvider) {}', `constructor(${constructorParams}) {}`);
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
- }) + relationshipImports;
439
+ STORE_CLASS: storeClass,
440
+ VALUE_OBJECT_IMPORTS: variables.VALUE_OBJECT_IMPORTS,
441
+ AGGREGATE_REF_IMPORTS: variables.AGGREGATE_REF_IMPORTS
442
+ });
279
443
  }
280
- generateStores(models) {
444
+ generateFromConfig(config) {
281
445
  const result = {};
282
- models.forEach(model => {
283
- result[model.name] = this.generateStore(model);
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
- const result = {};
291
- if (config.modules) {
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
- else if (config.models) {
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 = constants_1.COMMON_FILES.APP_YAML, outputDir = 'infrastructure', opts) {
313
- const stores = this.generateFromYamlFile(yamlFilePath);
314
- const storesDir = path.join(outputDir, 'stores');
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
- fs.mkdirSync(interfacesDir, { recursive: true });
318
- const storeInterface = this.generateStoreInterface();
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, storeCode, { force: !!(opts === null || opts === void 0 ? void 0 : opts.force), skipOnConflict: !!(opts === null || opts === void 0 ? void 0 : opts.skipOnConflict) });
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('All store files generated successfully!') + '\n');
488
+ console.log('\n' + colors_1.colors.green('Store files generated successfully!') + '\n');
329
489
  }
330
490
  }
331
491
  exports.StoreGenerator = StoreGenerator;