@triophore/falcon-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +62 -0
  2. package/auth/basic.js +8 -0
  3. package/auth/cookie.js +10 -0
  4. package/auth/jwks.js +6 -0
  5. package/auth/jwt.js +9 -0
  6. package/auth/openid.js +5 -0
  7. package/auth/webscoket.js +6 -0
  8. package/builder/EnvBuilder.js +0 -0
  9. package/builder/createCjsModule.js +95 -0
  10. package/builder/editModelInteractive.js +159 -0
  11. package/builder/interactiveModelBuilder.js +215 -0
  12. package/builder/interactiveMongobuilder.js +189 -0
  13. package/builder/interactiveUnifiedBuilder.js +277 -0
  14. package/builder/joiValidatorBuilder.js +218 -0
  15. package/builder/mongooseModelBuilder.js +290 -0
  16. package/builder/mongooseModelBuilder2.js +313 -0
  17. package/builder/runMigrations.js +106 -0
  18. package/builder/sequelizeModelBuilder.js +180 -0
  19. package/cli.js +60 -0
  20. package/commands/create.js +57 -0
  21. package/commands/generate.js +74 -0
  22. package/dev/Uset.schema.json +18 -0
  23. package/dev/buildSchemaInteractive.js +189 -0
  24. package/dev/buildSequelizeSchemaInteractive.js +128 -0
  25. package/dev/createJoiSchemaFromJson.js +137 -0
  26. package/dev/createModelFromJson.js +280 -0
  27. package/dev/generateAllFiles.js +45 -0
  28. package/dev/generateJoiFile.js +95 -0
  29. package/dev/generateSequelizeFiles.js +167 -0
  30. package/dev/interactiveJoiBuilder.js +177 -0
  31. package/dev/ra.js +22 -0
  32. package/dev/rj.js +18 -0
  33. package/dev/run.js +16 -0
  34. package/dev/run_seq.js +18 -0
  35. package/dev/tracker.js +23 -0
  36. package/editJsConfig.js +188 -0
  37. package/index.js +548 -0
  38. package/lib/ModelGenerator.js +203 -0
  39. package/lib/ProjectGenerator.js +246 -0
  40. package/lib/utils.js +100 -0
  41. package/logo.js +3 -0
  42. package/package.json +35 -0
  43. package/readme.md +2 -0
  44. package/schema.json +42 -0
  45. package/templates/auth_vals.json +3 -0
  46. package/templates/config.js +0 -0
  47. package/templates/example-route.js +94 -0
  48. package/templates/example-service.js +63 -0
  49. package/templates/example-validator.js +15 -0
  50. package/templates/example-worker.js +83 -0
  51. package/templates/index.txt +41 -0
  52. package/templates/post-init.js +78 -0
  53. package/templates/settings.js +192 -0
  54. package/templates/template1.settings.txt +15 -0
  55. package/templates/templatev1.json +38 -0
  56. package/validateJsConfig.js +125 -0
