beech-api 3.9.75 → 3.9.80

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 CHANGED
@@ -142,25 +142,27 @@ Options:
142
142
  ?, -h, --help Display this help message.
143
143
  -v, --version Display the application version.
144
144
 
145
- The following commands are available:
146
-
147
- $ beech make <endpoint> Create a new Endpoints and unit test file,
148
- You might using [special] `-R, --require`
149
- for choose Model(s) used to endpoint file.
150
-
151
- $ beech make <model> -M, --model Create a new Models file, You might using
152
- [special] `--no-comment` for ignore comment
153
- Table Property in your Schema.
154
-
155
- $ beech update model <model_name> Update new Table Structure for latest, You
156
- might using [special] `--no-comment` for
157
- ignore comment Table Property in your Schema.
145
+ Commands:
146
+ $ beech make <endpoint> Create a new Endpoints and unit test file.
147
+ Use [special] `-R, --require` to select Model(s).
148
+
149
+ $ beech make <model> --model, -M Create a new Models file.
150
+ Options:
151
+ --no-comment Ignore property comments in Schema.
152
+ --name <custom_name> Rename for the Model file.
153
+
154
+ $ beech update model <model_name> Update existing Model with latest DB structure.
155
+ Options:
156
+ --no-comment Ignore property comments in Schema.
157
+ --name <custom_name> Rename for the Model file.
158
+ --inject <table_name> Update schema to latest,
159
+ from a specific table name.
158
160
 
159
161
  $ beech make <helper> --helper Create a new Helpers file.
160
162
  $ beech passport init Initialize authentication with passport-jwt.
161
163
  $ beech skd init Initialize Job Scheduler file.
162
164
  $ beech key:generate, key:gen Re-Generate application key (Dangerous!).
