@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,640 +37,417 @@ 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
|
|
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.
|
|
47
|
+
this.availableAggregates = new Map();
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
-
this.
|
|
50
|
-
models.forEach(model => {
|
|
51
|
-
this.availableModels.add(model.name);
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
isRelationshipField(field) {
|
|
55
|
-
return this.availableModels.has(field.type);
|
|
56
|
-
}
|
|
57
|
-
getForeignKeyFieldName(field) {
|
|
58
|
-
// Convention: fieldName + 'Id' (e.g., owner -> ownerId)
|
|
59
|
-
return field.name + 'Id';
|
|
60
|
-
}
|
|
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
|
-
}
|
|
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] = [];
|
|
76
|
-
});
|
|
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}');
|
|
49
|
+
mapType(yamlType) {
|
|
50
|
+
return (0, typeUtils_1.mapType)(yamlType, this.availableAggregates);
|
|
139
51
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}`;
|
|
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';
|
|
52
|
+
getDefaultHandlerReturnType(actionName, modelName) {
|
|
53
|
+
switch (actionName) {
|
|
169
54
|
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
|
-
}
|
|
178
|
-
}
|
|
179
|
-
generateReturnType(action, entityName) {
|
|
180
|
-
switch (action) {
|
|
181
|
-
case 'list':
|
|
182
|
-
return `${entityName}[]`;
|
|
183
55
|
case 'get':
|
|
184
|
-
case 'getById':
|
|
185
|
-
case 'create':
|
|
186
56
|
case 'update':
|
|
187
|
-
return
|
|
57
|
+
return modelName;
|
|
188
58
|
case 'delete':
|
|
189
59
|
return '{ success: boolean; message: string }';
|
|
60
|
+
case 'list':
|
|
61
|
+
return `{ items: ${modelName}[]; total: number; page: number; limit: number }`;
|
|
190
62
|
default:
|
|
191
|
-
return
|
|
63
|
+
return modelName;
|
|
192
64
|
}
|
|
193
65
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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}`);
|
|
66
|
+
buildHandlerContextMap(modelName, useCases) {
|
|
67
|
+
const contextMap = new Map();
|
|
68
|
+
Object.entries(useCases).forEach(([actionName, useCaseConfig]) => {
|
|
69
|
+
const inputDtoType = `${modelName}${(0, typeUtils_1.capitalize)(actionName)}Input`;
|
|
70
|
+
let useCaseReturnType;
|
|
71
|
+
if (useCaseConfig.output === 'void') {
|
|
72
|
+
useCaseReturnType = '{ success: boolean; message: string }';
|
|
73
|
+
}
|
|
74
|
+
else if (actionName === 'list') {
|
|
75
|
+
useCaseReturnType = `{ items: ${modelName}[]; total: number; page: number; limit: number }`;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
useCaseReturnType = modelName;
|
|
79
|
+
}
|
|
80
|
+
useCaseConfig.handlers.forEach((handler, index) => {
|
|
81
|
+
const isFirst = index === 0;
|
|
82
|
+
const isLast = index === useCaseConfig.handlers.length - 1;
|
|
83
|
+
let prevHandlerReturnType = null;
|
|
84
|
+
if (!isFirst) {
|
|
85
|
+
const prevHandler = useCaseConfig.handlers[index - 1];
|
|
86
|
+
if (prevHandler.startsWith('default:')) {
|
|
87
|
+
prevHandlerReturnType = this.getDefaultHandlerReturnType(prevHandler.replace('default:', ''), modelName);
|
|
265
88
|
}
|
|
266
89
|
else {
|
|
267
|
-
|
|
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
|
-
}
|
|
90
|
+
prevHandlerReturnType = modelName;
|
|
279
91
|
}
|
|
280
92
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
93
|
+
const context = {
|
|
94
|
+
actionName,
|
|
95
|
+
index,
|
|
96
|
+
isFirst,
|
|
97
|
+
isLast,
|
|
98
|
+
prevHandlerReturnType,
|
|
99
|
+
inputDtoType,
|
|
100
|
+
useCaseReturnType,
|
|
101
|
+
inputConfig: useCaseConfig.input
|
|
102
|
+
};
|
|
103
|
+
const existing = contextMap.get(handler) || [];
|
|
104
|
+
existing.push(context);
|
|
105
|
+
contextMap.set(handler, existing);
|
|
106
|
+
});
|
|
288
107
|
});
|
|
289
|
-
return
|
|
290
|
-
}
|
|
291
|
-
extractFunctionName(filePath) {
|
|
292
|
-
const parts = filePath.split('/');
|
|
293
|
-
const fileName = parts[parts.length - 1];
|
|
294
|
-
return fileName;
|
|
108
|
+
return contextMap;
|
|
295
109
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return 'page, limit';
|
|
300
|
-
case 'get':
|
|
301
|
-
return 'id';
|
|
302
|
-
case 'create':
|
|
303
|
-
return 'userData';
|
|
304
|
-
case 'update':
|
|
305
|
-
return 'id, updates';
|
|
306
|
-
case 'delete':
|
|
307
|
-
return 'id';
|
|
308
|
-
default:
|
|
309
|
-
return '/* custom params */';
|
|
310
|
-
}
|
|
110
|
+
deriveInputType(contexts) {
|
|
111
|
+
const inputTypes = [...new Set(contexts.map(c => c.inputDtoType))];
|
|
112
|
+
return inputTypes.join(' | ');
|
|
311
113
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
114
|
+
deriveCustomHandlerTypes(contexts, modelName) {
|
|
115
|
+
const inputTypes = [...new Set(contexts.map(c => c.inputDtoType))];
|
|
116
|
+
const resultTypeParts = new Set();
|
|
117
|
+
contexts.forEach(c => {
|
|
118
|
+
if (c.isFirst) {
|
|
119
|
+
resultTypeParts.add('null');
|
|
120
|
+
}
|
|
121
|
+
if (c.prevHandlerReturnType) {
|
|
122
|
+
resultTypeParts.add(c.prevHandlerReturnType);
|
|
320
123
|
}
|
|
321
|
-
return aRequired ? -1 : 1; // Required fields come first
|
|
322
124
|
});
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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`);
|
|
125
|
+
const returnTypeParts = new Set();
|
|
126
|
+
contexts.forEach(c => {
|
|
127
|
+
if (c.isLast) {
|
|
128
|
+
returnTypeParts.add(c.useCaseReturnType);
|
|
340
129
|
}
|
|
341
130
|
else {
|
|
342
|
-
|
|
131
|
+
returnTypeParts.add(modelName);
|
|
343
132
|
}
|
|
344
133
|
});
|
|
345
|
-
return
|
|
134
|
+
return {
|
|
135
|
+
inputType: inputTypes.join(' | '),
|
|
136
|
+
resultType: [...resultTypeParts].join(' | '),
|
|
137
|
+
returnType: [...returnTypeParts].join(' | ')
|
|
138
|
+
};
|
|
346
139
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
140
|
+
getInputDtoFields(inputConfig, aggregateConfig, childInfo) {
|
|
141
|
+
const fields = new Set();
|
|
142
|
+
if (!inputConfig)
|
|
143
|
+
return fields;
|
|
144
|
+
if (!inputConfig.identifier && !inputConfig.partial) {
|
|
145
|
+
fields.add(childInfo ? childInfo.parentIdField : 'ownerId');
|
|
350
146
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
}`;
|
|
147
|
+
if (inputConfig.from) {
|
|
148
|
+
let fieldNames = Object.keys(aggregateConfig.fields)
|
|
149
|
+
.filter(f => !aggregateConfig.fields[f].auto && f !== 'id');
|
|
150
|
+
if (inputConfig.pick && inputConfig.pick.length > 0) {
|
|
151
|
+
fieldNames = fieldNames.filter(f => inputConfig.pick.includes(f));
|
|
367
152
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
? await this.${relatedModelLower}Store.getById(${entityLower}Data.${foreignKeyName})
|
|
371
|
-
: null;`;
|
|
153
|
+
if (inputConfig.omit && inputConfig.omit.length > 0) {
|
|
154
|
+
fieldNames = fieldNames.filter(f => !inputConfig.omit.includes(f));
|
|
372
155
|
}
|
|
373
|
-
|
|
374
|
-
|
|
156
|
+
fieldNames.forEach(f => fields.add(f));
|
|
157
|
+
}
|
|
158
|
+
if (inputConfig.add) {
|
|
159
|
+
Object.keys(inputConfig.add).forEach(f => fields.add(f));
|
|
160
|
+
}
|
|
161
|
+
return fields;
|
|
375
162
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
163
|
+
computeDtoFieldsForHandler(contexts, aggregateConfig, childInfo) {
|
|
164
|
+
const fieldSets = contexts.map(ctx => this.getInputDtoFields(ctx.inputConfig, aggregateConfig, childInfo));
|
|
165
|
+
if (fieldSets.length === 0) {
|
|
166
|
+
return new Set(Object.keys(aggregateConfig.fields).filter(f => !aggregateConfig.fields[f].auto && f !== 'id'));
|
|
379
167
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const relationshipLoading = this.generateRelationshipLoading(moduleConfig, entityName);
|
|
387
|
-
if (relationshipLoading) {
|
|
388
|
-
code = relationshipLoading + '\n ';
|
|
168
|
+
const result = new Set(fieldSets[0]);
|
|
169
|
+
for (let i = 1; i < fieldSets.length; i++) {
|
|
170
|
+
for (const field of result) {
|
|
171
|
+
if (!fieldSets[i].has(field)) {
|
|
172
|
+
result.delete(field);
|
|
173
|
+
}
|
|
389
174
|
}
|
|
390
175
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
generateListHandler(modelName, storeName, hasPagination) {
|
|
179
|
+
const returnType = `{ items: ${modelName}[]; total: number; page: number; limit: number }`;
|
|
180
|
+
if (hasPagination) {
|
|
181
|
+
return ` async list(page: number = 1, limit: number = 20, ownerId?: number): Promise<${returnType}> {
|
|
182
|
+
const [items, total] = await Promise.all([
|
|
183
|
+
this.${storeName}.getPaginated(page, limit, ownerId),
|
|
184
|
+
this.${storeName}.count(ownerId)
|
|
185
|
+
]);
|
|
186
|
+
return { items, total, page, limit };
|
|
187
|
+
}`;
|
|
188
|
+
}
|
|
189
|
+
return ` async list(ownerId?: number): Promise<${returnType}> {
|
|
190
|
+
const items = await this.${storeName}.getAll(ownerId);
|
|
191
|
+
return { items, total: items.length, page: 1, limit: items.length };
|
|
192
|
+
}`;
|
|
193
|
+
}
|
|
194
|
+
generateGetHandler(modelName, storeName, entityLower) {
|
|
195
|
+
return ` async get(id: number): Promise<${modelName}> {
|
|
196
|
+
const ${entityLower} = await this.${storeName}.getById(id);
|
|
197
|
+
if (!${entityLower}) {
|
|
198
|
+
throw new Error('${modelName} not found');
|
|
199
|
+
}
|
|
200
|
+
return ${entityLower};
|
|
201
|
+
}`;
|
|
202
|
+
}
|
|
203
|
+
generateCreateHandler(modelName, storeName, entityLower, aggregateConfig, childInfo, inputType, dtoFields) {
|
|
204
|
+
const firstArgField = childInfo ? childInfo.parentIdField : 'ownerId';
|
|
205
|
+
const fields = Object.entries(aggregateConfig.fields)
|
|
206
|
+
.filter(([fieldName, fieldConfig]) => !fieldConfig.auto && fieldName !== 'id')
|
|
207
|
+
.sort((a, b) => {
|
|
208
|
+
const aIsAggRef = (0, typeUtils_1.isAggregateReference)(a[1].type, this.availableAggregates);
|
|
209
|
+
const bIsAggRef = (0, typeUtils_1.isAggregateReference)(b[1].type, this.availableAggregates);
|
|
210
|
+
const aRequired = a[1].required !== false && !aIsAggRef;
|
|
211
|
+
const bRequired = b[1].required !== false && !bIsAggRef;
|
|
212
|
+
if (aRequired === bRequired)
|
|
213
|
+
return 0;
|
|
214
|
+
return aRequired ? -1 : 1;
|
|
215
|
+
});
|
|
216
|
+
const fieldArgs = fields.map(([fieldName, fieldConfig]) => {
|
|
217
|
+
if (!dtoFields.has(fieldName)) {
|
|
218
|
+
return 'undefined';
|
|
219
|
+
}
|
|
220
|
+
if ((0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates)) {
|
|
221
|
+
return `input.${fieldName} != null ? ({ id: input.${fieldName} } as unknown as ${fieldConfig.type}) : undefined`;
|
|
222
|
+
}
|
|
223
|
+
if (fieldConfig.type === 'enum' && fieldConfig.values && fieldConfig.values.length > 0) {
|
|
224
|
+
const enumTypeName = `${modelName}${(0, typeUtils_1.capitalize)(fieldName)}`;
|
|
225
|
+
return `input.${fieldName} as ${enumTypeName}`;
|
|
226
|
+
}
|
|
227
|
+
return `input.${fieldName}`;
|
|
228
|
+
}).join(', ');
|
|
229
|
+
const constructorArgs = `input.${firstArgField}, ${fieldArgs}`;
|
|
230
|
+
return ` async create(input: ${inputType}): Promise<${modelName}> {
|
|
231
|
+
const ${entityLower} = new ${modelName}(0, ${constructorArgs});
|
|
232
|
+
return await this.${storeName}.insert(${entityLower});
|
|
233
|
+
}`;
|
|
234
|
+
}
|
|
235
|
+
generateUpdateHandler(modelName, storeName, aggregateConfig, inputType, dtoFields) {
|
|
236
|
+
const setterCalls = Object.entries(aggregateConfig.fields)
|
|
237
|
+
.filter(([fieldName, fieldConfig]) => !fieldConfig.auto && fieldName !== 'id' && dtoFields.has(fieldName))
|
|
238
|
+
.map(([fieldName, fieldConfig]) => {
|
|
239
|
+
const methodName = `set${(0, typeUtils_1.capitalize)(fieldName)}`;
|
|
240
|
+
if ((0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates)) {
|
|
241
|
+
return ` if (input.${fieldName} !== undefined) {
|
|
242
|
+
existing${modelName}.${methodName}(input.${fieldName} != null ? ({ id: input.${fieldName} } as unknown as ${fieldConfig.type}) : undefined);
|
|
400
243
|
}`;
|
|
401
244
|
}
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
return `if (
|
|
405
|
-
existing${
|
|
245
|
+
if (fieldConfig.type === 'enum' && fieldConfig.values && fieldConfig.values.length > 0) {
|
|
246
|
+
const enumTypeName = `${modelName}${(0, typeUtils_1.capitalize)(fieldName)}`;
|
|
247
|
+
return ` if (input.${fieldName} !== undefined) {
|
|
248
|
+
existing${modelName}.${methodName}(input.${fieldName} as ${enumTypeName});
|
|
406
249
|
}`;
|
|
407
250
|
}
|
|
251
|
+
return ` if (input.${fieldName} !== undefined) {
|
|
252
|
+
existing${modelName}.${methodName}(input.${fieldName});
|
|
253
|
+
}`;
|
|
408
254
|
})
|
|
409
|
-
.join('\n
|
|
410
|
-
return
|
|
255
|
+
.join('\n');
|
|
256
|
+
return ` async update(id: number, input: ${inputType}): Promise<${modelName}> {
|
|
257
|
+
const existing${modelName} = await this.${storeName}.getById(id);
|
|
258
|
+
if (!existing${modelName}) {
|
|
259
|
+
throw new Error('${modelName} not found');
|
|
411
260
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
});
|
|
418
|
-
return result;
|
|
261
|
+
|
|
262
|
+
${setterCalls}
|
|
263
|
+
|
|
264
|
+
return await this.${storeName}.update(id, existing${modelName});
|
|
265
|
+
}`;
|
|
419
266
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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;
|
|
267
|
+
generateDeleteHandler(modelName, storeName) {
|
|
268
|
+
return ` async delete(id: number): Promise<{ success: boolean; message: string }> {
|
|
269
|
+
const success = await this.${storeName}.softDelete(id);
|
|
270
|
+
if (!success) {
|
|
271
|
+
throw new Error('${modelName} not found or could not be deleted');
|
|
448
272
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
273
|
+
return { success: true, message: '${modelName} deleted successfully' };
|
|
274
|
+
}`;
|
|
275
|
+
}
|
|
276
|
+
generateDefaultHandlerMethod(modelName, actionName, aggregateConfig, childInfo, inputType, dtoFields, listConfig) {
|
|
277
|
+
var _a;
|
|
278
|
+
const entityLower = modelName.toLowerCase();
|
|
279
|
+
const storeName = `${entityLower}Store`;
|
|
280
|
+
switch (actionName) {
|
|
281
|
+
case 'list':
|
|
282
|
+
return this.generateListHandler(modelName, storeName, (_a = listConfig === null || listConfig === void 0 ? void 0 : listConfig.hasPagination) !== null && _a !== void 0 ? _a : true);
|
|
283
|
+
case 'get':
|
|
284
|
+
return this.generateGetHandler(modelName, storeName, entityLower);
|
|
285
|
+
case 'create':
|
|
286
|
+
return this.generateCreateHandler(modelName, storeName, entityLower, aggregateConfig, childInfo, inputType, dtoFields);
|
|
287
|
+
case 'update':
|
|
288
|
+
return this.generateUpdateHandler(modelName, storeName, aggregateConfig, inputType, dtoFields);
|
|
289
|
+
case 'delete':
|
|
290
|
+
return this.generateDeleteHandler(modelName, storeName);
|
|
291
|
+
default:
|
|
292
|
+
return ` async ${actionName}(input: ${inputType}): Promise<${modelName}> {
|
|
293
|
+
// TODO: Implement default ${actionName} handler
|
|
294
|
+
throw new Error('Not implemented');
|
|
295
|
+
}`;
|
|
467
296
|
}
|
|
468
|
-
|
|
297
|
+
}
|
|
298
|
+
generateCustomHandlerMethod(modelName, handlerName, resultType, inputType, returnType) {
|
|
299
|
+
return ` async ${handlerName}(result: ${resultType}, input: ${inputType}): Promise<${returnType}> {
|
|
300
|
+
// TODO: Implement custom ${handlerName} handler
|
|
301
|
+
// This method receives the result from the previous handler (or null if first)
|
|
302
|
+
// and the input context
|
|
303
|
+
return result;
|
|
304
|
+
}`;
|
|
305
|
+
}
|
|
306
|
+
collectHandlers(useCases) {
|
|
307
|
+
const handlers = new Set();
|
|
308
|
+
Object.values(useCases).forEach(useCaseConfig => {
|
|
309
|
+
useCaseConfig.handlers.forEach(handler => {
|
|
310
|
+
handlers.add(handler);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
return handlers;
|
|
314
|
+
}
|
|
315
|
+
generateListByParentMethod(modelName, childInfo) {
|
|
316
|
+
if (!childInfo)
|
|
469
317
|
return '';
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
318
|
+
const storeVar = `${modelName.toLowerCase()}Store`;
|
|
319
|
+
return `
|
|
320
|
+
async listByParent(parentId: number): Promise<${modelName}[]> {
|
|
321
|
+
return await this.${storeVar}.getByParentId(parentId);
|
|
322
|
+
}`;
|
|
323
|
+
}
|
|
324
|
+
generateGetResourceOwnerMethod(modelName) {
|
|
325
|
+
const storeVar = `${modelName.toLowerCase()}Store`;
|
|
326
|
+
return `
|
|
327
|
+
/**
|
|
328
|
+
* Get the owner ID of a resource by its ID.
|
|
329
|
+
* Used for pre-mutation authorization checks.
|
|
330
|
+
*/
|
|
331
|
+
async getResourceOwner(id: number): Promise<number | null> {
|
|
332
|
+
return await this.${storeVar}.getResourceOwner(id);
|
|
333
|
+
}`;
|
|
334
|
+
}
|
|
335
|
+
generateService(modelName, useCases, aggregateConfig, childInfo) {
|
|
336
|
+
const serviceName = `${modelName}Service`;
|
|
337
|
+
const storeName = `${modelName}Store`;
|
|
338
|
+
const storeVar = `${modelName.toLowerCase()}Store`;
|
|
339
|
+
// Build handler-to-context map for type inference
|
|
340
|
+
const handlerContextMap = this.buildHandlerContextMap(modelName, useCases);
|
|
341
|
+
// Collect all unique handlers
|
|
342
|
+
const handlers = this.collectHandlers(useCases);
|
|
343
|
+
// Collect DTO types needed for imports
|
|
344
|
+
const dtoTypes = new Set();
|
|
345
|
+
const enumTypeNames = new Set();
|
|
346
|
+
// Generate methods for each handler
|
|
347
|
+
const methods = [];
|
|
348
|
+
handlers.forEach(handler => {
|
|
349
|
+
var _a, _b;
|
|
350
|
+
const contexts = handlerContextMap.get(handler) || [];
|
|
351
|
+
if (handler.startsWith('default:')) {
|
|
352
|
+
const actionName = handler.replace('default:', '');
|
|
353
|
+
const inputType = this.deriveInputType(contexts);
|
|
354
|
+
const dtoFields = this.computeDtoFieldsForHandler(contexts, aggregateConfig, childInfo);
|
|
355
|
+
if (actionName !== 'list' && actionName !== 'get' && actionName !== 'delete') {
|
|
356
|
+
contexts.forEach(c => dtoTypes.add(c.inputDtoType));
|
|
500
357
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
358
|
+
if (actionName === 'create' || actionName === 'update') {
|
|
359
|
+
for (const [fieldName, fieldConfig] of Object.entries(aggregateConfig.fields)) {
|
|
360
|
+
if (fieldConfig.type === 'enum' && fieldConfig.values && fieldConfig.values.length > 0 && dtoFields.has(fieldName)) {
|
|
361
|
+
enumTypeNames.add(`${modelName}${(0, typeUtils_1.capitalize)(fieldName)}`);
|
|
362
|
+
}
|
|
506
363
|
}
|
|
507
364
|
}
|
|
365
|
+
const listConfig = actionName === 'list'
|
|
366
|
+
? { hasPagination: !!((_b = (_a = contexts[0]) === null || _a === void 0 ? void 0 : _a.inputConfig) === null || _b === void 0 ? void 0 : _b.pagination) }
|
|
367
|
+
: undefined;
|
|
368
|
+
methods.push(this.generateDefaultHandlerMethod(modelName, actionName, aggregateConfig, childInfo, inputType, dtoFields, listConfig));
|
|
508
369
|
}
|
|
509
370
|
else {
|
|
510
|
-
|
|
371
|
+
const { inputType, resultType, returnType } = this.deriveCustomHandlerTypes(contexts, modelName);
|
|
372
|
+
contexts.forEach(c => dtoTypes.add(c.inputDtoType));
|
|
373
|
+
methods.push(this.generateCustomHandlerMethod(modelName, handler, resultType, inputType, returnType));
|
|
511
374
|
}
|
|
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
375
|
});
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
ENTITY_NAME: entityName,
|
|
576
|
-
PERMISSIONS_IMPORT: hasPermissions ? "\nimport type { AuthenticatedUser } from '@currentjs/router';" : '',
|
|
577
|
-
CUSTOM_IMPORTS: customImports + foreignStoreImports,
|
|
578
|
-
SERVICE_CLASS: serviceClass
|
|
579
|
-
});
|
|
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 '';
|
|
376
|
+
const listByParentMethod = this.generateListByParentMethod(modelName, childInfo);
|
|
377
|
+
if (listByParentMethod) {
|
|
378
|
+
methods.push(listByParentMethod);
|
|
585
379
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const relationshipFields = model.fields.filter(f => this.isRelationshipField(f));
|
|
590
|
-
if (relationshipFields.length === 0) {
|
|
591
|
-
return '';
|
|
380
|
+
const getResourceOwnerMethod = this.generateGetResourceOwnerMethod(modelName);
|
|
381
|
+
if (getResourceOwnerMethod) {
|
|
382
|
+
methods.push(getResourceOwnerMethod);
|
|
592
383
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
384
|
+
// Collect imports for aggregate reference types used in fields
|
|
385
|
+
const aggRefImports = Object.entries(aggregateConfig.fields)
|
|
386
|
+
.filter(([, fc]) => (0, typeUtils_1.isAggregateReference)(fc.type, this.availableAggregates) && fc.type !== modelName)
|
|
387
|
+
.map(([, fc]) => `import { ${fc.type} } from '../../domain/entities/${fc.type}';`)
|
|
388
|
+
.filter((imp, idx, arr) => arr.indexOf(imp) === idx);
|
|
389
|
+
const aggRefImportStr = aggRefImports.length > 0 ? '\n' + aggRefImports.join('\n') : '';
|
|
390
|
+
// Generate DTO import statements
|
|
391
|
+
const dtoImports = [...dtoTypes].map(dtoType => {
|
|
392
|
+
const fileSuffix = dtoType.replace(modelName, '').replace('Input', '');
|
|
393
|
+
return `import { ${dtoType} } from '../dto/${modelName}${fileSuffix}';`;
|
|
394
|
+
}).join('\n');
|
|
395
|
+
const dtoImportStr = dtoImports ? '\n' + dtoImports : '';
|
|
396
|
+
const entityImports = [modelName, ...enumTypeNames].join(', ');
|
|
397
|
+
return `import { Injectable } from '../../../../system';
|
|
398
|
+
import { ${entityImports} } from '../../domain/entities/${modelName}';${aggRefImportStr}${dtoImportStr}
|
|
399
|
+
import { ${storeName} } from '../../infrastructure/stores/${storeName}';
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Service layer for ${modelName}
|
|
403
|
+
* Contains business logic handlers that can be composed in use cases
|
|
404
|
+
*/
|
|
405
|
+
@Injectable()
|
|
406
|
+
export class ${serviceName} {
|
|
407
|
+
constructor(
|
|
408
|
+
private ${storeVar}: ${storeName}
|
|
409
|
+
) {}
|
|
410
|
+
|
|
411
|
+
${methods.join('\n\n')}
|
|
412
|
+
}`;
|
|
598
413
|
}
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
414
|
+
generateFromConfig(config) {
|
|
415
|
+
const result = {};
|
|
416
|
+
// Collect all aggregates
|
|
417
|
+
if (config.domain.aggregates) {
|
|
418
|
+
Object.entries(config.domain.aggregates).forEach(([name, aggConfig]) => {
|
|
419
|
+
this.availableAggregates.set(name, aggConfig);
|
|
420
|
+
});
|
|
603
421
|
}
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
});
|
|
422
|
+
const childEntityMap = (0, childEntityUtils_1.buildChildEntityMap)(config);
|
|
423
|
+
// Generate a Service file for each model
|
|
424
|
+
Object.entries(config.useCases).forEach(([modelName, useCases]) => {
|
|
425
|
+
const aggregateConfig = this.availableAggregates.get(modelName);
|
|
426
|
+
if (!aggregateConfig) {
|
|
427
|
+
console.warn(`Warning: No aggregate found for model ${modelName}`);
|
|
428
|
+
return;
|
|
624
429
|
}
|
|
430
|
+
const childInfo = childEntityMap.get(modelName);
|
|
431
|
+
result[modelName] = this.generateService(modelName, useCases, aggregateConfig, childInfo);
|
|
625
432
|
});
|
|
626
|
-
return
|
|
433
|
+
return result;
|
|
627
434
|
}
|
|
628
435
|
generateFromYamlFile(yamlFilePath) {
|
|
629
436
|
const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
|
|
630
437
|
const config = (0, yaml_1.parse)(yamlContent);
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
});
|
|
438
|
+
if (!(0, configTypes_1.isValidModuleConfig)(config)) {
|
|
439
|
+
throw new Error('Configuration does not match new module format. Expected useCases structure.');
|
|
647
440
|
}
|
|
648
|
-
|
|
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;
|
|
441
|
+
return this.generateFromConfig(config);
|
|
664
442
|
}
|
|
665
|
-
async generateAndSaveFiles(yamlFilePath
|
|
666
|
-
const
|
|
667
|
-
const servicesDir = path.join(
|
|
443
|
+
async generateAndSaveFiles(yamlFilePath, moduleDir, opts) {
|
|
444
|
+
const servicesByModel = this.generateFromYamlFile(yamlFilePath);
|
|
445
|
+
const servicesDir = path.join(moduleDir, 'application', 'services');
|
|
668
446
|
fs.mkdirSync(servicesDir, { recursive: true });
|
|
669
|
-
for (const [
|
|
670
|
-
const
|
|
671
|
-
const filePath = path.join(servicesDir, fileName);
|
|
447
|
+
for (const [modelName, code] of Object.entries(servicesByModel)) {
|
|
448
|
+
const filePath = path.join(servicesDir, `${modelName}Service.ts`);
|
|
672
449
|
// eslint-disable-next-line no-await-in-loop
|
|
673
|
-
await (0, generationRegistry_1.writeGeneratedFile)(filePath,
|
|
450
|
+
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
451
|
}
|
|
675
452
|
// eslint-disable-next-line no-console
|
|
676
453
|
console.log('\n' + colors_1.colors.green('Service files generated successfully!') + '\n');
|