leoric 1.15.0 → 2.0.2

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,56 @@
1
+ 2.0.2 / 2021-02-10
2
+ ==================
3
+
4
+ ## What's Changed
5
+ * fix: order by alias should not throw by @cyjake in https://github.com/cyjake/leoric/pull/255
6
+ * fix: fix #257 DataType.uncast should skip Raw type at type checking by @JimmyDaddy in https://github.com/cyjake/leoric/pull/258
7
+ * docs: async function in transaction by @cyjake in https://github.com/cyjake/leoric/pull/259
8
+ * fix: fixed #256 static create instance should check all default attri… by @JimmyDaddy in https://github.com/cyjake/leoric/pull/262
9
+ * fix: fix #260 UPDATE with LIMIT and ORDER should be formatted(mysql only) by @JimmyDaddy in https://github.com/cyjake/leoric/pull/261
10
+ * refactor: keep the UPDATE ... ORDER BY ... LIMIT to mysql driver by @cyjake in https://github.com/cyjake/leoric/pull/264
11
+ * fix: fix #263 upsert attributes should use defaultValue while there i… by @JimmyDaddy in https://github.com/cyjake/leoric/pull/265
12
+ * fix: fix restore Error `Undefined attribute "deletedAt"` by @JimmyDaddy in https://github.com/cyjake/leoric/pull/267
13
+ * fix: type checking adaption by @JimmyDaddy in https://github.com/cyjake/leoric/pull/266
14
+
15
+
16
+ **Full Changelog**: https://github.com/cyjake/leoric/compare/v2.0.1...v2.0.2
17
+
18
+ 2.0.1 / 2022-01-05
19
+ ==================
20
+
21
+ ## What's Changed
22
+ * fix: format numeric result by @JimmyDaddy in https://github.com/cyjake/leoric/pull/253
23
+ * fix: should still return number if value is '0.000' by @cyjake in https://github.com/cyjake/leoric/pull/254
24
+
25
+ **Full Changelog**: https://github.com/cyjake/leoric/compare/v1.15.1...v2.0.1
26
+
27
+ 2.0.0 / 2021-12-28
28
+ ==================
29
+
30
+ ## What's Changed
31
+ * breaking: model.sync add force/alter option by @SmartOrange in https://github.com/cyjake/leoric/pull/224
32
+ * breaking: logQueryError(err, sql, duration, options) by @cyjake in https://github.com/cyjake/leoric/pull/237
33
+ * test: add utf8mb4 test cases by @fengmk2 in https://github.com/cyjake/leoric/pull/239
34
+ * Merge 1.x changes by @cyjake in https://github.com/cyjake/leoric/pull/249
35
+
36
+ ## New Contributors
37
+ * @SmartOrange made their first contribution in https://github.com/cyjake/leoric/pull/222
38
+
39
+ **Full Changelog**: https://github.com/cyjake/leoric/compare/v1.15.1...v2.0.0
40
+
41
+ 1.15.1 / 2021-12-28
42
+ ===================
43
+
44
+ ## What's Changed
45
+ * fix: fix #242 date string format by @JimmyDaddy in https://github.com/cyjake/leoric/pull/243
46
+ * fix: update with empty conditions by @JimmyDaddy in https://github.com/cyjake/leoric/pull/241
47
+ * fix: silent option's priority should be lower than valueSet by @JimmyDaddy in https://github.com/cyjake/leoric/pull/244
48
+ * fix: information_schema.columns.datetime_precision by @cyjake in https://github.com/cyjake/leoric/pull/246
49
+ * fix: should not hoist subquery if query is ordered by external columns by @cyjake in https://github.com/cyjake/leoric/pull/247
50
+
51
+
52
+ **Full Changelog**: https://github.com/cyjake/leoric/compare/v1.15.0...v1.15.1
53
+
1
54
  1.15.0 / 2021-11-22
2
55
  ===================
3
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leoric",
3
- "version": "1.15.0",
3
+ "version": "2.0.2",
4
4
  "description": "JavaScript Object-relational mapping alchemy",
5
5
  "main": "index.js",
6
6
  "types": "types/index.d.ts",
@@ -238,9 +238,6 @@ module.exports = Bone => {
238
238
  }
239
239
 
240
240
  static build(values, options = {}) {
241
- if (options.validate !== false) {
242
- this._validateAttributes(values);
243
- }
244
241
  const { raw } = Object.assign({ raw: false, isNewRecord: true }, options);
245
242
  const { attributes } = this;
246
243
 
@@ -256,6 +253,7 @@ module.exports = Bone => {
256
253
  } else {
257
254
  instance = new this(values, options);
258
255
  }
256
+
259
257
  return instance;
260
258
  }
261
259
 
