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 +40 -0
- package/Readme.md +29 -3
- package/index.js +7 -0
- package/package.json +3 -1
- package/src/adapters/sequelize.js +11 -4
- package/src/bone.js +28 -13
- package/src/data_types.js +52 -12
- package/src/drivers/abstract/attribute.js +4 -0
- package/src/drivers/abstract/index.js +192 -1
- package/src/drivers/abstract/spellbook.js +403 -412
- package/src/drivers/index.js +15 -4
- package/src/drivers/mysql/attribute.js +11 -2
- package/src/drivers/mysql/index.js +101 -10
- package/src/drivers/mysql/spellbook.js +13 -11
- package/src/drivers/postgres/index.js +103 -109
- package/src/drivers/postgres/spellbook.js +9 -9
- package/src/drivers/postgres/sqlstring.js +124 -0
- package/src/drivers/sqlite/index.js +124 -13
- package/src/drivers/sqlite/spellbook.js +6 -6
- package/src/drivers/sqlite/sqlstring.js +88 -0
- package/src/hint.js +2 -1
- package/src/realm.js +8 -6
- package/src/spell.js +2 -4
- package/types/data_types.d.ts +9 -1
- package/types/hint.d.ts +96 -0
- package/types/index.d.ts +246 -20
- package/src/drivers/abstract/schema.js +0 -140
- package/src/drivers/mysql/schema.js +0 -98
- package/src/drivers/postgres/schema.js +0 -119
- package/src/drivers/sqlite/schema.js +0 -203
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
|
[](https://packagequality.com/#?package=leoric)
|
|
4
4
|
[](https://www.npmjs.com/package/leoric)
|
|
5
5
|
[](https://www.npmjs.com/package/leoric)
|
|
6
|
-
[](https://github.com/cyjake/leoric/actions/workflows/nodejs.yml)
|
|
7
7
|
[](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
|
-
##
|
|
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
|
+
"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
|
|
323
|
+
static bulkDestroy(options = {}) {
|
|
321
324
|
const { where, force } = options;
|
|
322
325
|
const spell = this._remove(where || {}, force, { ...options });
|
|
323
|
-
|
|
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
|
-
|
|
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 !
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
25
|
-
const
|
|
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(
|
|
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(
|
|
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(
|
|
52
|
+
return new INTEGER(...params);
|
|
49
53
|
case 'mediumint':
|
|
50
|
-
return new MEDIUMINT(
|
|
54
|
+
return new MEDIUMINT(...params);
|
|
51
55
|
case 'smallint':
|
|
52
|
-
return new SMALLINT(
|
|
56
|
+
return new SMALLINT(...params);
|
|
53
57
|
case 'tinyint':
|
|
54
|
-
return new TINYINT(
|
|
58
|
+
return new TINYINT(...params);
|
|
55
59
|
case 'bigint':
|
|
56
|
-
return new BIGINT(
|
|
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(
|
|
67
|
+
return new BINARY(...params);
|
|
64
68
|
// mysql only
|
|
65
69
|
case 'varbinary':
|
|
66
|
-
return new VARBINARY(
|
|
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
|
-
|
|
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;
|