leoric 1.9.0 → 1.12.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,38 @@
1
+ 1.12.0 / 2021-10-12
2
+ ===================
3
+
4
+ * feat: support custom fields query and sequelize mode export rawAttributes (#192)
5
+ * refactor: collection format query result (#194)
6
+ * refactor: object condition parsing and expression formatting (#191)
7
+
8
+ 1.11.1 / 2021-09-28
9
+ ===================
10
+
11
+ This version fixes lots of issues regarding logical operator in object conditions.
12
+
13
+ * fix: logical operator with multiple conditions such as (#190)
14
+ * fix: sequelize mode support HAVING, and select fields raw sql support (#187)
15
+ * fix: support len validator (#188)
16
+ * fix: normalize logical operator conditions before formatting with spellbook (#186)
17
+
18
+ 1.11.0 / 2021-09-24
19
+ ===================
20
+
21
+ * feat: support BINARY(length), VARBINARY(length), and BLOB (#169)
22
+ * fix: logic operate should adapt one argument (#183)
23
+ * fix: Bone.load() should be idempotent, make sure associations is intact (#184)
24
+ * fix: selected instance isNewRecord is false (#182)
25
+ * fix: set options.busyTimeout to mitigate SQLITE_BUSY (#176)
26
+ * fix: turn on long stack trace of sqlite driver (#175)
27
+ * docs: how to contribute (#180)
28
+
29
+ 1.10.0 / 2021-09-14
30
+ ===================
31
+
32
+ * feat: SQLite driver should emit "connection" event when new connection is created (#168)
33
+ * fix: bulkCreate(...records) should recognize custom setters (#168)
34
+ * fix: attribute.equals() check should ignore defaultValue (#172)
35
+
1
36
  1.9.0 / 2021-09-04
2
37
  ==================
3
38
 
@@ -74,7 +109,7 @@
74
109
  ==================
75
110
 
76
111
  * feat: support class static attributes and hooks (#131)
77
- * fix: names defined in Bone.attributes should be always enumerable (#128)
112
+ * fix: names defined in Bone.attributes should always be enumerable (#128)
78
113
  * chore: add quality badge to readme (#129)
79
114
 
80
115
  1.5.2 / 2021-07-02
package/Readme.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![NPM Downloads](https://img.shields.io/npm/dm/leoric.svg?style=flat)](https://www.npmjs.com/package/leoric)
5
5
  [![NPM Version](http://img.shields.io/npm/v/leoric.svg?style=flat)](https://www.npmjs.com/package/leoric)
6
6
  [![Build Status](https://travis-ci.org/cyjake/leoric.svg)](https://travis-ci.org/cyjake/leoric)
7
- [![Coverage Status](https://coveralls.io/repos/github/cyjake/leoric/badge.svg?branch=master)](https://coveralls.io/github/cyjake/leoric?branch=master)
7
+ [![codecov](https://codecov.io/gh/cyjake/leoric/branch/master/graph/badge.svg?token=OZZWTZTDS1)](https://codecov.io/gh/cyjake/leoric)
8
8
 
9
9
  Leoric is an object-relational mapping for Node.js, which is heavily influenced by Active Record of Ruby on Rails. See the [documentation](https://leoric.js.org) for detail.
10
10
 
package/index.js CHANGED
@@ -1,18 +1,16 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('fs').promises;
4
- const path = require('path');
5
3
 
6
4
  const Logger = require('./src/drivers/abstract/logger');
7
5
  const Spell = require('./src/spell');
8
6
  const Bone = require('./src/bone');
9
7
  const Collection = require('./src/collection');
10
8
  const { invokable: DataTypes } = require('./src/data_types');
11
- const { findDriver } = require('./src/drivers');
12
9
  const migrations = require('./src/migrations');
13
10
  const sequelize = require('./src/adapters/sequelize');
14
- const { camelCase, heresql } = require('./src/utils/string');
11
+ const { heresql } = require('./src/utils/string');
15
12
  const Hint = require('./src/hint');
13
+ const Realm = require('./src/realm');
16
14
 
17
15
  /**
18
16
  * @typedef {Object} RawSql
@@ -21,233 +19,6 @@ const Hint = require('./src/hint');
21
19
  * @property {string} type
22
20
  */
23
21
 
24
- /**
25
- *
26
- * @typedef {Object} QueryResult
27
- * @property {Array} rows
28
- * @property {Array} fields
29
- */
30
-
31
- /**
32
- * find models in directory
33
- * @param {string} dir
34
- * @returns {Array.<Bone>}
35
- */
36
- async function findModels(dir) {
37
- if (!dir || typeof dir !== 'string') {
38
- throw new Error(`Unexpected dir (${dir})`);
39
- }
40
- const entries = await fs.readdir(dir, { withFileTypes: true });
41
- const models = [];
42
-
43
- for (const entry of entries) {
44
- const extname = path.extname(entry.name);
45
- if (entry.isFile() && ['.js', '.mjs'].includes(extname)) {
46
- const model = require(path.join(dir, entry.name));
47
- if (model.prototype instanceof Bone) models.push(model);
48
- }
49
- }
50
-
51
- return models;
52
- }
53
-
54
- function initAttributes(model, columns) {
55
- const attributes = {};
56
-
57
- for (const columnInfo of columns) {
58
- const { columnName, dataType, defaultValue, ...restInfo } = columnInfo;
59
- const name = columnName == '_id' ? columnName : camelCase(columnName);
60
- // leave out defaultValue to let database take over the default
61
- attributes[name] = {
62
- ...restInfo,
63
- columnName,
64
- dataType,
65
- type: model.driver.DataTypes.findType(dataType),
66
- };
67
- }
68
-
69
- model.init(attributes);
70
- }
71
-
72
- async function loadModels(Spine, models, opts) {
73
- const { database } = opts;
74
- const tables = models.map(model => model.physicTable);
75
- const schemaInfo = await Spine.driver.querySchemaInfo(database, tables);
76
-
77
- for (const model of models) {
78
- const columns = schemaInfo[model.physicTable];
79
- if (!model.attributes) initAttributes(model, columns);
80
- model.load(columns);
81
- Spine.models[model.name] = model;
82
- }
83
-
84
- for (const model of models) {
85
- model.initialize();
86
- }
87
- }
88
-
89
- function createSpine(opts) {
90
- if (opts.Bone && opts.Bone.prototype instanceof Bone) return opts.Bone;
91
- if (opts.sequelize) return sequelize(Bone);
92
- if (opts.subclass !== true) return Bone;
93
- return class Spine extends Bone {};
94
- }
95
-
96
- const rReplacementKey = /\s:(\w+)\b/g;
97
-
98
- class Realm {
99
- constructor(opts = {}) {
100
- const { client, dialect, database, ...restOpts } = {
101
- dialect: 'mysql',
102
- database: opts.db || opts.storage,
103
- ...opts
104
- };
105
- const Spine = createSpine(opts);
106
- const models = {};
107
-
108
- const driver = new (findDriver(dialect))({
109
- client,
110
- database,
111
- ...restOpts
112
- });
113
-
114
- const options = {
115
- client,
116
- dialect,
117
- database,
118
- ...restOpts,
119
- define: { underscored: true, ...opts.define },
120
- };
121
-
122
- this.Bone = Spine;
123
- this.models = Spine.models = models;
124
- this.driver = Spine.driver = driver;
125
- this.options = Spine.options = options;
126
- }
127
-
128
- define(name, attributes, opts = {}, descriptors = {}) {
129
- const Model = class extends this.Bone {
130
- static name = name;
131
- };
132
- Model.init(attributes, opts, descriptors);
133
- this.Bone.models[name] = Model;
134
- return Model;
135
- }
136
-
137
- async connect() {
138
- const { models: dir } = this.options;
139
-
140
- let models;
141
- if (dir) {
142
- models = Array.isArray(dir) ? dir : (await findModels(dir));
143
- } else {
144
- models = Object.values(this.models);
145
- }
146
-
147
- if (models.length > 0) {
148
- await loadModels(this.Bone, models, this.options);
149
- }
150
- this.connected = true;
151
- return this.Bone;
152
- }
153
-
154
- async sync() {
155
- if (!this.connected) await this.connect();
156
- const { models } = this;
157
-
158
- for (const model of Object.values(models)) {
159
- await model.sync();
160
- }
161
- }
162
-
163
- /**
164
- * raw query
165
- * @param {string} query
166
- * @param {Array<any>} values
167
- * @param {Connection} opts.connection specific connection of this query, may used in a transaction
168
- * @param {Bone} opts.model target model to inject values
169
- * @returns {QueryResult}
170
- * @memberof Realm
171
- */
172
- async query(query, values, opts = {}) {
173
- if (values && typeof values === 'object' && !Array.isArray(values)) {
174
- if ('replacements' in values) {
175
- const { model, connection } = values;
176
- opts.replacements = values.replacements;
177
- if (model) opts.model = model;
178
- if (connection) opts.connection = connection;
179
- } else {
180
- opts.replacements = values;
181
- }
182
- values = [];
183
- }
184
-
185
- const replacements = opts.replacements || {};
186
- query = query.replace(rReplacementKey, function replacer(m, key) {
187
- if (!replacements.hasOwnProperty(key)) {
188
- throw new Error(`unable to replace :${key}`);
189
- }
190
- values.push(replacements[key]);
191
- return '?';
192
- });
193
-
194
- const { rows, ...restRes } = await this.driver.query(query, values, opts);
195
- const results = [];
196
-
197
- if (rows && rows.length && opts.model && opts.model.prototype instanceof this.Bone) {
198
- const { attributeMap } = opts.model;
199
-
200
- for (const data of rows) {
201
- const instance = opts.model.instantiate(data);
202
- for (const key in data) {
203
- if (!attributeMap.hasOwnProperty(key)) instance[key] = data[key];
204
- }
205
- results.push(instance);
206
- }
207
- }
208
-
209
- return {
210
- rows: results.length > 0 ? results : rows,
211
- ...restRes
212
- };
213
- }
214
-
215
- async transaction(callback) {
216
- return await this.Bone.transaction(callback);
217
- }
218
-
219
- /**
220
- * raw sql object
221
- * @static
222
- * @param {string} sql
223
- * @returns {RawSql}
224
- * @memberof Realm
225
- */
226
- static raw(sql) {
227
- if (typeof sql !== 'string') {
228
- throw new TypeError('sql must be a string');
229
- }
230
- return {
231
- __raw: true,
232
- value: sql,
233
- type: 'raw',
234
- };
235
- }
236
-
237
- // instance.raw
238
- raw(sql) {
239
- return Realm.raw(sql);
240
- }
241
- /**
242
- * escape value
243
- * @param {string} value
244
- * @returns {string} escaped value
245
- * @memberof Realm
246
- */
247
- escape(value) {
248
- return this.driver.escape(value);
249
- }
250
- }
251
22
 
252
23
  /**
253
24
  * Connect models to database. Need to provide both connect options and models.
@@ -261,7 +32,8 @@ const connect = async function connect(opts) {
261
32
  opts = { Bone, ...opts };
262
33
  if (opts.Bone.driver) throw new Error('connected already');
263
34
  const realm = new Realm(opts);
264
- return await realm.connect();
35
+ await realm.connect();
36
+ return realm;
265
37
  };
266
38
 
267
39
  Object.assign(Realm.prototype, migrations, { DataTypes });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leoric",
3
- "version": "1.9.0",
3
+ "version": "1.12.0",
4
4
  "description": "JavaScript Object-relational mapping alchemy",
5
5
  "main": "index.js",
6
6
  "types": "types/index.d.ts",
@@ -20,6 +20,7 @@
20
20
  "test:mysql2": "./test/start.sh test/integration/mysql2.test.js",
21
21
  "test:postgres": "./test/start.sh test/integration/postgres.test.js",
22
22
  "test:sqlite": "./test/start.sh test/integration/sqlite.test.js",
23
+ "test:dts": "./test/start.sh test/types/dts.test.js",
23
24
  "test:coverage": "nyc ./test/start.sh && nyc report --reporter=lcov",
24
25
  "lint": "eslint ./",
25
26
  "lint:fix": "eslint . --fix"
@@ -56,6 +57,9 @@
56
57
  "@babel/core": "^7.14.6",
57
58
  "@babel/eslint-parser": "^7.14.7",
58
59
  "@cara/minami": "^1.2.3",
60
+ "@journeyapps/sqlcipher": "^5.2.0",
61
+ "@types/node": "^16.10.1",
62
+ "coffee": "^5.4.0",
59
63
  "dayjs": "^1.10.3",
60
64
  "eslint": "^7.20.0",
61
65
  "expect.js": "^0.3.1",
@@ -66,6 +70,7 @@
66
70
  "nyc": "^15.1.0",
67
71
  "pg": "^8.5.1",
68
72
  "sinon": "^10.0.0",
69
- "sqlite3": "^5.0.2"
73
+ "sqlite3": "^5.0.2",
74
+ "typescript": "^4.4.3"
70
75
  }
71
76
  }
@@ -4,14 +4,19 @@ const { setupSingleHook } = require('../setup_hooks');
4
4
  const { compose, isPlainObject } = require('../utils');
5
5
 
6
6
  function translateOptions(spell, options) {
7
- const { attributes, where, group, order, offset, limit, include } = options;
7
+ const { attributes, where, group, order, offset, limit, include, having } = options;
8
8
 
9
9
  if (attributes) spell.$select(attributes);
10
10
  if (include) {
11
11
  if (typeof include === 'string') spell.$with(include);
12
12
  }
13
13
  if (where) spell.$where(where);
14
- if (group) spell.$group(group);
14
+ if (group) {
15
+ if (Array.isArray(group)) spell.$group(...group);
16
+ else spell.$group(group);
17
+ }
18
+ if (having) spell.$having(having);
19
+
15
20
  if (order) {
16
21
  if (typeof order === 'string') {
17
22
  spell.$order(order);
@@ -125,6 +130,10 @@ module.exports = Bone => {
125
130
  return this;
126
131
  }
127
132
 
133
+ static get rawAttributes() {
134
+ return this.attributes;
135
+ }
136
+
128
137
  /**
129
138
  * add scope see https://sequelize.org/master/class/lib/model.js~Model.html#static-method-addScope
130
139
  *
package/src/bone.js CHANGED
@@ -914,7 +914,7 @@ class Bone {
914
914
  */
915
915
  static load(columns = []) {
916
916
  const { Attribute } = this.driver;
917
- const { attributes, options } = this;
917
+ const { associations = {}, attributes, options } = this;
918
918
  const attributeMap = {};
919
919
  const table = this.table || snakeCase(pluralize(this.name));
920
920
  const tableAlias = camelCase(pluralize(this.name || table));
@@ -943,7 +943,7 @@ class Bone {
943
943
  primaryKey,
944
944
  columns,
945
945
  attributeMap,
946
- associations: [],
946
+ associations,
947
947
  tableAlias,
948
948
  synchronized: Object.keys(compare(attributes, columns)).length === 0,
949
949
  }));
@@ -1147,20 +1147,31 @@ class Bone {
1147
1147
  * @return {Bone}
1148
1148
  */
1149
1149
  static instantiate(row) {
1150
- const { attributes, driver } = this;
1150
+ const { attributes, driver, attributeMap } = this;
1151
1151
  const instance = new this();
1152
1152
 
1153
- for (const name in attributes) {
1154
- const { columnName, jsType } = attributes[name];
1155
- if (columnName in row) {
1153
+ for (const columnName in row) {
1154
+ const value = row[columnName];
1155
+ if (attributeMap.hasOwnProperty(columnName)) {
1156
+ const { jsType, name } = attributeMap[columnName];
1156
1157
  // to make sure raw and rawSaved hold two different objects
1157
- instance._setRaw(name, driver.cast(row[columnName], jsType));
1158
- instance._setRawSaved(name, driver.cast(row[columnName], jsType));
1158
+ instance._setRaw(name, driver.cast(value, jsType));
1159
+ instance._setRawSaved(name, driver.cast(value, jsType));
1159
1160
  } else {
1161
+ if (!isNaN(value)) instance[columnName] = Number(value);
1162
+ else if (!isNaN(Date.parse(value))) instance[columnName] = new Date(value);
1163
+ else instance[columnName] = value;
1164
+ }
1165
+ }
1166
+
1167
+ for (const name in attributes) {
1168
+ const { columnName } = attributes[name];
1169
+ if (!(columnName in row)) {
1160
1170
  instance._getRawUnset().add(name);
1161
1171
  }
1162
1172
  }
1163
1173
 
1174
+ instance.isNewRecord = false;
1164
1175
  return instance;
1165
1176
  }
1166
1177
 
@@ -1313,16 +1324,16 @@ class Bone {
1313
1324
  opts.uniqueKeys = opts.uniqueKeys.map((field) => this.unalias(field));
1314
1325
  }
1315
1326
 
1327
+ // records might change when filter through custom setters
1328
+ records = instances.map(instance => instance.getRaw());
1329
+
1316
1330
  // bulk create with instances is possible only if
1317
1331
  // 1) either all of records primary key are set
1318
1332
  // 2) or none of records priamry key is set and primary key is auto incremented
1319
1333
  if (!(autoIncrement && unset || allset)) {
1320
1334
  // validate first
1321
1335
  if (options.validate !== false) {
1322
- records.map(record => {
1323
- if (record instanceof Bone) record._validateAttributes();
1324
- else this._validateAttributes(record);
1325
- });
1336
+ for (const record of records) this._validateAttributes(record);
1326
1337
  }
1327
1338
  return await new Spell(this, options).$bulkInsert(records);
1328
1339
  }
@@ -1493,7 +1504,7 @@ class Bone {
1493
1504
 
1494
1505
  const { attributes, columns } = this;
1495
1506
 
1496
- if (Object.keys(columns).length === 0) {
1507
+ if (columns.length === 0) {
1497
1508
  await driver.createTable(table, attributes);
1498
1509
  } else {
1499
1510
  await driver.alterTable(table, compare(attributes, columns));