@@ -469,6 +467,7 @@ module.exports = Bone => {
469
467
  const { where, paranoid, individualHooks } = options;
470
468
  if (individualHooks) {
471
469
  let findSpell = this._find(where, options);
470
+ translateOptions(findSpell, options);
472
471
  if (paranoid === false) findSpell = findSpell.unparanoid;
473
472
  const instances = await findSpell;
474
473
  if (instances.length) {
@@ -484,6 +483,7 @@ module.exports = Bone => {
484
483
  const { where, paranoid = false, validate } = options;
485
484
  const whereConditions = where || {};
486
485
  const spell = super.update(whereConditions, values, { validate, hooks: false, ...options });
486
+ translateOptions(spell, options);
487
487
  if (!paranoid) return spell.unparanoid;
488
488
  return spell;
489
489
  }
package/src/bone.js CHANGED
@@ -11,9 +11,11 @@ const { executeValidator, LeoricValidateError } = require('./validator');
11
11
  const DataTypes = require('./data_types');
12
12
  const Collection = require('./collection');
13
13
  const Spell = require('./spell');
14
+ const Raw = require('./raw');
14
15
  const { capitalize, camelCase, snakeCase } = require('./utils/string');
15
16
  const { hookNames, setupSingleHook } = require('./setup_hooks');
16
17
  const { logger } = require('./utils/index');
18
+ const { TIMESTAMP_NAMES, LEGACY_TIMESTAMP_COLUMN_MAP } = require('./constants');
17
19
 
18
20
  function looseReadonly(props) {
19
21
  return Object.keys(props).reduce((result, name) => {
@@ -27,12 +29,8 @@ function looseReadonly(props) {
27
29
  }, {});
28
30
  }
29
31
 
30
- function compare(attributes, columns) {
32
+ function compare(attributes, columnMap) {
31
33
  const diff = {};
32
- const columnMap = columns.reduce((result, entry) => {
33
- result[entry.columnName] = entry;
34
- return result;
35
- }, {});
36
34
 
37
35
  for (const name in attributes) {
38
36
  const attribute = attributes[name];
@@ -71,7 +69,7 @@ function copyValues(values) {
71
69
  for (const key in values) {
72
70
  if (Object.hasOwnProperty.call(values, key)) {
73
71
  const v = values[key];
74
- if (v && (v.__raw || v.__expr || (v instanceof Spell))) continue;
72
+ if (v && ((v instanceof Raw) || v.__expr || (v instanceof Spell))) continue;
75
73
  copyValue[key] = v;
76
74
  }
77
75
  }
@@ -79,6 +77,23 @@ function copyValues(values) {
79
77
  return copyValue;
80
78
  }
81
79
 
80
+ function valuesValidate(values, attributes, ctx) {
81
+ for (const valueKey in values) {
82
+ const attribute = attributes[valueKey];
83
+ if (!attribute) continue;
84
+ const { validate = {}, name, allowNull, defaultValue } = attribute;
85
+ const value = values[valueKey];
86
+ if (value == null && defaultValue == null) {
87
+ if (allowNull === false) throw new LeoricValidateError('notNull', name);
88
+ if ((allowNull === true || allowNull === undefined) && validate.notNull === undefined ) continue;
89
+ }
90
+ if (!validate) continue;
91
+ for (const key in validate) {
92
+ if (validate.hasOwnProperty(key)) executeValidator(ctx, key, attribute, value);
93
+ }
94
+ }
95
+ }
96
+
82
97
  /**
83
98
  * The base class that provides Object-relational mapping. This class is never intended to be used directly. We need to create models that extends from Bone. Most of the query features of Bone is implemented by {@link Spell} such as {@link Spell#$group} and {@link Spell#$join}. With Bone, you can create models like this:
84
99
  *
@@ -162,14 +177,15 @@ class Bone {
162
177
  attribute(...args) {
163
178
  const [ name, value ] = args;
164
179
  const { attributes } = this.constructor;
180
+ const attribute = attributes[name];
165
181
 
166
- if (!attributes.hasOwnProperty(name)) {
182
+ if (!attribute) {
167
183
  throw new Error(`${this.constructor.name} has no attribute "${name}"`);
168
184
  }
169
185
 
170
186
  if (args.length > 1) {
171
187
  // execute validators
172
- this.#raw[name] = value;
188
+ this.#raw[name] = value instanceof Raw ? value : attribute.cast(value);
173
189
  this.#rawUnset.delete(name);
174
190
  return this;
175
191
  }
@@ -282,21 +298,7 @@ class Bone {
282
298
 
283
299
  // merge all changed values
284
300
  changedValues = Object.assign(changedValues, values);
285
-
286
- for (const valueKey in changedValues) {
287
- const attribute = attributes[valueKey];
288
- if (!attribute) continue;
289
- const { validate = {}, name, allowNull, defaultValue } = attribute;
290
- const value = changedValues[valueKey];
291
- if (value == null && defaultValue == null) {
292
- if (allowNull === false) throw new LeoricValidateError('notNull', name);
293
- if ((allowNull === true || allowNull === undefined) && validate.notNull === undefined ) return;
294
- }
295
- if (!validate) return;
296
- for (const key in validate) {
297
- if (validate.hasOwnProperty(key)) executeValidator(this, key, attribute, value);
298
- }
299
- }
301
+ valuesValidate(changedValues, attributes, this);
300
302
  }
301
303
 
302
304
  /**
@@ -308,22 +310,7 @@ class Bone {
308
310
  */
309
311
  static _validateAttributes(values = {}) {
310
312
  const { attributes } = this;
311
- for (const valueKey in values) {
312
- const attribute = attributes[valueKey];
313
- // If valueKey is not an attribute of the Model, go to the next loop instead of throw 'No Such Attribute' Error,
314
- // in case it is a custom property of the Model which defined by custom setters/getters.
315
- if (!attribute) return;
316
- const { validate = {}, name, allowNull, defaultValue } = attribute;
317
- const value = values[valueKey];
318
- if (value == null && defaultValue == null) {
319
- if (allowNull === false) throw new LeoricValidateError('notNull', name);
320
- if ((allowNull === true || allowNull === undefined) && validate.notNull === undefined) return;
321
- }
322
- if (!validate) return;
323
- for (const key in validate) {
324
- if (validate.hasOwnProperty(key)) executeValidator(this, key, attribute, value);
325
- }
326
- }
313
+ valuesValidate(values, attributes, this);
327
314
  }
328
315
 
329
316
  /**
@@ -682,7 +669,7 @@ class Bone {
682
669
  if (shardingKey) where[shardingKey] = this[shardingKey];
683
670
 
684
671
  const { updatedAt, deletedAt } = Model.timestamps;
685
- if (attributes[updatedAt] && !changes[updatedAt] && !changes[deletedAt]) {
672
+ if (attributes[updatedAt] && !changes[updatedAt] && !changes[deletedAt] && !options.silent) {
686
673
  changes[updatedAt] = new Date();
687
674
  }
688
675
  if (options.validate !== false ) {
@@ -727,6 +714,7 @@ class Bone {
727
714
  this[updatedAt] = this[createdAt];
728
715
  }
729
716
 
717
+ const validateValues = {};
730
718
  for (const name in attributes) {
731
719
  const value = this.attribute(name);
732
720
  const { defaultValue } = attributes[name];
@@ -735,10 +723,12 @@ class Bone {
735
723
  } else if (value === undefined && defaultValue != null) {
736
724
  data[name] = defaultValue;
737
725
  }
726
+ if (attributes[name].primaryKey) continue;
727
+ validateValues[name] = data[name];
738
728
  }
739
729
 
740
730
  if (opts.validate !== false) {
741
- this._validateAttributes();
731
+ this._validateAttributes(validateValues);
742
732
  }
743
733
 
744
734
  const spell = new Spell(Model, opts).$insert(data);
@@ -824,10 +814,10 @@ class Bone {
824
814
 
825
815
  const conditions = {
826
816
  [primaryKey]: this[primaryKey],
827
- deletedAt: { $ne: null },
817
+ [deletedAt]: { $ne: null },
828
818
  };
829
819
  if (shardingKey) conditions[shardingKey] = this[shardingKey];
830
- await this.update({ deletedAt: null }, { ...opts, paranoid: false });
820
+ await this.update({ [deletedAt]: null }, { ...opts, paranoid: false });
831
821
  return this;
832
822
  }
833
823
 
@@ -842,7 +832,7 @@ class Bone {
842
832
  if (deletedAt == null) {
843
833
  throw new Error('Model is not paranoid');
844
834
  }
845
- return Bone.update.call(this, conditions, { deletedAt: null }, { ...opts, paranoid: false });
835
+ return Bone.update.call(this, conditions, { [deletedAt]: null }, { ...opts, paranoid: false });
846
836
  }
847
837
 
848
838
  /**
@@ -857,9 +847,13 @@ class Bone {
857
847
  const data = {};
858
848
  const Model = this;
859
849
  const { attributes } = Model;
860
- for (const name in values) {
861
- if (this.hasAttribute(name)) {
862
- data[name] = values[name];
850
+ for (const key in attributes) {
851
+ const attribute = attributes[key];
852
+ if (attribute.primaryKey) continue;
853
+ if (values[key] == null && attribute.defaultValue != null) {
854
+ data[key] = attribute.defaultValue;
855
+ } else if (values[key] !== undefined){
856
+ data[key] = values[key];
863
857
  }
864
858
  }
865
859
 
@@ -933,20 +927,38 @@ class Bone {
933
927
  };
934
928
  }
935
929
 
930
+ const columnMap = columns.reduce((result, entry) => {
931
+ result[entry.columnName] = entry;
932
+ return result;
933
+ }, {});
934
+
936
935
  for (const name of Object.keys(attributes)) {
937
936
  const attribute = new Attribute(name, attributes[name], options.define);
938
937
  attributeMap[attribute.columnName] = attribute;
939
938
  attributes[name] = attribute;
939
+ if (TIMESTAMP_NAMES.includes(name)) {
940
+ const { columnName } = attribute;
941
+ const legacyColumnName = LEGACY_TIMESTAMP_COLUMN_MAP[columnName];
942
+ if (!columnMap[columnName] && legacyColumnName && columnMap[legacyColumnName]) {
943
+ // correct columname
944
+ attribute.columnName = legacyColumnName;
945
+ attributeMap[attribute.columnName] = attribute;
946
+ }
947
+ }
948
+ const columnInfo = columnMap[attribute.columnName];
949
+ // if datetime or timestamp precision not defined, default to column info
950
+ if (columnInfo && attribute.type instanceof DataTypes.DATE && attribute.type.precision == null) {
951
+ attribute.type.precision = columnInfo.datetimePrecision;
952
+ }
940
953
  }
941
954
 
942
955
  const primaryKey = Object.keys(attributes).find(key => attributes[key].primaryKey);
943
956
  const timestamps = {};
944
- for (const key of [ 'createdAt', 'updatedAt', 'deletedAt' ]) {
957
+ for (const key of TIMESTAMP_NAMES) {
945
958
  const name = attributes.hasOwnProperty(key) ? key : snakeCase(key);
946
959
  const attribute = attributes[name];
947
960
 
948
- if (!attribute) continue;
949
- if (columns.some(column => column.columnName === attribute.columnName)) {
961
+ if (attribute && columnMap[attribute.columnName]) {
950
962
  timestamps[key] = name;
951
963
  }
952
964
  }
@@ -960,7 +972,7 @@ class Bone {
960
972
  attributeMap,
961
973
  associations,
962
974
  tableAlias,
963
- synchronized: Object.keys(compare(attributes, columns)).length === 0,
975
+ synchronized: Object.keys(compare(attributes, columnMap)).length === 0,
964
976
  }));
965
977
 
966
978
  for (const hookName of hookNames) {
@@ -1311,13 +1323,9 @@ class Bone {
1311
1323
  static create(values, opts = {}) {
1312
1324
  const data = Object.assign({}, values);
1313
1325
  const instance = new this(data);
1314
- if (opts.validate !== false) {
1315
- instance._validateAttributes(data); // call instance._validateAttributes manually to validate the raw value
1316
- }
1317
1326
  // static create proxy to instance.create
1318
1327
  return instance.create({
1319
1328
  ...opts,
1320
- validate: false, // should not validate again
1321
1329
  });
1322
1330
  }
1323
1331
 
@@ -1416,7 +1424,7 @@ class Bone {
1416
1424
  // values should be immutable
1417
1425
  const data = Object.assign({}, values);
1418
1426
  const { updatedAt, deletedAt } = this.timestamps;
1419
- if (attributes[updatedAt] && !data[updatedAt] && !data[deletedAt]) {
1427
+ if (attributes[updatedAt] && !data[updatedAt] && !data[deletedAt] && !options.silent) {
1420
1428
  data[updatedAt] = new Date();
1421
1429
  }
1422
1430
 
@@ -1537,7 +1545,7 @@ class Bone {
1537
1545
  Object.defineProperties(this, looseReadonly({ ...hookMethods, attributes, table }));
1538
1546
  }
1539
1547
 
1540
- static async sync() {
1548
+ static async sync({ force = false, alter = false } = {}) {
1541
1549
  const { driver, physicTable: table } = this;
1542
1550
  const { database } = this.options;
1543
1551
 
@@ -1554,11 +1562,22 @@ class Bone {
1554
1562
  }
1555
1563
 
1556
1564
  const { attributes, columns } = this;
1565
+ const columnMap = columns.reduce((result, entry) => {
1566
+ result[entry.columnName] = entry;
1567
+ return result;
1568
+ }, {});
1557
1569
 
1558
1570
  if (columns.length === 0) {
1559
1571
  await driver.createTable(table, attributes);
1560
1572
  } else {
1561
- await driver.alterTable(table, compare(attributes, columns));
1573
+ if (force) {
1574
+ await driver.dropTable(table);
1575
+ await driver.createTable(table, attributes);
1576
+ } else if (alter){
1577
+ await driver.alterTable(table, compare(attributes, columnMap));
1578
+ } else {
1579
+ console.warn('[synchronize_fail] %s couldn\'t be synchronized, please use force or alter to specify execution', this.name);
1580
+ }
1562
1581
  }
1563
1582
 
1564
1583
  const schemaInfo = await driver.querySchemaInfo(database, table);
package/src/collection.js CHANGED
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ const { AGGREGATOR_MAP } = require('./constants');
4
+
5
+ const AGGREGATORS = Object.values(AGGREGATOR_MAP);
3
6
  /**
4
7
  * An extended Array to represent collections of models.
5
8
  */
@@ -87,8 +90,14 @@ function dispatch(spell, rows, fields) {
87
90
  const { type, value, args } = columns[0];
88
91
  if (type === 'alias' && args && args[0].type === 'func') {
89
92
  const row = rows[0];
90
- const result = row && (row[''] || row[table]);
91
- return result && result[value] || 0;
93
+ const record = row && (row[''] || row[table]);
94
+ const result = record && record[value];
95
+ // see https://www.w3schools.com/mysql/mysql_ref_functions.asp
96
+ if (AGGREGATORS.includes(args[0].name)) {
97
+ const num = Number(result);
98
+ return isNaN(num) ? result : num;
99
+ }
100
+ return result;
92
101
  }
93
102
  }
94
103
 
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ const AGGREGATOR_MAP = {
4
+ count: 'count',
5
+ average: 'avg',
6
+ minimum: 'min',
7
+ maximum: 'max',
8
+ sum: 'sum'
9
+ };
10
+
11
+ const LEGACY_TIMESTAMP_MAP = {
12
+ gmtCreate: 'createdAt',
13
+ gmtModified: 'updatedAt',
14
+ gmtDeleted: 'deletedAt',
15
+ };
16
+
17
+ const LEGACY_TIMESTAMP_COLUMN_MAP = {
18
+ created_at: 'gmt_create',
19
+ updated_at: 'gmt_modified',
20
+ deleted_at: 'gmt_deleted',
21
+ };
22
+
23
+ const TIMESTAMP_NAMES = [ 'createdAt', 'updatedAt', 'deletedAt' ];
24
+
25
+ module.exports = {
26
+ AGGREGATOR_MAP,
27
+ LEGACY_TIMESTAMP_MAP,
28
+ TIMESTAMP_NAMES,
29
+ LEGACY_TIMESTAMP_COLUMN_MAP
30
+ };
package/src/data_types.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const util = require('util');
4
4
  const invokable = require('./utils/invokable');
5
+ const Raw = require('./raw');
5
6
 
6
7
  /**
7
8
  * @example
@@ -114,7 +115,7 @@ class STRING extends DataType {
114
115
  }
115
116
 
116
117
  uncast(value) {
117
- if (value == null) return value;
118
+ if (value == null || value instanceof Raw) return value;
118
119
  return '' + value;
119
120
  }
120
121
  }
@@ -186,15 +187,18 @@ class INTEGER extends DataType {
186
187
  }
187
188
 
188
189
  cast(value) {
189
- if (value == null) return value;
190
+ if (value == null || isNaN(value)) return value;
190
191
  return Number(value);
191
192
  }
192
193
 
193
- uncast(value) {
194
+ uncast(value, strict = true) {
194
195
  const originValue = value;
195
- if (value == null) return value;
196
+ if (value == null || value instanceof Raw) return value;
196
197
  if (typeof value === 'string') value = parseInt(value, 10);
197
- if (isNaN(value)) throw new Error(util.format('invalid integer: %s', originValue));
198
+ if (isNaN(value)) {
199
+ if (strict) throw new Error(util.format('invalid integer: %s', originValue));
200
+ return originValue;
201
+ }
198
202
  return value;
199
203
  }
200
204
  }
@@ -214,7 +218,7 @@ class BIGINT extends INTEGER {
214
218
  }
215
219
  }
216
220
 
217
- const rDateFormat = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:[,.]\d{3,6})?$/;
221
+ const rDateFormat = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:[,.]\d{3,6})$/;
218
222
 
219
223
  class DATE extends DataType {
220
224
  constructor(precision, timezone = true) {
@@ -229,20 +233,31 @@ class DATE extends DataType {
229
233
  toSqlString() {
230
234
  const { precision } = this;
231
235
  const dataType = this.dataType.toUpperCase();
232
- if (precision > 0) return `${dataType}(${precision})`;
236
+ if (precision != null && precision >= 0) return `${dataType}(${precision})`;
233
237
  return dataType;
234
238
  }
235
239
 
240
+ _round(value) {
241
+ const { precision } = this;
242
+ if (precision != null && precision < 3 && value instanceof Date) {
243
+ const divider = 10 ** (3 - precision);
244
+ return new Date(Math.round(value.getTime() / divider) * divider);
245
+ }
246
+ return value;
247
+ }
248
+
236
249
  cast(value) {
250
+ const original = value;
237
251
  if (value == null) return value;
238
- if (value instanceof Date) return value;
239
- return new Date(value);
252
+ if (!(value instanceof Date)) value = new Date(value);
253
+ if (isNaN(value.getTime())) return original;
254
+ return this._round(value);
240
255
  }
241
256
 
242
257
  uncast(value) {
243
258
  const originValue = value;
244
259
 
245
- if (value == null) return value;
260
+ if (value == null || value instanceof Raw) return value;
246
261
  if (typeof value.toDate === 'function') {
247
262
  value = value.toDate();
248
263
  }
@@ -258,13 +273,7 @@ class DATE extends DataType {
258
273
  if (!(value instanceof Date)) value = new Date(value);
259
274
  if (isNaN(value)) throw new Error(util.format('invalid date: %s', originValue));
260
275
 
261
- const { precision } = this;
262
- if (precision < 3) {
263
- const result = new Date(value);
264
- result.setMilliseconds(result.getMilliseconds() % (10 ** precision));
265
- return result;
266
- }
267
- return value;
276
+ return this._round(value);
268
277
  }
269
278
  }
270
279
 
@@ -278,16 +287,25 @@ class DATEONLY extends DataType {
278
287
  return this.dataType.toUpperCase();
279
288
  }
280
289
 
290
+ _round(value) {
291
+ if (value instanceof Date) {
292
+ return new Date(value.getFullYear(), value.getMonth(), value.getDate());
293
+ }
294
+ return value;
295
+ }
296
+
281
297
  cast(value) {
298
+ const original = value;
282
299
  if (value == null) return value;
283
- if (value instanceof Date) return value;
284
- return new Date(value);
300
+ if (!(value instanceof Date)) value = new Date(value);
301
+ if (isNaN(value.getTime())) return original;
302
+ return this._round(value);
285
303
  }
286
304
 
287
305
  uncast(value) {
288
306
  const originValue = value;
289
307
 
290
- if (value == null) return value;
308
+ if (value == null || value instanceof Raw) return value;
291
309
  if (typeof value.toDate === 'function') {
292
310
  value = value.toDate();
293
311
  }
@@ -299,9 +317,9 @@ class DATEONLY extends DataType {
299
317
  }
300
318
 
301
319
  if (!(value instanceof Date)) value = new Date(value);
302
- if (isNaN(value)) throw new Error(util.format('invalid date: %s', originValue));;
320
+ if (isNaN(value)) throw new Error(util.format('invalid date: %s', originValue));
303
321
 
304
- return new Date(value.getFullYear(), value.getMonth(), value.getDate());
322
+ return this._round(value);
305
323
  }
306
324
  }
307
325
 
@@ -374,11 +392,16 @@ class JSON extends DataType {
374
392
  if (!value) return value;
375
393
  // type === JSONB
376
394
  if (typeof value === 'object') return value;
377
- return global.JSON.parse(value);
395
+ try {
396
+ return global.JSON.parse(value);
397
+ } catch (err) {
398
+ console.error(new Error(`unable to cast ${value} to JSON`));
399
+ return value;
400
+ }
378
401
  }
379
402
 
380
403
  uncast(value) {
381
- if (value == null) return value;
404
+ if (value == null || value instanceof Raw) return value;
382
405
  return global.JSON.stringify(value);
383
406
  }
384
407
  }
@@ -126,11 +126,11 @@ class Attribute {
126
126
  return this.type.cast(value);
127
127
  }
128
128
 
129
- uncast(value) {
129
+ uncast(value, strict = true) {
130
130
  if (Array.isArray(value) && this.jsType !== JSON) {
131
- return value.map(entry => this.type.uncast(entry));
131
+ return value.map(entry => this.type.uncast(entry, strict));
132
132
  }
133
- return this.type.uncast(value);
133
+ return this.type.uncast(value, strict);
134
134
  }
135
135
  }
136
136
 
@@ -4,6 +4,7 @@ const SqlString = require('sqlstring');
4
4
 
5
5
  const { copyExpr, findExpr, walkExpr } = require('../../expr');
6
6
  const { formatExpr, formatConditions, collectLiteral } = require('../../expr_formatter');
7
+ const Raw = require('../../raw');
7
8
 
8
9
  /**
9
10
  * Format orders into ORDER BY clause in SQL
@@ -128,7 +129,7 @@ function createSubspell(spell) {
128
129
  subspell.orders = [];
129
130
  for (const order of orders) {
130
131
  const [token, direction] = order;
131
- const { type, qualifiers, value } = token;
132
+ const { type, qualifiers = [], value } = token;
132
133
  if (type == 'id' && qualifiers[0] == baseName) {
133
134
  subspell.orders.push([{ type, value }, direction]);
134
135
  }
@@ -219,13 +220,13 @@ function formatSelectWithJoin(spell) {
219
220
 
220
221
  let hoistable = skip > 0 || rowCount > 0;
221
222
  if (hoistable) {
222
- for (const condition of whereConditions) {
223
- walkExpr(condition, ({ type, qualifiers }) => {
224
- if (type === 'id' && qualifiers.length> 0 && !qualifiers.includes(baseName)) {
225
- hoistable = false;
226
- }
227
- });
223
+ function checkQualifier({ type, qualifiers = [] }) {
224
+ if (type === 'id' && qualifiers.length> 0 && !qualifiers.includes(baseName)) {
225
+ hoistable = false;
226
+ }
228
227
  }
228
+ for (const condition of whereConditions) walkExpr(condition, checkQualifier);
229
+ for (const orderExpr of orders) walkExpr(orderExpr[0], checkQualifier);
229
230
  }
230
231
 
231
232
  if (hoistable) {
@@ -386,7 +387,7 @@ function formatInsert(spell) {
386
387
  const value = sets[name];
387
388
  // upsert should not update createdAt
388
389
  columns.push(Model.unalias(name));
389
- if (value && value.__raw) {
390
+ if (value instanceof Raw) {
390
391
  values.push(SqlString.raw(value.value));
391
392
  } else {
392
393
  values.push(value);
@@ -449,7 +450,7 @@ function formatUpdate(spell) {
449
450
  if (value && value.__expr) {
450
451
  assigns.push(`${escapeId(Model.unalias(name))} = ${formatExpr(spell, value)}`);
451
452
  collectLiteral(spell, value, values);
452
- } else if (value && value.__raw) {
453
+ } else if (value instanceof Raw) {
453
454
  assigns.push(`${escapeId(Model.unalias(name))} = ${value.value}`);
454
455
  } else {
455
456
  assigns.push(`${escapeId(Model.unalias(name))} = ?`);
@@ -457,7 +458,6 @@ function formatUpdate(spell) {
457
458
  }
458
459
  }
459
460
 
460
- for (const condition of whereConditions) collectLiteral(spell, condition, values);
461
461
  // see https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html
462
462
  const hintStr = this.formatOptimizerHints(spell);
463
463
  // see https://dev.mysql.com/doc/refman/8.0/en/index-hints.html
@@ -472,7 +472,11 @@ function formatUpdate(spell) {
472
472
  }
473
473
 
474
474
  chunks.push(`SET ${assigns.join(', ')}`);
475
- chunks.push(`WHERE ${formatConditions(spell, whereConditions)}`);
475
+ if (whereConditions.length > 0) {
476
+ for (const condition of whereConditions) collectLiteral(spell, condition, values);
477
+ chunks.push(`WHERE ${formatConditions(spell, whereConditions)}`);
478
+ }
479
+
476
480
  return {
477
481
  sql: chunks.join(' '),
478
482
  values,
@@ -603,4 +607,5 @@ module.exports = {
603
607
  formatSelectWithoutJoin,
604
608
  formatUpdateOnDuplicate,
605
609
  formatReturning,
610
+ formatOrders
606
611
  };
@@ -93,7 +93,7 @@ class MysqlDriver extends AbstractDriver {
93
93
  try {
94
94
  result = await promise;
95
95
  } catch (err) {
96
- logger.logQueryError(sql, err, calculateDuration(start), opts);
96
+ logger.logQueryError(err, sql, calculateDuration(start), opts);
97
97
  throw err;
98
98
  } finally {
99
99
  if (!opts.connection) connection.release();
@@ -17,7 +17,8 @@ module.exports = {
17
17
  tables = [].concat(tables);
18
18
  const sql = heresql(`
19
19
  SELECT table_name, column_name, column_type, data_type, is_nullable,
20
- column_default, column_key, column_comment
20
+ column_default, column_key, column_comment,
21
+ datetime_precision
21
22
  FROM information_schema.columns
22
23
  WHERE table_schema = ? AND table_name in (?)
23
24
  ORDER BY table_name, column_name
@@ -43,6 +44,7 @@ module.exports = {
43
44
  allowNull: row.is_nullable === 'YES',
44
45
  primaryKey: row.column_key == 'PRI',
45
46
  unique: row.column_key == 'PRI' || row.column_key == 'UNI',
47
+ datetimePrecision: row.datetime_precision,
46
48
  });
47
49
  }
48
50
 
@@ -58,4 +58,21 @@ module.exports = {
58
58
  formatReturning() {
59
59
  return '';
60
60
  },
61
+
62
+ /**
63
+ * UPDATE ... ORDER BY ... LIMIT ${rowCount}
64
+ * - https://dev.mysql.com/doc/refman/8.0/en/update.html
65
+ * @param {Spell} spell
66
+ */
67
+ formatUpdate(spell) {
68
+ const result = spellbook.formatUpdate.call(this, spell);
69
+ const { rowCount, orders } = spell;
70
+ const chunks = [];
71
+
72
+ if (orders.length > 0) chunks.push(`ORDER BY ${this.formatOrders(spell, orders).join(', ')}`);
73
+ if (rowCount > 0) chunks.push(`LIMIT ${rowCount}`);
74
+ if (chunks.length > 0) result.sql += ` ${chunks.join(' ')}`;
75
+
76
+ return result;
77
+ },
61
78
  };
@@ -35,7 +35,6 @@ class PostgresAttribute extends Attribute {
35
35
  return chunks.join(' ');
36
36
  }
37
37
 
38
-
39
38
  equals(columnInfo) {
40
39
  if (!columnInfo) return false;
41
40
  if (this.type.toSqlString() !== columnInfo.columnType.toUpperCase()) {
@@ -1,6 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const DataTypes = require('../../data_types');
4
+ const util = require('util');
5
+ const Raw = require('../../raw');
6
+
4
7
 
5
8
  class Postgres_DATE extends DataTypes.DATE {
6
9
  constructor(precision, timezone = true) {
@@ -37,12 +40,35 @@ class Postgres_BINARY extends DataTypes {
37
40
  }
38
41
  }
39
42
 
43
+ class Postgres_INTEGER extends DataTypes.INTEGER {
44
+ constructor(length) {
45
+ super(length);
46
+ }
47
+
48
+ uncast(value) {
49
+ const originValue = value;
50
+ if (value == null || value instanceof Raw) return value;
51
+ if (typeof value === 'string') value = parseInt(value, 10);
52
+ if (isNaN(value)) throw new Error(util.format('invalid integer: %s', originValue));
53
+ return value;
54
+ }
55
+ }
56
+
57
+ class Postgres_BIGINT extends Postgres_INTEGER {
58
+ constructor() {
59
+ super();
60
+ this.dataType = 'bigint';
61
+ }
62
+ }
63
+
40
64
  class Postgres_DataTypes extends DataTypes {
41
65
  static DATE = Postgres_DATE;
42
66
  static JSONB = Postgres_JSONB;
43
67
  static BINARY = Postgres_BINARY;
44
68
  static VARBINARY = Postgres_BINARY;
45
69
  static BLOB = Postgres_BINARY;
70
+ static INTEGER = Postgres_INTEGER;
71
+ static BIGINT = Postgres_BIGINT;
46
72
  }
47
73
 
48
74
  module.exports = Postgres_DataTypes;
@@ -137,7 +137,7 @@ class PostgresDriver extends AbstractDriver {
137
137
  try {
138
138
  result = await connection.query(...args);
139
139
  } catch (err) {
140
- logger.logQueryError(formatted, err, calculateDuration(start), spell);
140
+ logger.logQueryError(err, formatted, calculateDuration(start), spell);
141
141
  throw err;
142
142
  } finally {
143
143
  if (!spell.connection) connection.release();
@@ -58,6 +58,8 @@ module.exports = {
58
58
  if (dataType === 'character varying') dataType = 'varchar';
59
59
  if (dataType === 'timestamp without time zone') dataType = 'timestamp';
60
60
  const primaryKey = row.constraint_type === 'PRIMARY KEY';
61
+ const precision = row.datetime_precision;
62
+
61
63
  columns.push({
62
64
  columnName: row.column_name,
63
65
  columnType: length > 0 ? `${dataType}(${length})` : dataType,
@@ -67,6 +69,7 @@ module.exports = {
67
69
  // https://www.postgresql.org/docs/9.5/infoschema-table-constraints.html
68
70
  primaryKey,
69
71
  unique: row.constraint_type === 'UNIQUE',
72
+ datetimePrecision: precision === 6 ? null : precision,
70
73
  });
71
74
  }
72
75
 
@@ -3,16 +3,46 @@
3
3
  const DataTypes = require('../../data_types');
4
4
 
5
5
  class Sqlite_DATE extends DataTypes.DATE {
6
- constructor(precision) {
7
- super(precision);
6
+ constructor(precision, timezone) {
7
+ super(precision, timezone);
8
8
  this.dataType = 'datetime';
9
9
  }
10
10
 
11
- toSqlString() {
12
- return this.dataType.toUpperCase();
11
+ uncast(value) {
12
+ try {
13
+ return super.uncast(value);
14
+ } catch (error) {
15
+ console.error(new Error(`unable to cast ${value} to DATE`));
16
+ return value;
17
+ }
18
+ }
19
+ }
20
+
21
+ class Sqlite_DATEONLY extends DataTypes.DATEONLY {
22
+ constructor() {
23
+ super();
24
+ }
25
+
26
+ uncast(value) {
27
+ try {
28
+ return super.uncast(value);
29
+ } catch (error) {
30
+ console.error(new Error(`unable to cast ${value} to DATEONLY`));
31
+ return value;
32
+ }
13
33
  }
14
34
  }
15
35
 
36
+ class Sqlite_INTEGER extends DataTypes.INTEGER {
37
+ constructor(length) {
38
+ super(length);
39
+ }
40
+
41
+ uncast(value) {
42
+ return super.uncast(value, false);
43
+ }
44
+
45
+ }
16
46
  class Sqlite_BIGINT extends DataTypes.BIGINT {
17
47
  constructor() {
18
48
  super();
@@ -62,6 +92,14 @@ class Sqlite_DataTypes extends DataTypes {
62
92
  static get VARBINARY() {
63
93
  return Sqlite_VARBINARY;
64
94
  }
95
+
96
+ static get DATEONLY() {
97
+ return Sqlite_DATEONLY;
98
+ }
99
+
100
+ static get INTEGER() {
101
+ return Sqlite_INTEGER;
102
+ }
65
103
  }
66
104
 
67
105
  module.exports = Sqlite_DataTypes;
@@ -48,7 +48,7 @@ class SqliteDriver extends AbstractDriver {
48
48
  try {
49
49
  result = await connection.query(query, values, opts);
50
50
  } catch (err) {
51
- logger.logQueryError(sql, err, calculateDuration(start), opts);
51
+ logger.logQueryError(err, sql, calculateDuration(start), opts);
52
52
  throw err;
53
53
  } finally {
54
54
  if (!opts.connection) connection.release();
@@ -101,19 +101,24 @@ module.exports = {
101
101
  });
102
102
  const results = await Promise.all(queries);
103
103
  const schemaInfo = {};
104
+ const rColumnType = /^(\w+)(?:\(([^)]+)\))?/i;
105
+ const rDateType = /(?:date|datetime|timestamp)/i;
106
+
104
107
  for (let i = 0; i < tables.length; i++) {
105
108
  const table = tables[i];
106
109
  const { rows } = results[i];
107
110
  const columns = rows.map(({ name, type, notnull, dflt_value, pk }) => {
108
111
  const columnType = type.toLowerCase();
112
+ const [, dataType, precision ] = columnType.match(rColumnType);
109
113
  const primaryKey = pk === 1;
110
114
  const result = {
111
115
  columnName: name,
112
116
  columnType,
113
117
  defaultValue: parseDefaultValue(dflt_value),
114
- dataType: columnType.split('(')[0],
118
+ dataType: dataType,
115
119
  allowNull: primaryKey ? false : notnull == 0,
116
120
  primaryKey,
121
+ datetimePrecision: rDateType.test(dataType) ? parseInt(precision, 10) : null,
117
122
  };
118
123
  return result;
119
124
  });
@@ -208,7 +208,7 @@ function coerceLiteral(spell, ast) {
208
208
  if (arg.type === 'literal') {
209
209
  // { params: { $like: '%foo%' } }
210
210
  if (attribute.jsType === JSON && typeof arg.value === 'string') continue;
211
- arg.value = attribute.uncast(arg.value);
211
+ arg.value = attribute.uncast(arg.value, false);
212
212
  }
213
213
  }
214
214
  }
@@ -3,6 +3,7 @@
3
3
  const util = require('util');
4
4
  const { isPlainObject } = require('./utils');
5
5
  const { parseExpr } = require('./expr');
6
+ const Raw = require('./raw');
6
7
  // deferred to break cyclic dependencies
7
8
  let Spell;
8
9
 
@@ -29,7 +30,7 @@ const OPERATOR_MAP = {
29
30
  */
30
31
  function parseValue(value) {
31
32
  if (value instanceof Spell) return { type: 'subquery', value };
32
- if (value && value.__raw) return { type: 'raw', value: value.value };
33
+ if (value instanceof Raw) return { type: 'raw', value: value.value };
33
34
  return parseExpr('?', value);
34
35
  }
35
36
 
package/src/raw.js ADDED
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ module.exports = class Raw {
4
+ constructor(value) {
5
+ this.value = value;
6
+ // consumed in expr_formatter.js
7
+ this.type = 'raw';
8
+ }
9
+ };
package/src/realm.js CHANGED
@@ -7,6 +7,8 @@ const Bone = require('./bone');
7
7
  const { findDriver } = require('./drivers');
8
8
  const { camelCase } = require('./utils/string');
9
9
  const sequelize = require('./adapters/sequelize');
10
+ const Raw = require('./raw');
11
+ const { LEGACY_TIMESTAMP_MAP } = require('./constants');
10
12
 
11
13
  /**
12
14
  *
@@ -38,12 +40,6 @@ async function findModels(dir) {
38
40
  return models;
39
41
  }
40
42
 
41
- const LEGACY_TIMESTAMP_MAP = {
42
- gmtCreate: 'createdAt',
43
- gmtModified: 'updatedAt',
44
- gmtDeleted: 'deletedAt',
45
- };
46
-
47
43
  /**
48
44
  * construct model attributes entirely from column definitions
49
45
  * @param {Bone} model
@@ -163,12 +159,12 @@ class Realm {
163
159
  return this.Bone;
164
160
  }
165
161
 
166
- async sync() {
162
+ async sync(options) {
167
163
  if (!this.connected) await this.connect();
168
164
  const { models } = this;
169
165
 
170
166
  for (const model of Object.values(models)) {
171
- await model.sync();
167
+ await model.sync(options);
172
168
  }
173
169
  }
174
170
 
@@ -239,11 +235,7 @@ class Realm {
239
235
  if (typeof sql !== 'string') {
240
236
  throw new TypeError('sql must be a string');
241
237
  }
242
- return {
243
- __raw: true,
244
- value: sql,
245
- type: 'raw',
246
- };
238
+ return new Raw(sql);
247
239
  }
248
240
 
249
241
  // instance.raw
package/src/spell.js CHANGED
@@ -10,6 +10,8 @@ const { parseExprList, parseExpr, walkExpr } = require('./expr');
10
10
  const { isPlainObject } = require('./utils');
11
11
  const { IndexHint, INDEX_HINT_TYPE, Hint } = require('./hint');
12
12
  const { parseObject } = require('./query_object');
13
+ const Raw = require('./raw');
14
+ const { AGGREGATOR_MAP } = require('./constants');
13
15
 
14
16
  /**
15
17
  * Parse condition expressions
@@ -21,7 +23,7 @@ const { parseObject } = require('./query_object');
21
23
  * @returns {Array}
22
24
  */
23
25
  function parseConditions(conditions, ...values) {
24
- if (conditions.__raw) return [ conditions ];
26
+ if (conditions instanceof Raw) return [ conditions ];
25
27
  if (isPlainObject(conditions)) {
26
28
  return parseObject(conditions);
27
29
  } else if (typeof conditions == 'string') {
@@ -41,7 +43,7 @@ function parseSelect(spell, ...names) {
41
43
 
42
44
  const columns = [];
43
45
  for (const name of names) {
44
- if (name.__raw) columns.push(name);
46
+ if (name instanceof Raw) columns.push(name);
45
47
  else columns.push(...parseExprList(name));
46
48
  }
47
49
 
@@ -68,8 +70,7 @@ function parseSelect(spell, ...names) {
68
70
  * @returns {Object}
69
71
  */
70
72
  function formatValueSet(spell, obj, strict = true) {
71
- const { Model, silent = false, command } = spell;
72
- const { timestamps } = Model;
73
+ const { Model } = spell;
73
74
  const sets = {};
74
75
  for (const name in obj) {
75
76
  const attribute = Model.attributes[name];
@@ -79,12 +80,8 @@ function formatValueSet(spell, obj, strict = true) {
79
80
  throw new Error(`Undefined attribute "${name}"`);
80
81
  }
81
82
 
82
- if (silent && timestamps.updatedAt && name === timestamps.updatedAt && command === 'update') {
83
- continue;
84
- }
85
-
86
83
  // raw sql don't need to uncast
87
- if (value && value.__raw) {
84
+ if (value instanceof Raw) {
88
85
  sets[name] = value;
89
86
  } else {
90
87
  sets[name] = attribute.uncast(value);
@@ -402,7 +399,6 @@ class Spell {
402
399
  hints: [...this.hints],
403
400
  // used by transaction
404
401
  connection: this.connection,
405
- silent: this.silent,
406
402
  });
407
403
  }
408
404
 
@@ -530,8 +526,8 @@ class Spell {
530
526
  }
531
527
 
532
528
  $increment(name, by = 1, opts = {}) {
533
- let { Model, silent = false } = this;
534
- if (opts.silent != null) silent = opts.silent;
529
+ const { Model } = this;
530
+ const silent = opts.silent;
535
531
  const { timestamps } = Model;
536
532
  this.command = 'update';
537
533
  if (!Number.isFinite(by)) throw new Error(`unexpected increment value ${by}`);
@@ -652,7 +648,7 @@ class Spell {
652
648
  */
653
649
  $order(name, direction) {
654
650
  if (isPlainObject(name)) {
655
- if (name.__raw) {
651
+ if (name instanceof Raw) {
656
652
  this.orders.push([
657
653
  name,
658
654
  ]);
@@ -717,7 +713,7 @@ class Spell {
717
713
  for (const condition of parseConditions(conditions, ...values)) {
718
714
  // Postgres can't have alias in HAVING caluse
719
715
  // https://stackoverflow.com/questions/32730296/referring-to-a-select-aggregate-column-alias-in-the-having-clause-in-postgres
720
- if (this.Model.driver.type === 'postgres' && !condition.__raw) {
716
+ if (this.Model.driver.type === 'postgres' && !(condition instanceof Raw)) {
721
717
  const { value } = condition.args[0];
722
718
  for (const column of this.columns) {
723
719
  if (column.value === value && column.type === 'alias') {
@@ -908,14 +904,6 @@ class Spell {
908
904
  }
909
905
  }
910
906
 
911
- const AGGREGATOR_MAP = {
912
- count: 'count',
913
- average: 'avg',
914
- minimum: 'min',
915
- maximum: 'max',
916
- sum: 'sum'
917
- };
918
-
919
907
  for (const aggregator in AGGREGATOR_MAP) {
920
908
  const func = AGGREGATOR_MAP[aggregator];
921
909
  Object.defineProperty(Spell.prototype, `$${aggregator}`, {
package/types/index.d.ts CHANGED
@@ -64,10 +64,10 @@ export class Spell<T extends typeof Bone, U = InstanceType<T> | Collection<Insta
64
64
  constructor(Model: T, opts: SpellOptions);
65
65
 
66
66
  select(...names: Array<string | RawSql> | Array<(name: string) => boolean>): Spell<T, U>;
67
- insert(opts: SetOptions): Spell<T, number>;
68
- update(opts: SetOptions): Spell<T, number>;
69
- upsert(opts: SetOptions): Spell<T, number>;
70
- delete(): Spell<T, number>;
67
+ insert(opts: SetOptions): Spell<T, QueryResult>;
68
+ update(opts: SetOptions): Spell<T, QueryResult>;
69
+ upsert(opts: SetOptions): Spell<T, QueryResult>;
70
+ delete(): Spell<T, QueryResult>;
71
71
 
72
72
  from(table: string | Spell<T>): Spell<T, U>;
73
73
 
@@ -107,8 +107,8 @@ export class Spell<T extends typeof Bone, U = InstanceType<T> | Collection<Insta
107
107
 
108
108
  batch(size?: number): AsyncIterable<T>;
109
109
 
110
- increment(name: string, by?: number, options?: QueryOptions): Spell<T, number>;
111
- decrement(name: string, by?: number, options?: QueryOptions): Spell<T, number>;
110
+ increment(name: string, by?: number, options?: QueryOptions): Spell<T, QueryResult>;
111
+ decrement(name: string, by?: number, options?: QueryOptions): Spell<T, QueryResult>;
112
112
 
113
113
  toSqlString(): string;
114
114
  toString(): string;