@currentjs/gen 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -289
- 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/commands/migratePush.d.ts +1 -0
- package/dist/commands/migratePush.js +135 -0
- package/dist/commands/migrateUpdate.d.ts +1 -0
- package/dist/commands/migrateUpdate.js +147 -0
- package/dist/commands/newGenerateAll.d.ts +4 -0
- package/dist/commands/newGenerateAll.js +336 -0
- package/dist/generators/controllerGenerator.d.ts +43 -19
- package/dist/generators/controllerGenerator.js +547 -329
- package/dist/generators/domainLayerGenerator.d.ts +21 -0
- package/dist/generators/domainLayerGenerator.js +276 -0
- package/dist/generators/dtoGenerator.d.ts +21 -0
- package/dist/generators/dtoGenerator.js +518 -0
- package/dist/generators/newControllerGenerator.d.ts +55 -0
- package/dist/generators/newControllerGenerator.js +644 -0
- package/dist/generators/newServiceGenerator.d.ts +19 -0
- package/dist/generators/newServiceGenerator.js +266 -0
- package/dist/generators/newStoreGenerator.d.ts +39 -0
- package/dist/generators/newStoreGenerator.js +408 -0
- package/dist/generators/newTemplateGenerator.d.ts +29 -0
- package/dist/generators/newTemplateGenerator.js +510 -0
- package/dist/generators/serviceGenerator.d.ts +16 -51
- package/dist/generators/serviceGenerator.js +167 -586
- package/dist/generators/storeGenerator.d.ts +35 -32
- package/dist/generators/storeGenerator.js +291 -238
- package/dist/generators/storeGeneratorV2.d.ts +31 -0
- package/dist/generators/storeGeneratorV2.js +190 -0
- 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 +15 -10
- package/dist/generators/templates/data/appYamlTemplate +5 -2
- package/dist/generators/templates/data/cursorRulesTemplate +315 -221
- package/dist/generators/templates/data/frontendScriptTemplate +76 -47
- package/dist/generators/templates/data/mainViewTemplate +1 -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/newStoreTemplates.d.ts +5 -0
- package/dist/generators/templates/newStoreTemplates.js +141 -0
- package/dist/generators/templates/storeTemplates.d.ts +1 -5
- package/dist/generators/templates/storeTemplates.js +102 -219
- package/dist/generators/templates/viewTemplates.js +1 -1
- package/dist/generators/useCaseGenerator.d.ts +13 -0
- package/dist/generators/useCaseGenerator.js +188 -0
- package/dist/types/configTypes.d.ts +148 -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/new_parts_of_migrationUtils.d.ts +0 -0
- package/dist/utils/new_parts_of_migrationUtils.js +164 -0
- package/dist/utils/typeUtils.d.ts +19 -0
- package/dist/utils/typeUtils.js +70 -0
- package/package.json +7 -3
|
@@ -37,389 +37,607 @@ 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
|
-
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
|
|
92
88
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 */';
|
|
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
|
+
}`;
|
|
110
94
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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':
|
|
123
|
-
return `
|
|
124
|
-
const id = parseInt(context.request.parameters.id as string);
|
|
125
|
-
if (isNaN(id)) {
|
|
126
|
-
throw new Error('Invalid ID parameter');
|
|
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}');
|
|
127
100
|
}`;
|
|
128
|
-
case 'create':
|
|
129
|
-
return ''; // No additional parameter extraction needed for create
|
|
130
|
-
default:
|
|
131
|
-
return '';
|
|
132
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}');
|
|
133
118
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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.`;
|
|
119
|
+
if (context.request.user.role !== '${roleChecks[0]}') {
|
|
120
|
+
throw new Error('${constants_1.AUTH_ERRORS.INSUFFICIENT_PERMISSIONS}: ${roleChecks[0]} role required');
|
|
121
|
+
}`;
|
|
143
122
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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}`;
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
return `// Invalid handler format: ${handler}`;
|
|
169
|
-
}
|
|
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}`;
|
|
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}');
|
|
136
|
+
}
|
|
137
|
+
if (!(${roleConditions})) {
|
|
138
|
+
throw new Error('${constants_1.AUTH_ERRORS.INSUFFICIENT_PERMISSIONS}: one of [${roleChecks.join(', ')}] role required');
|
|
139
|
+
}`;
|
|
178
140
|
}
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
+
}`;
|
|
183
146
|
}
|
|
184
|
-
|
|
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);
|
|
147
|
+
return '';
|
|
218
148
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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) {
|
|
164
|
+
return `
|
|
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');
|
|
223
169
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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);
|
|
170
|
+
if (resourceOwnerId !== context.request.user?.id) {
|
|
171
|
+
throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
|
|
172
|
+
}`;
|
|
232
173
|
}
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
});
|
|
240
|
-
return parts.join('');
|
|
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');
|
|
241
180
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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}');
|
|
185
|
+
}`;
|
|
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
|
+
}`;
|
|
255
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
|
+
}`;
|
|
256
203
|
}
|
|
257
204
|
/**
|
|
258
|
-
* Generate
|
|
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
|
|
259
211
|
*/
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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 '';
|
|
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
|
|
276
216
|
}
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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;
|
|
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');
|
|
296
226
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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}');
|
|
244
|
+
}
|
|
245
|
+
`;
|
|
246
|
+
}
|
|
247
|
+
generateApiEndpointMethod(endpoint, resourceName, childInfo) {
|
|
248
|
+
const { model, action } = this.parseUseCase(endpoint.useCase);
|
|
249
|
+
const methodName = action;
|
|
250
|
+
const decorator = this.getHttpDecorator(endpoint.method);
|
|
251
|
+
const useCaseVar = `${model.toLowerCase()}UseCase`;
|
|
252
|
+
const inputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Input`;
|
|
253
|
+
const outputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Output`;
|
|
254
|
+
const dtoImports = new Set();
|
|
255
|
+
dtoImports.add(`${model}${(0, typeUtils_1.capitalize)(action)}`);
|
|
256
|
+
// Generate auth check (pre-fetch)
|
|
257
|
+
const authCheck = this.generateAuthCheck(endpoint.auth);
|
|
258
|
+
const authLine = authCheck ? `\n ${authCheck}\n` : '';
|
|
259
|
+
// Build parsing logic
|
|
260
|
+
// For create: root gets ownerId from user, child gets parentId from URL params
|
|
261
|
+
let parseLogic;
|
|
262
|
+
if (action === 'list') {
|
|
263
|
+
parseLogic = `const input = ${inputClass}.parse(context.request.parameters);`;
|
|
264
|
+
}
|
|
265
|
+
else if (action === 'get' || action === 'delete') {
|
|
266
|
+
parseLogic = `const input = ${inputClass}.parse({ id: context.request.parameters.id });`;
|
|
267
|
+
}
|
|
268
|
+
else if (action === 'create') {
|
|
269
|
+
if (childInfo) {
|
|
270
|
+
parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ${childInfo.parentIdField}: context.request.parameters.${childInfo.parentIdField} });`;
|
|
319
271
|
}
|
|
320
272
|
else {
|
|
321
|
-
|
|
273
|
+
parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ownerId: context.request.user?.id });`;
|
|
322
274
|
}
|
|
323
275
|
}
|
|
324
|
-
else if (
|
|
325
|
-
|
|
276
|
+
else if (action === 'update') {
|
|
277
|
+
parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, id: context.request.parameters.id });`;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
parseLogic = `const input = ${inputClass}.parse(context.request.body || {});`;
|
|
281
|
+
}
|
|
282
|
+
// Generate owner checks:
|
|
283
|
+
// - For mutations (update, delete): PRE-mutation check (before operation)
|
|
284
|
+
// - For reads (get): POST-fetch check (after fetching)
|
|
285
|
+
const hasOwner = this.hasOwnerAuth(endpoint.auth);
|
|
286
|
+
const isMutation = action === 'update' || action === 'delete';
|
|
287
|
+
const isRead = action === 'get';
|
|
288
|
+
// Pre-mutation owner check for write operations
|
|
289
|
+
const preMutationOwnerCheck = (hasOwner && isMutation)
|
|
290
|
+
? this.generatePreMutationOwnerCheck(endpoint.auth, useCaseVar)
|
|
291
|
+
: '';
|
|
292
|
+
// Post-fetch owner check for read operations only
|
|
293
|
+
const postFetchOwnerCheck = (hasOwner && isRead)
|
|
294
|
+
? this.generatePostFetchOwnerCheck(endpoint.auth, 'result', useCaseVar, childInfo)
|
|
295
|
+
: '';
|
|
296
|
+
// Generate output transformation based on action
|
|
297
|
+
let outputTransform;
|
|
298
|
+
if (action === 'list') {
|
|
299
|
+
outputTransform = `return ${outputClass}.from(result);`;
|
|
300
|
+
}
|
|
301
|
+
else if (action === 'delete') {
|
|
302
|
+
outputTransform = `return result;`;
|
|
326
303
|
}
|
|
327
304
|
else {
|
|
328
|
-
|
|
305
|
+
outputTransform = `return ${outputClass}.from(result);`;
|
|
306
|
+
}
|
|
307
|
+
const method = ` @${decorator}('${endpoint.path}')
|
|
308
|
+
async ${methodName}(context: IContext): Promise<any> {${authLine}
|
|
309
|
+
${parseLogic}${preMutationOwnerCheck}
|
|
310
|
+
const result = await this.${useCaseVar}.${action}(input);${postFetchOwnerCheck}
|
|
311
|
+
${outputTransform}
|
|
312
|
+
}`;
|
|
313
|
+
return { method, dtoImports };
|
|
314
|
+
}
|
|
315
|
+
generateWebPageMethod(page, resourceName, layout, methodIndex, childInfo, withChildChildren) {
|
|
316
|
+
const method = page.method || 'GET';
|
|
317
|
+
const decorator = this.getHttpDecorator(method);
|
|
318
|
+
const dtoImports = new Set();
|
|
319
|
+
// Generate unique method name by appending method type for POST routes
|
|
320
|
+
const pathSegments = page.path.split('/').filter(Boolean);
|
|
321
|
+
let baseMethodName = pathSegments.length === 0
|
|
322
|
+
? 'index'
|
|
323
|
+
: pathSegments.map((seg, idx) => {
|
|
324
|
+
if (seg.startsWith(':')) {
|
|
325
|
+
return 'By' + (0, typeUtils_1.capitalize)(seg.slice(1));
|
|
326
|
+
}
|
|
327
|
+
return idx === 0 ? seg : (0, typeUtils_1.capitalize)(seg);
|
|
328
|
+
}).join('');
|
|
329
|
+
// Append method suffix for POST to avoid duplicates
|
|
330
|
+
const methodName = method === 'POST' ? `${baseMethodName}Submit` : baseMethodName;
|
|
331
|
+
// Generate auth check (pre-fetch)
|
|
332
|
+
const authCheck = this.generateAuthCheck(page.auth);
|
|
333
|
+
const authLine = authCheck ? `\n ${authCheck}\n` : '';
|
|
334
|
+
// For GET requests with views (display pages)
|
|
335
|
+
if (method === 'GET' && page.view) {
|
|
336
|
+
const renderDecorator = `\n @Render("${page.view}", "${layout}")`;
|
|
337
|
+
if (page.useCase) {
|
|
338
|
+
const { model, action } = this.parseUseCase(page.useCase);
|
|
339
|
+
const useCaseVar = `${model.toLowerCase()}UseCase`;
|
|
340
|
+
const inputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Input`;
|
|
341
|
+
dtoImports.add(`${model}${(0, typeUtils_1.capitalize)(action)}`);
|
|
342
|
+
let parseLogic;
|
|
343
|
+
if (page.path.includes(':id')) {
|
|
344
|
+
parseLogic = `const input = ${inputClass}.parse({ id: context.request.parameters.id });`;
|
|
345
|
+
}
|
|
346
|
+
else if (action === 'list') {
|
|
347
|
+
parseLogic = `const input = ${inputClass}.parse(context.request.parameters);`;
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
parseLogic = `const input = ${inputClass}.parse({});`;
|
|
351
|
+
}
|
|
352
|
+
// Generate post-fetch owner check for GET pages (reads only)
|
|
353
|
+
const hasOwner = this.hasOwnerAuth(page.auth);
|
|
354
|
+
const isReadAction = action === 'get' || action === 'list';
|
|
355
|
+
const postFetchOwnerCheck = (hasOwner && isReadAction)
|
|
356
|
+
? this.generatePostFetchOwnerCheck(page.auth, 'result', useCaseVar, childInfo)
|
|
357
|
+
: '';
|
|
358
|
+
// For get + withChild: load child entities and merge into result for template
|
|
359
|
+
const loadChildBlocks = [];
|
|
360
|
+
let returnExpr;
|
|
361
|
+
if (childInfo) {
|
|
362
|
+
returnExpr = `{ ...result, ${childInfo.parentIdField}: context.request.parameters.${childInfo.parentIdField} }`;
|
|
363
|
+
}
|
|
364
|
+
else if ((withChildChildren === null || withChildChildren === void 0 ? void 0 : withChildChildren.length) && action === 'get') {
|
|
365
|
+
const childKeys = [];
|
|
366
|
+
for (const child of withChildChildren) {
|
|
367
|
+
const childVar = child.childEntityName.charAt(0).toLowerCase() + child.childEntityName.slice(1);
|
|
368
|
+
const childItemsKey = `${childVar}Items`;
|
|
369
|
+
childKeys.push(childItemsKey);
|
|
370
|
+
loadChildBlocks.push(`const ${childItemsKey} = await this.${childVar}Service.listByParent(result.id);`);
|
|
371
|
+
}
|
|
372
|
+
returnExpr = `{ ...result, ${childKeys.map(k => `${k}: ${k}`).join(', ')} }`;
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
returnExpr = 'result';
|
|
376
|
+
}
|
|
377
|
+
const loadChildCode = loadChildBlocks.length ? '\n ' + loadChildBlocks.join('\n ') + '\n ' : '';
|
|
378
|
+
const methodCode = `${renderDecorator}
|
|
379
|
+
@${decorator}('${page.path}')
|
|
380
|
+
async ${methodName}(context: IContext): Promise<any> {${authLine}
|
|
381
|
+
${parseLogic}
|
|
382
|
+
const result = await this.${useCaseVar}.${action}(input);${postFetchOwnerCheck}${loadChildCode}
|
|
383
|
+
return ${returnExpr};
|
|
384
|
+
}`;
|
|
385
|
+
return { method: methodCode, dtoImports };
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
// No use case - just render view (e.g. create form)
|
|
389
|
+
// Child entities need the parent ID from URL params for link rendering
|
|
390
|
+
const emptyFormData = childInfo
|
|
391
|
+
? `{ formData: {}, ${childInfo.parentIdField}: context.request.parameters.${childInfo.parentIdField} }`
|
|
392
|
+
: '{ formData: {} }';
|
|
393
|
+
const methodCode = `${renderDecorator}
|
|
394
|
+
@${decorator}('${page.path}')
|
|
395
|
+
async ${methodName}(context: IContext): Promise<any> {${authLine}
|
|
396
|
+
return ${emptyFormData};
|
|
397
|
+
}`;
|
|
398
|
+
return { method: methodCode, dtoImports };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else if (method === 'POST' && page.useCase) {
|
|
402
|
+
// POST request - form submission
|
|
403
|
+
const { model, action } = this.parseUseCase(page.useCase);
|
|
404
|
+
const useCaseVar = `${model.toLowerCase()}UseCase`;
|
|
405
|
+
const inputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Input`;
|
|
406
|
+
dtoImports.add(`${model}${(0, typeUtils_1.capitalize)(action)}`);
|
|
407
|
+
let parseLogic;
|
|
408
|
+
if (page.path.includes(':id')) {
|
|
409
|
+
parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, id: context.request.parameters.id });`;
|
|
410
|
+
}
|
|
411
|
+
else if (action === 'create') {
|
|
412
|
+
if (childInfo) {
|
|
413
|
+
parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ${childInfo.parentIdField}: context.request.parameters.${childInfo.parentIdField} });`;
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ownerId: context.request.user?.id });`;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
parseLogic = `const input = ${inputClass}.parse(context.request.body);`;
|
|
421
|
+
}
|
|
422
|
+
// Generate PRE-mutation owner check for update/delete actions (POST requests)
|
|
423
|
+
const hasOwner = this.hasOwnerAuth(page.auth);
|
|
424
|
+
const isMutation = action === 'update' || action === 'delete';
|
|
425
|
+
const preMutationOwnerCheck = (hasOwner && isMutation)
|
|
426
|
+
? this.generatePreMutationOwnerCheck(page.auth, useCaseVar)
|
|
427
|
+
: '';
|
|
428
|
+
// Handle onSuccess and onError strategies
|
|
429
|
+
const onSuccessHandler = this.generateOnSuccessHandler(page);
|
|
430
|
+
const onErrorHandler = this.generateOnErrorHandler(page);
|
|
431
|
+
const methodCode = ` @${decorator}('${page.path}')
|
|
432
|
+
async ${methodName}(context: IContext): Promise<any> {${authLine}
|
|
433
|
+
try {
|
|
434
|
+
${parseLogic}${preMutationOwnerCheck}
|
|
435
|
+
const result = await this.${useCaseVar}.${action}(input);
|
|
436
|
+
${onSuccessHandler}
|
|
437
|
+
return { success: true, data: result };
|
|
438
|
+
} catch (error) {
|
|
439
|
+
${onErrorHandler}
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
}`;
|
|
443
|
+
return { method: methodCode, dtoImports };
|
|
444
|
+
}
|
|
445
|
+
const methodCode = ` @${decorator}('${page.path}')
|
|
446
|
+
async ${methodName}(context: IContext): Promise<any> {
|
|
447
|
+
// TODO: Implement ${methodName}
|
|
448
|
+
return {};
|
|
449
|
+
}`;
|
|
450
|
+
return { method: methodCode, dtoImports };
|
|
451
|
+
}
|
|
452
|
+
generateOnSuccessHandler(page) {
|
|
453
|
+
if (!page.onSuccess)
|
|
454
|
+
return '// Success';
|
|
455
|
+
const handlers = [];
|
|
456
|
+
if (page.onSuccess.toast) {
|
|
457
|
+
handlers.push(`// Toast: ${page.onSuccess.toast}`);
|
|
458
|
+
}
|
|
459
|
+
if (page.onSuccess.redirect) {
|
|
460
|
+
const redirectPath = page.onSuccess.redirect.replace(':id', '${result.id}');
|
|
461
|
+
handlers.push(`// Redirect: ${redirectPath}`);
|
|
462
|
+
}
|
|
463
|
+
if (page.onSuccess.back) {
|
|
464
|
+
handlers.push('// Navigate back');
|
|
329
465
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
466
|
+
return handlers.join('\n ') || '// Success';
|
|
467
|
+
}
|
|
468
|
+
generateOnErrorHandler(page) {
|
|
469
|
+
if (!page.onError)
|
|
470
|
+
return '// Error occurred';
|
|
471
|
+
const handlers = [];
|
|
472
|
+
if (page.onError.stay) {
|
|
473
|
+
handlers.push('// Stay on page');
|
|
336
474
|
}
|
|
337
|
-
|
|
338
|
-
|
|
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 '';
|
|
475
|
+
if (page.onError.toast) {
|
|
476
|
+
handlers.push(`// Error toast: ${page.onError.toast}`);
|
|
344
477
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
478
|
+
return handlers.join('\n ') || '// Error occurred';
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Sort routes so static paths are registered before parameterized ones.
|
|
482
|
+
* This prevents parameterized routes (e.g. /:id) from catching requests
|
|
483
|
+
* meant for static routes (e.g. /create).
|
|
484
|
+
*/
|
|
485
|
+
sortRoutesBySpecificity(routes) {
|
|
486
|
+
return [...routes].sort((a, b) => {
|
|
487
|
+
const aSegments = a.path.split('/').filter(Boolean);
|
|
488
|
+
const bSegments = b.path.split('/').filter(Boolean);
|
|
489
|
+
const aParamCount = aSegments.filter(s => s.startsWith(':')).length;
|
|
490
|
+
const bParamCount = bSegments.filter(s => s.startsWith(':')).length;
|
|
491
|
+
return aParamCount - bParamCount;
|
|
354
492
|
});
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
493
|
+
}
|
|
494
|
+
generateApiController(resourceName, prefix, endpoints, childInfo) {
|
|
495
|
+
const controllerName = `${resourceName}ApiController`;
|
|
496
|
+
// Determine which use cases and DTOs are referenced
|
|
497
|
+
const useCaseModels = new Set();
|
|
498
|
+
const allDtoImports = new Set();
|
|
499
|
+
const methods = [];
|
|
500
|
+
const sortedEndpoints = this.sortRoutesBySpecificity(endpoints);
|
|
501
|
+
sortedEndpoints.forEach(endpoint => {
|
|
502
|
+
const { model } = this.parseUseCase(endpoint.useCase);
|
|
503
|
+
useCaseModels.add(model);
|
|
504
|
+
const { method, dtoImports } = this.generateApiEndpointMethod(endpoint, resourceName, childInfo);
|
|
505
|
+
methods.push(method);
|
|
506
|
+
dtoImports.forEach(d => allDtoImports.add(d));
|
|
359
507
|
});
|
|
508
|
+
// Generate imports
|
|
509
|
+
const useCaseImports = Array.from(useCaseModels)
|
|
510
|
+
.map(model => `import { ${model}UseCase } from '../../application/useCases/${model}UseCase';`)
|
|
511
|
+
.join('\n');
|
|
512
|
+
const dtoImportStatements = Array.from(allDtoImports)
|
|
513
|
+
.map(dto => `import { ${dto}Input, ${dto}Output } from '../../application/dto/${dto}';`)
|
|
514
|
+
.join('\n');
|
|
515
|
+
// Generate constructor parameters
|
|
516
|
+
const constructorParams = Array.from(useCaseModels)
|
|
517
|
+
.map(model => `private ${model.toLowerCase()}UseCase: ${model}UseCase`)
|
|
518
|
+
.join(',\n ');
|
|
519
|
+
return `import { Controller, Get, Post, Put, Delete, type IContext } from '@currentjs/router';
|
|
520
|
+
${useCaseImports}
|
|
521
|
+
${dtoImportStatements}
|
|
522
|
+
|
|
523
|
+
@Controller('${prefix}')
|
|
524
|
+
export class ${controllerName} {
|
|
525
|
+
constructor(
|
|
526
|
+
${constructorParams}
|
|
527
|
+
) {}
|
|
528
|
+
|
|
529
|
+
${methods.join('\n\n')}
|
|
530
|
+
}`;
|
|
360
531
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
532
|
+
generateWebController(resourceName, prefix, layout, pages, config, childInfo) {
|
|
533
|
+
const controllerName = `${resourceName}WebController`;
|
|
534
|
+
// Child entities of this resource (for withChild). Only root entities can have withChild.
|
|
535
|
+
const withChildChildren = childInfo ? [] : (0, childEntityUtils_1.getChildrenOfParent)(config, resourceName);
|
|
536
|
+
// Determine which use cases and DTOs are referenced
|
|
537
|
+
const useCaseModels = new Set();
|
|
538
|
+
const allDtoImports = new Set();
|
|
539
|
+
const methods = [];
|
|
540
|
+
const sortedPages = this.sortRoutesBySpecificity(pages);
|
|
541
|
+
sortedPages.forEach((page, index) => {
|
|
542
|
+
var _a, _b;
|
|
543
|
+
if (page.useCase) {
|
|
544
|
+
const { model } = this.parseUseCase(page.useCase);
|
|
545
|
+
useCaseModels.add(model);
|
|
546
|
+
}
|
|
547
|
+
const { model, action } = page.useCase ? this.parseUseCase(page.useCase) : { model: '', action: '' };
|
|
548
|
+
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;
|
|
549
|
+
const withChildForThisPage = useCaseWithChild && action === 'get' && withChildChildren.length > 0 ? withChildChildren : undefined;
|
|
550
|
+
const { method, dtoImports } = this.generateWebPageMethod(page, resourceName, layout, index, childInfo, withChildForThisPage);
|
|
551
|
+
methods.push(method);
|
|
552
|
+
dtoImports.forEach(d => allDtoImports.add(d));
|
|
553
|
+
});
|
|
554
|
+
// Determine if any page actually uses withChild (only then inject child services)
|
|
555
|
+
const needsChildServices = withChildChildren.length > 0 && sortedPages.some(page => {
|
|
556
|
+
var _a, _b;
|
|
557
|
+
if (!page.useCase)
|
|
558
|
+
return false;
|
|
559
|
+
const { model: m, action: a } = this.parseUseCase(page.useCase);
|
|
560
|
+
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;
|
|
561
|
+
});
|
|
562
|
+
// Constructor: use cases + child entity services when withChild is actually used
|
|
563
|
+
const serviceImports = [];
|
|
564
|
+
const constructorParams = [];
|
|
565
|
+
Array.from(useCaseModels).forEach(model => {
|
|
566
|
+
constructorParams.push(`private ${model.toLowerCase()}UseCase: ${model}UseCase`);
|
|
567
|
+
});
|
|
568
|
+
if (needsChildServices) {
|
|
569
|
+
withChildChildren.forEach(child => {
|
|
570
|
+
const childVar = child.childEntityName.charAt(0).toLowerCase() + child.childEntityName.slice(1);
|
|
571
|
+
serviceImports.push(`import { ${child.childEntityName}Service } from '../../application/services/${child.childEntityName}Service';`);
|
|
572
|
+
constructorParams.push(`private ${childVar}Service: ${child.childEntityName}Service`);
|
|
573
|
+
});
|
|
365
574
|
}
|
|
366
|
-
|
|
575
|
+
const useCaseImports = Array.from(useCaseModels)
|
|
576
|
+
.map(model => `import { ${model}UseCase } from '../../application/useCases/${model}UseCase';`)
|
|
577
|
+
.join('\n');
|
|
578
|
+
const dtoImportStatements = Array.from(allDtoImports)
|
|
579
|
+
.map(dto => `import { ${dto}Input } from '../../application/dto/${dto}';`)
|
|
580
|
+
.join('\n');
|
|
581
|
+
const constructorBlock = constructorParams.length > 0
|
|
582
|
+
? `constructor(
|
|
583
|
+
${constructorParams.join(',\n ')}
|
|
584
|
+
) {}`
|
|
585
|
+
: 'constructor() {}';
|
|
586
|
+
return `import { Controller, Get, Post, Render, type IContext } from '@currentjs/router';
|
|
587
|
+
${useCaseImports}
|
|
588
|
+
${serviceImports.join('\n')}
|
|
589
|
+
${dtoImportStatements}
|
|
590
|
+
|
|
591
|
+
@Controller('${prefix}')
|
|
592
|
+
export class ${controllerName} {
|
|
593
|
+
${constructorBlock}
|
|
594
|
+
|
|
595
|
+
${methods.join('\n\n')}
|
|
596
|
+
}`;
|
|
367
597
|
}
|
|
368
|
-
|
|
369
|
-
const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
|
|
370
|
-
const config = (0, yaml_1.parse)(yamlContent);
|
|
598
|
+
generateFromConfig(config) {
|
|
371
599
|
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
|
-
}
|
|
600
|
+
const childEntityMap = (0, childEntityUtils_1.buildChildEntityMap)(config);
|
|
601
|
+
// Generate API controllers
|
|
602
|
+
if (config.api) {
|
|
603
|
+
Object.entries(config.api).forEach(([resourceName, resourceConfig]) => {
|
|
604
|
+
const childInfo = childEntityMap.get(resourceName);
|
|
605
|
+
const code = this.generateApiController(resourceName, resourceConfig.prefix, resourceConfig.endpoints, childInfo);
|
|
606
|
+
result[`${resourceName}Api`] = code;
|
|
386
607
|
});
|
|
387
608
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
}
|
|
609
|
+
// Generate Web controllers
|
|
610
|
+
if (config.web) {
|
|
611
|
+
Object.entries(config.web).forEach(([resourceName, resourceConfig]) => {
|
|
612
|
+
const childInfo = childEntityMap.get(resourceName);
|
|
613
|
+
const code = this.generateWebController(resourceName, resourceConfig.prefix, resourceConfig.layout || 'main_view', resourceConfig.pages, config, childInfo);
|
|
614
|
+
result[`${resourceName}Web`] = code;
|
|
615
|
+
});
|
|
402
616
|
}
|
|
403
617
|
return result;
|
|
404
618
|
}
|
|
405
|
-
|
|
619
|
+
generateFromYamlFile(yamlFilePath) {
|
|
406
620
|
const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
|
|
407
621
|
const config = (0, yaml_1.parse)(yamlContent);
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
622
|
+
if (!(0, configTypes_1.isValidModuleConfig)(config)) {
|
|
623
|
+
throw new Error('Configuration does not match new module format. Expected domain/useCases/api/web structure.');
|
|
624
|
+
}
|
|
625
|
+
return this.generateFromConfig(config);
|
|
626
|
+
}
|
|
627
|
+
async generateAndSaveFiles(yamlFilePath, moduleDir, opts) {
|
|
628
|
+
const controllersByName = this.generateFromYamlFile(yamlFilePath);
|
|
629
|
+
const controllersDir = path.join(moduleDir, 'infrastructure', 'controllers');
|
|
412
630
|
fs.mkdirSync(controllersDir, { recursive: true });
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const filePath = path.join(controllersDir,
|
|
631
|
+
const generatedPaths = [];
|
|
632
|
+
for (const [name, code] of Object.entries(controllersByName)) {
|
|
633
|
+
const filePath = path.join(controllersDir, `${name}Controller.ts`);
|
|
416
634
|
// eslint-disable-next-line no-await-in-loop
|
|
417
|
-
await (0, generationRegistry_1.writeGeneratedFile)(filePath,
|
|
418
|
-
|
|
635
|
+
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) });
|
|
636
|
+
generatedPaths.push(filePath);
|
|
419
637
|
}
|
|
420
638
|
// eslint-disable-next-line no-console
|
|
421
639
|
console.log('\n' + colors_1.colors.green('Controller files generated successfully!') + '\n');
|
|
422
|
-
return
|
|
640
|
+
return generatedPaths;
|
|
423
641
|
}
|
|
424
642
|
}
|
|
425
643
|
exports.ControllerGenerator = ControllerGenerator;
|