@currentjs/gen 0.5.5 ā 0.5.7
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 -0
- package/README.md +2 -0
- package/dist/commands/generateAll.js +7 -6
- package/dist/commands/migrateCommit.js +6 -3
- package/dist/generators/controllerGenerator.d.ts +10 -4
- package/dist/generators/controllerGenerator.js +60 -28
- package/dist/generators/domainLayerGenerator.d.ts +5 -4
- package/dist/generators/domainLayerGenerator.js +11 -8
- package/dist/generators/dtoGenerator.d.ts +5 -4
- package/dist/generators/dtoGenerator.js +29 -16
- package/dist/generators/serviceGenerator.d.ts +5 -4
- package/dist/generators/serviceGenerator.js +23 -14
- package/dist/generators/storeGenerator.d.ts +11 -4
- package/dist/generators/storeGenerator.js +147 -24
- package/dist/generators/templates/data/appYamlTemplate +1 -1
- package/dist/generators/templates/storeTemplates.d.ts +1 -1
- package/dist/generators/templates/storeTemplates.js +20 -19
- package/dist/generators/useCaseGenerator.d.ts +5 -4
- package/dist/generators/useCaseGenerator.js +10 -7
- package/dist/types/configTypes.d.ts +3 -0
- package/dist/types/configTypes.js +15 -0
- package/dist/utils/commandUtils.d.ts +1 -1
- package/dist/utils/commandUtils.js +5 -3
- package/dist/utils/migrationUtils.d.ts +8 -6
- package/dist/utils/migrationUtils.js +37 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.7] - 2026-04-08
|
|
4
|
+
|
|
5
|
+
- HTTP errors in generated controllers
|
|
6
|
+
|
|
7
|
+
## [0.5.6] - 2026-04-08
|
|
8
|
+
|
|
9
|
+
- database identifier types (numeric, uuid, nanoid)
|
|
10
|
+
|
|
3
11
|
## [0.5.5] - 2026-04-07
|
|
4
12
|
|
|
5
13
|
- data access layer generation fixes and improvements
|
package/README.md
CHANGED
|
@@ -492,6 +492,8 @@ Collects all aggregate definitions from module YAMLs, compares them against the
|
|
|
492
492
|
|
|
493
493
|
The migration file contains `CREATE TABLE`, `ALTER TABLE ADD/MODIFY/DROP COLUMN`, and `DROP TABLE` statements as needed. Foreign keys, indexes, and standard timestamp columns (`created_at`, `updated_at`, `deleted_at`) are handled automatically.
|
|
494
494
|
|
|
495
|
+
The SQL types of primary keys and foreign keys are determined by the `config.identifiers` setting in `app.yaml`.
|
|
496
|
+
|
|
495
497
|
After generating the file, the schema state is updated so the next `migrate commit` only produces a diff of subsequent changes.
|
|
496
498
|
|
|
497
499
|
### `migrate push` *(not yet implemented)*
|
|
@@ -81,18 +81,19 @@ async function handleGenerateAll(yamlPathArg, _outArg, moduleName, opts) {
|
|
|
81
81
|
const moduleDir = path.dirname(moduleYamlPath);
|
|
82
82
|
// eslint-disable-next-line no-console
|
|
83
83
|
console.log(colors_1.colors.blue(`\nGenerating module: ${path.basename(moduleDir)}`));
|
|
84
|
+
const identifiers = entry.identifiers;
|
|
84
85
|
// eslint-disable-next-line no-await-in-loop
|
|
85
|
-
await domainGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts);
|
|
86
|
+
await domainGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts, identifiers);
|
|
86
87
|
// eslint-disable-next-line no-await-in-loop
|
|
87
|
-
await dtoGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts);
|
|
88
|
+
await dtoGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts, identifiers);
|
|
88
89
|
// eslint-disable-next-line no-await-in-loop
|
|
89
|
-
await useCaseGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts);
|
|
90
|
+
await useCaseGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts, identifiers);
|
|
90
91
|
// eslint-disable-next-line no-await-in-loop
|
|
91
|
-
await serviceGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts);
|
|
92
|
+
await serviceGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts, identifiers);
|
|
92
93
|
// eslint-disable-next-line no-await-in-loop
|
|
93
|
-
await storeGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts);
|
|
94
|
+
await storeGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts, identifiers);
|
|
94
95
|
// eslint-disable-next-line no-await-in-loop
|
|
95
|
-
await controllerGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts);
|
|
96
|
+
await controllerGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts, identifiers);
|
|
96
97
|
// eslint-disable-next-line no-await-in-loop
|
|
97
98
|
await templateGen.generateAndSaveFiles(moduleYamlPath, moduleDir, { force: opts === null || opts === void 0 ? void 0 : opts.force, skipOnConflict: opts === null || opts === void 0 ? void 0 : opts.skip, onlyIfMissing: !(opts === null || opts === void 0 ? void 0 : opts.withTemplates) });
|
|
98
99
|
// Find srcDir by probing upward for app.ts
|
|
@@ -43,12 +43,15 @@ const commandUtils_1 = require("../utils/commandUtils");
|
|
|
43
43
|
const configTypes_1 = require("../types/configTypes");
|
|
44
44
|
const migrationUtils_1 = require("../utils/migrationUtils");
|
|
45
45
|
function collectSchemaFromModules(appYamlPath) {
|
|
46
|
+
var _a, _b;
|
|
46
47
|
const appConfig = (0, commandUtils_1.loadAppConfig)(appYamlPath);
|
|
47
48
|
const moduleEntries = (0, commandUtils_1.getModuleEntries)(appConfig);
|
|
48
49
|
const projectRoot = path.dirname(appYamlPath);
|
|
49
50
|
const allAggregates = {};
|
|
50
51
|
const allValueObjects = new Set();
|
|
51
52
|
const sources = [];
|
|
53
|
+
const rawIdentifiers = (_b = (_a = appConfig.config) === null || _a === void 0 ? void 0 : _a.identifiers) !== null && _b !== void 0 ? _b : 'numeric';
|
|
54
|
+
const identifiers = (0, configTypes_1.normalizeIdentifierType)(rawIdentifiers);
|
|
52
55
|
for (const entry of moduleEntries) {
|
|
53
56
|
const moduleYamlPath = path.isAbsolute(entry.path)
|
|
54
57
|
? entry.path
|
|
@@ -79,7 +82,7 @@ function collectSchemaFromModules(appYamlPath) {
|
|
|
79
82
|
// eslint-disable-next-line no-console
|
|
80
83
|
console.log(colors_1.colors.gray(` Sources: ${sources.join(', ')}`));
|
|
81
84
|
}
|
|
82
|
-
return { aggregates: allAggregates, valueObjects: allValueObjects };
|
|
85
|
+
return { aggregates: allAggregates, valueObjects: allValueObjects, identifiers };
|
|
83
86
|
}
|
|
84
87
|
function handleMigrateCommit(yamlPath) {
|
|
85
88
|
try {
|
|
@@ -100,7 +103,7 @@ function handleMigrateCommit(yamlPath) {
|
|
|
100
103
|
}
|
|
101
104
|
// eslint-disable-next-line no-console
|
|
102
105
|
console.log(colors_1.colors.cyan('\nš Collecting aggregates from all modules...'));
|
|
103
|
-
const { aggregates: currentAggregates, valueObjects: currentValueObjects } = collectSchemaFromModules(resolvedYamlPath);
|
|
106
|
+
const { aggregates: currentAggregates, valueObjects: currentValueObjects, identifiers } = collectSchemaFromModules(resolvedYamlPath);
|
|
104
107
|
if (Object.keys(currentAggregates).length === 0) {
|
|
105
108
|
// eslint-disable-next-line no-console
|
|
106
109
|
console.log(colors_1.colors.yellow('ā ļø No aggregates found in module configuration.'));
|
|
@@ -119,7 +122,7 @@ function handleMigrateCommit(yamlPath) {
|
|
|
119
122
|
}
|
|
120
123
|
// eslint-disable-next-line no-console
|
|
121
124
|
console.log(colors_1.colors.cyan('\nš Comparing schemas...'));
|
|
122
|
-
const sqlStatements = (0, migrationUtils_1.compareSchemas)(oldState, currentAggregates, currentValueObjects);
|
|
125
|
+
const sqlStatements = (0, migrationUtils_1.compareSchemas)(oldState, currentAggregates, currentValueObjects, identifiers);
|
|
123
126
|
if (sqlStatements.length === 0 || sqlStatements.every(s => s.trim() === '' || s.startsWith('--'))) {
|
|
124
127
|
// eslint-disable-next-line no-console
|
|
125
128
|
console.log(colors_1.colors.yellow('ā ļø No changes detected. Schema is up to date.'));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { ModuleConfig } from '../types/configTypes';
|
|
1
|
+
import { ModuleConfig, IdentifierType } from '../types/configTypes';
|
|
2
2
|
export declare class ControllerGenerator {
|
|
3
|
+
private identifiers;
|
|
3
4
|
private getHttpDecorator;
|
|
4
5
|
private parseUseCase;
|
|
5
6
|
/**
|
|
@@ -10,6 +11,11 @@ export declare class ControllerGenerator {
|
|
|
10
11
|
* Check if auth config includes owner permission
|
|
11
12
|
*/
|
|
12
13
|
private hasOwnerAuth;
|
|
14
|
+
/**
|
|
15
|
+
* Determine which HTTP error classes from @currentjs/router are needed
|
|
16
|
+
* based on the auth configs of all endpoints in a controller.
|
|
17
|
+
*/
|
|
18
|
+
private getNeededHttpErrorImports;
|
|
13
19
|
/**
|
|
14
20
|
* Generate pre-fetch authentication/authorization check code.
|
|
15
21
|
* This runs before fetching the entity and validates authentication and role-based access.
|
|
@@ -52,10 +58,10 @@ export declare class ControllerGenerator {
|
|
|
52
58
|
private resolveLayout;
|
|
53
59
|
private generateApiController;
|
|
54
60
|
private generateWebController;
|
|
55
|
-
generateFromConfig(config: ModuleConfig): Record<string, string>;
|
|
56
|
-
generateFromYamlFile(yamlFilePath: string): Record<string, string>;
|
|
61
|
+
generateFromConfig(config: ModuleConfig, identifiers?: IdentifierType): Record<string, string>;
|
|
62
|
+
generateFromYamlFile(yamlFilePath: string, identifiers?: IdentifierType): Record<string, string>;
|
|
57
63
|
generateAndSaveFiles(yamlFilePath: string, moduleDir: string, opts?: {
|
|
58
64
|
force?: boolean;
|
|
59
65
|
skipOnConflict?: boolean;
|
|
60
|
-
}): Promise<string[]>;
|
|
66
|
+
}, identifiers?: IdentifierType): Promise<string[]>;
|
|
61
67
|
}
|
|
@@ -44,6 +44,9 @@ const childEntityUtils_1 = require("../utils/childEntityUtils");
|
|
|
44
44
|
const typeUtils_1 = require("../utils/typeUtils");
|
|
45
45
|
const constants_1 = require("../utils/constants");
|
|
46
46
|
class ControllerGenerator {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.identifiers = 'numeric';
|
|
49
|
+
}
|
|
47
50
|
getHttpDecorator(method) {
|
|
48
51
|
switch (method.toUpperCase()) {
|
|
49
52
|
case 'GET': return 'Get';
|
|
@@ -75,6 +78,28 @@ class ControllerGenerator {
|
|
|
75
78
|
const roles = this.normalizeAuth(auth);
|
|
76
79
|
return roles.includes(constants_1.AUTH_ROLES.OWNER);
|
|
77
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Determine which HTTP error classes from @currentjs/router are needed
|
|
83
|
+
* based on the auth configs of all endpoints in a controller.
|
|
84
|
+
*/
|
|
85
|
+
getNeededHttpErrorImports(auths) {
|
|
86
|
+
const needed = new Set();
|
|
87
|
+
for (const auth of auths) {
|
|
88
|
+
const roles = this.normalizeAuth(auth);
|
|
89
|
+
if (roles.length === 0 || (roles.length === 1 && roles[0] === constants_1.AUTH_ROLES.ALL))
|
|
90
|
+
continue;
|
|
91
|
+
needed.add('UnauthorizedError');
|
|
92
|
+
const roleChecks = roles.filter(r => r !== constants_1.AUTH_ROLES.OWNER && r !== constants_1.AUTH_ROLES.ALL && r !== constants_1.AUTH_ROLES.AUTHENTICATED);
|
|
93
|
+
if (roleChecks.length > 0) {
|
|
94
|
+
needed.add('ForbiddenError');
|
|
95
|
+
}
|
|
96
|
+
if (roles.includes(constants_1.AUTH_ROLES.OWNER)) {
|
|
97
|
+
needed.add('ForbiddenError');
|
|
98
|
+
needed.add('NotFoundError');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return Array.from(needed).sort();
|
|
102
|
+
}
|
|
78
103
|
/**
|
|
79
104
|
* Generate pre-fetch authentication/authorization check code.
|
|
80
105
|
* This runs before fetching the entity and validates authentication and role-based access.
|
|
@@ -89,14 +114,14 @@ class ControllerGenerator {
|
|
|
89
114
|
// If only 'authenticated' is specified
|
|
90
115
|
if (roles.length === 1 && roles[0] === constants_1.AUTH_ROLES.AUTHENTICATED) {
|
|
91
116
|
return `if (!context.request.user) {
|
|
92
|
-
throw new
|
|
117
|
+
throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
|
|
93
118
|
}`;
|
|
94
119
|
}
|
|
95
120
|
// If only 'owner' is specified - just require authentication here
|
|
96
121
|
// (owner check happens post-fetch)
|
|
97
122
|
if (roles.length === 1 && roles[0] === constants_1.AUTH_ROLES.OWNER) {
|
|
98
123
|
return `if (!context.request.user) {
|
|
99
|
-
throw new
|
|
124
|
+
throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
|
|
100
125
|
}`;
|
|
101
126
|
}
|
|
102
127
|
// Filter out 'owner' and 'all' for role checks (owner is checked post-fetch)
|
|
@@ -108,16 +133,16 @@ class ControllerGenerator {
|
|
|
108
133
|
if (roleChecks.length === 0) {
|
|
109
134
|
// Only owner (and maybe authenticated) - just require auth
|
|
110
135
|
return `if (!context.request.user) {
|
|
111
|
-
throw new
|
|
136
|
+
throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
|
|
112
137
|
}`;
|
|
113
138
|
}
|
|
114
139
|
if (roleChecks.length === 1 && !hasOwner) {
|
|
115
140
|
// Single role check
|
|
116
141
|
return `if (!context.request.user) {
|
|
117
|
-
throw new
|
|
142
|
+
throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
|
|
118
143
|
}
|
|
119
144
|
if (context.request.user.role !== '${roleChecks[0]}') {
|
|
120
|
-
throw new
|
|
145
|
+
throw new ForbiddenError('${constants_1.AUTH_ERRORS.INSUFFICIENT_PERMISSIONS}: ${roleChecks[0]} role required');
|
|
121
146
|
}`;
|
|
122
147
|
}
|
|
123
148
|
// Multiple roles OR owner - use OR logic
|
|
@@ -126,22 +151,22 @@ class ControllerGenerator {
|
|
|
126
151
|
if (hasOwner) {
|
|
127
152
|
// With owner: require auth, role check will be combined with owner check post-fetch
|
|
128
153
|
return `if (!context.request.user) {
|
|
129
|
-
throw new
|
|
154
|
+
throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
|
|
130
155
|
}`;
|
|
131
156
|
}
|
|
132
157
|
// Multiple roles without owner - check if user has ANY of the roles
|
|
133
158
|
const roleConditions = roleChecks.map(r => `context.request.user.role === '${r}'`).join(' || ');
|
|
134
159
|
return `if (!context.request.user) {
|
|
135
|
-
throw new
|
|
160
|
+
throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
|
|
136
161
|
}
|
|
137
162
|
if (!(${roleConditions})) {
|
|
138
|
-
throw new
|
|
163
|
+
throw new ForbiddenError('${constants_1.AUTH_ERRORS.INSUFFICIENT_PERMISSIONS}: one of [${roleChecks.join(', ')}] role required');
|
|
139
164
|
}`;
|
|
140
165
|
}
|
|
141
166
|
// Only 'authenticated' in the mix
|
|
142
167
|
if (hasAuthenticated) {
|
|
143
168
|
return `if (!context.request.user) {
|
|
144
|
-
throw new
|
|
169
|
+
throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
|
|
145
170
|
}`;
|
|
146
171
|
}
|
|
147
172
|
return '';
|
|
@@ -165,10 +190,10 @@ class ControllerGenerator {
|
|
|
165
190
|
// Owner validation (post-fetch for reads, via parent)
|
|
166
191
|
const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(${resultVar}.id);
|
|
167
192
|
if (resourceOwnerId === null) {
|
|
168
|
-
throw new
|
|
193
|
+
throw new NotFoundError('Resource not found');
|
|
169
194
|
}
|
|
170
195
|
if (resourceOwnerId !== context.request.user?.id) {
|
|
171
|
-
throw new
|
|
196
|
+
throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
|
|
172
197
|
}`;
|
|
173
198
|
}
|
|
174
199
|
const bypassConditions = bypassRoles.map(r => `context.request.user?.role === '${r}'`).join(' || ');
|
|
@@ -176,12 +201,12 @@ class ControllerGenerator {
|
|
|
176
201
|
// Owner validation (post-fetch for reads, via parent, bypassed for: ${bypassRoles.join(', ')})
|
|
177
202
|
const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(${resultVar}.id);
|
|
178
203
|
if (resourceOwnerId === null) {
|
|
179
|
-
throw new
|
|
204
|
+
throw new NotFoundError('Resource not found');
|
|
180
205
|
}
|
|
181
206
|
const isOwner = resourceOwnerId === context.request.user?.id;
|
|
182
207
|
const hasPrivilegedRole = ${bypassConditions};
|
|
183
208
|
if (!isOwner && !hasPrivilegedRole) {
|
|
184
|
-
throw new
|
|
209
|
+
throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
|
|
185
210
|
}`;
|
|
186
211
|
}
|
|
187
212
|
// Root entity: result has ownerId
|
|
@@ -189,7 +214,7 @@ class ControllerGenerator {
|
|
|
189
214
|
return `
|
|
190
215
|
// Owner validation (post-fetch for reads)
|
|
191
216
|
if (${resultVar}.ownerId !== context.request.user?.id) {
|
|
192
|
-
throw new
|
|
217
|
+
throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
|
|
193
218
|
}`;
|
|
194
219
|
}
|
|
195
220
|
const bypassConditions = bypassRoles.map(r => `context.request.user?.role === '${r}'`).join(' || ');
|
|
@@ -198,7 +223,7 @@ class ControllerGenerator {
|
|
|
198
223
|
const isOwner = ${resultVar}.ownerId === context.request.user?.id;
|
|
199
224
|
const hasPrivilegedRole = ${bypassConditions};
|
|
200
225
|
if (!isOwner && !hasPrivilegedRole) {
|
|
201
|
-
throw new
|
|
226
|
+
throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
|
|
202
227
|
}`;
|
|
203
228
|
}
|
|
204
229
|
/**
|
|
@@ -222,10 +247,10 @@ class ControllerGenerator {
|
|
|
222
247
|
// Pre-mutation owner validation
|
|
223
248
|
const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(input.id);
|
|
224
249
|
if (resourceOwnerId === null) {
|
|
225
|
-
throw new
|
|
250
|
+
throw new NotFoundError('Resource not found');
|
|
226
251
|
}
|
|
227
252
|
if (resourceOwnerId !== context.request.user?.id) {
|
|
228
|
-
throw new
|
|
253
|
+
throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
|
|
229
254
|
}
|
|
230
255
|
`;
|
|
231
256
|
}
|
|
@@ -235,12 +260,12 @@ class ControllerGenerator {
|
|
|
235
260
|
// Pre-mutation owner validation (bypassed for: ${bypassRoles.join(', ')})
|
|
236
261
|
const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(input.id);
|
|
237
262
|
if (resourceOwnerId === null) {
|
|
238
|
-
throw new
|
|
263
|
+
throw new NotFoundError('Resource not found');
|
|
239
264
|
}
|
|
240
265
|
const isOwner = resourceOwnerId === context.request.user?.id;
|
|
241
266
|
const hasPrivilegedRole = ${bypassConditions};
|
|
242
267
|
if (!isOwner && !hasPrivilegedRole) {
|
|
243
|
-
throw new
|
|
268
|
+
throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
|
|
244
269
|
}
|
|
245
270
|
`;
|
|
246
271
|
}
|
|
@@ -278,7 +303,8 @@ class ControllerGenerator {
|
|
|
278
303
|
parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ${childInfo.parentIdField}: context.request.parameters.${childInfo.parentIdField} });`;
|
|
279
304
|
}
|
|
280
305
|
else {
|
|
281
|
-
|
|
306
|
+
const idTs = (0, configTypes_1.idTsType)(this.identifiers);
|
|
307
|
+
parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ownerId: context.request.user?.id as ${idTs} });`;
|
|
282
308
|
}
|
|
283
309
|
}
|
|
284
310
|
else if (action === 'update') {
|
|
@@ -428,7 +454,8 @@ class ControllerGenerator {
|
|
|
428
454
|
parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ${childInfo.parentIdField}: context.request.parameters.${childInfo.parentIdField} });`;
|
|
429
455
|
}
|
|
430
456
|
else {
|
|
431
|
-
|
|
457
|
+
const idTs = (0, configTypes_1.idTsType)(this.identifiers);
|
|
458
|
+
parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ownerId: context.request.user?.id as ${idTs} });`;
|
|
432
459
|
}
|
|
433
460
|
}
|
|
434
461
|
else {
|
|
@@ -554,7 +581,9 @@ class ControllerGenerator {
|
|
|
554
581
|
const constructorParams = Array.from(useCaseModels)
|
|
555
582
|
.map(model => `private ${model.toLowerCase()}UseCase: ${model}UseCase`)
|
|
556
583
|
.join(',\n ');
|
|
557
|
-
|
|
584
|
+
const errorImports = this.getNeededHttpErrorImports(sortedEndpoints.map(e => e.auth));
|
|
585
|
+
const routerImports = ['Controller', 'Get', 'Post', 'Put', 'Delete', 'type IContext', ...errorImports].join(', ');
|
|
586
|
+
return `import { ${routerImports} } from '@currentjs/router';
|
|
558
587
|
${useCaseImports}
|
|
559
588
|
${dtoImportStatements}
|
|
560
589
|
|
|
@@ -621,7 +650,9 @@ ${methods.join('\n\n')}
|
|
|
621
650
|
${constructorParams.join(',\n ')}
|
|
622
651
|
) {}`
|
|
623
652
|
: 'constructor() {}';
|
|
624
|
-
|
|
653
|
+
const errorImports = this.getNeededHttpErrorImports(sortedPages.map(p => p.auth));
|
|
654
|
+
const routerImports = ['Controller', 'Get', 'Post', 'Render', 'type IContext', ...errorImports].join(', ');
|
|
655
|
+
return `import { ${routerImports} } from '@currentjs/router';
|
|
625
656
|
${useCaseImports}
|
|
626
657
|
${serviceImports.join('\n')}
|
|
627
658
|
${dtoImportStatements}
|
|
@@ -633,8 +664,9 @@ export class ${controllerName} {
|
|
|
633
664
|
${methods.join('\n\n')}
|
|
634
665
|
}`;
|
|
635
666
|
}
|
|
636
|
-
generateFromConfig(config) {
|
|
667
|
+
generateFromConfig(config, identifiers = 'numeric') {
|
|
637
668
|
const result = {};
|
|
669
|
+
this.identifiers = identifiers;
|
|
638
670
|
const childEntityMap = (0, childEntityUtils_1.buildChildEntityMap)(config);
|
|
639
671
|
// Generate API controllers
|
|
640
672
|
if (config.api) {
|
|
@@ -655,16 +687,16 @@ ${methods.join('\n\n')}
|
|
|
655
687
|
}
|
|
656
688
|
return result;
|
|
657
689
|
}
|
|
658
|
-
generateFromYamlFile(yamlFilePath) {
|
|
690
|
+
generateFromYamlFile(yamlFilePath, identifiers = 'numeric') {
|
|
659
691
|
const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
|
|
660
692
|
const config = (0, yaml_1.parse)(yamlContent);
|
|
661
693
|
if (!(0, configTypes_1.isValidModuleConfig)(config)) {
|
|
662
694
|
throw new Error('Configuration does not match new module format. Expected domain/useCases/api/web structure.');
|
|
663
695
|
}
|
|
664
|
-
return this.generateFromConfig(config);
|
|
696
|
+
return this.generateFromConfig(config, identifiers);
|
|
665
697
|
}
|
|
666
|
-
async generateAndSaveFiles(yamlFilePath, moduleDir, opts) {
|
|
667
|
-
const controllersByName = this.generateFromYamlFile(yamlFilePath);
|
|
698
|
+
async generateAndSaveFiles(yamlFilePath, moduleDir, opts, identifiers = 'numeric') {
|
|
699
|
+
const controllersByName = this.generateFromYamlFile(yamlFilePath, identifiers);
|
|
668
700
|
const controllersDir = path.join(moduleDir, 'infrastructure', 'controllers');
|
|
669
701
|
fs.mkdirSync(controllersDir, { recursive: true });
|
|
670
702
|
const generatedPaths = [];
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import { ModuleConfig } from '../types/configTypes';
|
|
1
|
+
import { ModuleConfig, IdentifierType } from '../types/configTypes';
|
|
2
2
|
export declare class DomainLayerGenerator {
|
|
3
3
|
private availableAggregates;
|
|
4
4
|
private availableValueObjects;
|
|
5
|
+
private identifiers;
|
|
5
6
|
private mapType;
|
|
6
7
|
private getDefaultValue;
|
|
7
8
|
private generateValueObject;
|
|
8
9
|
private generateAggregate;
|
|
9
|
-
generateFromConfig(config: ModuleConfig): Record<string, {
|
|
10
|
+
generateFromConfig(config: ModuleConfig, identifiers?: IdentifierType): Record<string, {
|
|
10
11
|
code: string;
|
|
11
12
|
type: 'entity' | 'valueObject';
|
|
12
13
|
}>;
|
|
13
|
-
generateFromYamlFile(yamlFilePath: string): Record<string, {
|
|
14
|
+
generateFromYamlFile(yamlFilePath: string, identifiers?: IdentifierType): Record<string, {
|
|
14
15
|
code: string;
|
|
15
16
|
type: 'entity' | 'valueObject';
|
|
16
17
|
}>;
|
|
17
18
|
generateAndSaveFiles(yamlFilePath: string, moduleDir: string, opts?: {
|
|
18
19
|
force?: boolean;
|
|
19
20
|
skipOnConflict?: boolean;
|
|
20
|
-
}): Promise<void>;
|
|
21
|
+
}, identifiers?: IdentifierType): Promise<void>;
|
|
21
22
|
}
|
|
@@ -46,6 +46,7 @@ class DomainLayerGenerator {
|
|
|
46
46
|
constructor() {
|
|
47
47
|
this.availableAggregates = new Set();
|
|
48
48
|
this.availableValueObjects = new Set();
|
|
49
|
+
this.identifiers = 'numeric';
|
|
49
50
|
}
|
|
50
51
|
mapType(yamlType) {
|
|
51
52
|
return (0, typeUtils_1.mapType)(yamlType, this.availableAggregates, this.availableValueObjects);
|
|
@@ -154,12 +155,13 @@ class DomainLayerGenerator {
|
|
|
154
155
|
.join('\n');
|
|
155
156
|
const imports = [entityImports, valueObjectImports, aggregateRefImports].filter(Boolean).join('\n');
|
|
156
157
|
// Generate constructor parameters: id, then ownerId (root) or parentId field (child)
|
|
157
|
-
const
|
|
158
|
+
const idTs = (0, configTypes_1.idTsType)(this.identifiers);
|
|
159
|
+
const constructorParams = [`public id: ${idTs}`];
|
|
158
160
|
if (childInfo) {
|
|
159
|
-
constructorParams.push(`public ${childInfo.parentIdField}:
|
|
161
|
+
constructorParams.push(`public ${childInfo.parentIdField}: ${idTs}`);
|
|
160
162
|
}
|
|
161
163
|
else {
|
|
162
|
-
constructorParams.push(
|
|
164
|
+
constructorParams.push(`public ownerId: ${idTs}`);
|
|
163
165
|
}
|
|
164
166
|
// Sort fields: required first, then optional
|
|
165
167
|
// Fields are required by default unless required: false
|
|
@@ -256,8 +258,9 @@ class DomainLayerGenerator {
|
|
|
256
258
|
${setterMethods}
|
|
257
259
|
}`;
|
|
258
260
|
}
|
|
259
|
-
generateFromConfig(config) {
|
|
261
|
+
generateFromConfig(config, identifiers = 'numeric') {
|
|
260
262
|
const result = {};
|
|
263
|
+
this.identifiers = identifiers;
|
|
261
264
|
// First pass: collect all aggregate and value object names
|
|
262
265
|
if (config.domain.aggregates) {
|
|
263
266
|
Object.keys(config.domain.aggregates).forEach(name => {
|
|
@@ -291,16 +294,16 @@ ${setterMethods}
|
|
|
291
294
|
}
|
|
292
295
|
return result;
|
|
293
296
|
}
|
|
294
|
-
generateFromYamlFile(yamlFilePath) {
|
|
297
|
+
generateFromYamlFile(yamlFilePath, identifiers = 'numeric') {
|
|
295
298
|
const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
|
|
296
299
|
const config = (0, yaml_1.parse)(yamlContent);
|
|
297
300
|
if (!(0, configTypes_1.isValidModuleConfig)(config)) {
|
|
298
301
|
throw new Error('Configuration does not match new module format. Expected domain.aggregates structure.');
|
|
299
302
|
}
|
|
300
|
-
return this.generateFromConfig(config);
|
|
303
|
+
return this.generateFromConfig(config, identifiers);
|
|
301
304
|
}
|
|
302
|
-
async generateAndSaveFiles(yamlFilePath, moduleDir, opts) {
|
|
303
|
-
const codeByEntity = this.generateFromYamlFile(yamlFilePath);
|
|
305
|
+
async generateAndSaveFiles(yamlFilePath, moduleDir, opts, identifiers = 'numeric') {
|
|
306
|
+
const codeByEntity = this.generateFromYamlFile(yamlFilePath, identifiers);
|
|
304
307
|
const entitiesDir = path.join(moduleDir, 'domain', 'entities');
|
|
305
308
|
const valueObjectsDir = path.join(moduleDir, 'domain', 'valueObjects');
|
|
306
309
|
fs.mkdirSync(entitiesDir, { recursive: true });
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { ModuleConfig } from '../types/configTypes';
|
|
1
|
+
import { ModuleConfig, IdentifierType } from '../types/configTypes';
|
|
2
2
|
export declare class DtoGenerator {
|
|
3
3
|
private availableAggregates;
|
|
4
4
|
private availableValueObjects;
|
|
5
|
+
private identifiers;
|
|
5
6
|
private mapType;
|
|
6
7
|
private isValueObjectType;
|
|
7
8
|
private getValidationCode;
|
|
@@ -12,10 +13,10 @@ export declare class DtoGenerator {
|
|
|
12
13
|
* Collect types that need to be imported for a use case DTO.
|
|
13
14
|
*/
|
|
14
15
|
private collectRequiredImports;
|
|
15
|
-
generateFromConfig(config: ModuleConfig): Record<string, string>;
|
|
16
|
-
generateFromYamlFile(yamlFilePath: string): Record<string, string>;
|
|
16
|
+
generateFromConfig(config: ModuleConfig, identifiers?: IdentifierType): Record<string, string>;
|
|
17
|
+
generateFromYamlFile(yamlFilePath: string, identifiers?: IdentifierType): Record<string, string>;
|
|
17
18
|
generateAndSaveFiles(yamlFilePath: string, moduleDir: string, opts?: {
|
|
18
19
|
force?: boolean;
|
|
19
20
|
skipOnConflict?: boolean;
|
|
20
|
-
}): Promise<void>;
|
|
21
|
+
}, identifiers?: IdentifierType): Promise<void>;
|
|
21
22
|
}
|
|
@@ -46,6 +46,7 @@ class DtoGenerator {
|
|
|
46
46
|
constructor() {
|
|
47
47
|
this.availableAggregates = new Map();
|
|
48
48
|
this.availableValueObjects = new Map();
|
|
49
|
+
this.identifiers = 'numeric';
|
|
49
50
|
}
|
|
50
51
|
mapType(yamlType) {
|
|
51
52
|
return (0, typeUtils_1.mapType)(yamlType, this.availableAggregates, this.availableValueObjects);
|
|
@@ -116,13 +117,17 @@ class DtoGenerator {
|
|
|
116
117
|
// Handle identifier (for get, update, delete)
|
|
117
118
|
if (inputConfig.identifier) {
|
|
118
119
|
const fieldName = inputConfig.identifier;
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
const idTs = (0, configTypes_1.idTsType)(this.identifiers);
|
|
121
|
+
const idTransform = this.identifiers === 'numeric'
|
|
122
|
+
? `typeof b.${fieldName} === 'string' ? parseInt(b.${fieldName}, 10) : b.${fieldName} as number`
|
|
123
|
+
: `b.${fieldName} as string`;
|
|
124
|
+
fieldDeclarations.push(` readonly ${fieldName}: ${idTs};`);
|
|
125
|
+
constructorParams.push(`${fieldName}: ${idTs}`);
|
|
121
126
|
constructorAssignments.push(` this.${fieldName} = ${fieldName};`);
|
|
122
127
|
validationChecks.push(` if (b.${fieldName} === undefined || b.${fieldName} === null) {
|
|
123
128
|
throw new Error('${fieldName} is required');
|
|
124
129
|
}`);
|
|
125
|
-
fieldTransforms.push(` ${fieldName}:
|
|
130
|
+
fieldTransforms.push(` ${fieldName}: ${idTransform}`);
|
|
126
131
|
}
|
|
127
132
|
// Handle pagination
|
|
128
133
|
if (inputConfig.pagination) {
|
|
@@ -158,21 +163,26 @@ class DtoGenerator {
|
|
|
158
163
|
const isCreateAction = !inputConfig.identifier && !inputConfig.partial;
|
|
159
164
|
if (isCreateAction) {
|
|
160
165
|
const ownerOrParentField = childInfo ? childInfo.parentIdField : 'ownerId';
|
|
161
|
-
|
|
162
|
-
|
|
166
|
+
const idTs = (0, configTypes_1.idTsType)(this.identifiers);
|
|
167
|
+
const ownerTransform = this.identifiers === 'numeric'
|
|
168
|
+
? `typeof b.${ownerOrParentField} === 'string' ? parseInt(b.${ownerOrParentField}, 10) : b.${ownerOrParentField} as number`
|
|
169
|
+
: `b.${ownerOrParentField} as string`;
|
|
170
|
+
fieldDeclarations.push(` readonly ${ownerOrParentField}: ${idTs};`);
|
|
171
|
+
constructorParams.push(`${ownerOrParentField}: ${idTs}`);
|
|
163
172
|
constructorAssignments.push(` this.${ownerOrParentField} = ${ownerOrParentField};`);
|
|
164
173
|
validationChecks.push(` if (b.${ownerOrParentField} === undefined || b.${ownerOrParentField} === null) {
|
|
165
174
|
throw new Error('${ownerOrParentField} is required');
|
|
166
175
|
}`);
|
|
167
|
-
fieldTransforms.push(` ${ownerOrParentField}:
|
|
176
|
+
fieldTransforms.push(` ${ownerOrParentField}: ${ownerTransform}`);
|
|
168
177
|
}
|
|
169
178
|
// Add fields
|
|
179
|
+
const aggIdTs = (0, configTypes_1.idTsType)(this.identifiers);
|
|
170
180
|
fieldsToInclude.forEach(([fieldName, fieldConfig]) => {
|
|
171
181
|
if (fieldName === 'id' || fieldConfig.auto)
|
|
172
182
|
return;
|
|
173
183
|
const isAggRef = (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
|
|
174
|
-
const tsType = isAggRef ?
|
|
175
|
-
const effectiveFieldType = isAggRef ? 'number' : fieldConfig.type;
|
|
184
|
+
const tsType = isAggRef ? aggIdTs : this.mapType(fieldConfig.type);
|
|
185
|
+
const effectiveFieldType = isAggRef ? (this.identifiers === 'numeric' ? 'number' : 'string') : fieldConfig.type;
|
|
176
186
|
// Aggregate references are always optional in DTOs; other fields default to required
|
|
177
187
|
const isRequired = !isAggRef && !inputConfig.partial && fieldConfig.required !== false;
|
|
178
188
|
const optional = isRequired ? '' : '?';
|
|
@@ -252,8 +262,9 @@ ${transformsStr}
|
|
|
252
262
|
const fromMappings = [];
|
|
253
263
|
// Always include id for entity outputs
|
|
254
264
|
if (outputConfig.from) {
|
|
255
|
-
|
|
256
|
-
|
|
265
|
+
const idTs = (0, configTypes_1.idTsType)(this.identifiers);
|
|
266
|
+
fieldDeclarations.push(` readonly id: ${idTs};`);
|
|
267
|
+
constructorParams.push(`id: ${idTs}`);
|
|
257
268
|
fromMappings.push(` id: entity.id`);
|
|
258
269
|
}
|
|
259
270
|
// Handle fields from aggregate (pick)
|
|
@@ -265,11 +276,12 @@ ${transformsStr}
|
|
|
265
276
|
fieldsToInclude = fieldsToInclude.filter(([fieldName]) => outputConfig.pick.includes(fieldName));
|
|
266
277
|
}
|
|
267
278
|
// Add fields
|
|
279
|
+
const idTs = (0, configTypes_1.idTsType)(this.identifiers);
|
|
268
280
|
fieldsToInclude.forEach(([fieldName, fieldConfig]) => {
|
|
269
281
|
if (fieldName === 'id')
|
|
270
282
|
return;
|
|
271
283
|
const isAggRef = (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
|
|
272
|
-
const tsType = isAggRef ?
|
|
284
|
+
const tsType = isAggRef ? idTs : this.mapType(fieldConfig.type);
|
|
273
285
|
const isOptional = fieldConfig.required === false || isAggRef;
|
|
274
286
|
const optional = isOptional ? '?' : '';
|
|
275
287
|
fieldDeclarations.push(` readonly ${fieldName}${optional}: ${tsType};`);
|
|
@@ -447,8 +459,9 @@ ${mappingsStr}
|
|
|
447
459
|
}
|
|
448
460
|
return { valueObjects, entities };
|
|
449
461
|
}
|
|
450
|
-
generateFromConfig(config) {
|
|
462
|
+
generateFromConfig(config, identifiers = 'numeric') {
|
|
451
463
|
const result = {};
|
|
464
|
+
this.identifiers = identifiers;
|
|
452
465
|
// Collect all aggregates
|
|
453
466
|
if (config.domain.aggregates) {
|
|
454
467
|
Object.entries(config.domain.aggregates).forEach(([name, aggConfig]) => {
|
|
@@ -498,16 +511,16 @@ ${mappingsStr}
|
|
|
498
511
|
});
|
|
499
512
|
return result;
|
|
500
513
|
}
|
|
501
|
-
generateFromYamlFile(yamlFilePath) {
|
|
514
|
+
generateFromYamlFile(yamlFilePath, identifiers = 'numeric') {
|
|
502
515
|
const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
|
|
503
516
|
const config = (0, yaml_1.parse)(yamlContent);
|
|
504
517
|
if (!(0, configTypes_1.isValidModuleConfig)(config)) {
|
|
505
518
|
throw new Error('Configuration does not match new module format. Expected useCases structure.');
|
|
506
519
|
}
|
|
507
|
-
return this.generateFromConfig(config);
|
|
520
|
+
return this.generateFromConfig(config, identifiers);
|
|
508
521
|
}
|
|
509
|
-
async generateAndSaveFiles(yamlFilePath, moduleDir, opts) {
|
|
510
|
-
const dtosByName = this.generateFromYamlFile(yamlFilePath);
|
|
522
|
+
async generateAndSaveFiles(yamlFilePath, moduleDir, opts, identifiers = 'numeric') {
|
|
523
|
+
const dtosByName = this.generateFromYamlFile(yamlFilePath, identifiers);
|
|
511
524
|
const dtoDir = path.join(moduleDir, 'application', 'dto');
|
|
512
525
|
fs.mkdirSync(dtoDir, { recursive: true });
|
|
513
526
|
for (const [name, code] of Object.entries(dtosByName)) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { ModuleConfig } from '../types/configTypes';
|
|
1
|
+
import { ModuleConfig, IdentifierType } from '../types/configTypes';
|
|
2
2
|
export declare class ServiceGenerator {
|
|
3
3
|
private availableAggregates;
|
|
4
|
+
private identifiers;
|
|
4
5
|
private mapType;
|
|
5
6
|
private getDefaultHandlerReturnType;
|
|
6
7
|
private buildHandlerContextMap;
|
|
@@ -19,10 +20,10 @@ export declare class ServiceGenerator {
|
|
|
19
20
|
private generateListByParentMethod;
|
|
20
21
|
private generateGetResourceOwnerMethod;
|
|
21
22
|
private generateService;
|
|
22
|
-
generateFromConfig(config: ModuleConfig): Record<string, string>;
|
|
23
|
-
generateFromYamlFile(yamlFilePath: string): Record<string, string>;
|
|
23
|
+
generateFromConfig(config: ModuleConfig, identifiers?: IdentifierType): Record<string, string>;
|
|
24
|
+
generateFromYamlFile(yamlFilePath: string, identifiers?: IdentifierType): Record<string, string>;
|
|
24
25
|
generateAndSaveFiles(yamlFilePath: string, moduleDir: string, opts?: {
|
|
25
26
|
force?: boolean;
|
|
26
27
|
skipOnConflict?: boolean;
|
|
27
|
-
}): Promise<void>;
|
|
28
|
+
}, identifiers?: IdentifierType): Promise<void>;
|
|
28
29
|
}
|