@currentjs/gen 0.2.2 → 0.3.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +256 -0
  3. package/dist/cli.js +26 -0
  4. package/dist/commands/createApp.js +2 -0
  5. package/dist/commands/generateAll.js +153 -29
  6. package/dist/commands/migrateCommit.d.ts +1 -0
  7. package/dist/commands/migrateCommit.js +201 -0
  8. package/dist/generators/controllerGenerator.d.ts +7 -0
  9. package/dist/generators/controllerGenerator.js +60 -29
  10. package/dist/generators/domainModelGenerator.d.ts +7 -0
  11. package/dist/generators/domainModelGenerator.js +57 -3
  12. package/dist/generators/serviceGenerator.d.ts +16 -1
  13. package/dist/generators/serviceGenerator.js +125 -12
  14. package/dist/generators/storeGenerator.d.ts +8 -0
  15. package/dist/generators/storeGenerator.js +133 -7
  16. package/dist/generators/templateGenerator.d.ts +19 -0
  17. package/dist/generators/templateGenerator.js +216 -11
  18. package/dist/generators/templates/appTemplates.d.ts +8 -7
  19. package/dist/generators/templates/appTemplates.js +11 -1572
  20. package/dist/generators/templates/data/appTsTemplate +39 -0
  21. package/dist/generators/templates/data/appYamlTemplate +4 -0
  22. package/dist/generators/templates/data/cursorRulesTemplate +671 -0
  23. package/dist/generators/templates/data/errorTemplate +28 -0
  24. package/dist/generators/templates/data/frontendScriptTemplate +739 -0
  25. package/dist/generators/templates/data/mainViewTemplate +16 -0
  26. package/dist/generators/templates/data/translationsTemplate +68 -0
  27. package/dist/generators/templates/data/tsConfigTemplate +19 -0
  28. package/dist/generators/templates/viewTemplates.d.ts +10 -1
  29. package/dist/generators/templates/viewTemplates.js +138 -6
  30. package/dist/generators/validationGenerator.d.ts +5 -0
  31. package/dist/generators/validationGenerator.js +51 -0
  32. package/dist/utils/constants.d.ts +3 -0
  33. package/dist/utils/constants.js +5 -2
  34. package/dist/utils/migrationUtils.d.ts +49 -0
  35. package/dist/utils/migrationUtils.js +291 -0
  36. package/howto.md +157 -65
  37. package/package.json +3 -2
