nodester 0.2.2 → 0.2.4

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.
@@ -17,7 +17,7 @@ module.exports = function initNodesterQL() {
17
17
  return nqlHandle;
18
18
  };
19
19
 
20
- async function nqlHandle(req, res, next) {
20
+ function nqlHandle(req, res, next) {
21
21
  // Object, which will be populated with parsed query.
22
22
  req.nquery = {};
23
23
 
@@ -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',
@@ -59,13 +68,12 @@ module.exports = class QueryLexer {
59
68
 
60
69
  parseIsolatedQuery(queryString='', startAt=0, tree) {
61
70
  const isSubQuery = tree.node.model !== 'root';
62
- debug({ isSubQuery, startAt });
63
71
 
64
- // Token is String, accumulated char-by-char.
72
+ // Token is a String, accumulated char-by-char.
65
73
  let token = '';
66
74
  // Value of param ('id=10' OR 'fields=id,text').
67
75
  let value = [];
68
- // Model, that was active before cursor went up in the tree.
76
+ // Model, that was active before a cursor went up in the tree.
69
77
  let previousActive = null;
70
78
 
71
79
  for (let i=startAt; i < queryString.length; i++) {
@@ -125,8 +133,12 @@ module.exports = class QueryLexer {
125
133
  // Structure of a value depends on OP:
126
134
  let fullOp = {};
127
135
  switch (tree.node.op) {
128
- case 'not':
129
- case 'like':
136
+ case OP_TOKENS.NOT:
137
+ case OP_TOKENS.LIKE:
138
+ case OP_TOKENS.GREATER:
139
+ case OP_TOKENS.GREATER_OR_EQUAL:
140
+ case OP_TOKENS.LOWER:
141
+ case OP_TOKENS.LOWER_OR_EQUAL:
130
142
  fullOp = { [tree.node.activeParam]: { [tree.node.op]: [token] } };
131
143
  break;
132
144
  default:
@@ -139,7 +151,7 @@ module.exports = class QueryLexer {
139
151
 
140
152
  // Reset:
141
153
  tree.node.resetOP();
142
- tree.node.activeParam = 'includes';
154
+ tree.node.activeParam = PARAM_TOKENS.INCLUDES;
143
155
  token = '';
144
156
  value = [];
145
157
  continue;
@@ -173,19 +185,23 @@ module.exports = class QueryLexer {
173
185
 
174
186
  // Reset:
175
187
  tree.node.resetFN();
176
- tree.node.activeParam = 'includes';
188
+ tree.node.activeParam = PARAM_TOKENS.INCLUDES;
177
189
  token = '';
178
190
  value = [];
179
191
  continue;
180
192
  }
181
193
 
182
194
  // If end of subquery:
183
- if (!!tree.node.activeParam && tree.node.activeParam !== 'includes') {
195
+ if (!!tree.node.activeParam && tree.node.activeParam !== PARAM_TOKENS.INCLUDES) {
184
196
  // Set value.
185
197
  this.setNodeParam(tree.node, token, value);
198
+
186
199
  // Reset:
187
200
  tree.node.resetActiveParam();
188
201
  tree.node.resetOP();
202
+
203
+ // Lift from subquery.
204
+ tree.up();
189
205
  }
190
206
  const numberOfProcessedChars = i - startAt;
191
207
  return [ numberOfProcessedChars ];
@@ -206,7 +222,7 @@ module.exports = class QueryLexer {
206
222
  }
207
223
 
208
224
  // If param value:
209
- if (tree.node.activeParam !== 'includes') {
225
+ if (tree.node.activeParam !== PARAM_TOKENS.INCLUDES) {
210
226
  value.push(token);
211
227
  token = '';
212
228
  continue;
@@ -218,7 +234,7 @@ module.exports = class QueryLexer {
218
234
  }
219
235
 
220
236
  // Horizontal include:
221
- if (tree.node.activeParam === 'includes') {
237
+ if (tree.node.activeParam === PARAM_TOKENS.INCLUDES) {
222
238
  const model = token;
223
239
  tree.use(model) ?? tree.include(model);
224
240
 
@@ -227,7 +243,7 @@ module.exports = class QueryLexer {
227
243
  tree.node.resetActiveParam();
228
244
  tree.upToRoot();
229
245
 
230
- tree.node.activeParam = 'includes';
246
+ tree.node.activeParam = PARAM_TOKENS.INCLUDES;
231
247
 
232
248
  token = '';
233
249
  continue;
@@ -245,7 +261,7 @@ module.exports = class QueryLexer {
245
261
  // Vertical include:
246
262
  if (!!previousActive) {
247
263
  tree.use(previousActive);
248
- tree.node.activeParam = 'includes';
264
+ tree.node.activeParam = PARAM_TOKENS.INCLUDES;
249
265
  token = '';
250
266
  continue;
251
267
  }
@@ -256,7 +272,7 @@ module.exports = class QueryLexer {
256
272
  tree.use(model) ?? tree.include(model).use(model);
257
273
 
258
274
  // Prepare for more includes:
259
- tree.node.activeParam = 'includes';
275
+ tree.node.activeParam = PARAM_TOKENS.INCLUDES;
260
276
 
261
277
  token = '';
262
278
  continue;
@@ -278,7 +294,7 @@ module.exports = class QueryLexer {
278
294
  tree.up();
279
295
 
280
296
  // Prepare for more includes:
281
- tree.node.activeParam = 'includes';
297
+ tree.node.activeParam = PARAM_TOKENS.INCLUDES;
282
298
 
283
299
  token = '';
284
300
  continue;
@@ -290,12 +306,12 @@ module.exports = class QueryLexer {
290
306
  }
291
307
 
292
308
  tree.up();
293
- tree.node.activeParam = 'includes';
309
+ tree.node.activeParam = PARAM_TOKENS.INCLUDES;
294
310
 
295
311
  continue;
296
312
  }
297
313
 
298
- // & can mean the end of key=value pair,
314
+ // & can mean the end of key=value pair in root and sub query,
299
315
  // or the end of subincludes:
300
316
  if (char === '&') {
301
317
  debug('char', char, { token, node: tree.node });
@@ -307,7 +323,7 @@ module.exports = class QueryLexer {
307
323
  }
308
324
 
309
325
  // If end of key=value pair:
310
- if (!!tree.node.activeParam && tree.node.activeParam !== 'includes') {
326
+ if (!!tree.node.activeParam && tree.node.activeParam !== PARAM_TOKENS.INCLUDES) {
311
327
  // Set value.
312
328
  this.setNodeParam(tree.node, token, value);
313
329
  // Reset:
@@ -316,13 +332,19 @@ module.exports = class QueryLexer {
316
332
  value = [];
317
333
  continue;
318
334
  }
319
- else if (tree.node.activeParam === 'includes') {
320
- // If include of new model:
335
+ else if (tree.node.activeParam === PARAM_TOKENS.INCLUDES) {
336
+ // If token has some chars,
337
+ // then it's include of a new model:
321
338
  if (token.length > 0) {
322
339
  const model = token;
323
340
  // Just include, no use.
324
341
  tree.include(model);
325
342
  }
343
+ // If token is empty,
344
+ // it's most possibly a subquery
345
+ else {
346
+ continue;
347
+ }
326
348
 
327
349
  // Then jump to root.
328
350
  tree.upToRoot();
@@ -382,7 +404,7 @@ module.exports = class QueryLexer {
382
404
  if (char === '=') {
383
405
  const param = this.parseParamFromToken(token);
384
406
 
385
- if (isSubQuery === true && param === 'includes') {
407
+ if (isSubQuery === true && param === PARAM_TOKENS.INCLUDES) {
386
408
  const err = new TypeError(`'include' is forbidden inside subquery (position ${ i }). Use: 'model.submodel' or 'model.submodel1+submodel2'.`);
387
409
  throw err;
388
410
  }
@@ -428,23 +450,29 @@ module.exports = class QueryLexer {
428
450
  switch(token) {
429
451
  case 'limit':
430
452
  case 'l':
431
- return 'limit';
453
+ return PARAM_TOKENS.LIMIT;
454
+
432
455
  case 'skip':
433
456
  case 's':
434
457
  case 'offset':
435
- return 'skip';
458
+ return PARAM_TOKENS.SKIP;
459
+
436
460
  case 'order':
437
461
  case 'o':
438
- return 'order';
462
+ return PARAM_TOKENS.ORDER;
463
+
439
464
  case 'order_by':
440
465
  case 'o_by':
441
- return 'order_by';
466
+ return PARAM_TOKENS.ORDER_BY;
467
+
442
468
  case 'fields':
443
469
  case 'f':
444
- return 'fields';
470
+ return PARAM_TOKENS.FIELDS;
471
+
445
472
  case 'includes':
446
473
  case 'in':
447
- return 'includes';
474
+ return PARAM_TOKENS.INCLUDES;
475
+
448
476
  default:
449
477
  return token;
450
478
  }
@@ -453,34 +481,37 @@ module.exports = class QueryLexer {
453
481
  setNodeParam(treeNode, token, value) {
454
482
  const param = treeNode.activeParam;
455
483
 
456
- debug(`set param ${ param }`, { token, value });
484
+ debug(`set param`, { param, token, value });
457
485
 
458
486
  switch(param) {
459
- case 'limit':
487
+ case PARAM_TOKENS.LIMIT:
460
488
  treeNode.limit = parseInt(token);
461
489
  break;
462
- case 'skip':
463
- case 'offset':
490
+
491
+ case PARAM_TOKENS.SKIP:
464
492
  treeNode.skip = parseInt(token);
465
493
  break;
466
- case 'order':
494
+
495
+ case PARAM_TOKENS.ORDER:
467
496
  treeNode.order = token;
468
497
  break;
469
- case 'order_by':
498
+
499
+ case PARAM_TOKENS.ORDER_BY:
470
500
  treeNode.order_by = token;
471
501
  break;
472
- case 'fields':
473
- if (token)
474
- value.push(token);
502
+
503
+ case PARAM_TOKENS.FIELDS:
504
+ if (token) value.push(token);
475
505
  treeNode.fields = value;
476
506
  break;
477
- case 'includes':
507
+
508
+ case PARAM_TOKENS.INCLUDES:
478
509
  const node = new ModelsTreeNode(token);
479
510
  treeNode.include(node);
480
511
  break;
512
+
481
513
  default:
482
- if (token)
483
- value.push(token);
514
+ if (token) value.push(token);
484
515
  treeNode.addWhere({ [param]: value });
485
516
  break;
486
517
  }
@@ -102,49 +102,57 @@ function traverse(queryNode, filter=null, model=null) {
102
102
  // Functions:
103
103
  for (const fnParams of functions) {
104
104
 
105
- // If COUNT() is requested:
106
- if (fnParams.fn === 'count') {
107
- const countParams = fnParams.args;
108
-
109
- const [ countTarget ] = countParams;
110
- // Count can be requested for this model,
111
- // or for any of the available uncludes.
112
- const isForRootModel = countTarget === rootModelName.plural.toLowerCase();
113
-
114
- // Compile request:
115
- // Example:
116
- // `(SELECT COUNT(*) FROM comments WHERE comments.morph_id=Morph.id)`
117
-
118
- // Params for attribute:
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 (
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
- const err = new NodesterQueryError(`Count for '${ countTarget }' is not available.`);
133
- Error.captureStackTrace(err, traverse);
134
- throw err;
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
- const {
138
- foreignKey,
139
- sourceKey
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
- newQuery.attributes.push(
146
- [sequelize.literal(rawSQL), countAttribute]
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" override or set any query in clauses:
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 ( ['rand', 'random'].indexOf(order.order) > -1) {
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
- function _traverseIncludes(includes, model, filter, resultQuery) {
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 = model.associations[includeName];
321
+ const association = rootModel.associations[includeName];
300
322
 
301
323
  // If no such association:
302
324
  if (!association) {
@@ -31,7 +31,9 @@ module.exports = class MiddlewaresStack {
31
31
 
32
32
  // Indicates whether we can add more middlewares or no.
33
33
  this._isLocked = false;
34
- this.finalhandlerEnabled = !!opts.finalhandlerEnabled;
34
+
35
+ // TODO: disable/enable it in the process().
36
+ this._finalhandlerEnabled = !!opts.finalhandlerEnabled;
35
37
 
36
38
 
37
39
  const env = process.env.NODE_ENV || 'development';
@@ -139,7 +141,7 @@ module.exports = class MiddlewaresStack {
139
141
  * @api public
140
142
  */
141
143
  lock() {
142
- if (this.finalhandlerEnabled) {
144
+ if (this._finalhandlerEnabled) {
143
145
  // Add final handler to the stack.
144
146
  this.add((req, res)=>this.finalhandler(req, res)());
145
147
  }
@@ -159,7 +161,7 @@ module.exports = class MiddlewaresStack {
159
161
  unlock() {
160
162
  this._isLocked = false;
161
163
 
162
- if (this.finalhandlerEnabled) {
164
+ if (this._finalhandlerEnabled) {
163
165
  this._middlewares.pop();
164
166
  }
165
167
 
@@ -185,7 +187,15 @@ module.exports = class MiddlewaresStack {
185
187
  return next.call(null, req, res, next, ...args);
186
188
  }
187
189
  else if (!!fn) {
188
- return await fn.call(null, req, res, _next, ...args);
190
+ // Check for a middleware (fn) type (async or sync):
191
+ // 👇 Why it's important
192
+ // https://stackoverflow.com/questions/60330963/why-an-async-function-takes-more-time-to-execute-than-a-sync-one
193
+ if (fn.constructor.name === 'AsyncFunction') {
194
+ return await fn.call(null, req, res, _next, ...args);
195
+ }
196
+ else {
197
+ return fn.call(null, req, res, _next, ...args);
198
+ }
189
199
  }
190
200
  }
191
201
  catch(error) {
@@ -22,10 +22,10 @@ function _toAST_ModelsTreeNode(node, spacing=0) {
22
22
 
23
23
  ast += `${ spaces }model: ${ node.model }\n\n`;
24
24
 
25
- ast += `${ spaces }fields: [\n${ node.fields.map(f => ` • ${ f },\n`) }`;
25
+ ast += `${ spaces }fields (${ node.fields.length }): [\n${ node.fields.map(f => ` • ${ f },\n`) }`;
26
26
  ast += `${ spaces }]\n\n`;
27
27
 
28
- ast += `${ spaces }functions: [\n${ node.functions.map(f => ` • ${ f },\n`) }`;
28
+ ast += `${ spaces }functions (${ node.functions.length }): [\n${ node.functions.map(f => ` • ${ f },\n`) }`;
29
29
  ast += `${ spaces }]\n\n`;
30
30
 
31
31
  ast += `${ spaces }where: ${ JSON.stringify(node.where) }\n\n`;
@@ -34,7 +34,7 @@ function _toAST_ModelsTreeNode(node, spacing=0) {
34
34
  c => ast += `${ spaces }${ c }: ${ node[c] }\n\n`
35
35
  );
36
36
 
37
- ast += `${ spaces }includes: [\n`
37
+ ast += `${ spaces }includes (${ node.includes.length }): [\n`
38
38
  node.includes.map(n => ast += _toAST_ModelsTreeNode(n, spacing + 2));
39
39
  ast += `${ spaces }]\n`;
40
40
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodester",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "A versatile REST framework for Node.js",
5
5
  "exports": {
6
6
  ".": "./lib/application/index.js",
package/tests/nql.test.js CHANGED
@@ -10,285 +10,354 @@ const {
10
10
  const { ModelsTree } = require('../lib/middlewares/ql/sequelize/interpreter/ModelsTree');
11
11
  const QueryLexer = require('../lib/middlewares/ql/sequelize/interpreter/QueryLexer');
12
12
 
13
- describe('nodester Query Language', () => {
14
- const queryStrings = [
15
- // Simple where.
16
- 'id=10',
17
- // All possible params.
18
- 'id=10&position=4&limit=3&skip=10&order=desc&order_by=index&fields=id,content,position,created_at',
19
- // Simple includes.
20
- 'includes=comments&id=7',
21
- // Include with All possible params.
22
- 'includes=comments(id=10&position=4&limit=3&skip=10&order=desc&order_by=index&fields=id,content,position)',
23
-
24
- // Subinclude horizontal.
25
- 'includes=comments,users&id=1000',
26
- // Subinclude horizontal (more entries).
27
- 'includes=comments(order=desc),users,likes(order=rand),reposts&id=1000',
28
- // Subinclude horizontal (+ syntaxis).
29
- 'includes=comments(order=desc).users+likes(order=rand&order_by=position)&id=1000',
30
-
31
- // Subinclude vertical.
32
- 'includes=comments.users&id=1000',
33
- // Subinclude vertical (more entries).
34
- 'in=comments.users.avatars.sizes&position=200',
35
-
36
- // Complex includes.
37
- 'includes=comments.users.avatars(fields=id,content&order=rand)&id=7&limit=3',
38
-
39
- // Broken includes.
40
- 'includes=comments(order=rand)&id=7&limit=3&includes=users(fields=id,content)',
41
-
42
- // OR simple.
43
- 'or(index=2,position=5)',
44
- // OR shortened.
45
- '|(index=2,position=5)',
46
-
47
- // NOT inside include.
48
- 'includes=comments(id=not(7))',
49
-
50
- // Like simple.
51
- 'title=like(some_text)',
52
-
53
- // Subinclude and isolated Horizontal.
54
- 'in=comments.user,likes',
55
- ];
56
-
57
- it('query "Simple where"', () => {
58
- const lexer = new QueryLexer( queryStrings[0] );
59
- const result = lexer.query;
60
-
61
- const tree = new ModelsTree();
62
- tree.node.addWhere({ id: ['10'] });
63
- const expected = tree.root.toObject();
64
-
65
- expect(result).toMatchObject(expected);
66
- });
67
-
68
- test('query "All possible params"', () => {
69
- const lexer = new QueryLexer( queryStrings[1] );
70
- const result = lexer.query;
71
13
 
72
14
 
73
- const tree = new ModelsTree();
74
- tree.node.addWhere({ id: ['10'], position: ['4'] });
75
- tree.node.fields = [ 'id', 'content', 'position', 'created_at' ];
76
- tree.node.limit = 3;
77
- tree.node.skip = 10;
78
- tree.node.order = 'desc';
79
- tree.node.order_by = 'index';
80
- const expected = tree.root.toObject();
15
+ describe('nodester Query Language', () => {
16
+ describe('flat', () => {
17
+ const queryStrings = [
18
+ // Simple where.
19
+ 'id=10',
20
+ // All possible params.
21
+ 'id=10&position=4&limit=3&skip=10&order=desc&order_by=index&fields=id,content,position,created_at',
22
+ ];
23
+
24
+ it('Simple where', () => {
25
+ const lexer = new QueryLexer( queryStrings[0] );
26
+ const result = lexer.query;
27
+
28
+ const tree = new ModelsTree();
29
+ tree.node.addWhere({ id: ['10'] });
30
+ const expected = tree.root.toObject();
31
+
32
+ expect(result).toMatchObject(expected);
33
+ });
34
+
35
+ test('All possible params', () => {
36
+ const lexer = new QueryLexer( queryStrings[1] );
37
+ const result = lexer.query;
38
+
39
+
40
+ const tree = new ModelsTree();
41
+ tree.node.addWhere({ id: ['10'], position: ['4'] });
42
+ tree.node.fields = [ 'id', 'content', 'position', 'created_at' ];
43
+ tree.node.limit = 3;
44
+ tree.node.skip = 10;
45
+ tree.node.order = 'desc';
46
+ tree.node.order_by = 'index';
47
+ const expected = tree.root.toObject();
48
+
49
+ expect(result).toMatchObject(expected);
50
+ });
51
+ });
81
52
 
82
- expect(result).toMatchObject(expected);
53
+ describe('includes', () => {
54
+ const queryStrings = [
55
+ // Simple includes.
56
+ 'includes=comments&id=7',
57
+ // Include with All possible params.
58
+ 'includes=comments(id=10&position=4&limit=3&skip=10&order=desc&order_by=index&fields=id,content,position)',
59
+
60
+ // 2 horizontals
61
+ 'includes=comments,users&id=1000',
62
+
63
+ // Horizontals queried.
64
+ 'includes=comments(order=desc),users,likes(order=rand),reposts&id=1000',
65
+ // Horizontals queried №2.
66
+ 'in=reactions,comments(user_id=gte(4)&skip=10&limit=2).users,likes,reposts',
67
+
68
+ // Separated includes.
69
+ 'includes=comments(order=rand)&id=7&limit=3&includes=users(fields=id,content)',
70
+ ];
71
+
72
+ test('Simple includes', () => {
73
+ const lexer = new QueryLexer( queryStrings[0] );
74
+ const result = lexer.query;
75
+
76
+
77
+ const tree = new ModelsTree();
78
+ tree.node.addWhere({ id: ['7'] });
79
+ tree.include('comments');
80
+ const expected = tree.root.toObject();
81
+
82
+ expect(result).toMatchObject(expected);
83
+ });
84
+
85
+ test('Include with all possible params', () => {
86
+ const lexer = new QueryLexer( queryStrings[1] );
87
+ const result = lexer.query;
88
+
89
+ const tree = new ModelsTree();
90
+ tree.include('comments').use('comments');
91
+ tree.node.addWhere({ id: ['10'], position: ['4'] });
92
+ tree.node.fields = [ 'id', 'content', 'position' ];
93
+ tree.node.limit = 3;
94
+ tree.node.skip = 10;
95
+ tree.node.order = 'desc';
96
+ tree.node.order_by = 'index';
97
+ const expected = tree.root.toObject();
98
+
99
+ expect(result).toMatchObject(expected);
100
+ });
101
+
102
+ test('2 horizontals', () => {
103
+ const lexer = new QueryLexer( queryStrings[2] );
104
+ const result = lexer.query;
105
+
106
+
107
+ const tree = new ModelsTree();
108
+ tree.node.addWhere({ id: ['1000'] });
109
+ tree.include('comments');
110
+ tree.include('users');
111
+ const expected = tree.root.toObject();
112
+
113
+ expect(result).toMatchObject(expected);
114
+ });
115
+
116
+ test('Horizontals queried', () => {
117
+ const lexer = new QueryLexer( queryStrings[3] );
118
+ const result = lexer.query;
119
+
120
+
121
+ const tree = new ModelsTree();
122
+ tree.node.addWhere({ id: ['1000'] });
123
+ tree.include('comments').use('comments');
124
+ tree.node.order = 'desc';
125
+ tree.up();
126
+ tree.include('users');
127
+ tree.include('likes') && tree.use('likes');
128
+ tree.node.order = 'rand';
129
+ tree.up();
130
+ tree.include('reposts');
131
+ const expected = tree.root.toObject();
132
+
133
+ expect(result).toMatchObject(expected);
134
+ });
135
+
136
+ test('Horizontals queried №2', () => {
137
+ const lexer = new QueryLexer( queryStrings[4] );
138
+ const result = lexer.query;
139
+
140
+ const tree = new ModelsTree();
141
+ tree.include('reactions');
142
+
143
+ tree.include('comments').use('comments');
144
+ tree.node.addWhere({
145
+ user_id: {
146
+ gte: ['4']
147
+ }
148
+ });
149
+ tree.node.skip = 10;
150
+ tree.node.limit = 2;
151
+
152
+ tree.include('users');
153
+ tree.up();
154
+
155
+ tree.include('likes');
156
+ tree.include('reposts');
157
+
158
+ const expected = tree.root.toObject();
159
+
160
+ expect(result).toMatchObject(expected);
161
+ });
162
+
163
+ test('Separated includes"', () => {
164
+ const lexer = new QueryLexer( queryStrings[5] );
165
+ const result = lexer.query;
166
+
167
+ const tree = new ModelsTree();
168
+ tree.node.addWhere({ id: ['7'] });
169
+ tree.node.limit = 3;
170
+ tree.include('comments').use('comments');
171
+ tree.node.order = 'rand';
172
+ tree.up();
173
+ tree.include('users').use('users');
174
+ tree.node.fields = [ 'id', 'content' ];
175
+ const expected = tree.root.toObject();
176
+
177
+ expect(result).toMatchObject(expected);
178
+ });
83
179
  });
84
180
 
85
- test('query "Simple includes"', () => {
86
- const lexer = new QueryLexer( queryStrings[2] );
87
- const result = lexer.query;
181
+ describe('subincludes', () => {
182
+ const queryStrings = [
183
+ // Simple subinclude.
184
+ 'includes=comments.users',
88
185
 
186
+ // Deep subincludes.
187
+ 'in=posts.comments.users.avatars.sizes&position=200',
89
188
 
90
- const tree = new ModelsTree();
91
- tree.node.addWhere({ id: ['7'] });
92
- tree.include('comments');
93
- const expected = tree.root.toObject();
189
+ // Simple horizontal subinclude, "+" syntaxis.
190
+ 'includes=comments.users+likes',
94
191
 
95
- expect(result).toMatchObject(expected);
96
- });
192
+ // Subinclude query.
193
+ 'includes=comments.users(order=rand&order_by=position)',
97
194
 
98
- test('query "Include with all possible params"', () => {
99
- const lexer = new QueryLexer( queryStrings[3] );
100
- const result = lexer.query;
101
-
102
- const tree = new ModelsTree();
103
- tree.include('comments').use('comments');
104
- tree.node.addWhere({ id: ['10'], position: ['4'] });
105
- tree.node.fields = [ 'id', 'content', 'position' ];
106
- tree.node.limit = 3;
107
- tree.node.skip = 10;
108
- tree.node.order = 'desc';
109
- tree.node.order_by = 'index';
110
- const expected = tree.root.toObject();
111
-
112
- expect(result).toMatchObject(expected);
113
- });
195
+ // Complex subincludes query, "+" syntaxis.
196
+ 'includes=comments(order=desc).users+likes(order=rand&order_by=position)&id=1000',
197
+ ];
114
198
 
199
+ test('Simple subinclude', () => {
200
+ const lexer = new QueryLexer( queryStrings[0] );
201
+ const result = lexer.query;
115
202
 
116
- test('query "Subinclude horizontal"', () => {
117
- const lexer = new QueryLexer( queryStrings[4] );
118
- const result = lexer.query;
119
203
 
204
+ const tree = new ModelsTree();
205
+ tree.include('comments').use('comments');
206
+ tree.include('users');
207
+ const expected = tree.root.toObject();
120
208
 
121
- const tree = new ModelsTree();
122
- tree.node.addWhere({ id: ['1000'] });
123
- tree.include('comments');
124
- tree.include('users');
125
- const expected = tree.root.toObject();
209
+ expect(result).toMatchObject(expected);
210
+ });
126
211
 
127
- expect(result).toMatchObject(expected);
128
- });
212
+ test('Deep subincludes', () => {
213
+ const lexer = new QueryLexer( queryStrings[1] );
214
+ const result = lexer.query;
129
215
 
130
- test('query "Subinclude horizontal (complex)"', () => {
131
- const lexer = new QueryLexer( queryStrings[5] );
132
- const result = lexer.query;
133
216
 
217
+ const tree = new ModelsTree();
218
+ tree.include('posts').use('posts');
219
+ tree.include('comments').use('comments');
220
+ tree.include('users').use('users');
221
+ tree.include('avatars').use('avatars');
222
+ tree.include('sizes').use('sizes');
223
+ const expected = tree.root.toObject();
134
224
 
135
- const tree = new ModelsTree();
136
- tree.node.addWhere({ id: ['1000'] });
137
- tree.include('comments').use('comments');
138
- tree.node.order = 'desc';
139
- tree.up();
140
- tree.include('users');
141
- tree.include('likes') && tree.use('likes');
142
- tree.node.order = 'rand';
143
- tree.up();
144
- tree.include('reposts');
145
- const expected = tree.root.toObject();
225
+ expect(result).toMatchObject(expected);
226
+ });
146
227
 
147
- expect(result).toMatchObject(expected);
148
- });
228
+ test('Simple horizontal subinclude, "+" syntaxis"', () => {
229
+ const lexer = new QueryLexer( queryStrings[2] );
230
+ const result = lexer.query;
149
231
 
150
232
 
151
- test('query "Subinclude horizontal (+ syntaxis)"', () => {
152
- const lexer = new QueryLexer( queryStrings[6] );
153
- const result = lexer.query;
233
+ const tree = new ModelsTree();
234
+ tree.include('comments').use('comments');
235
+ tree.include('users');
236
+ tree.include('likes');
237
+ const expected = tree.root.toObject();
154
238
 
239
+ expect(result).toMatchObject(expected);
240
+ });
155
241
 
156
- const tree = new ModelsTree();
157
- tree.node.addWhere({ id: ['1000'] });
158
- tree.include('comments').use('comments');
159
- tree.node.order = 'desc';
160
- tree.include('users');
161
- tree.include('likes') && tree.use('likes');
162
- tree.node.order = 'rand';
163
- tree.node.order_by = 'position';
164
- tree.up();
165
- const expected = tree.root.toObject();
242
+ test('Subinclude query', () => {
243
+ const lexer = new QueryLexer( queryStrings[3] );
244
+ const result = lexer.query;
166
245
 
167
- expect(result).toMatchObject(expected);
168
- });
169
246
 
247
+ const tree = new ModelsTree();
248
+ tree.include('comments').use('comments');
249
+ tree.include('users').use('users');
250
+ tree.node.order = 'rand';
251
+ tree.node.order_by = 'position';
252
+ const expected = tree.root.toObject();
253
+
254
+ expect(result).toMatchObject(expected);
255
+ });
170
256
 
171
- test('query "Subinclude vertical"', () => {
172
- const lexer = new QueryLexer( queryStrings[7] );
173
- const result = lexer.query;
257
+ test('Complex subincludes query, "+" syntaxis', () => {
258
+ const lexer = new QueryLexer( queryStrings[4] );
259
+ const result = lexer.query;
174
260
 
175
261
 
176
- const tree = new ModelsTree();
177
- tree.node.addWhere({ id: ['1000'] });
178
- tree.include('comments').use('comments');
179
- tree.include('users');
180
- const expected = tree.root.toObject();
262
+ const tree = new ModelsTree();
263
+ tree.node.addWhere({ id: ['1000'] });
264
+ tree.include('comments').use('comments');
265
+ tree.node.order = 'desc';
266
+ tree.include('users');
267
+ tree.include('likes') && tree.use('likes');
268
+ tree.node.order = 'rand';
269
+ tree.node.order_by = 'position';
270
+ tree.up();
271
+ const expected = tree.root.toObject();
181
272
 
182
- expect(result).toMatchObject(expected);
273
+ expect(result).toMatchObject(expected);
274
+ });
183
275
  });
184
276
 
185
- test('query "Subinclude vertical (complex)"', () => {
186
- const lexer = new QueryLexer( queryStrings[8] );
187
- const result = lexer.query;
277
+ describe('operations', () => {
278
+ const queryStrings = [
279
+ // OR simple.
280
+ 'or(index=2,position=5)',
281
+ // OR short.
282
+ '|(index=2,position=5)',
188
283
 
284
+ // Not simple.
285
+ 'key=not(main)',
286
+ // Not short.
287
+ 'key=!(main)',
288
+ // NOT inside include.
289
+ 'includes=comments(id=not(7))',
189
290
 
190
- const tree = new ModelsTree();
191
- tree.node.addWhere({ position: ['200'] });
192
- tree.include('comments').use('comments');
193
- tree.include('users').use('users');
194
- tree.include('avatars').use('avatars');
195
- tree.include('sizes').use('sizes');
196
- const expected = tree.root.toObject();
291
+ // Like simple.
292
+ 'title=like(some_text)',
293
+ ];
197
294
 
198
- expect(result).toMatchObject(expected);
199
- });
295
+ test('"OR" simple', () => {
296
+ const lexer = new QueryLexer( queryStrings[0] );
297
+ const result = lexer.query;
200
298
 
201
- test('query "Complex includes"', () => {
202
- const lexer = new QueryLexer( queryStrings[9] );
203
- const result = lexer.query;
204
-
205
- const tree = new ModelsTree();
206
- tree.node.addWhere({ id: ['7'] });
207
- tree.node.limit = 3;
208
- tree.include('comments').use('comments');
209
- tree.include('users').use('users');
210
- tree.include('avatars').use('avatars');
211
- tree.node.fields = [ 'id', 'content' ];
212
- tree.node.order = 'rand';
213
- const expected = tree.root.toObject();
214
-
215
- expect(result).toMatchObject(expected);
216
- });
299
+ const tree = new ModelsTree();
300
+ tree.node.addWhere({ or: [ { index: ['2'] }, { position: ['5'] } ] });
301
+ const expected = tree.root.toObject();
217
302
 
218
- test('query "Broken includes"', () => {
219
- const lexer = new QueryLexer( queryStrings[10] );
220
- const result = lexer.query;
221
-
222
- const tree = new ModelsTree();
223
- tree.node.addWhere({ id: ['7'] });
224
- tree.node.limit = 3;
225
- tree.include('comments').use('comments');
226
- tree.node.order = 'rand';
227
- tree.up();
228
- tree.include('users').use('users');
229
- tree.node.fields = [ 'id', 'content' ];
230
- const expected = tree.root.toObject();
231
-
232
- expect(result).toMatchObject(expected);
233
- });
303
+ expect(result).toMatchObject(expected);
304
+ });
234
305
 
235
- test('Token "OR" simple', () => {
236
- const lexer = new QueryLexer( queryStrings[11] );
237
- const result = lexer.query;
306
+ test('"OR" short', () => {
307
+ const lexer = new QueryLexer( queryStrings[1] );
308
+ const result = lexer.query;
238
309
 
239
- const tree = new ModelsTree();
240
- tree.node.addWhere({ or: [ { index: ['2'] }, { position: ['5'] } ] });
241
- const expected = tree.root.toObject();
310
+ const tree = new ModelsTree();
311
+ tree.node.addWhere({ or: [ { index: ['2'] }, { position: ['5'] } ] });
312
+ const expected = tree.root.toObject();
242
313
 
243
- expect(result).toMatchObject(expected);
244
- });
314
+ expect(result).toMatchObject(expected);
315
+ });
245
316
 
246
- test('Token "OR" shortened', () => {
247
- const lexer = new QueryLexer( queryStrings[12] );
248
- const result = lexer.query;
317
+ test('"NOT" simple', () => {
318
+ const lexer = new QueryLexer( queryStrings[2] );
319
+ const result = lexer.query;
249
320
 
250
- const tree = new ModelsTree();
251
- tree.node.addWhere({ or: [ { index: ['2'] }, { position: ['5'] } ] });
252
- const expected = tree.root.toObject();
321
+ const tree = new ModelsTree();
322
+ tree.node.addWhere({ key: { not: ['main'] } });
323
+ const expected = tree.root.toObject();
253
324
 
254
- expect(result).toMatchObject(expected);
255
- });
325
+ expect(result).toMatchObject(expected);
326
+ });
256
327
 
257
- test('Token "NOT"', () => {
258
- const lexer = new QueryLexer( queryStrings[13] );
259
- const result = lexer.query;
328
+ test('"NOT" short', () => {
329
+ const lexer = new QueryLexer( queryStrings[3] );
330
+ const result = lexer.query;
260
331
 
261
- const tree = new ModelsTree();
262
- tree.include('comments').use('comments');
263
- tree.node.addWhere({ id: { not: ['7'] }});
264
- const expected = tree.root.toObject();
332
+ const tree = new ModelsTree();
333
+ tree.node.addWhere({ key: { not: ['main'] } });
334
+ const expected = tree.root.toObject();
265
335
 
266
- expect(result).toMatchObject(expected);
267
- });
336
+ expect(result).toMatchObject(expected);
337
+ });
268
338
 
269
- test('Token "Like" simple', () => {
270
- const lexer = new QueryLexer( queryStrings[14] );
271
- const result = lexer.query;
339
+ test('"NOT" inside includes', () => {
340
+ const lexer = new QueryLexer( queryStrings[4] );
341
+ const result = lexer.query;
272
342
 
273
- const tree = new ModelsTree();
274
- tree.node.addWhere({ title: { like: ['some_text'] }});
275
- const expected = tree.root.toObject();
343
+ const tree = new ModelsTree();
344
+ tree.include('comments').use('comments');
345
+ tree.node.addWhere({ id: { not: ['7'] }});
346
+ const expected = tree.root.toObject();
276
347
 
277
- expect(result).toMatchObject(expected);
278
- });
348
+ expect(result).toMatchObject(expected);
349
+ });
279
350
 
280
- it('query "Subinclude and isolated Horizontal"', () => {
281
- const lexer = new QueryLexer( queryStrings[15] );
282
- result = lexer.query;
351
+ test('"Like" simple', () => {
352
+ const lexer = new QueryLexer( queryStrings[5] );
353
+ const result = lexer.query;
283
354
 
284
- const tree = new ModelsTree();
285
- tree.include('comments').use('comments');
286
- tree.include('user');
287
- tree.up();
288
- tree.include('likes');
289
- const expected = tree.root.toObject();
355
+ const tree = new ModelsTree();
356
+ tree.node.addWhere({ title: { like: ['some_text'] }});
357
+ const expected = tree.root.toObject();
290
358
 
291
- expect(result).toMatchObject(expected);
359
+ expect(result).toMatchObject(expected);
360
+ });
292
361
  });
293
362
 
294
363
  });
File without changes