leoric 2.2.3 → 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 +9 -0
- package/index.js +3 -0
- package/package.json +4 -2
- package/src/bone.js +97 -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 +4 -2
- 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/setup_hooks.js +1 -1
- 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,12 @@
|
|
|
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
|
|
1
10
|
2.2.3 / 2022-03-01
|
|
2
11
|
==================
|
|
3
12
|
|
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"
|
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
|
|
|
@@ -204,9 +208,9 @@ class Bone {
|
|
|
204
208
|
* @memberof Bone
|
|
205
209
|
*/
|
|
206
210
|
_clone(target) {
|
|
207
|
-
this.#raw = Object.assign({}, target.getRaw());
|
|
208
|
-
this.#rawSaved = Object.assign({}, target.getRawSaved());
|
|
209
|
-
this.#rawPrevious = Object.assign({}, target.getRawPrevious());
|
|
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
|
}
|
|
@@ -717,7 +754,6 @@ class Bone {
|
|
|
717
754
|
for (const name in attributes) {
|
|
718
755
|
const value = this.attribute(name);
|
|
719
756
|
const { defaultValue } = attributes[name];
|
|
720
|
-
// console.log(attributes[name], name, defaultValue);
|
|
721
757
|
if (value != null) {
|
|
722
758
|
data[name] = value;
|
|
723
759
|
} else if (value === undefined && defaultValue != null) {
|
|
@@ -731,9 +767,15 @@ class Bone {
|
|
|
731
767
|
this._validateAttributes(validateValues);
|
|
732
768
|
}
|
|
733
769
|
|
|
770
|
+
if (!Object.keys(Model._getColumns(data)).length) {
|
|
771
|
+
this.syncRaw();
|
|
772
|
+
return this;
|
|
773
|
+
}
|
|
774
|
+
|
|
734
775
|
const spell = new Spell(Model, opts).$insert(data);
|
|
735
776
|
return spell.later(result => {
|
|
736
777
|
this[primaryKey] = result.insertId;
|
|
778
|
+
// this.#rawSaved[primaryKey] = null;
|
|
737
779
|
this.syncRaw();
|
|
738
780
|
return this;
|
|
739
781
|
});
|
|
@@ -857,7 +899,10 @@ class Bone {
|
|
|
857
899
|
}
|
|
858
900
|
}
|
|
859
901
|
|
|
860
|
-
if (Object.keys(data).length
|
|
902
|
+
if (!Object.keys(Model._getColumns(data)).length) {
|
|
903
|
+
this.syncRaw();
|
|
904
|
+
return Promise.resolve(0);
|
|
905
|
+
}
|
|
861
906
|
|
|
862
907
|
const { createdAt, updatedAt } = Model.timestamps;
|
|
863
908
|
|
|
@@ -978,6 +1023,7 @@ class Bone {
|
|
|
978
1023
|
for (const hookName of hookNames) {
|
|
979
1024
|
if (this[hookName]) setupSingleHook(this, hookName, this[hookName]);
|
|
980
1025
|
}
|
|
1026
|
+
this[columnAttributesKey] = null;
|
|
981
1027
|
}
|
|
982
1028
|
|
|
983
1029
|
/**
|
|
@@ -990,7 +1036,12 @@ class Bone {
|
|
|
990
1036
|
* }
|
|
991
1037
|
* }
|
|
992
1038
|
*/
|
|
993
|
-
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
|
+
}
|
|
994
1045
|
|
|
995
1046
|
/**
|
|
996
1047
|
* The primary key of the model, in camelCase.
|
|
@@ -1108,6 +1159,7 @@ class Bone {
|
|
|
1108
1159
|
Reflect.deleteProperty(this.prototype, originalName);
|
|
1109
1160
|
this.loadAttribute(newName);
|
|
1110
1161
|
}
|
|
1162
|
+
this[columnAttributesKey] = null;
|
|
1111
1163
|
}
|
|
1112
1164
|
|
|
1113
1165
|
/**
|
|
@@ -1117,15 +1169,16 @@ class Bone {
|
|
|
1117
1169
|
* @param {string} [opts.className]
|
|
1118
1170
|
* @param {string} [opts.foreignKey]
|
|
1119
1171
|
*/
|
|
1120
|
-
static hasOne(name,
|
|
1121
|
-
|
|
1172
|
+
static hasOne(name, options) {
|
|
1173
|
+
options = ({
|
|
1122
1174
|
className: capitalize(name),
|
|
1123
|
-
foreignKey: this.
|
|
1124
|
-
|
|
1175
|
+
foreignKey: camelCase(`${this.name}Id`),
|
|
1176
|
+
...options,
|
|
1177
|
+
});
|
|
1125
1178
|
|
|
1126
|
-
if (
|
|
1179
|
+
if (options.through) options.foreignKey = '';
|
|
1127
1180
|
|
|
1128
|
-
this.associate(name,
|
|
1181
|
+
this.associate(name, options);
|
|
1129
1182
|
}
|
|
1130
1183
|
|
|
1131
1184
|
/**
|
|
@@ -1135,16 +1188,17 @@ class Bone {
|
|
|
1135
1188
|
* @param {string} [opts.className]
|
|
1136
1189
|
* @param {string} [opts.foreignKey]
|
|
1137
1190
|
*/
|
|
1138
|
-
static hasMany(name,
|
|
1139
|
-
|
|
1191
|
+
static hasMany(name, options) {
|
|
1192
|
+
options = {
|
|
1140
1193
|
className: capitalize(pluralize(name, 1)),
|
|
1141
|
-
|
|
1194
|
+
foreignKey: camelCase(`${this.name}Id`),
|
|
1195
|
+
...options,
|
|
1142
1196
|
hasMany: true,
|
|
1143
|
-
}
|
|
1197
|
+
};
|
|
1144
1198
|
|
|
1145
|
-
if (
|
|
1199
|
+
if (options.through) options.foreignKey = '';
|
|
1146
1200
|
|
|
1147
|
-
this.associate(name,
|
|
1201
|
+
this.associate(name, options);
|
|
1148
1202
|
}
|
|
1149
1203
|
|
|
1150
1204
|
/**
|
|
@@ -1154,15 +1208,15 @@ class Bone {
|
|
|
1154
1208
|
* @param {string} [opts.className]
|
|
1155
1209
|
* @param {string} [opts.foreignKey]
|
|
1156
1210
|
*/
|
|
1157
|
-
static belongsTo(name,
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1211
|
+
static belongsTo(name, options = {}) {
|
|
1212
|
+
const { className = capitalize(name) } = options;
|
|
1213
|
+
options = {
|
|
1214
|
+
className,
|
|
1215
|
+
foreignKey: camelCase(`${className}Id`),
|
|
1216
|
+
...options,
|
|
1217
|
+
};
|
|
1164
1218
|
|
|
1165
|
-
this.associate(name,
|
|
1219
|
+
this.associate(name, { ...options, belongsTo: true });
|
|
1166
1220
|
}
|
|
1167
1221
|
|
|
1168
1222
|
/**
|
|
@@ -1182,6 +1236,9 @@ class Bone {
|
|
|
1182
1236
|
const { className } = opts;
|
|
1183
1237
|
const Model = this.models[className];
|
|
1184
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
|
+
}
|
|
1185
1242
|
|
|
1186
1243
|
const { deletedAt } = this.timestamps;
|
|
1187
1244
|
if (Model.attributes[deletedAt] && !opts.where) {
|
|
@@ -1560,6 +1617,7 @@ class Bone {
|
|
|
1560
1617
|
return result;
|
|
1561
1618
|
}, {});
|
|
1562
1619
|
|
|
1620
|
+
this[columnAttributesKey] = null;
|
|
1563
1621
|
Object.defineProperties(this, looseReadonly({ ...hookMethods, attributes, table }));
|
|
1564
1622
|
}
|
|
1565
1623
|
|
|
@@ -1579,7 +1637,7 @@ class Bone {
|
|
|
1579
1637
|
throw new Error('unable to sync model with custom physic tables');
|
|
1580
1638
|
}
|
|
1581
1639
|
|
|
1582
|
-
const { attributes, columns } = this;
|
|
1640
|
+
const { columnAttributes: attributes, columns } = this;
|
|
1583
1641
|
const columnMap = columns.reduce((result, entry) => {
|
|
1584
1642
|
result[entry.columnName] = entry;
|
|
1585
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":"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"}
|
|
@@ -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) {
|
|
@@ -109,15 +110,16 @@ class Attribute {
|
|
|
109
110
|
|
|
110
111
|
Object.assign(this, {
|
|
111
112
|
name,
|
|
112
|
-
columnName,
|
|
113
113
|
primaryKey: false,
|
|
114
114
|
allowNull: !params.primaryKey,
|
|
115
|
-
columnType: type.toSqlString().toLowerCase(),
|
|
116
115
|
...params,
|
|
116
|
+
columnName: params.columnName || columnName,
|
|
117
|
+
columnType: type.toSqlString().toLowerCase(),
|
|
117
118
|
type,
|
|
118
119
|
defaultValue,
|
|
119
120
|
dataType,
|
|
120
121
|
jsType: findJsType(DataTypes, type, dataType),
|
|
122
|
+
virtual: type.virtual,
|
|
121
123
|
});
|
|
122
124
|
}
|
|
123
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/setup_hooks.js
CHANGED
|
@@ -134,7 +134,7 @@ function addHook(target, hookName, func) {
|
|
|
134
134
|
const originalRaw = {};
|
|
135
135
|
const changeRaw = {};
|
|
136
136
|
for (const name in values) {
|
|
137
|
-
if (!fields.length || fields.includes(name)) {
|
|
137
|
+
if ((!fields.length || fields.includes(name)) && this.hasAttribute(name)) {
|
|
138
138
|
originalRaw[name] = this.attribute(name);
|
|
139
139
|
this[name] = values[name];
|
|
140
140
|
changeRaw[name] = this.attribute(name);
|
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
|
}
|