@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.
- package/README.md +62 -0
- package/auth/basic.js +8 -0
- package/auth/cookie.js +10 -0
- package/auth/jwks.js +6 -0
- package/auth/jwt.js +9 -0
- package/auth/openid.js +5 -0
- package/auth/webscoket.js +6 -0
- package/builder/EnvBuilder.js +0 -0
- package/builder/createCjsModule.js +95 -0
- package/builder/editModelInteractive.js +159 -0
- package/builder/interactiveModelBuilder.js +215 -0
- package/builder/interactiveMongobuilder.js +189 -0
- package/builder/interactiveUnifiedBuilder.js +277 -0
- package/builder/joiValidatorBuilder.js +218 -0
- package/builder/mongooseModelBuilder.js +290 -0
- package/builder/mongooseModelBuilder2.js +313 -0
- package/builder/runMigrations.js +106 -0
- package/builder/sequelizeModelBuilder.js +180 -0
- package/cli.js +60 -0
- package/commands/create.js +57 -0
- package/commands/generate.js +74 -0
- package/dev/Uset.schema.json +18 -0
- package/dev/buildSchemaInteractive.js +189 -0
- package/dev/buildSequelizeSchemaInteractive.js +128 -0
- package/dev/createJoiSchemaFromJson.js +137 -0
- package/dev/createModelFromJson.js +280 -0
- package/dev/generateAllFiles.js +45 -0
- package/dev/generateJoiFile.js +95 -0
- package/dev/generateSequelizeFiles.js +167 -0
- package/dev/interactiveJoiBuilder.js +177 -0
- package/dev/ra.js +22 -0
- package/dev/rj.js +18 -0
- package/dev/run.js +16 -0
- package/dev/run_seq.js +18 -0
- package/dev/tracker.js +23 -0
- package/editJsConfig.js +188 -0
- package/index.js +548 -0
- package/lib/ModelGenerator.js +203 -0
- package/lib/ProjectGenerator.js +246 -0
- package/lib/utils.js +100 -0
- package/logo.js +3 -0
- package/package.json +35 -0
- package/readme.md +2 -0
- package/schema.json +42 -0
- package/templates/auth_vals.json +3 -0
- package/templates/config.js +0 -0
- package/templates/example-route.js +94 -0
- package/templates/example-service.js +63 -0
- package/templates/example-validator.js +15 -0
- package/templates/example-worker.js +83 -0
- package/templates/index.txt +41 -0
- package/templates/post-init.js +78 -0
- package/templates/settings.js +192 -0
- package/templates/template1.settings.txt +15 -0
- package/templates/templatev1.json +38 -0
- 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
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
|
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 };
|