@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,290 @@
|
|
|
1
|
+
// mongooseModelBuilder.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 mongoose = require('mongoose');
|
|
11
|
+
const { Schema } = mongoose;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Interactive Mongoose model builder with timestamps & relations
|
|
15
|
+
* @param {string} outputDir - Folder to save the .js model file
|
|
16
|
+
* @returns {Promise<void>}
|
|
17
|
+
*/
|
|
18
|
+
async function mongooseModelBuilder(outputDir) {
|
|
19
|
+
console.log('\nMongoose Model Builder (with Timestamps & Relations)\n');
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(outputDir)) {
|
|
22
|
+
console.log(`Creating directory: ${outputDir}`);
|
|
23
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const modelName = await input({
|
|
27
|
+
message: 'Model name (e.g., User):',
|
|
28
|
+
validate: v => v.trim() ? true : 'Required',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const schemaDef = {};
|
|
32
|
+
const indexes = [];
|
|
33
|
+
const virtuals = [];
|
|
34
|
+
const options = { toJSON: { virtuals: true }, toObject: { virtuals: true } };
|
|
35
|
+
|
|
36
|
+
// Timestamps
|
|
37
|
+
const useTimestamps = await confirm({ message: 'Enable timestamps (createdAt, updatedAt)?', default: true });
|
|
38
|
+
if (useTimestamps) options.timestamps = true;
|
|
39
|
+
|
|
40
|
+
console.log('\nAdd fields:\n');
|
|
41
|
+
|
|
42
|
+
while (true) {
|
|
43
|
+
const addField = await confirm({ message: 'Add a field?', default: true });
|
|
44
|
+
if (!addField) break;
|
|
45
|
+
|
|
46
|
+
const fieldName = await input({
|
|
47
|
+
message: 'Field name:',
|
|
48
|
+
validate: v => v && !schemaDef[v] ? true : 'Invalid/duplicate',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const type = await select({
|
|
52
|
+
message: `Type for "${fieldName}":`,
|
|
53
|
+
choices: [
|
|
54
|
+
'String', 'Number', 'Date', 'Boolean',
|
|
55
|
+
'ObjectId', 'Array', 'Object'
|
|
56
|
+
].map(t => ({ name: t, value: t })),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const field = { type };
|
|
60
|
+
|
|
61
|
+
// Required
|
|
62
|
+
const required = await confirm({ message: 'Required?', default: false });
|
|
63
|
+
if (required) field.required = true;
|
|
64
|
+
|
|
65
|
+
// Unique
|
|
66
|
+
const unique = await confirm({ message: 'Unique?', default: false });
|
|
67
|
+
if (unique) field.unique = true;
|
|
68
|
+
|
|
69
|
+
// Default
|
|
70
|
+
const hasDefault = await confirm({ message: 'Set default?', default: false });
|
|
71
|
+
if (hasDefault) {
|
|
72
|
+
if (type === 'Boolean') {
|
|
73
|
+
field.default = await confirm({ message: 'Default TRUE?', default: true });
|
|
74
|
+
} else if (type === 'Date') {
|
|
75
|
+
field.default = await confirm({ message: 'Default NOW?', default: true }) ? 'Date.now' : await input({ message: 'JS expression:' });
|
|
76
|
+
} else {
|
|
77
|
+
field.default = await input({ message: 'Default value (JS):' });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// String-specific
|
|
82
|
+
if (type === 'String') {
|
|
83
|
+
const opts = await checkbox({
|
|
84
|
+
message: 'String options:',
|
|
85
|
+
choices: ['lowercase', 'uppercase', 'trim', 'enum', 'match'],
|
|
86
|
+
});
|
|
87
|
+
if (opts.includes('lowercase')) field.lowercase = true;
|
|
88
|
+
if (opts.includes('uppercase')) field.uppercase = true;
|
|
89
|
+
if (opts.includes('trim')) field.trim = true;
|
|
90
|
+
if (opts.includes('enum')) {
|
|
91
|
+
const values = await input({ message: 'Enum values (comma-separated):' });
|
|
92
|
+
field.enum = values.split(',').map(v => v.trim()).filter(Boolean);
|
|
93
|
+
}
|
|
94
|
+
if (opts.includes('match')) {
|
|
95
|
+
field.match = await input({ message: 'Regex pattern:' });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ObjectId → Ref
|
|
100
|
+
if (type === 'ObjectId') {
|
|
101
|
+
const refModel = await input({ message: 'Reference model name:' });
|
|
102
|
+
field.ref = refModel;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Array
|
|
106
|
+
if (type === 'Array') {
|
|
107
|
+
const itemType = await select({
|
|
108
|
+
message: 'Array item type:',
|
|
109
|
+
choices: ['String', 'Number', 'ObjectId', 'Object'],
|
|
110
|
+
});
|
|
111
|
+
if (itemType === 'ObjectId') {
|
|
112
|
+
const refModel = await input({ message: 'Ref model for array items:' });
|
|
113
|
+
field.type = [{ type: mongoose.Schema.Types.ObjectId, ref: refModel }];
|
|
114
|
+
} else if (itemType === 'Object') {
|
|
115
|
+
field.type = [await buildNestedSchema()];
|
|
116
|
+
} else {
|
|
117
|
+
field.type = [`[${itemType}]`];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Nested Object
|
|
122
|
+
if (type === 'Object') {
|
|
123
|
+
field.type = await buildNestedSchema();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
schemaDef[fieldName] = field;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Indexes
|
|
130
|
+
while (true) {
|
|
131
|
+
const addIndex = await confirm({ message: 'Add index?', default: false });
|
|
132
|
+
if (!addIndex) break;
|
|
133
|
+
|
|
134
|
+
const field = await input({ message: 'Field to index:' });
|
|
135
|
+
const order = await select({ message: 'Order:', choices: ['1 (asc)', '-1 (desc)'], default: '1' });
|
|
136
|
+
const opts = await checkbox({
|
|
137
|
+
message: 'Index options:',
|
|
138
|
+
choices: ['unique', 'sparse', 'text', 'ttl'],
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const indexObj = { [field]: parseInt(order) };
|
|
142
|
+
const indexOpts = {};
|
|
143
|
+
|
|
144
|
+
if (opts.includes('unique')) indexOpts.unique = true;
|
|
145
|
+
if (opts.includes('sparse')) indexOpts.sparse = true;
|
|
146
|
+
if (opts.includes('text')) indexOpts.text = true;
|
|
147
|
+
if (opts.includes('ttl')) {
|
|
148
|
+
const ttl = await input({ message: 'TTL seconds:', default: '3600' });
|
|
149
|
+
indexOpts.expireAfterSeconds = Number(ttl);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
indexes.push({ fields: indexObj, options: indexOpts });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Relations (Virtuals)
|
|
156
|
+
while (true) {
|
|
157
|
+
const addRel = await confirm({ message: 'Add relation (virtual)?', default: false });
|
|
158
|
+
if (!addRel) break;
|
|
159
|
+
|
|
160
|
+
const relType = await select({
|
|
161
|
+
message: 'Relation type:',
|
|
162
|
+
choices: ['hasMany', 'belongsTo', 'hasOne'],
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const targetModel = await input({ message: 'Target model name:' });
|
|
166
|
+
|
|
167
|
+
let localField, foreignField, justOne = false;
|
|
168
|
+
|
|
169
|
+
if (relType === 'belongsTo') {
|
|
170
|
+
localField = await input({ message: 'Local field (e.g., userId):', default: `${targetModel.toLowerCase()}Id` });
|
|
171
|
+
foreignField = await input({ message: 'Foreign field (e.g., _id):', default: '_id' });
|
|
172
|
+
justOne = true;
|
|
173
|
+
} else {
|
|
174
|
+
localField = await input({ message: 'Local field (e.g., _id):', default: '_id' });
|
|
175
|
+
foreignField = await input({ message: 'Foreign field (e.g., userId):', default: `${modelName.toLowerCase()}Id` });
|
|
176
|
+
justOne = relType === 'hasOne';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const virtualName = await input({
|
|
180
|
+
message: 'Virtual name (e.g., posts, author):',
|
|
181
|
+
default: relType === 'belongsTo' ? targetModel.toLowerCase() : `${targetModel.toLowerCase()}s`
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
virtuals.push({ virtualName, ref: targetModel, localField, foreignField, justOne });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Generate code
|
|
188
|
+
const code = generateMongooseModelCode(modelName, schemaDef, indexes, virtuals, options);
|
|
189
|
+
const filePath = path.join(outputDir, `${modelName}.js`);
|
|
190
|
+
|
|
191
|
+
fs.writeFileSync(filePath, code, 'utf8');
|
|
192
|
+
console.log(`\nMongoose model saved: ${filePath}`);
|
|
193
|
+
return modelName
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Nested schema builder
|
|
197
|
+
async function buildNestedSchema() {
|
|
198
|
+
const nested = {};
|
|
199
|
+
while (true) {
|
|
200
|
+
const add = await confirm({ message: 'Add nested field?', default: true });
|
|
201
|
+
if (!add) break;
|
|
202
|
+
|
|
203
|
+
const name = await input({ message: 'Field name:' });
|
|
204
|
+
const type = await select({
|
|
205
|
+
message: 'Type:',
|
|
206
|
+
choices: ['String', 'Number', 'Boolean', 'Date', 'ObjectId', 'Array', 'Object'],
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const field = { type };
|
|
210
|
+
if (type === 'ObjectId') field.ref = await input({ message: 'Ref:' });
|
|
211
|
+
if (type === 'Object') field.type = await buildNestedSchema();
|
|
212
|
+
if (type === 'Array') {
|
|
213
|
+
const itemType = await select({ message: 'Array item type:', choices: ['String', 'Number', 'ObjectId', 'Object'] });
|
|
214
|
+
field.type = itemType === 'ObjectId' ? [{ type: mongoose.Schema.Types.ObjectId, ref: await input({ message: 'Ref:' }) }] : [`[${itemType}]`];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
nested[name] = field;
|
|
218
|
+
}
|
|
219
|
+
return Object.keys(nested).length > 0 ? nested : null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Generate full .js file
|
|
223
|
+
function generateMongooseModelCode(modelName, schemaDef, indexes = [], virtuals = [], options = {}) {
|
|
224
|
+
let code = `module.exports = async function (mongoose) {\n`;
|
|
225
|
+
// code += `const { Schema } = mongoose;\n\n`;
|
|
226
|
+
|
|
227
|
+
code += `const ${modelName}Schema = new mongoose.Schema(\n`;
|
|
228
|
+
code += ` {\n`;
|
|
229
|
+
|
|
230
|
+
Object.entries(schemaDef).forEach(([name, def]) => {
|
|
231
|
+
code += ` ${name}: {\n`;
|
|
232
|
+
|
|
233
|
+
// Type
|
|
234
|
+
if (Array.isArray(def.type)) {
|
|
235
|
+
code += ` type: ${formatArrayType(def.type)},\n`;
|
|
236
|
+
} else if (typeof def.type === 'object' && def.type !== null) {
|
|
237
|
+
code += ` type: ${JSON.stringify(def.type, null, 6).replace(/\n/g, '\n ')},\n`;
|
|
238
|
+
} else {
|
|
239
|
+
code += ` type: mongoose.Schema.Types.${def.type},\n`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (def.required) code += ` required: ${def.required},\n`;
|
|
243
|
+
if (def.unique) code += ` unique: ${def.unique},\n`;
|
|
244
|
+
if (def.default !== undefined) code += ` default: ${def.default === 'Date.now' ? 'Date.now' : JSON.stringify(def.default)},\n`;
|
|
245
|
+
if (def.lowercase) code += ` lowercase: true,\n`;
|
|
246
|
+
if (def.uppercase) code += ` uppercase: true,\n`;
|
|
247
|
+
if (def.trim) code += ` trim: true,\n`;
|
|
248
|
+
if (def.enum) code += ` enum: ${JSON.stringify(def.enum)},\n`;
|
|
249
|
+
if (def.match) code += ` match: /${def.match}/,\n`;
|
|
250
|
+
if (def.ref) code += ` ref: '${def.ref}',\n`;
|
|
251
|
+
|
|
252
|
+
code = code.replace(/,\n$/, '\n') + ` },\n`;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
code += ` },\n`;
|
|
256
|
+
code += ` ${JSON.stringify(options, null, 2).replace(/\n/g, '\n ')}\n`;
|
|
257
|
+
code += `);\n\n`;
|
|
258
|
+
|
|
259
|
+
// Indexes
|
|
260
|
+
indexes.forEach(idx => {
|
|
261
|
+
const fields = JSON.stringify(idx.fields);
|
|
262
|
+
const opts = Object.keys(idx.options).length > 0 ? `, ${JSON.stringify(idx.options)}` : '';
|
|
263
|
+
code += `${modelName}mongoose.Schema.index(${fields}${opts});\n`;
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Virtuals (Relations)
|
|
267
|
+
virtuals.forEach(v => {
|
|
268
|
+
code += `\n${modelName}mongoose.Schema.virtual('${v.virtualName}', {\n`;
|
|
269
|
+
code += ` ref: '${v.ref}',\n`;
|
|
270
|
+
code += ` localField: '${v.localField}',\n`;
|
|
271
|
+
code += ` foreignField: '${v.foreignField}',\n`;
|
|
272
|
+
code += ` justOne: ${v.justOne}\n`;
|
|
273
|
+
code += ` });\n`;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
code += `\n return mongoose.model('${modelName}', ${modelName}Schema);\n`;
|
|
277
|
+
|
|
278
|
+
code += `}`;
|
|
279
|
+
|
|
280
|
+
return code;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function formatArrayType(arr) {
|
|
284
|
+
if (arr.length === 1 && typeof arr[0] === 'string' && arr[0].startsWith('[')) {
|
|
285
|
+
return arr[0];
|
|
286
|
+
}
|
|
287
|
+
return JSON.stringify(arr);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = { mongooseModelBuilder };
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// builder/mongooseModelBuilder.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 mongoose = require('mongoose');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Interactive Mongoose model builder (fixed & polished)
|
|
14
|
+
* Generates clean, correct, ready-to-use model files for Falcon
|
|
15
|
+
*/
|
|
16
|
+
async function mongooseModelBuilder(outputDir) {
|
|
17
|
+
console.log('\nMongoose Model Builder (Timestamps, Relations, Indexes)\n');
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(outputDir)) {
|
|
20
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
21
|
+
console.log(`Created directory: ${outputDir}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const modelName = await input({
|
|
25
|
+
message: 'Model name (e.g., User):',
|
|
26
|
+
validate: v => v.trim() ? true : 'Model name is required',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const capitalized = modelName.charAt(0).toUpperCase() + modelName.slice(1);
|
|
30
|
+
const schemaDef = {};
|
|
31
|
+
const indexes = [];
|
|
32
|
+
const virtuals = [];
|
|
33
|
+
const schemaOptions = {
|
|
34
|
+
toJSON: { virtuals: true },
|
|
35
|
+
toObject: { virtuals: true },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Timestamps
|
|
39
|
+
const useTimestamps = await confirm({
|
|
40
|
+
message: 'Enable timestamps (createdAt, updatedAt)?',
|
|
41
|
+
default: true,
|
|
42
|
+
});
|
|
43
|
+
if (useTimestamps) schemaOptions.timestamps = true;
|
|
44
|
+
|
|
45
|
+
console.log('\nAdd fields (leave empty to finish):\n');
|
|
46
|
+
|
|
47
|
+
while (true) {
|
|
48
|
+
const addMore = await confirm({ message: 'Add another field?', default: true });
|
|
49
|
+
if (!addMore) break;
|
|
50
|
+
|
|
51
|
+
const fieldName = await input({
|
|
52
|
+
message: 'Field name:',
|
|
53
|
+
validate: v => v && !schemaDef[v] ? true : 'Invalid or duplicate field name',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const typeChoice = await select({
|
|
57
|
+
message: `Type for "${fieldName}":`,
|
|
58
|
+
choices: [
|
|
59
|
+
'String', 'Number', 'Date', 'Boolean',
|
|
60
|
+
'ObjectId', 'Array', 'Mixed', 'Object'
|
|
61
|
+
].map(t => ({ name: t, value: t })),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const field = {};
|
|
65
|
+
|
|
66
|
+
// Handle type
|
|
67
|
+
switch (typeChoice) {
|
|
68
|
+
case 'String': field.type = String; break;
|
|
69
|
+
case 'Number': field.type = Number; break;
|
|
70
|
+
case 'Date': field.type = Date; break;
|
|
71
|
+
case 'Boolean': field.type = Boolean; break;
|
|
72
|
+
case 'Mixed': field.type = mongoose.Schema.Types.Mixed; break;
|
|
73
|
+
case 'ObjectId': field.type = mongoose.Schema.Types.ObjectId; break;
|
|
74
|
+
case 'Array':
|
|
75
|
+
case 'Object':
|
|
76
|
+
// Handled below
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Required / Unique
|
|
81
|
+
if (await confirm({ message: 'Required?', default: false })) field.required = true;
|
|
82
|
+
if (await confirm({ message: 'Unique?', default: false })) field.unique = true;
|
|
83
|
+
|
|
84
|
+
// Default value
|
|
85
|
+
const hasDefault = await confirm({ message: 'Set default value?', default: false });
|
|
86
|
+
if (hasDefault) {
|
|
87
|
+
if (typeChoice === 'Boolean') {
|
|
88
|
+
field.default = await confirm({ message: 'Default = true?', default: true });
|
|
89
|
+
} else if (typeChoice === 'Date') {
|
|
90
|
+
field.default = await confirm({ message: 'Default = now?', default: true }) ? Date.now : await input({ message: 'JS expression:' });
|
|
91
|
+
} else if (typeChoice === 'Number') {
|
|
92
|
+
field.default = Number(await input({ message: 'Default number:' }));
|
|
93
|
+
} else {
|
|
94
|
+
field.default = await input({ message: 'Default value (JS):' });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// String options
|
|
99
|
+
if (typeChoice === 'String') {
|
|
100
|
+
const opts = await checkbox({
|
|
101
|
+
message: 'String options:',
|
|
102
|
+
choices: ['trim', 'lowercase', 'uppercase', 'enum', 'match'],
|
|
103
|
+
});
|
|
104
|
+
if (opts.includes('trim')) field.trim = true;
|
|
105
|
+
if (opts.includes('lowercase')) field.lowercase = true;
|
|
106
|
+
if (opts.includes('uppercase')) field.uppercase = true;
|
|
107
|
+
if (opts.includes('enum')) {
|
|
108
|
+
const values = await input({ message: 'Enum values (comma-separated):' });
|
|
109
|
+
field.enum = values.split(',').map(v => v.trim()).filter(Boolean);
|
|
110
|
+
}
|
|
111
|
+
if (opts.includes('match')) {
|
|
112
|
+
const pattern = await input({ message: 'Regex pattern (without //):' });
|
|
113
|
+
field.match = new RegExp(pattern);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Reference (ObjectId)
|
|
118
|
+
if (typeChoice === 'ObjectId') {
|
|
119
|
+
const ref = await input({ message: 'Reference model (e.g., User):' });
|
|
120
|
+
field.ref = ref;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Array type
|
|
124
|
+
if (typeChoice === 'Array') {
|
|
125
|
+
const itemType = await select({
|
|
126
|
+
message: 'Array item type:',
|
|
127
|
+
choices: ['String', 'Number', 'Boolean', 'Date', 'ObjectId', 'Object', 'Mixed'],
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (itemType === 'ObjectId') {
|
|
131
|
+
const ref = await input({ message: 'Ref model:' });
|
|
132
|
+
field.type = [{ type: mongoose.Schema.Types.ObjectId, ref }];
|
|
133
|
+
} else if (itemType === 'Object') {
|
|
134
|
+
field.type = [await buildNestedSchema()];
|
|
135
|
+
} else if (itemType === 'Mixed') {
|
|
136
|
+
field.type = [mongoose.Schema.Types.Mixed];
|
|
137
|
+
} else {
|
|
138
|
+
const map = { String, Number, Boolean, Date };
|
|
139
|
+
field.type = [map[itemType]];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Nested object
|
|
144
|
+
if (typeChoice === 'Object') {
|
|
145
|
+
field.type = await buildNestedSchema();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
schemaDef[fieldName] = field;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Indexes
|
|
152
|
+
while (await confirm({ message: 'Add index?', default: false })) {
|
|
153
|
+
const field = await input({ message: 'Field(s) to index (comma-separated):' });
|
|
154
|
+
const fields = field.split(',').reduce((obj, f) => {
|
|
155
|
+
const trimmed = f.trim();
|
|
156
|
+
obj[trimmed] = trimmed.startsWith('-') ? -1 : 1;
|
|
157
|
+
return obj;
|
|
158
|
+
}, {});
|
|
159
|
+
|
|
160
|
+
const options = {};
|
|
161
|
+
const opts = await checkbox({
|
|
162
|
+
message: 'Index options:',
|
|
163
|
+
choices: ['unique', 'sparse', 'text', 'ttl'],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (opts.includes('unique')) options.unique = true;
|
|
167
|
+
if (opts.includes('sparse')) options.sparse = true;
|
|
168
|
+
if (opts.includes('text')) options.text = true;
|
|
169
|
+
if (opts.includes('ttl')) {
|
|
170
|
+
const secs = await input({ message: 'TTL seconds:', default: '3600' });
|
|
171
|
+
options.expireAfterSeconds = Number(secs);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
indexes.push({ fields, options });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Virtuals (Relations)
|
|
178
|
+
while (await confirm({ message: 'Add virtual relation?', default: false })) {
|
|
179
|
+
const relType = await select({
|
|
180
|
+
message: 'Relation type:',
|
|
181
|
+
choices: ['belongsTo', 'hasOne', 'hasMany'],
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const targetModel = await input({ message: 'Target model:' });
|
|
185
|
+
const virtualName = await input({
|
|
186
|
+
message: 'Virtual field name:',
|
|
187
|
+
default: relType === 'hasMany' ? targetModel.toLowerCase() + 's' : targetModel.toLowerCase(),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
let localField, foreignField, justOne = false;
|
|
191
|
+
|
|
192
|
+
if (relType === 'belongsTo') {
|
|
193
|
+
localField = await input({ message: 'Local field (e.g., userId):', default: targetModel.toLowerCase() + 'Id' });
|
|
194
|
+
foreignField = await input({ message: 'Foreign field:', default: '_id' });
|
|
195
|
+
justOne = true;
|
|
196
|
+
} else {
|
|
197
|
+
localField = await input({ message: 'Local field:', default: '_id' });
|
|
198
|
+
foreignField = await input({ message: 'Foreign field:', default: modelName.toLowerCase() + 'Id' });
|
|
199
|
+
justOne = relType === 'hasOne';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
virtuals.push({ virtualName, ref: targetModel, localField, foreignField, justOne });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Generate & save
|
|
206
|
+
const code = generateMongooseModelCode(capitalized, schemaDef, indexes, virtuals, schemaOptions);
|
|
207
|
+
const filePath = path.join(outputDir, `${capitalized}.js`);
|
|
208
|
+
|
|
209
|
+
fs.writeFileSync(filePath, code, 'utf8');
|
|
210
|
+
console.log(`\nModel created: ${filePath}\n`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Nested schema helper
|
|
214
|
+
async function buildNestedSchema() {
|
|
215
|
+
const obj = {};
|
|
216
|
+
console.log('\nNested object:\n');
|
|
217
|
+
while (await confirm({ message: 'Add nested field?', default: true })) {
|
|
218
|
+
const name = await input({ message: 'Field name:' });
|
|
219
|
+
const type = await select({
|
|
220
|
+
message: 'Type:',
|
|
221
|
+
choices: ['String', 'Number', 'Boolean', 'Date', 'ObjectId', 'Mixed'],
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const field = {};
|
|
225
|
+
switch (type) {
|
|
226
|
+
case 'String': field.type = String; break;
|
|
227
|
+
case 'Number': field.type = Number; break;
|
|
228
|
+
case 'Boolean': field.type = Boolean; break;
|
|
229
|
+
case 'Date': field.type = Date; break;
|
|
230
|
+
case 'Mixed': field.type = mongoose.Schema.Types.Mixed; break;
|
|
231
|
+
case 'ObjectId':
|
|
232
|
+
field.type = mongoose.Schema.Types.ObjectId;
|
|
233
|
+
field.ref = await input({ message: 'Ref model:' });
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
obj[name] = field;
|
|
237
|
+
}
|
|
238
|
+
return obj;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Clean, correct code generation
|
|
242
|
+
function generateMongooseModelCode(modelName, fields, indexes = [], virtuals = [], options = {}) {
|
|
243
|
+
let code = `module.exports = async function (mongoose) {\n`;
|
|
244
|
+
code += ` const { Schema } = mongoose;\n\n`;
|
|
245
|
+
code += ` const ${modelName}Schema = new Schema(\n`;
|
|
246
|
+
code += ` {\n`;
|
|
247
|
+
|
|
248
|
+
Object.entries(fields).forEach(([name, def]) => {
|
|
249
|
+
code += ` ${name}: {\n`;
|
|
250
|
+
if (def.type && typeof def.type !== 'object') {
|
|
251
|
+
const typeName = def.type === String ? 'String' :
|
|
252
|
+
def.type === Number ? 'Number' :
|
|
253
|
+
def.type === Boolean ? 'Boolean' :
|
|
254
|
+
def.type === Date ? 'Date' :
|
|
255
|
+
def.type === mongoose.Schema.Types.ObjectId ? 'Schema.Types.ObjectId' :
|
|
256
|
+
def.type === mongoose.Schema.Types.Mixed ? 'Schema.Types.Mixed' : '???';
|
|
257
|
+
code += ` type: ${typeName},\n`;
|
|
258
|
+
} else if (Array.isArray(def.type)) {
|
|
259
|
+
const inner = def.type[0];
|
|
260
|
+
if (typeof inner === 'function') {
|
|
261
|
+
const name = inner === String ? 'String' : inner === Number ? 'Number' : '???';
|
|
262
|
+
code += ` type: [${name}],\n`;
|
|
263
|
+
} else {
|
|
264
|
+
code += ` type: ${JSON.stringify(def.type, null, 8).replace(/\n/g, '\n ')},\n`;
|
|
265
|
+
}
|
|
266
|
+
} else if (def.type) {
|
|
267
|
+
code += ` type: ${JSON.stringify(def.type, null, 8).replace(/\n/g, '\n ')},\n`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (def.required) code += ` required: ${JSON.stringify(def.required)},\n`;
|
|
271
|
+
if (def.unique) code += ` unique: ${def.unique},\n`;
|
|
272
|
+
if (def.default !== undefined) {
|
|
273
|
+
const defVal = def.default === Date.now ? 'Date.now' : JSON.stringify(def.default);
|
|
274
|
+
code += ` default: ${defVal},\n`;
|
|
275
|
+
}
|
|
276
|
+
if (def.ref) code += ` ref: '${def.ref}',\n`;
|
|
277
|
+
if (def.enum) code += ` enum: ${JSON.stringify(def.enum)},\n`;
|
|
278
|
+
if (def.match) code += ` match: ${def.match},\n`;
|
|
279
|
+
if (def.trim) code += ` trim: true,\n`;
|
|
280
|
+
if (def.lowercase) code += ` lowercase: true,\n`;
|
|
281
|
+
if (def.uppercase) code += ` uppercase: true,\n`;
|
|
282
|
+
|
|
283
|
+
code = code.trimEnd() + '\n },\n';
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
code += ` },\n`;
|
|
287
|
+
code += ` ${JSON.stringify(options, null, 4).replace(/\n/g, '\n ')}\n`;
|
|
288
|
+
code += ` );\n\n`;
|
|
289
|
+
|
|
290
|
+
// Indexes
|
|
291
|
+
indexes.forEach(idx => {
|
|
292
|
+
const fields = JSON.stringify(idx.fields);
|
|
293
|
+
const opts = Object.keys(idx.options).length ? `, ${JSON.stringify(idx.options)}` : '';
|
|
294
|
+
code += ` ${modelName}Schema.index(${fields}${opts});\n`;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Virtuals
|
|
298
|
+
virtuals.forEach(v => {
|
|
299
|
+
code += `\n ${modelName}Schema.virtual('${v.virtualName}', {\n`;
|
|
300
|
+
code += ` ref: '${v.ref}',\n`;
|
|
301
|
+
code += ` localField: '${v.localField}',\n`;
|
|
302
|
+
code += ` foreignField: '${v.foreignField}',\n`;
|
|
303
|
+
code += ` justOne: ${v.justOne}\n`;
|
|
304
|
+
code += ` });\n`;
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
code += `\n return mongoose.model('${modelName}', ${modelName}Schema);\n`;
|
|
308
|
+
code += `};\n`;
|
|
309
|
+
|
|
310
|
+
return code;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
module.exports = { mongooseModelBuilder };
|