nodester 0.2.3 → 0.2.5
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 +4 -4
- package/lib/factories/responses/rest.js +7 -11
- package/lib/middlewares/ql/sequelize/interpreter/QueryLexer.js +118 -43
- package/lib/query/traverse.js +63 -48
- package/lib/tools/nql.tool.js +34 -21
- package/package.json +1 -1
- package/tests/{ast.test.js → ast.js} +4 -2
- package/tests/nql.test.js +442 -224
package/Readme.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/nodester)
|
|
4
4
|
[](https://www.npmjs.com/package/nodester)
|
|
5
5
|
|
|
6
|
-
> **nodester** is a
|
|
6
|
+
> **nodester** is a Node.js framework designed to solve the problem of a complex data querying over HTTP.
|
|
7
7
|
|
|
8
|
-
The main reason of nodester's existence is the [nodester Query Language (NQL)](docs/
|
|
8
|
+
The main reason of nodester's existence is the [nodester Query Language (NQL)](docs/nql/Introduction.md), an extension of standard REST API syntax, it lets you craft complex queries with hierarchical associations.
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
## Installation
|
|
@@ -49,11 +49,11 @@ app.listen(8080, function() {
|
|
|
49
49
|
[Core concepts documentation ➡️](docs/CoreConcepts.md)
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
### Queries & Querying -
|
|
52
|
+
### Queries & Querying - nodester Query Language (NQL)
|
|
53
53
|
The true strength of nodester lies in its query language. Serving as an extension of standard REST API syntax, it brings many aspects of SQL into REST requests, providing developers with a simple yet potent tool for expressive and efficient data querying.
|
|
54
54
|
|
|
55
55
|
Read more about it in the documentation:
|
|
56
|
-
[NQL documentaion ➡️](docs/
|
|
56
|
+
[NQL documentaion ➡️](docs/nql/Introduction.md)
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
### Database
|
|
@@ -62,29 +62,27 @@ function _createGenericResponse(res, options) {
|
|
|
62
62
|
|
|
63
63
|
switch(error.name) {
|
|
64
64
|
case 'Unauthorized': {
|
|
65
|
-
|
|
65
|
+
status = 401;
|
|
66
66
|
break;
|
|
67
67
|
}
|
|
68
68
|
case 'NotFound': {
|
|
69
|
-
|
|
69
|
+
status = 404;
|
|
70
70
|
break;
|
|
71
71
|
}
|
|
72
72
|
case 'ValidationError': {
|
|
73
|
-
|
|
73
|
+
status = 422;
|
|
74
74
|
break;
|
|
75
75
|
}
|
|
76
76
|
case 'ConflictError': {
|
|
77
|
-
|
|
77
|
+
status = 409;
|
|
78
78
|
break;
|
|
79
79
|
}
|
|
80
80
|
case 'SequelizeUniqueConstraintError': {
|
|
81
|
-
|
|
81
|
+
status = 409;
|
|
82
82
|
details.errors = error?.errors;
|
|
83
83
|
break;
|
|
84
84
|
}
|
|
85
85
|
default:
|
|
86
|
-
statusCode = status;
|
|
87
|
-
|
|
88
86
|
if (!!error?.errors) {
|
|
89
87
|
details.errors = error?.errors;
|
|
90
88
|
}
|
|
@@ -121,8 +119,7 @@ function _createGenericResponse(res, options) {
|
|
|
121
119
|
* @api public
|
|
122
120
|
*/
|
|
123
121
|
function _createOKResponse(res, options={}) {
|
|
124
|
-
|
|
125
|
-
return this.createGenericResponse(res, {
|
|
122
|
+
return _createGenericResponse(res, {
|
|
126
123
|
...options,
|
|
127
124
|
status: options?.status ?? 200,
|
|
128
125
|
});
|
|
@@ -143,8 +140,7 @@ function _createOKResponse(res, options={}) {
|
|
|
143
140
|
* @api public
|
|
144
141
|
*/
|
|
145
142
|
function _createErrorResponse(res, options) {
|
|
146
|
-
|
|
147
|
-
return this.createGenericResponse(res, {
|
|
143
|
+
return _createGenericResponse(res, {
|
|
148
144
|
...options,
|
|
149
145
|
status: options?.status ?? 500,
|
|
150
146
|
});
|
|
@@ -11,6 +11,15 @@ const { ModelsTree, ModelsTreeNode } = require('./ModelsTree');
|
|
|
11
11
|
const util = require('util');
|
|
12
12
|
const debug = require('debug')('nodester:interpreter:QueryLexer');
|
|
13
13
|
|
|
14
|
+
const PARAM_TOKENS = new Enum({
|
|
15
|
+
FIELDS: Symbol('fields'),
|
|
16
|
+
INCLUDES: Symbol('includes'),
|
|
17
|
+
LIMIT: Symbol('limit'),
|
|
18
|
+
ORDER: Symbol('order'),
|
|
19
|
+
ORDER_BY: Symbol('order_by'),
|
|
20
|
+
SKIP: Symbol('skip'),
|
|
21
|
+
});
|
|
22
|
+
|
|
14
23
|
const OP_TOKENS = new Enum({
|
|
15
24
|
AND: 'and',
|
|
16
25
|
BETWEEN: 'between',
|
|
@@ -21,8 +30,14 @@ const OP_TOKENS = new Enum({
|
|
|
21
30
|
XOR: 'xor',
|
|
22
31
|
NOT: 'not',
|
|
23
32
|
NOT_MARK: '!',
|
|
33
|
+
|
|
34
|
+
IN: 'in',
|
|
24
35
|
NOT_IN: 'notIn',
|
|
36
|
+
|
|
25
37
|
LIKE: 'like',
|
|
38
|
+
NOT_LIKE: 'notLike',
|
|
39
|
+
NOT_LIKE_SHORT: '!like',
|
|
40
|
+
|
|
26
41
|
GREATER: 'gt',
|
|
27
42
|
GREATER_OR_EQUAL: 'gte',
|
|
28
43
|
LOWER: 'lt',
|
|
@@ -59,13 +74,12 @@ module.exports = class QueryLexer {
|
|
|
59
74
|
|
|
60
75
|
parseIsolatedQuery(queryString='', startAt=0, tree) {
|
|
61
76
|
const isSubQuery = tree.node.model !== 'root';
|
|
62
|
-
debug({ isSubQuery, startAt });
|
|
63
77
|
|
|
64
|
-
// Token is String, accumulated char-by-char.
|
|
78
|
+
// Token is a String, accumulated char-by-char.
|
|
65
79
|
let token = '';
|
|
66
80
|
// Value of param ('id=10' OR 'fields=id,text').
|
|
67
81
|
let value = [];
|
|
68
|
-
// Model, that was active before cursor went up in the tree.
|
|
82
|
+
// Model, that was active before a cursor went up in the tree.
|
|
69
83
|
let previousActive = null;
|
|
70
84
|
|
|
71
85
|
for (let i=startAt; i < queryString.length; i++) {
|
|
@@ -78,7 +92,7 @@ module.exports = class QueryLexer {
|
|
|
78
92
|
|
|
79
93
|
// If OP token:
|
|
80
94
|
if (OP_TOKENS.asArray.indexOf(token) > -1) {
|
|
81
|
-
// Set
|
|
95
|
+
// Set operator token.
|
|
82
96
|
tree.node.op = this.parseOP(token);
|
|
83
97
|
token = '';
|
|
84
98
|
continue;
|
|
@@ -127,6 +141,7 @@ module.exports = class QueryLexer {
|
|
|
127
141
|
switch (tree.node.op) {
|
|
128
142
|
case OP_TOKENS.NOT:
|
|
129
143
|
case OP_TOKENS.LIKE:
|
|
144
|
+
case OP_TOKENS.NOT_LIKE:
|
|
130
145
|
case OP_TOKENS.GREATER:
|
|
131
146
|
case OP_TOKENS.GREATER_OR_EQUAL:
|
|
132
147
|
case OP_TOKENS.LOWER:
|
|
@@ -143,7 +158,7 @@ module.exports = class QueryLexer {
|
|
|
143
158
|
|
|
144
159
|
// Reset:
|
|
145
160
|
tree.node.resetOP();
|
|
146
|
-
tree.node.activeParam =
|
|
161
|
+
tree.node.activeParam = PARAM_TOKENS.INCLUDES;
|
|
147
162
|
token = '';
|
|
148
163
|
value = [];
|
|
149
164
|
continue;
|
|
@@ -177,41 +192,54 @@ module.exports = class QueryLexer {
|
|
|
177
192
|
|
|
178
193
|
// Reset:
|
|
179
194
|
tree.node.resetFN();
|
|
180
|
-
tree.node.activeParam =
|
|
195
|
+
tree.node.activeParam = PARAM_TOKENS.INCLUDES;
|
|
181
196
|
token = '';
|
|
182
197
|
value = [];
|
|
183
198
|
continue;
|
|
184
199
|
}
|
|
185
200
|
|
|
186
201
|
// If end of subquery:
|
|
187
|
-
if (!!tree.node.activeParam && tree.node.activeParam !==
|
|
202
|
+
if (!!tree.node.activeParam && tree.node.activeParam !== PARAM_TOKENS.INCLUDES) {
|
|
188
203
|
// Set value.
|
|
189
204
|
this.setNodeParam(tree.node, token, value);
|
|
205
|
+
|
|
190
206
|
// Reset:
|
|
191
207
|
tree.node.resetActiveParam();
|
|
192
208
|
tree.node.resetOP();
|
|
209
|
+
|
|
210
|
+
// Lift from subquery.
|
|
211
|
+
tree.up();
|
|
193
212
|
}
|
|
194
213
|
const numberOfProcessedChars = i - startAt;
|
|
195
214
|
return [ numberOfProcessedChars ];
|
|
196
215
|
}
|
|
197
216
|
|
|
198
217
|
// , can mean n-th value in value array,
|
|
199
|
-
// or it can be n-th key-value pair in subquery,
|
|
200
218
|
// or horizontal include:
|
|
201
219
|
if (char === ',') {
|
|
202
220
|
debug('char', char, { token, node: tree.node });
|
|
203
221
|
|
|
204
222
|
// If OP token:
|
|
205
223
|
if (!!tree.node.op) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
224
|
+
switch(tree.node.op) {
|
|
225
|
+
case OP_TOKENS.NOT_IN:
|
|
226
|
+
case OP_TOKENS.IN:
|
|
227
|
+
value.push(token);
|
|
228
|
+
break;
|
|
229
|
+
default:
|
|
230
|
+
value.push({
|
|
231
|
+
[tree.node.activeParam]: [token]
|
|
232
|
+
});
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Reset.
|
|
209
237
|
token = '';
|
|
210
238
|
continue;
|
|
211
239
|
}
|
|
212
240
|
|
|
213
241
|
// If param value:
|
|
214
|
-
if (tree.node.activeParam !==
|
|
242
|
+
if (tree.node.activeParam !== PARAM_TOKENS.INCLUDES) {
|
|
215
243
|
value.push(token);
|
|
216
244
|
token = '';
|
|
217
245
|
continue;
|
|
@@ -223,7 +251,7 @@ module.exports = class QueryLexer {
|
|
|
223
251
|
}
|
|
224
252
|
|
|
225
253
|
// Horizontal include:
|
|
226
|
-
if (tree.node.activeParam ===
|
|
254
|
+
if (tree.node.activeParam === PARAM_TOKENS.INCLUDES) {
|
|
227
255
|
const model = token;
|
|
228
256
|
tree.use(model) ?? tree.include(model);
|
|
229
257
|
|
|
@@ -232,7 +260,7 @@ module.exports = class QueryLexer {
|
|
|
232
260
|
tree.node.resetActiveParam();
|
|
233
261
|
tree.upToRoot();
|
|
234
262
|
|
|
235
|
-
tree.node.activeParam =
|
|
263
|
+
tree.node.activeParam = PARAM_TOKENS.INCLUDES;
|
|
236
264
|
|
|
237
265
|
token = '';
|
|
238
266
|
continue;
|
|
@@ -250,7 +278,7 @@ module.exports = class QueryLexer {
|
|
|
250
278
|
// Vertical include:
|
|
251
279
|
if (!!previousActive) {
|
|
252
280
|
tree.use(previousActive);
|
|
253
|
-
tree.node.activeParam =
|
|
281
|
+
tree.node.activeParam = PARAM_TOKENS.INCLUDES;
|
|
254
282
|
token = '';
|
|
255
283
|
continue;
|
|
256
284
|
}
|
|
@@ -261,7 +289,7 @@ module.exports = class QueryLexer {
|
|
|
261
289
|
tree.use(model) ?? tree.include(model).use(model);
|
|
262
290
|
|
|
263
291
|
// Prepare for more includes:
|
|
264
|
-
tree.node.activeParam =
|
|
292
|
+
tree.node.activeParam = PARAM_TOKENS.INCLUDES;
|
|
265
293
|
|
|
266
294
|
token = '';
|
|
267
295
|
continue;
|
|
@@ -283,7 +311,7 @@ module.exports = class QueryLexer {
|
|
|
283
311
|
tree.up();
|
|
284
312
|
|
|
285
313
|
// Prepare for more includes:
|
|
286
|
-
tree.node.activeParam =
|
|
314
|
+
tree.node.activeParam = PARAM_TOKENS.INCLUDES;
|
|
287
315
|
|
|
288
316
|
token = '';
|
|
289
317
|
continue;
|
|
@@ -295,12 +323,12 @@ module.exports = class QueryLexer {
|
|
|
295
323
|
}
|
|
296
324
|
|
|
297
325
|
tree.up();
|
|
298
|
-
tree.node.activeParam =
|
|
326
|
+
tree.node.activeParam = PARAM_TOKENS.INCLUDES;
|
|
299
327
|
|
|
300
328
|
continue;
|
|
301
329
|
}
|
|
302
330
|
|
|
303
|
-
// & can mean the end of key=value pair,
|
|
331
|
+
// & can mean the end of key=value pair in root and sub query,
|
|
304
332
|
// or the end of subincludes:
|
|
305
333
|
if (char === '&') {
|
|
306
334
|
debug('char', char, { token, node: tree.node });
|
|
@@ -312,7 +340,7 @@ module.exports = class QueryLexer {
|
|
|
312
340
|
}
|
|
313
341
|
|
|
314
342
|
// If end of key=value pair:
|
|
315
|
-
if (!!tree.node.activeParam && tree.node.activeParam !==
|
|
343
|
+
if (!!tree.node.activeParam && tree.node.activeParam !== PARAM_TOKENS.INCLUDES) {
|
|
316
344
|
// Set value.
|
|
317
345
|
this.setNodeParam(tree.node, token, value);
|
|
318
346
|
// Reset:
|
|
@@ -321,13 +349,19 @@ module.exports = class QueryLexer {
|
|
|
321
349
|
value = [];
|
|
322
350
|
continue;
|
|
323
351
|
}
|
|
324
|
-
else if (tree.node.activeParam ===
|
|
325
|
-
// If
|
|
352
|
+
else if (tree.node.activeParam === PARAM_TOKENS.INCLUDES) {
|
|
353
|
+
// If token has some chars,
|
|
354
|
+
// then it's include of a new model:
|
|
326
355
|
if (token.length > 0) {
|
|
327
356
|
const model = token;
|
|
328
357
|
// Just include, no use.
|
|
329
358
|
tree.include(model);
|
|
330
359
|
}
|
|
360
|
+
// If token is empty,
|
|
361
|
+
// it's most possibly a subquery
|
|
362
|
+
else {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
331
365
|
|
|
332
366
|
// Then jump to root.
|
|
333
367
|
tree.upToRoot();
|
|
@@ -357,27 +391,51 @@ module.exports = class QueryLexer {
|
|
|
357
391
|
throw err;
|
|
358
392
|
}
|
|
359
393
|
|
|
360
|
-
// [ can
|
|
394
|
+
// [ can mean start of 'in'/'notIn',
|
|
395
|
+
// or 'notIn':
|
|
361
396
|
if (char === '[') {
|
|
362
|
-
tree.node.op =
|
|
397
|
+
tree.node.op = OP_TOKENS.IN;
|
|
398
|
+
if (token.length > 0) {
|
|
399
|
+
if (token === '!' || token === 'not') {
|
|
400
|
+
tree.node.op = OP_TOKENS.NOT_IN;
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
const err = UnexpectedCharError(i - token.length, token);
|
|
404
|
+
throw err;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Reset:
|
|
409
|
+
token = '';
|
|
363
410
|
continue;
|
|
364
411
|
}
|
|
365
412
|
|
|
366
|
-
// ] can
|
|
413
|
+
// ] can mean end of 'in'/'notIn':
|
|
367
414
|
if (char === ']') {
|
|
368
415
|
// User missed first '[' :
|
|
369
|
-
if (
|
|
416
|
+
if (
|
|
417
|
+
tree.node.op !== OP_TOKENS.IN
|
|
418
|
+
&&
|
|
419
|
+
tree.node.op !== OP_TOKENS.NOT_IN
|
|
420
|
+
) {
|
|
370
421
|
const err = UnexpectedCharError(i, char);
|
|
371
422
|
throw err;
|
|
372
423
|
}
|
|
373
424
|
|
|
425
|
+
// Token is the last element in this array:
|
|
426
|
+
if (token.length > 0) {
|
|
427
|
+
value.push(token);
|
|
428
|
+
}
|
|
429
|
+
|
|
374
430
|
tree.node.addWhere({
|
|
375
431
|
[tree.node.activeParam]: {
|
|
376
432
|
[tree.node.op]: value
|
|
377
433
|
}
|
|
378
434
|
});
|
|
435
|
+
|
|
379
436
|
// Reset:
|
|
380
437
|
tree.node.resetOP();
|
|
438
|
+
tree.node.resetActiveParam();
|
|
381
439
|
value = [];
|
|
382
440
|
token = '';
|
|
383
441
|
continue;
|
|
@@ -387,7 +445,7 @@ module.exports = class QueryLexer {
|
|
|
387
445
|
if (char === '=') {
|
|
388
446
|
const param = this.parseParamFromToken(token);
|
|
389
447
|
|
|
390
|
-
if (isSubQuery === true && param ===
|
|
448
|
+
if (isSubQuery === true && param === PARAM_TOKENS.INCLUDES) {
|
|
391
449
|
const err = new TypeError(`'include' is forbidden inside subquery (position ${ i }). Use: 'model.submodel' or 'model.submodel1+submodel2'.`);
|
|
392
450
|
throw err;
|
|
393
451
|
}
|
|
@@ -433,23 +491,29 @@ module.exports = class QueryLexer {
|
|
|
433
491
|
switch(token) {
|
|
434
492
|
case 'limit':
|
|
435
493
|
case 'l':
|
|
436
|
-
return
|
|
494
|
+
return PARAM_TOKENS.LIMIT;
|
|
495
|
+
|
|
437
496
|
case 'skip':
|
|
438
497
|
case 's':
|
|
439
498
|
case 'offset':
|
|
440
|
-
return
|
|
499
|
+
return PARAM_TOKENS.SKIP;
|
|
500
|
+
|
|
441
501
|
case 'order':
|
|
442
502
|
case 'o':
|
|
443
|
-
return
|
|
503
|
+
return PARAM_TOKENS.ORDER;
|
|
504
|
+
|
|
444
505
|
case 'order_by':
|
|
445
506
|
case 'o_by':
|
|
446
|
-
return
|
|
507
|
+
return PARAM_TOKENS.ORDER_BY;
|
|
508
|
+
|
|
447
509
|
case 'fields':
|
|
448
510
|
case 'f':
|
|
449
|
-
return
|
|
511
|
+
return PARAM_TOKENS.FIELDS;
|
|
512
|
+
|
|
450
513
|
case 'includes':
|
|
451
514
|
case 'in':
|
|
452
|
-
return
|
|
515
|
+
return PARAM_TOKENS.INCLUDES;
|
|
516
|
+
|
|
453
517
|
default:
|
|
454
518
|
return token;
|
|
455
519
|
}
|
|
@@ -458,30 +522,35 @@ module.exports = class QueryLexer {
|
|
|
458
522
|
setNodeParam(treeNode, token, value) {
|
|
459
523
|
const param = treeNode.activeParam;
|
|
460
524
|
|
|
461
|
-
debug(`set param
|
|
525
|
+
debug(`set param`, { param, token, value });
|
|
462
526
|
|
|
463
527
|
switch(param) {
|
|
464
|
-
case
|
|
528
|
+
case PARAM_TOKENS.LIMIT:
|
|
465
529
|
treeNode.limit = parseInt(token);
|
|
466
530
|
break;
|
|
467
|
-
|
|
468
|
-
case
|
|
531
|
+
|
|
532
|
+
case PARAM_TOKENS.SKIP:
|
|
469
533
|
treeNode.skip = parseInt(token);
|
|
470
534
|
break;
|
|
471
|
-
|
|
535
|
+
|
|
536
|
+
case PARAM_TOKENS.ORDER:
|
|
472
537
|
treeNode.order = token;
|
|
473
538
|
break;
|
|
474
|
-
|
|
539
|
+
|
|
540
|
+
case PARAM_TOKENS.ORDER_BY:
|
|
475
541
|
treeNode.order_by = token;
|
|
476
542
|
break;
|
|
477
|
-
|
|
543
|
+
|
|
544
|
+
case PARAM_TOKENS.FIELDS:
|
|
478
545
|
if (token) value.push(token);
|
|
479
546
|
treeNode.fields = value;
|
|
480
547
|
break;
|
|
481
|
-
|
|
548
|
+
|
|
549
|
+
case PARAM_TOKENS.INCLUDES:
|
|
482
550
|
const node = new ModelsTreeNode(token);
|
|
483
551
|
treeNode.include(node);
|
|
484
552
|
break;
|
|
553
|
+
|
|
485
554
|
default:
|
|
486
555
|
if (token) value.push(token);
|
|
487
556
|
treeNode.addWhere({ [param]: value });
|
|
@@ -493,10 +562,16 @@ module.exports = class QueryLexer {
|
|
|
493
562
|
switch(opToken) {
|
|
494
563
|
case '|':
|
|
495
564
|
case 'or':
|
|
496
|
-
return
|
|
565
|
+
return OP_TOKENS.OR;
|
|
566
|
+
|
|
567
|
+
case '!like':
|
|
568
|
+
case 'notLike':
|
|
569
|
+
return OP_TOKENS.NOT_LIKE;
|
|
570
|
+
|
|
497
571
|
case 'not':
|
|
498
572
|
case '!':
|
|
499
|
-
return
|
|
573
|
+
return OP_TOKENS.NOT;
|
|
574
|
+
|
|
500
575
|
default:
|
|
501
576
|
return opToken;
|
|
502
577
|
}
|
package/lib/query/traverse.js
CHANGED
|
@@ -102,49 +102,57 @@ function traverse(queryNode, filter=null, model=null) {
|
|
|
102
102
|
// Functions:
|
|
103
103
|
for (const fnParams of functions) {
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
105
|
+
switch(fnParams.fn) {
|
|
106
|
+
// SQL COUNT():
|
|
107
|
+
case 'count': {
|
|
108
|
+
const countParams = fnParams.args;
|
|
109
|
+
|
|
110
|
+
const [ countTarget ] = countParams;
|
|
111
|
+
// Count can be requested for this model,
|
|
112
|
+
// or for any of the available uncludes.
|
|
113
|
+
const isForRootModel = countTarget === rootModelName.plural.toLowerCase();
|
|
114
|
+
|
|
115
|
+
// Compile request:
|
|
116
|
+
// Example of desired SQL:
|
|
117
|
+
// `(SELECT COUNT(*) FROM comments WHERE comments.post_id=Post.id)`
|
|
118
|
+
//
|
|
119
|
+
let rawSQL = '(SELECT COUNT(*) FROM ';
|
|
120
|
+
let countAttribute = 'count';
|
|
121
|
+
|
|
122
|
+
// If request to count one of the includes:
|
|
123
|
+
if (!isForRootModel) {
|
|
124
|
+
// Check if it's available:
|
|
125
|
+
if (
|
|
126
126
|
!filter
|
|
127
127
|
||
|
|
128
128
|
!filter?.includes[countTarget]
|
|
129
129
|
||
|
|
130
130
|
rootModelAssociations[countTarget] === undefined
|
|
131
131
|
) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
const err = new NodesterQueryError(`Count for '${ countTarget }' is not available.`);
|
|
133
|
+
Error.captureStackTrace(err, traverse);
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const {
|
|
138
|
+
as,
|
|
139
|
+
target,
|
|
140
|
+
foreignKey,
|
|
141
|
+
sourceKey
|
|
142
|
+
} = rootModelAssociations[countTarget];
|
|
143
|
+
const { tableName } = target;
|
|
144
|
+
|
|
145
|
+
rawSQL += `${ tableName } where ${ tableName }.${ foreignKey }=${ rootModelName.singular }.${ sourceKey })`;
|
|
146
|
+
countAttribute = `${ as }_count`;
|
|
135
147
|
}
|
|
136
148
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
} = rootModelAssociations[countTarget];
|
|
141
|
-
rawSQL += `${ countTarget } where ${ countTarget }.${ foreignKey }=${ rootModelName.singular }.${ sourceKey })`;
|
|
142
|
-
countAttribute = `${ countTarget }_count`;
|
|
149
|
+
newQuery.attributes.push(
|
|
150
|
+
[sequelize.literal(rawSQL), countAttribute]
|
|
151
|
+
);
|
|
143
152
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
);
|
|
153
|
+
// Unknow function:
|
|
154
|
+
default:
|
|
155
|
+
break;
|
|
148
156
|
}
|
|
149
157
|
}
|
|
150
158
|
// Functions\
|
|
@@ -194,7 +202,7 @@ function traverse(queryNode, filter=null, model=null) {
|
|
|
194
202
|
}
|
|
195
203
|
}
|
|
196
204
|
|
|
197
|
-
// "statics"
|
|
205
|
+
// Override clauses with "statics":
|
|
198
206
|
if (filter !== null) {
|
|
199
207
|
const staticClausesEntries = Object.entries(filter.statics.clauses);
|
|
200
208
|
|
|
@@ -228,7 +236,11 @@ function traverse(queryNode, filter=null, model=null) {
|
|
|
228
236
|
|
|
229
237
|
|
|
230
238
|
// Order:
|
|
231
|
-
if (
|
|
239
|
+
if (
|
|
240
|
+
order.order === 'rand'
|
|
241
|
+
||
|
|
242
|
+
order.order === 'random'
|
|
243
|
+
) {
|
|
232
244
|
newQuery.order = sequelize.random();
|
|
233
245
|
}
|
|
234
246
|
else {
|
|
@@ -292,11 +304,21 @@ function traverse(queryNode, filter=null, model=null) {
|
|
|
292
304
|
}
|
|
293
305
|
|
|
294
306
|
|
|
295
|
-
|
|
307
|
+
/**
|
|
308
|
+
* Traverses each include in the array.
|
|
309
|
+
*
|
|
310
|
+
* @param {Array} includes
|
|
311
|
+
* @param {Model} rootModel
|
|
312
|
+
* @param {NodesterFilter} filter
|
|
313
|
+
* @param {Object} resultQuery
|
|
314
|
+
*
|
|
315
|
+
* @api private
|
|
316
|
+
*/
|
|
317
|
+
function _traverseIncludes(includes, rootModel, filter, resultQuery) {
|
|
296
318
|
const filterIncludesEntries = Object.entries(filter.includes);
|
|
297
319
|
for (let [ includeName, includeFilter ] of filterIncludesEntries) {
|
|
298
320
|
|
|
299
|
-
const association =
|
|
321
|
+
const association = rootModel.associations[includeName];
|
|
300
322
|
|
|
301
323
|
// If no such association:
|
|
302
324
|
if (!association) {
|
|
@@ -375,17 +397,10 @@ function _disassembleQueryNode(queryNode) {
|
|
|
375
397
|
function _parseValue(value, attribute) {
|
|
376
398
|
// If value is Object:
|
|
377
399
|
if (typeof value === 'object' && Array.isArray(value) === false) {
|
|
378
|
-
const [opKey, rawValue] = (Object.entries(value))[0];
|
|
400
|
+
const [ opKey, rawValue ] = (Object.entries(value))[0];
|
|
379
401
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
// Unwrap rawValue.
|
|
383
|
-
return rawValue[0][attribute];
|
|
384
|
-
}
|
|
385
|
-
else {
|
|
386
|
-
const op = Op[opKey];
|
|
387
|
-
return { [op]: rawValue };
|
|
388
|
-
}
|
|
402
|
+
const op = Op[opKey];
|
|
403
|
+
return { [op]: rawValue };
|
|
389
404
|
}
|
|
390
405
|
|
|
391
406
|
return value;
|