@currentjs/gen 0.3.2 → 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.
Files changed (69) hide show
  1. package/CHANGELOG.md +10 -611
  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/commands/migratePush.d.ts +1 -0
  11. package/dist/commands/migratePush.js +135 -0
  12. package/dist/commands/migrateUpdate.d.ts +1 -0
  13. package/dist/commands/migrateUpdate.js +147 -0
  14. package/dist/commands/newGenerateAll.d.ts +4 -0
  15. package/dist/commands/newGenerateAll.js +336 -0
  16. package/dist/generators/controllerGenerator.d.ts +43 -19
  17. package/dist/generators/controllerGenerator.js +547 -329
  18. package/dist/generators/domainLayerGenerator.d.ts +21 -0
  19. package/dist/generators/domainLayerGenerator.js +276 -0
  20. package/dist/generators/dtoGenerator.d.ts +21 -0
  21. package/dist/generators/dtoGenerator.js +518 -0
  22. package/dist/generators/newControllerGenerator.d.ts +55 -0
  23. package/dist/generators/newControllerGenerator.js +644 -0
  24. package/dist/generators/newServiceGenerator.d.ts +19 -0
  25. package/dist/generators/newServiceGenerator.js +266 -0
  26. package/dist/generators/newStoreGenerator.d.ts +39 -0
  27. package/dist/generators/newStoreGenerator.js +408 -0
  28. package/dist/generators/newTemplateGenerator.d.ts +29 -0
  29. package/dist/generators/newTemplateGenerator.js +510 -0
  30. package/dist/generators/serviceGenerator.d.ts +16 -51
  31. package/dist/generators/serviceGenerator.js +167 -586
  32. package/dist/generators/storeGenerator.d.ts +35 -32
  33. package/dist/generators/storeGenerator.js +291 -238
  34. package/dist/generators/storeGeneratorV2.d.ts +31 -0
  35. package/dist/generators/storeGeneratorV2.js +190 -0
  36. package/dist/generators/templateGenerator.d.ts +21 -21
  37. package/dist/generators/templateGenerator.js +393 -268
  38. package/dist/generators/templates/appTemplates.d.ts +3 -1
  39. package/dist/generators/templates/appTemplates.js +15 -10
  40. package/dist/generators/templates/data/appYamlTemplate +5 -2
  41. package/dist/generators/templates/data/cursorRulesTemplate +315 -221
  42. package/dist/generators/templates/data/frontendScriptTemplate +45 -11
  43. package/dist/generators/templates/data/mainViewTemplate +1 -1
  44. package/dist/generators/templates/data/systemTsTemplate +5 -0
  45. package/dist/generators/templates/index.d.ts +0 -3
  46. package/dist/generators/templates/index.js +0 -3
  47. package/dist/generators/templates/newStoreTemplates.d.ts +5 -0
  48. package/dist/generators/templates/newStoreTemplates.js +141 -0
  49. package/dist/generators/templates/storeTemplates.d.ts +1 -5
  50. package/dist/generators/templates/storeTemplates.js +102 -219
  51. package/dist/generators/templates/viewTemplates.js +1 -1
  52. package/dist/generators/useCaseGenerator.d.ts +13 -0
  53. package/dist/generators/useCaseGenerator.js +188 -0
  54. package/dist/types/configTypes.d.ts +148 -0
  55. package/dist/types/configTypes.js +10 -0
  56. package/dist/utils/childEntityUtils.d.ts +18 -0
  57. package/dist/utils/childEntityUtils.js +78 -0
  58. package/dist/utils/commandUtils.d.ts +43 -0
  59. package/dist/utils/commandUtils.js +124 -0
  60. package/dist/utils/commitUtils.d.ts +4 -1
  61. package/dist/utils/constants.d.ts +10 -0
  62. package/dist/utils/constants.js +13 -1
  63. package/dist/utils/diResolver.d.ts +32 -0
  64. package/dist/utils/diResolver.js +204 -0
  65. package/dist/utils/new_parts_of_migrationUtils.d.ts +0 -0
  66. package/dist/utils/new_parts_of_migrationUtils.js +164 -0
  67. package/dist/utils/typeUtils.d.ts +19 -0
  68. package/dist/utils/typeUtils.js +70 -0
  69. package/package.json +7 -3
