@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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// runMigrations.js
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { Sequelize, QueryInterface } = require('sequelize');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Run migrations from a folder
|
|
8
|
+
* @param {string} migrationsDir - Path to migrations folder
|
|
9
|
+
* @param {Sequelize} sequelize - Sequelize instance
|
|
10
|
+
* @param {('up'|'down')} direction - 'up' to apply, 'down' to rollback
|
|
11
|
+
* @param {string} [toMigration] - Run down to this migration name (optional)
|
|
12
|
+
*/
|
|
13
|
+
async function runMigrations(migrationsDir, sequelize, direction = 'up', toMigration = null) {
|
|
14
|
+
if (!fs.existsSync(migrationsDir)) {
|
|
15
|
+
throw new Error(`Migrations directory not found: ${migrationsDir}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const queryInterface = sequelize.getQueryInterface();
|
|
19
|
+
|
|
20
|
+
// Ensure SequelizeMeta table exists
|
|
21
|
+
await ensureMetaTable(queryInterface);
|
|
22
|
+
|
|
23
|
+
const executed = await getExecutedMigrations(queryInterface);
|
|
24
|
+
const allFiles = fs.readdirSync(migrationsDir)
|
|
25
|
+
.filter(f => f.endsWith('.js'))
|
|
26
|
+
.sort();
|
|
27
|
+
|
|
28
|
+
let migrationsToRun = [];
|
|
29
|
+
|
|
30
|
+
if (direction === 'up') {
|
|
31
|
+
migrationsToRun = allFiles.filter(f => !executed.includes(f));
|
|
32
|
+
} else if (direction === 'down') {
|
|
33
|
+
const targetIndex = toMigration ? allFiles.indexOf(toMigration) : allFiles.length - 1;
|
|
34
|
+
if (targetIndex === -1) throw new Error(`Migration not found: ${toMigration}`);
|
|
35
|
+
migrationsToRun = allFiles
|
|
36
|
+
.slice(0, targetIndex + 1)
|
|
37
|
+
.filter(f => executed.includes(f))
|
|
38
|
+
.reverse();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (migrationsToRun.length === 0) {
|
|
42
|
+
console.log(`No migrations to run (${direction})`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(`Running ${migrationsToRun.length} migration(s) ${direction}...`);
|
|
47
|
+
|
|
48
|
+
for (const file of migrationsToRun) {
|
|
49
|
+
const filePath = path.join(migrationsDir, file);
|
|
50
|
+
const migration = require(filePath);
|
|
51
|
+
|
|
52
|
+
console.log(`${direction === 'up' ? 'Applying' : 'Rolling back'}: ${file}`);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
if (direction === 'up') {
|
|
56
|
+
await migration.up(queryInterface, Sequelize);
|
|
57
|
+
await queryInterface.sequelize.query(
|
|
58
|
+
`INSERT INTO "SequelizeMeta" (name) VALUES (:name)`,
|
|
59
|
+
{ replacements: { name: file } }
|
|
60
|
+
);
|
|
61
|
+
} else {
|
|
62
|
+
await migration.down(queryInterface, Sequelize);
|
|
63
|
+
await queryInterface.sequelize.query(
|
|
64
|
+
`DELETE FROM "SequelizeMeta" WHERE name = :name`,
|
|
65
|
+
{ replacements: { name: file } }
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(`Failed on ${file}:`, err.message);
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(`Completed ${direction} of ${migrationsToRun.length} migration(s)`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Ensure SequelizeMeta table exists
|
|
78
|
+
async function ensureMetaTable(queryInterface) {
|
|
79
|
+
const tableExists = await queryInterface.showAllTables().then(tables => tables.includes('SequelizeMeta'));
|
|
80
|
+
if (!tableExists) {
|
|
81
|
+
await queryInterface.createTable('SequelizeMeta', {
|
|
82
|
+
name: {
|
|
83
|
+
type: Sequelize.STRING,
|
|
84
|
+
allowNull: false,
|
|
85
|
+
unique: true,
|
|
86
|
+
primaryKey: true,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
console.log('Created SequelizeMeta table');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Get list of executed migrations
|
|
94
|
+
async function getExecutedMigrations(queryInterface) {
|
|
95
|
+
try {
|
|
96
|
+
const result = await queryInterface.sequelize.query(
|
|
97
|
+
`SELECT name FROM "SequelizeMeta" ORDER BY name`,
|
|
98
|
+
{ type: queryInterface.sequelize.QueryTypes.SELECT }
|
|
99
|
+
);
|
|
100
|
+
return result.map(row => row.name);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { runMigrations };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// builder/sequelizeModelBuilder.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
|
+
const { capitalize } = require('lodash');
|
|
11
|
+
|
|
12
|
+
async function sequelizeModelBuilder(baseDir = process.cwd()) {
|
|
13
|
+
console.log('\nSequelize Model + Migration Builder\n');
|
|
14
|
+
|
|
15
|
+
const sequelizeDir = path.join(baseDir, 'models', 'sequelize');
|
|
16
|
+
const migrationsDir = path.join(baseDir, 'migrations');
|
|
17
|
+
|
|
18
|
+
// Auto-create folders
|
|
19
|
+
[sequelizeDir, migrationsDir].forEach(dir => {
|
|
20
|
+
if (!fs.existsSync(dir)) {
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
console.log(`Created: ${dir}`);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const modelName = await input({
|
|
27
|
+
message: 'Model name (singular, e.g., User):',
|
|
28
|
+
validate: v => v.trim() ? true : 'Required',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const tableName = await input({
|
|
32
|
+
message: 'Table name (optional, leave empty for pluralized):',
|
|
33
|
+
default: '',
|
|
34
|
+
}) || undefined;
|
|
35
|
+
|
|
36
|
+
const fields = {};
|
|
37
|
+
const indexes = [];
|
|
38
|
+
const associations = [];
|
|
39
|
+
|
|
40
|
+
// === Field Builder ===
|
|
41
|
+
while (await confirm({ message: 'Add field?', default: true })) {
|
|
42
|
+
const name = await input({ message: 'Field name:' });
|
|
43
|
+
|
|
44
|
+
const type = await select({
|
|
45
|
+
message: 'Data type:',
|
|
46
|
+
choices: [
|
|
47
|
+
'STRING', 'TEXT', 'INTEGER', 'BIGINT', 'FLOAT', 'DOUBLE', 'DECIMAL',
|
|
48
|
+
'BOOLEAN', 'DATE', 'DATEONLY', 'JSON', 'UUID', 'ENUM'
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const field = { type: `DataTypes.${type}` };
|
|
53
|
+
|
|
54
|
+
// Common options
|
|
55
|
+
if (await confirm({ message: 'Allow null?', default: true })) field.allowNull = true;
|
|
56
|
+
if (await confirm({ message: 'Unique?', default: false })) field.unique = true;
|
|
57
|
+
if (await confirm({ message: 'Primary key?', default: false })) field.primaryKey = true;
|
|
58
|
+
|
|
59
|
+
const hasDefault = await confirm({ message: 'Default value?', default: false });
|
|
60
|
+
if (hasDefault) {
|
|
61
|
+
if (type === 'BOOLEAN') field.defaultValue = await confirm({ message: 'true?', default: true });
|
|
62
|
+
else if (type === 'DATE') field.defaultValue = "DataTypes.NOW";
|
|
63
|
+
else if (type === 'UUID') field.defaultValue = "DataTypes.UUIDV4";
|
|
64
|
+
else field.defaultValue = await input({ message: 'Default (JS):' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (type === 'ENUM') {
|
|
68
|
+
const values = await input({ message: 'Enum values (comma-separated):' });
|
|
69
|
+
field.type = `DataTypes.ENUM(${values.split(',').map(v => `'${v.trim()}'`).join(', ')})`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (type === 'DECIMAL') {
|
|
73
|
+
const precision = await input({ message: 'Precision (e.g. 10):', default: '10' });
|
|
74
|
+
const scale = await input({ message: 'Scale (e.g. 2):', default: '2' });
|
|
75
|
+
field.type = `DataTypes.DECIMAL(${precision}, ${scale})`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fields[name] = field;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// === Indexes ===
|
|
82
|
+
while (await confirm({ message: 'Add index?', default: false })) {
|
|
83
|
+
const fieldsList = await input({ message: 'Fields (comma-separated):' });
|
|
84
|
+
const opts = await checkbox({
|
|
85
|
+
message: 'Index options:',
|
|
86
|
+
choices: ['unique', 'fulltext', 'spatial'],
|
|
87
|
+
});
|
|
88
|
+
indexes.push({
|
|
89
|
+
fields: fieldsList.split(',').map(f => f.trim()),
|
|
90
|
+
unique: opts.includes('unique'),
|
|
91
|
+
type: opts.includes('fulltext') ? 'FULLTEXT' : opts.includes('spatial') ? 'SPATIAL' : undefined,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// === Associations (for future reference in code) ===
|
|
96
|
+
while (await confirm({ message: 'Add association?', default: false })) {
|
|
97
|
+
const type = await select({ message: 'Type:', choices: ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany'] });
|
|
98
|
+
const target = await input({ message: 'Target model:' });
|
|
99
|
+
const options = {};
|
|
100
|
+
if (type !== 'hasMany') options.foreignKey = await input({ message: 'Foreign key:', default: `${modelName.toLowerCase()}Id` });
|
|
101
|
+
if (type === 'belongsToMany') options.through = await input({ message: 'Through table:' });
|
|
102
|
+
associations.push({ type, target, options });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// === Generate Files ===
|
|
106
|
+
const timestamp = new Date().toISOString().replace(/[-:]/g, '').split('.')[0];
|
|
107
|
+
const migrationName = `${timestamp}-create-${modelName.toLowerCase()}-table`;
|
|
108
|
+
|
|
109
|
+
const modelCode = generateSequelizeModel(modelName, fields, tableName, indexes, associations);
|
|
110
|
+
const migrationCode = generateSequelizeMigration(migrationName, modelName, fields, tableName, indexes);
|
|
111
|
+
|
|
112
|
+
const modelPath = path.join(sequelizeDir, `${capitalize(modelName)}.js`);
|
|
113
|
+
const migrationPath = path.join(migrationsDir, `${migrationName}.js`);
|
|
114
|
+
|
|
115
|
+
fs.writeFileSync(modelPath, modelCode, 'utf8');
|
|
116
|
+
fs.writeFileSync(migrationPath, migrationCode, 'utf8');
|
|
117
|
+
|
|
118
|
+
console.log(`\nModel: ${modelPath}`);
|
|
119
|
+
console.log(`Migration: ${migrationPath}\n`);
|
|
120
|
+
console.log(`Run: npx sequelize-cli db:migrate (or let Falcon run it automatically)\n`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// === Code Generators ===
|
|
124
|
+
function generateSequelizeModel(name, fields, tableName, indexes, associations) {
|
|
125
|
+
const modelName = capitalize(name);
|
|
126
|
+
return `'use strict';
|
|
127
|
+
module.exports = {
|
|
128
|
+
up: async (queryInterface, Sequelize) => {
|
|
129
|
+
await queryInterface.createTable('${tableName || name.toLowerCase() + 's'}', {
|
|
130
|
+
id: {
|
|
131
|
+
allowNull: false,
|
|
132
|
+
autoIncrement: true,
|
|
133
|
+
primaryKey: true,
|
|
134
|
+
type: Sequelize.INTEGER
|
|
135
|
+
},
|
|
136
|
+
${Object.entries(fields)
|
|
137
|
+
.map(([k, v]) => ` ${k}: {\n type: ${v.type},\n allowNull: ${v.allowNull !== false},\n ${v.unique ? 'unique: true,' : ''}\n ${v.defaultValue ? `defaultValue: ${v.defaultValue},` : ''}\n },`)
|
|
138
|
+
.join('\n')}
|
|
139
|
+
createdAt: { allowNull: false, type: Sequelize.DATE },
|
|
140
|
+
updatedAt: { allowNull: false, type: Sequelize.DATE },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
${indexes.map(idx => ` await queryInterface.addIndex('${tableName || name.toLowerCase() + 's'}', [${idx.fields.map(f => `'${f}'`).join(', ')}], { ${idx.unique ? 'unique: true,' : ''} ${idx.type ? `type: '${idx.type}',` : ''} });`).join('\n')}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
down: async (queryInterface) => {
|
|
147
|
+
await queryInterface.dropTable('${tableName || name.toLowerCase() + 's'}');
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function generateSequelizeMigration(migrationName, modelName, fields, tableName, indexes) {
|
|
154
|
+
const capitalized = capitalize(modelName);
|
|
155
|
+
return `'use strict';
|
|
156
|
+
const { DataTypes } = require('sequelize');
|
|
157
|
+
|
|
158
|
+
module.exports = (sequelize) => {
|
|
159
|
+
const ${capitalized} = sequelize.define('${capitalized}', {
|
|
160
|
+
${Object.entries(fields)
|
|
161
|
+
.map(([name, def]) => ` ${name}: {\n type: ${def.type},\n allowNull: ${def.allowNull !== false},\n ${def.primaryKey ? 'primaryKey: true,' : ''}\n ${def.unique ? 'unique: true,' : ''}\n ${def.defaultValue ? `defaultValue: ${def.defaultValue},` : ''}\n },`)
|
|
162
|
+
.join('\n')}
|
|
163
|
+
}, {
|
|
164
|
+
tableName: '${tableName || modelName.toLowerCase() + 's'}',
|
|
165
|
+
timestamps: true,
|
|
166
|
+
indexes: [
|
|
167
|
+
${indexes.map(idx => ` { fields: [${idx.fields.map(f => `'${f}'`).join(', ')}], ${idx.unique ? 'unique: true,' : ''} ${idx.type ? `type: '${idx.type}',` : ''} },`).join('\n')}
|
|
168
|
+
]
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
${capitalized}.associate = (models) => {
|
|
172
|
+
// Add associations here later
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return ${capitalized};
|
|
176
|
+
};
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = { sequelizeModelBuilder };
|
package/cli.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Falcon CLI - Command Line Interface for Falcon.js Framework
|
|
5
|
+
*
|
|
6
|
+
* Standard CLI structure with proper command handling
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const yargs = require('yargs/yargs');
|
|
10
|
+
const { hideBin } = require('yargs/helpers');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
// Import command modules
|
|
14
|
+
const createCommand = require('./commands/create');
|
|
15
|
+
const generateCommand = require('./commands/generate');
|
|
16
|
+
const { version } = require('./package.json');
|
|
17
|
+
|
|
18
|
+
// Setup graceful shutdown
|
|
19
|
+
process.on('SIGINT', () => {
|
|
20
|
+
console.log('\n👋 Goodbye!');
|
|
21
|
+
process.exit(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
process.on('uncaughtException', (error) => {
|
|
25
|
+
if (error.name === 'ExitPromptError') {
|
|
26
|
+
console.log('\n👋 Operation cancelled');
|
|
27
|
+
process.exit(0);
|
|
28
|
+
} else {
|
|
29
|
+
console.error('❌ Unexpected error:', error.message);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// CLI Configuration
|
|
35
|
+
const cli = yargs(hideBin(process.argv))
|
|
36
|
+
.scriptName('falcon-cli')
|
|
37
|
+
.version(version)
|
|
38
|
+
.usage('Usage: $0 <command> [options]')
|
|
39
|
+
.help('h')
|
|
40
|
+
.alias('h', 'help')
|
|
41
|
+
.alias('v', 'version')
|
|
42
|
+
.demandCommand(1, 'You need at least one command before moving on')
|
|
43
|
+
.strict()
|
|
44
|
+
.recommendCommands()
|
|
45
|
+
.wrap(Math.min(120, process.stdout.columns || 80));
|
|
46
|
+
|
|
47
|
+
// Register commands
|
|
48
|
+
cli.command(createCommand);
|
|
49
|
+
cli.command(generateCommand);
|
|
50
|
+
|
|
51
|
+
// Global options
|
|
52
|
+
cli.option('verbose', {
|
|
53
|
+
alias: 'V',
|
|
54
|
+
type: 'boolean',
|
|
55
|
+
description: 'Run with verbose logging',
|
|
56
|
+
global: true
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Parse and execute
|
|
60
|
+
cli.parse();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const { input, select, confirm } = require("@inquirer/prompts");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
|
|
6
|
+
const ProjectGenerator = require('../lib/ProjectGenerator');
|
|
7
|
+
const { showLogo, showSuccess } = require('../lib/utils');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
command: 'create [name]',
|
|
11
|
+
describe: 'Create a new Falcon.js project',
|
|
12
|
+
builder: (yargs) => {
|
|
13
|
+
return yargs
|
|
14
|
+
.positional('name', {
|
|
15
|
+
describe: 'Project name',
|
|
16
|
+
type: 'string'
|
|
17
|
+
})
|
|
18
|
+
.option('template', {
|
|
19
|
+
alias: 't',
|
|
20
|
+
describe: 'Project template',
|
|
21
|
+
type: 'string',
|
|
22
|
+
choices: ['basic', 'api', 'microservice'],
|
|
23
|
+
default: 'basic'
|
|
24
|
+
})
|
|
25
|
+
.option('skip-install', {
|
|
26
|
+
describe: 'Skip npm install',
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
default: false
|
|
29
|
+
})
|
|
30
|
+
.option('yes', {
|
|
31
|
+
alias: 'y',
|
|
32
|
+
describe: 'Use default options',
|
|
33
|
+
type: 'boolean',
|
|
34
|
+
default: false
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
handler: async (argv) => {
|
|
38
|
+
try {
|
|
39
|
+
showLogo();
|
|
40
|
+
|
|
41
|
+
const generator = new ProjectGenerator({
|
|
42
|
+
verbose: argv.verbose,
|
|
43
|
+
skipInstall: argv.skipInstall,
|
|
44
|
+
useDefaults: argv.yes
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await generator.create(argv.name, argv.template);
|
|
48
|
+
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('❌ Failed to create project:', error.message);
|
|
51
|
+
if (argv.verbose) {
|
|
52
|
+
console.error(error.stack);
|
|
53
|
+
}
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const ModelGenerator = require('../lib/ModelGenerator');
|
|
2
|
+
const RouteGenerator = require('../lib/RouteGenerator');
|
|
3
|
+
const ServiceGenerator = require('../lib/ServiceGenerator');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
command: 'generate <type> [name]',
|
|
7
|
+
aliases: ['g'],
|
|
8
|
+
describe: 'Generate project components',
|
|
9
|
+
builder: (yargs) => {
|
|
10
|
+
return yargs
|
|
11
|
+
.positional('type', {
|
|
12
|
+
describe: 'Type of component to generate',
|
|
13
|
+
type: 'string',
|
|
14
|
+
choices: ['model', 'route', 'service', 'worker', 'validator']
|
|
15
|
+
})
|
|
16
|
+
.positional('name', {
|
|
17
|
+
describe: 'Component name',
|
|
18
|
+
type: 'string'
|
|
19
|
+
})
|
|
20
|
+
.option('fields', {
|
|
21
|
+
alias: 'f',
|
|
22
|
+
describe: 'Model fields (for model generation)',
|
|
23
|
+
type: 'array'
|
|
24
|
+
})
|
|
25
|
+
.option('crud', {
|
|
26
|
+
describe: 'Generate CRUD routes (for model generation)',
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
default: false
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
handler: async (argv) => {
|
|
32
|
+
try {
|
|
33
|
+
const { type, name } = argv;
|
|
34
|
+
|
|
35
|
+
switch (type) {
|
|
36
|
+
case 'model':
|
|
37
|
+
const modelGen = new ModelGenerator({ verbose: argv.verbose });
|
|
38
|
+
await modelGen.generate(name, { fields: argv.fields, crud: argv.crud });
|
|
39
|
+
break;
|
|
40
|
+
|
|
41
|
+
case 'route':
|
|
42
|
+
const routeGen = new RouteGenerator({ verbose: argv.verbose });
|
|
43
|
+
await routeGen.generate(name);
|
|
44
|
+
break;
|
|
45
|
+
|
|
46
|
+
case 'service':
|
|
47
|
+
const serviceGen = new ServiceGenerator({ verbose: argv.verbose });
|
|
48
|
+
await serviceGen.generate(name);
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
case 'worker':
|
|
52
|
+
const workerGen = new ServiceGenerator({ verbose: argv.verbose, type: 'worker' });
|
|
53
|
+
await workerGen.generate(name);
|
|
54
|
+
break;
|
|
55
|
+
|
|
56
|
+
case 'validator':
|
|
57
|
+
const validatorGen = new RouteGenerator({ verbose: argv.verbose, type: 'validator' });
|
|
58
|
+
await validatorGen.generate(name);
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
default:
|
|
62
|
+
console.error(`❌ Unknown generator type: ${type}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(`❌ Failed to generate ${argv.type}:`, error.message);
|
|
68
|
+
if (argv.verbose) {
|
|
69
|
+
console.error(error.stack);
|
|
70
|
+
}
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// buildSchemaInteractive.js
|
|
2
|
+
const {
|
|
3
|
+
input,
|
|
4
|
+
confirm,
|
|
5
|
+
select,
|
|
6
|
+
checkbox,
|
|
7
|
+
rawlist,
|
|
8
|
+
} = require('@inquirer/prompts');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Interactive function to build Mongoose JSON schema
|
|
12
|
+
* @returns {Promise<Object>} Final schema JSON
|
|
13
|
+
*/
|
|
14
|
+
async function buildMongooseSchemaInteractive() {
|
|
15
|
+
console.log('\nMongoose Schema Builder\n'.bold);
|
|
16
|
+
|
|
17
|
+
// 1. Model Name
|
|
18
|
+
const modelName = await input({
|
|
19
|
+
message: 'Model name (e.g., User, Product):',
|
|
20
|
+
validate: (v) => v.trim() ? true : 'Model name is required',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// 2. Options
|
|
24
|
+
const useTimestamps = await confirm({
|
|
25
|
+
message: 'Add timestamps (createdAt, updatedAt)?',
|
|
26
|
+
default: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const schema = {};
|
|
30
|
+
const options = { timestamps: useTimestamps };
|
|
31
|
+
|
|
32
|
+
console.log('\nAdd fields (one at a time):\n');
|
|
33
|
+
|
|
34
|
+
while (true) {
|
|
35
|
+
const addMore = await confirm({
|
|
36
|
+
message: 'Add a new field?',
|
|
37
|
+
default: true,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!addMore) break;
|
|
41
|
+
|
|
42
|
+
const fieldName = await input({
|
|
43
|
+
message: 'Field name:',
|
|
44
|
+
validate: (v) => v.trim() && !schema[v] ? true : 'Invalid or duplicate field name',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const fieldType = await select({
|
|
48
|
+
message: `Type for "${fieldName}":`,
|
|
49
|
+
choices: [
|
|
50
|
+
{ name: 'string', value: 'string' },
|
|
51
|
+
{ name: 'number', value: 'number' },
|
|
52
|
+
{ name: 'boolean', value: 'boolean' },
|
|
53
|
+
{ name: 'date', value: 'date' },
|
|
54
|
+
{ name: 'objectid', value: 'objectid' },
|
|
55
|
+
{ name: 'array', value: 'array' },
|
|
56
|
+
{ name: 'object', value: 'object' },
|
|
57
|
+
{ name: 'mixed', value: 'mixed' },
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const field = { type: fieldType };
|
|
62
|
+
|
|
63
|
+
// Common options
|
|
64
|
+
const required = await confirm({ message: 'Required?', default: false });
|
|
65
|
+
if (required) field.required = true;
|
|
66
|
+
|
|
67
|
+
const unique = await confirm({ message: 'Unique?', default: false });
|
|
68
|
+
if (unique) field.unique = true;
|
|
69
|
+
|
|
70
|
+
const index = await confirm({ message: 'Indexed?', default: false });
|
|
71
|
+
if (index) field.index = true;
|
|
72
|
+
|
|
73
|
+
// Type-specific
|
|
74
|
+
if (fieldType === 'string') {
|
|
75
|
+
const extras = await checkbox({
|
|
76
|
+
message: 'String options:',
|
|
77
|
+
choices: [
|
|
78
|
+
{ name: 'trim', value: 'trim' },
|
|
79
|
+
{ name: 'lowercase', value: 'lowercase' },
|
|
80
|
+
{ name: 'uppercase', value: 'uppercase' },
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
extras.forEach(opt => field[opt] = true);
|
|
84
|
+
|
|
85
|
+
const hasEnum = await confirm({ message: 'Use enum values?', default: false });
|
|
86
|
+
if (hasEnum) {
|
|
87
|
+
const enumStr = await input({ message: 'Comma-separated values (e.g., admin,user,guest):' });
|
|
88
|
+
field.enum = enumStr.split(',').map(v => v.trim()).filter(v => v);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (fieldType === 'date' || fieldType === 'number') {
|
|
93
|
+
const hasDefaultNow = await confirm({ message: 'Default to now/current?', default: fieldType === 'date' });
|
|
94
|
+
if (hasDefaultNow) {
|
|
95
|
+
field.default = 'now';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (fieldType === 'array') {
|
|
100
|
+
const itemType = await select({
|
|
101
|
+
message: 'Array item type:',
|
|
102
|
+
choices: [
|
|
103
|
+
{ name: 'string', value: 'string' },
|
|
104
|
+
{ name: 'number', value: 'number' },
|
|
105
|
+
{ name: 'boolean', value: 'boolean' },
|
|
106
|
+
{ name: 'date', value: 'date' },
|
|
107
|
+
{ name: 'objectid', value: 'objectid' },
|
|
108
|
+
{ name: 'object', value: 'object' },
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (itemType === 'object') {
|
|
113
|
+
console.log(`\nDefine nested object for "${fieldName}[]":\n`);
|
|
114
|
+
const nested = await buildNestedObject();
|
|
115
|
+
field.items = nested;
|
|
116
|
+
} else {
|
|
117
|
+
field.items = { type: itemType };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (fieldType === 'object') {
|
|
122
|
+
console.log(`\nDefine nested object for "${fieldName}":\n`);
|
|
123
|
+
field.properties = await buildNestedObject();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (fieldType === 'objectid') {
|
|
127
|
+
const ref = await input({ message: 'Reference model (e.g., User):', default: modelName });
|
|
128
|
+
field.ref = ref.trim();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
schema[fieldName] = field;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
name: modelName,
|
|
136
|
+
type: 'mongodb',
|
|
137
|
+
schema,
|
|
138
|
+
options,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Helper: recursively build nested object
|
|
143
|
+
async function buildNestedObject() {
|
|
144
|
+
const props = {};
|
|
145
|
+
|
|
146
|
+
while (true) {
|
|
147
|
+
const add = await confirm({ message: 'Add property to this object?', default: true });
|
|
148
|
+
if (!add) break;
|
|
149
|
+
|
|
150
|
+
const name = await input({ message: 'Property name:' });
|
|
151
|
+
const type = await select({
|
|
152
|
+
message: 'Type:',
|
|
153
|
+
choices: [
|
|
154
|
+
'string', 'number', 'boolean', 'date', 'objectid', 'array', 'object', 'mixed'
|
|
155
|
+
].map(t => ({ name: t, value: t })),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const prop = { type };
|
|
159
|
+
|
|
160
|
+
if (type === 'string') {
|
|
161
|
+
const opts = await checkbox({
|
|
162
|
+
message: 'Options:',
|
|
163
|
+
choices: ['required', 'trim', 'lowercase', 'uppercase'],
|
|
164
|
+
});
|
|
165
|
+
opts.forEach(o => prop[o] = true);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (['string', 'number', 'date'].includes(type)) {
|
|
169
|
+
const req = await confirm({ message: 'Required?', default: false });
|
|
170
|
+
if (req) prop.required = true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (type === 'array') {
|
|
174
|
+
const itemType = await select({ message: 'Item type:', choices: ['string', 'number', 'object'].map(t => ({ name: t, value: t })) });
|
|
175
|
+
prop.items = itemType === 'object' ? await buildNestedObject() : { type: itemType };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (type === 'object') {
|
|
179
|
+
console.log(`\nNested object for "${name}":\n`);
|
|
180
|
+
prop.properties = await buildNestedObject();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
props[name] = prop;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return props;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { buildMongooseSchemaInteractive };
|