nodester 0.2.4 → 0.2.6
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 +8 -4
- package/lib/body/extract.js +6 -6
- package/lib/factories/responses/rest.js +7 -11
- package/lib/middlewares/ql/sequelize/interpreter/ModelsTree.js +13 -2
- package/lib/middlewares/ql/sequelize/interpreter/QueryLexer.js +77 -26
- package/lib/query/traverse.js +32 -39
- package/lib/structures/Filter.js +11 -11
- package/lib/tools/nql.tool.js +34 -21
- package/package.json +1 -1
- package/tests/ast.js +4 -2
- package/tests/nql.test.js +184 -22
- /package/lib/constants/{Operations.js → Operators.js} +0 -0
package/Readme.md
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
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
|
+
|
|
10
|
+
Building an application which allows users to build their own REST queries raises huge security concerns.
|
|
11
|
+
That's why nodester was not developped as a middleware. It's a framework equipped which set of technologies enabling you to fully customize the request-response flow down to the specific user to only give them access to the data you intended.
|
|
12
|
+
Check out [core concepts documentation ➡️](docs/CoreConcepts.md) for more info.
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
## Installation
|
|
@@ -49,11 +53,11 @@ app.listen(8080, function() {
|
|
|
49
53
|
[Core concepts documentation ➡️](docs/CoreConcepts.md)
|
|
50
54
|
|
|
51
55
|
|
|
52
|
-
### Queries & Querying -
|
|
56
|
+
### Queries & Querying - nodester Query Language (NQL)
|
|
53
57
|
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
58
|
|
|
55
59
|
Read more about it in the documentation:
|
|
56
|
-
[NQL documentaion ➡️](docs/
|
|
60
|
+
[NQL documentaion ➡️](docs/nql/Introduction.md)
|
|
57
61
|
|
|
58
62
|
|
|
59
63
|
### Database
|
package/lib/body/extract.js
CHANGED
|
@@ -25,7 +25,7 @@ module.exports = extract;
|
|
|
25
25
|
function extract(body, filter=null, model) {
|
|
26
26
|
|
|
27
27
|
const sequelize = model.sequelize;
|
|
28
|
-
const
|
|
28
|
+
const modelAttributes = Object.keys(model.tableAttributes);
|
|
29
29
|
const availableIncludes = Object.keys(model.associations);
|
|
30
30
|
|
|
31
31
|
const bodyEntries = Object.entries(body);
|
|
@@ -38,15 +38,15 @@ function extract(body, filter=null, model) {
|
|
|
38
38
|
|
|
39
39
|
for (const [key, value] of bodyEntries) {
|
|
40
40
|
const isInclude = availableIncludes.indexOf(key) > -1;
|
|
41
|
-
const
|
|
41
|
+
const isAttribute = modelAttributes.indexOf(key) > -1;
|
|
42
42
|
|
|
43
|
-
if ((!
|
|
44
|
-
const err = new Error(`
|
|
43
|
+
if ((!isAttribute || filter.attributes.indexOf(key) === -1) && !isInclude) {
|
|
44
|
+
const err = new Error(`Attribute '${ key }' is not available.`);
|
|
45
45
|
err.status = httpCodes.NOT_ACCEPTABLE;
|
|
46
46
|
throw err;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
if (
|
|
49
|
+
if (isAttribute) {
|
|
50
50
|
const column = model.rawAttributes[key];
|
|
51
51
|
const typeName = column.type.constructor.name;
|
|
52
52
|
// Optional validation.
|
|
@@ -80,7 +80,7 @@ function extract(body, filter=null, model) {
|
|
|
80
80
|
continue;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
const err = new Error(`Unknown
|
|
83
|
+
const err = new Error(`Unknown attribute '${ key }'.`);
|
|
84
84
|
err.status = httpCodes.NOT_ACCEPTABLE;
|
|
85
85
|
throw err;
|
|
86
86
|
}
|
|
@@ -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
|
});
|
|
@@ -17,7 +17,7 @@ class ModelsTreeNode {
|
|
|
17
17
|
this.fn = null;
|
|
18
18
|
|
|
19
19
|
// for override:
|
|
20
|
-
this.
|
|
20
|
+
this._attributes = [];
|
|
21
21
|
this._where = {};
|
|
22
22
|
this._functions = [];
|
|
23
23
|
this.skip = 0;
|
|
@@ -28,6 +28,11 @@ class ModelsTreeNode {
|
|
|
28
28
|
this.order_by = opts.order_by ?? 'id';
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Getters:
|
|
32
|
+
get attributes() {
|
|
33
|
+
return this._attributes;
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
get where() {
|
|
32
37
|
return this._where;
|
|
33
38
|
}
|
|
@@ -51,6 +56,12 @@ class ModelsTreeNode {
|
|
|
51
56
|
get hasIncludes() {
|
|
52
57
|
return this.includesCount > 0;
|
|
53
58
|
}
|
|
59
|
+
// Getters\
|
|
60
|
+
|
|
61
|
+
// Setters:
|
|
62
|
+
set attributes(array) {
|
|
63
|
+
this._attributes = array;
|
|
64
|
+
}
|
|
54
65
|
|
|
55
66
|
resetActiveParam() {
|
|
56
67
|
this.activeParam = null;
|
|
@@ -85,7 +96,7 @@ class ModelsTreeNode {
|
|
|
85
96
|
return {
|
|
86
97
|
model: this.model,
|
|
87
98
|
|
|
88
|
-
|
|
99
|
+
attributes: this.attributes,
|
|
89
100
|
functions: this.functions,
|
|
90
101
|
|
|
91
102
|
where: this.where,
|
|
@@ -12,26 +12,37 @@ const util = require('util');
|
|
|
12
12
|
const debug = require('debug')('nodester:interpreter:QueryLexer');
|
|
13
13
|
|
|
14
14
|
const PARAM_TOKENS = new Enum({
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
ATTRIBUTES: Symbol('attributes'),
|
|
16
|
+
|
|
17
17
|
LIMIT: Symbol('limit'),
|
|
18
18
|
ORDER: Symbol('order'),
|
|
19
19
|
ORDER_BY: Symbol('order_by'),
|
|
20
20
|
SKIP: Symbol('skip'),
|
|
21
|
+
|
|
22
|
+
INCLUDES: Symbol('includes'),
|
|
21
23
|
});
|
|
22
24
|
|
|
23
25
|
const OP_TOKENS = new Enum({
|
|
24
26
|
AND: 'and',
|
|
27
|
+
|
|
25
28
|
BETWEEN: 'between',
|
|
26
29
|
NOT_BETWEEN: 'notBetween',
|
|
27
30
|
BETWEEN_MARK: '~',
|
|
31
|
+
|
|
28
32
|
OR: 'or',
|
|
29
|
-
|
|
33
|
+
OR_SHORT: '|',
|
|
30
34
|
XOR: 'xor',
|
|
35
|
+
|
|
31
36
|
NOT: 'not',
|
|
32
|
-
|
|
37
|
+
NOT_SHORT: '!',
|
|
38
|
+
|
|
39
|
+
IN: 'in',
|
|
33
40
|
NOT_IN: 'notIn',
|
|
41
|
+
|
|
34
42
|
LIKE: 'like',
|
|
43
|
+
NOT_LIKE: 'notLike',
|
|
44
|
+
NOT_LIKE_SHORT: '!like',
|
|
45
|
+
|
|
35
46
|
GREATER: 'gt',
|
|
36
47
|
GREATER_OR_EQUAL: 'gte',
|
|
37
48
|
LOWER: 'lt',
|
|
@@ -71,7 +82,7 @@ module.exports = class QueryLexer {
|
|
|
71
82
|
|
|
72
83
|
// Token is a String, accumulated char-by-char.
|
|
73
84
|
let token = '';
|
|
74
|
-
// Value of param ('id=10' OR '
|
|
85
|
+
// Value of param ('id=10' OR 'attributes=id,text').
|
|
75
86
|
let value = [];
|
|
76
87
|
// Model, that was active before a cursor went up in the tree.
|
|
77
88
|
let previousActive = null;
|
|
@@ -86,7 +97,7 @@ module.exports = class QueryLexer {
|
|
|
86
97
|
|
|
87
98
|
// If OP token:
|
|
88
99
|
if (OP_TOKENS.asArray.indexOf(token) > -1) {
|
|
89
|
-
// Set
|
|
100
|
+
// Set operator token.
|
|
90
101
|
tree.node.op = this.parseOP(token);
|
|
91
102
|
token = '';
|
|
92
103
|
continue;
|
|
@@ -135,6 +146,7 @@ module.exports = class QueryLexer {
|
|
|
135
146
|
switch (tree.node.op) {
|
|
136
147
|
case OP_TOKENS.NOT:
|
|
137
148
|
case OP_TOKENS.LIKE:
|
|
149
|
+
case OP_TOKENS.NOT_LIKE:
|
|
138
150
|
case OP_TOKENS.GREATER:
|
|
139
151
|
case OP_TOKENS.GREATER_OR_EQUAL:
|
|
140
152
|
case OP_TOKENS.LOWER:
|
|
@@ -214,9 +226,19 @@ module.exports = class QueryLexer {
|
|
|
214
226
|
|
|
215
227
|
// If OP token:
|
|
216
228
|
if (!!tree.node.op) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
229
|
+
switch(tree.node.op) {
|
|
230
|
+
case OP_TOKENS.NOT_IN:
|
|
231
|
+
case OP_TOKENS.IN:
|
|
232
|
+
value.push(token);
|
|
233
|
+
break;
|
|
234
|
+
default:
|
|
235
|
+
value.push({
|
|
236
|
+
[tree.node.activeParam]: [token]
|
|
237
|
+
});
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Reset.
|
|
220
242
|
token = '';
|
|
221
243
|
continue;
|
|
222
244
|
}
|
|
@@ -374,27 +396,51 @@ module.exports = class QueryLexer {
|
|
|
374
396
|
throw err;
|
|
375
397
|
}
|
|
376
398
|
|
|
377
|
-
// [ can
|
|
399
|
+
// [ can mean start of 'in'/'notIn',
|
|
400
|
+
// or 'notIn':
|
|
378
401
|
if (char === '[') {
|
|
379
|
-
tree.node.op =
|
|
402
|
+
tree.node.op = OP_TOKENS.IN;
|
|
403
|
+
if (token.length > 0) {
|
|
404
|
+
if (token === '!' || token === 'not') {
|
|
405
|
+
tree.node.op = OP_TOKENS.NOT_IN;
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
const err = UnexpectedCharError(i - token.length, token);
|
|
409
|
+
throw err;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Reset:
|
|
414
|
+
token = '';
|
|
380
415
|
continue;
|
|
381
416
|
}
|
|
382
417
|
|
|
383
|
-
// ] can
|
|
418
|
+
// ] can mean end of 'in'/'notIn':
|
|
384
419
|
if (char === ']') {
|
|
385
420
|
// User missed first '[' :
|
|
386
|
-
if (
|
|
421
|
+
if (
|
|
422
|
+
tree.node.op !== OP_TOKENS.IN
|
|
423
|
+
&&
|
|
424
|
+
tree.node.op !== OP_TOKENS.NOT_IN
|
|
425
|
+
) {
|
|
387
426
|
const err = UnexpectedCharError(i, char);
|
|
388
427
|
throw err;
|
|
389
428
|
}
|
|
390
429
|
|
|
430
|
+
// Token is the last element in this array:
|
|
431
|
+
if (token.length > 0) {
|
|
432
|
+
value.push(token);
|
|
433
|
+
}
|
|
434
|
+
|
|
391
435
|
tree.node.addWhere({
|
|
392
436
|
[tree.node.activeParam]: {
|
|
393
437
|
[tree.node.op]: value
|
|
394
438
|
}
|
|
395
439
|
});
|
|
440
|
+
|
|
396
441
|
// Reset:
|
|
397
442
|
tree.node.resetOP();
|
|
443
|
+
tree.node.resetActiveParam();
|
|
398
444
|
value = [];
|
|
399
445
|
token = '';
|
|
400
446
|
continue;
|
|
@@ -448,13 +494,16 @@ module.exports = class QueryLexer {
|
|
|
448
494
|
|
|
449
495
|
parseParamFromToken(token) {
|
|
450
496
|
switch(token) {
|
|
497
|
+
case 'attributes':
|
|
498
|
+
case 'a':
|
|
499
|
+
return PARAM_TOKENS.ATTRIBUTES;
|
|
500
|
+
|
|
451
501
|
case 'limit':
|
|
452
502
|
case 'l':
|
|
453
503
|
return PARAM_TOKENS.LIMIT;
|
|
454
504
|
|
|
455
505
|
case 'skip':
|
|
456
506
|
case 's':
|
|
457
|
-
case 'offset':
|
|
458
507
|
return PARAM_TOKENS.SKIP;
|
|
459
508
|
|
|
460
509
|
case 'order':
|
|
@@ -462,13 +511,9 @@ module.exports = class QueryLexer {
|
|
|
462
511
|
return PARAM_TOKENS.ORDER;
|
|
463
512
|
|
|
464
513
|
case 'order_by':
|
|
465
|
-
case '
|
|
514
|
+
case 'oby':
|
|
466
515
|
return PARAM_TOKENS.ORDER_BY;
|
|
467
516
|
|
|
468
|
-
case 'fields':
|
|
469
|
-
case 'f':
|
|
470
|
-
return PARAM_TOKENS.FIELDS;
|
|
471
|
-
|
|
472
517
|
case 'includes':
|
|
473
518
|
case 'in':
|
|
474
519
|
return PARAM_TOKENS.INCLUDES;
|
|
@@ -484,6 +529,11 @@ module.exports = class QueryLexer {
|
|
|
484
529
|
debug(`set param`, { param, token, value });
|
|
485
530
|
|
|
486
531
|
switch(param) {
|
|
532
|
+
case PARAM_TOKENS.ATTRIBUTES:
|
|
533
|
+
if (token) value.push(token);
|
|
534
|
+
treeNode.attributes = value;
|
|
535
|
+
break;
|
|
536
|
+
|
|
487
537
|
case PARAM_TOKENS.LIMIT:
|
|
488
538
|
treeNode.limit = parseInt(token);
|
|
489
539
|
break;
|
|
@@ -500,11 +550,6 @@ module.exports = class QueryLexer {
|
|
|
500
550
|
treeNode.order_by = token;
|
|
501
551
|
break;
|
|
502
552
|
|
|
503
|
-
case PARAM_TOKENS.FIELDS:
|
|
504
|
-
if (token) value.push(token);
|
|
505
|
-
treeNode.fields = value;
|
|
506
|
-
break;
|
|
507
|
-
|
|
508
553
|
case PARAM_TOKENS.INCLUDES:
|
|
509
554
|
const node = new ModelsTreeNode(token);
|
|
510
555
|
treeNode.include(node);
|
|
@@ -521,10 +566,16 @@ module.exports = class QueryLexer {
|
|
|
521
566
|
switch(opToken) {
|
|
522
567
|
case '|':
|
|
523
568
|
case 'or':
|
|
524
|
-
return
|
|
569
|
+
return OP_TOKENS.OR;
|
|
570
|
+
|
|
571
|
+
case '!like':
|
|
572
|
+
case 'notLike':
|
|
573
|
+
return OP_TOKENS.NOT_LIKE;
|
|
574
|
+
|
|
525
575
|
case 'not':
|
|
526
576
|
case '!':
|
|
527
|
-
return
|
|
577
|
+
return OP_TOKENS.NOT;
|
|
578
|
+
|
|
528
579
|
default:
|
|
529
580
|
return opToken;
|
|
530
581
|
}
|
package/lib/query/traverse.js
CHANGED
|
@@ -35,7 +35,7 @@ function traverse(queryNode, filter=null, model=null) {
|
|
|
35
35
|
const rootModelName = _model.options.name;
|
|
36
36
|
const rootModelAssociations = _model.associations;
|
|
37
37
|
const { sequelize } = _model;
|
|
38
|
-
const
|
|
38
|
+
const attributesAvailable = Object.keys(_model.tableAttributes);
|
|
39
39
|
|
|
40
40
|
const newQuery = {
|
|
41
41
|
attributes: [],
|
|
@@ -44,60 +44,61 @@ function traverse(queryNode, filter=null, model=null) {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
const {
|
|
47
|
+
attributes,
|
|
48
|
+
clauses,
|
|
49
|
+
functions,
|
|
47
50
|
where,
|
|
51
|
+
|
|
48
52
|
includes,
|
|
49
|
-
fields,
|
|
50
|
-
functions,
|
|
51
|
-
clauses,
|
|
52
53
|
} = _disassembleQueryNode(queryNode);
|
|
53
54
|
|
|
54
55
|
|
|
55
|
-
//
|
|
56
|
+
// Attribute:
|
|
56
57
|
//
|
|
57
58
|
// If Filter is not set,
|
|
58
|
-
// use every available
|
|
59
|
+
// use every available attribute:
|
|
59
60
|
if (filter === null) {
|
|
60
|
-
for (let
|
|
61
|
-
// If no query filter or
|
|
62
|
-
if (
|
|
63
|
-
newQuery.attributes.push(
|
|
61
|
+
for (let attribute of attributesAvailable) {
|
|
62
|
+
// If no query filter or attribute is requested:
|
|
63
|
+
if (attributes.length === 0 || attributes.indexOf(attribute) > -1) {
|
|
64
|
+
newQuery.attributes.push(attribute);
|
|
64
65
|
continue;
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
// Filter is present:
|
|
69
70
|
else {
|
|
70
|
-
// If no query
|
|
71
|
+
// If no query attributes were set,
|
|
71
72
|
// use the ones from Filter,
|
|
72
|
-
// If query
|
|
73
|
+
// If query attributes were set,
|
|
73
74
|
// put them through Filter:
|
|
74
|
-
for (let
|
|
75
|
-
if (
|
|
76
|
-
const err = new NodesterQueryError(`Field '${
|
|
75
|
+
for (let attribute of filter.attributes) {
|
|
76
|
+
if (attributesAvailable.indexOf(attribute) === -1) {
|
|
77
|
+
const err = new NodesterQueryError(`Field '${ attribute }' is not present in model.`);
|
|
77
78
|
Error.captureStackTrace(err, traverse);
|
|
78
79
|
throw err;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
// If
|
|
82
|
-
// if (filter.
|
|
82
|
+
// If attribute is not in available set:
|
|
83
|
+
// if (filter.attributes.indexOf(attribute) === -1) {
|
|
83
84
|
// continue;
|
|
84
85
|
// }
|
|
85
86
|
|
|
86
|
-
// If no query filter or
|
|
87
|
-
if (
|
|
88
|
-
newQuery.attributes.push(
|
|
87
|
+
// If no query filter or attribute is requested:
|
|
88
|
+
if (attributes.length === 0 || attributes.indexOf(attribute) > -1) {
|
|
89
|
+
newQuery.attributes.push(attribute);
|
|
89
90
|
continue;
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
// At least 1
|
|
95
|
+
// At least 1 attribute is mandatory:
|
|
95
96
|
if (newQuery.attributes.length === 0) {
|
|
96
|
-
const err = new NodesterQueryError(`No
|
|
97
|
+
const err = new NodesterQueryError(`No attributes were selected.`);
|
|
97
98
|
Error.captureStackTrace(err, traverse);
|
|
98
99
|
throw err;
|
|
99
100
|
}
|
|
100
|
-
//
|
|
101
|
+
// Attribute\
|
|
101
102
|
|
|
102
103
|
// Functions:
|
|
103
104
|
for (const fnParams of functions) {
|
|
@@ -377,37 +378,29 @@ function _parseWhereEntry(attribute, value, whereHolder, staticAttributes) {
|
|
|
377
378
|
function _disassembleQueryNode(queryNode) {
|
|
378
379
|
// Disassemble current query node:
|
|
379
380
|
const {
|
|
381
|
+
attributes,
|
|
382
|
+
functions,
|
|
380
383
|
where,
|
|
381
384
|
includes,
|
|
382
|
-
fields,
|
|
383
|
-
functions,
|
|
384
385
|
...clauses
|
|
385
386
|
} = queryNode;
|
|
386
|
-
// delete queryNode.model;
|
|
387
387
|
|
|
388
388
|
return {
|
|
389
|
+
attributes: attributes ?? [],
|
|
390
|
+
clauses: clauses ?? [],
|
|
391
|
+
functions: functions ?? [],
|
|
389
392
|
where: where ?? {},
|
|
390
393
|
includes: includes ?? [],
|
|
391
|
-
fields: fields ?? [],
|
|
392
|
-
functions: functions ?? [],
|
|
393
|
-
clauses: clauses ?? []
|
|
394
394
|
};
|
|
395
395
|
}
|
|
396
396
|
|
|
397
397
|
function _parseValue(value, attribute) {
|
|
398
398
|
// If value is Object:
|
|
399
399
|
if (typeof value === 'object' && Array.isArray(value) === false) {
|
|
400
|
-
const [opKey, rawValue] = (Object.entries(value))[0];
|
|
400
|
+
const [ opKey, rawValue ] = (Object.entries(value))[0];
|
|
401
401
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
// Unwrap rawValue.
|
|
405
|
-
return rawValue[0][attribute];
|
|
406
|
-
}
|
|
407
|
-
else {
|
|
408
|
-
const op = Op[opKey];
|
|
409
|
-
return { [op]: rawValue };
|
|
410
|
-
}
|
|
402
|
+
const op = Op[opKey];
|
|
403
|
+
return { [op]: rawValue };
|
|
411
404
|
}
|
|
412
405
|
|
|
413
406
|
return value;
|
package/lib/structures/Filter.js
CHANGED
|
@@ -18,12 +18,12 @@ module.exports = class NodesterFilter {
|
|
|
18
18
|
*
|
|
19
19
|
* @param {Model} model
|
|
20
20
|
* @param {Object} options
|
|
21
|
-
* @param {Array} options.
|
|
21
|
+
* @param {Array} options.attributes
|
|
22
22
|
* @param {Array} options.clauses
|
|
23
23
|
* @param {Object} options.includes
|
|
24
24
|
*
|
|
25
25
|
* @param {Object} options.bounds
|
|
26
|
-
* @param {Object} options.bounds.
|
|
26
|
+
* @param {Object} options.bounds.attributes
|
|
27
27
|
* @param {Object} options.bounds.clauses
|
|
28
28
|
*
|
|
29
29
|
* @param {Object} options.statics
|
|
@@ -37,12 +37,12 @@ module.exports = class NodesterFilter {
|
|
|
37
37
|
|
|
38
38
|
this._model = model;
|
|
39
39
|
|
|
40
|
-
this.
|
|
40
|
+
this._attributes = [];
|
|
41
41
|
this._clauses = [];
|
|
42
42
|
this._includes = {};
|
|
43
43
|
|
|
44
44
|
this._bounds = {
|
|
45
|
-
|
|
45
|
+
attributes: {},
|
|
46
46
|
clauses: {}
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -53,7 +53,7 @@ module.exports = class NodesterFilter {
|
|
|
53
53
|
|
|
54
54
|
// If model is present:
|
|
55
55
|
if (isModel(this.model)) {
|
|
56
|
-
this.
|
|
56
|
+
this._attributes = Object.keys(this.model.tableAttributes);
|
|
57
57
|
this._clauses = CLAUSES.asArray;
|
|
58
58
|
|
|
59
59
|
// ...and no 'bounds' and 'statics' are provided,
|
|
@@ -67,7 +67,7 @@ module.exports = class NodesterFilter {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const {
|
|
70
|
-
|
|
70
|
+
attributes,
|
|
71
71
|
clauses,
|
|
72
72
|
includes,
|
|
73
73
|
bounds,
|
|
@@ -75,9 +75,9 @@ module.exports = class NodesterFilter {
|
|
|
75
75
|
} = options;
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
// If
|
|
79
|
-
if (Array.isArray(
|
|
80
|
-
this.
|
|
78
|
+
// If attributes are array:
|
|
79
|
+
if (Array.isArray(attributes)) {
|
|
80
|
+
this._attributes = attributes;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
if (Array.isArray(clauses)) {
|
|
@@ -124,8 +124,8 @@ module.exports = class NodesterFilter {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
// Getters:
|
|
127
|
-
get
|
|
128
|
-
return this.
|
|
127
|
+
get attributes() {
|
|
128
|
+
return this._attributes;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
get clauses() {
|
package/lib/tools/nql.tool.js
CHANGED
|
@@ -5,41 +5,54 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
+
// Arguments validator.
|
|
9
|
+
const { ensure } = require('nodester/validators/arguments');
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
module.exports = {
|
|
10
|
-
|
|
13
|
+
AST_ModelsTree: _AST_ModelsTree,
|
|
14
|
+
AST_ModelsTreeNode: _AST_ModelsTreeNode
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function _AST_ModelsTree(modelsTree) {
|
|
18
|
+
ensure(modelsTree, 'object,required', 'modelsTree');
|
|
19
|
+
return _AST_ModelsTreeNode(modelsTree.root);
|
|
11
20
|
}
|
|
12
21
|
|
|
13
|
-
function
|
|
14
|
-
|
|
22
|
+
function _AST_ModelsTreeNode(node, spacing=0) {
|
|
23
|
+
ensure(node, 'object,required', 'node');
|
|
24
|
+
ensure(spacing, 'number,required', 'spacing');
|
|
25
|
+
|
|
26
|
+
let spaces = ' ';
|
|
15
27
|
for (let i = 0; i < spacing; i++) {
|
|
16
28
|
spaces += ' ';
|
|
17
29
|
}
|
|
18
30
|
|
|
19
|
-
let ast = `${ spaces }
|
|
20
|
-
|
|
21
|
-
spaces += ' ';
|
|
31
|
+
let ast = `${ spaces }┏ TreeNode\n`;
|
|
32
|
+
ast += `${ spaces }┃\n`;
|
|
22
33
|
|
|
23
|
-
ast += `${ spaces }model: ${ node.model }\n
|
|
34
|
+
ast += `${ spaces }┣ model: ${ node.model }\n`;
|
|
35
|
+
ast += `${ spaces }┃\n`;
|
|
24
36
|
|
|
25
|
-
ast += `${ spaces }fields (${ node.fields.length }): [\n${ node.fields.map(f => ` • ${ f },\n`) }`;
|
|
26
|
-
ast += `${ spaces }]\n
|
|
37
|
+
ast += `${ spaces }┣ fields (${ node.fields.length }): [\n${ node.fields.map(f => ` • ${ f },\n`) }`;
|
|
38
|
+
ast += `${ spaces }┃ ]\n`;
|
|
39
|
+
ast += `${ spaces }┃\n`;
|
|
27
40
|
|
|
28
|
-
ast += `${ spaces }functions (${ node.functions.length }): [\n${ node.functions.map(f => ` • ${ f },\n`) }`;
|
|
29
|
-
ast += `${ spaces }]\n
|
|
41
|
+
ast += `${ spaces }┣ functions (${ node.functions.length }): [\n${ node.functions.map(f => ` • ${ f },\n`) }`;
|
|
42
|
+
ast += `${ spaces }┃ ]\n`;
|
|
43
|
+
ast += `${ spaces }┃\n`;
|
|
30
44
|
|
|
31
|
-
ast += `${ spaces }where: ${ JSON.stringify(node.where) }\n
|
|
45
|
+
ast += `${ spaces }┣ where: ${ JSON.stringify(node.where) }\n`;
|
|
46
|
+
ast += `${ spaces }┃\n`;
|
|
32
47
|
|
|
33
|
-
['skip','limit','order','order_by'].map(
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
['skip','limit','order','order_by'].map(c => {
|
|
49
|
+
ast += `${ spaces }┣ ${ c }: ${ node[c] }\n`;
|
|
50
|
+
ast += `${ spaces }┃\n`;
|
|
51
|
+
});
|
|
36
52
|
|
|
37
|
-
ast += `${ spaces }includes (${ node.includes.length }): [\n`
|
|
38
|
-
node.includes.map(n => ast +=
|
|
39
|
-
ast += `${ spaces }]\n`;
|
|
40
|
-
|
|
41
|
-
spaces.slice(-1);
|
|
42
|
-
ast += `${ spaces }[TreeNode END]\n\n`;
|
|
53
|
+
ast += `${ spaces }┗ includes (${ node.includes.length }): [\n`
|
|
54
|
+
node.includes.map(n => ast += _AST_ModelsTreeNode(n, spacing + 2));
|
|
55
|
+
ast += `${ spaces } ]\n`;
|
|
43
56
|
|
|
44
57
|
return ast;
|
|
45
58
|
}
|
package/package.json
CHANGED
package/tests/ast.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { ModelsTree } = require('../lib/middlewares/ql/sequelize/interpreter/ModelsTree');
|
|
2
|
-
const {
|
|
2
|
+
const { AST_ModelsTree } = require('../lib/tools/nql.tool');
|
|
3
3
|
|
|
4
4
|
const tree = new ModelsTree();
|
|
5
5
|
tree.node.addWhere({ id: ['1000'] });
|
|
@@ -12,5 +12,7 @@ tree.node.order = 'rand';
|
|
|
12
12
|
tree.up();
|
|
13
13
|
tree.include('reposts');
|
|
14
14
|
|
|
15
|
-
console.debug(
|
|
15
|
+
console.debug(
|
|
16
|
+
AST_ModelsTree(tree)
|
|
17
|
+
);
|
|
16
18
|
|
package/tests/nql.test.js
CHANGED
|
@@ -17,8 +17,10 @@ describe('nodester Query Language', () => {
|
|
|
17
17
|
const queryStrings = [
|
|
18
18
|
// Simple where.
|
|
19
19
|
'id=10',
|
|
20
|
+
// Only certain attributes.
|
|
21
|
+
'a=id,text',
|
|
20
22
|
// All possible params.
|
|
21
|
-
'id=10&position=4&limit=3&skip=10&order=desc&order_by=index&
|
|
23
|
+
'id=10&position=4&limit=3&skip=10&order=desc&order_by=index&a=id,content,position,created_at',
|
|
22
24
|
];
|
|
23
25
|
|
|
24
26
|
it('Simple where', () => {
|
|
@@ -32,14 +34,25 @@ describe('nodester Query Language', () => {
|
|
|
32
34
|
expect(result).toMatchObject(expected);
|
|
33
35
|
});
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
it('Only certain attributes', () => {
|
|
36
38
|
const lexer = new QueryLexer( queryStrings[1] );
|
|
37
39
|
const result = lexer.query;
|
|
38
40
|
|
|
41
|
+
const tree = new ModelsTree();
|
|
42
|
+
tree.node.attributes = [ 'id', 'text' ];
|
|
43
|
+
const expected = tree.root.toObject();
|
|
44
|
+
|
|
45
|
+
expect(result).toMatchObject(expected);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('All possible params', () => {
|
|
49
|
+
const lexer = new QueryLexer( queryStrings[2] );
|
|
50
|
+
const result = lexer.query;
|
|
51
|
+
|
|
39
52
|
|
|
40
53
|
const tree = new ModelsTree();
|
|
41
54
|
tree.node.addWhere({ id: ['10'], position: ['4'] });
|
|
42
|
-
tree.node.
|
|
55
|
+
tree.node.attributes = [ 'id', 'content', 'position', 'created_at' ];
|
|
43
56
|
tree.node.limit = 3;
|
|
44
57
|
tree.node.skip = 10;
|
|
45
58
|
tree.node.order = 'desc';
|
|
@@ -55,7 +68,7 @@ describe('nodester Query Language', () => {
|
|
|
55
68
|
// Simple includes.
|
|
56
69
|
'includes=comments&id=7',
|
|
57
70
|
// Include with All possible params.
|
|
58
|
-
'includes=comments(id=10&position=4&limit=3&skip=10&order=desc&order_by=index&
|
|
71
|
+
'includes=comments(id=10&position=4&limit=3&skip=10&order=desc&order_by=index&a=id,content,position)',
|
|
59
72
|
|
|
60
73
|
// 2 horizontals
|
|
61
74
|
'includes=comments,users&id=1000',
|
|
@@ -66,7 +79,7 @@ describe('nodester Query Language', () => {
|
|
|
66
79
|
'in=reactions,comments(user_id=gte(4)&skip=10&limit=2).users,likes,reposts',
|
|
67
80
|
|
|
68
81
|
// Separated includes.
|
|
69
|
-
'includes=comments(order=rand)&id=7&limit=3&includes=users(
|
|
82
|
+
'includes=comments(order=rand)&id=7&limit=3&includes=users(a=id,content)',
|
|
70
83
|
];
|
|
71
84
|
|
|
72
85
|
test('Simple includes', () => {
|
|
@@ -89,7 +102,7 @@ describe('nodester Query Language', () => {
|
|
|
89
102
|
const tree = new ModelsTree();
|
|
90
103
|
tree.include('comments').use('comments');
|
|
91
104
|
tree.node.addWhere({ id: ['10'], position: ['4'] });
|
|
92
|
-
tree.node.
|
|
105
|
+
tree.node.attributes = [ 'id', 'content', 'position' ];
|
|
93
106
|
tree.node.limit = 3;
|
|
94
107
|
tree.node.skip = 10;
|
|
95
108
|
tree.node.order = 'desc';
|
|
@@ -171,7 +184,7 @@ describe('nodester Query Language', () => {
|
|
|
171
184
|
tree.node.order = 'rand';
|
|
172
185
|
tree.up();
|
|
173
186
|
tree.include('users').use('users');
|
|
174
|
-
tree.node.
|
|
187
|
+
tree.node.attributes = [ 'id', 'content' ];
|
|
175
188
|
const expected = tree.root.toObject();
|
|
176
189
|
|
|
177
190
|
expect(result).toMatchObject(expected);
|
|
@@ -274,22 +287,12 @@ describe('nodester Query Language', () => {
|
|
|
274
287
|
});
|
|
275
288
|
});
|
|
276
289
|
|
|
277
|
-
describe('
|
|
290
|
+
describe('operators:or', () => {
|
|
278
291
|
const queryStrings = [
|
|
279
292
|
// OR simple.
|
|
280
293
|
'or(index=2,position=5)',
|
|
281
294
|
// OR short.
|
|
282
295
|
'|(index=2,position=5)',
|
|
283
|
-
|
|
284
|
-
// Not simple.
|
|
285
|
-
'key=not(main)',
|
|
286
|
-
// Not short.
|
|
287
|
-
'key=!(main)',
|
|
288
|
-
// NOT inside include.
|
|
289
|
-
'includes=comments(id=not(7))',
|
|
290
|
-
|
|
291
|
-
// Like simple.
|
|
292
|
-
'title=like(some_text)',
|
|
293
296
|
];
|
|
294
297
|
|
|
295
298
|
test('"OR" simple', () => {
|
|
@@ -313,9 +316,20 @@ describe('nodester Query Language', () => {
|
|
|
313
316
|
|
|
314
317
|
expect(result).toMatchObject(expected);
|
|
315
318
|
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('operators:not', () => {
|
|
322
|
+
const queryStrings = [
|
|
323
|
+
// Not simple.
|
|
324
|
+
'key=not(main)',
|
|
325
|
+
// Not short.
|
|
326
|
+
'key=!(main)',
|
|
327
|
+
// NOT inside include.
|
|
328
|
+
'includes=comments(id=not(7))'
|
|
329
|
+
];
|
|
316
330
|
|
|
317
331
|
test('"NOT" simple', () => {
|
|
318
|
-
const lexer = new QueryLexer( queryStrings[
|
|
332
|
+
const lexer = new QueryLexer( queryStrings[0] );
|
|
319
333
|
const result = lexer.query;
|
|
320
334
|
|
|
321
335
|
const tree = new ModelsTree();
|
|
@@ -326,7 +340,7 @@ describe('nodester Query Language', () => {
|
|
|
326
340
|
});
|
|
327
341
|
|
|
328
342
|
test('"NOT" short', () => {
|
|
329
|
-
const lexer = new QueryLexer( queryStrings[
|
|
343
|
+
const lexer = new QueryLexer( queryStrings[1] );
|
|
330
344
|
const result = lexer.query;
|
|
331
345
|
|
|
332
346
|
const tree = new ModelsTree();
|
|
@@ -337,7 +351,7 @@ describe('nodester Query Language', () => {
|
|
|
337
351
|
});
|
|
338
352
|
|
|
339
353
|
test('"NOT" inside includes', () => {
|
|
340
|
-
const lexer = new QueryLexer( queryStrings[
|
|
354
|
+
const lexer = new QueryLexer( queryStrings[2] );
|
|
341
355
|
const result = lexer.query;
|
|
342
356
|
|
|
343
357
|
const tree = new ModelsTree();
|
|
@@ -348,8 +362,21 @@ describe('nodester Query Language', () => {
|
|
|
348
362
|
expect(result).toMatchObject(expected);
|
|
349
363
|
});
|
|
350
364
|
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe('operators:like', () => {
|
|
368
|
+
const queryStrings = [
|
|
369
|
+
// Like simple.
|
|
370
|
+
'title=like(some_text)',
|
|
371
|
+
|
|
372
|
+
// Not like simple.
|
|
373
|
+
'title=notLike(some_text)',
|
|
374
|
+
// Not like short.
|
|
375
|
+
'title=!like(some_text)',
|
|
376
|
+
];
|
|
377
|
+
|
|
351
378
|
test('"Like" simple', () => {
|
|
352
|
-
const lexer = new QueryLexer( queryStrings[
|
|
379
|
+
const lexer = new QueryLexer( queryStrings[0] );
|
|
353
380
|
const result = lexer.query;
|
|
354
381
|
|
|
355
382
|
const tree = new ModelsTree();
|
|
@@ -358,6 +385,141 @@ describe('nodester Query Language', () => {
|
|
|
358
385
|
|
|
359
386
|
expect(result).toMatchObject(expected);
|
|
360
387
|
});
|
|
388
|
+
|
|
389
|
+
test('"NotLike" simple', () => {
|
|
390
|
+
const lexer = new QueryLexer( queryStrings[1] );
|
|
391
|
+
const result = lexer.query;
|
|
392
|
+
|
|
393
|
+
const tree = new ModelsTree();
|
|
394
|
+
tree.node.addWhere({ title: { notLike: ['some_text'] }});
|
|
395
|
+
const expected = tree.root.toObject();
|
|
396
|
+
|
|
397
|
+
expect(result).toMatchObject(expected);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test('"NotLike" short', () => {
|
|
401
|
+
const lexer = new QueryLexer( queryStrings[2] );
|
|
402
|
+
const result = lexer.query;
|
|
403
|
+
|
|
404
|
+
const tree = new ModelsTree();
|
|
405
|
+
tree.node.addWhere({ title: { notLike: ['some_text'] }});
|
|
406
|
+
const expected = tree.root.toObject();
|
|
407
|
+
|
|
408
|
+
expect(result).toMatchObject(expected);
|
|
409
|
+
});
|
|
361
410
|
});
|
|
362
411
|
|
|
412
|
+
describe('operators:in', () => {
|
|
413
|
+
const queryStrings = [
|
|
414
|
+
// IN simple.
|
|
415
|
+
'status=[REVIEWED,ANSWERED]',
|
|
416
|
+
|
|
417
|
+
// IN and limit clause.
|
|
418
|
+
'status=[REVIEWED,ANSWERED]&limit=3',
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
test('"IN" simple', () => {
|
|
422
|
+
const lexer = new QueryLexer( queryStrings[0] );
|
|
423
|
+
const result = lexer.query;
|
|
424
|
+
|
|
425
|
+
const tree = new ModelsTree();
|
|
426
|
+
tree.node.addWhere({ status: { in: ['REVIEWED', 'ANSWERED'] }});
|
|
427
|
+
const expected = tree.root.toObject();
|
|
428
|
+
|
|
429
|
+
expect(result).toMatchObject(expected);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('"IN" and "limit" clause', () => {
|
|
433
|
+
const lexer = new QueryLexer( queryStrings[1] );
|
|
434
|
+
const result = lexer.query;
|
|
435
|
+
|
|
436
|
+
const tree = new ModelsTree();
|
|
437
|
+
tree.node.limit = 3;
|
|
438
|
+
tree.node.addWhere({ status: { in: ['REVIEWED', 'ANSWERED'] }});
|
|
439
|
+
const expected = tree.root.toObject();
|
|
440
|
+
|
|
441
|
+
expect(result).toMatchObject(expected);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe('operators:inequality', () => {
|
|
446
|
+
const queryStrings = [
|
|
447
|
+
// Greater than.
|
|
448
|
+
'created_at=gt(2022)',
|
|
449
|
+
|
|
450
|
+
// Greater than or equal to.
|
|
451
|
+
'created_at=gte(2023-12-08)',
|
|
452
|
+
|
|
453
|
+
// Lower than.
|
|
454
|
+
'index=lt(10)',
|
|
455
|
+
|
|
456
|
+
// Lower than or equal to.
|
|
457
|
+
'index=lte(9)',
|
|
458
|
+
|
|
459
|
+
// Greater than in subinclude.
|
|
460
|
+
'in=comments.likes(index=gt(60))'
|
|
461
|
+
];
|
|
462
|
+
|
|
463
|
+
test('Greater than', () => {
|
|
464
|
+
const lexer = new QueryLexer( queryStrings[0] );
|
|
465
|
+
const result = lexer.query;
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
const tree = new ModelsTree();
|
|
469
|
+
tree.node.addWhere({ created_at: { gt: ['2022'] }});
|
|
470
|
+
const expected = tree.root.toObject();
|
|
471
|
+
|
|
472
|
+
expect(result).toMatchObject(expected);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test('Greater than or equal to', () => {
|
|
476
|
+
const lexer = new QueryLexer( queryStrings[1] );
|
|
477
|
+
const result = lexer.query;
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
const tree = new ModelsTree();
|
|
481
|
+
tree.node.addWhere({ created_at: { gte: ['2023-12-08'] }});
|
|
482
|
+
const expected = tree.root.toObject();
|
|
483
|
+
|
|
484
|
+
expect(result).toMatchObject(expected);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test('Lower than', () => {
|
|
488
|
+
const lexer = new QueryLexer( queryStrings[2] );
|
|
489
|
+
const result = lexer.query;
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
const tree = new ModelsTree();
|
|
493
|
+
tree.node.addWhere({ index: { lt: ['10'] }});
|
|
494
|
+
const expected = tree.root.toObject();
|
|
495
|
+
|
|
496
|
+
expect(result).toMatchObject(expected);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test('Lower than or equal to', () => {
|
|
500
|
+
const lexer = new QueryLexer( queryStrings[3] );
|
|
501
|
+
const result = lexer.query;
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
const tree = new ModelsTree();
|
|
505
|
+
tree.node.addWhere({ index: { lte: ['9'] }});
|
|
506
|
+
const expected = tree.root.toObject();
|
|
507
|
+
|
|
508
|
+
expect(result).toMatchObject(expected);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test('Greater than in subinclude', () => {
|
|
512
|
+
const lexer = new QueryLexer( queryStrings[4] );
|
|
513
|
+
const result = lexer.query;
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
const tree = new ModelsTree();
|
|
517
|
+
tree.include('comments').use('comments');
|
|
518
|
+
tree.include('likes').use('likes');
|
|
519
|
+
tree.node.addWhere({ index: { gt: ['60'] }});
|
|
520
|
+
const expected = tree.root.toObject();
|
|
521
|
+
|
|
522
|
+
expect(result).toMatchObject(expected);
|
|
523
|
+
});
|
|
524
|
+
});
|
|
363
525
|
});
|
|
File without changes
|