@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,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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
364
|
+
parseLogic = `const input = ${inputClass}.parse({});`;
|
|
169
365
|
}
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
225
|
-
if (!
|
|
226
|
-
return '
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
325
|
-
|
|
519
|
+
const normalized = layout.trim();
|
|
520
|
+
if (!normalized || normalized.toLowerCase() === 'none') {
|
|
521
|
+
return undefined;
|
|
326
522
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
.
|
|
340
|
-
|
|
341
|
-
.
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
369
|
-
const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
|
|
370
|
-
const config = (0, yaml_1.parse)(yamlContent);
|
|
636
|
+
generateFromConfig(config) {
|
|
371
637
|
const result = {};
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
658
|
+
generateFromYamlFile(yamlFilePath) {
|
|
406
659
|
const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
|
|
407
660
|
const config = (0, yaml_1.parse)(yamlContent);
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
const filePath = path.join(controllersDir,
|
|
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,
|
|
418
|
-
|
|
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
|
|
679
|
+
return generatedPaths;
|
|
423
680
|
}
|
|
424
681
|
}
|
|
425
682
|
exports.ControllerGenerator = ControllerGenerator;
|