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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
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
|
|
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
|
@@ -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("[101m Fetching table schema [0m 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
|
-
|
|
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 <
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
-
|
|
4
|
+
return text ? text.toLowerCase() : '';
|
|
5
5
|
},
|
|
6
6
|
|
|
7
7
|
textUpperCase(text) {
|
|
8
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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(
|
|
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: "[
|
|
78
|
+
message: "[93mNo model found from your selection! Do you want to create the endpoint without requiring any model ?:[0m",
|
|
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[103m[90m Warning [0m[0m Please specify model name to update.");
|
|
189
188
|
} else {
|
|
190
|
-
let
|
|
191
|
-
this.updateModel(
|
|
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(
|
|
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
|
|
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
|
|
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[103m[90m Warning [0m[0m 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)
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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: "[93mDo you want to update the model with the latest database schema ?:[0m",
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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[101m Error [0m Table name mismatch!
|
|
539
|
+
\n - In File defined as: "${definedTableName}"
|
|
540
|
+
\n - Your command requested: "${targetTable}"
|
|
541
|
+
\n[36mNote:[0m 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[102m[90m Updated [0m[0m The model \`${modelFileName}\` has been updated via AST.`);
|
|
587
651
|
});
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
existingProperty.value.properties = currentProps.filter(p => latestPropNames.includes(p.key.name));
|
|
652
|
+
} else {
|
|
653
|
+
resolve(`\n[103m[90m Warning [0m[0m No new fields found to update for \`${modelFileName}\`.`);
|
|
591
654
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
fieldsObject.properties = updatedProperties;
|
|
598
|
-
}
|
|
599
|
-
return false;
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
} else {
|
|
658
|
+
// Nothing.
|
|
659
|
+
resolve(": Say no.");
|
|
600
660
|
}
|
|
601
661
|
});
|
|
602
|
-
|
|
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[102m[90m Updated [0m[0m The model \`${modelFileName}\` has been updated via AST.`);
|
|
609
|
-
});
|
|
610
|
-
} else {
|
|
611
|
-
resolve(`\n[103m[90m Warning [0m[0m 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
|
|
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('/') +
|
|
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[101m Fatal [0m", String(err), "\n");
|
|
697
751
|
} else {
|
|
698
752
|
// Raw model schema
|
|
699
|
-
this.rawSchemaTable(dbSelected, newModel, tableName, tableSchema, this.
|
|
753
|
+
this.rawSchemaTable(dbSelected, newModel, tableName, tableSchema, this.noComment || undefined, (SchemaErr, rawSchema) => {
|
|
700
754
|
if(err) {
|
|
701
755
|
throw logUpdate("\n[101m Fatal [0m RAW Schema ERR:", String(SchemaErr), "\n");
|
|
702
756
|
} else {
|
|
@@ -730,7 +784,7 @@ class Generator {
|
|
|
730
784
|
resolve("\n[101m Fatal [0m The pool_base in `global.config.js` file does not match the specific.");
|
|
731
785
|
}
|
|
732
786
|
} else {
|
|
733
|
-
resolve("\n[103m[90m Warning [0m[0m The model `" +
|
|
787
|
+
resolve("\n[103m[90m Warning [0m[0m 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,
|
|
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 <
|
|
785
|
-
if(
|
|
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
|
-
|
|
1073
|
-
|
|
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[101m Error [0m 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[103m[90m Warning [0m[0m 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[103m[90m Warning [0m[0m Custom model name flag `--name` found but no name provided. Usage: `--name <custom_model_name>`");
|
|
1203
|
+
} else {
|
|
1204
|
+
if(nameIndex !== -1) {
|
|
1205
|
+
reject("\n[36mNote:[0m 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[103m Warning [0m 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[103m[90m Warning [0m[0m 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);
|