leoric 2.2.2 → 2.3.1

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.
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BelongsTo = exports.HasOne = exports.HasMany = exports.Column = void 0;
7
+ const data_types_1 = __importDefault(require("./data_types"));
8
+ const constants_1 = require("./constants");
9
+ require("reflect-metadata");
10
+ function findType(tsType) {
11
+ const { BIGINT, INTEGER, DATE, STRING, BOOLEAN, } = data_types_1.default;
12
+ switch (tsType) {
13
+ case BigInt:
14
+ return BIGINT;
15
+ case Number:
16
+ return INTEGER;
17
+ case Date:
18
+ return DATE;
19
+ case String:
20
+ return STRING;
21
+ case Boolean:
22
+ return BOOLEAN;
23
+ default:
24
+ throw new Error(`unknown typescript type ${tsType}`);
25
+ }
26
+ }
27
+ function Column(options = {}) {
28
+ return function (target, propertyKey) {
29
+ if (options['prototype'] instanceof data_types_1.default)
30
+ options = { type: options };
31
+ if (!('type' in options)) {
32
+ const tsType = Reflect.getMetadata('design:type', target, propertyKey);
33
+ options['type'] = findType(tsType);
34
+ }
35
+ // narrowing the type of options to ColumnOption
36
+ if (!('type' in options))
37
+ throw new Error(`unknown column options ${options}`);
38
+ // target refers to model prototype, an internal instance of `Bone {}`
39
+ const model = target.constructor;
40
+ const { attributes = (model.attributes = {}) } = model;
41
+ const { name: columnName, ...restOptions } = options;
42
+ attributes[propertyKey] = { ...restOptions, columnName };
43
+ };
44
+ }
45
+ exports.Column = Column;
46
+ const { hasMany, hasOne, belongsTo } = constants_1.ASSOCIATE_METADATA_MAP;
47
+ function HasMany(options) {
48
+ return function (target, propertyKey) {
49
+ const model = target.constructor;
50
+ Reflect.defineMetadata(hasMany, {
51
+ ...Reflect.getMetadata(hasMany, model),
52
+ [propertyKey]: options,
53
+ }, model);
54
+ };
55
+ }
56
+ exports.HasMany = HasMany;
57
+ function HasOne(options) {
58
+ return function (target, propertyKey) {
59
+ const model = target.constructor;
60
+ Reflect.defineMetadata(hasOne, {
61
+ ...Reflect.getMetadata(hasOne, model),
62
+ [propertyKey]: options,
63
+ }, model);
64
+ };
65
+ }
66
+ exports.HasOne = HasOne;
67
+ function BelongsTo(options) {
68
+ return function (target, propertyKey) {
69
+ const model = target.constructor;
70
+ Reflect.defineMetadata(belongsTo, {
71
+ ...Reflect.getMetadata(belongsTo, model),
72
+ [propertyKey]: options,
73
+ }, model);
74
+ };
75
+ }
76
+ exports.BelongsTo = BelongsTo;
77
+ //# sourceMappingURL=decorators.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorators.js","sourceRoot":"","sources":["decorators.ts"],"names":[],"mappings":";;;;;;AACA,8DAAoC;AACpC,2CAAqD;AACrD,4BAA0B;AAa1B,SAAS,QAAQ,CAAC,MAAM;IACtB,MAAM,EACJ,MAAM,EAAE,OAAO,EACf,IAAI,EACJ,MAAM,EACN,OAAO,GACR,GAAG,oBAAQ,CAAC;IAEb,QAAQ,MAAM,EAAE;QACd,KAAK,MAAM;YACT,OAAO,MAAM,CAAC;QAChB,KAAK,MAAM;YACT,OAAO,OAAO,CAAC;QACjB,KAAK,IAAI;YACP,OAAO,IAAI,CAAC;QACd,KAAK,MAAM;YACT,OAAO,MAAM,CAAC;QAChB,KAAK,OAAO;YACV,OAAO,OAAO,CAAC;QACjB;YACE,MAAM,IAAI,KAAK,CAAC,2BAA2B,MAAM,EAAE,CAAC,CAAC;KACxD;AACH,CAAC;AAED,SAAgB,MAAM,CAAC,UAA8C,EAAE;IACrE,OAAO,UAAS,MAAY,EAAE,WAAmB;QAC/C,IAAI,OAAO,CAAC,WAAW,CAAC,YAAY,oBAAQ;YAAE,OAAO,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QAE1E,IAAI,CAAC,CAAC,MAAM,IAAI,OAAO,CAAC,EAAE;YACxB,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,aAAa,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;YACvE,OAAO,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;SACpC;QAED,gDAAgD;QAChD,IAAI,CAAC,CAAC,MAAM,IAAI,OAAO,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,OAAO,EAAE,CAAC,CAAC;QAE/E,sEAAsE;QACtE,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC;QACjC,MAAM,EAAE,UAAU,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,EAAE,CAAC,EAAE,GAAG,KAAK,CAAC;QACvD,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,WAAW,EAAE,GAAG,OAAO,CAAC;QACrD,UAAU,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,WAAW,EAAE,UAAU,EAAE,CAAC;IAC3D,CAAC,CAAC;AACJ,CAAC;AAlBD,wBAkBC;AAOD,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,kCAAsB,CAAC;AAE9D,SAAgB,OAAO,CAAC,OAA0B;IAChD,OAAO,UAAS,MAAY,EAAE,WAAmB;QAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC;QACjC,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE;YAC9B,GAAG,OAAO,CAAC,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC;YACtC,CAAC,WAAW,CAAC,EAAE,OAAO;SACvB,EAAE,KAAK,CAAC,CAAC;IACZ,CAAC,CAAA;AACH,CAAC;AARD,0BAQC;AAED,SAAgB,MAAM,CAAC,OAA0B;IAC/C,OAAO,UAAS,MAAY,EAAE,WAAmB;QAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC;QACjC,OAAO,CAAC,cAAc,CAAC,MAAM,EAAE;YAC7B,GAAG,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC;YACrC,CAAC,WAAW,CAAC,EAAE,OAAO;SACvB,EAAE,KAAK,CAAC,CAAC;IACZ,CAAC,CAAA;AACH,CAAC;AARD,wBAQC;AAED,SAAgB,SAAS,CAAC,OAA0B;IAClD,OAAO,UAAS,MAAY,EAAE,WAAmB;QAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC;QACjC,OAAO,CAAC,cAAc,CAAC,SAAS,EAAE;YAChC,GAAG,OAAO,CAAC,WAAW,CAAC,SAAS,EAAE,KAAK,CAAC;YACxC,CAAC,WAAW,CAAC,EAAE,OAAO;SACvB,EAAE,KAAK,CAAC,CAAC;IACZ,CAAC,CAAA;AACH,CAAC;AARD,8BAQC"}
@@ -0,0 +1,96 @@
1
+ import Bone from './bone';
2
+ import DataType from './data_types';
3
+ import { ASSOCIATE_METADATA_MAP } from './constants';
4
+ import 'reflect-metadata';
5
+
6
+ type DataTypes<T> = {
7
+ [Property in keyof T as Exclude<Property, "toSqlString">]: T[Property];
8
+ }
9
+
10
+ interface ColumnOption {
11
+ type?: DataTypes<DataType>;
12
+ name?: string;
13
+ defaultValue?: null | boolean | number | string | Date | JSON;
14
+ allowNull?: boolean;
15
+ }
16
+
17
+ function findType(tsType) {
18
+ const {
19
+ BIGINT, INTEGER,
20
+ DATE,
21
+ STRING,
22
+ BOOLEAN,
23
+ } = DataType;
24
+
25
+ switch (tsType) {
26
+ case BigInt:
27
+ return BIGINT;
28
+ case Number:
29
+ return INTEGER;
30
+ case Date:
31
+ return DATE;
32
+ case String:
33
+ return STRING;
34
+ case Boolean:
35
+ return BOOLEAN;
36
+ default:
37
+ throw new Error(`unknown typescript type ${tsType}`);
38
+ }
39
+ }
40
+
41
+ export function Column(options: ColumnOption | DataTypes<DataType> = {}) {
42
+ return function(target: Bone, propertyKey: string) {
43
+ if (options['prototype'] instanceof DataType) options = { type: options };
44
+
45
+ if (!('type' in options)) {
46
+ const tsType = Reflect.getMetadata('design:type', target, propertyKey);
47
+ options['type'] = findType(tsType);
48
+ }
49
+
50
+ // narrowing the type of options to ColumnOption
51
+ if (!('type' in options)) throw new Error(`unknown column options ${options}`);
52
+
53
+ // target refers to model prototype, an internal instance of `Bone {}`
54
+ const model = target.constructor;
55
+ const { attributes = (model.attributes = {}) } = model;
56
+ const { name: columnName, ...restOptions } = options;
57
+ attributes[propertyKey] = { ...restOptions, columnName };
58
+ };
59
+ }
60
+
61
+ interface AssociateOptions {
62
+ className?: string;
63
+ foreignKey?: string;
64
+ }
65
+
66
+ const { hasMany, hasOne, belongsTo } = ASSOCIATE_METADATA_MAP;
67
+
68
+ export function HasMany(options?: AssociateOptions) {
69
+ return function(target: Bone, propertyKey: string) {
70
+ const model = target.constructor;
71
+ Reflect.defineMetadata(hasMany, {
72
+ ...Reflect.getMetadata(hasMany, model),
73
+ [propertyKey]: options,
74
+ }, model);
75
+ }
76
+ }
77
+
78
+ export function HasOne(options?: AssociateOptions) {
79
+ return function(target: Bone, propertyKey: string) {
80
+ const model = target.constructor;
81
+ Reflect.defineMetadata(hasOne, {
82
+ ...Reflect.getMetadata(hasOne, model),
83
+ [propertyKey]: options,
84
+ }, model);
85
+ }
86
+ }
87
+
88
+ export function BelongsTo(options?: AssociateOptions) {
89
+ return function(target: Bone, propertyKey: string) {
90
+ const model = target.constructor;
91
+ Reflect.defineMetadata(belongsTo, {
92
+ ...Reflect.getMetadata(belongsTo, model),
93
+ [propertyKey]: options,
94
+ }, model);
95
+ }
96
+ }
@@ -9,6 +9,7 @@ const { snakeCase } = require('../../utils/string');
9
9
  * @param {string} dataType