163
- $ beech hash:<text> Hash text for Access to Database connection.
165
+ $ beech hash:<text> Hash text for Database connection access.
164
166
  ```
165
167
  ❓ **Note:** Every to create new project will be generated new ``app_key`` in ``app.config.js`` file.
166
168
 
@@ -282,7 +284,7 @@ module.exports = {
282
284
 
283
285
  Sequelize is a promise-based Node.js ORM tool for Postgres, MySQL, MariaDB, SQLite, Microsoft SQL Server, Oracle Database, Amazon Redshift and Snowflake’s Data Cloud. It features solid transaction support, relations, eager and lazy loading, read replication and more. <br/>You can learn more: [Sequelize docs](https://sequelize.org/docs/v6)
284
286
 
285
- You can asign more DataTypes, Learn more : [Sequelize docs](https://sequelize.org/docs/v6/core-concepts/model-basics/#data-types)
287
+ You can asign more DataTypes, Learn more : [Sequelize Data Types](https://sequelize.org/docs/v6/core-concepts/model-basics/#data-types)
286
288
 
287
289
  ❓ **Note:** When you generate a model it's create table structure for automatically for you.
288
290
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beech-api",
3
- "version": "3.9.75",
3
+ "version": "3.9.80",
4
4
  "description": "Command line interface for rapid Beech API development",
5
5
  "keywords": [
6
6
  "api",
@@ -244,7 +244,7 @@ async function getTableSchema(sq, tableName, cb) {
244
244
  const schema = await queryInterface.describeTable(tableName);
245
245
  cb(null, schema);
246
246
  } catch (error) {
247
- cb("Fetching table schema " + error, null);
247
+ cb(" Fetching table schema  Check " + error, null);
248
248
  }
249
249
  }
250
250
 
@@ -6,22 +6,24 @@ Options:
6
6
  ?, -h, --help Display this help message.
7
7
  -v, --version Display the application version.
8
8
 
9
- The following commands are available:
9
+ Commands:
10
+ $ beech make <endpoint> Create a new Endpoints and unit test file.
11
+ Use [special] `-R, --require` to select Model(s).
10
12
 
11
- $ beech make <endpoint> Create a new Endpoints and unit test file,
12
- You might using [special] `-R, --require`
13
- for choose Model(s) used to endpoint file.
13
+ $ beech make <model> --model, -M Create a new Models file.
14
+ Options:
15
+ --no-comment Ignore property comments in Schema.
16
+ --name <custom_name> Rename for the Model file.
14
17
 
15
- $ beech make <model> -M, --model Create a new Models file, You might using
16
- [special] `--no-comment` for ignore comment
17
- Table Property in your Schema.
18
-
19
- $ beech update model <model_name> Update new Table Structure for latest, You
20
- might using [special] `--no-comment` for
21
- ignore comment Table Property in your Schema.
18
+ $ beech update model <model_name> Update existing Model with latest DB structure.
19
+ Options:
20
+ --no-comment Ignore property comments in Schema.
21
+ --name <custom_name> Rename for the Model file.
22
+ --inject <table_name> Update schema to latest,
23
+ from a specific table name.
22
24
 
23
25
  $ beech make <helper> --helper Create a new Helpers file.
24
26
  $ beech passport init Initialize authentication with passport-jwt.
25
27
  $ beech skd init Initialize Job Scheduler file.
26
28
  $ beech key:generate, key:gen Re-Generate application key (Dangerous!).
27
- $ beech hash:<text> Hash text for Access to Database connection.
29
+ $ beech hash:<text> Hash text for Database connection access.
@@ -1,23 +1,167 @@
1
1
  module.exports = {
2
-
2
+ // --- 1. Text & String Utilities ---
3
3
  textLowerCase(text) {
4
- return text.toLowerCase();
4
+ return text ? text.toLowerCase() : '';
5
5
  },
6
6
 
7
7
  textUpperCase(text) {
8
- return text.toUpperCase();
8
+ return text ? text.toUpperCase() : '';
9
+ },
10
+
11
+ capitalize(text) {
12
+ if (!text) return '';
13
+ return text.charAt(0).toUpperCase() + text.slice(1);
9
14
  },
10
15
 
16
+ slugify(text) {
17
+ return text
18
+ .toString()
19
+ .toLowerCase()
20
+ .trim()
21
+ .replace(/\s+/g, '-')
22
+ .replace(/[^\w-]+/g, '')
23
+ .replace(/--+/g, '-');
24
+ },
25
+
26
+ stripHtmlTags(str) {
27
+ if (!str) return '';
28
+ return str.replace(/<[^>]*>?/gm, '');
29
+ },
30
+
31
+ // --- 2. Number & ID Utilities ---
11
32
  digit9(number) {
12
33
  return ('000000000' + number).slice(-9);
13
34
  },
14
35
 
36
+ formatCurrency(number) {
37
+ return new Intl.NumberFormat('th-TH', { style: 'currency', currency: 'THB' }).format(number);
38
+ },
39
+
40
+ toNumber(value, defaultValue = 0) {
41
+ const num = Number(value);
42
+ return isNaN(num) ? defaultValue : num;
43
+ },
44
+
45
+ formatBytes(bytes, decimals = 2) {
46
+ if (bytes === 0) return '0 Bytes';
47
+ const k = 1024;
48
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
49
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
50
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
51
+ },
52
+
53
+ // --- 3. Date & Time Utilities ---
15
54
  dateToThai(date) {
16
- return result = date.toLocaleDateString('th-TH', {
17
- year: 'numeric',
18
- month: 'long',
19
- day: 'numeric',
55
+ const d = new Date(date);
56
+ return d.toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric' });
57
+ },
58
+
59
+ formatDateISO(date = new Date()) {
60
+ return new Date(date).toISOString().split('T')[0];
61
+ },
62
+
63
+ calculateAge(birthDate) {
64
+ const birth = new Date(birthDate);
65
+ const today = new Date();
66
+ let age = today.getFullYear() - birth.getFullYear();
67
+ const monthDiff = today.getMonth() - birth.getMonth();
68
+ if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
69
+ age--;
70
+ }
71
+ return age;
72
+ },
73
+
74
+ addDays(date, days) {
75
+ const result = new Date(date);
76
+ result.setDate(result.getDate() + days);
77
+ return result;
78
+ },
79
+
80
+ isWeekend(date) {
81
+ const day = new Date(date).getDay();
82
+ return day === 0 || day === 6;
83
+ },
84
+
85
+ getTimestamp() {
86
+ return new Date().toISOString();
87
+ },
88
+
89
+ // --- 4. Object & Logic Utilities ---
90
+ isEmpty(value) {
91
+ return (
92
+ value === null ||
93
+ value === undefined ||
94
+ (typeof value === 'string' && value.trim() === '') ||
95
+ (Array.isArray(value) && value.length === 0) ||
96
+ (typeof value === 'object' && Object.keys(value).length === 0)
97
+ );
98
+ },
99
+
100
+ removeEmptyFields(obj) {
101
+ return Object.fromEntries(
102
+ Object.entries(obj).filter(([_, v]) => v != null)
103
+ );
104
+ },
105
+
106
+ pick(obj, keys) {
107
+ return keys.reduce((acc, key) => {
108
+ if (obj && Object.prototype.hasOwnProperty.call(obj, key)) {
109
+ acc[key] = obj[key];
110
+ }
111
+ return acc;
112
+ }, {});
113
+ },
114
+
115
+ safeJsonParse(jsonString, defaultValue = {}) {
116
+ try {
117
+ return JSON.parse(jsonString);
118
+ } catch (e) {
119
+ return defaultValue;
120
+ }
121
+ },
122
+
123
+ delay(ms) {
124
+ return new Promise(resolve => setTimeout(resolve, ms));
125
+ },
126
+
127
+ // --- 5. Web & API Utilities ---
128
+ jsonResponse(res, statusCode, message, data = null) {
129
+ return res.status(statusCode).json({
130
+ success: statusCode >= 200 && statusCode < 300,
131
+ message,
132
+ data,
133
+ timestamp: new Date().toISOString()
20
134
  });
21
135
  },
22
136
 
137
+ getQueryParam(req, key, defaultValue = null) {
138
+ return req.query[key] || defaultValue;
139
+ },
140
+
141
+ isEmail(email) {
142
+ const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
143
+ return regex.test(email);
144
+ },
145
+
146
+ toBoolean(value) {
147
+ if (typeof value === 'boolean') return value;
148
+ if (typeof value === 'string') {
149
+ return ['true', '1', 'yes', 'on'].includes(value.toLowerCase());
150
+ }
151
+ return !!value;
152
+ },
153
+
154
+ generateRandomString(length = 16) {
155
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
156
+ let result = '';
157
+ for (let i = 0; i < length; i++) {
158
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
159
+ }
160
+ return result;
161
+ },
162
+
163
+ isImageFile(filename) {
164
+ const ext = filename.split('.').pop().toLowerCase();
165
+ return ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext);
166
+ }
23
167
  };
@@ -10,14 +10,12 @@ class Generator {
10
10
  constructor() {
11
11
  this.embed(process.argv)
12
12
  .then(() => this.init()
13
- .then(status => console.log(status))
14
- .catch(err => {
13
+ .then((status) => console.log(status))
14
+ .catch((err) => {
15
15
  throw err;
16
16
  })
17
17
  )
18
- .catch(err => {
19
- throw err
20
- });
18
+ .catch(console.log);
21
19
  }
22
20
 
23
21
  init() {
@@ -77,7 +75,8 @@ class Generator {
77
75
  inquirer.prompt([ {
78
76
  type: "confirm",
79
77
  name: "confirmModelNF",
80
- message: "Model is not found, Do you only create Endpoint ?:",
78
+ message: "No model found from your selection! Do you want to create the endpoint without requiring any model ?:",
79
+ default: false,
81
80
  } ]).then(confirm => {
82
81
  if(confirm.confirmModelNF) {
83
82
  resolve([true, []]);
@@ -187,8 +186,8 @@ class Generator {
187
186
  if (!this.special) {
188
187
  resolve("\n Warning  Please specify model name to update.");
189
188
  } else {
190
- let extra = this.extra || undefined;
191
- this.updateModel(extra)
189
+ let noComment = this.noComment || false;
190
+ this.updateModel(noComment)
192
191
  .then(res => resolve(res))
193
192
  .catch(err => reject(err));
194
193
  }
@@ -472,7 +471,7 @@ class Generator {
472
471
  });
473
472
  }
474
473
 
475
- updateModel(extra) {
474
+ updateModel(noComment) {
476
475
  return new Promise((resolve, reject) => {
477
476
  try {
478
477
  this.fs.readFile("./global.config.js", 'utf8', (err, globalData) => {
@@ -482,10 +481,12 @@ class Generator {
482
481
  const appConfig = eval(appData);
483
482
  const rawSpecial = this.special; // "fruit/tropical/banana"
484
483
  const pathParts = rawSpecial.split('/');
485
- const modelName = pathParts[pathParts.length - 1];
484
+ const modelNameRaw = pathParts[pathParts.length - 1];
486
485
  const subFolder = pathParts.slice(0, pathParts.length - 1).join('/');
487
486
  const folderPrefix = subFolder ? `${subFolder}/` : '';
488
- const modelFileName = modelName.charAt(0).toUpperCase() + modelName.slice(1);
487
+ const finalTargetName = this.customName || modelNameRaw;
488
+ const modelName = this.realTableName || modelNameRaw;
489
+ const modelFileName = finalTargetName.charAt(0).toUpperCase() + finalTargetName.slice(1);
489
490
  const modelPath = `./src/models/${folderPrefix}${modelFileName}.js`;
490
491
  if (!this.fs.existsSync(modelPath)) {
491
492
  return resolve(`\n Warning  Model file \`${modelFileName}\` not found.`);
