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 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.2.1",
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
- "pretest": "./test/prepare.sh",
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.4.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 { logger } = require('./utils/index');
18
- const { TIMESTAMP_NAMES, LEGACY_TIMESTAMP_COLUMN_MAP } = require('./constants');
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.#raw, target.getRaw());
209
- Object.assign(this.#rawSaved, target.getRawSaved());
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(changes) {
570
+ syncRaw() {
540
571
  const { attributes } = this.constructor;
541
572
  this.isNewRecord = false;
542
- for (const name of Object.keys(changes || attributes)) {
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 (!changes && this.#rawPrevious[name] === undefined) {
579
+ } else if (this.#rawPrevious[name] === undefined && this.#raw[name] != null) {
549
580
  // first persisting
550
- this.#rawPrevious[name] = attribute.cast(value);
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 === 0) return Promise.resolve(0);
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 === 0) return Promise.resolve(0);
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(changes);
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 === 0) return Promise.resolve(0);
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, opts) {
1120
- opts = Object.assign({
1172
+ static hasOne(name, options) {
1173
+ options = ({
1121
1174
  className: capitalize(name),
1122
- foreignKey: this.table + 'Id',
1123
- }, opts);
1175
+ foreignKey: camelCase(`${this.name}Id`),
1176
+ ...options,
1177
+ });
1124
1178
 
1125
- if (opts.through) opts.foreignKey = '';
1179
+ if (options.through) options.foreignKey = '';
1126
1180
 
1127
- this.associate(name, opts);
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, opts) {
1138
- opts = Object.assign({
1191
+ static hasMany(name, options) {
1192
+ options = {
1139
1193
  className: capitalize(pluralize(name, 1)),
1140
- }, opts, {
1194
+ foreignKey: camelCase(`${this.name}Id`),
1195
+ ...options,
1141
1196
  hasMany: true,
1142
- });
1197
+ };
1143
1198
 
1144
- if (opts.through) opts.foreignKey = '';
1199
+ if (options.through) options.foreignKey = '';
1145
1200
 
1146
- this.associate(name, opts);
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, opts) {
1157
- opts = Object.assign({
1158
- className: capitalize(name),
1159
- }, opts);
1160
-
1161
- let { className, foreignKey } = opts;
1162
- if (!foreignKey) foreignKey = camelCase(className) + 'Id';
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, Object.assign(opts, { foreignKey, belongsTo: true }));
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 { attributes, tableAlias } = Model;
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 }) => attributes[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) {
@@ -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.attributes[node.value]) node.qualifiers = [baseName];
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, attributes: optAttrs, updateOnDuplicate } = spell;
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 attributes
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 attributes = [];
352
+ const columnAttributes = [];
353
353
  if (optAttrs) {
354
354
  for (const name in optAttrs) {
355
- if (involved.hasOwnProperty(name)) attributes.push(attributes[name]);
355
+ if (involved.hasOwnProperty(name)) columnAttributes.push(columnAttributes[name]);
356
356
  }
357
357
  } else {
358
358
  for (const name in involved) {
359
- attributes.push(Model.attributes[name]);
359
+ columnAttributes.push(Model.columnAttributes[name]);
360
360
  }
361
361
  }
362
362
 
363
- for (const entry of attributes) {
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 attributes) {
374
+ for (const attribute of columnAttributes) {
375
375
  const { name } = attribute;
376
376
  values.push(entry[name]);
377
377
  }
378
- placeholders.push(`(${new Array(attributes.length).fill('?').join(',')})`);
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 { attributes, primaryColumn } = Model;
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 (attributes) {
503
- for (const key in attributes) {
504
- const att = attributes[key];
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 => (attributes[column] && attributes[column].columnName )|| column);
518
+ columns = updateOnDuplicate.map(column => (columnAttributes[column] && columnAttributes[column].columnName )|| column);
519
519
  } else if (!columns.length) {
520
- columns = Object.values(attributes).map(({ columnName }) => columnName);
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 { attributes, primaryColumn } = Model;
40
+ const { columnAttributes, primaryColumn } = Model;
41
41
 
42
42
  if (Array.isArray(updateOnDuplicate) && updateOnDuplicate.length) {
43
- columns = updateOnDuplicate.map(column => (attributes[column] && attributes[column].columnName ) || 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(attributes).map(attribute => attribute.columnName).filter(column => column !== primaryColumn);
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 attributes include primary column, `primaryKey = LAST_INSERT_ID(primaryKey)` is not need any more
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
 
@@ -54,7 +54,7 @@ class SqliteDriver extends AbstractDriver {
54
54
  if (!opts.connection) connection.release();
55
55
  }
56
56
 
57
- logger.tryLogQuery(sql, calculateDuration(start), opts);
57
+ logger.tryLogQuery(sql, calculateDuration(start), opts, result);
58
58
  return result;
59
59
  }
60
60
 
@@ -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.attributes[token.value]) {
11
+ if (!token.qualifiers && Model.columnAttributes[token.value]) {
12
12
  token.qualifiers = [Model.tableAlias];
13
13
  }
14
14
  whitelist.add(token.qualifiers[0]);
@@ -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;
@@ -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
- for (const name in arguments[0]) this[name] = arguments[0][name];
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
- return parseObject(conditions);
49
+ conds = parseObject(conditions);
29
50
  } else if (typeof conditions == 'string') {
30
- return [parseExpr(conditions, ...values)];
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.attributes).filter(names[0]);
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.attributes[value]) {
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 attributes into key-value pairs of columns. Get ready for the SET part when generating SQL.
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 attributes
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.attributes[name];
102
+ const attribute = Model.columnAttributes[name];
77
103
  const value = obj[name];
78
104
 
79
- if (!attribute && strict) {
80
- throw new Error(`Undefined attribute "${name}"`);
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 attributes into key-value pairs of columns. Get ready for the SET part when generating SQL.
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 attributes
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.attributes[node.value]) {
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 attributes to be set.
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.attributes[deletedAt] && opts.paranoid !== false) {
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 attributes to select. Can be called repeatedly to select more attributes.
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.attributes.hasOwnProperty(name)) {
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.whereConditions.push(...parseConditions(conditions, ...values));
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 attributes. `select_expr` with `AS` is supported, hence following expressions have the same effect:
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
- this.orders.push([
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
- for (const condition of parseConditions(conditions, ...values)) {
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 (this.Model.driver.type === 'postgres' && !(condition instanceof Raw)) {
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
 
@@ -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['attributes']]?: Literal | Literal[] | OperatorCondition;
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['attributes']]?: Literal;
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
- column?: string;
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: DataTypes<DataType>;
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 model: { [key: string]: Bone };
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
  }