@currentjs/gen 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -611
- 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 +45 -11
- 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
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.NewControllerGenerator = void 0;
|
|
37
|
+
const yaml_1 = require("yaml");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const generationRegistry_1 = require("../utils/generationRegistry");
|
|
41
|
+
const colors_1 = require("../utils/colors");
|
|
42
|
+
const configTypes_1 = require("../types/configTypes");
|
|
43
|
+
const childEntityUtils_1 = require("../utils/childEntityUtils");
|
|
44
|
+
class NewControllerGenerator {
|
|
45
|
+
capitalize(str) {
|
|
46
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
47
|
+
}
|
|
48
|
+
getHttpDecorator(method) {
|
|
49
|
+
switch (method.toUpperCase()) {
|
|
50
|
+
case 'GET': return 'Get';
|
|
51
|
+
case 'POST': return 'Post';
|
|
52
|
+
case 'PUT': return 'Put';
|
|
53
|
+
case 'PATCH': return 'Patch';
|
|
54
|
+
case 'DELETE': return 'Delete';
|
|
55
|
+
default: return 'Get';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
parseUseCase(useCase) {
|
|
59
|
+
const [model, action] = useCase.split(':');
|
|
60
|
+
return { model, action };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Normalize auth config to an array of roles
|
|
64
|
+
*/
|
|
65
|
+
normalizeAuth(auth) {
|
|
66
|
+
if (!auth)
|
|
67
|
+
return [];
|
|
68
|
+
if (Array.isArray(auth))
|
|
69
|
+
return auth;
|
|
70
|
+
return [auth];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Check if auth config includes owner permission
|
|
74
|
+
*/
|
|
75
|
+
hasOwnerAuth(auth) {
|
|
76
|
+
const roles = this.normalizeAuth(auth);
|
|
77
|
+
return roles.includes('owner');
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Generate pre-fetch authentication/authorization check code.
|
|
81
|
+
* This runs before fetching the entity and validates authentication and role-based access.
|
|
82
|
+
* @param auth - The auth requirement: 'all', 'authenticated', 'owner', role names, or array of roles
|
|
83
|
+
* @returns Code string for the auth check, or empty string if no check needed
|
|
84
|
+
*/
|
|
85
|
+
generateAuthCheck(auth) {
|
|
86
|
+
const roles = this.normalizeAuth(auth);
|
|
87
|
+
if (roles.length === 0 || (roles.length === 1 && roles[0] === 'all')) {
|
|
88
|
+
return ''; // No check needed - public access
|
|
89
|
+
}
|
|
90
|
+
// If only 'authenticated' is specified
|
|
91
|
+
if (roles.length === 1 && roles[0] === 'authenticated') {
|
|
92
|
+
return `if (!context.request.user) {
|
|
93
|
+
throw new Error('Authentication required');
|
|
94
|
+
}`;
|
|
95
|
+
}
|
|
96
|
+
// If only 'owner' is specified - just require authentication here
|
|
97
|
+
// (owner check happens post-fetch)
|
|
98
|
+
if (roles.length === 1 && roles[0] === 'owner') {
|
|
99
|
+
return `if (!context.request.user) {
|
|
100
|
+
throw new Error('Authentication required');
|
|
101
|
+
}`;
|
|
102
|
+
}
|
|
103
|
+
// Filter out 'owner' and 'all' for role checks (owner is checked post-fetch)
|
|
104
|
+
const roleChecks = roles.filter(r => r !== 'owner' && r !== 'all' && r !== 'authenticated');
|
|
105
|
+
const hasOwner = roles.includes('owner');
|
|
106
|
+
const hasAuthenticated = roles.includes('authenticated');
|
|
107
|
+
// If we have role checks or owner, we need authentication
|
|
108
|
+
if (roleChecks.length > 0 || hasOwner) {
|
|
109
|
+
if (roleChecks.length === 0) {
|
|
110
|
+
// Only owner (and maybe authenticated) - just require auth
|
|
111
|
+
return `if (!context.request.user) {
|
|
112
|
+
throw new Error('Authentication required');
|
|
113
|
+
}`;
|
|
114
|
+
}
|
|
115
|
+
if (roleChecks.length === 1 && !hasOwner) {
|
|
116
|
+
// Single role check
|
|
117
|
+
return `if (!context.request.user) {
|
|
118
|
+
throw new Error('Authentication required');
|
|
119
|
+
}
|
|
120
|
+
if (context.request.user.role !== '${roleChecks[0]}') {
|
|
121
|
+
throw new Error('Insufficient permissions: ${roleChecks[0]} role required');
|
|
122
|
+
}`;
|
|
123
|
+
}
|
|
124
|
+
// Multiple roles OR owner - use OR logic
|
|
125
|
+
// If owner is included, we can't fully check here (post-fetch), so we just require auth
|
|
126
|
+
// and mark that owner check should happen later
|
|
127
|
+
if (hasOwner) {
|
|
128
|
+
// With owner: require auth, role check will be combined with owner check post-fetch
|
|
129
|
+
return `if (!context.request.user) {
|
|
130
|
+
throw new Error('Authentication required');
|
|
131
|
+
}`;
|
|
132
|
+
}
|
|
133
|
+
// Multiple roles without owner - check if user has ANY of the roles
|
|
134
|
+
const roleConditions = roleChecks.map(r => `context.request.user.role === '${r}'`).join(' || ');
|
|
135
|
+
return `if (!context.request.user) {
|
|
136
|
+
throw new Error('Authentication required');
|
|
137
|
+
}
|
|
138
|
+
if (!(${roleConditions})) {
|
|
139
|
+
throw new Error('Insufficient permissions: one of [${roleChecks.join(', ')}] role required');
|
|
140
|
+
}`;
|
|
141
|
+
}
|
|
142
|
+
// Only 'authenticated' in the mix
|
|
143
|
+
if (hasAuthenticated) {
|
|
144
|
+
return `if (!context.request.user) {
|
|
145
|
+
throw new Error('Authentication required');
|
|
146
|
+
}`;
|
|
147
|
+
}
|
|
148
|
+
return '';
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Generate post-fetch authorization check for owner validation.
|
|
152
|
+
* This runs after fetching the entity and validates ownership.
|
|
153
|
+
* Used for READ operations (get, list) where we check after fetch.
|
|
154
|
+
* For child entities, uses getResourceOwner() since result has no ownerId.
|
|
155
|
+
*/
|
|
156
|
+
generatePostFetchOwnerCheck(auth, resultVar = 'result', useCaseVar, childInfo) {
|
|
157
|
+
const roles = this.normalizeAuth(auth);
|
|
158
|
+
if (!roles.includes('owner')) {
|
|
159
|
+
return ''; // No owner check needed
|
|
160
|
+
}
|
|
161
|
+
const bypassRoles = roles.filter(r => r !== 'owner' && r !== 'all' && r !== 'authenticated');
|
|
162
|
+
// Child entities don't have ownerId on result; resolve via getResourceOwner
|
|
163
|
+
if (childInfo && useCaseVar) {
|
|
164
|
+
if (bypassRoles.length === 0) {
|
|
165
|
+
return `
|
|
166
|
+
// Owner validation (post-fetch for reads, via parent)
|
|
167
|
+
const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(${resultVar}.id);
|
|
168
|
+
if (resourceOwnerId === null) {
|
|
169
|
+
throw new Error('Resource not found');
|
|
170
|
+
}
|
|
171
|
+
if (resourceOwnerId !== context.request.user?.id) {
|
|
172
|
+
throw new Error('Access denied: you do not own this resource');
|
|
173
|
+
}`;
|
|
174
|
+
}
|
|
175
|
+
const bypassConditions = bypassRoles.map(r => `context.request.user?.role === '${r}'`).join(' || ');
|
|
176
|
+
return `
|
|
177
|
+
// Owner validation (post-fetch for reads, via parent, bypassed for: ${bypassRoles.join(', ')})
|
|
178
|
+
const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(${resultVar}.id);
|
|
179
|
+
if (resourceOwnerId === null) {
|
|
180
|
+
throw new Error('Resource not found');
|
|
181
|
+
}
|
|
182
|
+
const isOwner = resourceOwnerId === context.request.user?.id;
|
|
183
|
+
const hasPrivilegedRole = ${bypassConditions};
|
|
184
|
+
if (!isOwner && !hasPrivilegedRole) {
|
|
185
|
+
throw new Error('Access denied: you do not own this resource');
|
|
186
|
+
}`;
|
|
187
|
+
}
|
|
188
|
+
// Root entity: result has ownerId
|
|
189
|
+
if (bypassRoles.length === 0) {
|
|
190
|
+
return `
|
|
191
|
+
// Owner validation (post-fetch for reads)
|
|
192
|
+
if (${resultVar}.ownerId !== context.request.user?.id) {
|
|
193
|
+
throw new Error('Access denied: you do not own this resource');
|
|
194
|
+
}`;
|
|
195
|
+
}
|
|
196
|
+
const bypassConditions = bypassRoles.map(r => `context.request.user?.role === '${r}'`).join(' || ');
|
|
197
|
+
return `
|
|
198
|
+
// Owner validation (post-fetch for reads, bypassed for: ${bypassRoles.join(', ')})
|
|
199
|
+
const isOwner = ${resultVar}.ownerId === context.request.user?.id;
|
|
200
|
+
const hasPrivilegedRole = ${bypassConditions};
|
|
201
|
+
if (!isOwner && !hasPrivilegedRole) {
|
|
202
|
+
throw new Error('Access denied: you do not own this resource');
|
|
203
|
+
}`;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Generate pre-mutation authorization check for owner validation.
|
|
207
|
+
* This runs BEFORE the mutation to prevent unauthorized changes.
|
|
208
|
+
* Used for WRITE operations (update, delete).
|
|
209
|
+
* @param auth - The auth requirement
|
|
210
|
+
* @param useCaseVar - The use case variable name
|
|
211
|
+
* @returns Code string for the pre-mutation owner check, or empty string if not needed
|
|
212
|
+
*/
|
|
213
|
+
generatePreMutationOwnerCheck(auth, useCaseVar = 'useCase') {
|
|
214
|
+
const roles = this.normalizeAuth(auth);
|
|
215
|
+
if (!roles.includes('owner')) {
|
|
216
|
+
return ''; // No owner check needed
|
|
217
|
+
}
|
|
218
|
+
// Get non-owner roles for the bypass check
|
|
219
|
+
const bypassRoles = roles.filter(r => r !== 'owner' && r !== 'all' && r !== 'authenticated');
|
|
220
|
+
if (bypassRoles.length === 0) {
|
|
221
|
+
// Only owner - strict ownership check
|
|
222
|
+
return `
|
|
223
|
+
// Pre-mutation owner validation
|
|
224
|
+
const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(input.id);
|
|
225
|
+
if (resourceOwnerId === null) {
|
|
226
|
+
throw new Error('Resource not found');
|
|
227
|
+
}
|
|
228
|
+
if (resourceOwnerId !== context.request.user?.id) {
|
|
229
|
+
throw new Error('Access denied: you do not own this resource');
|
|
230
|
+
}
|
|
231
|
+
`;
|
|
232
|
+
}
|
|
233
|
+
// Owner OR other roles - bypass if user has a privileged role
|
|
234
|
+
const bypassConditions = bypassRoles.map(r => `context.request.user?.role === '${r}'`).join(' || ');
|
|
235
|
+
return `
|
|
236
|
+
// Pre-mutation owner validation (bypassed for: ${bypassRoles.join(', ')})
|
|
237
|
+
const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(input.id);
|
|
238
|
+
if (resourceOwnerId === null) {
|
|
239
|
+
throw new Error('Resource not found');
|
|
240
|
+
}
|
|
241
|
+
const isOwner = resourceOwnerId === context.request.user?.id;
|
|
242
|
+
const hasPrivilegedRole = ${bypassConditions};
|
|
243
|
+
if (!isOwner && !hasPrivilegedRole) {
|
|
244
|
+
throw new Error('Access denied: you do not own this resource');
|
|
245
|
+
}
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
generateApiEndpointMethod(endpoint, resourceName, childInfo) {
|
|
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}${this.capitalize(action)}Input`;
|
|
254
|
+
const outputClass = `${model}${this.capitalize(action)}Output`;
|
|
255
|
+
const dtoImports = new Set();
|
|
256
|
+
dtoImports.add(`${model}${this.capitalize(action)}`);
|
|
257
|
+
// Generate auth check (pre-fetch)
|
|
258
|
+
const authCheck = this.generateAuthCheck(endpoint.auth);
|
|
259
|
+
const authLine = authCheck ? `\n ${authCheck}\n` : '';
|
|
260
|
+
// Build parsing logic
|
|
261
|
+
// For create: root gets ownerId from user, child gets parentId from URL params
|
|
262
|
+
let parseLogic;
|
|
263
|
+
if (action === 'list') {
|
|
264
|
+
parseLogic = `const input = ${inputClass}.parse(context.request.parameters);`;
|
|
265
|
+
}
|
|
266
|
+
else if (action === 'get' || action === 'delete') {
|
|
267
|
+
parseLogic = `const input = ${inputClass}.parse({ id: context.request.parameters.id });`;
|
|
268
|
+
}
|
|
269
|
+
else if (action === 'create') {
|
|
270
|
+
if (childInfo) {
|
|
271
|
+
parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ${childInfo.parentIdField}: context.request.parameters.${childInfo.parentIdField} });`;
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ownerId: context.request.user?.id });`;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
else if (action === 'update') {
|
|
278
|
+
parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, id: context.request.parameters.id });`;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
parseLogic = `const input = ${inputClass}.parse(context.request.body || {});`;
|
|
282
|
+
}
|
|
283
|
+
// Generate owner checks:
|
|
284
|
+
// - For mutations (update, delete): PRE-mutation check (before operation)
|
|
285
|
+
// - For reads (get): POST-fetch check (after fetching)
|
|
286
|
+
const hasOwner = this.hasOwnerAuth(endpoint.auth);
|
|
287
|
+
const isMutation = action === 'update' || action === 'delete';
|
|
288
|
+
const isRead = action === 'get';
|
|
289
|
+
// Pre-mutation owner check for write operations
|
|
290
|
+
const preMutationOwnerCheck = (hasOwner && isMutation)
|
|
291
|
+
? this.generatePreMutationOwnerCheck(endpoint.auth, useCaseVar)
|
|
292
|
+
: '';
|
|
293
|
+
// Post-fetch owner check for read operations only
|
|
294
|
+
const postFetchOwnerCheck = (hasOwner && isRead)
|
|
295
|
+
? this.generatePostFetchOwnerCheck(endpoint.auth, 'result', useCaseVar, childInfo)
|
|
296
|
+
: '';
|
|
297
|
+
// Generate output transformation based on action
|
|
298
|
+
let outputTransform;
|
|
299
|
+
if (action === 'list') {
|
|
300
|
+
outputTransform = `return ${outputClass}.from(result);`;
|
|
301
|
+
}
|
|
302
|
+
else if (action === 'delete') {
|
|
303
|
+
outputTransform = `return result;`;
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
outputTransform = `return ${outputClass}.from(result);`;
|
|
307
|
+
}
|
|
308
|
+
const method = ` @${decorator}('${endpoint.path}')
|
|
309
|
+
async ${methodName}(context: IContext): Promise<any> {${authLine}
|
|
310
|
+
${parseLogic}${preMutationOwnerCheck}
|
|
311
|
+
const result = await this.${useCaseVar}.${action}(input);${postFetchOwnerCheck}
|
|
312
|
+
${outputTransform}
|
|
313
|
+
}`;
|
|
314
|
+
return { method, dtoImports };
|
|
315
|
+
}
|
|
316
|
+
generateWebPageMethod(page, resourceName, layout, methodIndex, childInfo, withChildChildren) {
|
|
317
|
+
const method = page.method || 'GET';
|
|
318
|
+
const decorator = this.getHttpDecorator(method);
|
|
319
|
+
const dtoImports = new Set();
|
|
320
|
+
// Generate unique method name by appending method type for POST routes
|
|
321
|
+
const pathSegments = page.path.split('/').filter(Boolean);
|
|
322
|
+
let baseMethodName = pathSegments.length === 0
|
|
323
|
+
? 'index'
|
|
324
|
+
: pathSegments.map((seg, idx) => {
|
|
325
|
+
if (seg.startsWith(':')) {
|
|
326
|
+
return 'By' + this.capitalize(seg.slice(1));
|
|
327
|
+
}
|
|
328
|
+
return idx === 0 ? seg : this.capitalize(seg);
|
|
329
|
+
}).join('');
|
|
330
|
+
// Append method suffix for POST to avoid duplicates
|
|
331
|
+
const methodName = method === 'POST' ? `${baseMethodName}Submit` : baseMethodName;
|
|
332
|
+
// Generate auth check (pre-fetch)
|
|
333
|
+
const authCheck = this.generateAuthCheck(page.auth);
|
|
334
|
+
const authLine = authCheck ? `\n ${authCheck}\n` : '';
|
|
335
|
+
// For GET requests with views (display pages)
|
|
336
|
+
if (method === 'GET' && page.view) {
|
|
337
|
+
const renderDecorator = `\n @Render("${page.view}", "${layout}")`;
|
|
338
|
+
if (page.useCase) {
|
|
339
|
+
const { model, action } = this.parseUseCase(page.useCase);
|
|
340
|
+
const useCaseVar = `${model.toLowerCase()}UseCase`;
|
|
341
|
+
const inputClass = `${model}${this.capitalize(action)}Input`;
|
|
342
|
+
dtoImports.add(`${model}${this.capitalize(action)}`);
|
|
343
|
+
let parseLogic;
|
|
344
|
+
if (page.path.includes(':id')) {
|
|
345
|
+
parseLogic = `const input = ${inputClass}.parse({ id: context.request.parameters.id });`;
|
|
346
|
+
}
|
|
347
|
+
else if (action === 'list') {
|
|
348
|
+
parseLogic = `const input = ${inputClass}.parse(context.request.parameters);`;
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
parseLogic = `const input = ${inputClass}.parse({});`;
|
|
352
|
+
}
|
|
353
|
+
// Generate post-fetch owner check for GET pages (reads only)
|
|
354
|
+
const hasOwner = this.hasOwnerAuth(page.auth);
|
|
355
|
+
const isReadAction = action === 'get' || action === 'list';
|
|
356
|
+
const postFetchOwnerCheck = (hasOwner && isReadAction)
|
|
357
|
+
? this.generatePostFetchOwnerCheck(page.auth, 'result', useCaseVar, childInfo)
|
|
358
|
+
: '';
|
|
359
|
+
// For get + withChild: load child entities and merge into result for template
|
|
360
|
+
const loadChildBlocks = [];
|
|
361
|
+
let returnExpr;
|
|
362
|
+
if (childInfo) {
|
|
363
|
+
returnExpr = `{ ...result, ${childInfo.parentIdField}: context.request.parameters.${childInfo.parentIdField} }`;
|
|
364
|
+
}
|
|
365
|
+
else if ((withChildChildren === null || withChildChildren === void 0 ? void 0 : withChildChildren.length) && action === 'get') {
|
|
366
|
+
const childKeys = [];
|
|
367
|
+
for (const child of withChildChildren) {
|
|
368
|
+
const childVar = child.childEntityName.charAt(0).toLowerCase() + child.childEntityName.slice(1);
|
|
369
|
+
const childItemsKey = `${childVar}Items`;
|
|
370
|
+
childKeys.push(childItemsKey);
|
|
371
|
+
loadChildBlocks.push(`const ${childItemsKey} = await this.${childVar}Service.listByParent(result.id);`);
|
|
372
|
+
}
|
|
373
|
+
returnExpr = `{ ...result, ${childKeys.map(k => `${k}: ${k}`).join(', ')} }`;
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
returnExpr = 'result';
|
|
377
|
+
}
|
|
378
|
+
const loadChildCode = loadChildBlocks.length ? '\n ' + loadChildBlocks.join('\n ') + '\n ' : '';
|
|
379
|
+
const methodCode = `${renderDecorator}
|
|
380
|
+
@${decorator}('${page.path}')
|
|
381
|
+
async ${methodName}(context: IContext): Promise<any> {${authLine}
|
|
382
|
+
${parseLogic}
|
|
383
|
+
const result = await this.${useCaseVar}.${action}(input);${postFetchOwnerCheck}${loadChildCode}
|
|
384
|
+
return ${returnExpr};
|
|
385
|
+
}`;
|
|
386
|
+
return { method: methodCode, dtoImports };
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
// No use case - just render view (e.g. create form)
|
|
390
|
+
// Child entities need the parent ID from URL params for link rendering
|
|
391
|
+
const emptyFormData = childInfo
|
|
392
|
+
? `{ formData: {}, ${childInfo.parentIdField}: context.request.parameters.${childInfo.parentIdField} }`
|
|
393
|
+
: '{ formData: {} }';
|
|
394
|
+
const methodCode = `${renderDecorator}
|
|
395
|
+
@${decorator}('${page.path}')
|
|
396
|
+
async ${methodName}(context: IContext): Promise<any> {${authLine}
|
|
397
|
+
return ${emptyFormData};
|
|
398
|
+
}`;
|
|
399
|
+
return { method: methodCode, dtoImports };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
else if (method === 'POST' && page.useCase) {
|
|
403
|
+
// POST request - form submission
|
|
404
|
+
const { model, action } = this.parseUseCase(page.useCase);
|
|
405
|
+
const useCaseVar = `${model.toLowerCase()}UseCase`;
|
|
406
|
+
const inputClass = `${model}${this.capitalize(action)}Input`;
|
|
407
|
+
dtoImports.add(`${model}${this.capitalize(action)}`);
|
|
408
|
+
let parseLogic;
|
|
409
|
+
if (page.path.includes(':id')) {
|
|
410
|
+
parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, id: context.request.parameters.id });`;
|
|
411
|
+
}
|
|
412
|
+
else if (action === 'create') {
|
|
413
|
+
if (childInfo) {
|
|
414
|
+
parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ${childInfo.parentIdField}: context.request.parameters.${childInfo.parentIdField} });`;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ownerId: context.request.user?.id });`;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
parseLogic = `const input = ${inputClass}.parse(context.request.body);`;
|
|
422
|
+
}
|
|
423
|
+
// Generate PRE-mutation owner check for update/delete actions (POST requests)
|
|
424
|
+
const hasOwner = this.hasOwnerAuth(page.auth);
|
|
425
|
+
const isMutation = action === 'update' || action === 'delete';
|
|
426
|
+
const preMutationOwnerCheck = (hasOwner && isMutation)
|
|
427
|
+
? this.generatePreMutationOwnerCheck(page.auth, useCaseVar)
|
|
428
|
+
: '';
|
|
429
|
+
// Handle onSuccess and onError strategies
|
|
430
|
+
const onSuccessHandler = this.generateOnSuccessHandler(page);
|
|
431
|
+
const onErrorHandler = this.generateOnErrorHandler(page);
|
|
432
|
+
const methodCode = ` @${decorator}('${page.path}')
|
|
433
|
+
async ${methodName}(context: IContext): Promise<any> {${authLine}
|
|
434
|
+
try {
|
|
435
|
+
${parseLogic}${preMutationOwnerCheck}
|
|
436
|
+
const result = await this.${useCaseVar}.${action}(input);
|
|
437
|
+
${onSuccessHandler}
|
|
438
|
+
return { success: true, data: result };
|
|
439
|
+
} catch (error) {
|
|
440
|
+
${onErrorHandler}
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
}`;
|
|
444
|
+
return { method: methodCode, dtoImports };
|
|
445
|
+
}
|
|
446
|
+
const methodCode = ` @${decorator}('${page.path}')
|
|
447
|
+
async ${methodName}(context: IContext): Promise<any> {
|
|
448
|
+
// TODO: Implement ${methodName}
|
|
449
|
+
return {};
|
|
450
|
+
}`;
|
|
451
|
+
return { method: methodCode, dtoImports };
|
|
452
|
+
}
|
|
453
|
+
generateOnSuccessHandler(page) {
|
|
454
|
+
if (!page.onSuccess)
|
|
455
|
+
return '// Success';
|
|
456
|
+
const handlers = [];
|
|
457
|
+
if (page.onSuccess.toast) {
|
|
458
|
+
handlers.push(`// Toast: ${page.onSuccess.toast}`);
|
|
459
|
+
}
|
|
460
|
+
if (page.onSuccess.redirect) {
|
|
461
|
+
const redirectPath = page.onSuccess.redirect.replace(':id', '${result.id}');
|
|
462
|
+
handlers.push(`// Redirect: ${redirectPath}`);
|
|
463
|
+
}
|
|
464
|
+
if (page.onSuccess.back) {
|
|
465
|
+
handlers.push('// Navigate back');
|
|
466
|
+
}
|
|
467
|
+
return handlers.join('\n ') || '// Success';
|
|
468
|
+
}
|
|
469
|
+
generateOnErrorHandler(page) {
|
|
470
|
+
if (!page.onError)
|
|
471
|
+
return '// Error occurred';
|
|
472
|
+
const handlers = [];
|
|
473
|
+
if (page.onError.stay) {
|
|
474
|
+
handlers.push('// Stay on page');
|
|
475
|
+
}
|
|
476
|
+
if (page.onError.toast) {
|
|
477
|
+
handlers.push(`// Error toast: ${page.onError.toast}`);
|
|
478
|
+
}
|
|
479
|
+
return handlers.join('\n ') || '// Error occurred';
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Sort routes so static paths are registered before parameterized ones.
|
|
483
|
+
* This prevents parameterized routes (e.g. /:id) from catching requests
|
|
484
|
+
* meant for static routes (e.g. /create).
|
|
485
|
+
*/
|
|
486
|
+
sortRoutesBySpecificity(routes) {
|
|
487
|
+
return [...routes].sort((a, b) => {
|
|
488
|
+
const aSegments = a.path.split('/').filter(Boolean);
|
|
489
|
+
const bSegments = b.path.split('/').filter(Boolean);
|
|
490
|
+
const aParamCount = aSegments.filter(s => s.startsWith(':')).length;
|
|
491
|
+
const bParamCount = bSegments.filter(s => s.startsWith(':')).length;
|
|
492
|
+
return aParamCount - bParamCount;
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
generateApiController(resourceName, prefix, endpoints, childInfo) {
|
|
496
|
+
const controllerName = `${resourceName}ApiController`;
|
|
497
|
+
// Determine which use cases and DTOs are referenced
|
|
498
|
+
const useCaseModels = new Set();
|
|
499
|
+
const allDtoImports = new Set();
|
|
500
|
+
const methods = [];
|
|
501
|
+
const sortedEndpoints = this.sortRoutesBySpecificity(endpoints);
|
|
502
|
+
sortedEndpoints.forEach(endpoint => {
|
|
503
|
+
const { model } = this.parseUseCase(endpoint.useCase);
|
|
504
|
+
useCaseModels.add(model);
|
|
505
|
+
const { method, dtoImports } = this.generateApiEndpointMethod(endpoint, resourceName, childInfo);
|
|
506
|
+
methods.push(method);
|
|
507
|
+
dtoImports.forEach(d => allDtoImports.add(d));
|
|
508
|
+
});
|
|
509
|
+
// Generate imports
|
|
510
|
+
const useCaseImports = Array.from(useCaseModels)
|
|
511
|
+
.map(model => `import { ${model}UseCase } from '../../application/useCases/${model}UseCase';`)
|
|
512
|
+
.join('\n');
|
|
513
|
+
const dtoImportStatements = Array.from(allDtoImports)
|
|
514
|
+
.map(dto => `import { ${dto}Input, ${dto}Output } from '../../application/dto/${dto}';`)
|
|
515
|
+
.join('\n');
|
|
516
|
+
// Generate constructor parameters
|
|
517
|
+
const constructorParams = Array.from(useCaseModels)
|
|
518
|
+
.map(model => `private ${model.toLowerCase()}UseCase: ${model}UseCase`)
|
|
519
|
+
.join(',\n ');
|
|
520
|
+
return `import { Controller, Get, Post, Put, Delete, type IContext } from '@currentjs/router';
|
|
521
|
+
${useCaseImports}
|
|
522
|
+
${dtoImportStatements}
|
|
523
|
+
|
|
524
|
+
@Controller('${prefix}')
|
|
525
|
+
export class ${controllerName} {
|
|
526
|
+
constructor(
|
|
527
|
+
${constructorParams}
|
|
528
|
+
) {}
|
|
529
|
+
|
|
530
|
+
${methods.join('\n\n')}
|
|
531
|
+
}`;
|
|
532
|
+
}
|
|
533
|
+
generateWebController(resourceName, prefix, layout, pages, config, childInfo) {
|
|
534
|
+
const controllerName = `${resourceName}WebController`;
|
|
535
|
+
// Child entities of this resource (for withChild). Only root entities can have withChild.
|
|
536
|
+
const withChildChildren = childInfo ? [] : (0, childEntityUtils_1.getChildrenOfParent)(config, resourceName);
|
|
537
|
+
// Determine which use cases and DTOs are referenced
|
|
538
|
+
const useCaseModels = new Set();
|
|
539
|
+
const allDtoImports = new Set();
|
|
540
|
+
const methods = [];
|
|
541
|
+
const sortedPages = this.sortRoutesBySpecificity(pages);
|
|
542
|
+
sortedPages.forEach((page, index) => {
|
|
543
|
+
var _a, _b;
|
|
544
|
+
if (page.useCase) {
|
|
545
|
+
const { model } = this.parseUseCase(page.useCase);
|
|
546
|
+
useCaseModels.add(model);
|
|
547
|
+
}
|
|
548
|
+
const { model, action } = page.useCase ? this.parseUseCase(page.useCase) : { model: '', action: '' };
|
|
549
|
+
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;
|
|
550
|
+
const withChildForThisPage = useCaseWithChild && action === 'get' && withChildChildren.length > 0 ? withChildChildren : undefined;
|
|
551
|
+
const { method, dtoImports } = this.generateWebPageMethod(page, resourceName, layout, index, childInfo, withChildForThisPage);
|
|
552
|
+
methods.push(method);
|
|
553
|
+
dtoImports.forEach(d => allDtoImports.add(d));
|
|
554
|
+
});
|
|
555
|
+
// Determine if any page actually uses withChild (only then inject child services)
|
|
556
|
+
const needsChildServices = withChildChildren.length > 0 && sortedPages.some(page => {
|
|
557
|
+
var _a, _b;
|
|
558
|
+
if (!page.useCase)
|
|
559
|
+
return false;
|
|
560
|
+
const { model: m, action: a } = this.parseUseCase(page.useCase);
|
|
561
|
+
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;
|
|
562
|
+
});
|
|
563
|
+
// Constructor: use cases + child entity services when withChild is actually used
|
|
564
|
+
const serviceImports = [];
|
|
565
|
+
const constructorParams = [];
|
|
566
|
+
Array.from(useCaseModels).forEach(model => {
|
|
567
|
+
constructorParams.push(`private ${model.toLowerCase()}UseCase: ${model}UseCase`);
|
|
568
|
+
});
|
|
569
|
+
if (needsChildServices) {
|
|
570
|
+
withChildChildren.forEach(child => {
|
|
571
|
+
const childVar = child.childEntityName.charAt(0).toLowerCase() + child.childEntityName.slice(1);
|
|
572
|
+
serviceImports.push(`import { ${child.childEntityName}Service } from '../../application/services/${child.childEntityName}Service';`);
|
|
573
|
+
constructorParams.push(`private ${childVar}Service: ${child.childEntityName}Service`);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
const useCaseImports = Array.from(useCaseModels)
|
|
577
|
+
.map(model => `import { ${model}UseCase } from '../../application/useCases/${model}UseCase';`)
|
|
578
|
+
.join('\n');
|
|
579
|
+
const dtoImportStatements = Array.from(allDtoImports)
|
|
580
|
+
.map(dto => `import { ${dto}Input } from '../../application/dto/${dto}';`)
|
|
581
|
+
.join('\n');
|
|
582
|
+
const constructorBlock = constructorParams.length > 0
|
|
583
|
+
? `constructor(
|
|
584
|
+
${constructorParams.join(',\n ')}
|
|
585
|
+
) {}`
|
|
586
|
+
: 'constructor() {}';
|
|
587
|
+
return `import { Controller, Get, Post, Render, type IContext } from '@currentjs/router';
|
|
588
|
+
${useCaseImports}
|
|
589
|
+
${serviceImports.join('\n')}
|
|
590
|
+
${dtoImportStatements}
|
|
591
|
+
|
|
592
|
+
@Controller('${prefix}')
|
|
593
|
+
export class ${controllerName} {
|
|
594
|
+
${constructorBlock}
|
|
595
|
+
|
|
596
|
+
${methods.join('\n\n')}
|
|
597
|
+
}`;
|
|
598
|
+
}
|
|
599
|
+
generateFromConfig(config) {
|
|
600
|
+
const result = {};
|
|
601
|
+
const childEntityMap = (0, childEntityUtils_1.buildChildEntityMap)(config);
|
|
602
|
+
// Generate API controllers
|
|
603
|
+
if (config.api) {
|
|
604
|
+
Object.entries(config.api).forEach(([resourceName, resourceConfig]) => {
|
|
605
|
+
const childInfo = childEntityMap.get(resourceName);
|
|
606
|
+
const code = this.generateApiController(resourceName, resourceConfig.prefix, resourceConfig.endpoints, childInfo);
|
|
607
|
+
result[`${resourceName}Api`] = code;
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
// Generate Web controllers
|
|
611
|
+
if (config.web) {
|
|
612
|
+
Object.entries(config.web).forEach(([resourceName, resourceConfig]) => {
|
|
613
|
+
const childInfo = childEntityMap.get(resourceName);
|
|
614
|
+
const code = this.generateWebController(resourceName, resourceConfig.prefix, resourceConfig.layout || 'main_view', resourceConfig.pages, config, childInfo);
|
|
615
|
+
result[`${resourceName}Web`] = code;
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
generateFromYamlFile(yamlFilePath) {
|
|
621
|
+
const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
|
|
622
|
+
const config = (0, yaml_1.parse)(yamlContent);
|
|
623
|
+
if (!(0, configTypes_1.isNewModuleConfig)(config)) {
|
|
624
|
+
throw new Error('Configuration does not match new module format. Expected domain/useCases/api/web structure.');
|
|
625
|
+
}
|
|
626
|
+
return this.generateFromConfig(config);
|
|
627
|
+
}
|
|
628
|
+
async generateAndSaveFiles(yamlFilePath, moduleDir, opts) {
|
|
629
|
+
const controllersByName = this.generateFromYamlFile(yamlFilePath);
|
|
630
|
+
const controllersDir = path.join(moduleDir, 'infrastructure', 'controllers');
|
|
631
|
+
fs.mkdirSync(controllersDir, { recursive: true });
|
|
632
|
+
const generatedPaths = [];
|
|
633
|
+
for (const [name, code] of Object.entries(controllersByName)) {
|
|
634
|
+
const filePath = path.join(controllersDir, `${name}Controller.ts`);
|
|
635
|
+
// eslint-disable-next-line no-await-in-loop
|
|
636
|
+
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) });
|
|
637
|
+
generatedPaths.push(filePath);
|
|
638
|
+
}
|
|
639
|
+
// eslint-disable-next-line no-console
|
|
640
|
+
console.log('\n' + colors_1.colors.green('Controller files generated successfully!') + '\n');
|
|
641
|
+
return generatedPaths;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
exports.NewControllerGenerator = NewControllerGenerator;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NewModuleConfig } from '../types/configTypes';
|
|
2
|
+
export declare class NewServiceGenerator {
|
|
3
|
+
private typeMapping;
|
|
4
|
+
private availableAggregates;
|
|
5
|
+
private mapType;
|
|
6
|
+
private capitalize;
|
|
7
|
+
private generateDefaultHandlerMethod;
|
|
8
|
+
private generateCustomHandlerMethod;
|
|
9
|
+
private collectHandlers;
|
|
10
|
+
private generateListByParentMethod;
|
|
11
|
+
private generateGetResourceOwnerMethod;
|
|
12
|
+
private generateService;
|
|
13
|
+
generateFromConfig(config: NewModuleConfig): Record<string, string>;
|
|
14
|
+
generateFromYamlFile(yamlFilePath: string): Record<string, string>;
|
|
15
|
+
generateAndSaveFiles(yamlFilePath: string, moduleDir: string, opts?: {
|
|
16
|
+
force?: boolean;
|
|
17
|
+
skipOnConflict?: boolean;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
}
|