@@ -500,119 +501,167 @@ class Generator {
500
501
  const { connectForGenerateModel } = require("../databases/test");
501
502
  // pull new schema from database
502
503
  connectForGenerateModel(dbSelected.selectDbConnect, modelName, appConfig.database_config, (dbErr, tableSchema, tableName) => {
503
- if (dbErr) return reject(dbErr);
504
- // read current model file for AST
505
- this.fs.readFile(modelPath, 'utf8', (readErr, code) => {
506
- if (readErr) return reject(readErr);
507
- const ast = recast.parse(code);
508
- let isUpdated = false;
509
- // Function for map raw database type to Sequelize DataTypes AST Node
510
- const getDataTypeNode = (rawType) => {
511
- const type = rawType.toUpperCase();
512
- if (type.includes('INT')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('INTEGER'));
513
- if (type.includes('BIGINT')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('BIGINT'));
514
- if (type.includes('FLOAT')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('FLOAT'));
515
- if (type.includes('DOUBLE')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('DOUBLE'));
516
- if (type.includes('DECIMAL')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('DECIMAL'));
517
- if (type.includes('BOOLEAN') || type === 'TINYINT(1)') return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('BOOLEAN'));
518
- if (type.includes('CHAR') || type.includes('VARCHAR')) {
519
- const match = type.match(/\((\d+)\)/);
520
- const length = match ? match[1] : '255';
521
- return builders.callExpression(
522
- builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('STRING')),
523
- [builders.literal(parseInt(length))]
524
- );
525
- }
526
- if (type.includes('TEXT')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('TEXT'));
527
- if (type.includes('DATE')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('DATE'));
528
- if (type.includes('TIME')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('TIME'));
529
- if (type.includes('JSON')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('JSON'));
530
- if (type.includes('UUID')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('UUID'));
531
- if (type.includes('BLOB')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('BLOB'));
532
- if (type.includes('ENUM')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('ENUM'));
533
- if (type.includes('GEOMETRY')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('GEOMETRY'));
534
- return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('STRING'));
535
- };
536
- // recast.visit for find position of Schema(...).define() and update fields
537
- recast.visit(ast, {
538
- visitCallExpression(path) {
539
- const node = path.node;
540
- if (node.callee.property && node.callee.property.name === 'define' && node.arguments[1] && node.arguments[1].type === 'ObjectExpression') {
541
- const fieldsObject = node.arguments[1];
542
- const newFieldNames = Object.keys(tableSchema);
543
- const updatedProperties = [];
544
- newFieldNames.forEach(dbFieldName => {
545
- const dbFieldData = tableSchema[dbFieldName];
546
- let existingProperty = fieldsObject.properties.find(p => {
547
- const propName = p.key.name || p.key.value;
548
- if (propName === dbFieldName) return true;
549
- if (p.value && p.value.properties) {
550
- const hasFieldMapping = p.value.properties.find(
551
- innerP => (innerP.key.name === 'field' || innerP.key.value === 'field') &&
552
- (innerP.value.value === dbFieldName)
553
- );
554
- if (hasFieldMapping) return true;
504
+ if (dbErr) {
505
+ console.log(dbErr);
506
+ throw dbErr;
507
+ } else {
508
+ inquirer.prompt([{
509
+ type: "confirm",
510
+ name: "confirmUpdateModel",
511
+ message: "Do you want to update the model with the latest database schema ?:",
512
+ default: false,
513
+ }]).then(confirm => {
514
+ if(confirm.confirmUpdateModel) {
515
+ // read current model file for AST
516
+ this.fs.readFile(modelPath, 'utf8', (readErr, code) => {
517
+ if (readErr) return reject(readErr);
518
+ const ast = recast.parse(code);
519
+ let isUpdated = false;
520
+ let definedTableName = null;
521
+ // Check defined table name
522
+ recast.visit(ast, {
523
+ visitCallExpression(path) {
524
+ const node = path.node;
525
+ // Check is .define(...)
526
+ if (node.callee.property && node.callee.property.name === 'define') {
527
+ // Get table name from .define(...)
528
+ if (node.arguments[0] && node.arguments[0].type === 'Literal') {
529
+ definedTableName = node.arguments[0].value;
530
+ }
555
531
  }
556
- return false;
557
- });
558
- const latestFieldProps = [];
559
- // Prepare properties from Table DB (Type, PK, AI, Default, etc.)
560
- latestFieldProps.push(builders.property('init', builders.identifier('type'), getDataTypeNode(dbFieldData.type)));
561
- latestFieldProps.push(builders.property('init', builders.identifier('allowNull'), builders.literal(dbFieldData.allowNull === 'YES' || dbFieldData.allowNull === true)));
562
- if (dbFieldData.primaryKey) latestFieldProps.push(builders.property('init', builders.identifier('primaryKey'), builders.literal(true)));
563
- if (dbFieldData.autoIncrement) latestFieldProps.push(builders.property('init', builders.identifier('autoIncrement'), builders.literal(true)));
564
- if (dbFieldData.unique) latestFieldProps.push(builders.property('init', builders.identifier('unique'), builders.literal(true)));
565
- // Check if not have variable extra '--no-comment' to raw add comment
566
- if (extra !== '--no-comment') {
567
- if (dbFieldData.comment) latestFieldProps.push(builders.property('init', builders.identifier('comment'), builders.literal(dbFieldData.comment)));
568
- }
569
- if (dbFieldData.defaultValue !== null && dbFieldData.defaultValue !== undefined) {
570
- const dVal = String(dbFieldData.defaultValue).toUpperCase();
571
- const defaultNode = (dVal === 'CURRENT_TIMESTAMP' || dVal === 'NOW()')
572
- ? builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('NOW'))
573
- : builders.literal(dbFieldData.defaultValue);
574
- latestFieldProps.push(builders.property('init', builders.identifier('defaultValue'), defaultNode));
532
+ return false; // End finding when found define
575
533
  }
576
- if (!existingProperty) {
577
- existingProperty = builders.property('init', builders.identifier(dbFieldName), builders.objectExpression(latestFieldProps));
578
- isUpdated = true;
579
- } else {
580
- const currentProps = existingProperty.value.properties;
581
- latestFieldProps.forEach(newProp => {
582
- const oldProp = currentProps.find(p => p.key.name === newProp.key.name);
583
- if (!oldProp || recast.print(oldProp.value).code !== recast.print(newProp.value).code) {
584
- isUpdated = true;
585
- if (!oldProp) currentProps.push(newProp); else oldProp.value = newProp.value;
534
+ });
535
+ // Check match table name between database and model file
536
+ const targetTable = this.realTableName || this.special;
537
+ if (definedTableName !== targetTable) {
538
+ return resolve(`\n Error  Table name mismatch!
539
+ \n - In File defined as: "${definedTableName}"
540
+ \n - Your command requested: "${targetTable}"
541
+ \nNote: Please ensure you are updating the correct model/table.`);
542
+ } else {
543
+ // Function for map raw database type to Sequelize DataTypes AST Node
544
+ const getDataTypeNode = (rawType) => {
545
+ const type = rawType.toUpperCase();
546
+ if (type.includes('INT')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('INTEGER'));
547
+ if (type.includes('BIGINT')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('BIGINT'));
548
+ if (type.includes('FLOAT')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('FLOAT'));
549
+ if (type.includes('DOUBLE')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('DOUBLE'));
550
+ if (type.includes('DECIMAL')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('DECIMAL'));
551
+ if (type.includes('BOOLEAN') || type === 'TINYINT(1)') return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('BOOLEAN'));
552
+ if (type.includes('CHAR') || type.includes('VARCHAR')) {
553
+ const match = type.match(/\((\d+)\)/);
554
+ const length = match ? match[1] : '255';
555
+ return builders.callExpression(
556
+ builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('STRING')),
557
+ [builders.literal(parseInt(length))]
558
+ );
559
+ }
560
+ if (type.includes('TEXT')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('TEXT'));
561
+ if (type.includes('DATE')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('DATE'));
562
+ if (type.includes('TIME')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('TIME'));
563
+ if (type.includes('JSON')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('JSON'));
564
+ if (type.includes('UUID')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('UUID'));
565
+ if (type.includes('BLOB')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('BLOB'));
566
+ if (type.includes('ENUM')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('ENUM'));
567
+ if (type.includes('GEOMETRY')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('GEOMETRY'));
568
+ return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('STRING'));
569
+ };
570
+ // recast.visit for find position of Schema(...).define() and update fields
571
+ recast.visit(ast, {
572
+ visitCallExpression(path) {
573
+ const node = path.node;
574
+ if (node.callee.property && node.callee.property.name === 'define' && node.arguments[1] && node.arguments[1].type === 'ObjectExpression') {
575
+ const fieldsObject = node.arguments[1];
576
+ const newFieldNames = Object.keys(tableSchema);
577
+ const updatedProperties = [];
578
+ newFieldNames.forEach(dbFieldName => {
579
+ const dbFieldData = tableSchema[dbFieldName];
580
+ let existingProperty = fieldsObject.properties.find(p => {
581
+ const propName = p.key.name || p.key.value;
582
+ if (propName === dbFieldName) return true;
583
+ if (p.value && p.value.properties) {
584
+ const hasFieldMapping = p.value.properties.find(innerP => (innerP.key.name === 'field' || innerP.key.value === 'field') && (innerP.value.value === dbFieldName));
585
+ if (hasFieldMapping) return true;
586
+ }
587
+ return false;
588
+ });
589
+ const latestFieldProps = [];
590
+ // Prepare properties from Table DB (Type, PK, AI, Default, etc.)
591
+ latestFieldProps.push(builders.property('init', builders.identifier('type'), getDataTypeNode(dbFieldData.type)));
592
+ latestFieldProps.push(builders.property('init', builders.identifier('allowNull'), builders.literal(dbFieldData.allowNull === 'YES' || dbFieldData.allowNull === true)));
593
+ if (dbFieldData.primaryKey) latestFieldProps.push(builders.property('init', builders.identifier('primaryKey'), builders.literal(true)));
594
+ if (dbFieldData.autoIncrement) latestFieldProps.push(builders.property('init', builders.identifier('autoIncrement'), builders.literal(true)));
595
+ if (dbFieldData.unique) latestFieldProps.push(builders.property('init', builders.identifier('unique'), builders.literal(true)));
596
+ // Check if not have variable noComment '--no-comment' to raw add comment
597
+ if (!noComment && dbFieldData.comment) {
598
+ latestFieldProps.push(builders.property('init', builders.identifier('comment'), builders.literal(dbFieldData.comment)));
599
+ }
600
+ if (dbFieldData.defaultValue !== null && dbFieldData.defaultValue !== undefined) {
601
+ const dVal = String(dbFieldData.defaultValue).toUpperCase();
602
+ const defaultNode = (dVal === 'CURRENT_TIMESTAMP' || dVal === 'NOW()')
603
+ ? builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('NOW'))
604
+ : builders.literal(dbFieldData.defaultValue);
605
+ latestFieldProps.push(builders.property('init', builders.identifier('defaultValue'), defaultNode));
606
+ }
607
+ if (!existingProperty) {
608
+ existingProperty = builders.property('init', builders.identifier(dbFieldName), builders.objectExpression(latestFieldProps));
609
+ isUpdated = true;
610
+ } else {
611
+ const currentProps = existingProperty.value.properties;
612
+ latestFieldProps.forEach(newProp => {
613
+ const oldProp = currentProps.find(p => p.key.name === newProp.key.name);
614
+ if (!oldProp || recast.print(oldProp.value).code !== recast.print(newProp.value).code) {
615
+ isUpdated = true;
616
+ if (!oldProp) currentProps.push(newProp);
617
+ else oldProp.value = newProp.value;
618
+ }
619
+ });
620
+ let allowedPropNames = latestFieldProps.map(p => p.key.name);
621
+ allowedPropNames.push('field');
622
+ // remove comment from allowedPropNames if have noComment variable '--no-comment'
623
+ if (!noComment) {
624
+ allowedPropNames.push('comment');
625
+ }
626
+ const nextProps = currentProps.filter(p => allowedPropNames.includes(p.key.name));
627
+ if (nextProps.length !== currentProps.length) {
628
+ isUpdated = true;
629
+ existingProperty.value.properties = nextProps;
630
+ } else {
631
+ // Nothing.
632
+ }
633
+ }
634
+ updatedProperties.push(existingProperty);
635
+ });
636
+ const currentOrder = fieldsObject.properties.map(p => p.key.name || p.key.value).join(',');
637
+ const newOrder = updatedProperties.map(p => p.key.name || p.key.value).join(',');
638
+ if (currentOrder !== newOrder || fieldsObject.properties.length !== updatedProperties.length) isUpdated = true;
639
+ fieldsObject.properties = updatedProperties;
586
640
  }
641
+ return false;
642
+ }
643
+ });
644
+ // Write back updated AST, Shout updated
645
+ if (isUpdated) {
646
+ // Write updated AST back to file
647
+ const output = recast.print(ast).code;
648
+ this.fs.writeFile(modelPath, output, 'utf8', (writeErr) => {
649
+ if (writeErr) return reject(writeErr);
650
+ resolve(`\n Updated  The model \`${modelFileName}\` has been updated via AST.`);
587
651
  });
588
- const latestPropNames = latestFieldProps.map(p => p.key.name);
589
- latestPropNames.push('field');
590
- existingProperty.value.properties = currentProps.filter(p => latestPropNames.includes(p.key.name));
652
+ } else {
653
+ resolve(`\n Warning  No new fields found to update for \`${modelFileName}\`.`);
591
654
  }
592
- updatedProperties.push(existingProperty);
593
- });
594
- const currentOrder = fieldsObject.properties.map(p => p.key.name || p.key.value).join(',');
595
- const newOrder = updatedProperties.map(p => p.key.name || p.key.value).join(',');
596
- if (currentOrder !== newOrder || fieldsObject.properties.length !== updatedProperties.length) isUpdated = true;
597
- fieldsObject.properties = updatedProperties;
598
- }
599
- return false;
655
+ }
656
+ });
657
+ } else {
658
+ // Nothing.
659
+ resolve(": Say no.");
600
660
  }
601
661
  });
602
- // Write back updated AST, Shout updated
603
- if (isUpdated) {
604
- // Write updated AST back to file
605
- const output = recast.print(ast).code;
606
- this.fs.writeFile(modelPath, output, 'utf8', (writeErr) => {
607
- if (writeErr) return reject(writeErr);
608
- resolve(`\n Updated  The model \`${modelFileName}\` has been updated via AST.`);
609
- });
610
- } else {
611
- resolve(`\n Warning  No new fields found to update for \`${modelFileName}\`.`);
612
- }
613
- });
662
+ }
614
663
  });
615
- });
664
+ }).catch(console.log);
616
665
  });
