leoric 2.2.1 → 2.3.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 +27 -0
- package/index.js +3 -0
- package/package.json +5 -3
- package/src/bone.js +98 -39
- package/src/collection.js +2 -2
- package/src/constants.js +8 -1
- package/src/data_types.js +13 -0
- package/src/decorators.js +77 -0
- package/src/decorators.js.map +1 -0
- package/src/decorators.ts +96 -0
- package/src/drivers/abstract/attribute.js +13 -3
- package/src/drivers/abstract/spellbook.js +15 -15
- package/src/drivers/mysql/index.js +1 -1
- package/src/drivers/mysql/spellbook.js +4 -4
- package/src/drivers/postgres/index.js +1 -1
- package/src/drivers/sqlite/index.js +1 -1
- package/src/drivers/sqlite/spellbook.js +1 -1
- package/src/expr_formatter.js +3 -0
- package/src/realm.js +1 -1
- package/src/setup_hooks.js +21 -2
- package/src/spell.js +63 -29
- package/types/data_types.d.ts +6 -0
- package/types/index.d.ts +13 -7
package/History.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
2.3.0 / 2022-03-10
|
|
2
|
+
==================
|
|
3
|
+
|
|
4
|
+
## What's Changed
|
|
5
|
+
* feat: model declaration with decorators by @cyjake in https://github.com/cyjake/leoric/pull/287
|
|
6
|
+
* feat: add VIRTUAL data type by @JimmyDaddy in https://github.com/cyjake/leoric/pull/289
|
|
7
|
+
* fix: create instance dirty check rule fix by @JimmyDaddy in https://github.com/cyjake/leoric/pull/290
|
|
8
|
+
|
|
9
|
+
**Full Changelog**: https://github.com/cyjake/leoric/compare/v2.2.3...v2.3.0
|
|
10
|
+
2.2.3 / 2022-03-01
|
|
11
|
+
==================
|
|
12
|
+
|
|
13
|
+
## What's Changed
|
|
14
|
+
* fix: normalize attribute defaultValue by @cyjake in https://github.com/cyjake/leoric/pull/285
|
|
15
|
+
* fix: instance beforeUpdate hooks should not modify any Raw if there are no Raw assignment in them by @JimmyDaddy in https://github.com/cyjake/leoric/pull/283
|
|
16
|
+
|
|
17
|
+
**Full Changelog**: https://github.com/cyjake/leoric/compare/v2.2.2...v2.2.3
|
|
18
|
+
|
|
19
|
+
2.2.2 / 2022-02-28
|
|
20
|
+
==================
|
|
21
|
+
|
|
22
|
+
## What's Changed
|
|
23
|
+
* fix: tddl gives misleading information_schema.columns.table_name by @cyjake in https://github.com/cyjake/leoric/pull/284
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
**Full Changelog**: https://github.com/cyjake/leoric/compare/v2.2.1...v2.2.2
|
|
27
|
+
|
|
1
28
|
2.2.1 / 2022-02-24
|
|
2
29
|
==================
|
|
3
30
|
|
package/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const sequelize = require('./src/adapters/sequelize');
|
|
|
11
11
|
const { heresql } = require('./src/utils/string');
|
|
12
12
|
const Hint = require('./src/hint');
|
|
13
13
|
const Realm = require('./src/realm');
|
|
14
|
+
const Decorators = require('./src/decorators');
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* @typedef {Object} RawSql
|
|
@@ -38,6 +39,7 @@ const connect = async function connect(opts) {
|
|
|
38
39
|
|
|
39
40
|
Object.assign(Realm.prototype, migrations, { DataTypes });
|
|
40
41
|
Object.assign(Realm, {
|
|
42
|
+
default: Realm,
|
|
41
43
|
connect,
|
|
42
44
|
Bone,
|
|
43
45
|
Collection,
|
|
@@ -47,6 +49,7 @@ Object.assign(Realm, {
|
|
|
47
49
|
sequelize,
|
|
48
50
|
heresql,
|
|
49
51
|
...Hint,
|
|
52
|
+
...Decorators,
|
|
50
53
|
});
|
|
51
54
|
|
|
52
55
|
module.exports = Realm;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leoric",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "JavaScript Object-relational mapping alchemy",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
13
|
"jsdoc": "rm -rf docs/api && jsdoc -c .jsdoc.json -d docs/api -t node_modules/@cara/minami",
|
|
14
|
-
"
|
|
14
|
+
"prepack": "tsc",
|
|
15
|
+
"pretest": "tsc && ./test/prepare.sh",
|
|
15
16
|
"test": "./test/start.sh",
|
|
16
17
|
"test:local": "./test/start.sh",
|
|
17
18
|
"test:unit": "./test/start.sh unit",
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
"debug": "^3.1.0",
|
|
51
52
|
"heredoc": "^1.3.1",
|
|
52
53
|
"pluralize": "^7.0.0",
|
|
54
|
+
"reflect-metadata": "^0.1.13",
|
|
53
55
|
"sqlstring": "^2.3.0",
|
|
54
56
|
"strftime": "^0.10.0",
|
|
55
57
|
"validator": "^13.5.2"
|
|
@@ -72,6 +74,6 @@
|
|
|
72
74
|
"pg": "^8.5.1",
|
|
73
75
|
"sinon": "^10.0.0",
|
|
74
76
|
"sqlite3": "^5.0.2",
|
|
75
|
-
"typescript": "^4.
|
|
77
|
+
"typescript": "^4.6.2"
|
|
76
78
|
}
|
|
77
79
|
}
|
package/src/bone.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
const util = require('util');
|
|
8
8
|
const pluralize = require('pluralize');
|
|
9
9
|
const { executeValidator, LeoricValidateError } = require('./validator');
|
|
10
|
+
require('reflect-metadata');
|
|
10
11
|
|
|
11
12
|
const DataTypes = require('./data_types');
|
|
12
13
|
const Collection = require('./collection');
|
|
@@ -14,8 +15,13 @@ const Spell = require('./spell');
|
|
|
14
15
|
const Raw = require('./raw');
|
|
15
16
|
const { capitalize, camelCase, snakeCase } = require('./utils/string');
|
|
16
17
|
const { hookNames, setupSingleHook } = require('./setup_hooks');
|
|
17
|
-
const {
|
|
18
|
-
|
|
18
|
+
const {
|
|
19
|
+
TIMESTAMP_NAMES,
|
|
20
|
+
LEGACY_TIMESTAMP_COLUMN_MAP,
|
|
21
|
+
ASSOCIATE_METADATA_MAP,
|
|
22
|
+
} = require('./constants');
|
|
23
|
+
|
|
24
|
+
const columnAttributesKey = Symbol('leoric#columns');
|
|
19
25
|
|
|
20
26
|
function looseReadonly(props) {
|
|
21
27
|
return Object.keys(props).reduce((result, name) => {
|
|
@@ -156,7 +162,6 @@ class Bone {
|
|
|
156
162
|
* Get or set attribute value by name. This method is quite similiar to `jQuery.attr()`. If the attribute isn't selected when queried from database, an error will be thrown when accessing it.
|
|
157
163
|
*
|
|
158
164
|
* const post = Post.select('title').first
|
|
159
|
-
* post.content // throw Error('Unset attribute "content"')
|
|
160
165
|
*
|
|
161
166
|
* This is the underlying method of attribute getter/setters:
|
|
162
167
|
*
|
|
@@ -189,7 +194,6 @@ class Bone {
|
|
|
189
194
|
}
|
|
190
195
|
|
|
191
196
|
if (this.#rawUnset.has(name)) {
|
|
192
|
-
logger.warn(`unset attribute "${name}"`);
|
|
193
197
|
return;
|
|
194
198
|
}
|
|
195
199
|
|
|
@@ -199,14 +203,14 @@ class Bone {
|
|
|
199
203
|
}
|
|
200
204
|
|
|
201
205
|
/**
|
|
202
|
-
*
|
|
203
|
-
* clone instance
|
|
206
|
+
* @protected clone instance
|
|
204
207
|
* @param {Bone} target
|
|
205
208
|
* @memberof Bone
|
|
206
209
|
*/
|
|
207
210
|
_clone(target) {
|
|
208
|
-
Object.assign(this
|
|
209
|
-
Object.assign(this
|
|
211
|
+
this.#raw = Object.assign({}, this.getRaw(), target.getRaw());
|
|
212
|
+
this.#rawSaved = Object.assign({}, this.getRawSaved(), target.getRawSaved());
|
|
213
|
+
this.#rawPrevious = Object.assign({}, this.getRawPrevious(), target.getRawPrevious());
|
|
210
214
|
}
|
|
211
215
|
|
|
212
216
|
/**
|
|
@@ -235,6 +239,34 @@ class Bone {
|
|
|
235
239
|
return attributes.hasOwnProperty(name);
|
|
236
240
|
}
|
|
237
241
|
|
|
242
|
+
/**
|
|
243
|
+
* get attributes except virtuals
|
|
244
|
+
*/
|
|
245
|
+
static get columnAttributes() {
|
|
246
|
+
if (this[columnAttributesKey]) return this[columnAttributesKey];
|
|
247
|
+
const { attributes } = this;
|
|
248
|
+
this[columnAttributesKey] = {};
|
|
249
|
+
for (const key in this.attributes) {
|
|
250
|
+
if (!attributes[key].virtual) this[columnAttributesKey][key] = attributes[key];
|
|
251
|
+
}
|
|
252
|
+
return this[columnAttributesKey];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* get actual update/insert columns to avoid empty insert or update
|
|
257
|
+
* @param {Object} data
|
|
258
|
+
* @returns
|
|
259
|
+
*/
|
|
260
|
+
static _getColumns(data) {
|
|
261
|
+
if (!Object.keys(data).length) return data;
|
|
262
|
+
const attributes = this.columnAttributes;
|
|
263
|
+
const res = {};
|
|
264
|
+
for (const key in data) {
|
|
265
|
+
if (attributes[key]) res[key] = data[key];
|
|
266
|
+
}
|
|
267
|
+
return res;
|
|
268
|
+
}
|
|
269
|
+
|
|
238
270
|
getRaw(key) {
|
|
239
271
|
if (key) return this.#raw[key];
|
|
240
272
|
return this.#raw;
|
|
@@ -320,7 +352,6 @@ class Bone {
|
|
|
320
352
|
* post.attributeWas('title') // => 'Leah'
|
|
321
353
|
*/
|
|
322
354
|
attributeWas(name) {
|
|
323
|
-
if (this.#rawUnset.has(name)) throw new Error(`unset attribute "${name}"`);
|
|
324
355
|
const value = this.#rawSaved[name];
|
|
325
356
|
return value == null ? null : value;
|
|
326
357
|
}
|
|
@@ -536,18 +567,18 @@ class Bone {
|
|
|
536
567
|
* Sync changes made in {@link Bone.raw} back to {@link Bone.rawSaved}.
|
|
537
568
|
* @private
|
|
538
569
|
*/
|
|
539
|
-
syncRaw(
|
|
570
|
+
syncRaw() {
|
|
540
571
|
const { attributes } = this.constructor;
|
|
541
572
|
this.isNewRecord = false;
|
|
542
|
-
for (const name of Object.keys(
|
|
573
|
+
for (const name of Object.keys(attributes)) {
|
|
543
574
|
const attribute = attributes[name];
|
|
544
575
|
// Take advantage of uncast/cast to create new copy of value
|
|
545
576
|
const value = attribute.uncast(this.#raw[name]);
|
|
546
577
|
if (this.#rawSaved[name] !== undefined) {
|
|
547
578
|
this.#rawPrevious[name] = this.#rawSaved[name];
|
|
548
|
-
} else if (
|
|
579
|
+
} else if (this.#rawPrevious[name] === undefined && this.#raw[name] != null) {
|
|
549
580
|
// first persisting
|
|
550
|
-
this.#rawPrevious[name] =
|
|
581
|
+
this.#rawPrevious[name] = null;
|
|
551
582
|
}
|
|
552
583
|
this.#rawSaved[name] = attribute.cast(value);
|
|
553
584
|
}
|
|
@@ -579,7 +610,10 @@ class Bone {
|
|
|
579
610
|
if (this.changed(name)) data[name] = this.attribute(name);
|
|
580
611
|
}
|
|
581
612
|
|
|
582
|
-
if (Object.keys(data).length
|
|
613
|
+
if (!Object.keys(Model._getColumns(data)).length) {
|
|
614
|
+
this.syncRaw();
|
|
615
|
+
return Promise.resolve(0);
|
|
616
|
+
}
|
|
583
617
|
|
|
584
618
|
const { createdAt, updatedAt } = Model.timestamps;
|
|
585
619
|
|
|
@@ -659,7 +693,10 @@ class Bone {
|
|
|
659
693
|
}
|
|
660
694
|
}
|
|
661
695
|
|
|
662
|
-
if (Object.keys(changes).length
|
|
696
|
+
if (!Object.keys(Model._getColumns(changes)).length) {
|
|
697
|
+
this.syncRaw();
|
|
698
|
+
return Promise.resolve(0);
|
|
699
|
+
}
|
|
663
700
|
if (this[primaryKey] == null) {
|
|
664
701
|
throw new Error(`unset primary key ${primaryKey}`);
|
|
665
702
|
}
|
|
@@ -680,7 +717,7 @@ class Bone {
|
|
|
680
717
|
for (const key in changes) {
|
|
681
718
|
this.attribute(key, changes[key]);
|
|
682
719
|
}
|
|
683
|
-
this.syncRaw(
|
|
720
|
+
this.syncRaw();
|
|
684
721
|
return result.affectedRows;
|
|
685
722
|
});
|
|
686
723
|
}
|
|
@@ -730,9 +767,15 @@ class Bone {
|
|
|
730
767
|
this._validateAttributes(validateValues);
|
|
731
768
|
}
|
|
732
769
|
|
|
770
|
+
if (!Object.keys(Model._getColumns(data)).length) {
|
|
771
|
+
this.syncRaw();
|
|
772
|
+
return this;
|
|
773
|
+
}
|
|
774
|
+
|
|
733
775
|
const spell = new Spell(Model, opts).$insert(data);
|
|
734
776
|
return spell.later(result => {
|
|
735
777
|
this[primaryKey] = result.insertId;
|
|
778
|
+
// this.#rawSaved[primaryKey] = null;
|
|
736
779
|
this.syncRaw();
|
|
737
780
|
return this;
|
|
738
781
|
});
|
|
@@ -856,7 +899,10 @@ class Bone {
|
|
|
856
899
|
}
|
|
857
900
|
}
|
|
858
901
|
|
|
859
|
-
if (Object.keys(data).length
|
|
902
|
+
if (!Object.keys(Model._getColumns(data)).length) {
|
|
903
|
+
this.syncRaw();
|
|
904
|
+
return Promise.resolve(0);
|
|
905
|
+
}
|
|
860
906
|
|
|
861
907
|
const { createdAt, updatedAt } = Model.timestamps;
|
|
862
908
|
|
|
@@ -977,6 +1023,7 @@ class Bone {
|
|
|
977
1023
|
for (const hookName of hookNames) {
|
|
978
1024
|
if (this[hookName]) setupSingleHook(this, hookName, this[hookName]);
|
|
979
1025
|
}
|
|
1026
|
+
this[columnAttributesKey] = null;
|
|
980
1027
|
}
|
|
981
1028
|
|
|
982
1029
|
/**
|
|
@@ -989,7 +1036,12 @@ class Bone {
|
|
|
989
1036
|
* }
|
|
990
1037
|
* }
|
|
991
1038
|
*/
|
|
992
|
-
static initialize() {
|
|
1039
|
+
static initialize() {
|
|
1040
|
+
for (const [key, metadataKey] of Object.entries(ASSOCIATE_METADATA_MAP)) {
|
|
1041
|
+
const result = Reflect.getMetadata(metadataKey, this);
|
|
1042
|
+
for (const property in result) this[key].call(this, property, result[property]);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
993
1045
|
|
|
994
1046
|
/**
|
|
995
1047
|
* The primary key of the model, in camelCase.
|
|
@@ -1107,6 +1159,7 @@ class Bone {
|
|
|
1107
1159
|
Reflect.deleteProperty(this.prototype, originalName);
|
|
1108
1160
|
this.loadAttribute(newName);
|
|
1109
1161
|
}
|
|
1162
|
+
this[columnAttributesKey] = null;
|
|
1110
1163
|
}
|
|
1111
1164
|
|
|
1112
1165
|
/**
|
|
@@ -1116,15 +1169,16 @@ class Bone {
|
|
|
1116
1169
|
* @param {string} [opts.className]
|
|
1117
1170
|
* @param {string} [opts.foreignKey]
|
|
1118
1171
|
*/
|
|
1119
|
-
static hasOne(name,
|
|
1120
|
-
|
|
1172
|
+
static hasOne(name, options) {
|
|
1173
|
+
options = ({
|
|
1121
1174
|
className: capitalize(name),
|
|
1122
|
-
foreignKey: this.
|
|
1123
|
-
|
|
1175
|
+
foreignKey: camelCase(`${this.name}Id`),
|
|
1176
|
+
...options,
|
|
1177
|
+
});
|
|
1124
1178
|
|
|
1125
|
-
if (
|
|
1179
|
+
if (options.through) options.foreignKey = '';
|
|
1126
1180
|
|
|
1127
|
-
this.associate(name,
|
|
1181
|
+
this.associate(name, options);
|
|
1128
1182
|
}
|
|
1129
1183
|
|
|
1130
1184
|
/**
|
|
@@ -1134,16 +1188,17 @@ class Bone {
|
|
|
1134
1188
|
* @param {string} [opts.className]
|
|
1135
1189
|
* @param {string} [opts.foreignKey]
|
|
1136
1190
|
*/
|
|
1137
|
-
static hasMany(name,
|
|
1138
|
-
|
|
1191
|
+
static hasMany(name, options) {
|
|
1192
|
+
options = {
|
|
1139
1193
|
className: capitalize(pluralize(name, 1)),
|
|
1140
|
-
|
|
1194
|
+
foreignKey: camelCase(`${this.name}Id`),
|
|
1195
|
+
...options,
|
|
1141
1196
|
hasMany: true,
|
|
1142
|
-
}
|
|
1197
|
+
};
|
|
1143
1198
|
|
|
1144
|
-
if (
|
|
1199
|
+
if (options.through) options.foreignKey = '';
|
|
1145
1200
|
|
|
1146
|
-
this.associate(name,
|
|
1201
|
+
this.associate(name, options);
|
|
1147
1202
|
}
|
|
1148
1203
|
|
|
1149
1204
|
/**
|
|
@@ -1153,15 +1208,15 @@ class Bone {
|
|
|
1153
1208
|
* @param {string} [opts.className]
|
|
1154
1209
|
* @param {string} [opts.foreignKey]
|
|
1155
1210
|
*/
|
|
1156
|
-
static belongsTo(name,
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1211
|
+
static belongsTo(name, options = {}) {
|
|
1212
|
+
const { className = capitalize(name) } = options;
|
|
1213
|
+
options = {
|
|
1214
|
+
className,
|
|
1215
|
+
foreignKey: camelCase(`${className}Id`),
|
|
1216
|
+
...options,
|
|
1217
|
+
};
|
|
1163
1218
|
|
|
1164
|
-
this.associate(name,
|
|
1219
|
+
this.associate(name, { ...options, belongsTo: true });
|
|
1165
1220
|
}
|
|
1166
1221
|
|
|
1167
1222
|
/**
|
|
@@ -1181,6 +1236,9 @@ class Bone {
|
|
|
1181
1236
|
const { className } = opts;
|
|
1182
1237
|
const Model = this.models[className];
|
|
1183
1238
|
if (!Model) throw new Error(`unable to find model "${className}"`);
|
|
1239
|
+
if (opts.foreignKey && Model.attributes[opts.foreignKey] && Model.attributes[opts.foreignKey].virtual) {
|
|
1240
|
+
throw new Error(`unable to use virtual attribute ${opts.foreignKey} as foreign key in model ${Model.name}`);
|
|
1241
|
+
}
|
|
1184
1242
|
|
|
1185
1243
|
const { deletedAt } = this.timestamps;
|
|
1186
1244
|
if (Model.attributes[deletedAt] && !opts.where) {
|
|
@@ -1559,6 +1617,7 @@ class Bone {
|
|
|
1559
1617
|
return result;
|
|
1560
1618
|
}, {});
|
|
1561
1619
|
|
|
1620
|
+
this[columnAttributesKey] = null;
|
|
1562
1621
|
Object.defineProperties(this, looseReadonly({ ...hookMethods, attributes, table }));
|
|
1563
1622
|
}
|
|
1564
1623
|
|
|
@@ -1578,7 +1637,7 @@ class Bone {
|
|
|
1578
1637
|
throw new Error('unable to sync model with custom physic tables');
|
|
1579
1638
|
}
|
|
1580
1639
|
|
|
1581
|
-
const { attributes, columns } = this;
|
|
1640
|
+
const { columnAttributes: attributes, columns } = this;
|
|
1582
1641
|
const columnMap = columns.reduce((result, entry) => {
|
|
1583
1642
|
result[entry.columnName] = entry;
|
|
1584
1643
|
return result;
|
package/src/collection.js
CHANGED
|
@@ -63,14 +63,14 @@ class Collection extends Array {
|
|
|
63
63
|
*/
|
|
64
64
|
function instantiatable(spell) {
|
|
65
65
|
const { columns, groups, Model } = spell;
|
|
66
|
-
const {
|
|
66
|
+
const { columnAttributes, tableAlias } = Model;
|
|
67
67
|
|
|
68
68
|
if (groups.length > 0) return false;
|
|
69
69
|
if (columns.length === 0) return true;
|
|
70
70
|
|
|
71
71
|
return columns
|
|
72
72
|
.filter(({ qualifiers }) => (!qualifiers || qualifiers.includes(tableAlias)))
|
|
73
|
-
.every(({ value }) =>
|
|
73
|
+
.every(({ value }) => columnAttributes[value]);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
package/src/constants.js
CHANGED
|
@@ -22,9 +22,16 @@ const LEGACY_TIMESTAMP_COLUMN_MAP = {
|
|
|
22
22
|
|
|
23
23
|
const TIMESTAMP_NAMES = [ 'createdAt', 'updatedAt', 'deletedAt' ];
|
|
24
24
|
|
|
25
|
+
const ASSOCIATE_METADATA_MAP = {
|
|
26
|
+
hasMany: Symbol('hasMany'),
|
|
27
|
+
hasOne: Symbol('hasOne'),
|
|
28
|
+
belongsTo: Symbol('belongsTo'),
|
|
29
|
+
};
|
|
30
|
+
|
|
25
31
|
module.exports = {
|
|
26
32
|
AGGREGATOR_MAP,
|
|
27
33
|
LEGACY_TIMESTAMP_MAP,
|
|
28
34
|
TIMESTAMP_NAMES,
|
|
29
|
-
LEGACY_TIMESTAMP_COLUMN_MAP
|
|
35
|
+
LEGACY_TIMESTAMP_COLUMN_MAP,
|
|
36
|
+
ASSOCIATE_METADATA_MAP
|
|
30
37
|
};
|
package/src/data_types.js
CHANGED
|
@@ -475,6 +475,18 @@ class JSONB extends JSON {
|
|
|
475
475
|
}
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
+
class VIRTUAL extends DataType {
|
|
479
|
+
constructor() {
|
|
480
|
+
super();
|
|
481
|
+
this.dataType = 'virtual';
|
|
482
|
+
this.virtual = true;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
toSqlString() {
|
|
486
|
+
return 'VIRTUAL';
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
478
490
|
const DataTypes = {
|
|
479
491
|
STRING,
|
|
480
492
|
TINYINT,
|
|
@@ -491,6 +503,7 @@ const DataTypes = {
|
|
|
491
503
|
JSONB,
|
|
492
504
|
BINARY,
|
|
493
505
|
VARBINARY,
|
|
506
|
+
VIRTUAL,
|
|
494
507
|
};
|
|
495
508
|
|
|
496
509
|
Object.assign(DataType, DataTypes);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.BelongsTo = exports.HasOne = exports.HasMany = exports.Column = void 0;
|
|
7
|
+
const data_types_1 = __importDefault(require("./data_types"));
|
|
8
|
+
const constants_1 = require("./constants");
|
|
9
|
+
require("reflect-metadata");
|
|
10
|
+
function findType(tsType) {
|
|
11
|
+
const { BIGINT, INTEGER, DATE, STRING, BOOLEAN, } = data_types_1.default;
|
|
12
|
+
switch (tsType) {
|
|
13
|
+
case BigInt:
|
|
14
|
+
return BIGINT;
|
|
15
|
+
case Number:
|
|
16
|
+
return INTEGER;
|
|
17
|
+
case Date:
|
|
18
|
+
return DATE;
|
|
19
|
+
case String:
|
|
20
|
+
return STRING;
|
|
21
|
+
case Boolean:
|
|
22
|
+
return BOOLEAN;
|
|
23
|
+
default:
|
|
24
|
+
throw new Error(`unknown typescript type ${tsType}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function Column(options = {}) {
|
|
28
|
+
return function (target, propertyKey) {
|
|
29
|
+
if (options['prototype'] instanceof data_types_1.default)
|
|
30
|
+
options = { type: options };
|
|
31
|
+
if (!('type' in options)) {
|
|
32
|
+
const tsType = Reflect.getMetadata('design:type', target, propertyKey);
|
|
33
|
+
options['type'] = findType(tsType);
|
|
34
|
+
}
|
|
35
|
+
// narrowing the type of options to ColumnOption
|
|
36
|
+
if (!('type' in options))
|
|
37
|
+
throw new Error(`unknown column options ${options}`);
|
|
38
|
+
// target refers to model prototype, an internal instance of `Bone {}`
|
|
39
|
+
const model = target.constructor;
|
|
40
|
+
const { attributes = (model.attributes = {}) } = model;
|
|
41
|
+
const { name: columnName, ...restOptions } = options;
|
|
42
|
+
attributes[propertyKey] = { ...restOptions, columnName };
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
exports.Column = Column;
|
|
46
|
+
const { hasMany, hasOne, belongsTo } = constants_1.ASSOCIATE_METADATA_MAP;
|
|
47
|
+
function HasMany(options) {
|
|
48
|
+
return function (target, propertyKey) {
|
|
49
|
+
const model = target.constructor;
|
|
50
|
+
Reflect.defineMetadata(hasMany, {
|
|
51
|
+
...Reflect.getMetadata(hasMany, model),
|
|
52
|
+
[propertyKey]: options,
|
|
53
|
+
}, model);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
exports.HasMany = HasMany;
|
|
57
|
+
function HasOne(options) {
|
|
58
|
+
return function (target, propertyKey) {
|
|
59
|
+
const model = target.constructor;
|
|
60
|
+
Reflect.defineMetadata(hasOne, {
|
|
61
|
+
...Reflect.getMetadata(hasOne, model),
|
|
62
|
+
[propertyKey]: options,
|
|
63
|
+
}, model);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
exports.HasOne = HasOne;
|
|
67
|
+
function BelongsTo(options) {
|
|
68
|
+
return function (target, propertyKey) {
|
|
69
|
+
const model = target.constructor;
|
|
70
|
+
Reflect.defineMetadata(belongsTo, {
|
|
71
|
+
...Reflect.getMetadata(belongsTo, model),
|
|
72
|
+
[propertyKey]: options,
|
|
73
|
+
}, model);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
exports.BelongsTo = BelongsTo;
|
|
77
|
+
//# sourceMappingURL=decorators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decorators.js","sourceRoot":"","sources":["decorators.ts"],"names":[],"mappings":";;;;;;AACA,8DAAoC;AACpC,2CAAqD;AACrD,4BAA0B;AAa1B,SAAS,QAAQ,CAAC,MAAM;IACtB,MAAM,EACJ,MAAM,EAAE,OAAO,EACf,IAAI,EACJ,MAAM,EACN,OAAO,GACR,GAAG,oBAAQ,CAAC;IAEb,QAAQ,MAAM,EAAE;QACd,KAAK,MAAM;YACT,OAAO,MAAM,CAAC;QAChB,KAAK,MAAM;YACT,OAAO,OAAO,CAAC;QACjB,KAAK,IAAI;YACP,OAAO,IAAI,CAAC;QACd,KAAK,MAAM;YACT,OAAO,MAAM,CAAC;QAChB,KAAK,OAAO;YACV,OAAO,OAAO,CAAC;QACjB;YACE,MAAM,IAAI,KAAK,CAAC,2BAA2B,MAAM,EAAE,CAAC,CAAC;KACxD;AACH,CAAC;AAED,SAAgB,MAAM,CAAC,UAA8C,EAAE;IACrE,OAAO,UAAS,MAAY,EAAE,WAAmB;QAC/C,IAAI,OAAO,CAAC,WAAW,CAAC,YAAY,oBAAQ;YAAE,OAAO,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QAE1E,IAAI,CAAC,CAAC,MAAM,IAAI,OAAO,CAAC,EAAE;YACxB,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;YACvE,OAAO,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;SACpC;QAED,gDAAgD;QAChD,IAAI,CAAC,CAAC,MAAM,IAAI,OAAO,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,OAAO,EAAE,CAAC,CAAC;QAE/E,sEAAsE;QACtE,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC;QACjC,MAAM,EAAE,UAAU,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,EAAE,CAAC,EAAE,GAAG,KAAK,CAAC;QACvD,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,WAAW,EAAE,GAAG,OAAO,CAAC;QACrD,UAAU,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,WAAW,EAAE,UAAU,EAAE,CAAC;IAC3D,CAAC,CAAC;AACJ,CAAC;AAlBD,wBAkBC;AAOD,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,kCAAsB,CAAC;AAE9D,SAAgB,OAAO,CAAC,OAA0B;IAChD,OAAO,UAAS,MAAY,EAAE,WAAmB;QAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC;QACjC,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE;YAC9B,GAAG,OAAO,CAAC,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC;YACtC,CAAC,WAAW,CAAC,EAAE,OAAO;SACvB,EAAE,KAAK,CAAC,CAAC;IACZ,CAAC,CAAA;AACH,CAAC;AARD,0BAQC;AAED,SAAgB,MAAM,CAAC,OAA0B;IAC/C,OAAO,UAAS,MAAY,EAAE,WAAmB;QAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC;QACjC,OAAO,CAAC,cAAc,CAAC,MAAM,EAAE;YAC7B,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC;YACrC,CAAC,WAAW,CAAC,EAAE,OAAO;SACvB,EAAE,KAAK,CAAC,CAAC;IACZ,CAAC,CAAA;AACH,CAAC;AARD,wBAQC;AAED,SAAgB,SAAS,CAAC,OAA0B;IAClD,OAAO,UAAS,MAAY,EAAE,WAAmB;QAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC;QACjC,OAAO,CAAC,cAAc,CAAC,SAAS,EAAE;YAChC,GAAG,OAAO,CAAC,WAAW,CAAC,SAAS,EAAE,KAAK,CAAC;YACxC,CAAC,WAAW,CAAC,EAAE,OAAO;SACvB,EAAE,KAAK,CAAC,CAAC;IACZ,CAAC,CAAA;AACH,CAAC;AARD,8BAQC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import Bone from './bone';
|
|
2
|
+
import DataType from './data_types';
|
|
3
|
+
import { ASSOCIATE_METADATA_MAP } from './constants';
|
|
4
|
+
import 'reflect-metadata';
|
|
5
|
+
|
|
6
|
+
type DataTypes<T> = {
|
|
7
|
+
[Property in keyof T as Exclude<Property, "toSqlString">]: T[Property];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ColumnOption {
|
|
11
|
+
type?: DataTypes<DataType>;
|
|
12
|
+
name?: string;
|
|
13
|
+
defaultValue?: null | boolean | number | string | Date | JSON;
|
|
14
|
+
allowNull?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function findType(tsType) {
|
|
18
|
+
const {
|
|
19
|
+
BIGINT, INTEGER,
|
|
20
|
+
DATE,
|
|
21
|
+
STRING,
|
|
22
|
+
BOOLEAN,
|
|
23
|
+
} = DataType;
|
|
24
|
+
|
|
25
|
+
switch (tsType) {
|
|
26
|
+
case BigInt:
|
|
27
|
+
return BIGINT;
|
|
28
|
+
case Number:
|
|
29
|
+
return INTEGER;
|
|
30
|
+
case Date:
|
|
31
|
+
return DATE;
|
|
32
|
+
case String:
|
|
33
|
+
return STRING;
|
|
34
|
+
case Boolean:
|
|
35
|
+
return BOOLEAN;
|
|
36
|
+
default:
|
|
37
|
+
throw new Error(`unknown typescript type ${tsType}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function Column(options: ColumnOption | DataTypes<DataType> = {}) {
|
|
42
|
+
return function(target: Bone, propertyKey: string) {
|
|
43
|
+
if (options['prototype'] instanceof DataType) options = { type: options };
|
|
44
|
+
|
|
45
|
+
if (!('type' in options)) {
|
|
46
|
+
const tsType = Reflect.getMetadata('design:type', target, propertyKey);
|
|
47
|
+
options['type'] = findType(tsType);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// narrowing the type of options to ColumnOption
|
|
51
|
+
if (!('type' in options)) throw new Error(`unknown column options ${options}`);
|
|
52
|
+
|
|
53
|
+
// target refers to model prototype, an internal instance of `Bone {}`
|
|
54
|
+
const model = target.constructor;
|
|
55
|
+
const { attributes = (model.attributes = {}) } = model;
|
|
56
|
+
const { name: columnName, ...restOptions } = options;
|
|
57
|
+
attributes[propertyKey] = { ...restOptions, columnName };
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface AssociateOptions {
|
|
62
|
+
className?: string;
|
|
63
|
+
foreignKey?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { hasMany, hasOne, belongsTo } = ASSOCIATE_METADATA_MAP;
|
|
67
|
+
|
|
68
|
+
export function HasMany(options?: AssociateOptions) {
|
|
69
|
+
return function(target: Bone, propertyKey: string) {
|
|
70
|
+
const model = target.constructor;
|
|
71
|
+
Reflect.defineMetadata(hasMany, {
|
|
72
|
+
...Reflect.getMetadata(hasMany, model),
|
|
73
|
+
[propertyKey]: options,
|
|
74
|
+
}, model);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function HasOne(options?: AssociateOptions) {
|
|
79
|
+
return function(target: Bone, propertyKey: string) {
|
|
80
|
+
const model = target.constructor;
|
|
81
|
+
Reflect.defineMetadata(hasOne, {
|
|
82
|
+
...Reflect.getMetadata(hasOne, model),
|
|
83
|
+
[propertyKey]: options,
|
|
84
|
+
}, model);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function BelongsTo(options?: AssociateOptions) {
|
|
89
|
+
return function(target: Bone, propertyKey: string) {
|
|
90
|
+
const model = target.constructor;
|
|
91
|
+
Reflect.defineMetadata(belongsTo, {
|
|
92
|
+
...Reflect.getMetadata(belongsTo, model),
|
|
93
|
+
[propertyKey]: options,
|
|
94
|
+
}, model);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -9,6 +9,7 @@ const { snakeCase } = require('../../utils/string');
|
|
|
9
9
|
* @param {string} dataType
|
|
10
10
|
*/
|
|
11
11
|
function findJsType(DataTypes, type, dataType) {
|
|
12
|
+
if (type instanceof DataTypes.VIRTUAL) return '';
|
|
12
13
|
if (type instanceof DataTypes.BOOLEAN) return Boolean;
|
|
13
14
|
if (type instanceof DataTypes.JSON) return JSON;
|
|
14
15
|
if (type instanceof DataTypes.BINARY || type instanceof DataTypes.BLOB) {
|
|
@@ -99,17 +100,26 @@ class Attribute {
|
|
|
99
100
|
}
|
|
100
101
|
const type = createType(DataTypes, params);
|
|
101
102
|
const dataType = params.dataType || type.dataType;
|
|
103
|
+
let { defaultValue = null } = params;
|
|
104
|
+
try {
|
|
105
|
+
// normalize column defaults like `'0'` or `CURRENT_TIMESTAMP`
|
|
106
|
+
defaultValue = type.cast(type.uncast(defaultValue));
|
|
107
|
+
} catch {
|
|
108
|
+
defaultValue = null;
|
|
109
|
+
}
|
|
110
|
+
|
|
102
111
|
Object.assign(this, {
|
|
103
112
|
name,
|
|
104
|
-
columnName,
|
|
105
113
|
primaryKey: false,
|
|
106
|
-
defaultValue: null,
|
|
107
114
|
allowNull: !params.primaryKey,
|
|
108
|
-
columnType: type.toSqlString().toLowerCase(),
|
|
109
115
|
...params,
|
|
116
|
+
columnName: params.columnName || columnName,
|
|
117
|
+
columnType: type.toSqlString().toLowerCase(),
|
|
110
118
|
type,
|
|
119
|
+
defaultValue,
|
|
111
120
|
dataType,
|
|
112
121
|
jsType: findJsType(DataTypes, type, dataType),
|
|
122
|
+
virtual: type.virtual,
|
|
113
123
|
});
|
|
114
124
|
}
|
|
115
125
|
|
|
@@ -146,7 +146,7 @@ function qualify(spell) {
|
|
|
146
146
|
const baseName = Model.tableAlias;
|
|
147
147
|
const clarify = node => {
|
|
148
148
|
if (node.type === 'id' && !node.qualifiers) {
|
|
149
|
-
if (Model.
|
|
149
|
+
if (Model.columnAttributes[node.value]) node.qualifiers = [baseName];
|
|
150
150
|
}
|
|
151
151
|
};
|
|
152
152
|
|
|
@@ -335,7 +335,7 @@ function formatDelete(spell) {
|
|
|
335
335
|
* @param {Spell} spell
|
|
336
336
|
*/
|
|
337
337
|
function formatInsert(spell) {
|
|
338
|
-
const { Model, sets,
|
|
338
|
+
const { Model, sets, columnAttributes: optAttrs, updateOnDuplicate } = spell;
|
|
339
339
|
const { shardingKey } = Model;
|
|
340
340
|
const { createdAt } = Model.timestamps;
|
|
341
341
|
const { escapeId } = Model.driver;
|
|
@@ -345,22 +345,22 @@ function formatInsert(spell) {
|
|
|
345
345
|
let values = [];
|
|
346
346
|
let placeholders = [];
|
|
347
347
|
if (Array.isArray(sets)) {
|
|
348
|
-
// merge records to get the big picture of involved
|
|
348
|
+
// merge records to get the big picture of involved columnAttributes
|
|
349
349
|
const involved = sets.reduce((result, entry) => {
|
|
350
350
|
return Object.assign(result, entry);
|
|
351
351
|
}, {});
|
|
352
|
-
const
|
|
352
|
+
const columnAttributes = [];
|
|
353
353
|
if (optAttrs) {
|
|
354
354
|
for (const name in optAttrs) {
|
|
355
|
-
if (involved.hasOwnProperty(name))
|
|
355
|
+
if (involved.hasOwnProperty(name)) columnAttributes.push(columnAttributes[name]);
|
|
356
356
|
}
|
|
357
357
|
} else {
|
|
358
358
|
for (const name in involved) {
|
|
359
|
-
|
|
359
|
+
columnAttributes.push(Model.columnAttributes[name]);
|
|
360
360
|
}
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
-
for (const entry of
|
|
363
|
+
for (const entry of columnAttributes) {
|
|
364
364
|
columns.push(entry.columnName);
|
|
365
365
|
if (updateOnDuplicate && createdAt && entry.name === createdAt
|
|
366
366
|
&& !(Array.isArray(updateOnDuplicate) && updateOnDuplicate.includes(createdAt))) continue;
|
|
@@ -371,11 +371,11 @@ function formatInsert(spell) {
|
|
|
371
371
|
if (shardingKey && entry[shardingKey] == null) {
|
|
372
372
|
throw new Error(`Sharding key ${Model.table}.${shardingKey} cannot be NULL.`);
|
|
373
373
|
}
|
|
374
|
-
for (const attribute of
|
|
374
|
+
for (const attribute of columnAttributes) {
|
|
375
375
|
const { name } = attribute;
|
|
376
376
|
values.push(entry[name]);
|
|
377
377
|
}
|
|
378
|
-
placeholders.push(`(${new Array(
|
|
378
|
+
placeholders.push(`(${new Array(columnAttributes.length).fill('?').join(',')})`);
|
|
379
379
|
}
|
|
380
380
|
|
|
381
381
|
} else {
|
|
@@ -488,7 +488,7 @@ function formatUpdate(spell) {
|
|
|
488
488
|
function formatUpdateOnDuplicate(spell, columns) {
|
|
489
489
|
const { updateOnDuplicate, uniqueKeys, Model } = spell;
|
|
490
490
|
if (!updateOnDuplicate) return '';
|
|
491
|
-
const {
|
|
491
|
+
const { columnAttributes, primaryColumn } = Model;
|
|
492
492
|
const { escapeId } = Model.driver;
|
|
493
493
|
const actualUniqueKeys = [];
|
|
494
494
|
|
|
@@ -499,9 +499,9 @@ function formatUpdateOnDuplicate(spell, columns) {
|
|
|
499
499
|
} else {
|
|
500
500
|
// conflict_target must be unique
|
|
501
501
|
// get all unique keys
|
|
502
|
-
if (
|
|
503
|
-
for (const key in
|
|
504
|
-
const att =
|
|
502
|
+
if (columnAttributes) {
|
|
503
|
+
for (const key in columnAttributes) {
|
|
504
|
+
const att = columnAttributes[key];
|
|
505
505
|
// use the first unique key
|
|
506
506
|
if (att.unique) {
|
|
507
507
|
actualUniqueKeys.push(escapeId(att.columnName));
|
|
@@ -515,9 +515,9 @@ function formatUpdateOnDuplicate(spell, columns) {
|
|
|
515
515
|
}
|
|
516
516
|
|
|
517
517
|
if (Array.isArray(updateOnDuplicate) && updateOnDuplicate.length) {
|
|
518
|
-
columns = updateOnDuplicate.map(column => (
|
|
518
|
+
columns = updateOnDuplicate.map(column => (columnAttributes[column] && columnAttributes[column].columnName )|| column);
|
|
519
519
|
} else if (!columns.length) {
|
|
520
|
-
columns = Object.values(
|
|
520
|
+
columns = Object.values(columnAttributes).map(({ columnName }) => columnName);
|
|
521
521
|
}
|
|
522
522
|
const updateKeys = columns.map((column) => `${escapeId(column)}=EXCLUDED.${escapeId(column)}`);
|
|
523
523
|
|
|
@@ -99,8 +99,8 @@ class MysqlDriver extends AbstractDriver {
|
|
|
99
99
|
if (!opts.connection) connection.release();
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
logger.tryLogQuery(sql, calculateDuration(start), opts);
|
|
103
102
|
const [ results, fields ] = result;
|
|
103
|
+
logger.tryLogQuery(sql, calculateDuration(start), opts, results);
|
|
104
104
|
if (fields) return { rows: results, fields };
|
|
105
105
|
return results;
|
|
106
106
|
}
|
|
@@ -37,19 +37,19 @@ module.exports = {
|
|
|
37
37
|
const { updateOnDuplicate, Model } = spell;
|
|
38
38
|
if (!updateOnDuplicate) return null;
|
|
39
39
|
const { escapeId } = Model.driver;
|
|
40
|
-
const {
|
|
40
|
+
const { columnAttributes, primaryColumn } = Model;
|
|
41
41
|
|
|
42
42
|
if (Array.isArray(updateOnDuplicate) && updateOnDuplicate.length) {
|
|
43
|
-
columns = updateOnDuplicate.map(column => (
|
|
43
|
+
columns = updateOnDuplicate.map(column => (columnAttributes[column] && columnAttributes[column].columnName ) || column)
|
|
44
44
|
.filter(column => column !== primaryColumn);
|
|
45
45
|
} else if (!columns.length) {
|
|
46
|
-
columns = Object.values(
|
|
46
|
+
columns = Object.values(columnAttributes).map(attribute => attribute.columnName).filter(column => column !== primaryColumn);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
const sets = [];
|
|
50
50
|
// Make sure the correct LAST_INSERT_ID is returned.
|
|
51
51
|
// - https://stackoverflow.com/questions/778534/mysql-on-duplicate-key-last-insert-id
|
|
52
|
-
// if insert
|
|
52
|
+
// if insert columnAttributes include primary column, `primaryKey = LAST_INSERT_ID(primaryKey)` is not need any more
|
|
53
53
|
if (!columns.includes(primaryColumn)) {
|
|
54
54
|
sets.push(`${escapeId(primaryColumn)} = LAST_INSERT_ID(${escapeId(primaryColumn)})`);
|
|
55
55
|
}
|
|
@@ -143,7 +143,7 @@ class PostgresDriver extends AbstractDriver {
|
|
|
143
143
|
if (!spell.connection) connection.release();
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
logger.tryLogQuery(formatted, calculateDuration(start), spell);
|
|
146
|
+
logger.tryLogQuery(formatted, calculateDuration(start), spell, result);
|
|
147
147
|
return result;
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -8,7 +8,7 @@ function renameSelectExpr(spell) {
|
|
|
8
8
|
|
|
9
9
|
for (const token of columns) {
|
|
10
10
|
if (token.type == 'id') {
|
|
11
|
-
if (!token.qualifiers && Model.
|
|
11
|
+
if (!token.qualifiers && Model.columnAttributes[token.value]) {
|
|
12
12
|
token.qualifiers = [Model.tableAlias];
|
|
13
13
|
}
|
|
14
14
|
whitelist.add(token.qualifiers[0]);
|
package/src/expr_formatter.js
CHANGED
|
@@ -203,6 +203,9 @@ function coerceLiteral(spell, ast) {
|
|
|
203
203
|
const attribute = model && model.attributes[firstArg.value];
|
|
204
204
|
|
|
205
205
|
if (!attribute) return;
|
|
206
|
+
if (attribute.virtual) {
|
|
207
|
+
throw new Error(`unable to use virtual attribute ${attribute.name} in model ${model.name}`);
|
|
208
|
+
}
|
|
206
209
|
|
|
207
210
|
for (const arg of args.slice(1)) {
|
|
208
211
|
if (arg.type === 'literal') {
|
package/src/realm.js
CHANGED
|
@@ -75,7 +75,7 @@ async function loadModels(Spine, models, opts) {
|
|
|
75
75
|
const schemaInfo = await Spine.driver.querySchemaInfo(database, tables);
|
|
76
76
|
|
|
77
77
|
for (const model of models) {
|
|
78
|
-
const columns = schemaInfo[model.physicTable];
|
|
78
|
+
const columns = schemaInfo[model.physicTable] || schemaInfo[model.table];
|
|
79
79
|
if (!model.attributes) initAttributes(model, columns);
|
|
80
80
|
model.load(columns);
|
|
81
81
|
Spine.models[model.name] = model;
|
package/src/setup_hooks.js
CHANGED
|
@@ -129,9 +129,28 @@ function addHook(target, hookName, func) {
|
|
|
129
129
|
if (useHooks && type === hookType.BEFORE) {
|
|
130
130
|
// this.change(key) or this.attributeChanged(key) should work at before update
|
|
131
131
|
if (method === 'update' && typeof arguments[0] === 'object' && !arguments[0] != null) {
|
|
132
|
-
|
|
132
|
+
const values = arguments[0];
|
|
133
|
+
const fields = arguments[1] && arguments[1].fields && arguments[1].fields.length? arguments[1].fields : [];
|
|
134
|
+
const originalRaw = {};
|
|
135
|
+
const changeRaw = {};
|
|
136
|
+
for (const name in values) {
|
|
137
|
+
if ((!fields.length || fields.includes(name)) && this.hasAttribute(name)) {
|
|
138
|
+
originalRaw[name] = this.attribute(name);
|
|
139
|
+
this[name] = values[name];
|
|
140
|
+
changeRaw[name] = this.attribute(name);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
await func.apply(this, args);
|
|
144
|
+
// revert instance after before hooks
|
|
145
|
+
Object.keys(originalRaw).forEach((key) => {
|
|
146
|
+
const current = this.attribute(key);
|
|
147
|
+
// raw[key] may changed in beforeUpdate hooks
|
|
148
|
+
if (current !== originalRaw[key] && current !== changeRaw[key]) return;
|
|
149
|
+
this.attribute(key, originalRaw[key]);
|
|
150
|
+
});
|
|
151
|
+
} else {
|
|
152
|
+
await func.apply(this, args);
|
|
133
153
|
}
|
|
134
|
-
await func.apply(this, args);
|
|
135
154
|
}
|
|
136
155
|
const res = await instanceOriginFunc.call(this, ...arguments);
|
|
137
156
|
if (useHooks && type === hookType.AFTER) {
|
package/src/spell.js
CHANGED
|
@@ -13,30 +13,53 @@ const { parseObject } = require('./query_object');
|
|
|
13
13
|
const Raw = require('./raw');
|
|
14
14
|
const { AGGREGATOR_MAP } = require('./constants');
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* check condition to avoid use virtual fields as where condtions
|
|
18
|
+
* @param {Bone} Model
|
|
19
|
+
* @param {Array<Object>} conds
|
|
20
|
+
*/
|
|
21
|
+
function checkCond(Model, conds) {
|
|
22
|
+
if (Array.isArray(conds)) {
|
|
23
|
+
for (const cond of conds) {
|
|
24
|
+
if (cond.type === 'id' && cond.value != null) {
|
|
25
|
+
if (Model.attributes[cond.value] && Model.attributes[cond.value].virtual) {
|
|
26
|
+
throw new Error(`unable to use virtual attribute ${cond.value} as condition in model ${Model.name}`);
|
|
27
|
+
}
|
|
28
|
+
} else if (cond.type === 'op' && cond.args && cond.args.length) {
|
|
29
|
+
checkCond(Model, cond.args);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
/**
|
|
17
36
|
* Parse condition expressions
|
|
18
37
|
* @example
|
|
19
|
-
* parseConditions({ foo: { $op: value } });
|
|
20
|
-
* parseConditions('foo = ?', value);
|
|
38
|
+
* parseConditions(Model, { foo: { $op: value } });
|
|
39
|
+
* parseConditions(Model, 'foo = ?', value);
|
|
40
|
+
* @param {Bone} Model
|
|
21
41
|
* @param {(string|Object)} conditions
|
|
22
42
|
* @param {...*} values
|
|
23
43
|
* @returns {Array}
|
|
24
44
|
*/
|
|
25
|
-
function parseConditions(conditions, ...values) {
|
|
45
|
+
function parseConditions(Model, conditions, ...values) {
|
|
26
46
|
if (conditions instanceof Raw) return [ conditions ];
|
|
47
|
+
let conds;
|
|
27
48
|
if (isPlainObject(conditions)) {
|
|
28
|
-
|
|
49
|
+
conds = parseObject(conditions);
|
|
29
50
|
} else if (typeof conditions == 'string') {
|
|
30
|
-
|
|
51
|
+
conds = [parseExpr(conditions, ...values)];
|
|
31
52
|
} else {
|
|
32
53
|
throw new Error(`unexpected conditions ${conditions}`);
|
|
33
54
|
}
|
|
55
|
+
checkCond(Model, conds);
|
|
56
|
+
return conds;
|
|
34
57
|
}
|
|
35
58
|
|
|
36
59
|
function parseSelect(spell, ...names) {
|
|
37
60
|
const { joins, Model } = spell;
|
|
38
61
|
if (typeof names[0] === 'function') {
|
|
39
|
-
names = Object.keys(Model.
|
|
62
|
+
names = Object.keys(Model.columnAttributes).filter(names[0]);
|
|
40
63
|
} else {
|
|
41
64
|
names = names.reduce((result, name) => result.concat(name), []);
|
|
42
65
|
}
|
|
@@ -53,7 +76,10 @@ function parseSelect(spell, ...names) {
|
|
|
53
76
|
if (type != 'id') return;
|
|
54
77
|
const qualifier = qualifiers && qualifiers[0];
|
|
55
78
|
const model = qualifier && joins && (qualifier in joins) ? joins[qualifier].Model : Model;
|
|
56
|
-
if (!model.
|
|
79
|
+
if (!model.columnAttributes[value]) {
|
|
80
|
+
if (model.attributes[value]) {
|
|
81
|
+
throw new Error(`unable to use virtual attribute ${value} as field in model ${model.name}`);
|
|
82
|
+
}
|
|
57
83
|
throw new Error(`unable to find attribute ${value} in model ${model.name}`);
|
|
58
84
|
}
|
|
59
85
|
});
|
|
@@ -63,9 +89,9 @@ function parseSelect(spell, ...names) {
|
|
|
63
89
|
}
|
|
64
90
|
|
|
65
91
|
/**
|
|
66
|
-
* Translate key-value pairs of
|
|
92
|
+
* Translate key-value pairs of columnAttributes into key-value pairs of columns. Get ready for the SET part when generating SQL.
|
|
67
93
|
* @param {Spell} spell
|
|
68
|
-
* @param {Object} obj - key-value pairs of
|
|
94
|
+
* @param {Object} obj - key-value pairs of columnAttributes
|
|
69
95
|
* @param {boolean} strict - check attribute exist or not
|
|
70
96
|
* @returns {Object}
|
|
71
97
|
*/
|
|
@@ -73,11 +99,11 @@ function formatValueSet(spell, obj, strict = true) {
|
|
|
73
99
|
const { Model } = spell;
|
|
74
100
|
const sets = {};
|
|
75
101
|
for (const name in obj) {
|
|
76
|
-
const attribute = Model.
|
|
102
|
+
const attribute = Model.columnAttributes[name];
|
|
77
103
|
const value = obj[name];
|
|
78
104
|
|
|
79
|
-
if (!attribute
|
|
80
|
-
|
|
105
|
+
if (!attribute) {
|
|
106
|
+
continue;
|
|
81
107
|
}
|
|
82
108
|
|
|
83
109
|
// raw sql don't need to uncast
|
|
@@ -91,9 +117,9 @@ function formatValueSet(spell, obj, strict = true) {
|
|
|
91
117
|
}
|
|
92
118
|
|
|
93
119
|
/**
|
|
94
|
-
* Translate key-value pairs of
|
|
120
|
+
* Translate key-value pairs of columnAttributes into key-value pairs of columns. Get ready for the SET part when generating SQL.
|
|
95
121
|
* @param {Spell} spell
|
|
96
|
-
* @param {Object|Array} obj - key-value pairs of
|
|
122
|
+
* @param {Object|Array} obj - key-value pairs of columnAttributes
|
|
97
123
|
*/
|
|
98
124
|
function parseSet(spell, obj) {
|
|
99
125
|
let sets;
|
|
@@ -139,7 +165,7 @@ function joinOnConditions(spell, BaseModel, baseName, refName, { where, associat
|
|
|
139
165
|
};
|
|
140
166
|
if (!where) where = association.where;
|
|
141
167
|
if (where) {
|
|
142
|
-
const whereConditions = walkExpr(parseConditions(where)[0], node => {
|
|
168
|
+
const whereConditions = walkExpr(parseConditions(BaseModel, where)[0], node => {
|
|
143
169
|
if (node.type == 'id') node.qualifiers = [refName];
|
|
144
170
|
});
|
|
145
171
|
return { type: 'op', name: 'and', args: [ onConditions, whereConditions ] };
|
|
@@ -212,7 +238,7 @@ function joinAssociation(spell, BaseModel, baseName, refName, opts = {}) {
|
|
|
212
238
|
const columns = parseSelect({ Model: RefModel }, select);
|
|
213
239
|
for (const token of columns) {
|
|
214
240
|
walkExpr(token, node => {
|
|
215
|
-
if (node.type === 'id' && !node.qualifiers && RefModel.
|
|
241
|
+
if (node.type === 'id' && !node.qualifiers && RefModel.columnAttributes[node.value]) {
|
|
216
242
|
node.qualifiers = [refName];
|
|
217
243
|
}
|
|
218
244
|
});
|
|
@@ -284,7 +310,7 @@ class Spell {
|
|
|
284
310
|
/**
|
|
285
311
|
* Create a spell.
|
|
286
312
|
* @param {Model} Model - A sub class of {@link Bone}.
|
|
287
|
-
* @param {Object} opts - Extra
|
|
313
|
+
* @param {Object} opts - Extra columnAttributes to be set.
|
|
288
314
|
*/
|
|
289
315
|
constructor(Model, opts = {}) {
|
|
290
316
|
if (Model.synchronized == null) {
|
|
@@ -299,7 +325,7 @@ class Spell {
|
|
|
299
325
|
|
|
300
326
|
const { deletedAt } = Model.timestamps;
|
|
301
327
|
// FIXME: need to implement paranoid mode
|
|
302
|
-
if (Model.
|
|
328
|
+
if (Model.columnAttributes[deletedAt] && opts.paranoid !== false) {
|
|
303
329
|
scopes.push(scopeDeletedAt);
|
|
304
330
|
}
|
|
305
331
|
|
|
@@ -502,7 +528,7 @@ class Spell {
|
|
|
502
528
|
}
|
|
503
529
|
|
|
504
530
|
/**
|
|
505
|
-
* Whitelist
|
|
531
|
+
* Whitelist columnAttributes to select. Can be called repeatedly to select more columnAttributes.
|
|
506
532
|
* @param {...string} names
|
|
507
533
|
* @example
|
|
508
534
|
* .select('title');
|
|
@@ -531,7 +557,7 @@ class Spell {
|
|
|
531
557
|
const { timestamps } = Model;
|
|
532
558
|
this.command = 'update';
|
|
533
559
|
if (!Number.isFinite(by)) throw new Error(`unexpected increment value ${by}`);
|
|
534
|
-
if (!Model.
|
|
560
|
+
if (!Model.columnAttributes.hasOwnProperty(name)) {
|
|
535
561
|
throw new Error(`undefined attribute "${name}"`);
|
|
536
562
|
}
|
|
537
563
|
|
|
@@ -590,7 +616,8 @@ class Spell {
|
|
|
590
616
|
* @returns {Spell}
|
|
591
617
|
*/
|
|
592
618
|
$where(conditions, ...values) {
|
|
593
|
-
this.
|
|
619
|
+
const Model = this.Model;
|
|
620
|
+
this.whereConditions.push(...parseConditions(Model, conditions, ...values));
|
|
594
621
|
return this;
|
|
595
622
|
}
|
|
596
623
|
|
|
@@ -600,15 +627,16 @@ class Spell {
|
|
|
600
627
|
const combined = whereConditions.slice(1).reduce((result, condition) => {
|
|
601
628
|
return { type: 'op', name: 'and', args: [result, condition] };
|
|
602
629
|
}, whereConditions[0]);
|
|
630
|
+
const Model = this.Model;
|
|
603
631
|
this.whereConditions = [
|
|
604
632
|
{ type: 'op', name: 'or', args:
|
|
605
|
-
[combined, ...parseConditions(conditions, ...values)] }
|
|
633
|
+
[combined, ...parseConditions(Model, conditions, ...values)] }
|
|
606
634
|
];
|
|
607
635
|
return this;
|
|
608
636
|
}
|
|
609
637
|
|
|
610
638
|
/**
|
|
611
|
-
* Set GROUP BY
|
|
639
|
+
* Set GROUP BY columnAttributes. `select_expr` with `AS` is supported, hence following expressions have the same effect:
|
|
612
640
|
*
|
|
613
641
|
* .select('YEAR(createdAt)) AS year').group('year');
|
|
614
642
|
*
|
|
@@ -619,9 +647,12 @@ class Spell {
|
|
|
619
647
|
* @returns {Spell}
|
|
620
648
|
*/
|
|
621
649
|
$group(...names) {
|
|
622
|
-
const { columns, groups } = this;
|
|
650
|
+
const { columns, groups, Model } = this;
|
|
623
651
|
|
|
624
652
|
for (const name of names) {
|
|
653
|
+
if (Model.attributes[name] && Model.attributes[name].virtual) {
|
|
654
|
+
throw new Error(`unable to use virtual attribute ${name} as group column in model ${Model.name}`);
|
|
655
|
+
}
|
|
625
656
|
const token = parseExpr(name);
|
|
626
657
|
if (token.type === 'alias') {
|
|
627
658
|
groups.push({ type: 'id', value: token.value });
|
|
@@ -666,10 +697,12 @@ class Spell {
|
|
|
666
697
|
});
|
|
667
698
|
}
|
|
668
699
|
else {
|
|
669
|
-
|
|
700
|
+
const order = [
|
|
670
701
|
parseExpr(name),
|
|
671
702
|
direction && direction.toLowerCase() == 'desc' ? 'desc' : 'asc'
|
|
672
|
-
]
|
|
703
|
+
];
|
|
704
|
+
checkCond(this.Model, order);
|
|
705
|
+
this.orders.push(order);
|
|
673
706
|
}
|
|
674
707
|
return this;
|
|
675
708
|
}
|
|
@@ -710,10 +743,11 @@ class Spell {
|
|
|
710
743
|
* @returns {Spell}
|
|
711
744
|
*/
|
|
712
745
|
$having(conditions, ...values) {
|
|
713
|
-
|
|
746
|
+
const Model = this.Model;
|
|
747
|
+
for (const condition of parseConditions(Model, conditions, ...values)) {
|
|
714
748
|
// Postgres can't have alias in HAVING caluse
|
|
715
749
|
// https://stackoverflow.com/questions/32730296/referring-to-a-select-aggregate-column-alias-in-the-having-clause-in-postgres
|
|
716
|
-
if (
|
|
750
|
+
if (Model.driver.type === 'postgres' && !(condition instanceof Raw)) {
|
|
717
751
|
const { value } = condition.args[0];
|
|
718
752
|
for (const column of this.columns) {
|
|
719
753
|
if (column.value === value && column.type === 'alias') {
|
|
@@ -784,7 +818,7 @@ class Spell {
|
|
|
784
818
|
if (qualifier in joins) {
|
|
785
819
|
throw new Error(`invalid join target. ${qualifier} already defined.`);
|
|
786
820
|
}
|
|
787
|
-
joins[qualifier] = { Model, on: parseConditions(onConditions, ...values)[0] };
|
|
821
|
+
joins[qualifier] = { Model, on: parseConditions(Model, onConditions, ...values)[0] };
|
|
788
822
|
return this;
|
|
789
823
|
}
|
|
790
824
|
|
package/types/data_types.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export default class DataType {
|
|
|
20
20
|
static DATE: typeof DATE & INVOKABLE<DATE>;
|
|
21
21
|
static DATEONLY: typeof DATEONLY & INVOKABLE<DATEONLY>;
|
|
22
22
|
static BOOLEAN: typeof BOOLEAN & INVOKABLE<BOOLEAN>;
|
|
23
|
+
static VIRTUAL: typeof VIRTUAL & INVOKABLE<VIRTUAL>;
|
|
24
|
+
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
declare class STRING extends DataType {
|
|
@@ -86,3 +88,7 @@ declare class DATEONLY extends DataType {
|
|
|
86
88
|
declare class BOOLEAN extends DataType {
|
|
87
89
|
dataType: 'boolean'
|
|
88
90
|
}
|
|
91
|
+
|
|
92
|
+
declare class VIRTUAL extends DataType {
|
|
93
|
+
dataType: 'virtual'
|
|
94
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import DataType from './data_types';
|
|
2
2
|
|
|
3
3
|
export { DataType as DataTypes };
|
|
4
|
+
export * from '../src/decorators';
|
|
4
5
|
|
|
5
6
|
type DataTypes<T> = {
|
|
6
7
|
[Property in keyof T as Exclude<Property, "toSqlString">]: T[Property]
|
|
@@ -129,26 +130,27 @@ type OperatorCondition = {
|
|
|
129
130
|
};
|
|
130
131
|
|
|
131
132
|
type WhereConditions<T extends typeof Bone> = {
|
|
132
|
-
[Property in keyof T
|
|
133
|
+
[Property in keyof Extract<InstanceType<T>, Literal>]?: Literal | Literal[] | OperatorCondition;
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
type Values<T extends typeof Bone> = {
|
|
136
|
-
[Property in keyof T
|
|
137
|
+
[Property in keyof Extract<InstanceType<T>, Literal>]?: Literal;
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
type InstanceValues<T> = {
|
|
140
141
|
[Property in keyof Extract<T, Literal>]?: Extract<T, Literal>[Property]
|
|
141
142
|
}
|
|
142
143
|
|
|
143
|
-
interface AttributeMeta {
|
|
144
|
-
|
|
144
|
+
export interface AttributeMeta {
|
|
145
|
+
columnName?: string;
|
|
145
146
|
columnType?: string;
|
|
146
147
|
allowNull?: boolean;
|
|
147
148
|
defaultValue?: Literal;
|
|
148
149
|
primaryKey?: boolean;
|
|
149
150
|
dataType?: string;
|
|
150
151
|
jsType?: Literal;
|
|
151
|
-
type:
|
|
152
|
+
type: DataType;
|
|
153
|
+
toSqlString: () => string;
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
interface RelateOptions {
|
|
@@ -233,7 +235,7 @@ export class Bone {
|
|
|
233
235
|
/**
|
|
234
236
|
* The connected models structured as `{ [model.name]: model }`, e.g. `Bone.model.Post => Post`
|
|
235
237
|
*/
|
|
236
|
-
static
|
|
238
|
+
static models: { [key: string]: typeof Bone };
|
|
237
239
|
|
|
238
240
|
/**
|
|
239
241
|
* The table name of the model, which needs to be specified by the model subclass.
|
|
@@ -468,10 +470,13 @@ export class Bone {
|
|
|
468
470
|
*/
|
|
469
471
|
static truncate(): Promise<void>;
|
|
470
472
|
|
|
473
|
+
static sync(options: SyncOptions): Promise<void>;
|
|
474
|
+
|
|
475
|
+
static initialize(): void;
|
|
476
|
+
|
|
471
477
|
constructor(values: { [key: string]: Literal });
|
|
472
478
|
|
|
473
479
|
/**
|
|
474
|
-
* Get or set attribute value. Getting the value of unset attribute gives an error.
|
|
475
480
|
* @example
|
|
476
481
|
* bone.attribute('foo'); // => 1
|
|
477
482
|
* bone.attribute('foo', 2); // => bone
|
|
@@ -601,6 +606,7 @@ export interface ConnectOptions {
|
|
|
601
606
|
user?: string;
|
|
602
607
|
password?: string;
|
|
603
608
|
database: string;
|
|
609
|
+
charset?: string;
|
|
604
610
|
models?: string | (typeof Bone)[];
|
|
605
611
|
subclass?: boolean;
|
|
606
612
|
}
|