@@ -37,640 +37,221 @@ exports.ServiceGenerator = 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 serviceTemplates_1 = require("./templates/serviceTemplates");
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 typeUtils_1 = require("../utils/typeUtils");
44
45
  class ServiceGenerator {
45
46
  constructor() {
46
- this.availableModels = new Set();
47
+ this.availableAggregates = new Map();
47
48
  }
48
- setAvailableModels(models) {
49
- this.availableModels.clear();
50
- models.forEach(model => {
51
- this.availableModels.add(model.name);
52
- });
49
+ mapType(yamlType) {
50
+ return (0, typeUtils_1.mapType)(yamlType, this.availableAggregates);
53
51
  }
54
- isRelationshipField(field) {
55
- return this.availableModels.has(field.type);
52
+ generateListHandler(modelName, storeName) {
53
+ return ` async list(page: number = 1, limit: number = 20): Promise<{ items: ${modelName}[]; total: number; page: number; limit: number }> {
54
+ const [items, total] = await Promise.all([
55
+ this.${storeName}.getAll(page, limit),
56
+ this.${storeName}.count()
57
+ ]);
58
+ return { items, total, page, limit };
59
+ }`;
56
60
  }
57
- getForeignKeyFieldName(field) {
58
- // Convention: fieldName + 'Id' (e.g., owner -> ownerId)
59
- return field.name + 'Id';
61
+ generateGetHandler(modelName, storeName, entityLower) {
62
+ return ` async get(id: number): Promise<${modelName}> {
63
+ const ${entityLower} = await this.${storeName}.getById(id);
64
+ if (!${entityLower}) {
65
+ throw new Error('${modelName} not found');
60
66
  }
61
- hasPermissions(config) {
62
- if (config.modules) {
63
- return Object.values(config.modules).some(module => module.permissions && module.permissions.length > 0);
64
- }
65
- const module = config;
66
- return !!(module.permissions && module.permissions.length > 0);
67
+ return ${entityLower};
68
+ }`;
67
69
  }
68
- getActionPermissions(moduleName, moduleConfig) {
69
- if (!moduleConfig.permissions || !moduleConfig.actions) {
70
- return {};
71
- }
72
- const actionPermissions = {};
73
- // Initialize all actions with empty permissions
74
- Object.keys(moduleConfig.actions).forEach(action => {
75
- actionPermissions[action] = [];
70
+ generateCreateHandler(modelName, storeName, entityLower, aggregateConfig, childInfo) {
71
+ const firstArgField = childInfo ? childInfo.parentIdField : 'ownerId';
72
+ const fields = Object.entries(aggregateConfig.fields)
73
+ .filter(([fieldName, fieldConfig]) => !fieldConfig.auto && fieldName !== 'id')
74
+ .sort((a, b) => {
75
+ const aRequired = a[1].required !== false;
76
+ const bRequired = b[1].required !== false;
77
+ if (aRequired === bRequired)
78
+ return 0;
79
+ return aRequired ? -1 : 1;
76
80
  });
77
- // Fill in permissions for each action
78
- (moduleConfig.permissions || []).forEach(permission => {
79
- (permission.actions || []).forEach(action => {
80
- if (actionPermissions[action]) {
81
- actionPermissions[action].push(permission.role);
82
- }
83
- });
84
- });
85
- return actionPermissions;
86
- }
87
- generatePermissionCheck(action, roles, entityName) {
88
- if (roles.length === 0 || roles.includes('all')) {
89
- return '';
90
- }
91
- if (roles.includes('none')) {
92
- return " throw new Error('This action is not permitted');";
93
- }
94
- const nonOwnerRoles = roles.filter(role => !['all', 'none', 'owner'].includes(role));
95
- const rolesArray = nonOwnerRoles.map(role => `'${role}'`).join(', ');
96
- // Special-case: list action with owner → rely on store-level filtering; only enforce static role checks for other roles
97
- if (action === 'list' && roles.includes('owner')) {
98
- if (nonOwnerRoles.length === 0) {
99
- return '';
100
- }
101
- return serviceTemplates_1.serviceTemplates.permissionCheck
102
- .replace(/{{REQUIRED_ROLES}}/g, roles.join(', '))
103
- .replace(/{{ACTION_NAME}}/g, action)
104
- .replace(/{{ROLES_ARRAY}}/g, rolesArray);
105
- }
106
- // For resource-based owner permission (non-list), fetch resource first and verify ownership
107
- if (roles.includes('owner')) {
108
- const isResourceBased = ['get', 'update', 'delete'].includes(action);
109
- if (!isResourceBased) {
110
- // Not a resource-based action; fall back to static role checks only (if any non-owner roles)
111
- if (nonOwnerRoles.length === 0) {
112
- return '';
113
- }
114
- return serviceTemplates_1.serviceTemplates.permissionCheck
115
- .replace(/{{REQUIRED_ROLES}}/g, roles.join(', '))
116
- .replace(/{{ACTION_NAME}}/g, action)
117
- .replace(/{{ROLES_ARRAY}}/g, rolesArray);
118
- }
119
- const entityLower = entityName.toLowerCase();
120
- const resourceIdParam = this.getResourceIdForAction(action);
121
- const notFoundMessage = `${entityName} not found`;
122
- // If there are additional roles besides owner, allow them to bypass owner check
123
- const maybeRoleBypass = nonOwnerRoles.length > 0
124
- ? ` const allowedRoles = [${rolesArray}];
125
- if (allowedRoles.includes(user.role)) {
126
- // Explicit role allowed; skip owner check
127
- } else {
128
- const ${entityLower} = await this.${entityLower}Store.getById(${resourceIdParam});
129
- if (!${entityLower}) {
130
- throw new Error('${notFoundMessage}');
131
- }
132
- if ((${entityLower} as any).userId !== user.id) {
133
- throw new Error('You can only access your own resources');
134
- }
135
- }`
136
- : ` const ${entityLower} = await this.${entityLower}Store.getById(${resourceIdParam});
137
- if (!${entityLower}) {
138
- throw new Error('${notFoundMessage}');
81
+ const fieldArgs = fields.map(([fieldName]) => `input.${fieldName}`).join(', ');
82
+ const constructorArgs = `input.${firstArgField}, ${fieldArgs}`;
83
+ return ` async create(input: any): Promise<${modelName}> {
84
+ const ${entityLower} = new ${modelName}(0, ${constructorArgs});
85
+ return await this.${storeName}.insert(${entityLower});
86
+ }`;
139
87
  }
140
- if ((${entityLower} as any).userId !== user.id) {
141
- throw new Error('You can only access your own resources');
88
+ generateUpdateHandler(modelName, storeName, aggregateConfig) {
89
+ const setterCalls = Object.entries(aggregateConfig.fields)
90
+ .filter(([fieldName, fieldConfig]) => !fieldConfig.auto && fieldName !== 'id')
91
+ .map(([fieldName]) => {
92
+ const methodName = `set${(0, typeUtils_1.capitalize)(fieldName)}`;
93
+ return ` if (input.${fieldName} !== undefined) {
94
+ existing${modelName}.${methodName}(input.${fieldName});
142
95
  }`;
143
- return maybeRoleBypass;
144
- }
145
- // Static role check for non-owner roles
146
- return serviceTemplates_1.serviceTemplates.permissionCheck
147
- .replace(/{{REQUIRED_ROLES}}/g, roles.join(', '))
148
- .replace(/{{ACTION_NAME}}/g, action)
149
- .replace(/{{ROLES_ARRAY}}/g, rolesArray);
150
- }
151
- getResourceIdForAction(action) {
152
- switch (action) {
153
- case 'get':
154
- case 'update':
155
- case 'delete':
156
- return 'id';
157
- default:
158
- return 'id';
159
- }
160
- }
161
- generateMethodParams(action, entityName) {
162
- const entityParam = `${entityName.toLowerCase()}Data`;
163
- switch (action) {
164
- case 'list':
165
- return 'page: number = 1, limit: number = 10';
166
- case 'get':
167
- case 'getById':
168
- return 'id: number';
169
- case 'create':
170
- return `${entityParam}: ${entityName}DTO`;
171
- case 'update':
172
- return `id: number, ${entityParam}: ${entityName}DTO`;
173
- case 'delete':
174
- return 'id: number';
175
- default:
176
- return '';
177
- }
96
+ })
97
+ .join('\n');
98
+ return ` async update(id: number, input: any): Promise<${modelName}> {
99
+ const existing${modelName} = await this.${storeName}.getById(id);
100
+ if (!existing${modelName}) {
101
+ throw new Error('${modelName} not found');
178
102
  }
179
- generateReturnType(action, entityName) {
180
- switch (action) {
181
- case 'list':
182
- return `${entityName}[]`;
183
- case 'get':
184
- case 'getById':
185
- case 'create':
186
- case 'update':
187
- return entityName;
188
- case 'delete':
189
- return '{ success: boolean; message: string }';
190
- default:
191
- return 'void';
192
- }
103
+
104
+ ${setterCalls}
105
+
106
+ return await this.${storeName}.update(id, existing${modelName});
107
+ }`;
193
108
  }
194
- generateMethodImplementation(action, entityName, handlers, moduleName, roles, moduleConfig) {
195
- if (!handlers || handlers.length === 0) {
196
- return `// TODO: Implement ${action} method`;
197
- }
198
- const implementations = [];
199
- const entityLower = entityName.toLowerCase();
200
- handlers.forEach(handler => {
201
- var _a, _b;
202
- // Handle new explicit format: modelname:default:action or modelname:custommethod
203
- if (handler.includes(':')) {
204
- const parts = handler.split(':');
205
- if (parts.length === 3 && parts[1] === 'default') {
206
- // Format: modelname:default:action
207
- const [modelName, , actionName] = parts;
208
- // Check if this handler applies to current model
209
- if (modelName.toLowerCase() === entityLower || modelName === entityName) {
210
- const template = serviceTemplates_1.serviceTemplates.defaultImplementations[actionName];
211
- if (template) {
212
- let processedTemplate = template
213
- .replace(/{{ENTITY_NAME}}/g, entityName)
214
- .replace(/{{ENTITY_LOWER}}/g, entityLower);
215
- // Handle constructor args for create action
216
- if (actionName === 'create') {
217
- const relationshipLoading = this.generateRelationshipLoading(moduleConfig, entityName);
218
- const constructorArgs = this.generateConstructorArgs(moduleConfig, entityName);
219
- if (relationshipLoading) {
220
- // Insert relationship loading before the entity creation
221
- processedTemplate = relationshipLoading + '\n ' + processedTemplate;
222
- }
223
- processedTemplate = processedTemplate.replace(/{{CONSTRUCTOR_ARGS}}/g, constructorArgs);
224
- }
225
- // Handle setter calls for update action
226
- if (actionName === 'update') {
227
- const setterCalls = this.generateUpdateSetterCalls(moduleConfig, entityName, true);
228
- processedTemplate = processedTemplate.replace(/{{UPDATE_SETTER_CALLS}}/g, setterCalls);
229
- }
230
- // Special-case: list action with only owner role → fetch by userId
231
- if (actionName === 'list') {
232
- const onlyOwner = roles.length > 0 && roles.every(r => r === 'owner');
233
- if (onlyOwner) {
234
- processedTemplate = `const ${entityLower}s = await this.${entityLower}Store.getAllByUserId(user.id, page, limit);\n return ${entityLower}s;`;
235
- }
236
- }
237
- implementations.push(processedTemplate);
238
- }
239
- else {
240
- implementations.push(`// TODO: Implement default ${actionName} method for ${entityName}`);
241
- }
242
- }
243
- else {
244
- // Cross-model default call
245
- const targetModel = (_a = moduleConfig.models) === null || _a === void 0 ? void 0 : _a.find(m => m.name.toLowerCase() === modelName.toLowerCase() || m.name === modelName);
246
- if (targetModel) {
247
- const targetServiceVar = `${targetModel.name.toLowerCase()}Service`;
248
- const customImpl = `// Cross-model call to ${targetModel.name}Service
249
- const result = await this.${targetServiceVar}.${actionName}(${this.getMethodCallParams(action)});
250
- return result;`;
251
- implementations.push(customImpl);
252
- }
253
- else {
254
- implementations.push(`// TODO: Model ${modelName} not found for handler ${handler}`);
255
- }
256
- }
257
- }
258
- else if (parts.length === 2) {
259
- // Format: modelname:custommethod
260
- const [modelName, methodName] = parts;
261
- // Check if this handler applies to current model
262
- if (modelName.toLowerCase() === entityLower || modelName === entityName) {
263
- // Same model - this indicates a custom method should be implemented in this service
264
- implementations.push(`// TODO: Implement custom ${methodName} method for ${entityName}`);
265
- }
266
- else {
267
- // Cross-model custom call
268
- const targetModel = (_b = moduleConfig.models) === null || _b === void 0 ? void 0 : _b.find(m => m.name.toLowerCase() === modelName.toLowerCase() || m.name === modelName);
269
- if (targetModel) {
270
- const targetServiceVar = `${targetModel.name.toLowerCase()}Service`;
271
- const customImpl = `// Cross-model call to ${targetModel.name}Service
272
- const result = await this.${targetServiceVar}.${methodName}(${this.getMethodCallParams(action)});
273
- return result;`;
274
- implementations.push(customImpl);
275
- }
276
- else {
277
- implementations.push(`// TODO: Model ${modelName} not found for handler ${handler}`);
278
- }
279
- }
280
- }
281
- else {
282
- implementations.push(`// TODO: Invalid handler format ${handler}. Use modelname:default:action or modelname:custommethod`);
283
- }
284
- }
285
- else {
286
- implementations.push(`// TODO: Invalid handler format ${handler}. Use modelname:default:action or modelname:custommethod`);
287
- }
288
- });
289
- return implementations.join('\n ');
109
+ generateDeleteHandler(modelName, storeName) {
110
+ return ` async delete(id: number): Promise<{ success: boolean; message: string }> {
111
+ const success = await this.${storeName}.softDelete(id);
112
+ if (!success) {
113
+ throw new Error('${modelName} not found or could not be deleted');
290
114
  }
291
- extractFunctionName(filePath) {
292
- const parts = filePath.split('/');
293
- const fileName = parts[parts.length - 1];
294
- return fileName;
115
+ return { success: true, message: '${modelName} deleted successfully' };
116
+ }`;
295
117
  }
296
- getMethodCallParams(action) {
297
- switch (action) {
118
+ generateDefaultHandlerMethod(modelName, actionName, aggregateConfig, childInfo) {
119
+ const entityLower = modelName.toLowerCase();
120
+ const storeName = `${entityLower}Store`;
121
+ switch (actionName) {
298
122
  case 'list':
299
- return 'page, limit';
123
+ return this.generateListHandler(modelName, storeName);
300
124
  case 'get':
301
- return 'id';
125
+ return this.generateGetHandler(modelName, storeName, entityLower);
302
126
  case 'create':
303
- return 'userData';
127
+ return this.generateCreateHandler(modelName, storeName, entityLower, aggregateConfig, childInfo);
304
128
  case 'update':
305
- return 'id, updates';
129
+ return this.generateUpdateHandler(modelName, storeName, aggregateConfig);
306
130
  case 'delete':
307
- return 'id';
131
+ return this.generateDeleteHandler(modelName, storeName);
308
132
  default:
309
- return '/* custom params */';
133
+ return ` async ${actionName}(input: any): Promise<any> {
134
+ // TODO: Implement default ${actionName} handler
135
+ throw new Error('Not implemented');
136
+ }`;
310
137
  }
311
138
  }
312
- sortFieldsByRequired(fields) {
313
- // Sort fields: required fields first, then optional fields
314
- // This must match the order used in domainModelGenerator
315
- return [...fields].sort((a, b) => {
316
- const aRequired = a.required !== false && !a.auto;
317
- const bRequired = b.required !== false && !b.auto;
318
- if (aRequired === bRequired) {
319
- return 0; // Keep original order if both have same required status
320
- }
321
- return aRequired ? -1 : 1; // Required fields come first
322
- });
139
+ generateCustomHandlerMethod(modelName, handlerName) {
140
+ return ` async ${handlerName}(result: any, input: any): Promise<any> {
141
+ // TODO: Implement custom ${handlerName} handler
142
+ // This method receives the result from the previous handler (or null if first)
143
+ // and the input context
144
+ return result;
145
+ }`;
323
146
  }
324
- generateConstructorArgs(moduleConfig, entityName) {
325
- if (!moduleConfig.models || moduleConfig.models.length === 0) {
326
- return '';
327
- }
328
- // Find the correct model by entityName instead of always using first model
329
- const model = moduleConfig.models.find((m) => m.name === entityName) || moduleConfig.models[0];
330
- const entityLower = entityName.toLowerCase();
331
- // Sort fields to match the constructor parameter order
332
- const sortedFields = this.sortFieldsByRequired(model.fields);
333
- const args = [];
334
- sortedFields
335
- .filter(field => !field.auto && field.name !== 'id')
336
- .forEach(field => {
337
- // For relationship fields, reference the loaded object variable
338
- if (this.isRelationshipField(field)) {
339
- args.push(`${field.name}Object`);
340
- }
341
- else {
342
- args.push(`${entityLower}Data.${field.name}`);
343
- }
147
+ collectHandlers(useCases) {
148
+ const handlers = new Set();
149
+ Object.values(useCases).forEach(useCaseConfig => {
150
+ useCaseConfig.handlers.forEach(handler => {
151
+ handlers.add(handler);
152
+ });
344
153
  });
345
- return args.join(', ');
346
- }
347
- generateRelationshipLoading(moduleConfig, entityName) {
348
- if (!moduleConfig.models || moduleConfig.models.length === 0) {
349
- return '';
350
- }
351
- const model = moduleConfig.models.find((m) => m.name === entityName) || moduleConfig.models[0];
352
- const entityLower = entityName.toLowerCase();
353
- const relationshipFields = model.fields.filter(f => this.isRelationshipField(f));
354
- if (relationshipFields.length === 0) {
355
- return '';
356
- }
357
- const loadingCode = relationshipFields.map(field => {
358
- const foreignKeyName = this.getForeignKeyFieldName(field);
359
- const relatedModel = field.type;
360
- const relatedModelLower = relatedModel.toLowerCase();
361
- const varName = `${field.name}Object`;
362
- if (field.required) {
363
- return `const ${varName} = await this.${relatedModelLower}Store.getById(${entityLower}Data.${foreignKeyName});
364
- if (!${varName}) {
365
- throw new Error('${relatedModel} not found with id ' + ${entityLower}Data.${foreignKeyName});
366
- }`;
367
- }
368
- else {
369
- return `const ${varName} = ${entityLower}Data.${foreignKeyName}
370
- ? await this.${relatedModelLower}Store.getById(${entityLower}Data.${foreignKeyName})
371
- : null;`;
372
- }
373
- }).join('\n ');
374
- return ' // Load relationship objects\n ' + loadingCode;
154
+ return handlers;
375
155
  }
376
- generateUpdateSetterCalls(moduleConfig, entityName, includeRelationshipLoading = false) {
377
- if (!moduleConfig.models || moduleConfig.models.length === 0) {
156
+ generateListByParentMethod(modelName, childInfo) {
157
+ if (!childInfo)
378
158
  return '';
379
- }
380
- // Find the correct model by entityName instead of always using first model
381
- const model = moduleConfig.models.find(m => m.name === entityName) || moduleConfig.models[0];
382
- const entityLower = entityName.toLowerCase();
383
- let code = '';
384
- // Add relationship loading if requested
385
- if (includeRelationshipLoading) {
386
- const relationshipLoading = this.generateRelationshipLoading(moduleConfig, entityName);
387
- if (relationshipLoading) {
388
- code = relationshipLoading + '\n ';
389
- }
390
- }
391
- const setterCalls = model.fields
392
- .filter(field => !field.auto && field.name !== 'id')
393
- .map(field => {
394
- // For relationship fields, set the loaded object
395
- if (this.isRelationshipField(field)) {
396
- const foreignKeyName = this.getForeignKeyFieldName(field);
397
- const methodName = `set${field.name.charAt(0).toUpperCase() + field.name.slice(1)}`;
398
- return `if (${entityLower}Data.${foreignKeyName} !== undefined) {
399
- existing${entityName}.${methodName}(${field.name}Object);
400
- }`;
401
- }
402
- else {
403
- const methodName = `set${field.name.charAt(0).toUpperCase() + field.name.slice(1)}`;
404
- return `if (${entityLower}Data.${field.name} !== undefined) {
405
- existing${entityName}.${methodName}(${entityLower}Data.${field.name});
406
- }`;
407
- }
408
- })
409
- .join('\n ');
410
- return code + setterCalls;
411
- }
412
- replaceTemplateVars(template, variables) {
413
- let result = template;
414
- Object.entries(variables).forEach(([key, value]) => {
415
- const regex = new RegExp(`{{${key}}}`, 'g');
416
- result = result.replace(regex, String(value));
417
- });
418
- return result;
159
+ const storeVar = `${modelName.toLowerCase()}Store`;
160
+ return `
161
+ async listByParent(parentId: number): Promise<${modelName}[]> {
162
+ return await this.${storeVar}.getByParentId(parentId);
163
+ }`;
419
164
  }
420
- getServiceMethodName(action, entityName, handlers) {
421
- const entityLower = entityName.toLowerCase();
422
- // Find handler that applies to this model
423
- const relevantHandler = handlers.find(h => {
424
- const parts = h.split(':');
425
- if (parts.length === 3 && parts[1] === 'default') {
426
- const [modelName] = parts;
427
- return modelName.toLowerCase() === entityLower || modelName === entityName;
428
- }
429
- else if (parts.length === 2) {
430
- const [modelName] = parts;
431
- return modelName.toLowerCase() === entityLower || modelName === entityName;
432
- }
433
- return false;
434
- });
435
- if (relevantHandler) {
436
- const parts = relevantHandler.split(':');
437
- if (parts.length === 3 && parts[1] === 'default') {
438
- // Format: modelname:default:action -> use action name as method
439
- return parts[2];
440
- }
441
- else if (parts.length === 2) {
442
- // Format: modelname:custommethod -> use custom method name
443
- return parts[1];
444
- }
445
- }
446
- // Fallback to action name
447
- return action;
165
+ generateGetResourceOwnerMethod(modelName) {
166
+ const storeVar = `${modelName.toLowerCase()}Store`;
167
+ return `
168
+ /**
169
+ * Get the owner ID of a resource by its ID.
170
+ * Used for pre-mutation authorization checks.
171
+ */
172
+ async getResourceOwner(id: number): Promise<number | null> {
173
+ return await this.${storeVar}.getResourceOwner(id);
174
+ }`;
448
175
  }
449
- generateHandlerMethod(handler, entityName, roles, hasPermissions, moduleConfig) {
450
- const parts = handler.split(':');
451
- if (parts.length < 2) {
452
- return '';
453
- }
454
- let methodName;
455
- let isDefault = false;
456
- let actionName = '';
457
- if (parts.length === 3 && parts[1] === 'default') {
458
- // Format: modelname:default:action
459
- actionName = parts[2];
460
- methodName = actionName === 'getById' ? 'get' : actionName; // Use 'get' instead of 'getById'
461
- isDefault = true;
462
- }
463
- else if (parts.length === 2) {
464
- // Format: modelname:custommethod
465
- methodName = parts[1];
466
- isDefault = false;
467
- }
468
- else {
469
- return '';
470
- }
471
- const entityLower = entityName.toLowerCase();
472
- // Generate method parameters based on whether it's default or custom
473
- let methodParams;
474
- let returnType;
475
- let methodImplementation;
476
- if (isDefault) {
477
- // For default handlers, use standard parameters
478
- methodParams = this.generateMethodParams(actionName, entityName);
479
- returnType = this.generateReturnType(actionName, entityName);
480
- // Get the default implementation template
481
- const template = serviceTemplates_1.serviceTemplates.defaultImplementations[actionName];
482
- if (template) {
483
- methodImplementation = template
484
- .replace(/{{ENTITY_NAME}}/g, entityName)
485
- .replace(/{{ENTITY_LOWER}}/g, entityLower);
486
- // Handle constructor args for create action
487
- if (actionName === 'create') {
488
- const relationshipLoading = this.generateRelationshipLoading(moduleConfig, entityName);
489
- const constructorArgs = this.generateConstructorArgs(moduleConfig, entityName);
490
- if (relationshipLoading) {
491
- // Insert relationship loading before the entity creation
492
- methodImplementation = relationshipLoading + '\n ' + methodImplementation;
493
- }
494
- methodImplementation = methodImplementation.replace(/{{CONSTRUCTOR_ARGS}}/g, constructorArgs);
495
- }
496
- // Handle setter calls for update action
497
- if (actionName === 'update') {
498
- const setterCalls = this.generateUpdateSetterCalls(moduleConfig, entityName, true);
499
- methodImplementation = methodImplementation.replace(/{{UPDATE_SETTER_CALLS}}/g, setterCalls);
500
- }
501
- // Special-case: list action with only owner role → fetch by userId
502
- if (actionName === 'list') {
503
- const onlyOwner = roles.length > 0 && roles.every(r => r === 'owner');
504
- if (onlyOwner) {
505
- methodImplementation = `const ${entityLower}s = await this.${entityLower}Store.getAllByUserId(user.id, page, limit);\n return ${entityLower}s;`;
506
- }
507
- }
176
+ generateService(modelName, useCases, aggregateConfig, childInfo) {
177
+ const serviceName = `${modelName}Service`;
178
+ const storeName = `${modelName}Store`;
179
+ const storeVar = `${modelName.toLowerCase()}Store`;
180
+ // Collect all unique handlers
181
+ const handlers = this.collectHandlers(useCases);
182
+ // Generate methods for each handler
183
+ const methods = [];
184
+ handlers.forEach(handler => {
185
+ if (handler.startsWith('default:')) {
186
+ const actionName = handler.replace('default:', '');
187
+ methods.push(this.generateDefaultHandlerMethod(modelName, actionName, aggregateConfig, childInfo));
508
188
  }
509
189
  else {
510
- methodImplementation = `// TODO: Implement default ${actionName} method for ${entityName}`;
190
+ methods.push(this.generateCustomHandlerMethod(modelName, handler));
511
191
  }
512
- }
513
- else {
514
- // For custom handlers, use result and context parameters
515
- methodParams = 'result: any, context: any';
516
- returnType = 'any';
517
- methodImplementation = `// TODO: Implement custom ${methodName} method for ${entityName}`;
518
- }
519
- const permissionCheck = isDefault ? this.generatePermissionCheck(actionName, roles, entityName) : '';
520
- const hasUserParam = roles.length > 0 && !roles.includes('all');
521
- const variables = {
522
- METHOD_NAME: methodName,
523
- METHOD_PARAMS: methodParams,
524
- USER_PARAM: hasUserParam ? ', user: AuthenticatedUser' : '',
525
- RETURN_TYPE: returnType,
526
- PERMISSION_CHECK: permissionCheck,
527
- METHOD_IMPLEMENTATION: methodImplementation
528
- };
529
- return this.replaceTemplateVars(serviceTemplates_1.serviceTemplates.serviceMethod, variables);
530
- }
531
- generateServiceForModel(model, moduleName, moduleConfig, hasGlobalPermissions) {
532
- if (!moduleConfig.actions) {
533
- return '';
534
- }
535
- const entityName = model.name;
536
- const entityLower = entityName.toLowerCase();
537
- const actionPermissions = this.getActionPermissions(moduleName, moduleConfig);
538
- const hasPermissions = !!(hasGlobalPermissions && moduleConfig.permissions && moduleConfig.permissions.length > 0);
539
- // Collect all unique handlers for this model
540
- const modelHandlers = new Set();
541
- Object.entries(moduleConfig.actions || {}).forEach(([action, actionConfig]) => {
542
- const handlers = actionConfig.handlers || [];
543
- handlers.forEach(handler => {
544
- // Only include handlers that apply to this model
545
- if (handler.startsWith(`${entityLower}:`) || handler.startsWith(`${entityName}:`)) {
546
- modelHandlers.add(handler);
547
- }
548
- });
549
- });
550
- // Generate a method for each unique handler
551
- const serviceMethods = Array.from(modelHandlers)
552
- .map(handler => {
553
- // Determine which actions use this handler to get permissions
554
- const actionsUsingHandler = Object.entries(moduleConfig.actions || {})
555
- .filter(([, actionConfig]) => (actionConfig.handlers || []).includes(handler))
556
- .map(([action]) => action);
557
- // Use permissions from the first action that uses this handler
558
- const firstAction = actionsUsingHandler[0];
559
- const roles = firstAction ? (actionPermissions[firstAction] || []) : [];
560
- return this.generateHandlerMethod(handler, entityName, roles, hasPermissions, moduleConfig);
561
- })
562
- .filter(method => method) // Remove empty methods
563
- .join('\n\n');
564
- // Add foreign store constructor params
565
- const foreignStoreParams = this.generateForeignStoreConstructorParams(model);
566
- const serviceClass = this.replaceTemplateVars(serviceTemplates_1.serviceTemplates.serviceClass, {
567
- ENTITY_NAME: entityName,
568
- ENTITY_LOWER: entityLower,
569
- AUTH_SERVICE_PARAM: foreignStoreParams,
570
- SERVICE_METHODS: serviceMethods
571
- });
572
- const customImports = this.generateCustomImports(moduleConfig);
573
- const foreignStoreImports = this.generateForeignStoreImports(model);
574
- return this.replaceTemplateVars(serviceTemplates_1.serviceFileTemplate, {
575
- ENTITY_NAME: entityName,
576
- PERMISSIONS_IMPORT: hasPermissions ? "\nimport type { AuthenticatedUser } from '@currentjs/router';" : '',
577
- CUSTOM_IMPORTS: customImports + foreignStoreImports,
578
- SERVICE_CLASS: serviceClass
579
192
  });
580
- }
581
- generateService(moduleName, moduleConfig, hasGlobalPermissions) {
582
- // Legacy method for backward compatibility - use first model
583
- if (!moduleConfig.models || moduleConfig.models.length === 0) {
584
- return '';
193
+ const listByParentMethod = this.generateListByParentMethod(modelName, childInfo);
194
+ if (listByParentMethod) {
195
+ methods.push(listByParentMethod);
585
196
  }
586
- return this.generateServiceForModel(moduleConfig.models[0], moduleName, moduleConfig, hasGlobalPermissions);
587
- }
588
- generateForeignStoreImports(model) {
589
- const relationshipFields = model.fields.filter(f => this.isRelationshipField(f));
590
- if (relationshipFields.length === 0) {
591
- return '';
197
+ const getResourceOwnerMethod = this.generateGetResourceOwnerMethod(modelName);
198
+ if (getResourceOwnerMethod) {
199
+ methods.push(getResourceOwnerMethod);
592
200
  }
593
- const imports = relationshipFields.map(field => {
594
- const relatedModel = field.type;
595
- return `import { ${relatedModel}Store } from '../../infrastructure/stores/${relatedModel}Store';`;
596
- });
597
- return '\n' + imports.join('\n');
201
+ return `import { Injectable } from '../../../../system';
202
+ import { ${modelName} } from '../../domain/entities/${modelName}';
203
+ import { ${storeName} } from '../../infrastructure/stores/${storeName}';
204
+
205
+ /**
206
+ * Service layer for ${modelName}
207
+ * Contains business logic handlers that can be composed in use cases
208
+ */
209
+ @Injectable()
210
+ export class ${serviceName} {
211
+ constructor(
212
+ private ${storeVar}: ${storeName}
213
+ ) {}
214
+
215
+ ${methods.join('\n\n')}
216
+ }`;
598
217
  }
599
- generateForeignStoreConstructorParams(model) {
600
- const relationshipFields = model.fields.filter(f => this.isRelationshipField(f));
601
- if (relationshipFields.length === 0) {
602
- return '';
218
+ generateFromConfig(config) {
219
+ const result = {};
220
+ // Collect all aggregates
221
+ if (config.domain.aggregates) {
222
+ Object.entries(config.domain.aggregates).forEach(([name, aggConfig]) => {
223
+ this.availableAggregates.set(name, aggConfig);
224
+ });
603
225
  }
604
- const params = relationshipFields.map(field => {
605
- const relatedModel = field.type;
606
- const relatedModelLower = relatedModel.toLowerCase();
607
- return `,\n private ${relatedModelLower}Store: ${relatedModel}Store`;
608
- });
609
- return params.join('');
610
- }
611
- generateCustomImports(moduleConfig) {
612
- if (!moduleConfig.actions)
613
- return '';
614
- const imports = [];
615
- Object.values(moduleConfig.actions).forEach(actionConfig => {
616
- if (actionConfig.handlers) {
617
- actionConfig.handlers.forEach(handler => {
618
- if (handler.startsWith(constants_1.PATH_PATTERNS.MODULES_DIRECTIVE)) {
619
- const functionName = this.extractFunctionName(handler);
620
- const importPath = handler.replace(constants_1.PATH_PATTERNS.MODULES_DIRECTIVE, '../../');
621
- imports.push(`import ${functionName} from '${importPath}';`);
622
- }
623
- });
226
+ const childEntityMap = (0, childEntityUtils_1.buildChildEntityMap)(config);
227
+ // Generate a Service file for each model
228
+ Object.entries(config.useCases).forEach(([modelName, useCases]) => {
229
+ const aggregateConfig = this.availableAggregates.get(modelName);
230
+ if (!aggregateConfig) {
231
+ console.warn(`Warning: No aggregate found for model ${modelName}`);
232
+ return;
624
233
  }
234
+ const childInfo = childEntityMap.get(modelName);
235
+ result[modelName] = this.generateService(modelName, useCases, aggregateConfig, childInfo);
625
236
  });
626
- return imports.length > 0 ? '\n' + imports.join('\n') : '';
237
+ return result;
627
238
  }
628
239
  generateFromYamlFile(yamlFilePath) {
629
240
  const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
630
241
  const config = (0, yaml_1.parse)(yamlContent);
631
- const result = {};
632
- const hasGlobalPermissions = this.hasPermissions(config);
633
- if (config.modules) {
634
- Object.entries(config.modules).forEach(([moduleName, moduleConfig]) => {
635
- if (moduleConfig.models && moduleConfig.models.length > 0) {
636
- // Set available models for relationship detection
637
- this.setAvailableModels(moduleConfig.models);
638
- // Generate a service for each model
639
- moduleConfig.models.forEach(model => {
640
- const serviceCode = this.generateServiceForModel(model, moduleName, moduleConfig, hasGlobalPermissions);
641
- if (serviceCode) {
642
- result[model.name] = serviceCode;
643
- }
644
- });
645
- }
646
- });
242
+ if (!(0, configTypes_1.isValidModuleConfig)(config)) {
243
+ throw new Error('Configuration does not match new module format. Expected useCases structure.');
647
244
  }
648
- else {
649
- const moduleName = 'Module';
650
- const moduleConfig = config;
651
- if (moduleConfig.models && moduleConfig.models.length > 0) {
652
- // Set available models for relationship detection
653
- this.setAvailableModels(moduleConfig.models);
654
- // Generate a service for each model
655
- moduleConfig.models.forEach(model => {
656
- const serviceCode = this.generateServiceForModel(model, moduleName, moduleConfig, hasGlobalPermissions);
657
- if (serviceCode) {
658
- result[model.name] = serviceCode;
659
- }
660
- });
661
- }
662
- }
663
- return result;
245
+ return this.generateFromConfig(config);
664
246
  }
665
- async generateAndSaveFiles(yamlFilePath = constants_1.COMMON_FILES.APP_YAML, outputDir = 'application', opts) {
666
- const services = this.generateFromYamlFile(yamlFilePath);
667
- const servicesDir = path.join(outputDir, 'services');
247
+ async generateAndSaveFiles(yamlFilePath, moduleDir, opts) {
248
+ const servicesByModel = this.generateFromYamlFile(yamlFilePath);
249
+ const servicesDir = path.join(moduleDir, 'application', 'services');
668
250
  fs.mkdirSync(servicesDir, { recursive: true });
669
- for (const [moduleName, serviceCode] of Object.entries(services)) {
670
- const fileName = `${moduleName}Service.ts`;
671
- const filePath = path.join(servicesDir, fileName);
251
+ for (const [modelName, code] of Object.entries(servicesByModel)) {
252
+ const filePath = path.join(servicesDir, `${modelName}Service.ts`);
672
253
  // eslint-disable-next-line no-await-in-loop
673
- await (0, generationRegistry_1.writeGeneratedFile)(filePath, serviceCode, { force: !!(opts === null || opts === void 0 ? void 0 : opts.force), skipOnConflict: !!(opts === null || opts === void 0 ? void 0 : opts.skipOnConflict) });
254
+ 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) });
674
255
  }
675
256
  // eslint-disable-next-line no-console
676
257
  console.log('\n' + colors_1.colors.green('Service files generated successfully!') + '\n');