@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,389 +37,646 @@ exports.ControllerGenerator = 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 controllerTemplates_1 = require("./templates/controllerTemplates");
41
40
  const generationRegistry_1 = require("../utils/generationRegistry");
42
41
  const colors_1 = require("../utils/colors");
42
+ const configTypes_1 = require("../types/configTypes");
43
+ const childEntityUtils_1 = require("../utils/childEntityUtils");
44
+ const typeUtils_1 = require("../utils/typeUtils");
43
45
  const constants_1 = require("../utils/constants");
44
46
  class ControllerGenerator {
45
- hasPermissions(config) {
46
- if (config.modules) {
47
- return Object.values(config.modules).some(module => module.permissions && module.permissions.length > 0);
47
+ getHttpDecorator(method) {
48
+ switch (method.toUpperCase()) {
49
+ case 'GET': return 'Get';
50
+ case 'POST': return 'Post';
51
+ case 'PUT': return 'Put';
52
+ case 'PATCH': return 'Patch';
53
+ case 'DELETE': return 'Delete';
54
+ default: return 'Get';
48
55
  }
49
- const module = config;
50
- return !!(module.permissions && module.permissions.length > 0);
51
56
  }
52
- getActionPermissions(moduleName, moduleConfig) {
53
- if (!moduleConfig.permissions || !moduleConfig.actions) {
54
- return {};
55
- }
56
- const actionPermissions = {};
57
- Object.keys(moduleConfig.actions).forEach(action => {
58
- actionPermissions[action] = [];
59
- });
60
- (moduleConfig.permissions || []).forEach(permission => {
61
- (permission.actions || []).forEach(action => {
62
- if (actionPermissions[action]) {
63
- actionPermissions[action].push(permission.role);
64
- }
65
- });
66
- });
67
- return actionPermissions;
68
- }
69
- shouldGenerateMethod(action, roles) {
70
- return !roles.includes('none');
57
+ parseUseCase(useCase) {
58
+ const [model, action] = useCase.split(':');
59
+ return { model, action };
71
60
  }
72
- needsUserParam(roles) {
73
- return roles.length > 0 && !roles.includes('all');
61
+ /**
62
+ * Normalize auth config to an array of roles
63
+ */
64
+ normalizeAuth(auth) {
65
+ if (!auth)
66
+ return [];
67
+ if (Array.isArray(auth))
68
+ return auth;
69
+ return [auth];
74
70
  }
75
- getHttpMethodName(_httpMethod, action) {
76
- return action;
71
+ /**
72
+ * Check if auth config includes owner permission
73
+ */
74
+ hasOwnerAuth(auth) {
75
+ const roles = this.normalizeAuth(auth);
76
+ return roles.includes(constants_1.AUTH_ROLES.OWNER);
77
77
  }
78
- getHttpDecorator(httpMethod) {
79
- switch ((httpMethod || 'GET').toUpperCase()) {
80
- case 'GET':
81
- return 'Get';
82
- case 'POST':
83
- return 'Post';
84
- case 'PUT':
85
- return 'Put';
86
- case 'PATCH':
87
- return 'Patch';
88
- case 'DELETE':
89
- return 'Delete';
90
- default:
91
- return 'Get';
78
+ /**
79
+ * Generate pre-fetch authentication/authorization check code.
80
+ * This runs before fetching the entity and validates authentication and role-based access.
81
+ * @param auth - The auth requirement: 'all', 'authenticated', 'owner', role names, or array of roles
82
+ * @returns Code string for the auth check, or empty string if no check needed
83
+ */
84
+ generateAuthCheck(auth) {
85
+ const roles = this.normalizeAuth(auth);
86
+ if (roles.length === 0 || (roles.length === 1 && roles[0] === constants_1.AUTH_ROLES.ALL)) {
87
+ return ''; // No check needed - public access
88
+ }
89
+ // If only 'authenticated' is specified
90
+ if (roles.length === 1 && roles[0] === constants_1.AUTH_ROLES.AUTHENTICATED) {
91
+ return `if (!context.request.user) {
92
+ throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
93
+ }`;
94
+ }
95
+ // If only 'owner' is specified - just require authentication here
96
+ // (owner check happens post-fetch)
97
+ if (roles.length === 1 && roles[0] === constants_1.AUTH_ROLES.OWNER) {
98
+ return `if (!context.request.user) {
99
+ throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
100
+ }`;
92
101
  }
102
+ // Filter out 'owner' and 'all' for role checks (owner is checked post-fetch)
103
+ const roleChecks = roles.filter(r => r !== constants_1.AUTH_ROLES.OWNER && r !== constants_1.AUTH_ROLES.ALL && r !== constants_1.AUTH_ROLES.AUTHENTICATED);
104
+ const hasOwner = roles.includes(constants_1.AUTH_ROLES.OWNER);
105
+ const hasAuthenticated = roles.includes(constants_1.AUTH_ROLES.AUTHENTICATED);
106
+ // If we have role checks or owner, we need authentication
107
+ if (roleChecks.length > 0 || hasOwner) {
108
+ if (roleChecks.length === 0) {
109
+ // Only owner (and maybe authenticated) - just require auth
110
+ return `if (!context.request.user) {
111
+ throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
112
+ }`;
113
+ }
114
+ if (roleChecks.length === 1 && !hasOwner) {
115
+ // Single role check
116
+ return `if (!context.request.user) {
117
+ throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
118
+ }
119
+ if (context.request.user.role !== '${roleChecks[0]}') {
120
+ throw new Error('${constants_1.AUTH_ERRORS.INSUFFICIENT_PERMISSIONS}: ${roleChecks[0]} role required');
121
+ }`;
122
+ }
123
+ // Multiple roles OR owner - use OR logic
124
+ // If owner is included, we can't fully check here (post-fetch), so we just require auth
125
+ // and mark that owner check should happen later
126
+ if (hasOwner) {
127
+ // With owner: require auth, role check will be combined with owner check post-fetch
128
+ return `if (!context.request.user) {
129
+ throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
130
+ }`;
131
+ }
132
+ // Multiple roles without owner - check if user has ANY of the roles
133
+ const roleConditions = roleChecks.map(r => `context.request.user.role === '${r}'`).join(' || ');
134
+ return `if (!context.request.user) {
135
+ throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
93
136
  }
94
- getMethodCallParams(action, entityName) {
95
- const dtoType = entityName ? `${entityName}DTO` : 'any';
96
- switch (action) {
97
- case 'list':
98
- return 'page, limit';
99
- case 'get':
100
- case 'getById':
101
- return 'id';
102
- case 'create':
103
- return `context.request.body as ${dtoType}`;
104
- case 'update':
105
- return `id, context.request.body as ${dtoType}`;
106
- case 'delete':
107
- return 'id';
108
- default:
109
- return '/* custom params */';
137
+ if (!(${roleConditions})) {
138
+ throw new Error('${constants_1.AUTH_ERRORS.INSUFFICIENT_PERMISSIONS}: one of [${roleChecks.join(', ')}] role required');
139
+ }`;
140
+ }
141
+ // Only 'authenticated' in the mix
142
+ if (hasAuthenticated) {
143
+ return `if (!context.request.user) {
144
+ throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
145
+ }`;
110
146
  }
147
+ return '';
111
148
  }
112
- generateParameterExtractions(action) {
113
- switch (action) {
114
- case 'list':
115
- return `
116
- // Extract pagination from URL parameters
117
- const page = parseInt(context.request.parameters.page as string) || 1;
118
- const limit = parseInt(context.request.parameters.limit as string) || 10;`;
119
- case 'get':
120
- case 'getById':
121
- case 'update':
122
- case 'delete':
149
+ /**
150
+ * Generate post-fetch authorization check for owner validation.
151
+ * This runs after fetching the entity and validates ownership.
152
+ * Used for READ operations (get, list) where we check after fetch.
153
+ * For child entities, uses getResourceOwner() since result has no ownerId.
154
+ */
155
+ generatePostFetchOwnerCheck(auth, resultVar = 'result', useCaseVar, childInfo) {
156
+ const roles = this.normalizeAuth(auth);
157
+ if (!roles.includes(constants_1.AUTH_ROLES.OWNER)) {
158
+ return ''; // No owner check needed
159
+ }
160
+ const bypassRoles = roles.filter(r => r !== constants_1.AUTH_ROLES.OWNER && r !== constants_1.AUTH_ROLES.ALL && r !== constants_1.AUTH_ROLES.AUTHENTICATED);
161
+ // Child entities don't have ownerId on result; resolve via getResourceOwner
162
+ if (childInfo && useCaseVar) {
163
+ if (bypassRoles.length === 0) {
123
164
  return `
124
- const id = parseInt(context.request.parameters.id as string);
125
- if (isNaN(id)) {
126
- throw new Error('Invalid ID parameter');
165
+ // Owner validation (post-fetch for reads, via parent)
166
+ const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(${resultVar}.id);
167
+ if (resourceOwnerId === null) {
168
+ throw new Error('Resource not found');
169
+ }
170
+ if (resourceOwnerId !== context.request.user?.id) {
171
+ throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
172
+ }`;
173
+ }
174
+ const bypassConditions = bypassRoles.map(r => `context.request.user?.role === '${r}'`).join(' || ');
175
+ return `
176
+ // Owner validation (post-fetch for reads, via parent, bypassed for: ${bypassRoles.join(', ')})
177
+ const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(${resultVar}.id);
178
+ if (resourceOwnerId === null) {
179
+ throw new Error('Resource not found');
180
+ }
181
+ const isOwner = resourceOwnerId === context.request.user?.id;
182
+ const hasPrivilegedRole = ${bypassConditions};
183
+ if (!isOwner && !hasPrivilegedRole) {
184
+ throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
127
185
  }`;
128
- case 'create':
129
- return ''; // No additional parameter extraction needed for create
130
- default:
131
- return '';
132
186
  }
187
+ // Root entity: result has ownerId
188
+ if (bypassRoles.length === 0) {
189
+ return `
190
+ // Owner validation (post-fetch for reads)
191
+ if (${resultVar}.ownerId !== context.request.user?.id) {
192
+ throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
193
+ }`;
194
+ }
195
+ const bypassConditions = bypassRoles.map(r => `context.request.user?.role === '${r}'`).join(' || ');
196
+ return `
197
+ // Owner validation (post-fetch for reads, bypassed for: ${bypassRoles.join(', ')})
198
+ const isOwner = ${resultVar}.ownerId === context.request.user?.id;
199
+ const hasPrivilegedRole = ${bypassConditions};
200
+ if (!isOwner && !hasPrivilegedRole) {
201
+ throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
202
+ }`;
203
+ }
204
+ /**
205
+ * Generate pre-mutation authorization check for owner validation.
206
+ * This runs BEFORE the mutation to prevent unauthorized changes.
207
+ * Used for WRITE operations (update, delete).
208
+ * @param auth - The auth requirement
209
+ * @param useCaseVar - The use case variable name
210
+ * @returns Code string for the pre-mutation owner check, or empty string if not needed
211
+ */
212
+ generatePreMutationOwnerCheck(auth, useCaseVar = 'useCase') {
213
+ const roles = this.normalizeAuth(auth);
214
+ if (!roles.includes(constants_1.AUTH_ROLES.OWNER)) {
215
+ return ''; // No owner check needed
216
+ }
217
+ // Get non-owner roles for the bypass check
218
+ const bypassRoles = roles.filter(r => r !== constants_1.AUTH_ROLES.OWNER && r !== constants_1.AUTH_ROLES.ALL && r !== constants_1.AUTH_ROLES.AUTHENTICATED);
219
+ if (bypassRoles.length === 0) {
220
+ // Only owner - strict ownership check
221
+ return `
222
+ // Pre-mutation owner validation
223
+ const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(input.id);
224
+ if (resourceOwnerId === null) {
225
+ throw new Error('Resource not found');
226
+ }
227
+ if (resourceOwnerId !== context.request.user?.id) {
228
+ throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
229
+ }
230
+ `;
231
+ }
232
+ // Owner OR other roles - bypass if user has a privileged role
233
+ const bypassConditions = bypassRoles.map(r => `context.request.user?.role === '${r}'`).join(' || ');
234
+ return `
235
+ // Pre-mutation owner validation (bypassed for: ${bypassRoles.join(', ')})
236
+ const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(input.id);
237
+ if (resourceOwnerId === null) {
238
+ throw new Error('Resource not found');
239
+ }
240
+ const isOwner = resourceOwnerId === context.request.user?.id;
241
+ const hasPrivilegedRole = ${bypassConditions};
242
+ if (!isOwner && !hasPrivilegedRole) {
243
+ throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
133
244
  }
134
- generateMethodImplementation(action, entityName, hasUserParam, actions) {
135
- // Check if we have action handlers
136
- if (actions && actions[action] && actions[action].handlers) {
137
- const handlers = actions[action].handlers;
138
- const entityLower = entityName.toLowerCase();
139
- // Filter handlers that apply to this model
140
- const modelHandlers = handlers.filter(h => h.startsWith(`${entityLower}:`) || h.startsWith(`${entityName}:`));
141
- if (modelHandlers.length === 0) {
142
- return `// TODO: No valid handlers found for action ${action}. Use ${entityName}:default:${action} or ${entityName}:customMethodName format.`;
245
+ `;
246
+ }
247
+ generateApiEndpointMethod(endpoint, resourceName, useCasesConfig, childInfo) {
248
+ var _a;
249
+ const { model, action } = this.parseUseCase(endpoint.useCase);
250
+ const methodName = action;
251
+ const decorator = this.getHttpDecorator(endpoint.method);
252
+ const useCaseVar = `${model.toLowerCase()}UseCase`;
253
+ const inputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Input`;
254
+ const outputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Output`;
255
+ const useCaseDef = (_a = useCasesConfig[model]) === null || _a === void 0 ? void 0 : _a[action];
256
+ const isVoidOutput = !(useCaseDef === null || useCaseDef === void 0 ? void 0 : useCaseDef.output) || useCaseDef.output === 'void';
257
+ const dtoImports = new Set();
258
+ const voidOutputDtos = new Set();
259
+ dtoImports.add(`${model}${(0, typeUtils_1.capitalize)(action)}`);
260
+ if (isVoidOutput) {
261
+ voidOutputDtos.add(`${model}${(0, typeUtils_1.capitalize)(action)}`);
262
+ }
263
+ // Generate auth check (pre-fetch)
264
+ const authCheck = this.generateAuthCheck(endpoint.auth);
265
+ const authLine = authCheck ? `\n ${authCheck}\n` : '';
266
+ const hasOwner = this.hasOwnerAuth(endpoint.auth);
267
+ // Build parsing logic
268
+ // For create: root gets ownerId from user, child gets parentId from URL params
269
+ let parseLogic;
270
+ if (action === 'list') {
271
+ parseLogic = `const input = ${inputClass}.parse(context.request.parameters);`;
272
+ }
273
+ else if (action === 'get' || action === 'delete') {
274
+ parseLogic = `const input = ${inputClass}.parse({ id: context.request.parameters.id });`;
275
+ }
276
+ else if (action === 'create') {
277
+ if (childInfo) {
278
+ parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ${childInfo.parentIdField}: context.request.parameters.${childInfo.parentIdField} });`;
279
+ }
280
+ else {
281
+ parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ownerId: context.request.user?.id });`;
143
282
  }
144
- const userExtraction = hasUserParam ? controllerTemplates_1.controllerTemplates.userExtraction : '';
145
- const userParam = hasUserParam ? ', user' : '';
146
- // Generate parameter extraction based on action type
147
- const paramExtractions = this.generateParameterExtractions(action);
148
- // Generate step-by-step calls for each handler
149
- const handlerCalls = modelHandlers.map((handler, index) => {
150
- const parts = handler.split(':');
151
- let methodName;
152
- let params;
153
- if (parts.length === 3 && parts[1] === 'default') {
154
- // Format: modelname:default:action
155
- const actionName = parts[2];
156
- methodName = actionName === 'getById' ? 'get' : actionName; // Use 'get' instead of 'getById'
157
- // For default handlers, use extracted parameters
158
- params = this.getMethodCallParams(actionName, entityName) + userParam;
283
+ }
284
+ else if (action === 'update') {
285
+ parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, id: context.request.parameters.id });`;
286
+ }
287
+ else {
288
+ parseLogic = `const input = ${inputClass}.parse(context.request.body || {});`;
289
+ }
290
+ // Generate owner checks:
291
+ // - For mutations (update, delete): PRE-mutation check (before operation)
292
+ // - For reads (get): POST-fetch check (after fetching)
293
+ const isMutation = action === 'update' || action === 'delete';
294
+ const isRead = action === 'get';
295
+ // Pre-mutation owner check for write operations
296
+ const preMutationOwnerCheck = (hasOwner && isMutation)
297
+ ? this.generatePreMutationOwnerCheck(endpoint.auth, useCaseVar)
298
+ : '';
299
+ // Post-fetch owner check for read operations only
300
+ const postFetchOwnerCheck = (hasOwner && isRead)
301
+ ? this.generatePostFetchOwnerCheck(endpoint.auth, 'result', useCaseVar, childInfo)
302
+ : '';
303
+ // Generate output transformation based on action
304
+ let outputTransform;
305
+ if (isVoidOutput || action === 'delete') {
306
+ outputTransform = `return result;`;
307
+ }
308
+ else if (action === 'list') {
309
+ outputTransform = `return ${outputClass}.from(result);`;
310
+ }
311
+ else {
312
+ outputTransform = `return ${outputClass}.from(result);`;
313
+ }
314
+ const useCaseArgs = (hasOwner && action === 'list')
315
+ ? 'input, context.request.user?.id as number'
316
+ : 'input';
317
+ const method = ` @${decorator}('${endpoint.path}')
318
+ async ${methodName}(context: IContext): Promise<any> {${authLine}
319
+ ${parseLogic}${preMutationOwnerCheck}
320
+ const result = await this.${useCaseVar}.${action}(${useCaseArgs});${postFetchOwnerCheck}
321
+ ${outputTransform}
322
+ }`;
323
+ return { method, dtoImports, voidOutputDtos };
324
+ }
325
+ generateWebPageMethod(page, resourceName, layout, methodIndex, childInfo, withChildChildren) {
326
+ const method = page.method || 'GET';
327
+ const decorator = this.getHttpDecorator(method);
328
+ const dtoImports = new Set();
329
+ // Generate unique method name by appending method type for POST routes
330
+ const pathSegments = page.path.split('/').filter(Boolean);
331
+ let baseMethodName = pathSegments.length === 0
332
+ ? 'index'
333
+ : pathSegments.map((seg, idx) => {
334
+ if (seg.startsWith(':')) {
335
+ return 'By' + (0, typeUtils_1.capitalize)(seg.slice(1));
159
336
  }
160
- else if (parts.length === 2) {
161
- // Format: modelname:custommethod
162
- methodName = parts[1];
163
- // For custom handlers, pass result from previous handler (or null) and context
164
- const prevResult = index === 0 ? 'null' : `result${index}`;
165
- params = `${prevResult}, context${userParam}`;
337
+ return idx === 0 ? seg : (0, typeUtils_1.capitalize)(seg);
338
+ }).join('');
339
+ // Append method suffix for POST to avoid duplicates
340
+ const methodName = method === 'POST' ? `${baseMethodName}Submit` : baseMethodName;
341
+ // Generate auth check (pre-fetch)
342
+ const authCheck = this.generateAuthCheck(page.auth);
343
+ const authLine = authCheck ? `\n ${authCheck}\n` : '';
344
+ // For GET requests with views (display pages)
345
+ if (method === 'GET' && page.view) {
346
+ const pageLayout = this.resolveLayout(page.layout, layout);
347
+ const renderDecorator = pageLayout
348
+ ? `\n @Render("${page.view}", "${pageLayout}")`
349
+ : `\n @Render("${page.view}")`;
350
+ if (page.useCase) {
351
+ const { model, action } = this.parseUseCase(page.useCase);
352
+ const useCaseVar = `${model.toLowerCase()}UseCase`;
353
+ const inputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Input`;
354
+ dtoImports.add(`${model}${(0, typeUtils_1.capitalize)(action)}`);
355
+ const hasOwner = this.hasOwnerAuth(page.auth);
356
+ let parseLogic;
357
+ if (page.path.includes(':id')) {
358
+ parseLogic = `const input = ${inputClass}.parse({ id: context.request.parameters.id });`;
359
+ }
360
+ else if (action === 'list') {
361
+ parseLogic = `const input = ${inputClass}.parse(context.request.parameters);`;
166
362
  }
167
363
  else {
168
- return `// Invalid handler format: ${handler}`;
364
+ parseLogic = `const input = ${inputClass}.parse({});`;
169
365
  }
170
- const isLast = index === modelHandlers.length - 1;
171
- const resultVar = isLast ? 'result' : `result${index + 1}`;
172
- return `const ${resultVar} = await this.${entityLower}Service.${methodName}(${params});`;
173
- }).join('\n ');
174
- // For multiple handlers, return the last result
175
- const returnStatement = '\n return result;';
176
- return `${userExtraction}${paramExtractions}
177
- ${handlerCalls}${returnStatement}`;
178
- }
179
- // Fallback to existing template lookup
180
- const template = controllerTemplates_1.controllerTemplates.methodImplementations[action];
181
- if (!template) {
182
- return ` // TODO: Implement ${action} method\n return {} as any;`;
183
- }
184
- const userExtraction = hasUserParam ? controllerTemplates_1.controllerTemplates.userExtraction : '';
185
- const userParam = hasUserParam ? ', user' : '';
186
- return template
187
- .replace(/{{ENTITY_NAME}}/g, entityName)
188
- .replace(/{{ENTITY_LOWER}}/g, entityName.toLowerCase())
189
- .replace(/{{USER_EXTRACTION}}/g, userExtraction)
190
- .replace(/{{USER_PARAM}}/g, userParam);
191
- }
192
- replaceTemplateVars(template, variables) {
193
- let result = template;
194
- Object.entries(variables).forEach(([key, value]) => {
195
- const regex = new RegExp(`{{${key}}}`, 'g');
196
- result = result.replace(regex, String(value));
197
- });
198
- return result;
199
- }
200
- generateControllerMethod(endpoint, entityName, roles, hasPermissions, kind, actions) {
201
- const methodName = kind === 'web' ? this.getWebMethodName(endpoint) : this.getHttpMethodName(endpoint.method, endpoint.action);
202
- const httpDecorator = kind === 'web' ? 'Get' : this.getHttpDecorator(endpoint.method);
203
- const needsUser = this.needsUserParam(roles);
204
- const methodImplementation = this.generateMethodImplementation(endpoint.action, entityName, needsUser, actions);
205
- const returnType = this.getReturnType(endpoint.action, entityName);
206
- const renderDecorator = kind === 'web' && endpoint.view
207
- ? `\n @Render("${endpoint.view}"${endpoint.layout ? `, "${endpoint.layout}"` : ', "main_view"'})`
208
- : '';
209
- const variables = {
210
- METHOD_NAME: methodName,
211
- HTTP_DECORATOR: httpDecorator,
212
- ENDPOINT_PATH: endpoint.path,
213
- METHOD_IMPLEMENTATION: methodImplementation,
214
- RETURN_TYPE: returnType,
215
- RENDER_DECORATOR: renderDecorator
216
- };
217
- return this.replaceTemplateVars(controllerTemplates_1.controllerTemplates.controllerMethod, variables);
366
+ const isReadAction = action === 'get' || action === 'list';
367
+ const postFetchOwnerCheck = (hasOwner && isReadAction)
368
+ ? this.generatePostFetchOwnerCheck(page.auth, 'result', useCaseVar, childInfo)
369
+ : '';
370
+ // For get + withChild: load child entities and merge into result for template
371
+ const loadChildBlocks = [];
372
+ let returnExpr;
373
+ if (childInfo) {
374
+ returnExpr = `{ ...result, ${childInfo.parentIdField}: context.request.parameters.${childInfo.parentIdField} }`;
375
+ }
376
+ else if ((withChildChildren === null || withChildChildren === void 0 ? void 0 : withChildChildren.length) && action === 'get') {
377
+ const childKeys = [];
378
+ for (const child of withChildChildren) {
379
+ const childVar = child.childEntityName.charAt(0).toLowerCase() + child.childEntityName.slice(1);
380
+ const childItemsKey = `${childVar}Items`;
381
+ childKeys.push(childItemsKey);
382
+ loadChildBlocks.push(`const ${childItemsKey} = await this.${childVar}Service.listByParent(result.id);`);
383
+ }
384
+ returnExpr = `{ ...result, ${childKeys.map(k => `${k}: ${k}`).join(', ')} }`;
385
+ }
386
+ else {
387
+ returnExpr = 'result';
388
+ }
389
+ const useCaseArgs = (hasOwner && action === 'list')
390
+ ? 'input, context.request.user?.id as number'
391
+ : 'input';
392
+ const loadChildCode = loadChildBlocks.length ? '\n ' + loadChildBlocks.join('\n ') + '\n ' : '';
393
+ const methodCode = `${renderDecorator}
394
+ @${decorator}('${page.path}')
395
+ async ${methodName}(context: IContext): Promise<any> {${authLine}
396
+ ${parseLogic}
397
+ const result = await this.${useCaseVar}.${action}(${useCaseArgs});${postFetchOwnerCheck}${loadChildCode}
398
+ return ${returnExpr};
399
+ }`;
400
+ return { method: methodCode, dtoImports };
401
+ }
402
+ else {
403
+ // No use case - just render view (e.g. create form)
404
+ // Child entities need the parent ID from URL params for link rendering
405
+ const emptyFormData = childInfo
406
+ ? `{ formData: {}, ${childInfo.parentIdField}: context.request.parameters.${childInfo.parentIdField} }`
407
+ : '{ formData: {} }';
408
+ const methodCode = `${renderDecorator}
409
+ @${decorator}('${page.path}')
410
+ async ${methodName}(context: IContext): Promise<any> {${authLine}
411
+ return ${emptyFormData};
412
+ }`;
413
+ return { method: methodCode, dtoImports };
414
+ }
415
+ }
416
+ else if (method === 'POST' && page.useCase) {
417
+ // POST request - form submission
418
+ const { model, action } = this.parseUseCase(page.useCase);
419
+ const useCaseVar = `${model.toLowerCase()}UseCase`;
420
+ const inputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Input`;
421
+ dtoImports.add(`${model}${(0, typeUtils_1.capitalize)(action)}`);
422
+ let parseLogic;
423
+ if (page.path.includes(':id')) {
424
+ parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, id: context.request.parameters.id });`;
425
+ }
426
+ else if (action === 'create') {
427
+ if (childInfo) {
428
+ parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ${childInfo.parentIdField}: context.request.parameters.${childInfo.parentIdField} });`;
429
+ }
430
+ else {
431
+ parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ownerId: context.request.user?.id });`;
432
+ }
433
+ }
434
+ else {
435
+ parseLogic = `const input = ${inputClass}.parse(context.request.body);`;
436
+ }
437
+ // Generate PRE-mutation owner check for update/delete actions (POST requests)
438
+ const hasOwner = this.hasOwnerAuth(page.auth);
439
+ const isMutation = action === 'update' || action === 'delete';
440
+ const preMutationOwnerCheck = (hasOwner && isMutation)
441
+ ? this.generatePreMutationOwnerCheck(page.auth, useCaseVar)
442
+ : '';
443
+ // Handle onSuccess and onError strategies
444
+ const onSuccessHandler = this.generateOnSuccessHandler(page);
445
+ const onErrorHandler = this.generateOnErrorHandler(page);
446
+ const methodCode = ` @${decorator}('${page.path}')
447
+ async ${methodName}(context: IContext): Promise<any> {${authLine}
448
+ try {
449
+ ${parseLogic}${preMutationOwnerCheck}
450
+ const result = await this.${useCaseVar}.${action}(input);
451
+ ${onSuccessHandler}
452
+ return { success: true, data: result };
453
+ } catch (error) {
454
+ ${onErrorHandler}
455
+ throw error;
218
456
  }
219
- getWebMethodName(endpoint) {
220
- const base = this.getHttpMethodName(endpoint.method, endpoint.action);
221
- const suffix = this.getPathSuffix(endpoint.path);
222
- return `${base}${suffix}`;
457
+ }`;
458
+ return { method: methodCode, dtoImports };
459
+ }
460
+ const methodCode = ` @${decorator}('${page.path}')
461
+ async ${methodName}(context: IContext): Promise<any> {
462
+ // TODO: Implement ${methodName}
463
+ return {};
464
+ }`;
465
+ return { method: methodCode, dtoImports };
223
466
  }
224
- getPathSuffix(routePath) {
225
- if (!routePath || routePath === '/')
226
- return 'Index';
227
- const segments = routePath.split('/').filter(Boolean);
228
- const parts = segments.map(seg => {
229
- if (seg.startsWith(':')) {
230
- const name = seg.slice(1);
231
- return 'By' + name.charAt(0).toUpperCase() + name.slice(1);
232
- }
233
- const cleaned = seg.replace(/[^a-zA-Z0-9]+/g, ' ')
234
- .split(' ')
235
- .filter(Boolean)
236
- .map(s => s.charAt(0).toUpperCase() + s.slice(1))
237
- .join('');
238
- return cleaned || 'Index';
239
- });
240
- return parts.join('');
467
+ generateOnSuccessHandler(page) {
468
+ if (!page.onSuccess)
469
+ return '// Success';
470
+ const handlers = [];
471
+ if (page.onSuccess.toast) {
472
+ handlers.push(`// Toast: ${page.onSuccess.toast}`);
473
+ }
474
+ if (page.onSuccess.redirect) {
475
+ const redirectPath = page.onSuccess.redirect.replace(':id', '${result.id}');
476
+ handlers.push(`// Redirect: ${redirectPath}`);
477
+ }
478
+ if (page.onSuccess.back) {
479
+ handlers.push('// Navigate back');
480
+ }
481
+ return handlers.join('\n ') || '// Success';
241
482
  }
242
- getReturnType(action, entityName) {
243
- switch (action) {
244
- case 'list':
245
- return `${entityName}[]`;
246
- case 'get':
247
- case 'create':
248
- case 'update':
249
- case 'empty':
250
- return entityName;
251
- case 'delete':
252
- return '{ success: boolean; message: string }';
253
- default:
254
- return 'any';
483
+ generateOnErrorHandler(page) {
484
+ if (!page.onError)
485
+ return '// Error occurred';
486
+ const handlers = [];
487
+ if (page.onError.stay) {
488
+ handlers.push('// Stay on page');
255
489
  }
490
+ if (page.onError.toast) {
491
+ handlers.push(`// Error toast: ${page.onError.toast}`);
492
+ }
493
+ return handlers.join('\n ') || '// Error occurred';
256
494
  }
257
495
  /**
258
- * Generate controller for a single config (api or routes)
496
+ * Sort routes so static paths are registered before parameterized ones.
497
+ * This prevents parameterized routes (e.g. /:id) from catching requests
498
+ * meant for static routes (e.g. /create).
259
499
  */
260
- generateControllerForModelWithConfig(model, moduleName, moduleConfig, cfg, hasGlobalPermissions, kind) {
261
- var _a;
262
- const isApi = kind === 'api';
263
- const entityName = model.name;
264
- const entityLower = entityName.toLowerCase();
265
- // Determine if we should generate a controller for this model
266
- const configModel = (cfg === null || cfg === void 0 ? void 0 : cfg.model) || (moduleConfig.models && moduleConfig.models[0] ? moduleConfig.models[0].name : null);
267
- const topLevelMatches = !configModel || configModel === entityName || configModel.toLowerCase() === entityLower;
268
- // Also check if any endpoints specifically target this model (Option A)
269
- const hasEndpointForThisModel = ((_a = cfg === null || cfg === void 0 ? void 0 : cfg.endpoints) === null || _a === void 0 ? void 0 : _a.some(endpoint => {
270
- const endpointModel = endpoint.model || configModel;
271
- return endpointModel === entityName || (endpointModel === null || endpointModel === void 0 ? void 0 : endpointModel.toLowerCase()) === entityLower;
272
- })) || false;
273
- const shouldGenerateForThisModel = topLevelMatches || hasEndpointForThisModel;
274
- if (!shouldGenerateForThisModel) {
275
- return '';
276
- }
277
- const controllerBase = (cfg.prefix || `/${isApi ? 'api/' : ''}${entityLower}`).replace(/\/$/, '');
278
- const actionPermissions = this.getActionPermissions(moduleName, moduleConfig);
279
- const hasPermissions = hasGlobalPermissions && !!(moduleConfig.permissions && moduleConfig.permissions.length > 0);
280
- // Filter endpoints that apply to this model (Option A)
281
- const modelEndpoints = (cfg.endpoints || []).filter(endpoint => {
282
- const endpointModel = endpoint.model || (cfg === null || cfg === void 0 ? void 0 : cfg.model) || (moduleConfig.models && moduleConfig.models[0] ? moduleConfig.models[0].name : null);
283
- return endpointModel === entityName || (endpointModel === null || endpointModel === void 0 ? void 0 : endpointModel.toLowerCase()) === entityLower;
500
+ sortRoutesBySpecificity(routes) {
501
+ return [...routes].sort((a, b) => {
502
+ const aSegments = a.path.split('/').filter(Boolean);
503
+ const bSegments = b.path.split('/').filter(Boolean);
504
+ const aParamCount = aSegments.filter(s => s.startsWith(':')).length;
505
+ const bParamCount = bSegments.filter(s => s.startsWith(':')).length;
506
+ return aParamCount - bParamCount;
284
507
  });
285
- const controllerMethods = modelEndpoints
286
- .filter(endpoint => {
287
- const roles = actionPermissions[endpoint.action] || [];
288
- return this.shouldGenerateMethod(endpoint.action, roles);
289
- })
290
- .map(endpoint => {
291
- const roles = actionPermissions[endpoint.action] || [];
292
- return this.generateControllerMethod(endpoint, entityName, roles, hasPermissions, kind, moduleConfig.actions);
293
- })
294
- .join('\n\n');
295
- return controllerMethods;
296
508
  }
297
509
  /**
298
- * Generate controller for a model (handles both single config and array) - Option D
510
+ * Resolve layout from YAML value.
511
+ * - undefined => use fallback (if provided)
512
+ * - "none" or "" => no layout
513
+ * - other values => use explicit layout name
299
514
  */
300
- generateControllerForModel(model, moduleName, moduleConfig, hasGlobalPermissions, kind) {
301
- const isApi = kind === 'api';
302
- const cfgRaw = isApi ? moduleConfig.api : moduleConfig.routes;
303
- const entityName = model.name;
304
- const entityLower = entityName.toLowerCase();
305
- // Support both single config and array (Option D)
306
- let configs = [];
307
- if (!cfgRaw) {
308
- // No config - generate defaults for web routes only
309
- if (!isApi) {
310
- configs = [{
311
- prefix: `/${entityLower}`,
312
- endpoints: [
313
- { path: '/', action: 'list', method: 'GET', view: `${entityLower}List` },
314
- { path: '/:id', action: 'get', method: 'GET', view: `${entityLower}Detail` },
315
- { path: '/create', action: 'empty', method: 'GET', view: `${entityLower}Create` },
316
- { path: '/:id/edit', action: 'get', method: 'GET', view: `${entityLower}Update` }
317
- ]
318
- }];
319
- }
320
- else {
321
- return '';
322
- }
515
+ resolveLayout(layout, fallback) {
516
+ if (layout === undefined) {
517
+ return fallback;
323
518
  }
324
- else if (Array.isArray(cfgRaw)) {
325
- configs = cfgRaw;
519
+ const normalized = layout.trim();
520
+ if (!normalized || normalized.toLowerCase() === 'none') {
521
+ return undefined;
326
522
  }
327
- else {
328
- configs = [cfgRaw];
329
- }
330
- // For web routes, force GET method
331
- if (!isApi) {
332
- configs = configs.map(cfg => ({
333
- ...cfg,
334
- endpoints: (cfg.endpoints || []).map(e => ({ ...e, method: 'GET' }))
335
- }));
336
- }
337
- // Generate methods from all configs
338
- const allMethods = configs
339
- .map(cfg => this.generateControllerForModelWithConfig(model, moduleName, moduleConfig, cfg, hasGlobalPermissions, kind))
340
- .filter(methods => methods.trim() !== '')
341
- .join('\n\n');
342
- if (!allMethods.trim()) {
343
- return '';
344
- }
345
- // Use the first config for controller base path (or default)
346
- const firstConfig = configs[0];
347
- const controllerBase = ((firstConfig === null || firstConfig === void 0 ? void 0 : firstConfig.prefix) || `/${isApi ? 'api/' : ''}${entityLower}`).replace(/\/$/, '');
348
- const controllerClass = this.replaceTemplateVars(controllerTemplates_1.controllerTemplates.controllerClass, {
349
- CONTROLLER_NAME: `${entityName}${isApi ? 'ApiController' : 'WebController'}`,
350
- ENTITY_NAME: entityName,
351
- ENTITY_LOWER: entityLower,
352
- CONTROLLER_BASE: controllerBase,
353
- CONTROLLER_METHODS: allMethods
354
- });
355
- return this.replaceTemplateVars(controllerTemplates_1.controllerFileTemplate, {
356
- ENTITY_NAME: entityName,
357
- JWT_IMPORT: '',
358
- CONTROLLER_CLASS: controllerClass
523
+ return normalized;
524
+ }
525
+ generateApiController(resourceName, prefix, endpoints, useCasesConfig, childInfo) {
526
+ const controllerName = `${resourceName}ApiController`;
527
+ // Determine which use cases and DTOs are referenced
528
+ const useCaseModels = new Set();
529
+ const allDtoImports = new Set();
530
+ const allVoidOutputDtos = new Set();
531
+ const methods = [];
532
+ const sortedEndpoints = this.sortRoutesBySpecificity(endpoints);
533
+ sortedEndpoints.forEach(endpoint => {
534
+ const { model } = this.parseUseCase(endpoint.useCase);
535
+ useCaseModels.add(model);
536
+ const { method, dtoImports, voidOutputDtos } = this.generateApiEndpointMethod(endpoint, resourceName, useCasesConfig, childInfo);
537
+ methods.push(method);
538
+ dtoImports.forEach(d => allDtoImports.add(d));
539
+ voidOutputDtos.forEach(d => allVoidOutputDtos.add(d));
359
540
  });
541
+ // Generate imports
542
+ const useCaseImports = Array.from(useCaseModels)
543
+ .map(model => `import { ${model}UseCase } from '../../application/useCases/${model}UseCase';`)
544
+ .join('\n');
545
+ const dtoImportStatements = Array.from(allDtoImports)
546
+ .map(dto => {
547
+ if (allVoidOutputDtos.has(dto)) {
548
+ return `import { ${dto}Input } from '../../application/dto/${dto}';`;
549
+ }
550
+ return `import { ${dto}Input, ${dto}Output } from '../../application/dto/${dto}';`;
551
+ })
552
+ .join('\n');
553
+ // Generate constructor parameters
554
+ const constructorParams = Array.from(useCaseModels)
555
+ .map(model => `private ${model.toLowerCase()}UseCase: ${model}UseCase`)
556
+ .join(',\n ');
557
+ return `import { Controller, Get, Post, Put, Delete, type IContext } from '@currentjs/router';
558
+ ${useCaseImports}
559
+ ${dtoImportStatements}
560
+
561
+ @Controller('${prefix}')
562
+ export class ${controllerName} {
563
+ constructor(
564
+ ${constructorParams}
565
+ ) {}
566
+
567
+ ${methods.join('\n\n')}
568
+ }`;
360
569
  }
361
- generateController(moduleName, moduleConfig, hasGlobalPermissions, kind) {
362
- // Legacy method for backward compatibility - use first model
363
- if (!moduleConfig.models || moduleConfig.models.length === 0) {
364
- return '';
570
+ generateWebController(resourceName, prefix, layout, pages, config, childInfo) {
571
+ const controllerName = `${resourceName}WebController`;
572
+ // Child entities of this resource (for withChild). Only root entities can have withChild.
573
+ const withChildChildren = childInfo ? [] : (0, childEntityUtils_1.getChildrenOfParent)(config, resourceName);
574
+ // Determine which use cases and DTOs are referenced
575
+ const useCaseModels = new Set();
576
+ const allDtoImports = new Set();
577
+ const methods = [];
578
+ const sortedPages = this.sortRoutesBySpecificity(pages);
579
+ sortedPages.forEach((page, index) => {
580
+ var _a, _b;
581
+ if (page.useCase) {
582
+ const { model } = this.parseUseCase(page.useCase);
583
+ useCaseModels.add(model);
584
+ }
585
+ const { model, action } = page.useCase ? this.parseUseCase(page.useCase) : { model: '', action: '' };
586
+ const useCaseWithChild = model && action && ((_b = (_a = config.useCases[model]) === null || _a === void 0 ? void 0 : _a[action]) === null || _b === void 0 ? void 0 : _b.withChild) === true;
587
+ const withChildForThisPage = useCaseWithChild && action === 'get' && withChildChildren.length > 0 ? withChildChildren : undefined;
588
+ const { method, dtoImports } = this.generateWebPageMethod(page, resourceName, layout, index, childInfo, withChildForThisPage);
589
+ methods.push(method);
590
+ dtoImports.forEach(d => allDtoImports.add(d));
591
+ });
592
+ // Determine if any page actually uses withChild (only then inject child services)
593
+ const needsChildServices = withChildChildren.length > 0 && sortedPages.some(page => {
594
+ var _a, _b;
595
+ if (!page.useCase)
596
+ return false;
597
+ const { model: m, action: a } = this.parseUseCase(page.useCase);
598
+ return a === 'get' && ((_b = (_a = config.useCases[m]) === null || _a === void 0 ? void 0 : _a[a]) === null || _b === void 0 ? void 0 : _b.withChild) === true;
599
+ });
600
+ // Constructor: use cases + child entity services when withChild is actually used
601
+ const serviceImports = [];
602
+ const constructorParams = [];
603
+ Array.from(useCaseModels).forEach(model => {
604
+ constructorParams.push(`private ${model.toLowerCase()}UseCase: ${model}UseCase`);
605
+ });
606
+ if (needsChildServices) {
607
+ withChildChildren.forEach(child => {
608
+ const childVar = child.childEntityName.charAt(0).toLowerCase() + child.childEntityName.slice(1);
609
+ serviceImports.push(`import { ${child.childEntityName}Service } from '../../application/services/${child.childEntityName}Service';`);
610
+ constructorParams.push(`private ${childVar}Service: ${child.childEntityName}Service`);
611
+ });
365
612
  }
366
- return this.generateControllerForModel(moduleConfig.models[0], moduleName, moduleConfig, hasGlobalPermissions, kind);
613
+ const useCaseImports = Array.from(useCaseModels)
614
+ .map(model => `import { ${model}UseCase } from '../../application/useCases/${model}UseCase';`)
615
+ .join('\n');
616
+ const dtoImportStatements = Array.from(allDtoImports)
617
+ .map(dto => `import { ${dto}Input } from '../../application/dto/${dto}';`)
618
+ .join('\n');
619
+ const constructorBlock = constructorParams.length > 0
620
+ ? `constructor(
621
+ ${constructorParams.join(',\n ')}
622
+ ) {}`
623
+ : 'constructor() {}';
624
+ return `import { Controller, Get, Post, Render, type IContext } from '@currentjs/router';
625
+ ${useCaseImports}
626
+ ${serviceImports.join('\n')}
627
+ ${dtoImportStatements}
628
+
629
+ @Controller('${prefix}')
630
+ export class ${controllerName} {
631
+ ${constructorBlock}
632
+
633
+ ${methods.join('\n\n')}
634
+ }`;
367
635
  }
368
- generateFromYamlFile(yamlFilePath) {
369
- const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
370
- const config = (0, yaml_1.parse)(yamlContent);
636
+ generateFromConfig(config) {
371
637
  const result = {};
372
- const hasGlobalPermissions = this.hasPermissions(config);
373
- if (config.modules) {
374
- Object.entries(config.modules).forEach(([moduleName, moduleConfig]) => {
375
- if (moduleConfig.models && moduleConfig.models.length > 0) {
376
- // Generate controllers for each model
377
- moduleConfig.models.forEach(model => {
378
- const apiControllerCode = this.generateControllerForModel(model, moduleName, moduleConfig, hasGlobalPermissions, 'api');
379
- if (apiControllerCode)
380
- result[`${model.name}Api`] = apiControllerCode;
381
- const webControllerCode = this.generateControllerForModel(model, moduleName, moduleConfig, hasGlobalPermissions, 'web');
382
- if (webControllerCode)
383
- result[`${model.name}Web`] = webControllerCode;
384
- });
385
- }
638
+ const childEntityMap = (0, childEntityUtils_1.buildChildEntityMap)(config);
639
+ // Generate API controllers
640
+ if (config.api) {
641
+ Object.entries(config.api).forEach(([resourceName, resourceConfig]) => {
642
+ const childInfo = childEntityMap.get(resourceName);
643
+ const code = this.generateApiController(resourceName, resourceConfig.prefix, resourceConfig.endpoints, config.useCases, childInfo);
644
+ result[`${resourceName}Api`] = code;
386
645
  });
387
646
  }
388
- else {
389
- const moduleName = 'Module';
390
- const moduleConfig = config;
391
- if (moduleConfig.models && moduleConfig.models.length > 0) {
392
- // Generate controllers for each model
393
- moduleConfig.models.forEach(model => {
394
- const apiControllerCode = this.generateControllerForModel(model, moduleName, moduleConfig, hasGlobalPermissions, 'api');
395
- if (apiControllerCode)
396
- result[`${model.name}Api`] = apiControllerCode;
397
- const webControllerCode = this.generateControllerForModel(model, moduleName, moduleConfig, hasGlobalPermissions, 'web');
398
- if (webControllerCode)
399
- result[`${model.name}Web`] = webControllerCode;
400
- });
401
- }
647
+ // Generate Web controllers
648
+ if (config.web) {
649
+ Object.entries(config.web).forEach(([resourceName, resourceConfig]) => {
650
+ const childInfo = childEntityMap.get(resourceName);
651
+ const moduleLayout = this.resolveLayout(resourceConfig.layout, 'main_view');
652
+ const code = this.generateWebController(resourceName, resourceConfig.prefix, moduleLayout, resourceConfig.pages, config, childInfo);
653
+ result[`${resourceName}Web`] = code;
654
+ });
402
655
  }
403
656
  return result;
404
657
  }
405
- async generateAndSaveFiles(yamlFilePath = constants_1.COMMON_FILES.APP_YAML, outputDir = 'infrastructure', opts) {
658
+ generateFromYamlFile(yamlFilePath) {
406
659
  const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
407
660
  const config = (0, yaml_1.parse)(yamlContent);
408
- const hasGlobalPermissions = this.hasPermissions(config);
409
- const controllers = this.generateFromYamlFile(yamlFilePath);
410
- const generatedControllerPaths = [];
411
- const controllersDir = path.join(outputDir, 'controllers');
661
+ if (!(0, configTypes_1.isValidModuleConfig)(config)) {
662
+ throw new Error('Configuration does not match new module format. Expected domain/useCases/api/web structure.');
663
+ }
664
+ return this.generateFromConfig(config);
665
+ }
666
+ async generateAndSaveFiles(yamlFilePath, moduleDir, opts) {
667
+ const controllersByName = this.generateFromYamlFile(yamlFilePath);
668
+ const controllersDir = path.join(moduleDir, 'infrastructure', 'controllers');
412
669
  fs.mkdirSync(controllersDir, { recursive: true });
413
- for (const [moduleName, controllerCode] of Object.entries(controllers)) {
414
- const fileName = `${moduleName}Controller.ts`;
415
- const filePath = path.join(controllersDir, fileName);
670
+ const generatedPaths = [];
671
+ for (const [name, code] of Object.entries(controllersByName)) {
672
+ const filePath = path.join(controllersDir, `${name}Controller.ts`);
416
673
  // eslint-disable-next-line no-await-in-loop
417
- await (0, generationRegistry_1.writeGeneratedFile)(filePath, controllerCode, { force: !!(opts === null || opts === void 0 ? void 0 : opts.force), skipOnConflict: !!(opts === null || opts === void 0 ? void 0 : opts.skipOnConflict) });
418
- generatedControllerPaths.push(filePath);
674
+ 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) });
675
+ generatedPaths.push(filePath);
419
676
  }
420
677
  // eslint-disable-next-line no-console
421
678
  console.log('\n' + colors_1.colors.green('Controller files generated successfully!') + '\n');
422
- return generatedControllerPaths;
679
+ return generatedPaths;
423
680
  }
424
681
  }
425
682
  exports.ControllerGenerator = ControllerGenerator;