@igojs/db 6.0.0-beta.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.
- package/README.md +153 -0
- package/examples/PaginatedOptimizedQueryExample.js +936 -0
- package/index.js +27 -0
- package/package.json +27 -0
- package/src/CacheStats.js +33 -0
- package/src/CachedQuery.js +40 -0
- package/src/DataTypes.js +23 -0
- package/src/Db.js +147 -0
- package/src/Model.js +261 -0
- package/src/PaginatedOptimizedQuery.js +902 -0
- package/src/PaginatedOptimizedSql.js +1352 -0
- package/src/Query.js +584 -0
- package/src/Schema.js +52 -0
- package/src/Sql.js +311 -0
- package/src/context.js +12 -0
- package/src/dbs.js +26 -0
- package/src/drivers/mysql.js +74 -0
- package/src/drivers/postgresql.js +70 -0
- package/src/migrations.js +140 -0
- package/test/AssociationsTest.js +301 -0
- package/test/CacheStatsTest.js +40 -0
- package/test/CachedQueryTest.js +49 -0
- package/test/JoinTest.js +207 -0
- package/test/ModelTest.js +510 -0
- package/test/PaginatedOptimizedQueryTest.js +1183 -0
- package/test/PerfTest.js +58 -0
- package/test/PostgreSqlTest.js +95 -0
- package/test/QueryTest.js +27 -0
- package/test/SimplifiedSyntaxTest.js +473 -0
- package/test/SqlTest.js +95 -0
- package/test/init.js +2 -0
package/src/Query.js
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
|
|
2
|
+
const _ = require('lodash');
|
|
3
|
+
|
|
4
|
+
const Sql = require('./Sql');
|
|
5
|
+
const dbs = require('./dbs');
|
|
6
|
+
const context = require('./context');
|
|
7
|
+
|
|
8
|
+
const DataTypes = require('./DataTypes');
|
|
9
|
+
|
|
10
|
+
//
|
|
11
|
+
const merge = (includes, includeParam) => {
|
|
12
|
+
// console.dir({MERGE: { includes, includeParam}}, { depth: 99 });
|
|
13
|
+
if (_.isString(includeParam)) {
|
|
14
|
+
if (!includes[includeParam]) {
|
|
15
|
+
includes[includeParam] = {};
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_.each(includeParam, (value, key) => {
|
|
21
|
+
if (includes[key]) {
|
|
22
|
+
if (_.isString(includes[key])) {
|
|
23
|
+
includes[key] = {};
|
|
24
|
+
}
|
|
25
|
+
merge(includes[key], value);
|
|
26
|
+
} else {
|
|
27
|
+
includes[key] = value;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
// console.dir({RESULT: { includes }}, { depth: 99 });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
//
|
|
35
|
+
module.exports = class Query {
|
|
36
|
+
|
|
37
|
+
constructor(modelClass, verb = 'select') {
|
|
38
|
+
this.modelClass = modelClass;
|
|
39
|
+
this.schema = modelClass.schema;
|
|
40
|
+
|
|
41
|
+
this.query = {
|
|
42
|
+
table: modelClass.schema.table,
|
|
43
|
+
select: null,
|
|
44
|
+
verb: verb,
|
|
45
|
+
where: [],
|
|
46
|
+
whereNot: [],
|
|
47
|
+
joins: [],
|
|
48
|
+
order: [],
|
|
49
|
+
distinct: null,
|
|
50
|
+
group: null,
|
|
51
|
+
includes: {},
|
|
52
|
+
options: {},
|
|
53
|
+
scopes: [ 'default' ]
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// filter on subclass
|
|
57
|
+
const key = _.findKey(this.schema.subclasses, { name: this.modelClass.name });
|
|
58
|
+
if (key) {
|
|
59
|
+
this.query.where.push({
|
|
60
|
+
[this.schema.subclass_column]: key
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// UPDATE
|
|
66
|
+
async update(values) {
|
|
67
|
+
this.query.verb = 'update';
|
|
68
|
+
this.values(values);
|
|
69
|
+
return await this.execute();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// DELETE
|
|
73
|
+
async delete() {
|
|
74
|
+
this.query.verb = 'delete';
|
|
75
|
+
return await this.execute();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// FROM
|
|
79
|
+
from(table) {
|
|
80
|
+
this.query.table = table;
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// WHERE
|
|
85
|
+
where(where, params) {
|
|
86
|
+
where = params !== undefined ? [where, params] : where;
|
|
87
|
+
this.query.where.push(where);
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// WHERE NOT
|
|
92
|
+
whereNot(whereNot) {
|
|
93
|
+
this.query.whereNot.push(whereNot);
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// VALUES
|
|
98
|
+
values(values) {
|
|
99
|
+
const { logger } = context;
|
|
100
|
+
this.query.values = _.transform(values, (result, value, key) => {
|
|
101
|
+
const column = this.schema.colsByAttr[key];
|
|
102
|
+
if (column) {
|
|
103
|
+
if (DataTypes[column.type]) {
|
|
104
|
+
result[column.name] = DataTypes[column.type].set(value);
|
|
105
|
+
} else {
|
|
106
|
+
// unknown type
|
|
107
|
+
logger.warn(`Unknown type '${column.type}' for column '${column.name}'`);
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
// unknown column (ignore)
|
|
111
|
+
}
|
|
112
|
+
}, {});
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// FIRST
|
|
117
|
+
async first() {
|
|
118
|
+
this.query.limit = 1;
|
|
119
|
+
this.query.take = 'first';
|
|
120
|
+
return await this.execute();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// LAST
|
|
124
|
+
async last() {
|
|
125
|
+
this.query.limit = 1;
|
|
126
|
+
this.query.take = 'last';
|
|
127
|
+
return await this.execute();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// LIMIT
|
|
131
|
+
limit(limit) {
|
|
132
|
+
this.query.limit = limit;
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// OFFSET
|
|
137
|
+
offset(offset) {
|
|
138
|
+
this.query.offset = offset;
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// PAGE
|
|
143
|
+
page(page, nb) {
|
|
144
|
+
this.query.page = parseInt(page, 10) || 1;
|
|
145
|
+
this.query.page = Math.max(1, this.query.page);
|
|
146
|
+
this.query.nb = parseInt(nb, 10) || 25;
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// SCOPE
|
|
151
|
+
scope(scope) {
|
|
152
|
+
this.query.scopes.push(scope);
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// UNSCOPED
|
|
157
|
+
unscoped() {
|
|
158
|
+
this.query.scopes.length = 0;
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// LIST
|
|
163
|
+
async list() {
|
|
164
|
+
return await this.execute();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// SELECT
|
|
168
|
+
select(select) {
|
|
169
|
+
this.query.select = select;
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// COUNT
|
|
174
|
+
async count() {
|
|
175
|
+
const countQuery = new Query(this.modelClass);
|
|
176
|
+
countQuery.query = _.cloneDeep(this.query);
|
|
177
|
+
|
|
178
|
+
countQuery.query.verb = 'count';
|
|
179
|
+
countQuery.query.limit = 1;
|
|
180
|
+
delete countQuery.query.page;
|
|
181
|
+
delete countQuery.query.nb;
|
|
182
|
+
|
|
183
|
+
const rows = await countQuery.execute();
|
|
184
|
+
const count = rows && rows[0] && Number(rows[0].count) || 0;
|
|
185
|
+
return count;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// JOIN
|
|
189
|
+
join(join, columns, type) {
|
|
190
|
+
if (_.isString(join)) {
|
|
191
|
+
return this.joinOne(join, columns, type);
|
|
192
|
+
} else if (_.isArray(join)) {
|
|
193
|
+
return this.joinMany(join, columns, type);
|
|
194
|
+
} else if (_.isObject(join)) {
|
|
195
|
+
return this.joinNested(join);
|
|
196
|
+
}
|
|
197
|
+
throw new Error('Invalid join argument. Must be a string, array, or object.');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// JOIN ONE
|
|
201
|
+
joinOne(associationName, columns, type = 'LEFT') {
|
|
202
|
+
const { query } = this;
|
|
203
|
+
const association = this._findAssociation(associationName, this.schema);
|
|
204
|
+
query.joins.push({ src_schema: this.schema, association, columns, type, src_alias: this.schema.table });
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// JOIN MANY
|
|
209
|
+
joinMany(associationNames, columns, type = 'LEFT') {
|
|
210
|
+
_.forEach(associationNames, name => this.joinOne(name, columns, type));
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// JOIN NESTED
|
|
215
|
+
joinNested(nestedAssociations) {
|
|
216
|
+
const processJoin = (join, current_schema, current_alias) => {
|
|
217
|
+
if (_.isString(join)) {
|
|
218
|
+
this._addJoin(join, null, 'LEFT', current_schema, current_alias);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (_.isArray(join)) {
|
|
223
|
+
join.forEach(j => processJoin(j, current_schema, current_alias));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (_.isObject(join)) {
|
|
228
|
+
_.each(join, (value, key) => {
|
|
229
|
+
const new_join_alias = key;
|
|
230
|
+
const association = this._addJoin(key, null, 'LEFT', current_schema, current_alias);
|
|
231
|
+
const next_schema = association[2].schema;
|
|
232
|
+
processJoin(value, next_schema, new_join_alias);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
processJoin(nestedAssociations, this.schema, this.schema.table);
|
|
237
|
+
return this;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Helper to find association
|
|
241
|
+
_findAssociation(associationName, src_schema) {
|
|
242
|
+
const association = _.find(src_schema.associations, assoc => assoc[1] === associationName);
|
|
243
|
+
if (!association) {
|
|
244
|
+
throw new Error(`Missing association '${associationName}' on '${src_schema.table}' schema.`);
|
|
245
|
+
}
|
|
246
|
+
if (association[0] !== 'belongs_to') {
|
|
247
|
+
throw new Error(`Association '${associationName}' on '${src_schema.table}' schema is not a 'belongs_to' association.`);
|
|
248
|
+
}
|
|
249
|
+
return association;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Helper to add join to query.joins
|
|
253
|
+
_addJoin(associationName, columns, type, src_schema, src_alias_for_join) {
|
|
254
|
+
const association = this._findAssociation(associationName, src_schema);
|
|
255
|
+
this.query.joins.push({ src_schema, association, columns, type, src_alias: src_alias_for_join });
|
|
256
|
+
return association;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// SCOPES
|
|
260
|
+
applyScopes() {
|
|
261
|
+
const { query, schema } = this;
|
|
262
|
+
_.forOwn(query.scopes, (scope) => {
|
|
263
|
+
if (!schema.scopes[scope]) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
schema.scopes[scope](this);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// INCLUDES
|
|
271
|
+
includes(includeParams) {
|
|
272
|
+
const { query } = this;
|
|
273
|
+
const pushInclude = includeParam => {
|
|
274
|
+
merge(query.includes, includeParam);
|
|
275
|
+
};
|
|
276
|
+
_.forEach(_.concat([], includeParams), pushInclude);
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// FIND
|
|
281
|
+
async find(id) {
|
|
282
|
+
if (id === null || id === undefined) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (_.isString(id) || _.isNumber(id)) {
|
|
287
|
+
return await this.where({ id }).first();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (_.isArray(id)) {
|
|
291
|
+
id = _.compact(id);
|
|
292
|
+
if (id.length === 0) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
return await this.where({ id }).first();
|
|
296
|
+
}
|
|
297
|
+
return await this.where(id).first();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ORDER BY
|
|
301
|
+
order(order) {
|
|
302
|
+
this.query.order.push(order);
|
|
303
|
+
return this;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// DISTINCT
|
|
307
|
+
distinct(columns) {
|
|
308
|
+
this.query.distinct = _.isArray(columns) ? columns : [ columns ];
|
|
309
|
+
return this;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// GROUP
|
|
313
|
+
group(columns) {
|
|
314
|
+
this.query.group = _.castArray(columns);
|
|
315
|
+
return this;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// QUERY OPTIONS
|
|
319
|
+
options(options) {
|
|
320
|
+
_.merge(this.query.options, options);
|
|
321
|
+
return this;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
getDb() {
|
|
325
|
+
return dbs[this.schema.database];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// generate SQL
|
|
329
|
+
toSQL() {
|
|
330
|
+
const { query } = this;
|
|
331
|
+
const db = this.getDb();
|
|
332
|
+
const sql = new Sql(this.query, db.driver.dialect)[this.query.verb + 'SQL']();
|
|
333
|
+
query.generated = sql;
|
|
334
|
+
return sql;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
//
|
|
338
|
+
async paginate() {
|
|
339
|
+
const { query } = this;
|
|
340
|
+
if (!query.page) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const count = await this.count();
|
|
345
|
+
const nb_pages = Math.ceil(count / query.nb);
|
|
346
|
+
query.page = Math.min(query.page, nb_pages);
|
|
347
|
+
query.page = Math.max(query.page, 1);
|
|
348
|
+
query.offset = (query.page - 1) * query.nb;
|
|
349
|
+
query.limit = query.nb;
|
|
350
|
+
|
|
351
|
+
const links = [];
|
|
352
|
+
const page = this.query.page;
|
|
353
|
+
const start = Math.max(1, page - 5);
|
|
354
|
+
for (let i = 0; i < 10; i++) {
|
|
355
|
+
const p = start + i;
|
|
356
|
+
if (p <= nb_pages) {
|
|
357
|
+
links.push({ page: p, current: page === p });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
page: this.query.page,
|
|
362
|
+
nb: this.query.nb,
|
|
363
|
+
previous: page > 1 ? page - 1 : null,
|
|
364
|
+
next: page < nb_pages ? page + 1 : null,
|
|
365
|
+
start: query.offset + 1,
|
|
366
|
+
end: query.offset + Math.min(query.nb, count - query.offset),
|
|
367
|
+
nb_pages,
|
|
368
|
+
count,
|
|
369
|
+
links,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
//
|
|
374
|
+
async loadAssociation(include, rows) {
|
|
375
|
+
|
|
376
|
+
let schema = this.schema;
|
|
377
|
+
let association = null;
|
|
378
|
+
let parts, path = null;
|
|
379
|
+
|
|
380
|
+
if (include.indexOf('.') !== -1) {
|
|
381
|
+
// nested include
|
|
382
|
+
parts = include.split('.');
|
|
383
|
+
path = parts.slice(0, parts.length - 1).join('.') + '.';
|
|
384
|
+
for (const part of parts) {
|
|
385
|
+
association = _.find(schema.associations, (assoc) => {
|
|
386
|
+
return assoc[1] === part;
|
|
387
|
+
});
|
|
388
|
+
schema = association ? association[2].schema : null;
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
association = _.find(schema.associations, (association) => {
|
|
392
|
+
return association[1] === include;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!association) {
|
|
397
|
+
throw new Error(`Missing association '${include}' on '${schema.table}' schema.`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const [type, attr, Obj, column = attr + '_id', ref_column = 'id', extraWhere] = association;
|
|
401
|
+
|
|
402
|
+
let column_path = column;
|
|
403
|
+
if (path) {
|
|
404
|
+
if (type === 'has_many') {
|
|
405
|
+
column_path = parts.slice(0, parts.length - 2).join('.');
|
|
406
|
+
if (column_path) {
|
|
407
|
+
column_path += '.';
|
|
408
|
+
}
|
|
409
|
+
column_path += ref_column;
|
|
410
|
+
} else {
|
|
411
|
+
column_path = path + column;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const ids = _.chain(rows).flatMap(column_path).uniq().compact().value();
|
|
417
|
+
const defaultValue = () => (type === 'has_many' ? [] : null);
|
|
418
|
+
|
|
419
|
+
if (ids.length === 0) {
|
|
420
|
+
_.forEach(rows, (row) => row[attr] = defaultValue());
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const where = {
|
|
425
|
+
[ref_column]: ids
|
|
426
|
+
};
|
|
427
|
+
const subincludes = this.query.includes[include];
|
|
428
|
+
let query = Obj.includes(subincludes).where(where);
|
|
429
|
+
if (extraWhere) {
|
|
430
|
+
query.where(extraWhere);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const objs = await query.list();
|
|
434
|
+
|
|
435
|
+
const objsByKey = {};
|
|
436
|
+
_.forEach(objs, (obj) => {
|
|
437
|
+
const key = obj[ref_column];
|
|
438
|
+
if (type === 'has_many') {
|
|
439
|
+
objsByKey[key] = objsByKey[key] || [];
|
|
440
|
+
objsByKey[key].push(obj);
|
|
441
|
+
} else {
|
|
442
|
+
objsByKey[key] = obj;
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const attr_path = path ? path + attr : attr;
|
|
447
|
+
_.forEach(rows, (row) => {
|
|
448
|
+
const value = _.get(row, column_path);
|
|
449
|
+
if (!Array.isArray(value)) {
|
|
450
|
+
_.set(row, attr_path, objsByKey[value] || defaultValue());
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
row[attr] = _.chain(value).flatMap(id => objsByKey[id]).compact().value();
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
//
|
|
457
|
+
async execute() {
|
|
458
|
+
const { query, schema } = this;
|
|
459
|
+
const db = this.getDb();
|
|
460
|
+
const { dialect } = db.driver;
|
|
461
|
+
const { esc } = dialect;
|
|
462
|
+
|
|
463
|
+
if (schema.scopes) {
|
|
464
|
+
this.applyScopes();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (query.order.length === 0 &&
|
|
468
|
+
(query.take === 'first' || query.take === 'last')) {
|
|
469
|
+
const order = query.take === 'first' ? 'ASC' : 'DESC';
|
|
470
|
+
// Default sort by primary key
|
|
471
|
+
_.forEach(schema.primary, (key) => {
|
|
472
|
+
query.order.push(`${esc}${schema.table}${esc}.${esc}${key}${esc} ${order}`);
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// force limit to 1 for first/last
|
|
477
|
+
if (query.take === 'first' || query.take === 'last') {
|
|
478
|
+
query.limit = 1;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const pagination = await this.paginate();
|
|
482
|
+
let rows = await this.runQuery();
|
|
483
|
+
|
|
484
|
+
if (query.verb === 'insert') {
|
|
485
|
+
const insertId = dialect.insertId(rows);
|
|
486
|
+
return { insertId };
|
|
487
|
+
} else if (query.verb !== 'select') {
|
|
488
|
+
return rows;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (query.distinct || query.group) {
|
|
492
|
+
return rows;
|
|
493
|
+
} else if (query.limit === 1 && (!rows || rows.length === 0)) {
|
|
494
|
+
return null;
|
|
495
|
+
} else if (query.verb === 'select') {
|
|
496
|
+
rows = _.each(rows, row => {
|
|
497
|
+
schema.parseTypes(row);
|
|
498
|
+
|
|
499
|
+
// parse joins values
|
|
500
|
+
_.forEach(this.query.joins, (join) => {
|
|
501
|
+
const { src_schema, association } = join;
|
|
502
|
+
const [assoc_type, name, Obj, src_column, column] = association;
|
|
503
|
+
Obj.schema.parseTypes(row, `${name}__`);
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (query.verb === 'select') {
|
|
509
|
+
rows = _.map(rows, row => {
|
|
510
|
+
const instance = this.newInstance(row);
|
|
511
|
+
|
|
512
|
+
if (this.query.joins.length === 0) {
|
|
513
|
+
return instance;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const createdInstances = new Map();
|
|
517
|
+
createdInstances.set(this.schema, instance);
|
|
518
|
+
|
|
519
|
+
_.forEach(this.query.joins, (join) => {
|
|
520
|
+
const { src_schema, association } = join;
|
|
521
|
+
const [assoc_type, name, Obj, src_column, column] = association;
|
|
522
|
+
const table_alias = name;
|
|
523
|
+
|
|
524
|
+
const params = {};
|
|
525
|
+
Obj.schema.columns.forEach(col => {
|
|
526
|
+
const alias = `${table_alias}__${col.attr}`;
|
|
527
|
+
params[col.attr] = row[alias];
|
|
528
|
+
delete instance[alias];
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
const joinInstance = this.newInstance(params, Obj);
|
|
532
|
+
|
|
533
|
+
const parentInstance = createdInstances.get(src_schema);
|
|
534
|
+
|
|
535
|
+
if (parentInstance) {
|
|
536
|
+
parentInstance[name] = joinInstance || null;
|
|
537
|
+
if (joinInstance) {
|
|
538
|
+
createdInstances.set(Obj.schema, joinInstance);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
return instance;
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Load associations
|
|
548
|
+
for (let include of _.keys(query.includes)) {
|
|
549
|
+
await this.loadAssociation(include, rows);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (pagination) {
|
|
553
|
+
return { pagination, rows };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (query.limit === 1) {
|
|
557
|
+
return rows[0];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return rows;
|
|
561
|
+
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// run the query
|
|
565
|
+
async runQuery() {
|
|
566
|
+
const { query } = this;
|
|
567
|
+
const sqlQuery = this.toSQL();
|
|
568
|
+
const db = this.getDb();
|
|
569
|
+
return await db.query(sqlQuery.sql, sqlQuery.params, query.options);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// new instance of model object
|
|
573
|
+
newInstance(row, instanceClass=this.modelClass) {
|
|
574
|
+
// let instanceClass = this.modelClass;
|
|
575
|
+
if (_.every(this.schema.primary, key => row[key] === null)) {
|
|
576
|
+
return null; // no primary key, no instance
|
|
577
|
+
}
|
|
578
|
+
const type = row[this.schema.subclass_column];
|
|
579
|
+
if (this.schema.subclasses && type) {
|
|
580
|
+
instanceClass = this.schema.subclasses[type];
|
|
581
|
+
}
|
|
582
|
+
return new instanceClass(row);
|
|
583
|
+
}
|
|
584
|
+
};
|
package/src/Schema.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
|
|
2
|
+
const _ = require('lodash');
|
|
3
|
+
|
|
4
|
+
const DataTypes = require('./DataTypes');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
module.exports = class Schema {
|
|
8
|
+
|
|
9
|
+
constructor(values) {
|
|
10
|
+
_.assign(this, values);
|
|
11
|
+
|
|
12
|
+
this.primary = values.primary || ['id'];
|
|
13
|
+
this.subclass_column = values.subclass_column || 'type';
|
|
14
|
+
this.database = values.database || 'main';
|
|
15
|
+
|
|
16
|
+
// Map columns
|
|
17
|
+
this.columns = _.map(values.columns, column => {
|
|
18
|
+
if (typeof column === 'object') {
|
|
19
|
+
column.attr = column.attr || column.name;
|
|
20
|
+
return column;
|
|
21
|
+
}
|
|
22
|
+
return { name: column, attr: column, type: 'default' };
|
|
23
|
+
});
|
|
24
|
+
this.colsByName = _.keyBy(this.columns, 'name');
|
|
25
|
+
this.colsByAttr = _.keyBy(this.columns, 'attr');
|
|
26
|
+
|
|
27
|
+
// asynchronous loading of associations for circular dependencies
|
|
28
|
+
process.nextTick(() => {
|
|
29
|
+
if (_.isFunction(values.associations)) {
|
|
30
|
+
this.associations = values.associations();
|
|
31
|
+
}
|
|
32
|
+
if (_.isFunction(values.subclasses)) {
|
|
33
|
+
this.subclasses = values.subclasses();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
parseTypes(row, prefix='') {
|
|
39
|
+
_.forOwn(row, (value, key) => {
|
|
40
|
+
if (prefix && !key.startsWith(prefix)) {
|
|
41
|
+
return; // skip if not prefixed
|
|
42
|
+
}
|
|
43
|
+
key = key.slice(prefix.length);
|
|
44
|
+
const column = this.colsByName[key];
|
|
45
|
+
const type = column && DataTypes[column.type];
|
|
46
|
+
if (type) {
|
|
47
|
+
row[prefix + column.attr] = type.get(value);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
};
|