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 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.2.3",
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"
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
 
@@ -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(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
  }
@@ -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 === 0) return Promise.resolve(0);
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, opts) {
1121
- opts = Object.assign({
1172
+ static hasOne(name, options) {
1173
+ options = ({
1122
1174
  className: capitalize(name),
1123
- foreignKey: this.table + 'Id',
1124
- }, opts);
1175
+ foreignKey: camelCase(`${this.name}Id`),
1176
+ ...options,
1177
+ });
1125
1178
 
1126
- if (opts.through) opts.foreignKey = '';
1179
+ if (options.through) options.foreignKey = '';
1127
1180
 
1128
- this.associate(name, opts);
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, opts) {
1139
- opts = Object.assign({
1191
+ static hasMany(name, options) {
1192
+ options = {
1140
1193
  className: capitalize(pluralize(name, 1)),
1141
- }, opts, {
1194
+ foreignKey: camelCase(`${this.name}Id`),
1195
+ ...options,
1142
1196
  hasMany: true,
1143
- });
1197
+ };
1144
1198
 
1145
- if (opts.through) opts.foreignKey = '';
1199
+ if (options.through) options.foreignKey = '';
1146
1200
 
1147
- this.associate(name, opts);
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, opts) {
1158
- opts = Object.assign({
1159
- className: capitalize(name),
1160
- }, opts);
1161
-
1162
- let { className, foreignKey } = opts;
1163
- 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
+ };
1164
1218
 
1165
- this.associate(name, Object.assign(opts, { foreignKey, belongsTo: true }));
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 { 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) {
@@ -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.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') {
@@ -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
- 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
  }