@@ -87,6 +87,7 @@ async function handleGenerateAll(yamlPathArg, _outArg, moduleName, opts) {
87
87
  const storeGen = new storeGenerator_1.StoreGenerator();
88
88
  const tplGen = new templateGenerator_1.TemplateGenerator();
89
89
  const initsBySrcDir = new Map();
90
+ const moduleConfigByFolder = new Map(); // Cache module configs by folder name
90
91
  // Run modules sequentially to avoid overlapping interactive prompts
91
92
  for (const moduleYamlRel of filteredModules) {
92
93
  const moduleYamlPath = path.isAbsolute(moduleYamlRel)
@@ -98,6 +99,11 @@ async function handleGenerateAll(yamlPathArg, _outArg, moduleName, opts) {
98
99
  continue;
99
100
  }
100
101
  const moduleDir = path.dirname(moduleYamlPath);
102
+ const moduleFolderName = path.basename(moduleDir);
103
+ // Parse and cache module config for dependency detection
104
+ const moduleYamlContent = fs.readFileSync(moduleYamlPath, 'utf8');
105
+ const moduleConfig = (0, yaml_1.parse)(moduleYamlContent);
106
+ moduleConfigByFolder.set(moduleFolderName, moduleConfig);
101
107
  // Output folders inside module structure
102
108
  const domainOut = path.join(moduleDir, 'domain', 'entities');
103
109
  const appOut = path.join(moduleDir, 'application');
@@ -131,6 +137,50 @@ async function handleGenerateAll(yamlPathArg, _outArg, moduleName, opts) {
131
137
  if (!srcDir)
132
138
  continue;
133
139
  const list = (_b = initsBySrcDir.get(srcDir)) !== null && _b !== void 0 ? _b : [];
140
+ // First, add entries for all models in this module (even without controllers)
141
+ // This ensures dependency stores are initialized
142
+ if (moduleConfig && moduleConfig.models) {
143
+ const m = moduleYamlPath.match(/modules\/([^/]+)\//);
144
+ const moduleFolder = m ? m[1] : path.basename(moduleDir);
145
+ for (const model of moduleConfig.models) {
146
+ const entityName = model.name;
147
+ const entityVar = entityName.charAt(0).toLowerCase() + entityName.slice(1);
148
+ // Check if we already have an entry for this entity
149
+ if (list.find(x => x.entityName === entityName)) {
150
+ continue; // Skip if already added via controller
151
+ }
152
+ const storeImportPath = `${constants_1.PATH_PATTERNS.MODULES_RELATIVE}${moduleFolder}/${constants_1.PATH_PATTERNS.INFRASTRUCTURE}/${constants_1.PATH_PATTERNS.STORES}/${entityName}${constants_1.GENERATOR_SUFFIXES.STORE}`;
153
+ const serviceImportPath = `${constants_1.PATH_PATTERNS.MODULES_RELATIVE}${moduleFolder}/${constants_1.PATH_PATTERNS.APPLICATION}/${constants_1.PATH_PATTERNS.SERVICES}/${entityName}${constants_1.GENERATOR_SUFFIXES.SERVICE}`;
154
+ // Detect dependencies
155
+ const storeDeps = [];
156
+ const serviceDeps = [];
157
+ if (model.fields) {
158
+ model.fields.forEach((field) => {
159
+ const isRelationship = moduleConfig.models.some((m) => m.name === field.type);
160
+ if (isRelationship) {
161
+ storeDeps.push(field.type);
162
+ serviceDeps.push(field.type);
163
+ }
164
+ });
165
+ }
166
+ // Add a minimal init entry (no controller, just for dependency resolution)
167
+ const init = {
168
+ ctrlName: '', // No controller
169
+ entityName,
170
+ entityVar,
171
+ importController: '',
172
+ importStore: `import { ${entityName}Store } from '${storeImportPath}';`,
173
+ importService: `import { ${entityName}Service } from '${serviceImportPath}';`,
174
+ importAuth: undefined,
175
+ wiring: [],
176
+ registration: '',
177
+ storeDependencies: storeDeps,
178
+ serviceDependencies: serviceDeps
179
+ };
180
+ list.push(init);
181
+ }
182
+ }
183
+ // Then process controllers
134
184
  for (const filePath of generatedControllers) {
135
185
  const rel = path
136
186
  .relative(srcDir, filePath)
@@ -151,25 +201,52 @@ async function handleGenerateAll(yamlPathArg, _outArg, moduleName, opts) {
151
201
  continue;
152
202
  const storeImportPath = `${constants_1.PATH_PATTERNS.MODULES_RELATIVE}${moduleFolder}/${constants_1.PATH_PATTERNS.INFRASTRUCTURE}/${constants_1.PATH_PATTERNS.STORES}/${entityName}${constants_1.GENERATOR_SUFFIXES.STORE}`;
153
203
  const serviceImportPath = `${constants_1.PATH_PATTERNS.MODULES_RELATIVE}${moduleFolder}/${constants_1.PATH_PATTERNS.APPLICATION}/${constants_1.PATH_PATTERNS.SERVICES}/${entityName}${constants_1.GENERATOR_SUFFIXES.SERVICE}`;
154
- //const authImportPath = `./modules/${moduleFolder}/application/auth/AuthService`;
155
- // const hasAuth = true;//fs.existsSync(path.join(srcDir, authImportPath + '.ts'));
156
- const init = {
157
- ctrlName,
158
- entityName,
159
- entityVar,
160
- importController: `import { ${ctrlName} } from '${importPath}';`,
161
- importStore: `import { ${entityName}Store } from '${storeImportPath}';`,
162
- importService: `import { ${entityName}Service } from '${serviceImportPath}';`,
163
- importAuth: undefined, //hasAuth ? `import { AuthService } from '${authImportPath}';` : undefined,
164
- wiring: [
165
- `const ${entityVar}Store = new ${entityName}Store(db);`,
166
- `const ${entityVar}Service = new ${entityName}Service(${entityVar}Store);`
167
- ],
168
- registration: `new ${ctrlName}(${entityVar}Service)`
169
- };
170
- // Avoid duplicates by controller (keep both Api/Web), dedupe wiring later by entityName
171
- if (!list.find((x) => x.ctrlName === init.ctrlName))
172
- list.push(init);
204
+ // Look up the correct module config for this controller
205
+ const controllerModuleConfig = moduleConfigByFolder.get(moduleFolder);
206
+ // Detect store and service dependencies from module config
207
+ const storeDeps = [];
208
+ const serviceDeps = [];
209
+ if (controllerModuleConfig && controllerModuleConfig.models) {
210
+ const model = controllerModuleConfig.models.find((m) => m.name === entityName);
211
+ if (model && model.fields) {
212
+ model.fields.forEach((field) => {
213
+ // Check if this field is a relationship (type is another model name)
214
+ const isRelationship = controllerModuleConfig.models.some((m) => m.name === field.type);
215
+ if (isRelationship) {
216
+ storeDeps.push(field.type);
217
+ serviceDeps.push(field.type);
218
+ }
219
+ });
220
+ }
221
+ }
222
+ // Check if we already have a base entry for this entity (from all models)
223
+ const existingEntry = list.find(x => x.entityName === entityName && !x.ctrlName);
224
+ if (existingEntry) {
225
+ // Update existing entry to add controller info
226
+ existingEntry.ctrlName = ctrlName;
227
+ existingEntry.importController = `import { ${ctrlName} } from '${importPath}';`;
228
+ existingEntry.registration = `new ${ctrlName}(${entityVar}Service)`;
229
+ // Keep the dependencies we already detected
230
+ }
231
+ else {
232
+ // Create new entry with controller
233
+ const init = {
234
+ ctrlName,
235
+ entityName,
236
+ entityVar,
237
+ importController: `import { ${ctrlName} } from '${importPath}';`,
238
+ importStore: `import { ${entityName}Store } from '${storeImportPath}';`,
239
+ importService: `import { ${entityName}Service } from '${serviceImportPath}';`,
240
+ importAuth: undefined,
241
+ wiring: [],
242
+ registration: `new ${ctrlName}(${entityVar}Service)`,
243
+ storeDependencies: storeDeps,
244
+ serviceDependencies: serviceDeps
245
+ };
246
+ if (!list.find((x) => x.ctrlName === init.ctrlName)) {
247
+ list.push(init);
248
+ }
249
+ }
173
250
  }
174
251
  initsBySrcDir.set(srcDir, list);
175
252
  await storeGen.generateAndSaveFiles(moduleYamlPath, infraOut, { force: !!(opts === null || opts === void 0 ? void 0 : opts.force), skipOnConflict: !!(opts === null || opts === void 0 ? void 0 : opts.skip) });
@@ -232,12 +309,17 @@ async function handleGenerateAll(yamlPathArg, _outArg, moduleName, opts) {
232
309
  importLines.push(`import { ProviderMysql } from '@currentjs/provider-mysql';`);
233
310
  }
234
311
  for (const init of controllerInits) {
235
- const maybe = [init.importController, init.importStore, init.importService];
312
+ const maybe = [init.importStore, init.importService];
313
+ // Only add controller import if entity has a controller
314
+ if (init.importController && init.importController.trim() !== '') {
315
+ maybe.push(init.importController);
316
+ }
236
317
  if (init.importAuth)
237
318
  maybe.push(init.importAuth);
238
319
  for (const line of maybe) {
239
- if (!appTs.includes(line) && !importLines.includes(line))
320
+ if (line && line.trim() !== '' && !appTs.includes(line) && !importLines.includes(line)) {
240
321
  importLines.push(line);
322
+ }
241
323
  }
242
324
  }
243
325
  if (importLines.length) {
@@ -249,20 +331,62 @@ async function handleGenerateAll(yamlPathArg, _outArg, moduleName, opts) {
249
331
  if (toAdd.length)
250
332
  appTs = toAdd.join('\n') + '\n' + appTs;
251
333
  }
252
- // Compose fresh block content
334
+ // Compose fresh block content with dependency-aware ordering
253
335
  const wiringLines = [];
254
336
  // DB placeholder once
255
337
  wiringLines.push(ensureDbLine);
256
- const wiredEntities = new Set();
257
- for (const init of controllerInits) {
258
- if (!wiredEntities.has(init.entityName)) {
259
- wiringLines.push(...init.wiring);
260
- wiredEntities.add(init.entityName);
338
+ // Deduplicate by entityName
339
+ const uniqueInits = Array.from(new Map(controllerInits.map(init => [init.entityName, init])).values());
340
+ // Sort entities by dependencies (entities with no deps first)
341
+ const sorted = [];
342
+ const processed = new Set();
343
+ const addEntity = (init) => {
344
+ if (processed.has(init.entityName))
345
+ return;
346
+ // Process dependencies first
347
+ if (init.storeDependencies) {
348
+ for (const depName of init.storeDependencies) {
349
+ const depInit = uniqueInits.find(i => i.entityName === depName);
350
+ if (depInit && !processed.has(depName)) {
351
+ addEntity(depInit);
352
+ }
353
+ }
261
354
  }
355
+ sorted.push(init);
356
+ processed.add(init.entityName);
357
+ };
358
+ uniqueInits.forEach(init => addEntity(init));
359
+ // Generate wiring for stores and services
360
+ for (const init of sorted) {
361
+ const entityVar = init.entityVar;
362
+ const entityName = init.entityName;
363
+ // Build store constructor parameters
364
+ const storeParams = ['db'];
365
+ if (init.storeDependencies && init.storeDependencies.length > 0) {
366
+ init.storeDependencies.forEach(depName => {
367
+ const depVar = depName.charAt(0).toLowerCase() + depName.slice(1);
368
+ storeParams.push(`${depVar}Store`);
369
+ });
370
+ }
371
+ // Build service constructor parameters
372
+ const serviceParams = [`${entityVar}Store`];
373
+ if (init.serviceDependencies && init.serviceDependencies.length > 0) {
374
+ init.serviceDependencies.forEach(depName => {
375
+ const depVar = depName.charAt(0).toLowerCase() + depName.slice(1);
376
+ serviceParams.push(`${depVar}Store`);
377
+ });
378
+ }
379
+ wiringLines.push(`const ${entityVar}Store = new ${entityName}Store(${storeParams.join(', ')});`);
380
+ wiringLines.push(`const ${entityVar}Service = new ${entityName}Service(${serviceParams.join(', ')});`);
262
381
  }
263
- const registrations = controllerInits.map((i) => i.registration);
382
+ // Only include registrations for entities with controllers
383
+ const registrations = controllerInits
384
+ .filter(i => i.registration && i.registration.trim() !== '')
385
+ .map(i => i.registration);
264
386
  wiringLines.push('const controllers = [');
265
- wiringLines.push(` ${registrations.join(',\n ')}`);
387
+ if (registrations.length > 0) {
388
+ wiringLines.push(` ${registrations.join(',\n ')}`);
389
+ }
266
390
  wiringLines.push('];');
267
391
  const startMarker = constants_1.GENERATOR_MARKERS.CONTROLLERS_START;
268
392
  const endMarker = constants_1.GENERATOR_MARKERS.CONTROLLERS_END;
@@ -0,0 +1 @@
1
+ export declare function handleMigrateCommit(yamlPath?: string): void;
@@ -0,0 +1,201 @@
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.handleMigrateCommit = handleMigrateCommit;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const yaml_1 = require("yaml");
40
+ const colors_1 = require("../utils/colors");
41
+ const cliUtils_1 = require("../utils/cliUtils");
42
+ const migrationUtils_1 = require("../utils/migrationUtils");
43
+ function collectModelsFromYaml(yamlPath) {
44
+ const yamlContent = fs.readFileSync(yamlPath, 'utf8');
45
+ const config = (0, yaml_1.parse)(yamlContent);
46
+ const projectRoot = path.dirname(yamlPath);
47
+ const allModels = [];
48
+ const sources = [];
49
+ // Check if it's a module YAML (has models directly)
50
+ if (config.models) {
51
+ allModels.push(...config.models);
52
+ sources.push(`app.yaml (${config.models.length} model(s))`);
53
+ }
54
+ // Check if it's an app YAML (has modules as array of paths)
55
+ if (config.modules && Array.isArray(config.modules)) {
56
+ let moduleCount = 0;
57
+ config.modules.forEach(moduleRef => {
58
+ // Handle both string and object format
59
+ const modulePath = typeof moduleRef === 'string' ? moduleRef : moduleRef.module;
60
+ const moduleYamlPath = path.isAbsolute(modulePath)
61
+ ? modulePath
62
+ : path.resolve(projectRoot, modulePath);
63
+ if (fs.existsSync(moduleYamlPath)) {
64
+ const moduleYamlContent = fs.readFileSync(moduleYamlPath, 'utf8');
65
+ const moduleConfig = (0, yaml_1.parse)(moduleYamlContent);
66
+ if (moduleConfig.models) {
67
+ allModels.push(...moduleConfig.models);
68
+ moduleCount++;
69
+ }
70
+ }
71
+ });
72
+ if (moduleCount > 0) {
73
+ sources.push(`app.yaml modules section (${moduleCount} module(s))`);
74
+ }
75
+ }
76
+ // Check if modules is an object (legacy format with models embedded)
77
+ else if (config.modules && typeof config.modules === 'object' && !Array.isArray(config.modules)) {
78
+ let moduleCount = 0;
79
+ Object.values(config.modules).forEach(moduleConfig => {
80
+ if (moduleConfig.models) {
81
+ allModels.push(...moduleConfig.models);
82
+ moduleCount++;
83
+ }
84
+ });
85
+ if (moduleCount > 0) {
86
+ sources.push(`app.yaml modules section (${moduleCount} module(s))`);
87
+ }
88
+ }
89
+ // Also check for module YAMLs in src/modules/*/module.yaml (as fallback)
90
+ const modulesDir = path.join(projectRoot, 'src', 'modules');
91
+ if (fs.existsSync(modulesDir)) {
92
+ const moduleFolders = fs.readdirSync(modulesDir).filter(f => {
93
+ const stat = fs.statSync(path.join(modulesDir, f));
94
+ return stat.isDirectory();
95
+ });
96
+ let moduleYamlCount = 0;
97
+ for (const moduleFolder of moduleFolders) {
98
+ const moduleYamlPath = path.join(modulesDir, moduleFolder, 'module.yaml');
99
+ if (fs.existsSync(moduleYamlPath)) {
100
+ const moduleYamlContent = fs.readFileSync(moduleYamlPath, 'utf8');
101
+ const moduleConfig = (0, yaml_1.parse)(moduleYamlContent);
102
+ if (moduleConfig.models) {
103
+ allModels.push(...moduleConfig.models);
104
+ moduleYamlCount++;
105
+ }
106
+ }
107
+ }
108
+ if (moduleYamlCount > 0) {
109
+ sources.push(`src/modules/*/module.yaml (${moduleYamlCount} module(s))`);
110
+ }
111
+ }
112
+ // Log sources
113
+ if (sources.length > 0) {
114
+ // eslint-disable-next-line no-console
115
+ console.log(colors_1.colors.gray(` Sources: ${sources.join(', ')}`));
116
+ }
117
+ return allModels;
118
+ }
119
+ function handleMigrateCommit(yamlPath) {
120
+ try {
121
+ const resolvedYamlPath = (0, cliUtils_1.resolveYamlPath)(yamlPath);
122
+ const projectRoot = path.dirname(resolvedYamlPath);
123
+ const migrationsDir = path.join(projectRoot, 'migrations');
124
+ const stateFilePath = path.join(migrationsDir, 'schema_state.yaml');
125
+ // eslint-disable-next-line no-console
126
+ console.log(colors_1.colors.cyan('\n🔄 Running migration commit at application level...'));
127
+ // eslint-disable-next-line no-console
128
+ console.log(colors_1.colors.gray(` Project root: ${projectRoot}`));
129
+ // eslint-disable-next-line no-console
130
+ console.log(colors_1.colors.gray(` Migrations dir: ${migrationsDir}`));
131
+ // Ensure migrations directory exists
132
+ if (!fs.existsSync(migrationsDir)) {
133
+ fs.mkdirSync(migrationsDir, { recursive: true });
134
+ // eslint-disable-next-line no-console
135
+ console.log(colors_1.colors.green(` ✓ Created migrations directory`));
136
+ }
137
+ // Collect all models from YAML files
138
+ // eslint-disable-next-line no-console
139
+ console.log(colors_1.colors.cyan('\n📋 Collecting models from all modules...'));
140
+ const currentModels = collectModelsFromYaml(resolvedYamlPath);
141
+ if (currentModels.length === 0) {
142
+ // eslint-disable-next-line no-console
143
+ console.log(colors_1.colors.yellow('⚠️ No models found in YAML configuration.'));
144
+ return;
145
+ }
146
+ // eslint-disable-next-line no-console
147
+ console.log(colors_1.colors.green(`✓ Found ${currentModels.length} model(s): ${currentModels.map(m => m.name).join(', ')}`));
148
+ // Load previous state
149
+ const oldState = (0, migrationUtils_1.loadSchemaState)(stateFilePath);
150
+ if (oldState) {
151
+ // eslint-disable-next-line no-console
152
+ console.log(colors_1.colors.cyan(`📖 Previous schema state loaded (version: ${oldState.version})`));
153
+ }
154
+ else {
155
+ // eslint-disable-next-line no-console
156
+ console.log(colors_1.colors.cyan('📖 No previous schema state found - will generate initial migration'));
157
+ }
158
+ // Compare schemas and generate SQL
159
+ // eslint-disable-next-line no-console
160
+ console.log(colors_1.colors.cyan('\n🔍 Comparing schemas...'));
161
+ const sqlStatements = (0, migrationUtils_1.compareSchemas)(oldState, currentModels);
162
+ if (sqlStatements.length === 0 || sqlStatements.every(s => s.trim() === '' || s.startsWith('--'))) {
163
+ // eslint-disable-next-line no-console
164
+ console.log(colors_1.colors.yellow('⚠️ No changes detected. Schema is up to date.'));
165
+ return;
166
+ }
167
+ // Generate migration file
168
+ const timestamp = (0, migrationUtils_1.generateTimestamp)();
169
+ const migrationFileName = (0, migrationUtils_1.getMigrationFileName)(timestamp);
170
+ const migrationFilePath = path.join(migrationsDir, migrationFileName);
171
+ const migrationHeader = `-- Migration: ${oldState ? 'Schema update' : 'Initial schema'}
172
+ -- Created: ${new Date().toISOString().split('T')[0]}
173
+ -- Timestamp: ${timestamp}
174
+
175
+ `;
176
+ const migrationContent = migrationHeader + sqlStatements.join('\n');
177
+ fs.writeFileSync(migrationFilePath, migrationContent);
178
+ // Update state file
179
+ const newState = {
180
+ models: currentModels,
181
+ version: timestamp,
182
+ timestamp: new Date().toISOString()
183
+ };
184
+ (0, migrationUtils_1.saveSchemaState)(stateFilePath, newState);
185
+ // eslint-disable-next-line no-console
186
+ console.log(colors_1.colors.green('\n✅ Migration created successfully!'));
187
+ // eslint-disable-next-line no-console
188
+ console.log(colors_1.colors.gray(` File: ${migrationFileName}`));
189
+ // eslint-disable-next-line no-console
190
+ console.log(colors_1.colors.gray(` Path: ${migrationFilePath}`));
191
+ // eslint-disable-next-line no-console
192
+ console.log(colors_1.colors.gray(` Location: Application-level migrations directory`));
193
+ // eslint-disable-next-line no-console
194
+ console.log(colors_1.colors.cyan('\n💡 Next step: Run "currentjs migrate push" to apply this migration to the database (not implemented yet).'));
195
+ }
196
+ catch (error) {
197
+ // eslint-disable-next-line no-console
198
+ console.error(colors_1.colors.red('❌ Error creating migration:'), error instanceof Error ? error.message : String(error));
199
+ process.exitCode = 1;
200
+ }
201
+ }
@@ -13,6 +13,13 @@ export declare class ControllerGenerator {
13
13
  private getWebMethodName;
14
14
  private getPathSuffix;
15
15
  private getReturnType;
16
+ /**
17
+ * Generate controller for a single config (api or routes)
18
+ */
19
+ private generateControllerForModelWithConfig;
20
+ /**
21
+ * Generate controller for a model (handles both single config and array) - Option D
22
+ */
16
23
  private generateControllerForModel;
17
24
  private generateController;
18
25
  generateFromYamlFile(yamlFilePath: string): Record<string, string>;
@@ -254,17 +254,18 @@ class ControllerGenerator {
254
254
  return 'any';
255
255
  }
256
256
  }
257
- generateControllerForModel(model, moduleName, moduleConfig, hasGlobalPermissions, kind) {
257
+ /**
258
+ * Generate controller for a single config (api or routes)
259
+ */
260
+ generateControllerForModelWithConfig(model, moduleName, moduleConfig, cfg, hasGlobalPermissions, kind) {
258
261
  var _a;
259
262
  const isApi = kind === 'api';
260
- const cfgRaw = isApi ? moduleConfig.api : moduleConfig.routes;
261
- let cfg = cfgRaw;
262
263
  const entityName = model.name;
263
264
  const entityLower = entityName.toLowerCase();
264
265
  // Determine if we should generate a controller for this model
265
266
  const configModel = (cfg === null || cfg === void 0 ? void 0 : cfg.model) || (moduleConfig.models && moduleConfig.models[0] ? moduleConfig.models[0].name : null);
266
267
  const topLevelMatches = !configModel || configModel === entityName || configModel.toLowerCase() === entityLower;
267
- // Also check if any endpoints specifically target this model
268
+ // Also check if any endpoints specifically target this model (Option A)
268
269
  const hasEndpointForThisModel = ((_a = cfg === null || cfg === void 0 ? void 0 : cfg.endpoints) === null || _a === void 0 ? void 0 : _a.some(endpoint => {
269
270
  const endpointModel = endpoint.model || configModel;
270
271
  return endpointModel === entityName || (endpointModel === null || endpointModel === void 0 ? void 0 : endpointModel.toLowerCase()) === entityLower;
@@ -273,33 +274,10 @@ class ControllerGenerator {
273
274
  if (!shouldGenerateForThisModel) {
274
275
  return '';
275
276
  }
276
- if (!isApi) {
277
- // Ensure sensible defaults for Web routes: list, detail, create (empty), edit (get)
278
- if (!cfgRaw || !cfgRaw.endpoints || cfgRaw.endpoints.length === 0) {
279
- cfg = {
280
- prefix: `/${entityLower}`,
281
- endpoints: [
282
- { path: '/', action: 'list', method: 'GET', view: `${entityLower}List` },
283
- { path: '/:id', action: 'get', method: 'GET', view: `${entityLower}Detail` },
284
- { path: '/create', action: 'empty', method: 'GET', view: `${entityLower}Create` },
285
- { path: '/:id/edit', action: 'get', method: 'GET', view: `${entityLower}Update` }
286
- ]
287
- };
288
- }
289
- else {
290
- // Force GET for all web endpoints; forms should submit to API using custom form handling
291
- cfg = {
292
- prefix: cfgRaw.prefix || `/${entityLower}`,
293
- endpoints: (cfgRaw.endpoints || []).map(e => ({ ...e, method: 'GET' }))
294
- };
295
- }
296
- }
297
- if (!cfg)
298
- return '';
299
277
  const controllerBase = (cfg.prefix || `/${isApi ? 'api/' : ''}${entityLower}`).replace(/\/$/, '');
300
278
  const actionPermissions = this.getActionPermissions(moduleName, moduleConfig);
301
279
  const hasPermissions = hasGlobalPermissions && !!(moduleConfig.permissions && moduleConfig.permissions.length > 0);
302
- // Filter endpoints that apply to this model
280
+ // Filter endpoints that apply to this model (Option A)
303
281
  const modelEndpoints = (cfg.endpoints || []).filter(endpoint => {
304
282
  const endpointModel = endpoint.model || (cfg === null || cfg === void 0 ? void 0 : cfg.model) || (moduleConfig.models && moduleConfig.models[0] ? moduleConfig.models[0].name : null);
305
283
  return endpointModel === entityName || (endpointModel === null || endpointModel === void 0 ? void 0 : endpointModel.toLowerCase()) === entityLower;
@@ -314,12 +292,65 @@ class ControllerGenerator {
314
292
  return this.generateControllerMethod(endpoint, entityName, roles, hasPermissions, kind, moduleConfig.actions);
315
293
  })
316
294
  .join('\n\n');
295
+ return controllerMethods;
296
+ }
297
+ /**
298
+ * Generate controller for a model (handles both single config and array) - Option D
299
+ */
300
+ generateControllerForModel(model, moduleName, moduleConfig, hasGlobalPermissions, kind) {
301
+ const isApi = kind === 'api';
302
+ const cfgRaw = isApi ? moduleConfig.api : moduleConfig.routes;
303
+ const entityName = model.name;
304
+ const entityLower = entityName.toLowerCase();
305
+ // Support both single config and array (Option D)
306
+ let configs = [];
307
+ if (!cfgRaw) {
308
+ // No config - generate defaults for web routes only
309
+ if (!isApi) {
310
+ configs = [{
311
+ prefix: `/${entityLower}`,
312
+ endpoints: [
313
+ { path: '/', action: 'list', method: 'GET', view: `${entityLower}List` },
314
+ { path: '/:id', action: 'get', method: 'GET', view: `${entityLower}Detail` },
315
+ { path: '/create', action: 'empty', method: 'GET', view: `${entityLower}Create` },
316
+ { path: '/:id/edit', action: 'get', method: 'GET', view: `${entityLower}Update` }
317
+ ]
318
+ }];
319
+ }
320
+ else {
321
+ return '';
322
+ }
323
+ }
324
+ else if (Array.isArray(cfgRaw)) {
325
+ configs = cfgRaw;
326
+ }
327
+ else {
328
+ configs = [cfgRaw];
329
+ }
330
+ // For web routes, force GET method
331
+ if (!isApi) {
332
+ configs = configs.map(cfg => ({
333
+ ...cfg,
334
+ endpoints: (cfg.endpoints || []).map(e => ({ ...e, method: 'GET' }))
335
+ }));
336
+ }
337
+ // Generate methods from all configs
338
+ const allMethods = configs
339
+ .map(cfg => this.generateControllerForModelWithConfig(model, moduleName, moduleConfig, cfg, hasGlobalPermissions, kind))
340
+ .filter(methods => methods.trim() !== '')
341
+ .join('\n\n');
342
+ if (!allMethods.trim()) {
343
+ return '';
344
+ }
345
+ // Use the first config for controller base path (or default)
346
+ const firstConfig = configs[0];
347
+ const controllerBase = ((firstConfig === null || firstConfig === void 0 ? void 0 : firstConfig.prefix) || `/${isApi ? 'api/' : ''}${entityLower}`).replace(/\/$/, '');
317
348
  const controllerClass = this.replaceTemplateVars(controllerTemplates_1.controllerTemplates.controllerClass, {
318
349
  CONTROLLER_NAME: `${entityName}${isApi ? 'ApiController' : 'WebController'}`,
319
350
  ENTITY_NAME: entityName,
320
351
  ENTITY_LOWER: entityLower,
321
352
  CONTROLLER_BASE: controllerBase,
322
- CONTROLLER_METHODS: controllerMethods
353
+ CONTROLLER_METHODS: allMethods
323
354
  });
324
355
  return this.replaceTemplateVars(controllerTemplates_1.controllerFileTemplate, {
325
356
  ENTITY_NAME: entityName,
@@ -4,6 +4,7 @@ interface FieldConfig {
4
4
  required?: boolean;
5
5
  unique?: boolean;
6
6
  auto?: boolean;
7
+ displayFields?: string[];
7
8
  }
8
9
  interface ModelConfig {
9
10
  name: string;
@@ -17,9 +18,15 @@ type AppConfig = {
17
18
  } | ModuleConfig;
18
19
  export declare class DomainModelGenerator {
19
20
  private typeMapping;
21
+ private availableModels;
20
22
  private getDefaultValue;
21
23
  private mapType;
24
+ private setAvailableModels;
25
+ private getRelatedModelImports;
22
26
  private generateConstructorParameter;
27
+ private isRelationshipField;
28
+ private getForeignKeyFieldName;
29
+ private generateForeignKeyParameter;
23
30
  private generateSetterMethods;
24
31
  private sortFieldsByRequired;
25
32
  generateModel(modelConfig: ModelConfig): string;