nodester 0.6.76 → 0.7.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.
@@ -14,7 +14,7 @@ const debug = require('debug')('nodester:interpreter:ModelsTree');
14
14
  * @access public
15
15
  */
16
16
  class ModelsTreeNode {
17
- constructor(model, parent=null, opts={}) {
17
+ constructor(model, parent = null, opts = {}) {
18
18
  this.model = model;
19
19
  this.parent = parent;
20
20
 
@@ -30,11 +30,13 @@ class ModelsTreeNode {
30
30
  this.group_by = opts.group_by ?? undefined;
31
31
  this.skip = 0;
32
32
  this.limit = -1; // No limit
33
-
33
+
34
34
  this.order = opts.order ?? undefined;
35
35
  this.order_by = opts.order_by ?? undefined;
36
36
  // Clauses\
37
37
 
38
+ this.required = false; // For root filtering via ^
39
+
38
40
  this._includes = opts.includes ?? [];
39
41
  }
40
42
 
@@ -85,14 +87,14 @@ class ModelsTreeNode {
85
87
  this.fn = null;
86
88
  }
87
89
 
88
- addWhere(condition={}) {
90
+ addWhere(condition = {}) {
89
91
  this._where = {
90
92
  ...this.where,
91
93
  ...condition
92
94
  }
93
95
  }
94
96
 
95
- addFunction(fnParams={}) {
97
+ addFunction(fnParams = {}) {
96
98
  this._functions.push(fnParams);
97
99
  }
98
100
 
@@ -102,6 +104,14 @@ class ModelsTreeNode {
102
104
  return modelTreeNode;
103
105
  }
104
106
 
107
+ markAsRequired() {
108
+ this.required = true;
109
+ // Propagate up the tree (except for root)
110
+ if (this.parent && this.parent.model !== 'root') {
111
+ this.parent.markAsRequired();
112
+ }
113
+ }
114
+
105
115
  toObject() {
106
116
  return {
107
117
  model: this.model,
@@ -116,7 +126,8 @@ class ModelsTreeNode {
116
126
  limit: this.limit,
117
127
  order: this.order,
118
128
  order_by: this.order_by,
119
-
129
+
130
+ required: this.required,
120
131
 
121
132
  includes: this.includes.map(i => i.toObject())
122
133
  }
@@ -134,7 +145,7 @@ class ModelsTree {
134
145
  this.node = this.root;
135
146
  }
136
147
 
137
- include(model, opts={}) {
148
+ include(model, opts = {}) {
138
149
  debug('include', model);
139
150
 
140
151
  const node = new ModelsTreeNode(model, this.node, opts);
@@ -153,7 +164,7 @@ class ModelsTree {
153
164
  }
154
165
 
155
166
  debug('use', model, !!foundOne ? '' : '-> failed.');
156
-
167
+
157
168
  return foundOne;
158
169
  }
159
170
 
@@ -242,9 +242,6 @@ module.exports = class QueryLexer {
242
242
  // Reset:
243
243
  tree.node.resetActiveParam();
244
244
  tree.node.resetOP();
245
-
246
- // Lift from subquery.
247
- tree.up();
248
245
  }
249
246
  const numberOfProcessedChars = i - startAt;
250
247
  return [numberOfProcessedChars];
@@ -259,7 +256,7 @@ module.exports = class QueryLexer {
259
256
 
260
257
  // If OP token:
261
258
  if (!!tree.node.op) {
262
- switch(tree.node.op) {
259
+ switch (tree.node.op) {
263
260
  case OP_TOKENS.NOT_IN:
264
261
  case OP_TOKENS.IN:
265
262
  value.push(token);
@@ -510,6 +507,19 @@ module.exports = class QueryLexer {
510
507
  continue;
511
508
  }
512
509
 
510
+
511
+ // ^ can mean:
512
+ // • root filtering (INNER JOIN) - attribute in subquery that filters root
513
+ if (char === '^') {
514
+ debug('char', char, { token, node: tree.node });
515
+
516
+ // Mark current node as required (will propagate up)
517
+ tree.node.markAsRequired();
518
+
519
+ // Continue to next char (don't add ^ to token)
520
+ continue;
521
+ }
522
+
513
523
  // = can only mean the end of a param name:
514
524
  if (char === '=') {
515
525
  const param = this.parseParamFromToken(token);
@@ -2,7 +2,7 @@
2
2
  * nodester
3
3
  * MIT Licensed
4
4
  */
5
-
5
+
6
6
  'use strict';
7
7
 
8
8
  const BOUNDS = require('../../constants/Bounds');
@@ -44,7 +44,7 @@ module.exports = traverse;
44
44
  *
45
45
  * @access public
46
46
  */
47
- function traverse(queryNode, filter=null, model=null, association=null) {
47
+ function traverse(queryNode, filter = null, model = null, association = null) {
48
48
  const _model = model ?? filter.model;
49
49
 
50
50
  try {
@@ -55,7 +55,7 @@ function traverse(queryNode, filter=null, model=null, association=null) {
55
55
  throw err;
56
56
  }
57
57
  }
58
- catch(error) {
58
+ catch (error) {
59
59
  Error.captureStackTrace(error, traverse);
60
60
  throw error;
61
61
  }
@@ -78,8 +78,15 @@ function traverse(queryNode, filter=null, model=null, association=null) {
78
78
  where,
79
79
 
80
80
  includes,
81
+ required,
81
82
  } = disassembleQueryNode(queryNode);
82
83
 
84
+ // If this include is marked as required (root filtering via ^),
85
+ // set it on the query:
86
+ if (required) {
87
+ newQuery.required = true;
88
+ }
89
+
83
90
 
84
91
  // Attribute:
85
92
  //
@@ -102,7 +109,7 @@ function traverse(queryNode, filter=null, model=null, association=null) {
102
109
  // put them through Filter:
103
110
  for (let attribute of filter.attributes) {
104
111
  if (attributesAvailable.indexOf(attribute) === -1) {
105
- const err = new NodesterQueryError(`Field '${ attribute }' is not present in model.`);
112
+ const err = new NodesterQueryError(`Field '${attribute}' is not present in model.`);
106
113
  Error.captureStackTrace(err, traverse);
107
114
  throw err;
108
115
  }
@@ -119,7 +126,7 @@ function traverse(queryNode, filter=null, model=null, association=null) {
119
126
  }
120
127
  }
121
128
  }
122
-
129
+
123
130
  // At least 1 attribute is mandatory
124
131
  // or "functions" must be set:
125
132
  if (
@@ -138,12 +145,12 @@ function traverse(queryNode, filter=null, model=null, association=null) {
138
145
  const fnName = fnParams.fn;
139
146
 
140
147
  if (typeof filter.functions[fnName] === 'undefined') {
141
- const err = new NodesterQueryError(`Function '${ fnName }' is not allowed.`);
148
+ const err = new NodesterQueryError(`Function '${fnName}' is not allowed.`);
142
149
  Error.captureStackTrace(err, traverse);
143
150
  throw err;
144
151
  }
145
152
 
146
- switch(fnName) {
153
+ switch (fnName) {
147
154
  // SQL COUNT():
148
155
  case 'count': {
149
156
  mapCOUNT(
@@ -157,7 +164,7 @@ function traverse(queryNode, filter=null, model=null, association=null) {
157
164
  }
158
165
  // Any other function:
159
166
  default:
160
- consl.warn(`function ${ fnName }() is not supported`);
167
+ consl.warn(`function ${fnName}() is not supported`);
161
168
  break;
162
169
  }
163
170
  }
@@ -167,7 +174,7 @@ function traverse(queryNode, filter=null, model=null, association=null) {
167
174
  const order = {};
168
175
 
169
176
  const clausesEntries = Object.entries(clauses);
170
- for (const [ clauseName, value ] of clausesEntries) {
177
+ for (const [clauseName, value] of clausesEntries) {
171
178
 
172
179
  // If clause is not available:
173
180
  if (filter != null) {
@@ -176,7 +183,7 @@ function traverse(queryNode, filter=null, model=null, association=null) {
176
183
  }
177
184
  }
178
185
 
179
- switch(clauseName) {
186
+ switch (clauseName) {
180
187
  case 'group_by': {
181
188
  // Check if this value is a valid attribute:
182
189
  if (typeof value === 'undefined') {
@@ -184,7 +191,7 @@ function traverse(queryNode, filter=null, model=null, association=null) {
184
191
  }
185
192
 
186
193
  if (typeof _model.tableAttributes[value] === 'undefined') {
187
- const err = new NodesterQueryError(`group_by '${ value }' is not allowed.`);
194
+ const err = new NodesterQueryError(`group_by '${value}' is not allowed.`);
188
195
  Error.captureStackTrace(err, traverse);
189
196
  throw err;
190
197
  }
@@ -197,7 +204,7 @@ function traverse(queryNode, filter=null, model=null, association=null) {
197
204
  const _value = _setValueWithBounds(value, 'number', filter.bounds.clauses.limit);
198
205
 
199
206
  // Do not set if negative:
200
- if (_value < 0)
207
+ if (_value < 0)
201
208
  continue;
202
209
 
203
210
  newQuery.limit = _value;
@@ -237,9 +244,9 @@ function traverse(queryNode, filter=null, model=null, association=null) {
237
244
  const staticClausesEntries = Object.entries(filter.statics.clauses);
238
245
 
239
246
  for (let entry of staticClausesEntries) {
240
- const [ clauseName, staticClauseValue ] = entry;
247
+ const [clauseName, staticClauseValue] = entry;
241
248
 
242
- switch(clauseName) {
249
+ switch (clauseName) {
243
250
  case 'group_by':
244
251
  newQuery.group = staticClauseValue;
245
252
  continue;
@@ -282,7 +289,7 @@ function traverse(queryNode, filter=null, model=null, association=null) {
282
289
  newQuery.order = sequelize.random();
283
290
  }
284
291
  else {
285
- const column = sequelize.col( order.by );
292
+ const column = sequelize.col(order.by);
286
293
  switch (order.order) {
287
294
  // MAX/MIN:
288
295
  case 'max-asc':
@@ -293,7 +300,7 @@ function traverse(queryNode, filter=null, model=null, association=null) {
293
300
  case 'min':
294
301
  case 'min-asc':
295
302
  case 'max-desc':
296
- newQuery.order = [ sequelize.fn('max', column), 'DESC' ];
303
+ newQuery.order = [sequelize.fn('max', column), 'DESC'];
297
304
  break;
298
305
  // MAX/MIN\
299
306
 
@@ -303,7 +310,7 @@ function traverse(queryNode, filter=null, model=null, association=null) {
303
310
  break;
304
311
 
305
312
  default:
306
- newQuery.order = [ [order.by, order.order] ];
313
+ newQuery.order = [[order.by, order.order]];
307
314
  break;
308
315
  }
309
316
  }
@@ -316,7 +323,7 @@ function traverse(queryNode, filter=null, model=null, association=null) {
316
323
  const includeName = include.model;
317
324
 
318
325
  if (rootModelAssociations[includeName] === undefined) {
319
- const err = new NodesterQueryError(`No include named '${ includeName }'`);
326
+ const err = new NodesterQueryError(`No include named '${includeName}'`);
320
327
  Error.captureStackTrace(err, traverse);
321
328
  throw err;
322
329
  }
@@ -330,13 +337,13 @@ function traverse(queryNode, filter=null, model=null, association=null) {
330
337
 
331
338
  // Set aatributes from Query:
332
339
  const whereEntries = Object.entries(where);
333
- for (const [ attribute, value ] of whereEntries) {
340
+ for (const [attribute, value] of whereEntries) {
334
341
  parseWhereEntry(attribute, value, newQuery.where, _model);
335
342
  }
336
343
 
337
344
  // Static attributes override previously set attributes:
338
345
  const staticAttributesEntries = Object.entries(filter.statics.attributes);
339
- for (const [ attribute, staticValue ] of staticAttributesEntries) {
346
+ for (const [attribute, staticValue] of staticAttributesEntries) {
340
347
  newQuery.where[attribute] = parseValue(staticValue, attribute, _model);
341
348
  }
342
349
 
@@ -369,13 +376,13 @@ function traverse(queryNode, filter=null, model=null, association=null) {
369
376
  */
370
377
  function _traverseIncludes(includes, rootModel, filter, resultQuery) {
371
378
  const filterIncludesEntries = Object.entries(filter.includes);
372
- for (let [ includeName, includeFilter ] of filterIncludesEntries) {
379
+ for (let [includeName, includeFilter] of filterIncludesEntries) {
373
380
 
374
381
  const association = rootModel.associations[includeName];
375
382
 
376
383
  // If no such association:
377
384
  if (!association) {
378
- const err = new NodesterQueryError(`No include named '${ includeName }'`);
385
+ const err = new NodesterQueryError(`No include named '${includeName}'`);
379
386
  Error.captureStackTrace(err, _traverseIncludes);
380
387
  throw err;
381
388
  }
@@ -394,7 +401,7 @@ function _traverseIncludes(includes, rootModel, filter, resultQuery) {
394
401
  }
395
402
 
396
403
  function _traverseIncludedOrders(resultQuery, rootModel) {
397
- for (let i=0; i < resultQuery.include.length; i++) {
404
+ for (let i = 0; i < resultQuery.include.length; i++) {
398
405
  const include = resultQuery.include[i];
399
406
 
400
407
  if (!include?.order) {
@@ -411,7 +418,7 @@ function _traverseIncludedOrders(resultQuery, rootModel) {
411
418
  associationType
412
419
  } = getModelAssociationProps(rootModel.associations[association]);
413
420
 
414
- switch(associationType) {
421
+ switch (associationType) {
415
422
  case 'HasMany': {
416
423
  include.separate = true;
417
424
  // resultQuery.order.push([
@@ -434,7 +441,7 @@ function _traverseIncludedOrders(resultQuery, rootModel) {
434
441
  function _setValueWithBounds(value, type, bounds) {
435
442
  if (typeof bounds === 'object') {
436
443
 
437
- switch(type) {
444
+ switch (type) {
438
445
  case 'number': {
439
446
  let _value = value;
440
447
 
@@ -2,7 +2,7 @@
2
2
  * nodester
3
3
  * MIT Licensed
4
4
  */
5
-
5
+
6
6
  'use strict';
7
7
 
8
8
 
@@ -18,6 +18,7 @@ function _disassembleQueryNode(queryNode) {
18
18
  functions,
19
19
  where,
20
20
  includes,
21
+ required,
21
22
  ...clauses
22
23
  } = queryNode;
23
24
 
@@ -27,6 +28,7 @@ function _disassembleQueryNode(queryNode) {
27
28
  functions: functions ?? [],
28
29
  where: where ?? {},
29
30
  includes: includes ?? [],
31
+ required: required ?? false,
30
32
  };
31
33
  }
32
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodester",
3
- "version": "0.6.76",
3
+ "version": "0.7.01",
4
4
  "description": "A versatile REST framework for Node.js",
5
5
  "directories": {
6
6
  "docs": "docs",
package/tests/nql.test.js CHANGED
@@ -24,7 +24,7 @@ describe('nodester Query Language', () => {
24
24
  ];
25
25
 
26
26
  it('Simple where', async () => {
27
- const lexer = new QueryLexer( queryStrings[0] );
27
+ const lexer = new QueryLexer(queryStrings[0]);
28
28
  const result = await lexer.parse();
29
29
 
30
30
  const tree = new ModelsTree();
@@ -35,24 +35,24 @@ describe('nodester Query Language', () => {
35
35
  });
36
36
 
37
37
  it('Only certain attributes', async () => {
38
- const lexer = new QueryLexer( queryStrings[1] );
38
+ const lexer = new QueryLexer(queryStrings[1]);
39
39
  const result = await lexer.parse();
40
40
 
41
41
  const tree = new ModelsTree();
42
- tree.node.attributes = [ 'id', 'text' ];
42
+ tree.node.attributes = ['id', 'text'];
43
43
  const expected = tree.root.toObject();
44
44
 
45
45
  expect(result).toMatchObject(expected);
46
46
  });
47
47
 
48
48
  test('All possible params', async () => {
49
- const lexer = new QueryLexer( queryStrings[2] );
49
+ const lexer = new QueryLexer(queryStrings[2]);
50
50
  const result = await lexer.parse();
51
51
 
52
52
 
53
53
  const tree = new ModelsTree();
54
54
  tree.node.addWhere({ id: ['10'], position: ['4'] });
55
- tree.node.attributes = [ 'id', 'content', 'position', 'created_at' ];
55
+ tree.node.attributes = ['id', 'content', 'position', 'created_at'];
56
56
  tree.node.limit = 3;
57
57
  tree.node.skip = 10;
58
58
  tree.node.order = 'desc';
@@ -74,20 +74,20 @@ describe('nodester Query Language', () => {
74
74
  '2-horizontals': 'includes=comments,users&id=1000',
75
75
  // 4 horizontals with subquery.
76
76
  '4-horizontals': 'in=categories,replies.users,comments(order_by=position&order=desc),users.avatars',
77
-
77
+
78
78
  // Horizontals queried.
79
79
  'horizontals-queried': 'includes=comments(order=desc),users,likes(order=rand),reposts&id=1000',
80
80
  // Horizontals queried №2.
81
81
  'horizontals-queried-2': 'in=comments(order_by=index&order=asc).users.karma',
82
82
  // Horizontals queried №3.
83
83
  'horizontals-queried-3': 'in=reactions,comments(user_id=gte(4)&skip=10&limit=2).users,likes,reposts',
84
-
84
+
85
85
  // Separated includes.
86
86
  'separated-includes': 'includes=comments(order=rand)&id=7&limit=3&includes=users(a=id,content)',
87
87
  };
88
88
 
89
89
  test('Simple includes', async () => {
90
- const lexer = new QueryLexer( queryStrings['simple-includes'] );
90
+ const lexer = new QueryLexer(queryStrings['simple-includes']);
91
91
  const result = await lexer.parse();
92
92
 
93
93
 
@@ -100,13 +100,13 @@ describe('nodester Query Language', () => {
100
100
  });
101
101
 
102
102
  test('Include with all possible params', async () => {
103
- const lexer = new QueryLexer( queryStrings['include-with-params'] );
103
+ const lexer = new QueryLexer(queryStrings['include-with-params']);
104
104
  const result = await lexer.parse();
105
105
 
106
106
  const tree = new ModelsTree();
107
107
  tree.include('comments').use('comments');
108
108
  tree.node.addWhere({ id: ['10'], position: ['4'] });
109
- tree.node.attributes = [ 'id', 'content', 'position' ];
109
+ tree.node.attributes = ['id', 'content', 'position'];
110
110
  tree.node.limit = 3;
111
111
  tree.node.skip = 10;
112
112
  tree.node.order = 'desc';
@@ -117,7 +117,7 @@ describe('nodester Query Language', () => {
117
117
  });
118
118
 
119
119
  test('2 horizontals', async () => {
120
- const lexer = new QueryLexer( queryStrings['2-horizontals'] );
120
+ const lexer = new QueryLexer(queryStrings['2-horizontals']);
121
121
  const result = await lexer.parse();
122
122
 
123
123
 
@@ -133,7 +133,7 @@ describe('nodester Query Language', () => {
133
133
  test('4 horizontals', async () => {
134
134
  // in=categories,replies.users,comments(order_by=position&order=desc),users.avatars
135
135
 
136
- const lexer = new QueryLexer( queryStrings['4-horizontals'] );
136
+ const lexer = new QueryLexer(queryStrings['4-horizontals']);
137
137
  const result = await lexer.parse();
138
138
 
139
139
 
@@ -162,7 +162,7 @@ describe('nodester Query Language', () => {
162
162
  });
163
163
 
164
164
  test('Horizontals queried', async () => {
165
- const lexer = new QueryLexer( queryStrings['horizontals-queried'] );
165
+ const lexer = new QueryLexer(queryStrings['horizontals-queried']);
166
166
  const result = await lexer.parse();
167
167
 
168
168
 
@@ -182,7 +182,7 @@ describe('nodester Query Language', () => {
182
182
  });
183
183
 
184
184
  test('Horizontals queried №2', async () => {
185
- const lexer = new QueryLexer( queryStrings['horizontals-queried-2'] );
185
+ const lexer = new QueryLexer(queryStrings['horizontals-queried-2']);
186
186
  const result = await lexer.parse();
187
187
 
188
188
  const tree = new ModelsTree();
@@ -200,7 +200,7 @@ describe('nodester Query Language', () => {
200
200
  });
201
201
 
202
202
  test('Horizontals queried №3', async () => {
203
- const lexer = new QueryLexer( queryStrings['horizontals-queried-3'] );
203
+ const lexer = new QueryLexer(queryStrings['horizontals-queried-3']);
204
204
  const result = await lexer.parse();
205
205
 
206
206
  const tree = new ModelsTree();
@@ -208,9 +208,9 @@ describe('nodester Query Language', () => {
208
208
 
209
209
  tree.include('comments').use('comments');
210
210
  tree.node.addWhere({
211
- user_id: {
212
- gte: ['4']
213
- }
211
+ user_id: {
212
+ gte: ['4']
213
+ }
214
214
  });
215
215
  tree.node.skip = 10;
216
216
  tree.node.limit = 2;
@@ -227,7 +227,7 @@ describe('nodester Query Language', () => {
227
227
  });
228
228
 
229
229
  test('Separated includes"', async () => {
230
- const lexer = new QueryLexer( queryStrings['separated-includes'] );
230
+ const lexer = new QueryLexer(queryStrings['separated-includes']);
231
231
  const result = await lexer.parse();
232
232
 
233
233
  const tree = new ModelsTree();
@@ -237,7 +237,7 @@ describe('nodester Query Language', () => {
237
237
  tree.node.order = 'rand';
238
238
  tree.up();
239
239
  tree.include('users').use('users');
240
- tree.node.attributes = [ 'id', 'content' ];
240
+ tree.node.attributes = ['id', 'content'];
241
241
  const expected = tree.root.toObject();
242
242
 
243
243
  expect(result).toMatchObject(expected);
@@ -260,10 +260,16 @@ describe('nodester Query Language', () => {
260
260
 
261
261
  // Complex subincludes query, "+" syntaxis.
262
262
  'includes=comments(order=desc).users+likes(order=rand&order_by=position)&id=1000',
263
+
264
+ // Complex subincludes query, "+" syntaxis with include.
265
+ 'includes=comments(order=desc).users+likes(order=rand&order_by=position).reviews&id=1000',
266
+
267
+ // Complex subincludes query, "+" syntaxis with double include.
268
+ 'includes=comments(order=desc).users+likes(order=rand&order_by=position).reviews.admin&id=1000',
263
269
  ];
264
270
 
265
271
  test('Simple subinclude', async () => {
266
- const lexer = new QueryLexer( queryStrings[0] );
272
+ const lexer = new QueryLexer(queryStrings[0]);
267
273
  const result = await lexer.parse();
268
274
 
269
275
 
@@ -276,7 +282,7 @@ describe('nodester Query Language', () => {
276
282
  });
277
283
 
278
284
  test('Deep subincludes', async () => {
279
- const lexer = new QueryLexer( queryStrings[1] );
285
+ const lexer = new QueryLexer(queryStrings[1]);
280
286
  const result = await lexer.parse();
281
287
 
282
288
 
@@ -292,7 +298,7 @@ describe('nodester Query Language', () => {
292
298
  });
293
299
 
294
300
  test('Simple horizontal subinclude, "+" syntaxis"', async () => {
295
- const lexer = new QueryLexer( queryStrings[2] );
301
+ const lexer = new QueryLexer(queryStrings[2]);
296
302
  const result = await lexer.parse();
297
303
 
298
304
 
@@ -306,7 +312,7 @@ describe('nodester Query Language', () => {
306
312
  });
307
313
 
308
314
  test('Subinclude query', async () => {
309
- const lexer = new QueryLexer( queryStrings[3] );
315
+ const lexer = new QueryLexer(queryStrings[3]);
310
316
  const result = await lexer.parse();
311
317
 
312
318
 
@@ -321,7 +327,45 @@ describe('nodester Query Language', () => {
321
327
  });
322
328
 
323
329
  test('Complex subincludes query, "+" syntaxis', async () => {
324
- const lexer = new QueryLexer( queryStrings[4] );
330
+ const lexer = new QueryLexer(queryStrings[4]);
331
+ const result = await lexer.parse();
332
+
333
+
334
+ const tree = new ModelsTree();
335
+ tree.include('comments').use('comments');
336
+ tree.node.addWhere({ id: ['1000'] });
337
+ tree.node.order = 'desc';
338
+ tree.include('users');
339
+ tree.include('likes') && tree.use('likes');
340
+ tree.node.order = 'rand';
341
+ tree.node.order_by = 'position';
342
+ const expected = tree.root.toObject();
343
+
344
+ expect(result).toMatchObject(expected);
345
+ });
346
+
347
+ test('Complex subincludes query, "+" syntaxis with include', async () => {
348
+ const lexer = new QueryLexer(queryStrings[5]);
349
+ const result = await lexer.parse();
350
+
351
+
352
+ const tree = new ModelsTree();
353
+ tree.node.addWhere({ id: ['1000'] });
354
+ tree.include('comments').use('comments');
355
+ tree.node.order = 'desc';
356
+ tree.include('users');
357
+ tree.include('likes') && tree.use('likes');
358
+ tree.node.order = 'rand';
359
+ tree.node.order_by = 'position';
360
+ tree.include('reviews');
361
+ tree.up();
362
+ const expected = tree.root.toObject();
363
+
364
+ expect(result).toMatchObject(expected);
365
+ });
366
+
367
+ test('Complex subincludes query, "+" syntaxis with double include', async () => {
368
+ const lexer = new QueryLexer(queryStrings[6]);
325
369
  const result = await lexer.parse();
326
370
 
327
371
 
@@ -333,6 +377,9 @@ describe('nodester Query Language', () => {
333
377
  tree.include('likes') && tree.use('likes');
334
378
  tree.node.order = 'rand';
335
379
  tree.node.order_by = 'position';
380
+ tree.include('reviews') && tree.use('reviews');
381
+ tree.include('admin')
382
+ tree.up();
336
383
  tree.up();
337
384
  const expected = tree.root.toObject();
338
385
 
@@ -349,22 +396,22 @@ describe('nodester Query Language', () => {
349
396
  ];
350
397
 
351
398
  test('"OR" simple', async () => {
352
- const lexer = new QueryLexer( queryStrings[0] );
399
+ const lexer = new QueryLexer(queryStrings[0]);
353
400
  const result = await lexer.parse();
354
401
 
355
402
  const tree = new ModelsTree();
356
- tree.node.addWhere({ or: [ { index: ['2'] }, { position: ['5'] } ] });
403
+ tree.node.addWhere({ or: [{ index: ['2'] }, { position: ['5'] }] });
357
404
  const expected = tree.root.toObject();
358
405
 
359
406
  expect(result).toMatchObject(expected);
360
407
  });
361
408
 
362
409
  test('"OR" short', async () => {
363
- const lexer = new QueryLexer( queryStrings[1] );
410
+ const lexer = new QueryLexer(queryStrings[1]);
364
411
  const result = await lexer.parse();
365
412
 
366
413
  const tree = new ModelsTree();
367
- tree.node.addWhere({ or: [ { index: ['2'] }, { position: ['5'] } ] });
414
+ tree.node.addWhere({ or: [{ index: ['2'] }, { position: ['5'] }] });
368
415
  const expected = tree.root.toObject();
369
416
 
370
417
  expect(result).toMatchObject(expected);
@@ -382,7 +429,7 @@ describe('nodester Query Language', () => {
382
429
  ];
383
430
 
384
431
  test('"NOT" simple', async () => {
385
- const lexer = new QueryLexer( queryStrings[0] );
432
+ const lexer = new QueryLexer(queryStrings[0]);
386
433
  const result = await lexer.parse();
387
434
 
388
435
  const tree = new ModelsTree();
@@ -393,7 +440,7 @@ describe('nodester Query Language', () => {
393
440
  });
394
441
 
395
442
  test('"NOT" short', async () => {
396
- const lexer = new QueryLexer( queryStrings[1] );
443
+ const lexer = new QueryLexer(queryStrings[1]);
397
444
  const result = await lexer.parse();
398
445
 
399
446
  const tree = new ModelsTree();
@@ -404,12 +451,12 @@ describe('nodester Query Language', () => {
404
451
  });
405
452
 
406
453
  test('"NOT" inside includes', async () => {
407
- const lexer = new QueryLexer( queryStrings[2] );
454
+ const lexer = new QueryLexer(queryStrings[2]);
408
455
  const result = await lexer.parse();
409
456
 
410
457
  const tree = new ModelsTree();
411
458
  tree.include('comments').use('comments');
412
- tree.node.addWhere({ id: { not: ['7'] }});
459
+ tree.node.addWhere({ id: { not: ['7'] } });
413
460
  const expected = tree.root.toObject();
414
461
 
415
462
  expect(result).toMatchObject(expected);
@@ -429,33 +476,33 @@ describe('nodester Query Language', () => {
429
476
  ];
430
477
 
431
478
  test('"Like" simple', async () => {
432
- const lexer = new QueryLexer( queryStrings[0] );
479
+ const lexer = new QueryLexer(queryStrings[0]);
433
480
  const result = await lexer.parse();
434
481
 
435
482
  const tree = new ModelsTree();
436
- tree.node.addWhere({ title: { like: ['some_text'] }});
483
+ tree.node.addWhere({ title: { like: ['some_text'] } });
437
484
  const expected = tree.root.toObject();
438
485
 
439
486
  expect(result).toMatchObject(expected);
440
487
  });
441
488
 
442
489
  test('"NotLike" simple', async () => {
443
- const lexer = new QueryLexer( queryStrings[1] );
490
+ const lexer = new QueryLexer(queryStrings[1]);
444
491
  const result = await lexer.parse();
445
492
 
446
493
  const tree = new ModelsTree();
447
- tree.node.addWhere({ title: { notLike: ['some_text'] }});
494
+ tree.node.addWhere({ title: { notLike: ['some_text'] } });
448
495
  const expected = tree.root.toObject();
449
496
 
450
497
  expect(result).toMatchObject(expected);
451
498
  });
452
499
 
453
500
  test('"NotLike" short', async () => {
454
- const lexer = new QueryLexer( queryStrings[2] );
501
+ const lexer = new QueryLexer(queryStrings[2]);
455
502
  const result = await lexer.parse();
456
503
 
457
504
  const tree = new ModelsTree();
458
- tree.node.addWhere({ title: { notLike: ['some_text'] }});
505
+ tree.node.addWhere({ title: { notLike: ['some_text'] } });
459
506
  const expected = tree.root.toObject();
460
507
 
461
508
  expect(result).toMatchObject(expected);
@@ -472,23 +519,23 @@ describe('nodester Query Language', () => {
472
519
  ];
473
520
 
474
521
  test('"IN" simple', async () => {
475
- const lexer = new QueryLexer( queryStrings[0] );
522
+ const lexer = new QueryLexer(queryStrings[0]);
476
523
  const result = await lexer.parse();
477
524
 
478
525
  const tree = new ModelsTree();
479
- tree.node.addWhere({ status: { in: ['REVIEWED', 'ANSWERED'] }});
526
+ tree.node.addWhere({ status: { in: ['REVIEWED', 'ANSWERED'] } });
480
527
  const expected = tree.root.toObject();
481
528
 
482
529
  expect(result).toMatchObject(expected);
483
530
  });
484
531
 
485
532
  test('"IN" and "limit" clause', async () => {
486
- const lexer = new QueryLexer( queryStrings[1] );
533
+ const lexer = new QueryLexer(queryStrings[1]);
487
534
  const result = await lexer.parse();
488
535
 
489
536
  const tree = new ModelsTree();
490
537
  tree.node.limit = 3;
491
- tree.node.addWhere({ status: { in: ['REVIEWED', 'ANSWERED'] }});
538
+ tree.node.addWhere({ status: { in: ['REVIEWED', 'ANSWERED'] } });
492
539
  const expected = tree.root.toObject();
493
540
 
494
541
  expect(result).toMatchObject(expected);
@@ -514,62 +561,62 @@ describe('nodester Query Language', () => {
514
561
  ];
515
562
 
516
563
  test('Greater than', async () => {
517
- const lexer = new QueryLexer( queryStrings[0] );
564
+ const lexer = new QueryLexer(queryStrings[0]);
518
565
  const result = await lexer.parse();
519
566
 
520
567
 
521
568
  const tree = new ModelsTree();
522
- tree.node.addWhere({ created_at: { gt: ['2022'] }});
569
+ tree.node.addWhere({ created_at: { gt: ['2022'] } });
523
570
  const expected = tree.root.toObject();
524
571
 
525
572
  expect(result).toMatchObject(expected);
526
573
  });
527
574
 
528
575
  test('Greater than or equal to', async () => {
529
- const lexer = new QueryLexer( queryStrings[1] );
576
+ const lexer = new QueryLexer(queryStrings[1]);
530
577
  const result = await lexer.parse();
531
578
 
532
579
 
533
580
  const tree = new ModelsTree();
534
- tree.node.addWhere({ created_at: { gte: ['2023-12-08'] }});
581
+ tree.node.addWhere({ created_at: { gte: ['2023-12-08'] } });
535
582
  const expected = tree.root.toObject();
536
583
 
537
584
  expect(result).toMatchObject(expected);
538
585
  });
539
586
 
540
587
  test('Lower than', async () => {
541
- const lexer = new QueryLexer( queryStrings[2] );
588
+ const lexer = new QueryLexer(queryStrings[2]);
542
589
  const result = await lexer.parse();
543
590
 
544
591
 
545
592
  const tree = new ModelsTree();
546
- tree.node.addWhere({ index: { lt: ['10'] }});
593
+ tree.node.addWhere({ index: { lt: ['10'] } });
547
594
  const expected = tree.root.toObject();
548
595
 
549
596
  expect(result).toMatchObject(expected);
550
597
  });
551
598
 
552
599
  test('Lower than or equal to', async () => {
553
- const lexer = new QueryLexer( queryStrings[3] );
600
+ const lexer = new QueryLexer(queryStrings[3]);
554
601
  const result = await lexer.parse();
555
602
 
556
603
 
557
604
  const tree = new ModelsTree();
558
- tree.node.addWhere({ index: { lte: ['9'] }});
605
+ tree.node.addWhere({ index: { lte: ['9'] } });
559
606
  const expected = tree.root.toObject();
560
607
 
561
608
  expect(result).toMatchObject(expected);
562
609
  });
563
610
 
564
611
  test('Greater than in subinclude', async () => {
565
- const lexer = new QueryLexer( queryStrings[4] );
612
+ const lexer = new QueryLexer(queryStrings[4]);
566
613
  const result = await lexer.parse();
567
614
 
568
615
 
569
616
  const tree = new ModelsTree();
570
617
  tree.include('comments').use('comments');
571
618
  tree.include('likes').use('likes');
572
- tree.node.addWhere({ index: { gt: ['60'] }});
619
+ tree.node.addWhere({ index: { gt: ['60'] } });
573
620
  const expected = tree.root.toObject();
574
621
 
575
622
  expect(result).toMatchObject(expected);
@@ -587,18 +634,18 @@ describe('nodester Query Language', () => {
587
634
  }
588
635
 
589
636
  test('AND (simple)', async () => {
590
- const lexer = new QueryLexer( queryStrings.and_simple );
637
+ const lexer = new QueryLexer(queryStrings.and_simple);
591
638
  const result = await lexer.parse();
592
639
 
593
640
  const tree = new ModelsTree();
594
- tree.node.addWhere({ id: { gte: ['2'], lt: ['5'] }});
641
+ tree.node.addWhere({ id: { gte: ['2'], lt: ['5'] } });
595
642
  const expected = tree.root.toObject();
596
643
 
597
644
  expect(result).toMatchObject(expected);
598
645
  });
599
646
 
600
647
  test('AND (more OP)', async () => {
601
- const lexer = new QueryLexer( queryStrings.and_more_op );
648
+ const lexer = new QueryLexer(queryStrings.and_more_op);
602
649
  const result = await lexer.parse();
603
650
 
604
651
  const tree = new ModelsTree();
@@ -611,7 +658,7 @@ describe('nodester Query Language', () => {
611
658
  });
612
659
 
613
660
  test('AND (in subincludes #0)', async () => {
614
- const lexer = new QueryLexer( queryStrings.and_in_subincludes_0 );
661
+ const lexer = new QueryLexer(queryStrings.and_in_subincludes_0);
615
662
  const result = await lexer.parse();
616
663
 
617
664
  const tree = new ModelsTree();
@@ -625,7 +672,7 @@ describe('nodester Query Language', () => {
625
672
  });
626
673
 
627
674
  test('AND (in subincludes #1)', async () => {
628
- const lexer = new QueryLexer( queryStrings.and_in_subincludes_1 );
675
+ const lexer = new QueryLexer(queryStrings.and_in_subincludes_1);
629
676
  const result = await lexer.parse();
630
677
 
631
678
  const tree = new ModelsTree();
@@ -642,7 +689,7 @@ describe('nodester Query Language', () => {
642
689
  });
643
690
 
644
691
  test('AND (in subincludes #2)', async () => {
645
- const lexer = new QueryLexer( queryStrings.and_in_subincludes_2 );
692
+ const lexer = new QueryLexer(queryStrings.and_in_subincludes_2);
646
693
  const result = await lexer.parse();
647
694
 
648
695
  const tree = new ModelsTree();
@@ -669,7 +716,7 @@ describe('nodester Query Language', () => {
669
716
  }
670
717
 
671
718
  test('Count (full key name)', async () => {
672
- const lexer = new QueryLexer( queryStrings.count_long );
719
+ const lexer = new QueryLexer(queryStrings.count_long);
673
720
  const result = await lexer.parse();
674
721
 
675
722
 
@@ -684,7 +731,7 @@ describe('nodester Query Language', () => {
684
731
  });
685
732
 
686
733
  test('Count (short key name)', async () => {
687
- const lexer = new QueryLexer( queryStrings.count_long );
734
+ const lexer = new QueryLexer(queryStrings.count_long);
688
735
  const result = await lexer.parse();
689
736
 
690
737
 
@@ -699,7 +746,7 @@ describe('nodester Query Language', () => {
699
746
  });
700
747
 
701
748
  test('Count and includes', async () => {
702
- const lexer = new QueryLexer( queryStrings.count_and_includes );
749
+ const lexer = new QueryLexer(queryStrings.count_and_includes);
703
750
  const result = await lexer.parse();
704
751
 
705
752
 
@@ -714,4 +761,46 @@ describe('nodester Query Language', () => {
714
761
  expect(result).toMatchObject(expected);
715
762
  });
716
763
  });
764
+
765
+ describe('root-filtering', () => {
766
+ const queryStrings = {
767
+ simple: 'includes=order(^is_filled=1)',
768
+ complex: 'includes=product.order(^is_filled=1)+photos(limit=5)',
769
+ }
770
+
771
+ test('Simple root filtering with ^', async () => {
772
+ const lexer = new QueryLexer(queryStrings.simple);
773
+ const result = await lexer.parse();
774
+
775
+ const tree = new ModelsTree();
776
+ tree.include('order').use('order');
777
+ tree.node.addWhere({ is_filled: ['1'] });
778
+ tree.node.markAsRequired();
779
+ const expected = tree.root.toObject();
780
+
781
+ expect(result).toMatchObject(expected);
782
+ expect(result.includes[0].required).toBe(true);
783
+ });
784
+
785
+ test('Complex nested root filtering with ^', async () => {
786
+ const lexer = new QueryLexer(queryStrings.complex);
787
+ const result = await lexer.parse();
788
+
789
+ const tree = new ModelsTree();
790
+ tree.include('product').use('product');
791
+ tree.include('order').use('order');
792
+ tree.node.addWhere({ is_filled: ['1'] });
793
+ tree.node.markAsRequired();
794
+ tree.up();
795
+ tree.up();
796
+ tree.include('photos').use('photos');
797
+ tree.node.limit = 5;
798
+ const expected = tree.root.toObject();
799
+
800
+ expect(result).toMatchObject(expected);
801
+ expect(result.includes[0].required).toBe(true);
802
+ expect(result.includes[0].includes[0].required).toBe(true);
803
+ expect(result.includes[1].required).toBe(false);
804
+ });
805
+ });
717
806
  });