@triophore/falcon-cli 1.0.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 (56) hide show
  1. package/README.md +62 -0
  2. package/auth/basic.js +8 -0
  3. package/auth/cookie.js +10 -0
  4. package/auth/jwks.js +6 -0
  5. package/auth/jwt.js +9 -0
  6. package/auth/openid.js +5 -0
  7. package/auth/webscoket.js +6 -0
  8. package/builder/EnvBuilder.js +0 -0
  9. package/builder/createCjsModule.js +95 -0
  10. package/builder/editModelInteractive.js +159 -0
  11. package/builder/interactiveModelBuilder.js +215 -0
  12. package/builder/interactiveMongobuilder.js +189 -0
  13. package/builder/interactiveUnifiedBuilder.js +277 -0
  14. package/builder/joiValidatorBuilder.js +218 -0
  15. package/builder/mongooseModelBuilder.js +290 -0
  16. package/builder/mongooseModelBuilder2.js +313 -0
  17. package/builder/runMigrations.js +106 -0
  18. package/builder/sequelizeModelBuilder.js +180 -0
  19. package/cli.js +60 -0
  20. package/commands/create.js +57 -0
  21. package/commands/generate.js +74 -0
  22. package/dev/Uset.schema.json +18 -0
  23. package/dev/buildSchemaInteractive.js +189 -0
  24. package/dev/buildSequelizeSchemaInteractive.js +128 -0
  25. package/dev/createJoiSchemaFromJson.js +137 -0
  26. package/dev/createModelFromJson.js +280 -0
  27. package/dev/generateAllFiles.js +45 -0
  28. package/dev/generateJoiFile.js +95 -0
  29. package/dev/generateSequelizeFiles.js +167 -0
  30. package/dev/interactiveJoiBuilder.js +177 -0
  31. package/dev/ra.js +22 -0
  32. package/dev/rj.js +18 -0
  33. package/dev/run.js +16 -0
  34. package/dev/run_seq.js +18 -0
  35. package/dev/tracker.js +23 -0
  36. package/editJsConfig.js +188 -0
  37. package/index.js +548 -0
  38. package/lib/ModelGenerator.js +203 -0
  39. package/lib/ProjectGenerator.js +246 -0
  40. package/lib/utils.js +100 -0
  41. package/logo.js +3 -0
  42. package/package.json +35 -0
  43. package/readme.md +2 -0
  44. package/schema.json +42 -0
  45. package/templates/auth_vals.json +3 -0
  46. package/templates/config.js +0 -0
  47. package/templates/example-route.js +94 -0
  48. package/templates/example-service.js +63 -0
  49. package/templates/example-validator.js +15 -0
  50. package/templates/example-worker.js +83 -0
  51. package/templates/index.txt +41 -0
  52. package/templates/post-init.js +78 -0
  53. package/templates/settings.js +192 -0
  54. package/templates/template1.settings.txt +15 -0
  55. package/templates/templatev1.json +38 -0
  56. package/validateJsConfig.js +125 -0
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # Falcon CLI
2
+
3
+ Command-line interface for creating and managing Falcon.js applications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g falcon-cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Create a new project
14
+ ```bash
15
+ falcon-cli create my-app
16
+ falcon-cli create my-app --template api
17
+ falcon-cli create my-app --skip-install
18
+ ```
19
+
20
+ ### Generate components
21
+ ```bash
22
+ # Generate a model
23
+ falcon-cli generate model User
24
+ falcon-cli g model Product --crud
25
+
26
+ # Generate a route
27
+ falcon-cli generate route users
28
+
29
+ # Generate a service
30
+ falcon-cli generate service email
31
+
32
+ # Generate a worker
33
+ falcon-cli generate worker image-processor
34
+ ```
35
+
36
+ ## Commands
37
+
38
+ - `create [name]` - Create a new Falcon.js project
39
+ - `generate <type> [name]` - Generate project components
40
+ - Types: `model`, `route`, `service`, `worker`, `validator`
41
+
42
+ ## Options
43
+
44
+ - `--verbose, -V` - Run with verbose logging
45
+ - `--help, -h` - Show help
46
+ - `--version, -v` - Show version
47
+
48
+ ## Project Structure
49
+
50
+ ```
51
+ my-falcon-app/
52
+ ├── index.js # Application entry point
53
+ ├── settings.js # Falcon configuration
54
+ ├── .env # Environment variables
55
+ ├── models/mongo/ # Mongoose models
56
+ ├── routes/ # API routes
57
+ ├── services/ # Background services
58
+ ├── workers/ # Job processors
59
+ ├── validators/ # Joi validation schemas
60
+ ├── init/ # Initialization scripts
61
+ └── logs/ # Application logs
62
+ ```
package/auth/basic.js ADDED
@@ -0,0 +1,8 @@
1
+ module.exports.validate = async function (request, username, password, CONTXET) {
2
+ // TODO: Implement your own logic here
3
+ // Example: Check against database or environment variables
4
+ if (username === process.env.ADMIN_USER && password === process.env.ADMIN_PASSWORD) {
5
+ return { isValid: true, credentials: { id: 1, name: 'Admin', roles: ['admin'], permissions: { admin: ['*'] } } };
6
+ }
7
+ return { isValid: false };
8
+ }
package/auth/cookie.js ADDED
@@ -0,0 +1,10 @@
1
+ module.exports.validate = async function (request, session, CONTXET) {
2
+ // TODO: Implement your own logic here
3
+ // Example: Validate session against database or Redis
4
+ /*
5
+ if (session.valid) {
6
+ return { isValid: true, credentials: { id: session.userId, ... } };
7
+ }
8
+ */
9
+ return { isValid: false };
10
+ }
package/auth/jwks.js ADDED
@@ -0,0 +1,6 @@
1
+ module.exports.validate = async function (decoded, CONTXET) {
2
+ if (username === 'admin' && password === 'secret') {
3
+ return { isValid: true, credentials: { id: 1, name: 'Admin', roles: ['admin'], permissions: { admin: ['*'] } } };
4
+ }
5
+ return { isValid: false };
6
+ }
package/auth/jwt.js ADDED
@@ -0,0 +1,9 @@
1
+ module.exports.validate = async function (decoded, CONTXET) {
2
+ // TODO: Implement your own logic here
3
+ // Example: Check against database or environment variables
4
+ // For example, check if a specific environment variable matches a decoded value
5
+ if (decoded.role === process.env.EXPECTED_ADMIN_ROLE && decoded.secret === process.env.EXPECTED_ADMIN_SECRET) {
6
+ return { isValid: true, credentials: { id: decoded.sub, name: 'Admin', roles: ['admin'] } };
7
+ }
8
+ return { isValid: false };
9
+ }
package/auth/openid.js ADDED
@@ -0,0 +1,5 @@
1
+ module.exports.validate = async function (decoded, CONTXET) {
2
+ // TODO: Implement your own logic here
3
+ // Example: Validate OpenID token claims
4
+ return { isValid: false };
5
+ }
@@ -0,0 +1,6 @@
1
+ module.exports.validate = async function (decoded, CONTXET) {
2
+ if (username === 'admin' && password === 'secret') {
3
+ return { isValid: true, credentials: { id: 1, name: 'Admin', roles: ['admin'], permissions: { admin: ['*'] } } };
4
+ }
5
+ return { isValid: false };
6
+ }
File without changes
@@ -0,0 +1,95 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Create a CJS module from data that may include raw JS expressions
6
+ * @param {string} filePath
7
+ * @param {Object} data - Can include strings starting with 'JS:' for raw code
8
+ * @param {Object} options
9
+ */
10
+ async function createCjsModule(filePath, data, options = {}) {
11
+ const {
12
+ exportStyle = 'default',
13
+ functionName = 'getValue',
14
+ pretty = true,
15
+ async = false,
16
+ imports = false
17
+ } = options;
18
+
19
+ const dir = path.dirname(filePath);
20
+ await fs.promises.mkdir(dir, { recursive: true });
21
+
22
+ let content = '';
23
+
24
+ // Helper: convert value to JS source string
25
+ function toSource(value, indent = ' ') {
26
+ // Special case: raw JS code via prefix
27
+ if (typeof value === 'string' && value.startsWith('JS:')) {
28
+ const rawCode = value.slice(3).trim();
29
+ return rawCode;
30
+ }
31
+
32
+ // Functions: preserve source if possible
33
+ if (typeof value === 'function') {
34
+ const funcStr = value.toString();
35
+ return pretty ? funcStr.split('\n').map(line => indent + line).join('\n') : funcStr;
36
+ }
37
+
38
+ // Objects/Arrays: recursive
39
+ if (value && typeof value === 'object') {
40
+ if (Array.isArray(value)) {
41
+ const items = value.map(v => toSource(v, indent + ' '));
42
+ return `[\n${indent} ${items.join(`,\n${indent} `)}\n${indent}]`;
43
+ } else {
44
+ const entries = Object.entries(value)
45
+ .map(([k, v]) => `${indent} ${JSON.stringify(k)}: ${toSource(v, indent + ' ')}`);
46
+ return `{\n${entries.join(`,\n`)}\n${indent}}`;
47
+ }
48
+ }
49
+
50
+ // Primitives
51
+ return JSON.stringify(value);
52
+ }
53
+
54
+ const dataSource = toSource(data, '');
55
+
56
+ switch (exportStyle) {
57
+ case 'default':
58
+ content = `module.exports.${functionName} = ${dataSource};\n`;
59
+ break;
60
+
61
+ case 'named':
62
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
63
+ throw new Error('Named exports require a plain object');
64
+ }
65
+ const namedLines = Object.entries(data)
66
+ .filter(([_, v]) => !v?.toString().startsWith?.('function')) // skip functions for now
67
+ .map(([key, value]) => {
68
+ const src = toSource(value, '');
69
+ return `exports.${key} = ${src};`;
70
+ });
71
+ content = namedLines.join('\n') + '\n';
72
+ break;
73
+
74
+ case 'function':
75
+ const asyncKw = async ? 'async ' : '';
76
+ const returnVal = async ? `Promise.resolve(${dataSource})` : dataSource;
77
+ content = `module.exports.${functionName} = ${asyncKw}() => ${returnVal};\n`;
78
+ break;
79
+
80
+ default:
81
+ throw new Error(`Invalid exportStyle: ${exportStyle}`);
82
+ }
83
+
84
+ if (imports) {
85
+ await fs.promises.appendFile(filePath, imports, 'utf8');
86
+ await fs.promises.appendFile(filePath, content, 'utf8');
87
+ } else {
88
+ await fs.promises.writeFile(filePath, content, 'utf8');
89
+ return path.resolve(filePath);
90
+ }
91
+
92
+
93
+ }
94
+
95
+ module.exports = { createCjsModule };
@@ -0,0 +1,159 @@
1
+ // editModelInteractive.js
2
+ const {
3
+ input,
4
+ confirm,
5
+ select,
6
+ } = require('@inquirer/prompts');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { interactiveUnifiedBuilder } = require('./interactiveUnifiedBuilder');
10
+
11
+ async function editModelInteractive(baseDir, modelName) {
12
+ const mongoDir = path.join(baseDir, 'mongo');
13
+ const sequelizeDir = path.join(baseDir, 'sequelize');
14
+
15
+ // Find model
16
+ let filePath = path.join(mongoDir, `${modelName}.json`);
17
+ let isMongo = fs.existsSync(filePath);
18
+ if (!isMongo) {
19
+ filePath = path.join(sequelizeDir, `${modelName}.json`);
20
+ isMongo = false;
21
+ }
22
+
23
+ if (!fs.existsSync(filePath)) {
24
+ throw new Error(`Model "${modelName}" not found in ${mongoDir} or ${sequelizeDir}`);
25
+ }
26
+
27
+ const existingConfig = JSON.parse(fs.readFileSync(filePath, 'utf8'));
28
+ const dbType = existingConfig.type || (isMongo ? 'mongodb' : 'sequelize');
29
+
30
+ if (dbType === 'mongodb') {
31
+ throw new Error('Edit with alter migration is only supported for Sequelize (SQL). Use Mongoose schema evolution.');
32
+ }
33
+
34
+ console.log(`\nEditing SEQUELIZE model: ${modelName}\n`);
35
+
36
+ // Reuse interactive builder to get new config
37
+ const { config: newConfig } = await interactiveUnifiedBuilder(baseDir);
38
+
39
+ // Merge: keep only name, type, tableName, options
40
+ const updatedConfig = {
41
+ name: modelName,
42
+ type: 'sequelize',
43
+ tableName: newConfig.tableName || existingConfig.tableName,
44
+ schema: newConfig.schema,
45
+ associations: newConfig.associations.length > 0 ? newConfig.associations : (existingConfig.associations || []),
46
+ options: { ...existingConfig.options, ...newConfig.options },
47
+ };
48
+
49
+ // Generate alter migration
50
+ const migration = generateAlterMigration(existingConfig, updatedConfig);
51
+
52
+ // Save updated JSON
53
+ const savePath = path.join(sequelizeDir, `${modelName}.json`);
54
+ fs.writeFileSync(savePath, JSON.stringify(updatedConfig, null, 2), 'utf8');
55
+ console.log(`Updated model saved: ${savePath}`);
56
+
57
+ return { config: updatedConfig, migration };
58
+ }
59
+
60
+ // ── Generate Real Alter Migration
61
+ function generateAlterMigration(oldConfig, newConfig) {
62
+ const timestamp = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14);
63
+ const table = newConfig.tableName || `${newConfig.name.toLowerCase()}s`;
64
+ const fileName = `${timestamp}-alter-${table}.js`;
65
+
66
+ const oldFields = oldConfig.schema || {};
67
+ const newFields = newConfig.schema || {};
68
+
69
+ const added = Object.keys(newFields).filter(f => !oldFields[f]);
70
+ const removed = Object.keys(oldFields).filter(f => !newFields[f]);
71
+ const changed = Object.keys(newFields).filter(f => oldFields[f] && !deepEqual(oldFields[f], newFields[f]));
72
+
73
+ if (added.length === 0 && removed.length === 0 && changed.length === 0 && oldConfig.tableName === newConfig.tableName) {
74
+ return null; // No changes
75
+ }
76
+
77
+ let upCode = [];
78
+ let downCode = [];
79
+
80
+ // Add columns
81
+ added.forEach(field => {
82
+ const def = newFields[field];
83
+ upCode.push(` await queryInterface.addColumn('${table}', '${field}', ${formatField(def)});`);
84
+ downCode.push(` await queryInterface.removeColumn('${table}', '${field}');`);
85
+ });
86
+
87
+ // Remove columns
88
+ removed.forEach(field => {
89
+ upCode.push(` await queryInterface.removeColumn('${table}', '${field}');`);
90
+ downCode.push(` await queryInterface.addColumn('${table}', '${field}', ${formatField(oldFields[field])});`);
91
+ });
92
+
93
+ // Change columns
94
+ changed.forEach(field => {
95
+ const def = newFields[field];
96
+ upCode.push(` await queryInterface.changeColumn('${table}', '${field}', ${formatField(def)});`);
97
+ downCode.push(` await queryInterface.changeColumn('${table}', '${field}', ${formatField(oldFields[field])});`);
98
+ });
99
+
100
+ // Rename table
101
+ if (oldConfig.tableName && newConfig.tableName && oldConfig.tableName !== newConfig.tableName) {
102
+ upCode.unshift(` await queryInterface.renameTable('${oldConfig.tableName}', '${newConfig.tableName}');`);
103
+ downCode.unshift(` await queryInterface.renameTable('${newConfig.tableName}', '${oldConfig.tableName}');`);
104
+ }
105
+
106
+ let code = `'use strict';\n\nmodule.exports = {\n`;
107
+ code += ` up: async (queryInterface, Sequelize) => {\n`;
108
+ code += upCode.join('\n') || ` // No schema changes detected\n`;
109
+ code += `\n },\n\n`;
110
+ code += ` down: async (queryInterface, Sequelize) => {\n`;
111
+ code += downCode.join('\n') || ` // No rollback needed\n`;
112
+ code += `\n }\n};\n`;
113
+
114
+ return { fileName, code };
115
+ }
116
+
117
+ // Format field for migration
118
+ function formatField(field) {
119
+ const parts = [];
120
+ parts.push(`type: ${formatType(field.type, field.values)}`);
121
+ if (field.allowNull === false) parts.push(`allowNull: false`);
122
+ if (field.unique) parts.push(`unique: true`);
123
+ if (field.default !== undefined) parts.push(`defaultValue: ${formatDefault(field.default)}`);
124
+
125
+ return `{ ${parts.join(', ')} }`;
126
+ }
127
+
128
+ function formatType(type, values) {
129
+ if (type.includes('[]')) return `Sequelize.ARRAY(Sequelize.${type.replace('[]', '')})`;
130
+ if (type.includes('(')) return `Sequelize.${type}`;
131
+ if (type === 'ENUM') return `Sequelize.ENUM(${values.map(v => `'${v}'`).join(', ')})`;
132
+ return `Sequelize.${type}`;
133
+ }
134
+
135
+ function formatDefault(val) {
136
+ if (val === 'now') return `Sequelize.fn('NOW')`;
137
+ if (val === true) return 'true';
138
+ if (val === false) return 'false';
139
+ if (!isNaN(val)) return val;
140
+ return `'${val}'`;
141
+ }
142
+
143
+ // Deep compare (for changed detection)
144
+ function deepEqual(a, b) {
145
+ if (a === b) return true;
146
+ if (typeof a !== 'object' || typeof b !== 'object' || a == null || b == null) return false;
147
+
148
+ const keysA = Object.keys(a);
149
+ const keysB = Object.keys(b);
150
+ if (keysA.length !== keysB.length) return false;
151
+
152
+ for (const key of keysA) {
153
+ if (!keysB.includes(key)) return false;
154
+ if (!deepEqual(a[key], b[key])) return false;
155
+ }
156
+ return true;
157
+ }
158
+
159
+ module.exports = { editModelInteractive };
@@ -0,0 +1,215 @@
1
+ // interactiveModelBuilder.js
2
+ const {
3
+ input,
4
+ confirm,
5
+ select,
6
+ checkbox,
7
+ } = require('@inquirer/prompts');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ async function interactiveModelBuilder() {
12
+ console.log('\nUnified Model Builder (Mongoose / Sequelize / Joi)\n');
13
+
14
+ // ── 1. Database Type
15
+ const dbType = await select({
16
+ message: 'Choose database:',
17
+ choices: [
18
+ { name: 'MongoDB (Mongoose)', value: 'mongodb' },
19
+ { name: 'SQL (Sequelize)', value: 'sequelize' },
20
+ ],
21
+ });
22
+
23
+ // ── 2. Model Name
24
+ const modelName = await input({
25
+ message: 'Model name (e.g., User):',
26
+ validate: v => v.trim() ? true : 'Required',
27
+ });
28
+
29
+ const tableName = dbType === 'sequelize' ? await input({
30
+ message: 'Table name (or Enter for default):',
31
+ default: modelName.toLowerCase() + 's',
32
+ }) : undefined;
33
+
34
+ const schema = {};
35
+ const joiRules = {};
36
+ const associations = [];
37
+ const options = { timestamps: true };
38
+
39
+ if (dbType === 'sequelize') {
40
+ options.paranoid = await confirm({ message: 'Enable soft deletes (deletedAt)?', default: false });
41
+ }
42
+
43
+ console.log('\nAdd fields:\n');
44
+
45
+ // ── 3. Fields
46
+ while (true) {
47
+ const addField = await confirm({ message: 'Add a field?', default: true });
48
+ if (!addField) break;
49
+
50
+ const fieldName = await input({
51
+ message: 'Field name:',
52
+ validate: v => v && !schema[v] ? true : 'Invalid/duplicate',
53
+ });
54
+
55
+ // Type selection
56
+ const typeChoices = dbType === 'mongodb'
57
+ ? ['string', 'number', 'boolean', 'date', 'objectid', 'array', 'object', 'mixed']
58
+ : ['STRING', 'TEXT', 'INTEGER', 'FLOAT', 'BOOLEAN', 'DATE', 'JSON', 'ARRAY', 'ENUM'];
59
+
60
+ const fieldType = await select({
61
+ message: `Type for "${fieldName}":`,
62
+ choices: typeChoices.map(t => ({ name: t, value: t })),
63
+ });
64
+
65
+ const field = { type: fieldType };
66
+ const joiField = {};
67
+
68
+ // ── Common Options
69
+ const required = await confirm({ message: 'Required?', default: false });
70
+ if (required) {
71
+ field[dbType === 'mongodb' ? 'required' : 'allowNull'] = dbType === 'mongodb' ? true : false;
72
+ joiField.required = true;
73
+ }
74
+
75
+ const unique = await confirm({ message: 'Unique?', default: false });
76
+ if (unique) {
77
+ field.unique = true;
78
+ joiField.unique = true;
79
+ }
80
+
81
+ // ── Default Value
82
+ const hasDefault = await confirm({ message: 'Set default?', default: false });
83
+ if (hasDefault) {
84
+ if (fieldType.toLowerCase() === 'boolean') {
85
+ const val = await confirm({ message: 'Default TRUE?', default: true });
86
+ field.default = val;
87
+ joiField.default = val;
88
+ } else if (fieldType.toLowerCase().includes('date')) {
89
+ const now = await confirm({ message: 'Default to NOW?', default: true });
90
+ field.default = now ? 'now' : await input({ message: 'Default value:' });
91
+ joiField.default = now ? 'now' : field.default;
92
+ } else {
93
+ const def = await input({ message: 'Default value:' });
94
+ field.default = def;
95
+ joiField.default = def;
96
+ }
97
+ }
98
+
99
+ // ── String Options (Mongoose & Sequelize)
100
+ if (fieldType.toLowerCase() === 'string') {
101
+ const stringOpts = await checkbox({
102
+ message: 'String options:',
103
+ choices: ['trim', 'lowercase', 'uppercase', 'email'],
104
+ });
105
+ stringOpts.forEach(opt => {
106
+ if (opt !== 'email') field[opt] = true;
107
+ if (opt === 'email') joiField.email = true;
108
+ if (opt === 'trim') joiField.trim = true;
109
+ if (opt === 'lowercase') joiField.lowercase = true;
110
+ });
111
+
112
+ if (dbType === 'sequelize') {
113
+ const len = await input({ message: 'Length (default 255):', default: '255' });
114
+ if (len !== '255') field.type = `STRING(${len})`;
115
+ }
116
+ }
117
+
118
+ // ── Number/Date Min/Max
119
+ if (['number', 'integer', 'float', 'date'].includes(fieldType.toLowerCase())) {
120
+ const min = await input({ message: 'Min value (or leave empty):' });
121
+ const max = await input({ message: 'Max value (or leave empty):' });
122
+ if (min) { field.min = +min; joiField.min = +min; }
123
+ if (max) { field.max = +max; joiField.max = +max; }
124
+ }
125
+
126
+ // ── Enum
127
+ if (fieldType.toUpperCase() === 'ENUM') {
128
+ const values = await input({ message: 'Comma-separated values:' });
129
+ const vals = values.split(',').map(v => v.trim()).filter(Boolean);
130
+ field.values = vals;
131
+ joiField.enum = vals;
132
+ }
133
+
134
+ // ── Array
135
+ if (fieldType.toLowerCase() === 'array') {
136
+ const itemType = await select({
137
+ message: 'Array item type:',
138
+ choices: dbType === 'mongodb'
139
+ ? ['string', 'number', 'object', 'objectid']
140
+ : ['STRING', 'INTEGER', 'FLOAT'],
141
+ });
142
+ field.items = { type: itemType };
143
+ joiField.items = { type: itemType.toLowerCase() };
144
+ }
145
+
146
+ // ── Nested Object
147
+ if (fieldType.toLowerCase() === 'object') {
148
+ field.properties = await buildNestedObject(dbType);
149
+ joiField.properties = await buildNestedJoi();
150
+ }
151
+
152
+ schema[fieldName] = field;
153
+ joiRules[fieldName] = joiField;
154
+ }
155
+
156
+ // ── 4. Associations
157
+ while (true) {
158
+ const addAssoc = await confirm({ message: 'Add association?', default: false });
159
+ if (!addAssoc) break;
160
+
161
+ const assocType = await select({
162
+ message: 'Type:',
163
+ choices: dbType === 'mongodb'
164
+ ? ['hasMany', 'belongsTo']
165
+ : ['hasMany', 'belongsTo', 'hasOne'],
166
+ });
167
+
168
+ const target = await input({ message: 'Target model:' });
169
+ const fk = await input({ message: 'Foreign key:', default: `${modelName.toLowerCase()}Id` });
170
+ const as = await input({ message: 'Alias (as):', default: target.toLowerCase() });
171
+
172
+ associations.push({ type: assocType, targetModel: target, foreignKey: fk, as });
173
+ }
174
+
175
+ return {
176
+ name: modelName,
177
+ type: dbType,
178
+ tableName: tableName?.trim(),
179
+ schema,
180
+ joi: joiRules,
181
+ associations,
182
+ options,
183
+ };
184
+ }
185
+
186
+ // Helper: nested object
187
+ async function buildNestedObject(dbType) {
188
+ const props = {};
189
+ while (true) {
190
+ const add = await confirm({ message: 'Add property?', default: true });
191
+ if (!add) break;
192
+ const name = await input({ message: 'Name:' });
193
+ const type = await select({
194
+ message: 'Type:',
195
+ choices: dbType === 'mongodb'
196
+ ? ['string', 'number', 'boolean', 'date', 'objectid']
197
+ : ['STRING', 'INTEGER', 'BOOLEAN', 'DATE'],
198
+ });
199
+ props[name] = { type };
200
+ }
201
+ return props;
202
+ }
203
+
204
+ async function buildNestedJoi() {
205
+ const props = {};
206
+ while (true) {
207
+ const add = await confirm({ message: 'Add Joi rule?', default: true });
208
+ if (!add) break;
209
+ const name = await input({ message: 'Field name:' });
210
+ props[name] = { type: 'string' }; // simplified
211
+ }
212
+ return props;
213
+ }
214
+
215
+ module.exports = { interactiveModelBuilder };