@@ -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 };
@@ -0,0 +1,277 @@
1
+ // interactiveUnifiedBuilder.js
2
+ const {
3
+ input,
4
+ confirm,
5
+ select,
6
+ } = require('@inquirer/prompts');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ async function interactiveUnifiedBuilder(baseDir, migrationsDir = './migrations') {
11
+ console.log('\nUnified Model Builder (Mongoose / Sequelize)\n');
12
+
13
+ // Ensure base directories exist
14
+ if (!fs.existsSync(baseDir)) {
15
+ console.log(`Creating base directory: ${baseDir}`);
16
+ fs.mkdirSync(baseDir, { recursive: true });
17
+ }
18
+
19
+ const mongoDir = path.join(baseDir, 'mongo');
20
+ const sequelizeDir = path.join(baseDir, 'sequelize');
21
+ [mongoDir, sequelizeDir].forEach(dir => {
22
+ if (!fs.existsSync(dir)) {
23
+ console.log(`Creating directory: ${dir}`);
24
+ fs.mkdirSync(dir, { recursive: true });
25
+ }
26
+ });
27
+
28
+ // Ensure migrations directory exists
29
+ if (!fs.existsSync(migrationsDir)) {
30
+ console.log(`Creating migrations directory: ${migrationsDir}`);
31
+ fs.mkdirSync(migrationsDir, { recursive: true });
32
+ }
33
+
34
+ // 1. Choose DB type
35
+ const dbType = await select({
36
+ message: 'Database type:',
37
+ choices: [
38
+ { name: 'MongoDB (Mongoose)', value: 'mongodb' },
39
+ { name: 'SQL (Sequelize)', value: 'sequelize' },
40
+ ],
41
+ });
42
+
43
+ const modelName = await input({
44
+ message: 'Model name:',
45
+ validate: v => v.trim() ? true : 'Required',
46
+ });
47
+
48
+ const tableName = dbType === 'sequelize' ? await input({
49
+ message: 'Table name (or Enter for default):',
50
+ default: modelName.toLowerCase() + 's',
51
+ }) : undefined;
52
+
53
+ const schema = {};
54
+ const associations = [];
55
+ const options = { timestamps: true };
56
+
57
+ if (dbType === 'sequelize') {
58
+ options.paranoid = await confirm({ message: 'Enable soft deletes?', default: false });
59
+ }
60
+
61
+ // Load models from correct subdir
62
+ const modelsSubDir = dbType === 'mongodb' ? mongoDir : sequelizeDir;
63
+ const availableModels = loadModelsFromDir(modelsSubDir);
64
+
65
+ console.log('\nAdd fields:\n');
66
+
67
+ while (true) {
68
+ const add = await confirm({ message: 'Add field?', default: true });
69
+ if (!add) break;
70
+
71
+ const fieldName = await input({
72
+ message: 'Field name:',
73
+ validate: v => v && !schema[v] ? true : 'Invalid/duplicate',
74
+ });
75
+
76
+ const typeChoices = dbType === 'mongodb'
77
+ ? ['string', 'number', 'boolean', 'date', 'objectid', 'array', 'object', 'mixed']
78
+ : ['STRING', 'TEXT', 'INTEGER', 'FLOAT', 'BOOLEAN', 'DATE', 'JSON', 'ARRAY', 'ENUM'];
79
+
80
+ const fieldType = await select({
81
+ message: `Type for "${fieldName}":`,
82
+ choices: typeChoices.map(t => ({ name: t, value: t })),
83
+ });
84
+
85
+ const field = { type: fieldType };
86
+
87
+ const required = await confirm({ message: 'Required?', default: false });
88
+ if (required) {
89
+ field[dbType === 'mongodb' ? 'required' : 'allowNull'] = dbType === 'mongodb' ? true : false;
90
+ }
91
+
92
+ const unique = await confirm({ message: 'Unique?', default: false });
93
+ if (unique) field.unique = true;
94
+
95
+ const hasDefault = await confirm({ message: 'Set default?', default: false });
96
+ if (hasDefault) {
97
+ if (fieldType.toLowerCase().includes('boolean')) {
98
+ field.default = await confirm({ message: 'Default TRUE?', default: true });
99
+ } else if (fieldType.toLowerCase().includes('date')) {
100
+ field.default = await confirm({ message: 'Default NOW?', default: true }) ? 'now' : await input({ message: 'Default:' });
101
+ } else {
102
+ field.default = await input({ message: 'Default value:' });
103
+ }
104
+ }
105
+
106
+ if (fieldType === 'STRING' && dbType === 'sequelize') {
107
+ const len = await input({ message: 'Length (default 255):', default: '255' });
108
+ if (len !== '255') field.type = `STRING(${len})`;
109
+ }
110
+
111
+ if (fieldType.toUpperCase() === 'ENUM') {
112
+ const vals = await input({ message: 'Comma-separated values:' });
113
+ field.values = vals.split(',').map(v => v.trim()).filter(Boolean);
114
+ }
115
+
116
+ if (fieldType.toLowerCase() === 'array') {
117
+ const itemType = await select({
118
+ message: 'Array item type:',
119
+ choices: dbType === 'mongodb'
120
+ ? ['string', 'number', 'object', 'objectid']
121
+ : ['STRING', 'INTEGER', 'FLOAT'],
122
+ });
123
+ field.items = { type: itemType };
124
+ }
125
+
126
+ if (fieldType.toLowerCase() === 'object') {
127
+ field.properties = await buildNestedObject(dbType);
128
+ }
129
+
130
+ schema[fieldName] = field;
131
+ }
132
+
133
+ // Associations
134
+ if (availableModels.length > 0) {
135
+ while (true) {
136
+ const addAssoc = await confirm({ message: 'Add association?', default: false });
137
+ if (!addAssoc) break;
138
+
139
+ const assocType = await select({
140
+ message: 'Association type:',
141
+ choices: dbType === 'mongodb'
142
+ ? ['hasMany', 'belongsTo']
143
+ : ['hasMany', 'belongsTo', 'hasOne'],
144
+ });
145
+
146
+ const targetModel = await select({
147
+ message: 'Target model:',
148
+ choices: availableModels.map(m => ({ name: m, value: m })),
149
+ });
150
+
151
+ const fk = await input({
152
+ message: 'Foreign key:',
153
+ default: `${targetModel.toLowerCase()}Id`,
154
+ });
155
+
156
+ const as = await input({ message: 'Alias (as):', default: targetModel.toLowerCase() });
157
+
158
+ associations.push({ type: assocType, targetModel, foreignKey: fk, as });
159
+ }
160
+ }
161
+
162
+ // Build final config
163
+ const config = {
164
+ name: modelName,
165
+ type: dbType,
166
+ tableName: tableName?.trim(),
167
+ schema,
168
+ associations,
169
+ options,
170
+ };
171
+
172
+ // Generate and SAVE migration for Sequelize
173
+ let migration = null;
174
+ if (dbType === 'sequelize') {
175
+ migration = generateMigration(config);
176
+ const migrationPath = path.join(migrationsDir, migration.fileName);
177
+ fs.writeFileSync(migrationPath, migration.code, 'utf8');
178
+ console.log(`Migration saved: ${migrationPath}`);
179
+ }
180
+
181
+ return {
182
+ config,
183
+ migration, // { fileName, code } or null
184
+ };
185
+ }
186
+
187
+ // Load model names
188
+ function loadModelsFromDir(dir) {
189
+ if (!fs.existsSync(dir)) return [];
190
+ return fs.readdirSync(dir)
191
+ .filter(file => file.endsWith('.json'))
192
+ .map(file => {
193
+ try {
194
+ const content = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
195
+ return content.name || path.basename(file, '.json');
196
+ } catch (err) {
197
+ console.warn(`Failed to read ${file}:`, err.message);
198
+ return null;
199
+ }
200
+ })
201
+ .filter(Boolean);
202
+ }
203
+
204
+ // Nested object
205
+ async function buildNestedObject(dbType) {
206
+ const props = {};
207
+ while (true) {
208
+ const add = await confirm({ message: 'Add nested field?', default: true });
209
+ if (!add) break;
210
+ const name = await input({ message: 'Name:' });
211
+ const type = await select({
212
+ message: 'Type:',
213
+ choices: dbType === 'mongodb'
214
+ ? ['string', 'number', 'boolean', 'date', 'objectid']
215
+ : ['STRING', 'INTEGER', 'BOOLEAN', 'DATE'],
216
+ });
217
+ props[name] = { type };
218
+ }
219
+ return props;
220
+ }
221
+
222
+ // ── Generate + Save Migration
223
+ function generateMigration(config) {
224
+ const timestamp = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14);
225
+ const table = config.tableName || `${config.name.toLowerCase()}s`;
226
+ const fileName = `${timestamp}-create-${table}.js`;
227
+
228
+ let code = `'use strict';\n\nmodule.exports = {\n`;
229
+ code += ` up: async (queryInterface, Sequelize) => {\n`;
230
+ code += ` await queryInterface.createTable('${table}', {\n`;
231
+ code += ` id: {\n`;
232
+ code += ` allowNull: false,\n`;
233
+ code += ` autoIncrement: true,\n`;
234
+ code += ` primaryKey: true,\n`;
235
+ code += ` type: Sequelize.INTEGER\n`;
236
+ code += ` },\n`;
237
+
238
+ for (const [field, def] of Object.entries(config.schema)) {
239
+ code += ` ${field}: {\n`;
240
+ code += ` type: ${formatType(def.type, def.values)},\n`;
241
+ if (def.allowNull === false) code += ` allowNull: false,\n`;
242
+ if (def.unique) code += ` unique: true,\n`;
243
+ if (def.default !== undefined) code += ` defaultValue: ${formatDefault(def.default)},\n`;
244
+ code = code.replace(/,\n$/, '\n') + ` },\n`;
245
+ }
246
+
247
+ if (config.options.timestamps) {
248
+ code += ` createdAt: { allowNull: false, type: Sequelize.DATE, defaultValue: Sequelize.fn('NOW') },\n`;
249
+ code += ` updatedAt: { allowNull: false, type: Sequelize.DATE, defaultValue: Sequelize.fn('NOW') },\n`;
250
+ }
251
+ if (config.options.paranoid) code += ` deletedAt: { type: Sequelize.DATE },\n`;
252
+
253
+ code += ` });\n`;
254
+ code += ` },\n\n`;
255
+ code += ` down: async (queryInterface) => {\n`;
256
+ code += ` await queryInterface.dropTable('${table}');\n`;
257
+ code += ` }\n};\n`;
258
+
259
+ return { fileName, code };
260
+ }
261
+
262
+ function formatType(type, values) {
263
+ if (type.includes('[]')) return `Sequelize.ARRAY(Sequelize.${type.replace('[]', '')})`;
264
+ if (type.includes('(')) return `Sequelize.${type}`;
265
+ if (type === 'ENUM') return `Sequelize.ENUM(${values.map(v => `'${v}'`).join(', ')})`;
266
+ return `Sequelize.${type}`;
267
+ }
268
+
269
+ function formatDefault(val) {
270
+ if (val === 'now') return `Sequelize.fn('NOW')`;
271
+ if (val === true) return 'true';
272
+ if (val === false) return 'false';
273
+ if (!isNaN(val)) return val;
274
+ return `'${val}'`;
275
+ }
276
+
277
+ module.exports = { interactiveUnifiedBuilder };
@@ -0,0 +1,218 @@
1
+ // builder/joiValidatorBuilder.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 joiValidatorBuilder(outputDir) {
12
+ console.log('\nJoi Validator Builder (Nested, Refs, Messages, Labels)\n');
13
+
14
+ if (!fs.existsSync(outputDir)) {
15
+ fs.mkdirSync(outputDir, { recursive: true });
16
+ console.log(`Created directory: ${outputDir}`);
17
+ }
18
+
19
+ const validatorName = await input({
20
+ message: 'Validator name (e.g., CreateUser, UpdatePost):',
21
+ validate: v => v.trim() ? true : 'Required',
22
+ });
23
+
24
+ const schemaDef = {};
25
+ const refs = new Set();
26
+
27
+ console.log('\nAdd validation rules:\n');
28
+
29
+ while (true) {
30
+ const addMore = await confirm({ message: 'Add another field?', default: true });
31
+ if (!addMore) break;
32
+
33
+ const fieldName = await input({
34
+ message: 'Field name:',
35
+ validate: v => v && !schemaDef[v] ? true : 'Invalid or duplicate',
36
+ });
37
+
38
+ const type = await select({
39
+ message: `Joi type for "${fieldName}":`,
40
+ choices: [
41
+ 'string', 'number', 'boolean', 'date',
42
+ 'object', 'array', 'binary', 'any', 'ref'
43
+ ].map(t => ({ name: t, value: t })),
44
+ });
45
+
46
+ let rule = type === 'ref' ? null : `Joi.${type}()`;
47
+
48
+ // Required / Optional / Forbidden
49
+ const presence = await select({
50
+ message: 'Presence:',
51
+ choices: [
52
+ { name: 'Required', value: 'required' },
53
+ { name: 'Optional', value: 'optional' },
54
+ { name: 'Forbidden', value: 'forbidden' },
55
+ { name: 'Allow null/empty', value: 'allow_null' },
56
+ ],
57
+ default: 'optional',
58
+ });
59
+
60
+ if (presence === 'required') rule += `.required()`;
61
+ if (presence === 'forbidden') rule += `.forbidden()`;
62
+ if (presence === 'allow_null') rule += `.allow(null, '')`;
63
+
64
+ // Label & Description
65
+ const label = await input({ message: 'Label (for error messages):', default: fieldName });
66
+ if (label) rule += `.label('${label}')`;
67
+
68
+ const description = await input({ message: 'Description (optional):' });
69
+ if (description) rule += `.description('${description}')`;
70
+
71
+ // Type-specific rules
72
+ if (type === 'string') {
73
+ const opts = await checkbox({
74
+ message: 'String rules:',
75
+ choices: ['email', 'uri', 'uuid', 'lowercase', 'uppercase', 'trim', 'min', 'max', 'length', 'pattern', 'alphanum', 'token'],
76
+ });
77
+
78
+ if (opts.includes('email')) rule += `.email({ tlds: { allow: false } })`;
79
+ if (opts.includes('uri')) rule += `.uri()`;
80
+ if (opts.includes('uuid')) rule += `.uuid()`;
81
+ if (opts.includes('lowercase')) rule += `.lowercase()`;
82
+ if (opts.includes('uppercase')) rule += `.uppercase()`;
83
+ if (opts.includes('trim')) rule += `.trim()`;
84
+ if (opts.includes('alphanum')) rule += `.alphanum()`;
85
+ if (opts.includes('token')) rule += `.token()`;
86
+
87
+ if (opts.includes('min')) {
88
+ const min = await input({ message: 'Min length:' });
89
+ rule += `.min(${min})`;
90
+ }
91
+ if (opts.includes('max')) {
92
+ const max = await input({ message: 'Max length:' });
93
+ rule += `.max(${max})`;
94
+ }
95
+ if (opts.includes('length')) {
96
+ const len = await input({ message: 'Exact length:' });
97
+ rule += `.length(${len})`;
98
+ }
99
+ if (opts.includes('pattern')) {
100
+ const regex = await input({ message: 'Regex (without //):' });
101
+ rule += `.pattern(new RegExp('${regex}'))`;
102
+ }
103
+ }
104
+
105
+ if (type === 'number') {
106
+ const opts = await checkbox({
107
+ message: 'Number rules:',
108
+ choices: ['integer', 'positive', 'min', 'max', 'greater', 'less', 'precision'],
109
+ });
110
+ if (opts.includes('integer')) rule += `.integer()`;
111
+ if (opts.includes('positive')) rule += `.positive()`;
112
+ if (opts.includes('min')) {
113
+ const min = await input({ message: 'Min value:' });
114
+ rule += `.min(${min})`;
115
+ }
116
+ if (opts.includes('max')) {
117
+ const max = await input({ message: 'Max value:' });
118
+ rule += `.max(${max})`;
119
+ }
120
+ if (opts.includes('precision')) {
121
+ const p = await input({ message: 'Precision (decimal places):' });
122
+ rule += `.precision(${p})`;
123
+ }
124
+ }
125
+
126
+ if (type === 'array') {
127
+ const itemsType = await select({
128
+ message: 'Array items type:',
129
+ choices: ['string', 'number', 'object', 'ref', 'nested validator'],
130
+ });
131
+
132
+ if (itemsType === 'ref') {
133
+ const refKey = await input({ message: 'Ref key (e.g., userId):' });
134
+ refs.add(refKey);
135
+ rule += `.items(Joi.ref('${refKey}'))`;
136
+ } else if (itemsType === 'nested validator') {
137
+ const nested = await buildNestedObject();
138
+ rule += `.items(${nested})`;
139
+ } else {
140
+ rule += `.items(Joi.${itemsType}())`;
141
+ }
142
+
143
+ const arrayRules = await checkbox({
144
+ message: 'Array rules:',
145
+ choices: ['min', 'max', 'length', 'unique'],
146
+ });
147
+ if (arrayRules.includes('min')) {
148
+ const min = await input({ message: 'Min items:' });
149
+ rule += `.min(${min})`;
150
+ }
151
+ if (arrayRules.includes('max')) {
152
+ const max = await input({ message: 'Max items:' });
153
+ rule += `.max(${max})`;
154
+ }
155
+ if (arrayRules.includes('unique')) rule += `.unique()`;
156
+ }
157
+
158
+ if (type === 'object') {
159
+ const nested = await buildNestedObject();
160
+ rule = nested;
161
+ }
162
+
163
+ if (type === 'ref') {
164
+ const refKey = await input({ message: 'Reference key (e.g., user.id):' });
165
+ refs.add(refKey.split('.')[0]);
166
+ rule = `Joi.ref('${refKey}')`;
167
+ }
168
+
169
+ // Custom error message
170
+ const customMsg = await input({ message: 'Custom error message (optional):' });
171
+ if (customMsg) rule += `.messages({ 'any.required': '${customMsg}', 'string.base': '${customMsg}' })`;
172
+
173
+ schemaDef[fieldName] = rule;
174
+ }
175
+
176
+ const code = generateJoiValidatorCode(validatorName, schemaDef, refs);
177
+ const filePath = path.join(outputDir, `${validatorName}.js`);
178
+
179
+ fs.writeFileSync(filePath, code, 'utf8');
180
+ console.log(`\nValidator created: ${filePath}\n`);
181
+ }
182
+
183
+ async function buildNestedObject() {
184
+ let code = 'Joi.object({\n';
185
+ while (await confirm({ message: 'Add nested field?', default: true })) {
186
+ const name = await input({ message: 'Field name:' });
187
+ const type = await select({
188
+ message: 'Type:',
189
+ choices: ['string', 'number', 'boolean', 'date', 'array', 'object', 'any'],
190
+ });
191
+ let rule = `Joi.${type}()`;
192
+ if (type === 'array') {
193
+ const itemType = await select({ message: 'Array items:', choices: ['string', 'number', 'object'] });
194
+ rule = `Joi.array().items(Joi.${itemType}())`;
195
+ }
196
+ if (type === 'object') rule = await buildNestedObject();
197
+ code += ` ${name}: ${rule},\n`;
198
+ }
199
+ code += ' })';
200
+ return code;
201
+ }
202
+
203
+ function generateJoiValidatorCode(name, fields, refs) {
204
+ let code = `'use strict';\n`;
205
+ code += `const Joi = require('joi');\n\n`;
206
+ code += `const ${name} = Joi.object({\n`;
207
+
208
+ Object.entries(fields).forEach(([field, rule]) => {
209
+ code += ` ${field}: ${rule},\n`;
210
+ });
211
+
212
+ code += `});\n\n`;
213
+ code += `module.exports = ${name};\n`;
214
+
215
+ return code;
216
+ }
217
+
218
+ module.exports = { joiValidatorBuilder };