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 CHANGED
@@ -3,9 +3,13 @@
3
3
  [![NPM version](https://img.shields.io/npm/v/nodester)](https://www.npmjs.com/package/nodester)
4
4
  [![License](https://img.shields.io/npm/l/nodester)](https://www.npmjs.com/package/nodester)
5
5
 
6
- > **nodester** is a modern and versatile Node.js framework designed to streamline the development of robust and scalable web applications.
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/Queries.md), an extension of standard REST API syntax, it lets you craft complex queries with hierarchical associations.
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 - Nodester Query Language (NQL)
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/Queries.md)
60
+ [NQL documentaion ➡️](docs/nql/Introduction.md)
57
61
 
58
62
 
59
63
  ### Database
@@ -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 modelFields = Object.keys(model.tableAttributes);
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 isField = modelFields.indexOf(key) > -1;
41
+ const isAttribute = modelAttributes.indexOf(key) > -1;
42
42
 
43
- if ((!isField || filter.fields.indexOf(key) === -1) && !isInclude) {
44
- const err = new Error(`Field '${ key }' is not available.`);
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 (isField) {
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 field '${ key }'.`);
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
- statusCode = 401;
65
+ status = 401;
66
66
  break;
67
67
  }
68
68
  case 'NotFound': {
69
- statusCode = 404;
69
+ status = 404;
70
70
  break;
71
71
  }
72
72
  case 'ValidationError': {
73
- statusCode = 422;
73
+ status = 422;
74
74
  break;
75
75
  }
76
76
  case 'ConflictError': {
77
- statusCode = 409;
77
+ status = 409;
78
78
  break;
79
79
  }
80
80
  case 'SequelizeUniqueConstraintError': {
81
- statusCode = 409;
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.fields = [];
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
- fields: this.fields,
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
- FIELDS: Symbol('fields'),
16
- INCLUDES: Symbol('includes'),
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
- OR_MARK: '|',
33
+ OR_SHORT: '|',
30
34
  XOR: 'xor',
35
+
31
36
  NOT: 'not',
32
- NOT_MARK: '!',
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 'fields=id,text').
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 operation token.
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
- value.push({
218
- [tree.node.activeParam]: [token]
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 only mean start of 'in':
399
+ // [ can mean start of 'in'/'notIn',
400
+ // or 'notIn':
378
401
  if (char === '[') {
379
- tree.node.op = 'in';
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 only mean end if 'in':
418
+ // ] can mean end of 'in'/'notIn':
384
419
  if (char === ']') {
385
420
  // User missed first '[' :
386
- if (tree.node.op !== 'in') {
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 'o_by':
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 'or';
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 'not';
577
+ return OP_TOKENS.NOT;
578
+
528
579
  default:
529
580
  return opToken;
530
581
  }
@@ -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 fieldsAvailable = Object.keys(_model.tableAttributes);
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
- // Fields:
56
+ // Attribute:
56
57
  //
57
58
  // If Filter is not set,
58
- // use every available field:
59
+ // use every available attribute:
59
60
  if (filter === null) {
60
- for (let field of fieldsAvailable) {
61
- // If no query filter or field is requested:
62
- if (fields.length === 0 || fields.indexOf(field) > -1) {
63
- newQuery.attributes.push(field);
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 fields were set,
71
+ // If no query attributes were set,
71
72
  // use the ones from Filter,
72
- // If query fields were set,
73
+ // If query attributes were set,
73
74
  // put them through Filter:
74
- for (let field of filter.fields) {
75
- if (fieldsAvailable.indexOf(field) === -1) {
76
- const err = new NodesterQueryError(`Field '${ field }' is not present in model.`);
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 field is not in available set:
82
- // if (filter.fields.indexOf(field) === -1) {
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 field is requested:
87
- if (fields.length === 0 || fields.indexOf(field) > -1) {
88
- newQuery.attributes.push(field);
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 field is mandatory:
95
+ // At least 1 attribute is mandatory:
95
96
  if (newQuery.attributes.length === 0) {
96
- const err = new NodesterQueryError(`No fields were selected.`);
97
+ const err = new NodesterQueryError(`No attributes were selected.`);
97
98
  Error.captureStackTrace(err, traverse);
98
99
  throw err;
99
100
  }
100
- // Fields\
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
- // If operation is "in":
403
- if (opKey === 'in') {
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;
@@ -18,12 +18,12 @@ module.exports = class NodesterFilter {
18
18
  *
19
19
  * @param {Model} model
20
20
  * @param {Object} options
21
- * @param {Array} options.fields
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.fields
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._fields = [];
40
+ this._attributes = [];
41
41
  this._clauses = [];
42
42
  this._includes = {};
43
43
 
44
44
  this._bounds = {
45
- fields: {},
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._fields = Object.keys(this.model.tableAttributes);
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
- fields,
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 fields are array:
79
- if (Array.isArray(fields)) {
80
- this._fields = fields;
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 fields() {
128
- return this._fields;
127
+ get attributes() {
128
+ return this._attributes;
129
129
  }
130
130
 
131
131
  get clauses() {
@@ -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
- toAST_ModelsTreeNode: _toAST_ModelsTreeNode
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 _toAST_ModelsTreeNode(node, spacing=0) {
14
- let spaces = '';
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 }[TreeNode]\n`;
20
-
21
- spaces += ' ';
31
+ let ast = `${ spaces }TreeNode\n`;
32
+ ast += `${ spaces }┃\n`;
22
33
 
23
- ast += `${ spaces }model: ${ node.model }\n\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\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\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\n`;
45
+ ast += `${ spaces }where: ${ JSON.stringify(node.where) }\n`;
46
+ ast += `${ spaces }┃\n`;
32
47
 
33
- ['skip','limit','order','order_by'].map(
34
- c => ast += `${ spaces }${ c }: ${ node[c] }\n\n`
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 += _toAST_ModelsTreeNode(n, spacing + 2));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodester",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "A versatile REST framework for Node.js",
5
5
  "exports": {
6
6
  ".": "./lib/application/index.js",
package/tests/ast.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const { ModelsTree } = require('../lib/middlewares/ql/sequelize/interpreter/ModelsTree');
2
- const { toAST_ModelsTreeNode } = require('../lib/tools/nql.tool');
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(toAST_ModelsTreeNode(tree.root));
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&fields=id,content,position,created_at',
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
- test('All possible params', () => {
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.fields = [ 'id', 'content', 'position', 'created_at' ];
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&fields=id,content,position)',
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(fields=id,content)',
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.fields = [ 'id', 'content', 'position' ];
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.fields = [ 'id', 'content' ];
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('operations', () => {
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[2] );
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[3] );
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[4] );
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[5] );
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