@axiosleo/orm-mysql 0.8.6 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -195,6 +195,31 @@ try {
195
195
  }
196
196
  ```
197
197
 
198
+ ### Migration
199
+
200
+ ```javascript
201
+ 'use strict';
202
+
203
+ /**
204
+ * @param {import('@axiosleo/orm-mysql').MigrationInterface} migration
205
+ */
206
+ function up(migration) {
207
+ migration.createDatabase({ database_name: 'demo' });
208
+ }
209
+
210
+ /**
211
+ * @param {import('@axiosleo/orm-mysql').MigrationInterface} migration
212
+ */
213
+ function down(migration) {
214
+ migration.dropDatabase({ database_name: 'demo' });
215
+ }
216
+
217
+ module.exports = {
218
+ up,
219
+ down
220
+ };
221
+ ```
222
+
198
223
  ### Custom query driver
199
224
 
200
225
  ```javascript
@@ -215,7 +240,7 @@ const conn = createClient({
215
240
 
216
241
  const hanlder = new QueryHandler(conn, {
217
242
  driver: 'custom',
218
- query_handler: (con, options) => {
243
+ queryHandler: (con, options) => {
219
244
  const builder = new Builder(options);
220
245
  return new Promise((resolve, reject) => {
221
246
  if (options.operator === 'select') {
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const path = require('path');
6
+ const { App } = require('@axiosleo/cli-tool');
7
+
8
+ const app = new App({
9
+ name: 'MySQL ORM CLI',
10
+ desc: 'migrate, model, seed, etc.',
11
+ bin: 'orm-mysql',
12
+ version: '0.9.0',
13
+ commands_dir: path.join(__dirname, '../commands'),
14
+ });
15
+
16
+ app.start();
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { Command, printer } = require('@axiosleo/cli-tool');
5
+ const { _exists, _write } = require('@axiosleo/cli-tool/src/helper/fs');
6
+ const { _snake_case } = require('@axiosleo/cli-tool/src/helper/str');
7
+
8
+ class GenerateCommand extends Command {
9
+ constructor() {
10
+ super({
11
+ name: 'generate',
12
+ desc: ''
13
+ });
14
+ this.addArgument('name', 'Migration name', 'required', '');
15
+ this.addArgument('dir', 'Migration scripts directory', 'optional', process.cwd());
16
+ }
17
+
18
+ /**
19
+ * @param {*} args
20
+ * @param {*} options
21
+ * @param {string[]} argList
22
+ * @param {import('@axiosleo/cli-tool').App} app
23
+ */
24
+ async exec(args, options) {
25
+ const { name, dir } = args;
26
+ const fileName = parseInt((new Date()).valueOf() / 1000) + '.' + _snake_case(name) + '.js';
27
+ const filePath = path.join(dir, fileName);
28
+ if (await _exists(filePath)) {
29
+ printer.error('Migration script file already exists: ' + fileName);
30
+ return;
31
+ }
32
+ const template = `
33
+ 'use strict';
34
+
35
+ /**
36
+ * @param {import('@axiosleo/orm-mysql').MigrationInterface} migration
37
+ */
38
+ function up(migration) {
39
+ }
40
+
41
+ /**
42
+ * @param {import('@axiosleo/orm-mysql').MigrationInterface} migration
43
+ */
44
+ function down(migration) {
45
+ }
46
+
47
+ module.exports = {
48
+ up,
49
+ down
50
+ };
51
+ `;
52
+ await _write(filePath, template);
53
+ printer.info('Migration file created: ' + filePath);
54
+ }
55
+ }
56
+
57
+ module.exports = GenerateCommand;
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const { Command, debug, Workflow } = require('@axiosleo/cli-tool');
4
+ const is = require('@axiosleo/cli-tool/src/helper/is');
5
+ const migration = require('../src/migration');
6
+
7
+ class MigrateCommand extends Command {
8
+ constructor() {
9
+ super({
10
+ name: 'migrate',
11
+ desc: 'Migrate database',
12
+ });
13
+ this.addArgument('action', 'up or down', 'required');
14
+ this.addArgument('dir', 'migration directory', 'optional', process.cwd());
15
+
16
+ this.addOption('debug', 'd', '[false] debug mode', 'optional', false);
17
+ this.addOption('host', null, '[localhost] mysql host', 'optional', 'localhost');
18
+ this.addOption('user', null, '[root] username for connect to the database', 'optional', 'root');
19
+ this.addOption('pass', null, 'password to connect to the database', 'optional', '');
20
+ this.addOption('port', null, '[3306] port number to connect to the database', 'optional', 3306);
21
+ this.addOption('db', null, 'database name', 'optional', '');
22
+ }
23
+
24
+ /**
25
+ * @param {*} args
26
+ * @param {*} options
27
+ */
28
+ async exec(args, options) {
29
+ const workflow = new Workflow(migration);
30
+ try {
31
+ await workflow.start({
32
+ action: args.action,
33
+ config: {
34
+ dir: args.dir
35
+ },
36
+ connection: {
37
+ host: options.host,
38
+ port: is.number(options.port) ?
39
+ options.port : parseInt(options.port),
40
+ user: options.user,
41
+ password: options.pass,
42
+ database: options.db
43
+ },
44
+ debug: options.debug
45
+ });
46
+ process.exit(0);
47
+ } catch (e) {
48
+ if (e.curr && e.curr.error) {
49
+ debug.error(e.curr.error);
50
+ } else {
51
+ debug.log(e);
52
+ }
53
+ process.exit(1);
54
+ }
55
+ }
56
+ }
57
+
58
+ module.exports = MigrateCommand;
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ host: 'localhost',
5
+ port: 13306,
6
+ user: 'root',
7
+ password: '3AQqZTfmww=Ftj',
8
+ database: 'cms'
9
+ };
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @param {import('../../').MigrationInterface} migration
5
+ */
6
+ function up(migration) {
7
+ migration.createTable({
8
+ table_name: 'organization',
9
+ columns: [{
10
+ column_name: 'id',
11
+ type: 'int(11)',
12
+ not_null: true,
13
+ is_primary_key: true,
14
+ }]
15
+ });
16
+ }
17
+
18
+ /**
19
+ * @param {import('../../').MigrationInterface} migration
20
+ */
21
+ function down(migration) {
22
+ migration.dropTable({ table_name: 'table1' });
23
+ }
24
+
25
+ module.exports = {
26
+ up,
27
+ down
28
+ };
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ async function up(migration) {
4
+
5
+ }
6
+
7
+ async function down(migration) {
8
+
9
+ }
10
+
11
+ module.exports = {
12
+ up,
13
+ down
14
+ };
@@ -0,0 +1 @@
1
+ 'use strict';
package/index.d.ts CHANGED
@@ -50,7 +50,7 @@ export interface TableOption {
50
50
 
51
51
  export type QueryOperatorBaseOptions = {
52
52
  driver?: string | 'mysql';
53
- query_handler?: QueryHandler;
53
+ queryHandler?: QueryHandler;
54
54
  };
55
55
 
56
56
  export type QueryOperatorOptions = QueryOperatorBaseOptions & {
@@ -161,6 +161,17 @@ export declare class QueryHandler {
161
161
  * @param condition
162
162
  */
163
163
  upsert(tableName: string, data: any, condition: Record<string, ConditionValueType>): Promise<OkPacket>;
164
+
165
+ /**
166
+ * @param database default is options.database
167
+ */
168
+ existDatabase(database?: string): Promise<boolean>;
169
+
170
+ /**
171
+ * @param table
172
+ * @param database default is options.database
173
+ */
174
+ existTable(table: string, database?: string): Promise<boolean>;
164
175
  }
165
176
 
166
177
  export declare class TransactionOperator extends QueryOperator {
@@ -256,26 +267,117 @@ export declare class Builder {
256
267
 
257
268
  export declare class MySQLClient extends QueryHandler {
258
269
 
259
- constructor(options?: ConnectionOptions, name?: string | null | undefined);
260
-
261
- /**
262
- * @param database default is options.database
263
- */
264
- existDatabase(database?: string): Promise<boolean>;
265
-
266
- /**
267
- * @param table
268
- * @param database default is options.database
269
- */
270
- existTable(table: string, database?: string): Promise<boolean>;
270
+ constructor(options?: ConnectionOptions, name?: string | null | undefined, type?: 'default' | 'promise' | 'pool');
271
271
 
272
272
  /**
273
- *
274
273
  * @param query
275
274
  * @param operator default is 'select'
276
275
  */
277
276
  execQuery(query: Query, operator?: OperatorType): Promise<QueryResult>;
278
277
 
279
278
  close(): Promise<void>;
279
+ }
280
+
281
+ type FieldType =
282
+ 'TINYINT' | 'SMALLINT' | 'MEDIUMINT' | 'INT' | 'BIGINT' | 'FLOAT' | 'DOUBLE' | 'DECIMAL' |
283
+ 'DATE' | 'TIME' | 'YEAR' | 'DATETIME' | 'TIMESTAMP' |
284
+ 'CHAR' | 'VARCHAR' | 'TINYBLOB' | 'TINYTEXT' | 'BLOB' | 'TEXT' | 'MEDIUMBLOB' | 'MEDIUMTEXT' |
285
+ 'LONGBLOB' | 'LONGTEXT' | 'ENUM' | 'SET' | 'JSON';
286
+
287
+ interface ColumnItem {
288
+ type: FieldType,
289
+ length?: number,
290
+ unsigned?: boolean,
291
+ allowNull?: boolean,
292
+ default?: string | number | boolean | null | 'timestamp',
293
+ onUpdate?: boolean,
294
+ comment?: string,
295
+ autoIncrement?: boolean,
296
+ primaryKey?: boolean,
297
+ uniqIndex?: boolean
298
+ }
299
+
300
+ interface CreateColumnOptions {
301
+ length?: number,
302
+ unsigned?: boolean,
303
+ allowNull?: boolean,
304
+ default?: string | number | boolean | null | 'timestamp',
305
+ comment?: string,
306
+ autoIncrement?: boolean,
307
+ primaryKey?: boolean,
308
+ uniqIndex?: boolean
309
+ }
310
+
311
+ interface CreateIndexOptions {
312
+ unique?: boolean,
313
+ fulltext?: boolean,
314
+ spatial?: boolean
315
+ }
280
316
 
317
+ export type ManageBuilderOptions = {
318
+ operator: 'create' | 'drop' | 'alter';
319
+ columns: Record<string, ColumnItem>;
320
+ target: 'table' | 'column' | 'index' | 'foreign_key';
281
321
  }
322
+
323
+ export declare class MigrationInterface {
324
+
325
+ /**
326
+ * @param tableName
327
+ * @param columns
328
+ * @param options default engine is InnoDB; default charset is utf8mb4
329
+ */
330
+ createTable(tableName: string, columns: Record<string, ColumnItem>, options?: {
331
+ engine?: 'InnoDB' | 'MyISAM' | 'MEMORY',
332
+ charset?: string
333
+ }): void;
334
+
335
+ /**
336
+ * @param columnName
337
+ * @param columnType
338
+ * @param tableName
339
+ * @param options allowNull default is true
340
+ */
341
+ createColumn(columnName: string, columnType: FieldType, tableName: string, options?: {
342
+ length?: number,
343
+ unsigned?: boolean,
344
+ allowNull?: boolean,
345
+ default?: string | number | boolean | null | 'timestamp',
346
+ onUpdate?: string,
347
+ comment?: string,
348
+ autoIncrement?: boolean,
349
+ primaryKey?: boolean,
350
+ uniqIndex?: boolean,
351
+ after?: string
352
+ }): void;
353
+
354
+ createIndex(tableName: string, columns: string[], options?: {
355
+ indexName?: string,
356
+ unique?: boolean,
357
+ fulltext?: boolean,
358
+ spatial?: boolean
359
+ }): void;
360
+
361
+ createForeignKey(options: {
362
+ foreignKey?: string,
363
+ tableName: string,
364
+ columnName: string,
365
+ reference: {
366
+ tableName: string,
367
+ columnName: string,
368
+ onDelete?: 'RESTRICT' | 'CASCADE' | 'SET NULL' | 'NO ACTION' | 'restrict' | 'cascade' | 'set null' | 'no action',
369
+ onUpdate?: 'RESTRICT' | 'CASCADE' | 'SET NULL' | 'NO ACTION' | 'restrict' | 'cascade' | 'set null' | 'no action',
370
+ }
371
+ }): void;
372
+
373
+ dropTable(tableName: string): void;
374
+
375
+ dropColumn(columnName: string, tableName: string): void;
376
+
377
+ dropIndex(indexName: string): void;
378
+
379
+ dropForeignKey(foreign_key: string, tableName: string): void;
380
+ }
381
+
382
+ export declare function up(migration: MigrationInterface): Promise<void>;
383
+ export declare function down(migration: MigrationInterface): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiosleo/orm-mysql",
3
- "version": "0.8.6",
3
+ "version": "0.9.0",
4
4
  "description": "MySQL ORM tool",
5
5
  "keywords": [
6
6
  "mysql",
@@ -10,16 +10,20 @@
10
10
  "directories": {
11
11
  "lib": "src"
12
12
  },
13
+ "bin": {
14
+ "orm-mysql": "./bin/orm-mysql.js"
15
+ },
13
16
  "scripts": {
14
17
  "lint": "tsc ./index.d.ts && eslint --fix src/",
15
18
  "test": "mocha --reporter spec --timeout 3000 tests/*.tests.js",
16
19
  "test-cov": "nyc -r=lcov -r=html -r=text -r=json mocha -t 10000 -R spec tests/*.tests.js",
20
+ "test-one": "mocha --reporter spec --timeout 3000 ",
17
21
  "ci": "npm run lint && npm run test-cov",
18
22
  "clear": "rm -rf ./nyc_output ./coverage"
19
23
  },
20
24
  "license": "MIT",
21
25
  "dependencies": {
22
- "@axiosleo/cli-tool": "^1.4.11",
26
+ "@axiosleo/cli-tool": "^1.6.0",
23
27
  "mysql2": "^2.3.3",
24
28
  "validatorjs": "^3.22.1"
25
29
  },
package/src/builder.js CHANGED
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const { debug } = require('@axiosleo/cli-tool');
3
4
  const Query = require('./query');
4
5
  const is = require('@axiosleo/cli-tool/src/helper/is');
6
+ const { _caml_case, _render } = require('@axiosleo/cli-tool/src/helper/str');
7
+ const { _validate } = require('./utils');
8
+ const { _assign } = require('@axiosleo/cli-tool/src/helper/obj');
5
9
 
6
10
  /**
7
11
  * @param {array} arr
@@ -14,6 +18,9 @@ const emit = (arr, res) => {
14
18
  };
15
19
 
16
20
  class Builder {
21
+ /**
22
+ * @param {import('../index').QueryOperatorOptions} options
23
+ */
17
24
  constructor(options) {
18
25
  let sql = '';
19
26
  this.values = [];
@@ -80,6 +87,9 @@ class Builder {
80
87
  sql = tmp.join(' ');
81
88
  break;
82
89
  }
90
+ case 'manage': {
91
+ break;
92
+ }
83
93
  default:
84
94
  throw new Error('Invalid operator: ' + options.operator);
85
95
  }
@@ -285,6 +295,235 @@ class Builder {
285
295
  }
286
296
  }
287
297
 
298
+ class ManageSQLBuilder extends Builder {
299
+ /**
300
+ * @param {import('../index').ManageBuilderOptions} options
301
+ */
302
+ constructor(options) {
303
+ super({ operator: 'manage' });
304
+ // const emitter = new Emitter();
305
+ const action = `${options.operator}_${options.target}`;
306
+ const method = _caml_case(action, false);
307
+ if (!this[method]) {
308
+ throw new Error(`'${options.target}' Unsupported '${options.operator}' operation.`);
309
+ }
310
+ try {
311
+ this.sql = this[method].call(this, options);
312
+ } catch (err) {
313
+ debug.dump(`${options.operator} ${options.target} error: ${err.message}`);
314
+ throw err;
315
+ }
316
+ }
317
+
318
+ /**
319
+ * @param {import('../index').ManageBuilderOptions} options
320
+ */
321
+ createTable(options) {
322
+ _validate(options, {
323
+ name: 'required|string',
324
+ engine: [{ in: ['InnoDB', 'MyISAM', 'MEMORY'] }],
325
+ charset: 'string'
326
+ });
327
+ if (is.empty(options.columns)) {
328
+ throw new Error('At least one column is required');
329
+ }
330
+ if (!Object.values(options.columns).find(c => c.primaryKey === true)) {
331
+ throw new Error('At least one primary key column is required');
332
+ }
333
+ let columns = Object.keys(options.columns).map(name => {
334
+ return { name, ...options.columns[name] };
335
+ });
336
+ options = _assign({
337
+ engine: 'InnoDB',
338
+ charset: 'utf8mb4'
339
+ }, options, {
340
+ columns: this.createColumns(columns)
341
+ });
342
+ return _render('CREATE TABLE `${name}` ( ${columns} ) ENGINE=${engine} DEFAULT CHARSET=${charset}', options);
343
+ }
344
+
345
+ createColumn(options) {
346
+ _validate(options, {
347
+ table: 'required|string',
348
+ name: 'required|string',
349
+ type: 'required|string',
350
+ length: 'number',
351
+ unsigned: 'boolean',
352
+ allowNull: 'boolean',
353
+ default: 'string',
354
+ comment: 'string',
355
+ autoIncrement: 'boolean',
356
+ primaryKey: 'boolean',
357
+ uniqIndex: 'boolean',
358
+ after: 'string'
359
+ });
360
+ return `ALTER TABLE \`${options.table}\` ADD COLUMN ` + this.renderSingleColumn(options);
361
+ }
362
+
363
+ createIndex(options) {
364
+ _validate(options, {
365
+ name: 'required|string',
366
+ table: 'required|string',
367
+ columns: 'required|array',
368
+ unique: 'boolean',
369
+ fulltext: 'boolean',
370
+ spatial: 'boolean',
371
+ order: [{ in: ['asc', 'desc'] }],
372
+ visible: 'boolean'
373
+ });
374
+
375
+ return _render('CREATE INDEX `${index_name}` ON `${table_name}` (${column_names}) ${visible}', {
376
+ index_name: options.name,
377
+ table_name: options.table,
378
+ visible: options.visible === false ? 'INVISIBLE' : 'VISIBLE',
379
+ column_names: options.columns.map(c => {
380
+ if (c.indexOf(' ') !== -1) {
381
+ let t = c.split(' ', 2);
382
+ return `\`${t[0]}\` ${t[1].toUpperCase()}`;
383
+ }
384
+ return `\`${c}\``;
385
+ }).join(', ')
386
+ });
387
+ }
388
+
389
+ createForeignKey(options) {
390
+ _validate(options, {
391
+ name: 'required|string',
392
+ table: 'required|string',
393
+ column: 'required|string',
394
+ 'reference.tableName': 'required|string',
395
+ 'reference.columnName': 'required|string',
396
+ 'reference.onUpdate': [{ in: ['RESTRICT', 'CASCADE', 'SET NULL', 'NO ACTION'] }],
397
+ 'reference.onDelete': [{ in: ['RESTRICT', 'CASCADE', 'SET NULL', 'NO ACTION'] }]
398
+ });
399
+ return _render('ALTER TABLE `${table_name}` ADD CONSTRAINT `${name}` FOREIGN KEY (`${column_name}`) REFERENCES `${foreign_table}` (`${foreign_column}`) ON DELETE ${on_delete} ON UPDATE ${on_update}', {
400
+ table_name: options.tableName,
401
+ name: options.name,
402
+ column_name: options.columnName,
403
+ foreign_table: options.reference.tableName,
404
+ foreign_column: options.reference.columnName,
405
+ on_delete: options.reference.onDelete || 'NO ACTION',
406
+ on_update: options.reference.onUpdate || 'NO ACTION',
407
+ });
408
+ }
409
+
410
+ dropTable(options) {
411
+ _validate(options, {
412
+ name: 'required|string',
413
+ });
414
+ return _render('DROP TABLE `${name}`', options);
415
+ }
416
+
417
+ dropColumn(options) {
418
+ _validate(options, {
419
+ table: 'required|string',
420
+ name: 'required|string',
421
+ });
422
+ return _render('ALTER TABLE `${table}` DROP COLUMN `${name}`', options);
423
+
424
+ }
425
+
426
+ dropIndex(options) {
427
+ _validate(options, {
428
+ name: 'required|string',
429
+ table: 'required|string',
430
+ });
431
+ return _render('DROP INDEX `${name}` ON `${table}`', options);
432
+ }
433
+
434
+ dropForeignKey(options) {
435
+ _validate(options, {
436
+ name: 'required|string',
437
+ table: 'required|string',
438
+ });
439
+ return _render('ALTER TABLE `${table}` DROP FOREIGN KEY `${name}`', options);
440
+ }
441
+
442
+ createColumns(columns) {
443
+ let primaryColumn = null;
444
+ let indexColumns = [];
445
+ let strs = columns.map(column => {
446
+ let str = this.renderSingleColumn(column);
447
+ if (column.primaryKey === true) {
448
+ primaryColumn = column;
449
+ } else if (column.uniqIndex === true) {
450
+ indexColumns.push(column);
451
+ }
452
+ return str;
453
+ });
454
+ if (primaryColumn) {
455
+ strs.push(`PRIMARY KEY (\`${primaryColumn.name}\`)`);
456
+ strs.push(`UNIQUE INDEX \`${primaryColumn.name}\` (\`${primaryColumn.name}\` ASC) VISIBLE`);
457
+ }
458
+ if (indexColumns.length > 0) {
459
+ indexColumns.forEach((i) => {
460
+ strs.push(`UNIQUE INDEX \`${i.name}\` (\`${i.name}\` ASC) VISIBLE`);
461
+ });
462
+ }
463
+ return strs.join(', ');
464
+ }
465
+
466
+ renderSingleColumn(options) {
467
+ _validate(options, {
468
+ name: 'required|string',
469
+ type: 'required|string',
470
+ default: 'string',
471
+ onUpdate: 'string',
472
+ length: 'integer',
473
+ comment: 'string',
474
+ allowNull: 'boolean',
475
+ autoIncrement: 'boolean',
476
+ collate: 'string',
477
+ primaryKey: 'boolean',
478
+ uniqIndex: 'boolean'
479
+ });
480
+ let type = options.type.toUpperCase();
481
+ if (type === 'STRING') {
482
+ type = 'VARCHAR';
483
+ }
484
+ let str = `\`${options.name}\` ${type}`;
485
+ if (typeof options.length !== 'undefined') {
486
+ str += `(${options.length})`;
487
+ } else if (type === 'INT') {
488
+ str += '(11)';
489
+ } else if (type === 'VARCHAR') {
490
+ str += '(255)';
491
+ }
492
+ if (options.allowNull === false) {
493
+ str += ' NOT NULL';
494
+ }
495
+ if (options.unsigned === true) {
496
+ str += ' UNSIGNED';
497
+ }
498
+ if (typeof options.default !== 'undefined') {
499
+ if (options.primaryKey === true) {
500
+ throw new Error('Primary key can not have default value.');
501
+ }
502
+ if (options.default === null) {
503
+ str += ' DEFAULT NULL';
504
+ } else if (options.default === 'timestamp') {
505
+ str += ' DEFAULT CURRENT_TIMESTAMP';
506
+ } else if (is.string(options.default)) {
507
+ str += ` DEFAULT ${options.default}`;
508
+ }
509
+ }
510
+ if (options.onUpdate) {
511
+ str += ` ON UPDATE ${options.onUpdate}`;
512
+ }
513
+ if (is.string(options.comment) && is.empty(options.comment) === false) {
514
+ str += ` COMMENT '${options.comment}'`;
515
+ }
516
+ if (options.autoIncrement === true) {
517
+ str += ' AUTO_INCREMENT';
518
+ }
519
+ if (options.after) {
520
+ str += ' AFTER `' + options.after + '`';
521
+ }
522
+ return str;
523
+ }
524
+ }
525
+
288
526
  module.exports = {
289
- Builder
527
+ Builder,
528
+ ManageSQLBuilder
290
529
  };
package/src/client.js CHANGED
@@ -3,8 +3,9 @@
3
3
  const mysql = require('mysql2');
4
4
  const mysqlPromise = require('mysql2/promise');
5
5
  const { _validate } = require('./utils');
6
- const { QueryHandler, Query } = require('./operator');
7
- const { _query } = require('./utils');
6
+ const { _query } = require('./core');
7
+ const { QueryHandler } = require('./operator');
8
+ const Query = require('./query');
8
9
 
9
10
  const clients = {};
10
11
 
@@ -16,9 +17,9 @@ const clients = {};
16
17
  const createClient = (options, name = null) => {
17
18
  _validate(options, {
18
19
  host: 'required|string',
20
+ port: 'required|integer',
19
21
  user: 'required|string',
20
22
  password: 'required|string',
21
- port: 'required|integer',
22
23
  database: 'required|string',
23
24
  });
24
25
  const key = name ? name :
@@ -39,9 +40,9 @@ const createClient = (options, name = null) => {
39
40
  const createPromiseClient = async (options, name = null) => {
40
41
  _validate(options, {
41
42
  host: 'required|string',
43
+ port: 'required|integer',
42
44
  user: 'required|string',
43
45
  password: 'required|string',
44
- port: 'required|integer',
45
46
  database: 'required|string',
46
47
  });
47
48
  const key = name ? name :
@@ -96,31 +97,24 @@ class MySQLClient extends QueryHandler {
96
97
  /**
97
98
  * @param {mysql.ConnectionOptions} options
98
99
  * @param {*} name
100
+ * @param {'default'|'promise'|'pool'} type
99
101
  */
100
- constructor(options, name = 'default') {
101
- const conn = createClient(options, name);
102
+ constructor(options, name = 'default', type = 'default') {
103
+ let conn;
104
+ switch (type) {
105
+ case 'default':
106
+ conn = createClient(options, name);
107
+ break;
108
+ case 'pool':
109
+ conn = createPool(options, name);
110
+ break;
111
+ default:
112
+ throw new Error(`client type ${type} not found`);
113
+ }
102
114
  super(conn);
103
115
  this.database = options.database;
104
116
  }
105
117
 
106
- async existTable(table, database = null) {
107
- if (!table) {
108
- throw new Error('table name is required');
109
- }
110
- const c = await this.table('information_schema.TABLES')
111
- .where('TABLE_SCHEMA', database || this.database)
112
- .where('TABLE_NAME', table)
113
- .count();
114
- return !!c;
115
- }
116
-
117
- async existDatabase(database) {
118
- const c = await this.table('information_schema.SCHEMATA')
119
- .where('SCHEMA_NAME', database)
120
- .count();
121
- return !!c;
122
- }
123
-
124
118
  /**
125
119
  * @param {import('./operator').Query} query
126
120
  */
package/src/core.js ADDED
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ const { Builder } = require('./builder');
4
+
5
+ /**
6
+ *
7
+ * @param {import('mysql2/promise').Connection} conn
8
+ * @param {*} options
9
+ * @param {*} opt
10
+ * @returns
11
+ */
12
+ const _query = (conn, options, opt = null) => {
13
+ switch (options.driver) {
14
+ case 'mysql': {
15
+ if (opt === null) {
16
+ const builder = new Builder(options);
17
+ opt = {
18
+ sql: builder.sql,
19
+ values: builder.values || [],
20
+ };
21
+ }
22
+ return new Promise((resolve, reject) => {
23
+ if (options.transaction) {
24
+ conn.execute(opt.sql, opt.values || []).then((res) => {
25
+ resolve(res[0]);
26
+ }).catch((err) => reject(err));
27
+ return;
28
+ }
29
+ conn.query(opt, (err, result) => {
30
+ if (err) {
31
+ reject(err);
32
+ } else {
33
+ resolve(result);
34
+ }
35
+ });
36
+
37
+ });
38
+ }
39
+ default: {
40
+ if (typeof options.queryHandler === 'function') {
41
+ const promise = options.queryHandler(conn, options, opt);
42
+ if (promise instanceof Promise) {
43
+ return promise;
44
+ }
45
+ }
46
+ throw new Error('queryHandler must return a promise');
47
+ }
48
+ }
49
+ };
50
+
51
+ const _execSQL = (conn, sql, values = []) => {
52
+ let opt = { sql, values };
53
+ return new Promise((resolve, reject) => {
54
+ if (conn.query instanceof Function) {
55
+ conn.query(opt, (err, result) => {
56
+ if (err) {
57
+ reject(err);
58
+ } else {
59
+ resolve(result);
60
+ }
61
+ });
62
+ } else {
63
+ conn.execute(opt)
64
+ .then((res) => resolve(res))
65
+ .catch((err) => reject(err));
66
+ }
67
+ });
68
+ };
69
+
70
+ module.exports = {
71
+ _query,
72
+ _execSQL
73
+ };
@@ -0,0 +1,25 @@
1
+ type QueryItem = {
2
+ sql: string,
3
+ values: any[],
4
+ };
5
+
6
+ export type Context = {
7
+ action: 'up' | 'down',
8
+ connection: {
9
+ host: string;
10
+ port: number;
11
+ user: string;
12
+ password: string;
13
+ database: string;
14
+ },
15
+ items: string[],
16
+ task_key: string,
17
+ config: {
18
+ dir: string,
19
+ },
20
+ files: [],
21
+ runtime?: {
22
+ script: string,
23
+ queries: QueryItem[],
24
+ },
25
+ };
@@ -0,0 +1,319 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { _list, _exists } = require('@axiosleo/cli-tool/src/helper/fs');
5
+ const { createClient, createPool, createPromiseClient } = require('./client');
6
+ const { QueryHandler } = require('./operator');
7
+ const { printer, debug } = require('@axiosleo/cli-tool');
8
+ const { _execSQL } = require('./core');
9
+ const { _render } = require('@axiosleo/cli-tool/src/helper/str');
10
+ const { _foreach } = require('@axiosleo/cli-tool/src/helper/cmd');
11
+ const { _assign } = require('@axiosleo/cli-tool/src/helper/obj');
12
+ const { TransactionHandler } = require('..');
13
+ const { ManageSQLBuilder } = require('./builder');
14
+
15
+ const migrationColumns = [
16
+ {
17
+ name: 'id',
18
+ type: 'int',
19
+ length: 11,
20
+ allowNull: false,
21
+ autoIncrement: true,
22
+ primaryKey: true,
23
+ },
24
+ {
25
+ name: 'migration_key',
26
+ type: 'varchar',
27
+ length: 255,
28
+ allowNull: false,
29
+ },
30
+ {
31
+ name: 'filename',
32
+ type: 'varchar',
33
+ length: 255,
34
+ allowNull: false,
35
+ uniqIndex: true,
36
+ },
37
+ {
38
+ name: 'created_at',
39
+ type: 'datetime',
40
+ allowNull: false,
41
+ default: 'timestamp',
42
+ }
43
+ ];
44
+
45
+ /**
46
+ * initialize migration
47
+ * @param {import('./migration').Context} context
48
+ */
49
+ async function init(context) {
50
+ let files = await _list(context.config.dir, false, '.js');
51
+ if (context.action === 'down') {
52
+ files = files.reverse();
53
+ }
54
+ context.files = files.filter(f => f !== '.connect.js');
55
+
56
+ const connectPath = path.join(context.config.dir, '.connect.js');
57
+ if (await _exists(connectPath)) {
58
+ const connect = require(connectPath);
59
+ _assign(context.connection, connect);
60
+ }
61
+ context.task_key = 'migrate_' + context.connection.database;
62
+ let globalConn = createClient({
63
+ ...context.connection,
64
+ database: 'mysql'
65
+ });
66
+ let handler = new QueryHandler(globalConn);
67
+
68
+ const database = context.connection.database;
69
+ if (!await handler.existDatabase(database)) {
70
+ printer.yellow('will create database ' + database).println().println();
71
+ await _execSQL(globalConn, _render('CREATE DATABASE `${database_name}` CHARACTER SET ${charset} COLLATE ${collate}', {
72
+ database_name: database,
73
+ charset: 'utf8mb4',
74
+ collate: 'utf8mb4_unicode_ci'
75
+ }));
76
+ }
77
+ globalConn.end();
78
+ const conn = createPool(context.connection, context.task_key);
79
+ context.pool = conn;
80
+
81
+ handler = new QueryHandler(conn);
82
+ if (await handler.existTable(context.task_key, database)) {
83
+ conn.end();
84
+ return;
85
+ }
86
+
87
+ // migration table not exists
88
+ printer.yellow('will create table ' + context.task_key).println();
89
+ let builder = new ManageSQLBuilder({
90
+ operator: 'create',
91
+ target: 'table',
92
+ name: context.task_key,
93
+ columns: migrationColumns
94
+ });
95
+ let res = await _execSQL(conn, builder.sql);
96
+ conn.end();
97
+ if (res.serverStatus !== 2) {
98
+ printer.error('create migration table failed.');
99
+ process.exit(1);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * initialize migration
105
+ * @param {import('./migration').Context} context
106
+ */
107
+ async function _exec(context, queries) {
108
+ const conn = await createPromiseClient(context.connection, context.task_key + '_transaction');
109
+ const transaction = new TransactionHandler(conn);
110
+ await transaction.begin();
111
+
112
+ try {
113
+ const files = Object.keys(queries);
114
+ await _foreach(files, async (file) => {
115
+ const hasMigarated = await transaction.table(context.task_key)
116
+ .where('migration_key', context.task_key)
117
+ .where('filename', file)
118
+ .count();
119
+ if (context.action === 'up' && hasMigarated) {
120
+ if (hasMigarated) {
121
+ printer.yellow(`Migration file "${file}" has been migrated.`).println();
122
+ return;
123
+ }
124
+ } else if (context.action === 'down' && !hasMigarated) {
125
+ return;
126
+ }
127
+
128
+ const sqls = queries[file];
129
+ await _foreach(sqls, async (query) => {
130
+ if (context.debug) {
131
+ debug.log(query.sql);
132
+ }
133
+ await transaction.query(query);
134
+ });
135
+
136
+ if (context.action === 'up') {
137
+ await transaction.table(context.task_key).insert({
138
+ migration_key: context.task_key,
139
+ filename: file,
140
+ created_at: new Date()
141
+ });
142
+ printer.green('Success migrate up: ').yellow(file).println(' ');
143
+ } else {
144
+ const item = await transaction.table(context.task_key)
145
+ .where('migration_key', context.task_key)
146
+ .where('filename', file).find();
147
+ if (item) {
148
+ await transaction.table(context.task_key).where('id', item.id).delete();
149
+ }
150
+ printer.green('Success migrate down: ').yellow(file).println(' ');
151
+ }
152
+ });
153
+ await transaction.commit();
154
+ } catch (e) {
155
+ await transaction.rollback();
156
+ debug.log(e);
157
+ }
158
+ }
159
+
160
+ function _initMigration(file, queries = {}) {
161
+ const migration = {};
162
+ let baseAttr = {
163
+ writable: true,
164
+ enumerable: true,
165
+ configurable: true
166
+ };
167
+ Object.defineProperty(migration, 'createTable', {
168
+ value: function (table, columns, options = {}) {
169
+ _assign(options, {
170
+ operator: 'create',
171
+ target: 'table',
172
+ name: table,
173
+ columns
174
+ });
175
+ const builder = new ManageSQLBuilder(options);
176
+ queries[file].push({ sql: builder.sql, values: builder.values });
177
+ }, ...baseAttr
178
+ });
179
+ Object.defineProperty(migration, 'createColumn', {
180
+ value: function (name, type, table, options = {}) {
181
+ _assign(options, {
182
+ operator: 'create',
183
+ target: 'column',
184
+ table,
185
+ name,
186
+ type
187
+ });
188
+ const builder = new ManageSQLBuilder(options);
189
+ queries[file].push({ sql: builder.sql, values: builder.values });
190
+ }, ...baseAttr
191
+ });
192
+
193
+ Object.defineProperty(migration, 'createIndex', {
194
+ value: function (table, columns, options = {}) {
195
+ _assign(options, {
196
+ operator: 'create',
197
+ target: 'index',
198
+ name: options.indexName ? options.indexName : 'idx_' + table + '_' + columns.join('_'),
199
+ table,
200
+ columns
201
+ });
202
+ const builder = new ManageSQLBuilder(options);
203
+ queries[file].push({ sql: builder.sql, values: builder.values });
204
+ }, ...baseAttr
205
+ });
206
+
207
+ Object.defineProperty(migration, 'createForeignKey', {
208
+ value: function (options = {}) {
209
+ _assign(options, {
210
+ operator: 'create',
211
+ target: 'foreignKey',
212
+ name: options.foreignKey ? options.foreignKey : 'fk_' + options.tableName + '_' + options.columnName,
213
+ table: options.tableName,
214
+ column: options.columnName,
215
+ reference: options.reference
216
+ });
217
+ const builder = new ManageSQLBuilder(options);
218
+ queries[file].push({ sql: builder.sql, values: builder.values });
219
+ }, ...baseAttr
220
+ });
221
+
222
+ Object.defineProperty(migration, 'dropTable', {
223
+ value: function (table) {
224
+ const builder = new ManageSQLBuilder({
225
+ operator: 'drop',
226
+ target: 'table',
227
+ name: table
228
+ });
229
+ queries[file].push({ sql: builder.sql, values: builder.values });
230
+ }, ...baseAttr
231
+ });
232
+
233
+ Object.defineProperty(migration, 'dropColumn', {
234
+ value: function (name, table) {
235
+ const builder = new ManageSQLBuilder({
236
+ operator: 'drop',
237
+ target: 'column',
238
+ name,
239
+ table
240
+ });
241
+ queries[file].push({ sql: builder.sql, values: builder.values });
242
+ }, ...baseAttr
243
+ });
244
+
245
+ Object.defineProperty(migration, 'dropIndex', {
246
+ value: function (name, table) {
247
+ const builder = new ManageSQLBuilder({
248
+ operator: 'drop',
249
+ target: 'index',
250
+ name,
251
+ table
252
+ });
253
+ queries[file].push({ sql: builder.sql, values: builder.values });
254
+ }, ...baseAttr
255
+ });
256
+
257
+ Object.defineProperty(migration, 'dropForeignKey', {
258
+ value: function (name, table) {
259
+ const builder = new ManageSQLBuilder({
260
+ operator: 'drop',
261
+ target: 'foreignKey',
262
+ name,
263
+ table
264
+ });
265
+ queries[file].push({ sql: builder.sql, values: builder.values });
266
+ }, ...baseAttr
267
+ });
268
+
269
+ return migration;
270
+ }
271
+
272
+ /**
273
+ * initialize migration
274
+ * @param {import('./migration').Context} context
275
+ */
276
+ async function run(context) {
277
+ const { files } = context;
278
+ const queries = {};
279
+ await _foreach(files, async (file) => {
280
+ const scriptPath = path.join(context.config.dir, file);
281
+ const script = require(scriptPath);
282
+ queries[file] = [];
283
+
284
+ const migration = _initMigration(file, queries);
285
+
286
+ switch (context.action) {
287
+ case 'up': {
288
+ if (typeof script.up !== 'function') {
289
+ printer.error(`Migration file "${file}" must have a function named up.`);
290
+ process.exit(1);
291
+ }
292
+ await script.up(migration);
293
+ break;
294
+ }
295
+ case 'down': {
296
+ if (typeof script.down !== 'function') {
297
+ printer.error(`Migration file "${file}" must have a function named down.`);
298
+ process.exit(1);
299
+ }
300
+ await script.down(migration);
301
+ break;
302
+ }
303
+ default: {
304
+ throw new Error(`Unknown migration action ${context.action}.`);
305
+ }
306
+ }
307
+ });
308
+ await _exec(context, queries);
309
+ }
310
+
311
+ async function end(context) {
312
+ printer.yellow('Done').println(' ');
313
+ }
314
+
315
+ module.exports = {
316
+ init,
317
+ run,
318
+ end
319
+ };
package/src/operator.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const { Builder } = require('./builder');
4
4
  const Query = require('./query');
5
5
  const Hook = require('./hook');
6
- const { _query } = require('./utils');
6
+ const { _query } = require('./core');
7
7
 
8
8
  class QueryOperator extends Query {
9
9
  /**
@@ -13,13 +13,13 @@ class QueryOperator extends Query {
13
13
  super(null);
14
14
  this.conn = conn;
15
15
  this.options.driver = options.driver || 'mysql';
16
- this.options.query_handler = options.query_handler || null;
16
+ this.options.queryHandler = options.queryHandler || null;
17
17
  if (this.options.driver !== 'mysql') {
18
- if (!this.options.query_handler) {
19
- throw new Error('query_handler is required');
18
+ if (!this.options.queryHandler) {
19
+ throw new Error('queryHandler is required');
20
20
  }
21
- if (!(this.options.query_handler instanceof Function)) {
22
- throw new Error('query_handler must be a function');
21
+ if (!(this.options.queryHandler instanceof Function)) {
22
+ throw new Error('queryHandler must be a function');
23
23
  }
24
24
  }
25
25
  }
@@ -131,6 +131,26 @@ class QueryHandler {
131
131
  }
132
132
  return await this.table(tableName).insert(data);
133
133
  }
134
+
135
+ async existTable(table, database = null) {
136
+ if (!table) {
137
+ throw new Error('table name is required');
138
+ }
139
+ const query = new QueryOperator(this.conn, this.options);
140
+ const c = await query.table('information_schema.TABLES')
141
+ .where('TABLE_SCHEMA', database || this.database)
142
+ .where('TABLE_NAME', table)
143
+ .count();
144
+ return !!c;
145
+ }
146
+
147
+ async existDatabase(database) {
148
+ const query = new QueryOperator(this.conn, this.options);
149
+ const c = await query.table('information_schema.SCHEMATA')
150
+ .where('SCHEMA_NAME', database)
151
+ .count();
152
+ return !!c;
153
+ }
134
154
  }
135
155
 
136
156
  module.exports = {
package/src/query.js CHANGED
@@ -4,7 +4,7 @@ class Query {
4
4
  constructor(operator = 'select') {
5
5
  this.options = {
6
6
  driver: 'mysql',
7
- query_handler: null,
7
+ queryHandler: null,
8
8
  conditions: [],
9
9
  orders: [],
10
10
  tables: [],
@@ -3,6 +3,7 @@
3
3
  // eslint-disable-next-line no-unused-vars
4
4
  const mysql = require('mysql2/promise');
5
5
  const { QueryOperator } = require('./operator');
6
+ const { _query } = require('./core');
6
7
 
7
8
  const levels = {
8
9
  RU: 'READ UNCOMMITTED',
@@ -19,13 +20,13 @@ class TransactionOperator extends QueryOperator {
19
20
  super(conn);
20
21
  this.options.transaction = true;
21
22
  this.options.driver = options.driver || 'mysql';
22
- this.options.query_handler = options.query_handler || null;
23
+ this.options.queryHandler = options.queryHandler || null;
23
24
  if (this.options.driver !== 'mysql') {
24
- if (!this.options.query_handler) {
25
- throw new Error('query_handler is required');
25
+ if (!this.options.queryHandler) {
26
+ throw new Error('queryHandler is required');
26
27
  }
27
- if (!(this.options.query_handler instanceof Function)) {
28
- throw new Error('query_handler must be a function');
28
+ if (!(this.options.queryHandler instanceof Function)) {
29
+ throw new Error('queryHandler must be a function');
29
30
  }
30
31
  }
31
32
  }
@@ -60,13 +61,9 @@ class TransactionHandler {
60
61
 
61
62
  async query(options) {
62
63
  return new Promise((resolve, reject) => {
63
- this.conn.query(options, (err, result) => {
64
- if (err) {
65
- reject(err);
66
- } else {
67
- resolve(result);
68
- }
69
- });
64
+ _query(this.conn, { transaction: true, driver: 'mysql' }, options)
65
+ .catch((res) => reject(res))
66
+ .then((res) => resolve(res));
70
67
  });
71
68
  }
72
69
 
package/src/utils.js CHANGED
@@ -1,55 +1,21 @@
1
1
  'use strict';
2
2
 
3
3
  const Validator = require('validatorjs');
4
- const { Builder } = require('./builder');
5
4
 
6
- const _validate = (obj, rules) => {
5
+ const _validate = (obj, rules, throwError = true) => {
7
6
  let validation = new Validator(obj, rules);
8
7
  validation.check();
9
8
  if (validation.fails()) {
10
9
  const errors = validation.errors.all();
11
10
  const keys = Object.keys(errors);
12
- throw new Error(`${keys[0]}: ${errors[keys[0]]}`);
13
- }
14
- };
15
-
16
- const _query = async (conn, options, opt = null) => {
17
- switch (options.driver) {
18
- case 'mysql': {
19
- if (opt === null) {
20
- const builder = new Builder(options);
21
- opt = {
22
- sql: builder.sql,
23
- values: builder.values || [],
24
- };
25
- }
26
- return new Promise((resolve, reject) => {
27
- if (options.transaction) {
28
- conn.execute(opt)
29
- .then((res) => resolve(res))
30
- .catch((err) => reject(err));
31
- } else {
32
- conn.query(opt, (err, result) => {
33
- if (err) {
34
- reject(err);
35
- } else {
36
- resolve(result);
37
- }
38
- });
39
- }
40
- });
41
- }
42
- default: {
43
- const promise = options.query_handler(conn, options, opt);
44
- if (!(promise instanceof Promise)) {
45
- throw new Error('query_handler must return a promise');
46
- }
47
- return promise;
11
+ if (throwError) {
12
+ throw new Error(`${keys[0]}: ${errors[keys[0]]}`);
48
13
  }
14
+ return errors;
49
15
  }
16
+ return null;
50
17
  };
51
18
 
52
19
  module.exports = {
53
20
  _validate,
54
- _query
55
21
  };