leoric 2.3.0 → 2.4.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/History.md CHANGED
@@ -1,3 +1,43 @@
1
+ 2.4.0 / 2022-04-24
2
+ ==================
3
+
4
+ ## What's Changed
5
+ * feat: support custom driver by @JimmyDaddy in https://github.com/cyjake/leoric/pull/304
6
+ * chore: update build status badge by @snapre in https://github.com/cyjake/leoric/pull/305
7
+ * feat: export more ts type definitions and use deep-equal module by @JimmyDaddy in https://github.com/cyjake/leoric/pull/306
8
+
9
+ ## New Contributors
10
+ * @snapre made their first contribution in https://github.com/cyjake/leoric/pull/305
11
+
12
+ **Full Changelog**: https://github.com/cyjake/leoric/compare/v2.3.2...v2.4.0
13
+
14
+ 2.3.2 / 2022-04-15
15
+ ==================
16
+
17
+ ## What's Changed
18
+ * fix: order by raw with mix-type array in sequelize mode by @JimmyDaddy in https://github.com/cyjake/leoric/pull/298
19
+ * docs: monthly updates and example about egg-orm usage with TypeScript by @cyjake in https://github.com/cyjake/leoric/pull/299
20
+ * docs: monthly updates in en & docmentation about typescript support by @cyjake in https://github.com/cyjake/leoric/pull/300
21
+ * fix: raw query should format replacements with extra blank by @JimmyDaddy in https://github.com/cyjake/leoric/pull/301
22
+ * docs: elaborate on querying by @cyjake in https://github.com/cyjake/leoric/pull/302
23
+ * feat: transaction should return result by @JimmyDaddy in https://github.com/cyjake/leoric/pull/303
24
+
25
+
26
+ **Full Changelog**: https://github.com/cyjake/leoric/compare/v2.3.1...v2.3.2
27
+
28
+ 2.3.1 / 2022-03-22
29
+ ==================
30
+
31
+ ## What's Changed
32
+ * fix: mysql2 Invalid Date compatible by @JimmyDaddy in https://github.com/cyjake/leoric/pull/291
33
+ * fix: order by raw in sequelize mode by @JimmyDaddy in https://github.com/cyjake/leoric/pull/292
34
+ * fix: bulk update query conditions duplicated in sequelize mode by @JimmyDaddy in https://github.com/cyjake/leoric/pull/293
35
+ * fix: bulk destroy query conditions duplicated in sequelize mode by @JimmyDaddy in https://github.com/cyjake/leoric/pull/295
36
+ * fix: drop column if not defined in attributes when alter table by @cyjake in https://github.com/cyjake/leoric/pull/296
37
+
38
+
39
+ **Full Changelog**: https://github.com/cyjake/leoric/compare/v2.3.0...v2.3.1
40
+
1
41
  2.3.0 / 2022-03-10
2
42
  ==================
3
43
 