10
10
  */
11
11
  function findJsType(DataTypes, type, dataType) {
12
+ if (type instanceof DataTypes.VIRTUAL) return '';
12
13
  if (type instanceof DataTypes.BOOLEAN) return Boolean;
13
14
  if (type instanceof DataTypes.JSON) return JSON;
14
15
  if (type instanceof DataTypes.BINARY || type instanceof DataTypes.BLOB) {
@@ -65,6 +66,8 @@ function createType(DataTypes, params) {
65
66
  switch (type.constructor.name) {
66
67
  case 'DATE':
67
68
  return new DataType(type.precision, type.timezone);
69
+ case 'DECIMAL':
70
+ return new DataType(type.precision, type.scale);
68
71
  case 'TINYINT':
69
72
  case 'SMALLINT':
70
73
  case 'MEDIUMINT':
@@ -72,6 +75,8 @@ function createType(DataTypes, params) {
72
75
  case 'BIGINT':
73
76
  case 'BINARY':
74
77
  case 'VARBINARY':
78
+ case 'CHAR':
79
+ case 'VARCHAR':
75
80
  return new DataType(type.length);
76
81
  default:
77
82
  return new DataType();
@@ -99,17 +104,26 @@ class Attribute {
99
104
  }
100
105
  const type = createType(DataTypes, params);
101
106
  const dataType = params.dataType || type.dataType;
107
+ let { defaultValue = null } = params;
108
+ try {
109
+ // normalize column defaults like `'0'` or `CURRENT_TIMESTAMP`
110
+ defaultValue = type.cast(type.uncast(defaultValue));
111
+ } catch {
112
+ defaultValue = null;
113
+ }
114
+
102
115
  Object.assign(this, {
103
116
  name,
104
- columnName,
105
117
  primaryKey: false,
106
- defaultValue: null,
107
118
  allowNull: !params.primaryKey,
108
- columnType: type.toSqlString().toLowerCase(),
109
119
  ...params,
120
+ columnName: params.columnName || columnName,
121
+ columnType: type.toSqlString().toLowerCase(),
110
122
  type,
123
+ defaultValue,
111
124
  dataType,
112
125
  jsType: findJsType(DataTypes, type, dataType),
126
+ virtual: type.virtual,
113
127
  });
114
128
  }
115
129
 
@@ -19,9 +19,12 @@ module.exports = {
19
19
  const chunks = [ `ALTER TABLE ${escapeId(table)}` ];
20
20
 
21
21
  const actions = Object.keys(attributes).map(name => {
22
- const attribute = new Attribute(name, attributes[name]);
22
+ const options = attributes[name];
23
+ // { [columnName]: { remove: true } }
24
+ if (options.remove) return `DROP COLUMN ${escapeId(name)}`;
25
+ const attribute = new Attribute(name, options);
23
26
  return [
24
- attribute.modify ? 'MODIFY COLUMN' : 'ADD COLUMN',
27
+ options.modify ? 'MODIFY COLUMN' : 'ADD COLUMN',
25
28
  attribute.toSqlString(),
26
29
  ].join(' ');
27
30
  });
@@ -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
 
@@ -15,8 +15,13 @@ class MysqlAttribute extends Attribute {
15
15
  }
16
16
 
17
17
  toSqlString() {
18
- const { allowNull, defaultValue, primaryKey } = this;
19
- const { columnName, type, columnType } = this;
18
+ const {
19
+ columnName,
20
+ type, columnType,
21
+ allowNull, defaultValue,
22
+ primaryKey,
23
+ comment,
24
+ } = this;
20
25
 
21
26
  const chunks = [
22
27
  escapeId(columnName),
@@ -33,6 +38,10 @@ class MysqlAttribute extends Attribute {
33
38
  chunks.push(`DEFAULT ${escape(defaultValue)}`);
34
39
  }
35
40
 
41
+ if (typeof comment === 'string') {
42
+ chunks.push(`COMMENT ${escape(comment)}`);
43
+ }
44
+
36
45
  return chunks.join(' ');
37
46
  }
38
47
  }
@@ -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
  }
@@ -38,7 +38,7 @@ module.exports = {
38
38
  columns.push({
39
39
  columnName: row.column_name,
40
40
  columnType: row.column_type,
41
- columnComment: row.column_comment,
41
+ comment: row.column_comment,
42
42
  defaultValue: row.column_default,
43
43
  dataType: row.data_type,
44
44
  allowNull: row.is_nullable === 'YES',
@@ -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
 
@@ -28,6 +28,10 @@ function formatAddColumn(driver, columnName, attribute) {
28
28
  return `ADD COLUMN ${attribute.toSqlString()}`;
29
29
  }
30
30
 
31
+ function formatDropColumn(driver, columnName) {
32
+ return `DROP COLUMN ${driver.escapeId(columnName)}`;
33
+ }
34
+
31
35
  module.exports = {
32
36
  ...schema,
33
37
 
@@ -85,7 +89,9 @@ module.exports = {
85
89
  const { escapeId } = this;
86
90
  const chunks = [ `ALTER TABLE ${escapeId(table)}` ];
87
91
  const actions = Object.keys(changes).map(name => {
88
- const attribute = new Attribute(name, changes[name]);
92
+ const options = changes[name];
93
+ if (options.remove) return formatDropColumn(this, name);
94
+ const attribute = new Attribute(name, options);
89
95
  const { columnName } = attribute;;
90
96
  return attribute.modify
91
97
  ? formatAlterColumns(this, columnName, attribute).join(', ')
@@ -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
 
@@ -108,7 +108,8 @@ module.exports = {
108
108
  for (let i = 0; i < tables.length; i++) {
109
109
  const table = tables[i];
110
110
  const { rows } = results[i];
111
- const columns = rows.map(({ name, type, notnull, dflt_value, pk }) => {
111
+ const columns = rows.map(row => {
112
+ const { name, type, notnull, dflt_value, pk } = row;
112
113
  const columnType = type.toLowerCase();
113
114
  const [, dataType, precision ] = columnType.match(rColumnType);
114
115
  const primaryKey = pk === 1;
@@ -145,6 +146,8 @@ module.exports = {
145
146
  const { escapeId } = this;
146
147
  const chunks = [ `ALTER TABLE ${escapeId(table)}` ];
147
148
  const attributes = Object.keys(changes).map(name => {
149
+ const options = changes[name];
150
+ if (options.remove) return { columnName: name, remove: true };
148
151
  return new Attribute(name, changes[name]);
149
152
  });
150
153
 
@@ -157,7 +160,12 @@ module.exports = {
157
160
  // SQLite can only add one column a time
158
161
  // - https://www.sqlite.org/lang_altertable.html
159
162
  for (const attribute of attributes) {
160
- await this.query(chunks.concat(`ADD COLUMN ${attribute.toSqlString()}`).join(' '));
163
+ if (attribute.remove) {
164
+ const { columnName } = attribute;
165
+ await this.query(chunks.concat(`DROP COLUMN ${this.escapeId(columnName)}`).join(' '));
166
+ } else {
167
+ await this.query(chunks.concat(`ADD COLUMN ${attribute.toSqlString()}`).join(' '));
168
+ }
161
169
  }
162
170
  },
163
171
 
@@ -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') {
@@ -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) {