@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,128 @@
|
|
|
1
|
+
// buildSequelizeSchemaInteractive.js
|
|
2
|
+
const {
|
|
3
|
+
input,
|
|
4
|
+
confirm,
|
|
5
|
+
select,
|
|
6
|
+
checkbox,
|
|
7
|
+
} = require('@inquirer/prompts');
|
|
8
|
+
|
|
9
|
+
async function buildSequelizeSchemaInteractive() {
|
|
10
|
+
console.log('\nSequelize Model + Migration Builder\n');
|
|
11
|
+
|
|
12
|
+
const modelName = await input({
|
|
13
|
+
message: 'Model name (singular, e.g., User):',
|
|
14
|
+
validate: v => v.trim() ? true : 'Required',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const tableName = await input({
|
|
18
|
+
message: 'Table name (or press Enter to use model name):',
|
|
19
|
+
default: modelName.toLowerCase() + 's',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const useTimestamps = await confirm({
|
|
23
|
+
message: 'Add timestamps (createdAt, updatedAt)?',
|
|
24
|
+
default: true,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const useParanoid = await confirm({
|
|
28
|
+
message: 'Enable soft deletes (deletedAt)?',
|
|
29
|
+
default: false,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const schema = {};
|
|
33
|
+
const associations = [];
|
|
34
|
+
const options = {
|
|
35
|
+
timestamps: useTimestamps,
|
|
36
|
+
paranoid: useParanoid,
|
|
37
|
+
tableName: tableName.trim() || undefined,
|
|
38
|
+
freezeTableName: true,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ── Fields
|
|
42
|
+
while (true) {
|
|
43
|
+
const add = await confirm({ message: 'Add field?', default: true });
|
|
44
|
+
if (!add) break;
|
|
45
|
+
|
|
46
|
+
const fieldName = await input({
|
|
47
|
+
message: 'Field name:',
|
|
48
|
+
validate: v => v && !schema[v] ? true : 'Invalid/duplicate',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const dataType = await select({
|
|
52
|
+
message: `Data type for "${fieldName}":`,
|
|
53
|
+
choices: [
|
|
54
|
+
'STRING', 'TEXT', 'INTEGER', 'BIGINT', 'FLOAT', 'DOUBLE',
|
|
55
|
+
'BOOLEAN', 'DATE', 'DATEONLY', 'JSON', 'ARRAY', 'ENUM'
|
|
56
|
+
].map(t => ({ name: t, value: t })),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const field = { type: dataType };
|
|
60
|
+
|
|
61
|
+
const allowNull = await confirm({ message: 'Allow NULL?', default: true });
|
|
62
|
+
if (!allowNull) field.allowNull = false;
|
|
63
|
+
|
|
64
|
+
const unique = await confirm({ message: 'Unique?', default: false });
|
|
65
|
+
if (unique) field.unique = true;
|
|
66
|
+
|
|
67
|
+
// Default value
|
|
68
|
+
const hasDefault = await confirm({ message: 'Set default?', default: false });
|
|
69
|
+
if (hasDefault) {
|
|
70
|
+
if (dataType === 'BOOLEAN') {
|
|
71
|
+
field.defaultValue = await confirm({ message: 'Default TRUE?', default: true });
|
|
72
|
+
} else if (dataType === 'DATE') {
|
|
73
|
+
field.defaultValue = await confirm({ message: 'Default NOW?', default: true })
|
|
74
|
+
? 'sequelize.fn("NOW")'
|
|
75
|
+
: await input({ message: 'Default (JS):' });
|
|
76
|
+
} else if (dataType === 'ENUM') {
|
|
77
|
+
const vals = await input({ message: 'Enum values (comma-separated):' });
|
|
78
|
+
field.values = vals.split(',').map(v => v.trim()).filter(Boolean);
|
|
79
|
+
const def = await input({ message: 'Default value:' });
|
|
80
|
+
field.defaultValue = def.trim();
|
|
81
|
+
} else {
|
|
82
|
+
const def = await input({ message: 'Default value (JS):' });
|
|
83
|
+
field.defaultValue = def;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// String length
|
|
88
|
+
if (dataType === 'STRING') {
|
|
89
|
+
const len = await input({ message: 'Length (default 255):', default: '255' });
|
|
90
|
+
if (len !== '255') field.type = `STRING(${len})`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Array items
|
|
94
|
+
if (dataType === 'ARRAY') {
|
|
95
|
+
const itemType = await select({
|
|
96
|
+
message: 'Array item type:',
|
|
97
|
+
choices: ['STRING', 'INTEGER', 'FLOAT', 'BOOLEAN', 'DATE'].map(t => ({ name: t, value: t })),
|
|
98
|
+
});
|
|
99
|
+
field.type = `${itemType}[]`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
schema[fieldName] = field;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Associations
|
|
106
|
+
while (true) {
|
|
107
|
+
const addAssoc = await confirm({ message: 'Add association?', default: false });
|
|
108
|
+
if (!addAssoc) break;
|
|
109
|
+
|
|
110
|
+
const type = await select({
|
|
111
|
+
message: 'Association type:',
|
|
112
|
+
choices: [
|
|
113
|
+
{ name: 'belongsTo', value: 'belongsTo' },
|
|
114
|
+
{ name: 'hasMany', value: 'hasMany' },
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const targetModel = await input({ message: 'Target model name:' });
|
|
119
|
+
const foreignKey = await input({ message: 'Foreign key (in this table):', default: `${targetModel}Id` });
|
|
120
|
+
const as = await input({ message: 'Alias (as):', default: targetModel.toLowerCase() });
|
|
121
|
+
|
|
122
|
+
associations.push({ type, targetModel, foreignKey, as });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { name: modelName, tableName: tableName.trim(), schema, associations, options };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { buildSequelizeSchemaInteractive };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// createJoiSchemaFromJson.js
|
|
2
|
+
const Joi = require('joi');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts custom JSON schema → Joi schema
|
|
6
|
+
* @param {Object} config - Your JSON config
|
|
7
|
+
* @returns {Joi.ObjectSchema}
|
|
8
|
+
*/
|
|
9
|
+
function createJoiSchemaFromJson(config) {
|
|
10
|
+
if (!config || !config.schema) {
|
|
11
|
+
throw new Error('Config must have "schema"');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const schemaDef = config.schema;
|
|
15
|
+
const joiDef = {};
|
|
16
|
+
|
|
17
|
+
for (const [fieldName, field] of Object.entries(schemaDef)) {
|
|
18
|
+
let joiField = null;
|
|
19
|
+
|
|
20
|
+
const type = (field.type || '').toLowerCase();
|
|
21
|
+
|
|
22
|
+
// Base type
|
|
23
|
+
switch (type) {
|
|
24
|
+
case 'string':
|
|
25
|
+
joiField = Joi.string();
|
|
26
|
+
if (field.trim) joiField = joiField.trim();
|
|
27
|
+
if (field.lowercase) joiField = joiField.lowercase();
|
|
28
|
+
if (field.uppercase) joiField = joiField.uppercase();
|
|
29
|
+
if (field.email) joiField = joiField.email();
|
|
30
|
+
if (field.min) joiField = joiField.min(field.min);
|
|
31
|
+
if (field.max) joiField = joiField.max(field.max);
|
|
32
|
+
if (field.regex) joiField = joiField.pattern(new RegExp(field.regex));
|
|
33
|
+
if (field.enum) joiField = joiField.valid(...field.enum);
|
|
34
|
+
break;
|
|
35
|
+
|
|
36
|
+
case 'number':
|
|
37
|
+
joiField = Joi.number();
|
|
38
|
+
if (field.min !== undefined) joiField = joiField.min(field.min);
|
|
39
|
+
if (field.max !== undefined) joiField = joiField.max(field.max);
|
|
40
|
+
if (field.integer) joiField = joiField.integer();
|
|
41
|
+
break;
|
|
42
|
+
|
|
43
|
+
case 'boolean':
|
|
44
|
+
joiField = Joi.boolean();
|
|
45
|
+
break;
|
|
46
|
+
|
|
47
|
+
case 'date':
|
|
48
|
+
joiField = Joi.date();
|
|
49
|
+
if (field.min) joiField = joiField.min(field.min);
|
|
50
|
+
if (field.max) joiField = joiField.max(field.max);
|
|
51
|
+
break;
|
|
52
|
+
|
|
53
|
+
case 'array':
|
|
54
|
+
const items = convertItem(field.items || { type: 'string' });
|
|
55
|
+
joiField = Joi.array().items(items);
|
|
56
|
+
if (field.min) joiField = joiField.min(field.min);
|
|
57
|
+
if (field.max) joiField = joiField.max(field.max);
|
|
58
|
+
if (field.unique) joiField = joiField.unique();
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
case 'object':
|
|
62
|
+
joiField = Joi.object(createNestedJoi(field.properties || {}));
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case 'objectid':
|
|
66
|
+
joiField = field.ref
|
|
67
|
+
? Joi.any().meta({ ref: field.ref }) // custom tag
|
|
68
|
+
: Joi.string().length(24).hex(); // MongoID
|
|
69
|
+
break;
|
|
70
|
+
|
|
71
|
+
case 'mixed':
|
|
72
|
+
default:
|
|
73
|
+
joiField = Joi.any();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Required
|
|
77
|
+
if (field.required) {
|
|
78
|
+
joiField = joiField.required();
|
|
79
|
+
} else {
|
|
80
|
+
joiField = joiField.optional();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Default
|
|
84
|
+
if (field.default !== undefined) {
|
|
85
|
+
const def = field.default === 'now' ? new Date() : field.default;
|
|
86
|
+
joiField = joiField.default(def);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
joiDef[fieldName] = joiField;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return Joi.object(joiDef).options({ stripUnknown: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Helper: recursive nested object
|
|
96
|
+
function createNestedJoi(properties) {
|
|
97
|
+
const nested = {};
|
|
98
|
+
for (const [key, field] of Object.entries(properties)) {
|
|
99
|
+
nested[key] = convertFieldToJoi(field);
|
|
100
|
+
}
|
|
101
|
+
return nested;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Convert single field (used in array items & nested)
|
|
105
|
+
function convertFieldToJoi(field) {
|
|
106
|
+
const type = (field.type || '').toLowerCase();
|
|
107
|
+
let joi = null;
|
|
108
|
+
|
|
109
|
+
switch (type) {
|
|
110
|
+
case 'string': joi = Joi.string(); break;
|
|
111
|
+
case 'number': joi = Joi.number(); break;
|
|
112
|
+
case 'boolean': joi = Joi.boolean(); break;
|
|
113
|
+
case 'date': joi = Joi.date(); break;
|
|
114
|
+
case 'objectid': joi = Joi.string().hex().length(24); break;
|
|
115
|
+
case 'array':
|
|
116
|
+
const items = convertFieldToJoi(field.items || { type: 'string' });
|
|
117
|
+
joi = Joi.array().items(items);
|
|
118
|
+
break;
|
|
119
|
+
case 'object':
|
|
120
|
+
joi = Joi.object(createNestedJoi(field.properties || {}));
|
|
121
|
+
break;
|
|
122
|
+
default: joi = Joi.any();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (field.required) joi = joi.required();
|
|
126
|
+
if (field.default !== undefined) joi = joi.default(field.default === 'now' ? new Date() : field.default);
|
|
127
|
+
if (field.enum) joi = joi.valid(...field.enum);
|
|
128
|
+
|
|
129
|
+
return joi;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Helper for array items
|
|
133
|
+
function convertItem(item) {
|
|
134
|
+
return convertFieldToJoi(item);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { createJoiSchemaFromJson };
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// interactiveUnifiedBuilder.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 interactiveUnifiedBuilder(baseDir) {
|
|
12
|
+
console.log('\nUnified Model Builder (Mongoose / Sequelize)\n');
|
|
13
|
+
|
|
14
|
+
// Ensure base directory exists
|
|
15
|
+
if (!fs.existsSync(baseDir)) {
|
|
16
|
+
console.log(`Creating base directory: ${baseDir}`);
|
|
17
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Define subdirectories
|
|
21
|
+
const mongoDir = path.join(baseDir, 'mongo');
|
|
22
|
+
const sequelizeDir = path.join(baseDir, 'sequelize');
|
|
23
|
+
|
|
24
|
+
// Create mongo and sequelize folders if they don't exist
|
|
25
|
+
[mongoDir, sequelizeDir].forEach(dir => {
|
|
26
|
+
if (!fs.existsSync(dir)) {
|
|
27
|
+
console.log(`Creating directory: ${dir}`);
|
|
28
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// 1. Choose DB type
|
|
33
|
+
const dbType = await select({
|
|
34
|
+
message: 'Database type:',
|
|
35
|
+
choices: [
|
|
36
|
+
{ name: 'MongoDB (Mongoose)', value: 'mongodb' },
|
|
37
|
+
{ name: 'SQL (Sequelize)', value: 'sequelize' },
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const modelName = await input({
|
|
42
|
+
message: 'Model name:',
|
|
43
|
+
validate: v => v.trim() ? true : 'Required',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const tableName = dbType === 'sequelize' ? await input({
|
|
47
|
+
message: 'Table name (or Enter for default):',
|
|
48
|
+
default: modelName.toLowerCase() + 's',
|
|
49
|
+
}) : undefined;
|
|
50
|
+
|
|
51
|
+
const schema = {};
|
|
52
|
+
const associations = [];
|
|
53
|
+
const options = { timestamps: true };
|
|
54
|
+
|
|
55
|
+
if (dbType === 'sequelize') {
|
|
56
|
+
options.paranoid = await confirm({ message: 'Enable soft deletes?', default: false });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Load models from correct subdir
|
|
60
|
+
const modelsSubDir = dbType === 'mongodb' ? mongoDir : sequelizeDir;
|
|
61
|
+
const availableModels = loadModelsFromDir(modelsSubDir);
|
|
62
|
+
|
|
63
|
+
console.log('\nAdd fields:\n');
|
|
64
|
+
|
|
65
|
+
while (true) {
|
|
66
|
+
const add = await confirm({ message: 'Add field?', default: true });
|
|
67
|
+
if (!add) break;
|
|
68
|
+
|
|
69
|
+
const fieldName = await input({
|
|
70
|
+
message: 'Field name:',
|
|
71
|
+
validate: v => v && !schema[v] ? true : 'Invalid/duplicate',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const typeChoices = dbType === 'mongodb'
|
|
75
|
+
? ['string', 'number', 'boolean', 'date', 'objectid', 'array', 'object', 'mixed']
|
|
76
|
+
: ['STRING', 'TEXT', 'INTEGER', 'FLOAT', 'BOOLEAN', 'DATE', 'JSON', 'ARRAY', 'ENUM'];
|
|
77
|
+
|
|
78
|
+
const fieldType = await select({
|
|
79
|
+
message: `Type for "${fieldName}":`,
|
|
80
|
+
choices: typeChoices.map(t => ({ name: t, value: t })),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const field = { type: fieldType };
|
|
84
|
+
|
|
85
|
+
const required = await confirm({ message: 'Required?', default: false });
|
|
86
|
+
if (required) {
|
|
87
|
+
field[dbType === 'mongodb' ? 'required' : 'allowNull'] = dbType === 'mongodb' ? true : false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const unique = await confirm({ message: 'Unique?', default: false });
|
|
91
|
+
if (unique) field.unique = true;
|
|
92
|
+
|
|
93
|
+
const hasDefault = await confirm({ message: 'Set default?', default: false });
|
|
94
|
+
if (hasDefault) {
|
|
95
|
+
if (fieldType.toLowerCase().includes('boolean')) {
|
|
96
|
+
field.default = await confirm({ message: 'Default TRUE?', default: true });
|
|
97
|
+
} else if (fieldType.toLowerCase().includes('date')) {
|
|
98
|
+
field.default = await confirm({ message: 'Default NOW?', default: true }) ? 'now' : await input({ message: 'Default:' });
|
|
99
|
+
} else {
|
|
100
|
+
field.default = await input({ message: 'Default value:' });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (fieldType === 'STRING' && dbType === 'sequelize') {
|
|
105
|
+
const len = await input({ message: 'Length (default 255):', default: '255' });
|
|
106
|
+
if (len !== '255') field.type = `STRING(${len})`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (fieldType.toUpperCase() === 'ENUM') {
|
|
110
|
+
const vals = await input({ message: 'Comma-separated values:' });
|
|
111
|
+
field.values = vals.split(',').map(v => v.trim()).filter(Boolean);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (fieldType.toLowerCase() === 'array') {
|
|
115
|
+
const itemType = await select({
|
|
116
|
+
message: 'Array item type:',
|
|
117
|
+
choices: dbType === 'mongodb'
|
|
118
|
+
? ['string', 'number', 'object', 'objectid']
|
|
119
|
+
: ['STRING', 'INTEGER', 'FLOAT'],
|
|
120
|
+
});
|
|
121
|
+
field.items = { type: itemType };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (fieldType.toLowerCase() === 'object') {
|
|
125
|
+
field.properties = await buildNestedObject(dbType);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
schema[fieldName] = field;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Associations
|
|
132
|
+
if (availableModels.length > 0) {
|
|
133
|
+
while (true) {
|
|
134
|
+
const addAssoc = await confirm({ message: 'Add association?', default: false });
|
|
135
|
+
if (!addAssoc) break;
|
|
136
|
+
|
|
137
|
+
const assocType = await select({
|
|
138
|
+
message: 'Association type:',
|
|
139
|
+
choices: dbType === 'mongodb'
|
|
140
|
+
? ['hasMany', 'belongsTo']
|
|
141
|
+
: ['hasMany', 'belongsTo', 'hasOne'],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const targetModel = await select({
|
|
145
|
+
message: 'Target model:',
|
|
146
|
+
choices: availableModels.map(m => ({ name: m, value: m })),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const fk = await input({
|
|
150
|
+
message: 'Foreign key:',
|
|
151
|
+
default: `${targetModel.toLowerCase()}Id`,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const as = await input({ message: 'Alias (as):', default: targetModel.toLowerCase() });
|
|
155
|
+
|
|
156
|
+
associations.push({ type: assocType, targetModel, foreignKey: fk, as });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Build final config
|
|
161
|
+
const config = {
|
|
162
|
+
name: modelName,
|
|
163
|
+
type: dbType,
|
|
164
|
+
tableName: tableName?.trim(),
|
|
165
|
+
schema,
|
|
166
|
+
associations,
|
|
167
|
+
options,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Generate migration only for Sequelize
|
|
171
|
+
let migration = null;
|
|
172
|
+
if (dbType === 'sequelize') {
|
|
173
|
+
migration = generateMigration(config);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
config,
|
|
178
|
+
migration, // { fileName, code } or null
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Load model names from directory
|
|
183
|
+
function loadModelsFromDir(dir) {
|
|
184
|
+
if (!fs.existsSync(dir)) {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return fs.readdirSync(dir)
|
|
189
|
+
.filter(file => file.endsWith('.json'))
|
|
190
|
+
.map(file => {
|
|
191
|
+
try {
|
|
192
|
+
const filePath = path.join(dir, file);
|
|
193
|
+
const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
194
|
+
return content.name || path.basename(file, '.json');
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.warn(`Failed to read ${file}:`, err.message);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
.filter(Boolean);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Nested object builder
|
|
204
|
+
async function buildNestedObject(dbType) {
|
|
205
|
+
const props = {};
|
|
206
|
+
while (true) {
|
|
207
|
+
const add = await confirm({ message: 'Add nested field?', default: true });
|
|
208
|
+
if (!add) break;
|
|
209
|
+
const name = await input({ message: 'Name:' });
|
|
210
|
+
const type = await select({
|
|
211
|
+
message: 'Type:',
|
|
212
|
+
choices: dbType === 'mongodb'
|
|
213
|
+
? ['string', 'number', 'boolean', 'date', 'objectid']
|
|
214
|
+
: ['STRING', 'INTEGER', 'BOOLEAN', 'DATE'],
|
|
215
|
+
});
|
|
216
|
+
props[name] = { type };
|
|
217
|
+
}
|
|
218
|
+
return props;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Generate Migration String (Sequelize only)
|
|
222
|
+
function generateMigration(config) {
|
|
223
|
+
const timestamp = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14);
|
|
224
|
+
const table = config.tableName || `${config.name.toLowerCase()}s`;
|
|
225
|
+
const fileName = `${timestamp}-create-${table}.js`;
|
|
226
|
+
|
|
227
|
+
let code = `'use strict';\n\nmodule.exports = {\n`;
|
|
228
|
+
code += ` up: async (queryInterface, Sequelize) => {\n`;
|
|
229
|
+
code += ` await queryInterface.createTable('${table}', {\n`;
|
|
230
|
+
code += ` id: {\n`;
|
|
231
|
+
code += ` allowNull: false,\n`;
|
|
232
|
+
code += ` autoIncrement: true,\n`;
|
|
233
|
+
code += ` primaryKey: true,\n`;
|
|
234
|
+
code += ` type: Sequelize.INTEGER\n`;
|
|
235
|
+
code += ` },\n`;
|
|
236
|
+
|
|
237
|
+
for (const [field, def] of Object.entries(config.schema)) {
|
|
238
|
+
code += ` ${field}: {\n`;
|
|
239
|
+
code += ` type: ${formatType(def.type, def.values)},\n`;
|
|
240
|
+
if (def.allowNull === false) code += ` allowNull: false,\n`;
|
|
241
|
+
if (def.unique) code += ` unique: true,\n`;
|
|
242
|
+
if (def.default !== undefined) {
|
|
243
|
+
code += ` defaultValue: ${formatDefault(def.default)},\n`;
|
|
244
|
+
}
|
|
245
|
+
code = code.replace(/,\n$/, '\n') + ` },\n`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (config.options.timestamps) {
|
|
249
|
+
code += ` createdAt: { allowNull: false, type: Sequelize.DATE, defaultValue: Sequelize.fn('NOW') },\n`;
|
|
250
|
+
code += ` updatedAt: { allowNull: false, type: Sequelize.DATE, defaultValue: Sequelize.fn('NOW') },\n`;
|
|
251
|
+
}
|
|
252
|
+
if (config.options.paranoid) {
|
|
253
|
+
code += ` deletedAt: { type: Sequelize.DATE },\n`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
code += ` });\n`;
|
|
257
|
+
code += ` },\n\n`;
|
|
258
|
+
code += ` down: async (queryInterface) => {\n`;
|
|
259
|
+
code += ` await queryInterface.dropTable('${table}');\n`;
|
|
260
|
+
code += ` }\n};\n`;
|
|
261
|
+
|
|
262
|
+
return { fileName, code };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function formatType(type, values) {
|
|
266
|
+
if (type.includes('[]')) return `Sequelize.ARRAY(Sequelize.${type.replace('[]', '')})`;
|
|
267
|
+
if (type.includes('(')) return `Sequelize.${type}`;
|
|
268
|
+
if (type === 'ENUM') return `Sequelize.ENUM(${values.map(v => `'${v}'`).join(', ')})`;
|
|
269
|
+
return `Sequelize.${type}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function formatDefault(val) {
|
|
273
|
+
if (val === 'now') return `Sequelize.fn('NOW')`;
|
|
274
|
+
if (val === true) return 'true';
|
|
275
|
+
if (val === false) return 'false';
|
|
276
|
+
if (!isNaN(val)) return val;
|
|
277
|
+
return `'${val}'`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
module.exports = { interactiveUnifiedBuilder };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// generateAllFiles.js
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { createModelFromJson } = require('./createModelFromJson');
|
|
5
|
+
const { createJoiSchemaFromJson } = require('./createJoiSchemaFromJson');
|
|
6
|
+
|
|
7
|
+
function generateAllFiles(config) {
|
|
8
|
+
const baseDir = 'generated';
|
|
9
|
+
const modelDir = path.join(baseDir, config.type);
|
|
10
|
+
const joiDir = path.join(baseDir, 'joi');
|
|
11
|
+
const migDir = path.join(baseDir, 'migrations');
|
|
12
|
+
|
|
13
|
+
[baseDir, modelDir, joiDir, migDir].forEach(dir => {
|
|
14
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// 1. Model (Mongoose or Sequelize)
|
|
18
|
+
const result = createModelFromJson(config);
|
|
19
|
+
const modelPath = path.join(modelDir, `${config.name}.js`);
|
|
20
|
+
fs.writeFileSync(modelPath, generateModelCode(result));
|
|
21
|
+
|
|
22
|
+
// 2. Joi Schema
|
|
23
|
+
const joiSchema = createJoiSchemaFromJson({ schema: config.joi });
|
|
24
|
+
const joiCode = `const Joi = require('joi');\nmodule.exports = ${joiSchema.toString().replace('Joi.object()', 'Joi.object')};`;
|
|
25
|
+
fs.writeFileSync(path.join(joiDir, `${config.name}.js`), joiCode);
|
|
26
|
+
|
|
27
|
+
// 3. Migration (Sequelize only)
|
|
28
|
+
if (config.type === 'sequelize' && result.migration) {
|
|
29
|
+
fs.writeFileSync(path.join(migDir, result.migration.fileName), result.migration.code);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(`Model: ${modelPath}`);
|
|
33
|
+
console.log(`Joi: ${joiDir}/${config.name}.js`);
|
|
34
|
+
if (result.migration) console.log(`Migration: ${migDir}/${result.migration.fileName}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function generateModelCode(result) {
|
|
38
|
+
if (result.type === 'mongoose') {
|
|
39
|
+
return `const mongoose = require('mongoose');\nconst schema = new mongoose.Schema(${JSON.stringify(result.schema.tree, null, 2)}, { timestamps: true });\nmodule.exports = mongoose.model('${result.modelName}', schema);`;
|
|
40
|
+
} else {
|
|
41
|
+
return `const { DataTypes } = require('sequelize');\nmodule.exports = (sequelize) => {\n return sequelize.define('${result.modelName}', ${JSON.stringify(result.Model.rawAttributes, null, 2)}, { tableName: '${result.Model.getTableName()}', timestamps: true });\n};`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { generateAllFiles };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// generateJoiFile.js
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const Joi = require('joi');
|
|
5
|
+
|
|
6
|
+
function generateJoiFile(config, outputDir = './joi') {
|
|
7
|
+
const { name, schema } = config;
|
|
8
|
+
|
|
9
|
+
// Build Joi schema
|
|
10
|
+
const joiSchema = buildJoiSchema(schema);
|
|
11
|
+
|
|
12
|
+
// Generate code
|
|
13
|
+
let code = `const Joi = require('joi');\n\n`;
|
|
14
|
+
code += `const ${name} = ${joiSchema.toString().replace(/Joi\.object\(\)/g, 'Joi.object')}\n\n`;
|
|
15
|
+
code += `module.exports = ${name};\n`;
|
|
16
|
+
|
|
17
|
+
// Ensure directory
|
|
18
|
+
if (!fs.existsSync(outputDir)) {
|
|
19
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Write file
|
|
23
|
+
const filePath = path.join(outputDir, `${name}.js`);
|
|
24
|
+
fs.writeFileSync(filePath, code);
|
|
25
|
+
|
|
26
|
+
console.log(`Joi schema saved: ${filePath}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildJoiSchema(def) {
|
|
30
|
+
const schema = {};
|
|
31
|
+
|
|
32
|
+
for (const [key, field] of Object.entries(def)) {
|
|
33
|
+
let joiField;
|
|
34
|
+
|
|
35
|
+
switch (field.type) {
|
|
36
|
+
case 'string':
|
|
37
|
+
joiField = Joi.string();
|
|
38
|
+
if (field.email) joiField = joiField.email();
|
|
39
|
+
if (field.trim) joiField = joiField.trim();
|
|
40
|
+
if (field.lowercase) joiField = joiField.lowercase();
|
|
41
|
+
if (field.uppercase) joiField = joiField.uppercase();
|
|
42
|
+
if (field.alphanum) joiField = joiField.alphanum();
|
|
43
|
+
if (field.uuid) joiField = joiField.uuid();
|
|
44
|
+
if (field.min) joiField = joiField.min(field.min);
|
|
45
|
+
if (field.max) joiField = joiField.max(field.max);
|
|
46
|
+
if (field.regex) joiField = joiField.pattern(new RegExp(field.regex));
|
|
47
|
+
if (field.enum) joiField = joiField.valid(...field.enum);
|
|
48
|
+
break;
|
|
49
|
+
|
|
50
|
+
case 'number':
|
|
51
|
+
joiField = Joi.number();
|
|
52
|
+
if (field.integer) joiField = joiField.integer();
|
|
53
|
+
if (field.positive) joiField = joiField.positive();
|
|
54
|
+
if (field.negative) joiField = joiField.negative();
|
|
55
|
+
if (field.min) joiField = joiField.min(field.min);
|
|
56
|
+
if (field.max) joiField = joiField.max(field.max);
|
|
57
|
+
break;
|
|
58
|
+
|
|
59
|
+
case 'boolean':
|
|
60
|
+
joiField = Joi.boolean();
|
|
61
|
+
break;
|
|
62
|
+
|
|
63
|
+
case 'date':
|
|
64
|
+
joiField = Joi.date();
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
case 'array':
|
|
68
|
+
const items = buildJoiSchema({ item: field.items }).item;
|
|
69
|
+
joiField = Joi.array().items(items);
|
|
70
|
+
if (field.min) joiField = joiField.min(field.min);
|
|
71
|
+
if (field.max) joiField = joiField.max(field.max);
|
|
72
|
+
if (field.unique) joiField = joiField.unique();
|
|
73
|
+
break;
|
|
74
|
+
|
|
75
|
+
case 'object':
|
|
76
|
+
joiField = Joi.object(buildJoiSchema(field.properties));
|
|
77
|
+
break;
|
|
78
|
+
|
|
79
|
+
default:
|
|
80
|
+
joiField = Joi.any();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (field.required) joiField = joiField.required();
|
|
84
|
+
if (field.default !== undefined) {
|
|
85
|
+
const def = field.default === 'Date.now()' ? Date.now : field.default;
|
|
86
|
+
joiField = joiField.default(def);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
schema[key] = joiField;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return Joi.object(schema);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { generateJoiFile };
|