package/Readme.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Package Quality](https://packagequality.com/shield/leoric.svg)](https://packagequality.com/#?package=leoric)
4
4
  [![NPM Downloads](https://img.shields.io/npm/dm/leoric.svg?style=flat)](https://www.npmjs.com/package/leoric)
5
5
  [![NPM Version](http://img.shields.io/npm/v/leoric.svg?style=flat)](https://www.npmjs.com/package/leoric)
6
- [![Build Status](https://travis-ci.org/cyjake/leoric.svg)](https://travis-ci.org/cyjake/leoric)
6
+ [![Build Status](https://github.com/cyjake/leoric/actions/workflows/nodejs.yml/badge.svg)](https://github.com/cyjake/leoric/actions/workflows/nodejs.yml)
7
7
  [![codecov](https://codecov.io/gh/cyjake/leoric/branch/master/graph/badge.svg?token=OZZWTZTDS1)](https://codecov.io/gh/cyjake/leoric)
8
8
 
9
9
  Leoric is an object-relational mapping for Node.js, which is heavily influenced by Active Record of Ruby on Rails. See the [documentation](https://leoric.js.org) for detail.
@@ -74,6 +74,32 @@ await realm.sync();
74
74
 
75
75
  A more detailed syntax table may be found at the [documentation](https://leoric.js.org/#syntax-table) site.
76
76
 
77
+ ## TypeScript charged
78
+
79
+ ```ts
80
+ import { Bone, BelongsTo, Column, DataTypes: { TEXT } } from 'leoric';
81
+ import User from './user';
82
+
83
+ export default class Post extends Bone {
84
+ @Column({ autoIncrement: true })
85
+ id: bigint;
86
+
87
+ @Column(TEXT)
88
+ content: string;
89
+
90
+ @Column()
91
+ description: string;
92
+
93
+ @Column()
94
+ userId: bigint;
95
+
96
+ @BelongsTo()
97
+ user: User;
98
+ }
99
+ ```
100
+
101
+ More about TypeScript integration examples can be found at [the TypeScript support documentation](https://leoric.js.org/types)
102
+
77
103
  ## Contributing
78
104
 
79
105
  There are many ways in which you can participate in the project, for example:
@@ -89,6 +115,6 @@ If you are interested in fixing issues and contributing directly to the code bas
89
115
  - Submitting pull requests
90
116
  - Contributing to translations
91
117
 
92
- ## Related Projects
118
+ ## egg-orm
93
119
 
94
- If developing web applications with [egg framework](https://eggjs.org/), it's highly recommended using the [egg-orm](https://github.com/eggjs/egg-orm) plugin.
120
+ If developing web applications with [egg framework](https://eggjs.org/), it's highly recommended using the [egg-orm](https://github.com/eggjs/egg-orm) plugin. More detailed examples about setting up egg-orm with egg framework in either JavaScript or TypeScript can be found at <https://github.com/eggjs/egg-orm/tree/master/examples>
package/index.js CHANGED
@@ -12,6 +12,8 @@ const { heresql } = require('./src/utils/string');
12
12
  const Hint = require('./src/hint');
13
13
  const Realm = require('./src/realm');
14
14
  const Decorators = require('./src/decorators');
15
+ const Raw = require('./src/raw');
16
+ const { MysqlDriver, PostgresDriver, SqliteDriver, AbstractDriver } = require('./src/drivers');
15
17
 
16
18
  /**
17
19
  * @typedef {Object} RawSql
@@ -50,6 +52,11 @@ Object.assign(Realm, {
50
52
  heresql,
51
53
  ...Hint,
52
54
  ...Decorators,
55
+ MysqlDriver,
56
+ PostgresDriver,
57
+ SqliteDriver,
58
+ AbstractDriver,
59
+ Raw,
53
60
  });
54
61
 
55
62
  module.exports = Realm;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leoric",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "JavaScript Object-relational mapping alchemy",
5
5
  "main": "index.js",
6
6
  "types": "types/index.d.ts",
@@ -21,6 +21,7 @@
21
21
  "test:mysql2": "./test/start.sh test/integration/mysql2.test.js",
22
22
  "test:postgres": "./test/start.sh test/integration/postgres.test.js",
23
23
  "test:sqlite": "./test/start.sh test/integration/sqlite.test.js",
24
+ "test:custom": "./test/start.sh test/integration/custom.test.js",
24
25
  "test:sqlcipher": "./test/start.sh test/integration/sqlcipher.test.js",
25
26
  "test:dts": "./test/start.sh dts",
26
27
  "test:coverage": "nyc ./test/start.sh && nyc report --reporter=lcov",
@@ -49,6 +50,7 @@
49
50
  },
50
51
  "dependencies": {
51
52
  "debug": "^3.1.0",
53
+ "deep-equal": "^2.0.5",
52
54
  "heredoc": "^1.3.1",
53
55
  "pluralize": "^7.0.0",
54
56
  "reflect-metadata": "^0.1.13",
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { setupSingleHook } = require('../setup_hooks');
4
4
  const { compose, isPlainObject } = require('../utils');
5
+ const Raw = require('../raw');
5
6
 
6
7
  function translateOptions(spell, options) {
7
8
  const { attributes, where, group, order, offset, limit, include, having } = options;
@@ -18,7 +19,7 @@ function translateOptions(spell, options) {
18
19
  if (having) spell.$having(having);
19
20
 
20
21
  if (order) {
21
- if (typeof order === 'string') {
22
+ if (typeof order === 'string' || order instanceof Raw) {
22
23
  spell.$order(order);
23
24
  } else if (Array.isArray(order) && order.length) {
24
25
  if (order.some(item => Array.isArray(item))) {
@@ -34,6 +35,8 @@ function translateOptions(spell, options) {
34
35
  } else if (order.length && order[0]) {
35
36
  // ['created_at', 'asc']
36
37
  spell.$order(order[0], order[1] || '');
38
+ } else if (order instanceof Raw) {
39
+ spell.$order(order);
37
40
  }
38
41
  }
39
42
  }
@@ -317,10 +320,12 @@ module.exports = Bone => {
317
320
  }
318
321
 
319
322
  // proxy to class.destroy({ individualHooks=false }) see https://github.com/sequelize/sequelize/blob/4063c2ab627ad57919d5b45cc7755f077a69fa5e/lib/model.js#L2895 before(after)BulkDestroy
320
- static async bulkDestroy(options = {}) {
323
+ static bulkDestroy(options = {}) {
321
324
  const { where, force } = options;
322
325
  const spell = this._remove(where || {}, force, { ...options });
323
- translateOptions(spell, options);
326
+ const transOptions = { ...options };
327
+ delete transOptions.where;
328
+ translateOptions(spell, transOptions);
324
329
  return spell;
325
330
  }
326
331
 
@@ -496,7 +501,9 @@ module.exports = Bone => {
496
501
  const { where, paranoid = false, validate } = options;
497
502
  const whereConditions = where || {};
498
503
  const spell = super.update(whereConditions, values, { validate, hooks: false, ...options });
499
- translateOptions(spell, options);
504
+ const transOptions = { ...options };
505
+ delete transOptions.where;
506
+ translateOptions(spell, transOptions);
500
507
  if (!paranoid) return spell.unparanoid;
501
508
  return spell;
502
509
  }
package/src/bone.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * @module
6
6
  */
7
7
  const util = require('util');
8
+ const deepEqual = require('deep-equal');
8
9
  const pluralize = require('pluralize');
9
10
  const { executeValidator, LeoricValidateError } = require('./validator');
10
11
  require('reflect-metadata');
@@ -37,10 +38,12 @@ function looseReadonly(props) {
37
38
 
38
39
  function compare(attributes, columnMap) {
39
40
  const diff = {};
41
+ const columnNames = new Set();
40
42
 
41
43
  for (const name in attributes) {
42
44
  const attribute = attributes[name];
43
45
  const { columnName } = attribute;
46
+ columnNames.add(columnName);
44
47
 
45
48
  if (!attribute.equals(columnMap[columnName])) {
46
49
  diff[name] = {
@@ -50,6 +53,12 @@ function compare(attributes, columnMap) {
50
53
  }
51
54
  }
52
55
 
56
+ for (const columnName in columnMap) {
57
+ if (!columnNames.has(columnName)) {
58
+ diff[columnName] = { remove: true };
59
+ }
60
+ }
61
+
53
62
  return diff;
54
63
  }
55
64
 
@@ -254,8 +263,8 @@ class Bone {
254
263
 
255
264
  /**
256
265
  * get actual update/insert columns to avoid empty insert or update
257
- * @param {Object} data
258
- * @returns
266
+ * @param {Object} data
267
+ * @returns
259
268
  */
260
269
  static _getColumns(data) {
261
270
  if (!Object.keys(data).length) return data;
@@ -372,7 +381,7 @@ class Bone {
372
381
  if (this.#rawUnset.has(name) || !this.hasAttribute(name)) return false;
373
382
  const value = this.attribute(name);
374
383
  const valueWas = this.attributeWas(name);
375
- return !util.isDeepStrictEqual(value, valueWas);
384
+ return !deepEqual(value, valueWas);
376
385
  }
377
386
 
378
387
  /**
@@ -404,7 +413,7 @@ class Bone {
404
413
  if (this.#rawUnset.has(name) || this.#rawPrevious[name] === undefined || !this.hasAttribute(name)) return {};
405
414
  const value = this.attribute(name);
406
415
  const valueWas = this.#rawPrevious[name] == null ? null : this.#rawPrevious[name];
407
- if (util.isDeepStrictEqual(value, valueWas)) return {};
416
+ if (deepEqual(value, valueWas)) return {};
408
417
  return { [name]: [ valueWas, value ] };
409
418
  }
410
419
  const result = {};
@@ -412,7 +421,7 @@ class Bone {
412
421
  if (this.#rawUnset.has(attrKey) || this.#rawPrevious[attrKey] === undefined) continue;
413
422
  const value = this.attribute(attrKey);
414
423
  const valueWas = this.#rawPrevious[attrKey] == null ? null : this.#rawPrevious[attrKey];
415
- if (!util.isDeepStrictEqual(value, valueWas)) result[attrKey] = [ valueWas, value ];
424
+ if (!deepEqual(value, valueWas)) result[attrKey] = [ valueWas, value ];
416
425
  }
417
426
  return result;
418
427
  }
@@ -431,7 +440,7 @@ class Bone {
431
440
  if (this.#rawUnset.has(name) || !this.hasAttribute(name)) return {};
432
441
  const value = this.attribute(name);
433
442
  const valueWas = this.attributeWas(name);
434
- if (util.isDeepStrictEqual(value, valueWas)) return {};
443
+ if (deepEqual(value, valueWas)) return {};
435
444
  return { [name]: [ valueWas, value ] };
436
445
  }
437
446
  const result = {};
@@ -440,7 +449,7 @@ class Bone {
440
449
  const value = this.attribute(attrKey);
441
450
  const valueWas = this.attributeWas(attrKey);
442
451
 
443
- if (!util.isDeepStrictEqual(value, valueWas)) {
452
+ if (!deepEqual(value, valueWas)) {
444
453
  result[attrKey] = [ valueWas, value ];
445
454
  }
446
455
  }
@@ -573,7 +582,14 @@ class Bone {
573
582
  for (const name of Object.keys(attributes)) {
574
583
  const attribute = attributes[name];
575
584
  // Take advantage of uncast/cast to create new copy of value
576
- const value = attribute.uncast(this.#raw[name]);
585
+ let value;
586
+ try {
587
+ value = attribute.uncast(this.#raw[name]);
588
+ } catch (error) {
589
+ console.error(error);
590
+ // do not interrupt sync raw
591
+ value = this.#raw[name];
592
+ }
577
593
  if (this.#rawSaved[name] !== undefined) {
578
594
  this.#rawPrevious[name] = this.#rawSaved[name];
579
595
  } else if (this.#rawPrevious[name] === undefined && this.#raw[name] != null) {
@@ -1545,18 +1561,17 @@ class Bone {
1545
1561
  }
1546
1562
 
1547
1563
  static query(spell) {
1548
- const { sql, values } = this.driver.format(spell);
1549
- const query = { sql, nestTables: spell.command === 'select' };
1550
- return this.driver.query(query, values, spell);
1564
+ return this.driver.cast(spell);
1551
1565
  }
1552
1566
 
1553
1567
  static async transaction(callback) {
1554
1568
  const connection = await this.driver.getConnection();
1569
+ let result;
1555
1570
  if (callback.constructor.name === 'AsyncFunction') {
1556
1571
  // if callback is an AsyncFunction
1557
1572
  await this.driver.query('BEGIN', [], { connection, Model: this, command: 'BEGIN' });
1558
1573
  try {
1559
- await callback({ connection });
1574
+ result = await callback({ connection });
1560
1575
  await this.driver.query('COMMIT', [], { connection, Model: this, command: 'COMMIT' });
1561
1576
  } catch (err) {
1562
1577
  await this.driver.query('ROLLBACK', [], { connection, Model: this, command: 'ROLLBACK' });
@@ -1566,7 +1581,6 @@ class Bone {
1566
1581
  }
1567
1582
  } else if (callback.constructor.name === 'GeneratorFunction') {
1568
1583
  const gen = callback({ connection });
1569
- let result;
1570
1584
 
1571
1585
  try {
1572
1586
  await this.driver.query('BEGIN', [], { connection, Model: this, command: 'BEGIN' });
@@ -1586,6 +1600,7 @@ class Bone {
1586
1600
  } else {
1587
1601
  throw new Error('unexpected transaction function, should be GeneratorFunction or AsyncFunction.');
1588
1602
  }
1603
+ return result;
1589
1604
  }
1590
1605
 
1591
1606
  static init(attributes = {}, opts = {}, overrides = {}) {
package/src/data_types.js CHANGED
@@ -17,17 +17,20 @@ class DataType {
17
17
  const {
18
18
  STRING, TEXT,
19
19
  DATE, DATEONLY,
20
- TINYINT, SMALLINT, MEDIUMINT, INTEGER, BIGINT,
20
+ TINYINT, SMALLINT, MEDIUMINT, INTEGER, BIGINT, DECIMAL,
21
21
  BOOLEAN,
22
22
  BINARY, VARBINARY, BLOB,
23
23
  } = this;
24
- const [ , dataType, appendix ] = columnType.match(/(\w+)(?:\((\d+)\))?/);
25
- const length = appendix && parseInt(appendix, 10);
24
+ const [ , dataType, ...matches ] = columnType.match(/(\w+)(?:\((\d+)(?:,(\d+))?\))?/);
25
+ const params = [];
26
+ for (let i = 0; i < matches.length; i++) {
27
+ if (matches[i] != null) params[i] = parseInt(matches[i], 10);
28
+ }
26
29
 
27
30
  switch (dataType) {
28
31
  case 'varchar':
29
32
  case 'char':
30
- return new STRING(length);
33
+ return new STRING(...params);
31
34
  // longtext is only for MySQL
32
35
  case 'longtext':
33
36
  return new TEXT('long');
@@ -40,30 +43,31 @@ class DataType {
40
43
  case 'datetime':
41
44
  case 'timestamp':
42
45
  // new DATE(precision)
43
- return new DATE(length);
46
+ return new DATE(...params);
44
47
  case 'decimal':
48
+ return new DECIMAL(...params);
45
49
  case 'int':
46
50
  case 'integer':
47
51
  case 'numeric':
48
- return new INTEGER(length);
52
+ return new INTEGER(...params);
49
53
  case 'mediumint':
50
- return new MEDIUMINT(length);
54
+ return new MEDIUMINT(...params);
51
55
  case 'smallint':
52
- return new SMALLINT(length);
56
+ return new SMALLINT(...params);
53
57
  case 'tinyint':
54
- return new TINYINT(length);
58
+ return new TINYINT(...params);
55
59
  case 'bigint':
56
- return new BIGINT(length);
60
+ return new BIGINT(...params);
57
61
  case 'boolean':
58
62
  return new BOOLEAN();
59
63
  // mysql only
60
64
  case 'binary':
61
65
  // postgres only
62
66
  case 'bytea':
63
- return new BINARY(length);
67
+ return new BINARY(...params);
64
68
  // mysql only
65
69
  case 'varbinary':
66
- return new VARBINARY(length);
70
+ return new VARBINARY(...params);
67
71
  case 'longblob':
68
72
  return new BLOB('long');
69
73
  case 'mediumblob':
@@ -273,6 +277,41 @@ class BIGINT extends INTEGER {
273
277
  }
274
278
  }
275
279
 
280
+ /**
281
+ * fixed-point decimal types
282
+ * @example
283
+ * DECIMAL
284
+ * DECIMAL.UNSIGNED
285
+ * DECIMAL(5, 2)
286
+ * @param {number} precision
287
+ * @param {number} scale
288
+ * - https://dev.mysql.com/doc/refman/8.0/en/fixed-point-types.html
289
+ */
290
+ class DECIMAL extends INTEGER {
291
+ constructor(precision, scale) {
292
+ super();
293
+ this.dataType = 'decimal';
294
+ this.precision = precision;
295
+ this.scale = scale;
296
+ }
297
+
298
+ toSqlString() {
299
+ const { precision, scale, unsigned, zerofill } = this;
300
+ const dataType = this.dataType.toUpperCase();
301
+ const chunks = [];
302
+ if (precision > 0 && scale >= 0) {
303
+ chunks.push(`${dataType}(${precision},${scale})`);
304
+ } else if (precision > 0) {
305
+ chunks.push(`${dataType}(${precision})`);
306
+ } else {
307
+ chunks.push(dataType);
308
+ }
309
+ if (unsigned) chunks.push('UNSIGNED');
310
+ if (zerofill) chunks.push('ZEROFILL');
311
+ return chunks.join(' ');
312
+ }
313
+ }
314
+
276
315
  const rDateFormat = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:[,.]\d{3,6})$/;
277
316
 
278
317
  class DATE extends DataType {
@@ -494,6 +533,7 @@ const DataTypes = {
494
533
  MEDIUMINT,
495
534
  INTEGER,
496
535
  BIGINT,
536
+ DECIMAL,
497
537
  DATE,
498
538
  DATEONLY,
499
539
  BOOLEAN,
@@ -66,6 +66,8 @@ function createType(DataTypes, params) {
66
66
  switch (type.constructor.name) {
67
67
  case 'DATE':
68
68
  return new DataType(type.precision, type.timezone);
69
+ case 'DECIMAL':
70
+ return new DataType(type.precision, type.scale);
69
71
  case 'TINYINT':
70
72
  case 'SMALLINT':
71
73
  case 'MEDIUMINT':
@@ -73,6 +75,8 @@ function createType(DataTypes, params) {
73
75
  case 'BIGINT':
74
76
  case 'BINARY':
75
77
  case 'VARBINARY':
78
+ case 'CHAR':
79
+ case 'VARCHAR':
76
80
  return new DataType(type.length);
77
81
  default:
78
82
  return new DataType();
@@ -1,6 +1,12 @@
1
1
  'use strict';
2
2
 
3
+ const SqlString = require('sqlstring');
4
+
3
5
  const Logger = require('./logger');
6
+ const Attribute = require('./attribute');
7
+ const DataTypes = require('../../data_types');
8
+ const Spellbook = require('./spellbook');
9
+ const { heresql, camelCase } = require('../../utils/string');
4
10
 
5
11
  /**
6
12
  * Migration methods
@@ -8,11 +14,196 @@ const Logger = require('./logger');
8
14
  * - https://dev.mysql.com/doc/refman/8.0/en/alter-table.html
9
15
  */
10
16
 
11
- module.exports = class AbstractDriver {
17
+ class AbstractDriver {
18
+
19
+ // define static properties as this way IDE will prompt
20
+ static Spellbook = Spellbook;
21
+ static Attribute = Attribute;
22
+ static DataTypes = DataTypes;
23
+
12
24
  constructor(opts = {}) {
13
25
  const { logger } = opts;
14
26
  this.logger = logger instanceof Logger ? logger : new Logger(logger);
15
27
  this.idleTimeout = opts.idleTimeout || 60;
16
28
  this.options = opts;
29
+ this.Attribute = this.constructor.Attribute;
30
+ this.DataTypes = this.constructor.DataTypes;
31
+ this.spellbook = new this.constructor.Spellbook();
32
+ this.escape = SqlString.escape;
33
+ this.escapeId = SqlString.escapeId;
34
+ }
35
+
36
+ /**
37
+ * query with spell
38
+ * @param {Spell} spell
39
+ * @returns
40
+ */
41
+ async cast(spell) {
42
+ const { sql, values } = this.format(spell);
43
+ const query = { sql, nestTables: spell.command === 'select' };
44
+ return await this.query(query, values, spell);
45
+ }
46
+
47
+ /**
48
+ * raw query
49
+ * @param {object|string} query
50
+ * @param {object | array} values
51
+ * @param {object} opts
52
+ */
53
+ async query(query, values, opts) {
54
+ throw new Error('unimplemented!');
55
+ }
56
+
57
+ get dialect() {
58
+ return camelCase(this.constructor.name.replace('Driver', ''));
59
+ }
60
+
61
+ /**
62
+ * use spellbook to format spell
63
+ * @param {Spell} spell
64
+ * @returns
65
+ */
66
+ format(spell) {
67
+ return this.spellbook.format(spell);
17
68
  }
69
+
70
+ async createTable(table, attributes) {
71
+ const { escapeId } = this;
72
+ const chunks = [ `CREATE TABLE ${escapeId(table)}` ];
73
+ const columns = Object.keys(attributes).map(name => {
74
+ const attribute = new this.Attribute(name, attributes[name]);
75
+ return attribute.toSqlString();
76
+ });
77
+ chunks.push(`(${columns.join(', ')})`);
78
+ await this.query(chunks.join(' '));
79
+ }
80
+
81
+ async alterTable(table, attributes) {
82
+ const { escapeId } = this;
83
+ const chunks = [ `ALTER TABLE ${escapeId(table)}` ];
84
+
85
+ const actions = Object.keys(attributes).map(name => {
86
+ const options = attributes[name];
87
+ // { [columnName]: { remove: true } }
88
+ if (options.remove) return `DROP COLUMN ${escapeId(name)}`;
89
+ const attribute = new this.Attribute(name, options);
90
+ return [
91
+ options.modify ? 'MODIFY COLUMN' : 'ADD COLUMN',
92
+ attribute.toSqlString(),
93
+ ].join(' ');
94
+ });
95
+ chunks.push(actions.join(', '));
96
+ await this.query(chunks.join(' '));
97
+ }
98
+
99
+ async describeTable(table) {
100
+ const { database } = this.options;
101
+ const schemaInfo = await this.querySchemaInfo(database, table);
102
+ return schemaInfo[table].reduce(function(result, column) {
103
+ result[column.columnName] = column;
104
+ return result;
105
+ }, {});
106
+ }
107
+
108
+ async addColumn(table, name, params) {
109
+ const { escapeId } = this;
110
+ const attribute = new this.Attribute(name, params);
111
+ const sql = heresql(`
112
+ ALTER TABLE ${escapeId(table)}
113
+ ADD COLUMN ${attribute.toSqlString()}
114
+ `);
115
+ await this.query(sql);
116
+ }
117
+
118
+ async changeColumn(table, name, params) {
119
+ const { escapeId } = this;
120
+ const attribute = new this.Attribute(name, params);
121
+ const sql = heresql(`
122
+ ALTER TABLE ${escapeId(table)}
123
+ MODIFY COLUMN ${attribute.toSqlString()}
124
+ `);
125
+ await this.query(sql);
126
+ }
127
+
128
+ async removeColumn(table, name) {
129
+ const { escapeId } = this;
130
+ const { columnName } = new this.Attribute(name);
131
+ const sql = heresql(`
132
+ ALTER TABLE ${escapeId(table)} DROP COLUMN ${escapeId(columnName)}
133
+ `);
134
+ await this.query(sql);
135
+ }
136
+
137
+ async renameColumn(table, name, newName) {
138
+ const { escapeId } = this;
139
+ const { columnName } = new this.Attribute(name);
140
+ const attribute = new this.Attribute(newName);
141
+ const sql = heresql(`
142
+ ALTER TABLE ${escapeId(table)}
143
+ RENAME COLUMN ${escapeId(columnName)} TO ${escapeId(attribute.columnName)}
144
+ `);
145
+ await this.query(sql);
146
+ }
147
+
148
+ async renameTable(table, newTable) {
149
+ const { escapeId } = this;
150
+ const sql = heresql(`
151
+ ALTER TABLE ${escapeId(table)} RENAME TO ${escapeId(newTable)}
152
+ `);
153
+ await this.query(sql);
154
+ }
155
+
156
+ async dropTable(table) {
157
+ const { escapeId } = this;
158
+ await this.query(`DROP TABLE IF EXISTS ${escapeId(table)}`);
159
+ }
160
+
161
+ async truncateTable(table) {
162
+ const { escapeId } = this;
163
+ await this.query(`TRUNCATE TABLE ${escapeId(table)}`);
164
+ }
165
+
166
+ async addIndex(table, attributes, opts = {}) {
167
+ const { escapeId } = this;
168
+ const columns = attributes.map(name => new this.Attribute(name).columnName);
169
+ const type = opts.unique ? 'UNIQUE' : opts.type;
170
+ const prefix = type === 'UNIQUE' ? 'uk' : 'idx';
171
+ const { name } = {
172
+ name: [ prefix, table ].concat(columns).join('_'),
173
+ ...opts,
174
+ };
175
+
176
+ if (type != null && ![ 'UNIQUE', 'FULLTEXT', 'SPATIAL' ].includes(type)) {
177
+ throw new Error(`Unexpected index type: ${type}`);
178
+ }
179
+
180
+ const sql = heresql(`
181
+ CREATE ${type ? `${type} INDEX` : 'INDEX'} ${escapeId(name)}
182
+ ON ${escapeId(table)} (${columns.map(escapeId).join(', ')})
183
+ `);
184
+ await this.query(sql);
185
+ }
186
+
187
+ async removeIndex(table, attributes, opts = {}) {
188
+ const { escapeId } = this;
189
+ let name;
190
+ if (Array.isArray(attributes)) {
191
+ const columns = attributes.map(entry => new this.Attribute(entry).columnName);
192
+ const type = opts.unique ? 'UNIQUE' : opts.type;
193
+ const prefix = type === 'UNIQUE' ? 'uk' : 'idx';
194
+ name = [ prefix, table ].concat(columns).join('_');
195
+ } else if (typeof attributes === 'string') {
196
+ name = attributes;
197
+ } else {
198
+ throw new Error(`Unexpected index name: ${attributes}`);
199
+ }
200
+
201
+ const sql = this.type === 'mysql'
202
+ ? `DROP INDEX ${escapeId(name)} ON ${escapeId(table)}`
203
+ : `DROP INDEX IF EXISTS ${escapeId(name)}`;
204
+ await this.query(sql);
205
+ }
206
+
18
207
  };
208
+
209
+ module.exports = AbstractDriver;