@currentjs/gen 0.5.0 → 0.5.2
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 +19 -0
- package/README.md +374 -996
- package/dist/cli.js +28 -10
- package/dist/commands/createModel.d.ts +1 -0
- package/dist/commands/createModel.js +764 -0
- package/dist/commands/createModule.js +13 -0
- package/dist/commands/generateAll.d.ts +1 -0
- package/dist/commands/generateAll.js +1 -1
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/{createApp.js → init.js} +2 -2
- package/dist/commands/migrateCommit.js +33 -68
- package/dist/generators/controllerGenerator.d.ts +7 -0
- package/dist/generators/controllerGenerator.js +56 -17
- package/dist/generators/domainLayerGenerator.js +51 -8
- package/dist/generators/dtoGenerator.js +13 -8
- package/dist/generators/serviceGenerator.d.ts +6 -0
- package/dist/generators/serviceGenerator.js +219 -23
- package/dist/generators/storeGenerator.d.ts +4 -0
- package/dist/generators/storeGenerator.js +116 -9
- package/dist/generators/templateGenerator.d.ts +1 -0
- package/dist/generators/templateGenerator.js +8 -2
- package/dist/generators/templates/appTemplates.js +1 -1
- package/dist/generators/templates/data/cursorRulesTemplate +11 -755
- package/dist/generators/templates/data/frontendScriptTemplate +11 -4
- package/dist/generators/templates/data/mainViewTemplate +1 -0
- package/dist/generators/templates/storeTemplates.d.ts +1 -1
- package/dist/generators/templates/storeTemplates.js +3 -26
- package/dist/generators/useCaseGenerator.js +6 -3
- package/dist/types/configTypes.d.ts +6 -0
- package/dist/utils/migrationUtils.d.ts +9 -19
- package/dist/utils/migrationUtils.js +80 -110
- package/dist/utils/promptUtils.d.ts +37 -0
- package/dist/utils/promptUtils.js +149 -0
- package/dist/utils/typeUtils.d.ts +4 -0
- package/dist/utils/typeUtils.js +7 -0
- package/package.json +1 -1
- package/dist/commands/createApp.d.ts +0 -1
- package/dist/commands/migratePush.d.ts +0 -1
- package/dist/commands/migratePush.js +0 -135
- package/dist/commands/migrateUpdate.d.ts +0 -1
- package/dist/commands/migrateUpdate.js +0 -147
- package/dist/commands/newGenerateAll.d.ts +0 -4
- package/dist/commands/newGenerateAll.js +0 -336
- package/dist/generators/domainModelGenerator.d.ts +0 -41
- package/dist/generators/domainModelGenerator.js +0 -242
- package/dist/generators/newControllerGenerator.d.ts +0 -55
- package/dist/generators/newControllerGenerator.js +0 -644
- package/dist/generators/newServiceGenerator.d.ts +0 -19
- package/dist/generators/newServiceGenerator.js +0 -266
- package/dist/generators/newStoreGenerator.d.ts +0 -39
- package/dist/generators/newStoreGenerator.js +0 -408
- package/dist/generators/newTemplateGenerator.d.ts +0 -29
- package/dist/generators/newTemplateGenerator.js +0 -510
- package/dist/generators/storeGeneratorV2.d.ts +0 -31
- package/dist/generators/storeGeneratorV2.js +0 -190
- package/dist/generators/templates/controllerTemplates.d.ts +0 -43
- package/dist/generators/templates/controllerTemplates.js +0 -82
- package/dist/generators/templates/newStoreTemplates.d.ts +0 -5
- package/dist/generators/templates/newStoreTemplates.js +0 -141
- package/dist/generators/templates/serviceTemplates.d.ts +0 -16
- package/dist/generators/templates/serviceTemplates.js +0 -59
- package/dist/generators/templates/validationTemplates.d.ts +0 -25
- package/dist/generators/templates/validationTemplates.js +0 -66
- package/dist/generators/templates/viewTemplates.d.ts +0 -25
- package/dist/generators/templates/viewTemplates.js +0 -491
- package/dist/generators/validationGenerator.d.ts +0 -29
- package/dist/generators/validationGenerator.js +0 -250
- package/dist/utils/new_parts_of_migrationUtils.d.ts +0 -0
- package/dist/utils/new_parts_of_migrationUtils.js +0 -164
- package/howto.md +0 -667
|
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const cliUtils_1 = require("../utils/cliUtils");
|
|
40
40
|
const yaml_1 = require("yaml");
|
|
41
|
+
const colors_1 = require("../utils/colors");
|
|
41
42
|
function moduleYamlTemplate(moduleName) {
|
|
42
43
|
const entityName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1);
|
|
43
44
|
const lower = moduleName.charAt(0).toLowerCase() + moduleName.slice(1);
|
|
@@ -226,4 +227,16 @@ function handleCreateModule(name) {
|
|
|
226
227
|
appConfig.modules[moduleKey] = { path: moduleYamlRel };
|
|
227
228
|
}
|
|
228
229
|
fs.writeFileSync(appYamlPath, (0, yaml_1.stringify)(appConfig), 'utf8');
|
|
230
|
+
const output = `
|
|
231
|
+
${colors_1.colors.green(`Module ${colors_1.colors.bold(name)} has been created`)}
|
|
232
|
+
|
|
233
|
+
Run command ${colors_1.colors.green(colors_1.colors.bold(`current create model ${name}:modelname`))}
|
|
234
|
+
where ${colors_1.colors.italic(colors_1.colors.yellow('modelname'))} is name of the model in your module.
|
|
235
|
+
At the end, that command will suggest you to create CRUD actions for the model and further steps.
|
|
236
|
+
You can run this command as many times as needed.
|
|
237
|
+
|
|
238
|
+
Alternatively, you may consider modifying module's config manually, and then run ${colors_1.colors.bold(colors_1.colors.green('current generate'))}
|
|
239
|
+
${colors_1.colors.gray('config:')} ${colors_1.colors.cyan(colors_1.colors.italic(moduleYamlFile))}
|
|
240
|
+
`;
|
|
241
|
+
console.log(output);
|
|
229
242
|
}
|
|
@@ -94,7 +94,7 @@ async function handleGenerateAll(yamlPathArg, _outArg, moduleName, opts) {
|
|
|
94
94
|
// eslint-disable-next-line no-await-in-loop
|
|
95
95
|
await controllerGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts);
|
|
96
96
|
// eslint-disable-next-line no-await-in-loop
|
|
97
|
-
await templateGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts);
|
|
97
|
+
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
98
|
// Find srcDir by probing upward for app.ts
|
|
99
99
|
let probeDir = moduleDir;
|
|
100
100
|
let srcDir = null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleInit(rawName?: string): void;
|
|
@@ -33,11 +33,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
36
|
+
exports.handleInit = handleInit;
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
38
|
const cliUtils_1 = require("../utils/cliUtils");
|
|
39
39
|
const appTemplates_1 = require("../generators/templates/appTemplates");
|
|
40
|
-
function
|
|
40
|
+
function handleInit(rawName) {
|
|
41
41
|
const targetRoot = rawName ? (0, cliUtils_1.toAbsolute)(rawName) : process.cwd();
|
|
42
42
|
(0, cliUtils_1.ensureDir)(targetRoot);
|
|
43
43
|
// Basic structure using constants
|
|
@@ -39,70 +39,41 @@ const path = __importStar(require("path"));
|
|
|
39
39
|
const yaml_1 = require("yaml");
|
|
40
40
|
const colors_1 = require("../utils/colors");
|
|
41
41
|
const cliUtils_1 = require("../utils/cliUtils");
|
|
42
|
+
const commandUtils_1 = require("../utils/commandUtils");
|
|
43
|
+
const configTypes_1 = require("../types/configTypes");
|
|
42
44
|
const migrationUtils_1 = require("../utils/migrationUtils");
|
|
43
|
-
function
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const projectRoot = path.dirname(
|
|
47
|
-
const
|
|
45
|
+
function collectAggregatesFromModules(appYamlPath) {
|
|
46
|
+
const appConfig = (0, commandUtils_1.loadAppConfig)(appYamlPath);
|
|
47
|
+
const moduleEntries = (0, commandUtils_1.getModuleEntries)(appConfig);
|
|
48
|
+
const projectRoot = path.dirname(appYamlPath);
|
|
49
|
+
const allAggregates = {};
|
|
48
50
|
const sources = [];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
for (const entry of Object.values(config.modules)) {
|
|
58
|
-
const modulePath = entry.path;
|
|
59
|
-
if (!modulePath)
|
|
60
|
-
continue;
|
|
61
|
-
const moduleYamlPath = path.isAbsolute(modulePath)
|
|
62
|
-
? modulePath
|
|
63
|
-
: path.resolve(projectRoot, modulePath);
|
|
64
|
-
if (fs.existsSync(moduleYamlPath)) {
|
|
65
|
-
const moduleYamlContent = fs.readFileSync(moduleYamlPath, 'utf8');
|
|
66
|
-
const moduleConfig = (0, yaml_1.parse)(moduleYamlContent);
|
|
67
|
-
if (moduleConfig.models) {
|
|
68
|
-
allModels.push(...moduleConfig.models);
|
|
69
|
-
moduleCount++;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (moduleCount > 0) {
|
|
74
|
-
sources.push(`app.yaml modules section (${moduleCount} module(s))`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
// Also check for module YAMLs in src/modules/*/module.yaml (as fallback)
|
|
78
|
-
const modulesDir = path.join(projectRoot, 'src', 'modules');
|
|
79
|
-
if (fs.existsSync(modulesDir)) {
|
|
80
|
-
const moduleFolders = fs.readdirSync(modulesDir).filter(f => {
|
|
81
|
-
const stat = fs.statSync(path.join(modulesDir, f));
|
|
82
|
-
return stat.isDirectory();
|
|
83
|
-
});
|
|
84
|
-
let moduleYamlCount = 0;
|
|
85
|
-
for (const moduleFolder of moduleFolders) {
|
|
86
|
-
const moduleYamlPath = path.join(modulesDir, moduleFolder, 'module.yaml');
|
|
87
|
-
if (fs.existsSync(moduleYamlPath)) {
|
|
88
|
-
const moduleYamlContent = fs.readFileSync(moduleYamlPath, 'utf8');
|
|
89
|
-
const moduleConfig = (0, yaml_1.parse)(moduleYamlContent);
|
|
90
|
-
if (moduleConfig.models) {
|
|
91
|
-
allModels.push(...moduleConfig.models);
|
|
92
|
-
moduleYamlCount++;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
51
|
+
for (const entry of moduleEntries) {
|
|
52
|
+
const moduleYamlPath = path.isAbsolute(entry.path)
|
|
53
|
+
? entry.path
|
|
54
|
+
: path.resolve(projectRoot, entry.path);
|
|
55
|
+
if (!fs.existsSync(moduleYamlPath)) {
|
|
56
|
+
// eslint-disable-next-line no-console
|
|
57
|
+
console.warn(colors_1.colors.yellow(` Module YAML not found: ${moduleYamlPath}`));
|
|
58
|
+
continue;
|
|
95
59
|
}
|
|
96
|
-
|
|
97
|
-
|
|
60
|
+
const moduleYamlContent = fs.readFileSync(moduleYamlPath, 'utf8');
|
|
61
|
+
const moduleConfig = (0, yaml_1.parse)(moduleYamlContent);
|
|
62
|
+
if (!(0, configTypes_1.isValidModuleConfig)(moduleConfig)) {
|
|
63
|
+
// eslint-disable-next-line no-console
|
|
64
|
+
console.warn(colors_1.colors.yellow(` Skipping ${moduleYamlPath}: not a valid module config (missing domain/useCases)`));
|
|
65
|
+
continue;
|
|
98
66
|
}
|
|
67
|
+
const aggregates = moduleConfig.domain.aggregates;
|
|
68
|
+
const count = Object.keys(aggregates).length;
|
|
69
|
+
Object.assign(allAggregates, aggregates);
|
|
70
|
+
sources.push(`${entry.name} (${count} aggregate(s))`);
|
|
99
71
|
}
|
|
100
|
-
// Log sources
|
|
101
72
|
if (sources.length > 0) {
|
|
102
73
|
// eslint-disable-next-line no-console
|
|
103
74
|
console.log(colors_1.colors.gray(` Sources: ${sources.join(', ')}`));
|
|
104
75
|
}
|
|
105
|
-
return
|
|
76
|
+
return allAggregates;
|
|
106
77
|
}
|
|
107
78
|
function handleMigrateCommit(yamlPath) {
|
|
108
79
|
try {
|
|
@@ -116,24 +87,21 @@ function handleMigrateCommit(yamlPath) {
|
|
|
116
87
|
console.log(colors_1.colors.gray(` Project root: ${projectRoot}`));
|
|
117
88
|
// eslint-disable-next-line no-console
|
|
118
89
|
console.log(colors_1.colors.gray(` Migrations dir: ${migrationsDir}`));
|
|
119
|
-
// Ensure migrations directory exists
|
|
120
90
|
if (!fs.existsSync(migrationsDir)) {
|
|
121
91
|
fs.mkdirSync(migrationsDir, { recursive: true });
|
|
122
92
|
// eslint-disable-next-line no-console
|
|
123
93
|
console.log(colors_1.colors.green(` ✓ Created migrations directory`));
|
|
124
94
|
}
|
|
125
|
-
// Collect all models from YAML files
|
|
126
95
|
// eslint-disable-next-line no-console
|
|
127
|
-
console.log(colors_1.colors.cyan('\n📋 Collecting
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
96
|
+
console.log(colors_1.colors.cyan('\n📋 Collecting aggregates from all modules...'));
|
|
97
|
+
const currentAggregates = collectAggregatesFromModules(resolvedYamlPath);
|
|
98
|
+
if (Object.keys(currentAggregates).length === 0) {
|
|
130
99
|
// eslint-disable-next-line no-console
|
|
131
|
-
console.log(colors_1.colors.yellow('⚠️ No
|
|
100
|
+
console.log(colors_1.colors.yellow('⚠️ No aggregates found in module configuration.'));
|
|
132
101
|
return;
|
|
133
102
|
}
|
|
134
103
|
// eslint-disable-next-line no-console
|
|
135
|
-
console.log(colors_1.colors.green(`✓ Found ${
|
|
136
|
-
// Load previous state
|
|
104
|
+
console.log(colors_1.colors.green(`✓ Found ${Object.keys(currentAggregates).length} aggregate(s): ${Object.keys(currentAggregates).join(', ')}`));
|
|
137
105
|
const oldState = (0, migrationUtils_1.loadSchemaState)(stateFilePath);
|
|
138
106
|
if (oldState) {
|
|
139
107
|
// eslint-disable-next-line no-console
|
|
@@ -143,16 +111,14 @@ function handleMigrateCommit(yamlPath) {
|
|
|
143
111
|
// eslint-disable-next-line no-console
|
|
144
112
|
console.log(colors_1.colors.cyan('📖 No previous schema state found - will generate initial migration'));
|
|
145
113
|
}
|
|
146
|
-
// Compare schemas and generate SQL
|
|
147
114
|
// eslint-disable-next-line no-console
|
|
148
115
|
console.log(colors_1.colors.cyan('\n🔍 Comparing schemas...'));
|
|
149
|
-
const sqlStatements = (0, migrationUtils_1.compareSchemas)(oldState,
|
|
116
|
+
const sqlStatements = (0, migrationUtils_1.compareSchemas)(oldState, currentAggregates);
|
|
150
117
|
if (sqlStatements.length === 0 || sqlStatements.every(s => s.trim() === '' || s.startsWith('--'))) {
|
|
151
118
|
// eslint-disable-next-line no-console
|
|
152
119
|
console.log(colors_1.colors.yellow('⚠️ No changes detected. Schema is up to date.'));
|
|
153
120
|
return;
|
|
154
121
|
}
|
|
155
|
-
// Generate migration file
|
|
156
122
|
const timestamp = (0, migrationUtils_1.generateTimestamp)();
|
|
157
123
|
const migrationFileName = (0, migrationUtils_1.getMigrationFileName)(timestamp);
|
|
158
124
|
const migrationFilePath = path.join(migrationsDir, migrationFileName);
|
|
@@ -163,9 +129,8 @@ function handleMigrateCommit(yamlPath) {
|
|
|
163
129
|
`;
|
|
164
130
|
const migrationContent = migrationHeader + sqlStatements.join('\n');
|
|
165
131
|
fs.writeFileSync(migrationFilePath, migrationContent);
|
|
166
|
-
// Update state file
|
|
167
132
|
const newState = {
|
|
168
|
-
|
|
133
|
+
aggregates: currentAggregates,
|
|
169
134
|
version: timestamp,
|
|
170
135
|
timestamp: new Date().toISOString()
|
|
171
136
|
};
|
|
@@ -43,6 +43,13 @@ export declare class ControllerGenerator {
|
|
|
43
43
|
* meant for static routes (e.g. /create).
|
|
44
44
|
*/
|
|
45
45
|
private sortRoutesBySpecificity;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve layout from YAML value.
|
|
48
|
+
* - undefined => use fallback (if provided)
|
|
49
|
+
* - "none" or "" => no layout
|
|
50
|
+
* - other values => use explicit layout name
|
|
51
|
+
*/
|
|
52
|
+
private resolveLayout;
|
|
46
53
|
private generateApiController;
|
|
47
54
|
private generateWebController;
|
|
48
55
|
generateFromConfig(config: ModuleConfig): Record<string, string>;
|
|
@@ -244,18 +244,26 @@ class ControllerGenerator {
|
|
|
244
244
|
}
|
|
245
245
|
`;
|
|
246
246
|
}
|
|
247
|
-
generateApiEndpointMethod(endpoint, resourceName, childInfo) {
|
|
247
|
+
generateApiEndpointMethod(endpoint, resourceName, useCasesConfig, childInfo) {
|
|
248
|
+
var _a;
|
|
248
249
|
const { model, action } = this.parseUseCase(endpoint.useCase);
|
|
249
250
|
const methodName = action;
|
|
250
251
|
const decorator = this.getHttpDecorator(endpoint.method);
|
|
251
252
|
const useCaseVar = `${model.toLowerCase()}UseCase`;
|
|
252
253
|
const inputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Input`;
|
|
253
254
|
const outputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Output`;
|
|
255
|
+
const useCaseDef = (_a = useCasesConfig[model]) === null || _a === void 0 ? void 0 : _a[action];
|
|
256
|
+
const isVoidOutput = !(useCaseDef === null || useCaseDef === void 0 ? void 0 : useCaseDef.output) || useCaseDef.output === 'void';
|
|
254
257
|
const dtoImports = new Set();
|
|
258
|
+
const voidOutputDtos = new Set();
|
|
255
259
|
dtoImports.add(`${model}${(0, typeUtils_1.capitalize)(action)}`);
|
|
260
|
+
if (isVoidOutput) {
|
|
261
|
+
voidOutputDtos.add(`${model}${(0, typeUtils_1.capitalize)(action)}`);
|
|
262
|
+
}
|
|
256
263
|
// Generate auth check (pre-fetch)
|
|
257
264
|
const authCheck = this.generateAuthCheck(endpoint.auth);
|
|
258
265
|
const authLine = authCheck ? `\n ${authCheck}\n` : '';
|
|
266
|
+
const hasOwner = this.hasOwnerAuth(endpoint.auth);
|
|
259
267
|
// Build parsing logic
|
|
260
268
|
// For create: root gets ownerId from user, child gets parentId from URL params
|
|
261
269
|
let parseLogic;
|
|
@@ -282,7 +290,6 @@ class ControllerGenerator {
|
|
|
282
290
|
// Generate owner checks:
|
|
283
291
|
// - For mutations (update, delete): PRE-mutation check (before operation)
|
|
284
292
|
// - For reads (get): POST-fetch check (after fetching)
|
|
285
|
-
const hasOwner = this.hasOwnerAuth(endpoint.auth);
|
|
286
293
|
const isMutation = action === 'update' || action === 'delete';
|
|
287
294
|
const isRead = action === 'get';
|
|
288
295
|
// Pre-mutation owner check for write operations
|
|
@@ -295,22 +302,25 @@ class ControllerGenerator {
|
|
|
295
302
|
: '';
|
|
296
303
|
// Generate output transformation based on action
|
|
297
304
|
let outputTransform;
|
|
298
|
-
if (action === '
|
|
299
|
-
outputTransform = `return ${outputClass}.from(result);`;
|
|
300
|
-
}
|
|
301
|
-
else if (action === 'delete') {
|
|
305
|
+
if (isVoidOutput || action === 'delete') {
|
|
302
306
|
outputTransform = `return result;`;
|
|
303
307
|
}
|
|
308
|
+
else if (action === 'list') {
|
|
309
|
+
outputTransform = `return ${outputClass}.from(result);`;
|
|
310
|
+
}
|
|
304
311
|
else {
|
|
305
312
|
outputTransform = `return ${outputClass}.from(result);`;
|
|
306
313
|
}
|
|
314
|
+
const useCaseArgs = (hasOwner && action === 'list')
|
|
315
|
+
? 'input, context.request.user?.id as number'
|
|
316
|
+
: 'input';
|
|
307
317
|
const method = ` @${decorator}('${endpoint.path}')
|
|
308
318
|
async ${methodName}(context: IContext): Promise<any> {${authLine}
|
|
309
319
|
${parseLogic}${preMutationOwnerCheck}
|
|
310
|
-
const result = await this.${useCaseVar}.${action}(
|
|
320
|
+
const result = await this.${useCaseVar}.${action}(${useCaseArgs});${postFetchOwnerCheck}
|
|
311
321
|
${outputTransform}
|
|
312
322
|
}`;
|
|
313
|
-
return { method, dtoImports };
|
|
323
|
+
return { method, dtoImports, voidOutputDtos };
|
|
314
324
|
}
|
|
315
325
|
generateWebPageMethod(page, resourceName, layout, methodIndex, childInfo, withChildChildren) {
|
|
316
326
|
const method = page.method || 'GET';
|
|
@@ -333,12 +343,16 @@ class ControllerGenerator {
|
|
|
333
343
|
const authLine = authCheck ? `\n ${authCheck}\n` : '';
|
|
334
344
|
// For GET requests with views (display pages)
|
|
335
345
|
if (method === 'GET' && page.view) {
|
|
336
|
-
const
|
|
346
|
+
const pageLayout = this.resolveLayout(page.layout, layout);
|
|
347
|
+
const renderDecorator = pageLayout
|
|
348
|
+
? `\n @Render("${page.view}", "${pageLayout}")`
|
|
349
|
+
: `\n @Render("${page.view}")`;
|
|
337
350
|
if (page.useCase) {
|
|
338
351
|
const { model, action } = this.parseUseCase(page.useCase);
|
|
339
352
|
const useCaseVar = `${model.toLowerCase()}UseCase`;
|
|
340
353
|
const inputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Input`;
|
|
341
354
|
dtoImports.add(`${model}${(0, typeUtils_1.capitalize)(action)}`);
|
|
355
|
+
const hasOwner = this.hasOwnerAuth(page.auth);
|
|
342
356
|
let parseLogic;
|
|
343
357
|
if (page.path.includes(':id')) {
|
|
344
358
|
parseLogic = `const input = ${inputClass}.parse({ id: context.request.parameters.id });`;
|
|
@@ -349,8 +363,6 @@ class ControllerGenerator {
|
|
|
349
363
|
else {
|
|
350
364
|
parseLogic = `const input = ${inputClass}.parse({});`;
|
|
351
365
|
}
|
|
352
|
-
// Generate post-fetch owner check for GET pages (reads only)
|
|
353
|
-
const hasOwner = this.hasOwnerAuth(page.auth);
|
|
354
366
|
const isReadAction = action === 'get' || action === 'list';
|
|
355
367
|
const postFetchOwnerCheck = (hasOwner && isReadAction)
|
|
356
368
|
? this.generatePostFetchOwnerCheck(page.auth, 'result', useCaseVar, childInfo)
|
|
@@ -374,12 +386,15 @@ class ControllerGenerator {
|
|
|
374
386
|
else {
|
|
375
387
|
returnExpr = 'result';
|
|
376
388
|
}
|
|
389
|
+
const useCaseArgs = (hasOwner && action === 'list')
|
|
390
|
+
? 'input, context.request.user?.id as number'
|
|
391
|
+
: 'input';
|
|
377
392
|
const loadChildCode = loadChildBlocks.length ? '\n ' + loadChildBlocks.join('\n ') + '\n ' : '';
|
|
378
393
|
const methodCode = `${renderDecorator}
|
|
379
394
|
@${decorator}('${page.path}')
|
|
380
395
|
async ${methodName}(context: IContext): Promise<any> {${authLine}
|
|
381
396
|
${parseLogic}
|
|
382
|
-
const result = await this.${useCaseVar}.${action}(
|
|
397
|
+
const result = await this.${useCaseVar}.${action}(${useCaseArgs});${postFetchOwnerCheck}${loadChildCode}
|
|
383
398
|
return ${returnExpr};
|
|
384
399
|
}`;
|
|
385
400
|
return { method: methodCode, dtoImports };
|
|
@@ -491,26 +506,49 @@ class ControllerGenerator {
|
|
|
491
506
|
return aParamCount - bParamCount;
|
|
492
507
|
});
|
|
493
508
|
}
|
|
494
|
-
|
|
509
|
+
/**
|
|
510
|
+
* Resolve layout from YAML value.
|
|
511
|
+
* - undefined => use fallback (if provided)
|
|
512
|
+
* - "none" or "" => no layout
|
|
513
|
+
* - other values => use explicit layout name
|
|
514
|
+
*/
|
|
515
|
+
resolveLayout(layout, fallback) {
|
|
516
|
+
if (layout === undefined) {
|
|
517
|
+
return fallback;
|
|
518
|
+
}
|
|
519
|
+
const normalized = layout.trim();
|
|
520
|
+
if (!normalized || normalized.toLowerCase() === 'none') {
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
return normalized;
|
|
524
|
+
}
|
|
525
|
+
generateApiController(resourceName, prefix, endpoints, useCasesConfig, childInfo) {
|
|
495
526
|
const controllerName = `${resourceName}ApiController`;
|
|
496
527
|
// Determine which use cases and DTOs are referenced
|
|
497
528
|
const useCaseModels = new Set();
|
|
498
529
|
const allDtoImports = new Set();
|
|
530
|
+
const allVoidOutputDtos = new Set();
|
|
499
531
|
const methods = [];
|
|
500
532
|
const sortedEndpoints = this.sortRoutesBySpecificity(endpoints);
|
|
501
533
|
sortedEndpoints.forEach(endpoint => {
|
|
502
534
|
const { model } = this.parseUseCase(endpoint.useCase);
|
|
503
535
|
useCaseModels.add(model);
|
|
504
|
-
const { method, dtoImports } = this.generateApiEndpointMethod(endpoint, resourceName, childInfo);
|
|
536
|
+
const { method, dtoImports, voidOutputDtos } = this.generateApiEndpointMethod(endpoint, resourceName, useCasesConfig, childInfo);
|
|
505
537
|
methods.push(method);
|
|
506
538
|
dtoImports.forEach(d => allDtoImports.add(d));
|
|
539
|
+
voidOutputDtos.forEach(d => allVoidOutputDtos.add(d));
|
|
507
540
|
});
|
|
508
541
|
// Generate imports
|
|
509
542
|
const useCaseImports = Array.from(useCaseModels)
|
|
510
543
|
.map(model => `import { ${model}UseCase } from '../../application/useCases/${model}UseCase';`)
|
|
511
544
|
.join('\n');
|
|
512
545
|
const dtoImportStatements = Array.from(allDtoImports)
|
|
513
|
-
.map(dto =>
|
|
546
|
+
.map(dto => {
|
|
547
|
+
if (allVoidOutputDtos.has(dto)) {
|
|
548
|
+
return `import { ${dto}Input } from '../../application/dto/${dto}';`;
|
|
549
|
+
}
|
|
550
|
+
return `import { ${dto}Input, ${dto}Output } from '../../application/dto/${dto}';`;
|
|
551
|
+
})
|
|
514
552
|
.join('\n');
|
|
515
553
|
// Generate constructor parameters
|
|
516
554
|
const constructorParams = Array.from(useCaseModels)
|
|
@@ -602,7 +640,7 @@ ${methods.join('\n\n')}
|
|
|
602
640
|
if (config.api) {
|
|
603
641
|
Object.entries(config.api).forEach(([resourceName, resourceConfig]) => {
|
|
604
642
|
const childInfo = childEntityMap.get(resourceName);
|
|
605
|
-
const code = this.generateApiController(resourceName, resourceConfig.prefix, resourceConfig.endpoints, childInfo);
|
|
643
|
+
const code = this.generateApiController(resourceName, resourceConfig.prefix, resourceConfig.endpoints, config.useCases, childInfo);
|
|
606
644
|
result[`${resourceName}Api`] = code;
|
|
607
645
|
});
|
|
608
646
|
}
|
|
@@ -610,7 +648,8 @@ ${methods.join('\n\n')}
|
|
|
610
648
|
if (config.web) {
|
|
611
649
|
Object.entries(config.web).forEach(([resourceName, resourceConfig]) => {
|
|
612
650
|
const childInfo = childEntityMap.get(resourceName);
|
|
613
|
-
const
|
|
651
|
+
const moduleLayout = this.resolveLayout(resourceConfig.layout, 'main_view');
|
|
652
|
+
const code = this.generateWebController(resourceName, resourceConfig.prefix, moduleLayout, resourceConfig.pages, config, childInfo);
|
|
614
653
|
result[`${resourceName}Web`] = code;
|
|
615
654
|
});
|
|
616
655
|
}
|
|
@@ -146,7 +146,14 @@ class DomainLayerGenerator {
|
|
|
146
146
|
})
|
|
147
147
|
.filter((imp, idx, arr) => arr.indexOf(imp) === idx) // dedupe
|
|
148
148
|
.join('\n');
|
|
149
|
-
|
|
149
|
+
// Generate imports for aggregate references in fields (e.g. idea: { type: Idea })
|
|
150
|
+
const aggregateRefImports = fields
|
|
151
|
+
.filter(([, fieldConfig]) => (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates) &&
|
|
152
|
+
fieldConfig.type !== name)
|
|
153
|
+
.map(([, fieldConfig]) => `import { ${fieldConfig.type} } from './${fieldConfig.type}';`)
|
|
154
|
+
.filter((imp, idx, arr) => arr.indexOf(imp) === idx)
|
|
155
|
+
.join('\n');
|
|
156
|
+
const imports = [entityImports, valueObjectImports, aggregateRefImports].filter(Boolean).join('\n');
|
|
150
157
|
// Generate constructor parameters: id, then ownerId (root) or parentId field (child)
|
|
151
158
|
const constructorParams = ['public id: number'];
|
|
152
159
|
if (childInfo) {
|
|
@@ -157,9 +164,12 @@ class DomainLayerGenerator {
|
|
|
157
164
|
}
|
|
158
165
|
// Sort fields: required first, then optional
|
|
159
166
|
// Fields are required by default unless required: false
|
|
167
|
+
// Aggregate references are always treated as optional (store can't populate them from FK alone)
|
|
160
168
|
const sortedFields = fields.sort((a, b) => {
|
|
161
|
-
const
|
|
162
|
-
const
|
|
169
|
+
const aIsAggRef = (0, typeUtils_1.isAggregateReference)(a[1].type, this.availableAggregates);
|
|
170
|
+
const bIsAggRef = (0, typeUtils_1.isAggregateReference)(b[1].type, this.availableAggregates);
|
|
171
|
+
const aRequired = a[1].required !== false && !a[1].auto && !aIsAggRef;
|
|
172
|
+
const bRequired = b[1].required !== false && !b[1].auto && !bIsAggRef;
|
|
163
173
|
if (aRequired === bRequired)
|
|
164
174
|
return 0;
|
|
165
175
|
return aRequired ? -1 : 1;
|
|
@@ -176,9 +186,9 @@ class DomainLayerGenerator {
|
|
|
176
186
|
}
|
|
177
187
|
});
|
|
178
188
|
sortedFields.forEach(([fieldName, fieldConfig]) => {
|
|
189
|
+
const isAggRef = (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
|
|
179
190
|
const tsType = enumTypeNames[fieldName] || this.mapType(fieldConfig.type);
|
|
180
|
-
|
|
181
|
-
const isOptional = fieldConfig.required === false;
|
|
191
|
+
const isOptional = fieldConfig.required === false || isAggRef;
|
|
182
192
|
const hasDefault = fieldConfig.auto;
|
|
183
193
|
let param = `public ${fieldName}`;
|
|
184
194
|
if (isOptional && !hasDefault) {
|
|
@@ -195,22 +205,55 @@ class DomainLayerGenerator {
|
|
|
195
205
|
const setterMethods = sortedFields
|
|
196
206
|
.filter(([fieldName, fieldConfig]) => !fieldConfig.auto && fieldName !== 'id')
|
|
197
207
|
.map(([fieldName, fieldConfig]) => {
|
|
208
|
+
const isAggRef = (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
|
|
198
209
|
const tsType = enumTypeNames[fieldName] || this.mapType(fieldConfig.type);
|
|
199
210
|
const methodName = `set${(0, typeUtils_1.capitalize)(fieldName)}`;
|
|
200
|
-
|
|
201
|
-
const isOptional = fieldConfig.required === false;
|
|
211
|
+
const isOptional = fieldConfig.required === false || isAggRef;
|
|
202
212
|
return `
|
|
203
213
|
${methodName}(${fieldName}: ${tsType}${isOptional ? ' | undefined' : ''}): void {
|
|
204
214
|
this.${fieldName} = ${fieldName};
|
|
205
215
|
}`;
|
|
206
216
|
})
|
|
207
217
|
.join('\n');
|
|
218
|
+
// Generate validation logic from field constraints
|
|
219
|
+
// we don't use constraints at models, since we use DTOs (use cases) for validation
|
|
220
|
+
// Model – is a place not for validation, but for business logic!
|
|
221
|
+
// kept this code for reference
|
|
222
|
+
/*
|
|
223
|
+
const validations: string[] = [];
|
|
224
|
+
sortedFields.forEach(([fieldName, fieldConfig]) => {
|
|
225
|
+
const { constraints } = fieldConfig;
|
|
226
|
+
if (!constraints) return;
|
|
227
|
+
|
|
228
|
+
if (constraints.min !== undefined) {
|
|
229
|
+
validations.push(` if (this.${fieldName} < ${constraints.min}) {
|
|
230
|
+
throw new Error('${name}.${fieldName} must be at least ${constraints.min}');
|
|
231
|
+
}`);
|
|
232
|
+
}
|
|
233
|
+
if (constraints.max !== undefined) {
|
|
234
|
+
validations.push(` if (this.${fieldName} > ${constraints.max}) {
|
|
235
|
+
throw new Error('${name}.${fieldName} must be at most ${constraints.max}');
|
|
236
|
+
}`);
|
|
237
|
+
}
|
|
238
|
+
if (constraints.pattern) {
|
|
239
|
+
validations.push(` if (!/${constraints.pattern}/.test(String(this.${fieldName}))) {
|
|
240
|
+
throw new Error('${name}.${fieldName} does not match required pattern');
|
|
241
|
+
}`);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const constructorBody = validations.length > 0
|
|
246
|
+
? `\n this.validate();\n }\n\n private validate(): void {\n${validations.join('\n')}\n }`
|
|
247
|
+
: ' }';
|
|
248
|
+
*/
|
|
249
|
+
const constructorBody = '';
|
|
208
250
|
const rootComment = config.root ? '// Aggregate Root\n' : '';
|
|
209
251
|
const enumTypeDefsCode = enumTypeDefinitions.length > 0 ? enumTypeDefinitions.join('\n') + '\n\n' : '';
|
|
210
252
|
return `${imports ? imports + '\n\n' : ''}${enumTypeDefsCode}${rootComment}export class ${name} {
|
|
211
253
|
public constructor(
|
|
212
254
|
${constructorParamsStr}
|
|
213
|
-
) {
|
|
255
|
+
) {${constructorBody}
|
|
256
|
+
}
|
|
214
257
|
${setterMethods}
|
|
215
258
|
}`;
|
|
216
259
|
}
|
|
@@ -171,15 +171,17 @@ class DtoGenerator {
|
|
|
171
171
|
fieldsToInclude.forEach(([fieldName, fieldConfig]) => {
|
|
172
172
|
if (fieldName === 'id' || fieldConfig.auto)
|
|
173
173
|
return;
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
const
|
|
174
|
+
const isAggRef = (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
|
|
175
|
+
const tsType = isAggRef ? 'number' : this.mapType(fieldConfig.type);
|
|
176
|
+
const effectiveFieldType = isAggRef ? 'number' : fieldConfig.type;
|
|
177
|
+
// Aggregate references are always optional in DTOs; other fields default to required
|
|
178
|
+
const isRequired = !isAggRef && !inputConfig.partial && fieldConfig.required !== false;
|
|
177
179
|
const optional = isRequired ? '' : '?';
|
|
178
180
|
fieldDeclarations.push(` readonly ${fieldName}${optional}: ${tsType};`);
|
|
179
181
|
constructorParams.push(`${fieldName}${optional}: ${tsType}`);
|
|
180
182
|
constructorAssignments.push(` this.${fieldName} = ${fieldName};`);
|
|
181
|
-
validationChecks.push(...this.getValidationCode(fieldName,
|
|
182
|
-
fieldTransforms.push(` ${fieldName}: ${this.getTransformCode(fieldName,
|
|
183
|
+
validationChecks.push(...this.getValidationCode(fieldName, effectiveFieldType, isRequired));
|
|
184
|
+
fieldTransforms.push(` ${fieldName}: ${this.getTransformCode(fieldName, effectiveFieldType)}`);
|
|
183
185
|
});
|
|
184
186
|
}
|
|
185
187
|
// Handle filters
|
|
@@ -267,12 +269,15 @@ ${transformsStr}
|
|
|
267
269
|
fieldsToInclude.forEach(([fieldName, fieldConfig]) => {
|
|
268
270
|
if (fieldName === 'id')
|
|
269
271
|
return;
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
+
const isAggRef = (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
|
|
273
|
+
const tsType = isAggRef ? 'number' : this.mapType(fieldConfig.type);
|
|
274
|
+
const isOptional = fieldConfig.required === false || isAggRef;
|
|
272
275
|
const optional = isOptional ? '?' : '';
|
|
273
276
|
fieldDeclarations.push(` readonly ${fieldName}${optional}: ${tsType};`);
|
|
274
277
|
constructorParams.push(`${fieldName}${optional}: ${tsType}`);
|
|
275
|
-
fromMappings.push(
|
|
278
|
+
fromMappings.push(isAggRef
|
|
279
|
+
? ` ${fieldName}: entity.${fieldName}?.id`
|
|
280
|
+
: ` ${fieldName}: entity.${fieldName}`);
|
|
276
281
|
});
|
|
277
282
|
}
|
|
278
283
|
// Handle includes (nested objects)
|
|
@@ -2,6 +2,12 @@ import { ModuleConfig } from '../types/configTypes';
|
|
|
2
2
|
export declare class ServiceGenerator {
|
|
3
3
|
private availableAggregates;
|
|
4
4
|
private mapType;
|
|
5
|
+
private getDefaultHandlerReturnType;
|
|
6
|
+
private buildHandlerContextMap;
|
|
7
|
+
private deriveInputType;
|
|
8
|
+
private deriveCustomHandlerTypes;
|
|
9
|
+
private getInputDtoFields;
|
|
10
|
+
private computeDtoFieldsForHandler;
|
|
5
11
|
private generateListHandler;
|
|
6
12
|
private generateGetHandler;
|
|
7
13
|
private generateCreateHandler;
|