beech-api 3.9.0-beta.9-rc → 3.9.57

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
@@ -146,7 +146,15 @@ The following commands are available:
146
146
  $ beech make <endpoint> Create a new Endpoints and unit test file,
147
147
  You might using [special] `-R, --require`
148
148
  for choose Model(s) used to endpoint file.
149
- $ beech make <model> -M, --model Create a new Models file.
149
+
150
+ $ beech make <model> -M, --model Create a new Models file, You might using
151
+ [special] `--no-comment` for ignore comment
152
+ Table Property in your Schema.
153
+
154
+ $ beech update model <model_name> Update new Table Structure for latest, You
155
+ might using [special] `--no-comment` for
156
+ ignore comment Table Property in your Schema.
157
+
150
158
  $ beech make <helper> --helper Create a new Helpers file.
151
159
  $ beech passport init Initialize authentication with passport-jwt.
152
160
  $ beech skd init Initialize Job Scheduler file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beech-api",
3
- "version": "3.9.0-beta.9-rc",
3
+ "version": "3.9.57",
4
4
  "description": "Command line interface for rapid Beech API development",
5
5
  "keywords": [
6
6
  "api",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "app-root-path": "^3.0.0",
35
+ "ast-types": "^0.14.2",
35
36
  "axios": "^1.8.4",
36
37
  "child-process-promise": "^2.2.1",
37
38
  "cli-clear": "^1.0.4",
@@ -56,6 +57,7 @@
56
57
  "method-override": "^3.0.0",
57
58
  "mkdirp": "^2.1.6",
58
59
  "module-alias": "^2.2.2",
60
+ "multer": "^2.1.1",
59
61
  "mysql": "^2.18.1",
60
62
  "mysql2": "^3.14.0",
61
63
  "node-cmd": "^3.0.0",
@@ -68,6 +70,7 @@
68
70
  "passport-local": "^1.0.0",
69
71
  "passport-oauth": "^1.0.0",
70
72
  "path": "^0.12.7",
73
+ "recast": "^0.23.11",
71
74
  "sequelize": "^6.21.3",
72
75
  "tedious": "^18.6.1",
73
76
  "walk": "^2.3.14"
@@ -11,7 +11,15 @@ The following commands are available:
11
11
  $ beech make <endpoint> Create a new Endpoints and unit test file,
12
12
  You might using [special] `-R, --require`
13
13
  for choose Model(s) used to endpoint file.
14
- $ beech make <model> -M, --model Create a new Models file.
14
+
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.
22
+
15
23
  $ beech make <helper> --helper Create a new Helpers file.
16
24
  $ beech passport init Initialize authentication with passport-jwt.
17
25
  $ beech skd init Initialize Job Scheduler file.
@@ -2,6 +2,8 @@
2
2
  const logUpdate = require("log-update");
3
3
  const inquirer = require('inquirer');
4
4
  const walk = require("walk");
5
+ const recast = require("recast");
6
+ const { builders } = require("ast-types");
5
7
  const { connectForGenerateModel } = require("../databases/test");
6
8
 
7
9
  class Generator {
@@ -180,16 +182,22 @@ class Generator {
180
182
  }
181
183
  }
182
184
  } else if (this.option == 'update') {
183
- console.log('------------------------------');
184
185
  if(this.argument == 'model') {
185
- // this.special = model name
186
- // if (!this.special) {
187
- // resolve("\n [103m [90m Warning [0m [0m Please specify model name to update.");
188
- // } else {
189
- // this.updateModel()
190
- // .then(res => resolve(res))
191
- // .catch(err => reject(err));
192
- // }
186
+ if(this.special) {
187
+ if (!this.special) {
188
+ resolve("\n Warning  Please specify model name to update.");
189
+ } else {
190
+ this.updateModel()
191
+ .then(res => resolve(res))
192
+ .catch(err => reject(err));
193
+ }
194
+ } else {
195
+ resolve("\n Warning  Please specify model name to update.");
196
+ }
197
+ } else if(this.argument != 'model') {
198
+ resolve("\n Faltal  commnad it's not available.");
199
+ } else {
200
+ resolve("\n Warning  Please specify what you want to update.");
193
201
  }
194
202
  } else if (this.option == 'passport') {
195
203
  if (this.argument == "init") {
@@ -466,60 +474,136 @@ class Generator {
466
474
  updateModel() {
467
475
  return new Promise((resolve, reject) => {
468
476
  try {
469
- // 1. อ่านการตั้งค่าจาก global.config และ app.config เหมือนตอนสร้าง Model
470
477
  this.fs.readFile("./global.config.js", 'utf8', (err, globalData) => {
471
- if (err) return resolve("\n [101m Faltal [0m Can't read `global.config.js` file.");
472
-
478
+ if (err) return resolve("\n Faltal  Can't read `global.config.js` file.");
473
479
  this.fs.readFile("./app.config.js", 'utf8', (appErr, appData) => {
474
- if (appErr) return resolve("\n [101m Faltal [0m Can't read `app.config.js` file.");
475
-
476
- let appBuf2eval = eval(appData);
477
- let modelName = this.special; // <table_name>
478
- let modelPath = `./src/models/${modelName.charAt(0).toUpperCase() + modelName.slice(1)}.js`;
479
-
480
- // ตรวจสอบว่ามีไฟล์ Model เดิมอยู่หรือไม่
480
+ if (appErr) return resolve("\n Faltal  Can't read `app.config.js` file.");
481
+ const appConfig = eval(appData);
482
+ const modelName = this.special; // <table_name>
483
+ const modelFileName = modelName.charAt(0).toUpperCase() + modelName.slice(1);
484
+ const modelPath = `./src/models/${modelFileName}.js`;
481
485
  if (!this.fs.existsSync(modelPath)) {
482
- return resolve(`\n [103m [90m Warning [0m [0m Model file \`${modelName}\` not found to update.`);
486
+ return resolve(`\n Warning  Model file \`${modelFileName}\` not found.`);
483
487
  }
484
-
485
- // ให้ผู้ใช้เลือก Connection ที่ต้องการดึง Schema ใหม่
488
+ // Choose connection for new schema
486
489
  inquirer.prompt([{
487
490
  type: "list",
488
491
  name: "selectDbConnect",
489
492
  message: " [93mSelect database connection to sync schema: [0m",
490
- choices: appBuf2eval.database_config.map(e => e.name),
493
+ choices: appConfig.database_config.map(e => e.name),
491
494
  }]).then(dbSelected => {
492
-
493
- // 2. ดึง Schema ใหม่จาก DB
494
495
  const { connectForGenerateModel } = require("../databases/test");
495
- let newModelClassName = modelName.split("_").map(e => e.charAt(0).toUpperCase() + e.slice(1)).join("");
496
-
497
- connectForGenerateModel(dbSelected.selectDbConnect, modelName, appBuf2eval.database_config, (err, tableSchema, tableName) => {
498
- if (err) return reject(err);
499
-
500
- // 3. แปลง Schema เป็น Code String
501
- this.rawSchemaTable(dbSelected.selectDbConnect, newModelClassName, tableName, tableSchema, (SchemaErr, rawSchema) => {
502
- if (SchemaErr) return reject(SchemaErr);
503
-
504
- // 4. อ่านไฟล์เดิมและทำการ Replace เฉพาะส่วน Schema
505
- this.fs.readFile(modelPath, 'utf8', (readErr, currentContent) => {
506
- if (readErr) return reject(readErr);
507
-
508
- // ใช้ Regex เพื่อหาช่วงของ Schema(...).define(...) และเปลี่ยนเฉพาะข้างใน
509
- // โดยจะหาตั้งแต่ Schema(sql.db).define("table", { จนถึง });
510
- const regex = /const\s+\w+\s+=\s+Schema\(sql\.\w+\)\.define\([\s\S]*?\}\);/;
511
-
512
- if (regex.test(currentContent)) {
513
- let updatedContent = currentContent.replace(regex, rawSchema);
514
-
515
- this.fs.writeFile(modelPath, updatedContent, 'utf8', (writeErr) => {
516
- if (writeErr) return reject(writeErr);
517
- resolve(`\n [102m [90m Passed [0m [0m The model \`${modelName}\` schema has been updated.`);
518
- });
519
- } else {
520
- resolve("\n [101m Faltal [0m Could not find Schema definition pattern in the file.");
496
+ // pull new schema from database
497
+ connectForGenerateModel(dbSelected.selectDbConnect, modelName, appConfig.database_config, (dbErr, tableSchema, tableName) => {
498
+ if (dbErr) return reject(dbErr);
499
+ // read current model file for AST
500
+ this.fs.readFile(modelPath, 'utf8', (readErr, code) => {
501
+ if (readErr) return reject(readErr);
502
+ const ast = recast.parse(code);
503
+ let isUpdated = false;
504
+ // Function for map raw database type to Sequelize DataTypes AST Node
505
+ const getDataTypeNode = (rawType) => {
506
+ const type = rawType.toUpperCase();
507
+ if (type.includes('INT')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('INTEGER'));
508
+ if (type.includes('BIGINT')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('BIGINT'));
509
+ if (type.includes('FLOAT')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('FLOAT'));
510
+ if (type.includes('DOUBLE')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('DOUBLE'));
511
+ if (type.includes('DECIMAL')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('DECIMAL'));
512
+ if (type.includes('BOOLEAN') || type === 'TINYINT(1)') return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('BOOLEAN'));
513
+ if (type.includes('CHAR') || type.includes('VARCHAR')) {
514
+ const match = type.match(/\((\d+)\)/);
515
+ const length = match ? match[1] : '255';
516
+ return builders.callExpression(
517
+ builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('STRING')),
518
+ [builders.literal(parseInt(length))]
519
+ );
520
+ }
521
+ if (type.includes('TEXT')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('TEXT'));
522
+ if (type.includes('DATE')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('DATE'));
523
+ if (type.includes('TIME')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('TIME'));
524
+ if (type.includes('JSON')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('JSON'));
525
+ if (type.includes('UUID')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('UUID'));
526
+ if (type.includes('BLOB')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('BLOB'));
527
+ if (type.includes('ENUM')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('ENUM'));
528
+ if (type.includes('GEOMETRY')) return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('GEOMETRY'));
529
+ return builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('STRING'));
530
+ };
531
+ // recast.visit for find position of Schema(...).define() and update fields
532
+ recast.visit(ast, {
533
+ visitCallExpression(path) {
534
+ const node = path.node;
535
+ if (node.callee.property && node.callee.property.name === 'define' && node.arguments[1] && node.arguments[1].type === 'ObjectExpression') {
536
+ const fieldsObject = node.arguments[1];
537
+ const newFieldNames = Object.keys(tableSchema);
538
+ const updatedProperties = [];
539
+ newFieldNames.forEach(dbFieldName => {
540
+ const dbFieldData = tableSchema[dbFieldName];
541
+ let existingProperty = fieldsObject.properties.find(p => {
542
+ const propName = p.key.name || p.key.value;
543
+ if (propName === dbFieldName) return true;
544
+ if (p.value && p.value.properties) {
545
+ const hasFieldMapping = p.value.properties.find(
546
+ innerP => (innerP.key.name === 'field' || innerP.key.value === 'field') &&
547
+ (innerP.value.value === dbFieldName)
548
+ );
549
+ if (hasFieldMapping) return true;
550
+ }
551
+ return false;
552
+ });
553
+ const latestFieldProps = [];
554
+ // Prepare properties from Table DB (Type, PK, AI, Default, etc.)
555
+ latestFieldProps.push(builders.property('init', builders.identifier('type'), getDataTypeNode(dbFieldData.type)));
556
+ latestFieldProps.push(builders.property('init', builders.identifier('allowNull'), builders.literal(dbFieldData.allowNull === 'YES' || dbFieldData.allowNull === true)));
557
+ if (dbFieldData.primaryKey) latestFieldProps.push(builders.property('init', builders.identifier('primaryKey'), builders.literal(true)));
558
+ if (dbFieldData.autoIncrement) latestFieldProps.push(builders.property('init', builders.identifier('autoIncrement'), builders.literal(true)));
559
+ if (dbFieldData.unique) latestFieldProps.push(builders.property('init', builders.identifier('unique'), builders.literal(true)));
560
+ if(this.extra != '--no-comment') {
561
+ if (dbFieldData.comment) latestFieldProps.push(builders.property('init', builders.identifier('comment'), builders.literal(dbFieldData.comment)));
562
+ }
563
+ if (dbFieldData.defaultValue !== null && dbFieldData.defaultValue !== undefined) {
564
+ const dVal = String(dbFieldData.defaultValue).toUpperCase();
565
+ const defaultNode = (dVal === 'CURRENT_TIMESTAMP' || dVal === 'NOW()')
566
+ ? builders.memberExpression(builders.identifier('DataTypes'), builders.identifier('NOW'))
567
+ : builders.literal(dbFieldData.defaultValue);
568
+ latestFieldProps.push(builders.property('init', builders.identifier('defaultValue'), defaultNode));
569
+ }
570
+ if (!existingProperty) {
571
+ existingProperty = builders.property('init', builders.identifier(dbFieldName), builders.objectExpression(latestFieldProps));
572
+ isUpdated = true;
573
+ } else {
574
+ const currentProps = existingProperty.value.properties;
575
+ latestFieldProps.forEach(newProp => {
576
+ const oldProp = currentProps.find(p => p.key.name === newProp.key.name);
577
+ if (!oldProp || recast.print(oldProp.value).code !== recast.print(newProp.value).code) {
578
+ isUpdated = true;
579
+ if (!oldProp) currentProps.push(newProp); else oldProp.value = newProp.value;
580
+ }
581
+ });
582
+ const latestPropNames = latestFieldProps.map(p => p.key.name);
583
+ latestPropNames.push('field');
584
+ existingProperty.value.properties = currentProps.filter(p => latestPropNames.includes(p.key.name));
585
+ }
586
+ updatedProperties.push(existingProperty);
587
+ });
588
+ const currentOrder = fieldsObject.properties.map(p => p.key.name || p.key.value).join(',');
589
+ const newOrder = updatedProperties.map(p => p.key.name || p.key.value).join(',');
590
+ if (currentOrder !== newOrder || fieldsObject.properties.length !== updatedProperties.length) isUpdated = true;
591
+ fieldsObject.properties = updatedProperties;
592
+ }
593
+ return false;
521
594
  }
522
595
  });
596
+ // Write back updated AST, Shout updated
597
+ if (isUpdated) {
598
+ // Write updated AST back to file
599
+ const output = recast.print(ast).code;
600
+ this.fs.writeFile(modelPath, output, 'utf8', (writeErr) => {
601
+ if (writeErr) return reject(writeErr);
602
+ resolve(`\n Updated  The model \`${modelFileName}\` has been updated via AST.`);
603
+ });
604
+ } else {
605
+ resolve(`\n Warning  No new fields found to update for \`${modelFileName}\`.`);
606
+ }
523
607
  });
524
608
  });
525
609
  });
@@ -691,21 +775,19 @@ class Generator {
691
775
  // Check is primary key
692
776
  if (props.primaryKey) lines.push(` primaryKey: true,`);
693
777
  if (props.autoIncrement) lines.push(` autoIncrement: true,`);
694
- //if (props.comment) lines.push(` comment: '${props.comment}',`);
695
-
778
+ // 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
779
+ if(this.extra != '--no-comment') {
780
+ if (props.comment) lines.push(` comment: "${props.comment}",`);
781
+ }
696
782
  // Handle defaultValue
697
783
  if (props.defaultValue !== null && props.defaultValue !== undefined) {
698
784
  const defaultVal = String(props.defaultValue).toUpperCase();
699
- if (
700
- defaultVal === 'CURRENT_TIMESTAMP' ||
701
- defaultVal === 'NOW()' ||
702
- defaultVal.includes('CURRENT_TIMESTAMP')
703
- ) {
785
+ if (defaultVal === 'CURRENT_TIMESTAMP' || defaultVal === 'NOW()' || defaultVal.includes('CURRENT_TIMESTAMP')) {
704
786
  lines.push(` defaultValue: DataTypes.NOW,`);
705
787
  } else if (defaultVal === 'UUID()' || defaultVal === 'uuid()' || defaultVal === 'UUID') {
706
788
  lines.push(` defaultValue: DataTypes.UUIDV4,`);
707
789
  } else if (typeof props.defaultValue === 'string') {
708
- lines.push(` defaultValue: '${props.defaultValue}',`);
790
+ lines.push(` defaultValue: "${props.defaultValue}",`);
709
791
  } else {
710
792
  lines.push(` defaultValue: ${props.defaultValue},`);
711
793
  }
@@ -43,7 +43,7 @@ const uploadStrategy = multer({
43
43
  }
44
44
  }
45
45
  }).any();
46
- const jsonLimitSize = _beech_?.payload.json?.limit || "100KB"; // json payload
46
+ const jsonLimitSize = _beech_?.payload?.json?.limit || "100KB"; // json payload
47
47
  const urlencodedLimitSize = _beech_?.payload?.urlencoded?.limit || "100KB"; // urlencoded payload (multipart/form-data)
48
48
  const urlencodedExtended = !!(_beech_?.payload?.urlencoded?.extended ?? true);
49
49
  _app_.use(_express_.urlencoded({ limit: urlencodedLimitSize, extended: urlencodedExtended })); // application/x-www-form-urlencoded payload