617
666
  });
618
667
  } catch (error) {
@@ -638,13 +687,18 @@ class Generator {
638
687
  // Argument join `slash`
639
688
  let arg = this.argument.replace(/^\/+|\/+$/g, '');
640
689
  arg = arg.split('/');
690
+ // New can custom model name using --name <rename_model_name>
641
691
  let models = arg.pop();
692
+ let modelsChangeName = this.customModelName || models;
642
693
  let oriModelsName = models.slice(0);
643
694
  models = models.charAt(0).toUpperCase() + models.slice(1);
644
- let newModel = models.split("_").map(e => e.charAt(0).toUpperCase() + e.slice(1)).join("");
695
+ let oriModelsChangeName = modelsChangeName.slice(0);
696
+ modelsChangeName = oriModelsChangeName.charAt(0).toUpperCase() + modelsChangeName.slice(1);
697
+ // New model name with upper case and remove underscore for first letter of each word
698
+ let newModel = modelsChangeName.split(/[-_]/).map(e => e.charAt(0).toUpperCase() + e.slice(1)).join("");
645
699
  let subFolder = arg.join('/');
646
700
  // Declare models
647
- let fullModels = modelPath + subFolder.concat('/') + models.concat('.js');
701
+ let fullModels = modelPath + subFolder.concat('/') + modelsChangeName.concat('.js');
648
702
 
649
703
  /**
650
704
  * All properties
@@ -696,7 +750,7 @@ class Generator {
696
750
  throw logUpdate("\n Fatal ", String(err), "\n");
697
751
  } else {
698
752
  // Raw model schema
699
- this.rawSchemaTable(dbSelected, newModel, tableName, tableSchema, this.extra || undefined, (SchemaErr, rawSchema) => {
753
+ this.rawSchemaTable(dbSelected, newModel, tableName, tableSchema, this.noComment || undefined, (SchemaErr, rawSchema) => {
700
754
  if(err) {
701
755
  throw logUpdate("\n Fatal  RAW Schema ERR:", String(SchemaErr), "\n");
702
756
  } else {
@@ -730,7 +784,7 @@ class Generator {
730
784
  resolve("\n Fatal  The pool_base in `global.config.js` file does not match the specific.");
731
785
  }
732
786
  } else {
733
- resolve("\n Warning  The model `" + models + "` it's duplicated.");
787
+ resolve("\n Warning  The model `" + modelsChangeName + "` it's duplicated.");
734
788
  }
735
789
  } catch (error) {
736
790
  reject(error);
@@ -738,7 +792,7 @@ class Generator {
738
792
  });
739
793
  }
740
794
 
741
- rawSchemaTable(dbNameSelected, newModelName, tableName, modelSchema, extra, cb) {
795
+ rawSchemaTable(dbNameSelected, newModelName, tableName, modelSchema, noComment, cb) {
742
796
  try {
743
797
  // Function map type
744
798
  const mapToSequelizeType = (rawType) => {
@@ -781,8 +835,8 @@ class Generator {
781
835
  // Check is primary key
782
836
  if (props.primaryKey) lines.push(` primaryKey: true,`);
783
837
  if (props.autoIncrement) lines.push(` autoIncrement: true,`);
784
- // Check <extra> for comment, because some database have extra comment in field but it's not comment for field, so we need to check it first before assign to comment
785
- if(extra != '--no-comment') {
838
+ // Check <noComment> for comment, because some database have noComment comment in field but it's not comment for field, so we need to check it first before assign to comment
839
+ if(!noComment) {
786
840
  if (props.comment) lines.push(` comment: "${props.comment}",`);
787
841
  }
788
842
  // Handle defaultValue
@@ -1060,6 +1114,53 @@ class Generator {
1060
1114
  });
1061
1115
  }
1062
1116
 
1117
+ findClosestOption(input, options) {
1118
+ let closest = null;
1119
+ let minDistance = Infinity;
1120
+ options.forEach(opt => {
1121
+ const distance = this.levenshteinDistance(input, opt);
1122
+ if (distance < minDistance) {
1123
+ minDistance = distance;
1124
+ closest = opt;
1125
+ }
1126
+ });
1127
+ return minDistance <= 3 ? closest : null;
1128
+ }
1129
+
1130
+ levenshteinDistance(a, b) {
1131
+ const tmp = [];
1132
+ for (let i = 0; i <= a.length; i++) tmp[i] = [i];
1133
+ for (let j = 0; j <= b.length; j++) tmp[0][j] = j;
1134
+ for (let i = 1; i <= a.length; i++) {
1135
+ for (let j = 1; j <= b.length; j++) {
1136
+ tmp[i][j] = Math.min(
1137
+ tmp[i - 1][j] + 1,
1138
+ tmp[i][j - 1] + 1,
1139
+ tmp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
1140
+ );
1141
+ }
1142
+ }
1143
+ return tmp[a.length][b.length];
1144
+ }
1145
+
1146
+ validateModelName(name, nextArg, cb) {
1147
+ // role:
1148
+ // 1. Only first text with (a-z, A-Z) or _ not number first charecter
1149
+ // 2. Then only text with (a-z, A-Z), number (0-9), - or _ allowed for next characters
1150
+ // 3. No space allowed
1151
+ if (name.includes(' ')) {
1152
+ return cb("Error: Model name cannot contain spaces.", false);
1153
+ }
1154
+ const regex = /^[a-zA-Z_][a-zA-Z0-9-_]*$/;
1155
+ if (!regex.test(name)) {
1156
+ return cb("Error: Invalid characters in model name.", false);
1157
+ } else if (nextArg && !nextArg.startsWith('--')) {
1158
+ cb("Error model name not space allowed.", null);
1159
+ } else {
1160
+ cb(null, true);
1161
+ }
1162
+ }
1163
+
1063
1164
  embed(argv) {
1064
1165
  return new Promise((resolve, reject) => {
1065
1166
  try {
@@ -1069,8 +1170,55 @@ class Generator {
1069
1170
  this.option = argv[ 2 ];
1070
1171
  this.argument = argv[ 3 ];
1071
1172
  this.special = argv[ 4 ];
1072
- this.extra = argv[ 5 ];
1073
- this.more = argv[ 6 ];
1173
+ // Validate argument with Whitelist `--`
1174
+ const validOptions = ['--model', '--name', '--no-comment', '--inject', '--require', '--version', '--help', '--helper'];
1175
+ const providedOptions = argv.filter(arg => arg.startsWith('--'));
1176
+ for (const opt of providedOptions) {
1177
+ if (!validOptions.includes(opt)) {
1178
+ const suggestion = this.findClosestOption(opt, validOptions);
1179
+ let errorMessage = `\n Error  Unknown option: "${opt}".`;
1180
+ if (suggestion) {
1181
+ errorMessage += ` \nDid you mean to use "${suggestion}" ?`;
1182
+ }
1183
+ return reject(errorMessage);
1184
+ }
1185
+ }
1186
+ // Check for noComment variable with `--no-comment`, `--name`, `--inject` flag
1187
+ this.noComment = argv.includes('--no-comment');
1188
+ this.customModelName = null;
1189
+ this.realTableName = null;
1190
+ // Check for custom model name with `--name` flag
1191
+ const nameIndex = argv.indexOf('--name');
1192
+ if (nameIndex !== -1 && argv[nameIndex + 1]) {
1193
+ const inputName = argv[nameIndex + 1];
1194
+ this.validateModelName(inputName, argv[nameIndex + 2], (err, isValid) => {
1195
+ if (isValid && !err) {
1196
+ this.customModelName = argv[nameIndex + 1];
1197
+ } else {
1198
+ reject("\n Warning  Invalid model name format: '" + inputName + " ...' Model name must start with a letter/underscore and contain only letters, numbers, '-', or '_' & no space.");
1199
+ }
1200
+ });
1201
+ } else if (nameIndex > 2) {
1202
+ reject("\n Warning  Custom model name flag `--name` found but no name provided. Usage: `--name <custom_model_name>`");
1203
+ } else {
1204
+ if(nameIndex !== -1) {
1205
+ reject("\nNote: You can specify a custom model name with the `--name` flag. Example: `--name fruit` to generate `Fruit.js` instead of default model name.");
1206
+ }
1207
+ }
1208
+ // Check for inject table to update schema to latest with `--inject` flag
1209
+ const injectIndex = argv.indexOf('--inject');
1210
+ if (injectIndex !== -1) {
1211
+ if (!argv[injectIndex + 1] || argv[injectIndex + 1].startsWith('--')) {
1212
+ return reject(`\n Warning  Option "--inject" requires a value. Usage: "--inject <table_name>"`);
1213
+ }
1214
+ if (injectIndex !== -1 && argv[injectIndex + 1]) {
1215
+ this.realTableName = argv[injectIndex + 1];
1216
+ } else {
1217
+ if(injectIndex !== -1) {
1218
+ reject("\n Warning  Inject table name flag `--inject` found but no table name provided. Usage: `--inject <table_name>`");
1219
+ }
1220
+ }
1221
+ }
1074
1222
  resolve(this);
1075
1223
  } catch (error) {
1076
1224
  reject(err);