nodester 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md CHANGED
@@ -3,9 +3,9 @@
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
9
 
10
10
 
11
11
  ## Installation
@@ -49,11 +49,11 @@ app.listen(8080, function() {
49
49
  [Core concepts documentation ➡️](docs/CoreConcepts.md)
50
50
 
51
51
 
52
- ### Queries & Querying - Nodester Query Language (NQL)
52
+ ### Queries & Querying - nodester Query Language (NQL)
53
53
  The true strength of nodester lies in its query language. Serving as an extension of standard REST API syntax, it brings many aspects of SQL into REST requests, providing developers with a simple yet potent tool for expressive and efficient data querying.
54
54
 
55
55
  Read more about it in the documentation:
56
- [NQL documentaion ➡️](docs/Queries.md)
56
+ [NQL documentaion ➡️](docs/nql/Introduction.md)
57
57
 
58
58
 
59
59
  ### Database
@@ -62,29 +62,27 @@ function _createGenericResponse(res, options) {
62
62
 
63
63
  switch(error.name) {
64
64
  case 'Unauthorized': {
65
- 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
  });
@@ -30,8 +30,14 @@ const OP_TOKENS = new Enum({
30
30
  XOR: 'xor',
31
31
  NOT: 'not',
32
32
  NOT_MARK: '!',
33
+
34
+ IN: 'in',
33
35
  NOT_IN: 'notIn',
36
+
34
37
  LIKE: 'like',
38
+ NOT_LIKE: 'notLike',
39
+ NOT_LIKE_SHORT: '!like',
40
+
35
41
  GREATER: 'gt',
36
42
  GREATER_OR_EQUAL: 'gte',
37
43
  LOWER: 'lt',
@@ -86,7 +92,7 @@ module.exports = class QueryLexer {
86
92
 
87
93
  // If OP token:
88
94
  if (OP_TOKENS.asArray.indexOf(token) > -1) {
89
- // Set operation token.
95
+ // Set operator token.
90
96
  tree.node.op = this.parseOP(token);
91
97
  token = '';
92
98
  continue;
@@ -135,6 +141,7 @@ module.exports = class QueryLexer {
135
141
  switch (tree.node.op) {
136
142
  case OP_TOKENS.NOT:
137
143
  case OP_TOKENS.LIKE:
144
+ case OP_TOKENS.NOT_LIKE:
138
145
  case OP_TOKENS.GREATER:
139
146
  case OP_TOKENS.GREATER_OR_EQUAL:
140
147
  case OP_TOKENS.LOWER:
@@ -214,9 +221,19 @@ module.exports = class QueryLexer {
214
221
 
215
222
  // If OP token:
216
223
  if (!!tree.node.op) {
217
- value.push({
218
- [tree.node.activeParam]: [token]
219
- });
224
+ switch(tree.node.op) {
225
+ case OP_TOKENS.NOT_IN:
226
+ case OP_TOKENS.IN:
227
+ value.push(token);
228
+ break;
229
+ default:
230
+ value.push({
231
+ [tree.node.activeParam]: [token]
232
+ });
233
+ break;
234
+ }
235
+
236
+ // Reset.
220
237
  token = '';
221
238
  continue;
222
239
  }
@@ -374,27 +391,51 @@ module.exports = class QueryLexer {
374
391
  throw err;
375
392
  }
376
393
 
377
- // [ can only mean start of 'in':
394
+ // [ can mean start of 'in'/'notIn',
395
+ // or 'notIn':
378
396
  if (char === '[') {
379
- tree.node.op = 'in';
397
+ tree.node.op = OP_TOKENS.IN;
398
+ if (token.length > 0) {
399
+ if (token === '!' || token === 'not') {
400
+ tree.node.op = OP_TOKENS.NOT_IN;
401
+ }
402
+ else {
403
+ const err = UnexpectedCharError(i - token.length, token);
404
+ throw err;
405
+ }
406
+ }
407
+
408
+ // Reset:
409
+ token = '';
380
410
  continue;
381
411
  }
382
412
 
383
- // ] can only mean end if 'in':
413
+ // ] can mean end of 'in'/'notIn':
384
414
  if (char === ']') {
385
415
  // User missed first '[' :
386
- if (tree.node.op !== 'in') {
416
+ if (
417
+ tree.node.op !== OP_TOKENS.IN
418
+ &&
419
+ tree.node.op !== OP_TOKENS.NOT_IN
420
+ ) {
387
421
  const err = UnexpectedCharError(i, char);
388
422
  throw err;
389
423
  }
390
424
 
425
+ // Token is the last element in this array:
426
+ if (token.length > 0) {
427
+ value.push(token);
428
+ }
429
+
391
430
  tree.node.addWhere({
392
431
  [tree.node.activeParam]: {
393
432
  [tree.node.op]: value
394
433
  }
395
434
  });
435
+
396
436
  // Reset:
397
437
  tree.node.resetOP();
438
+ tree.node.resetActiveParam();
398
439
  value = [];
399
440
  token = '';
400
441
  continue;
@@ -521,10 +562,16 @@ module.exports = class QueryLexer {
521
562
  switch(opToken) {
522
563
  case '|':
523
564
  case 'or':
524
- return 'or';
565
+ return OP_TOKENS.OR;
566
+
567
+ case '!like':
568
+ case 'notLike':
569
+ return OP_TOKENS.NOT_LIKE;
570
+
525
571
  case 'not':
526
572
  case '!':
527
- return 'not';
573
+ return OP_TOKENS.NOT;
574
+
528
575
  default:
529
576
  return opToken;
530
577
  }
@@ -397,17 +397,10 @@ function _disassembleQueryNode(queryNode) {
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;
@@ -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.5",
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
@@ -274,22 +274,12 @@ describe('nodester Query Language', () => {
274
274
  });
275
275
  });
276
276
 
277
- describe('operations', () => {
277
+ describe('operators:or', () => {
278
278
  const queryStrings = [
279
279
  // OR simple.
280
280
  'or(index=2,position=5)',
281
281
  // OR short.
282
282
  '|(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
283
  ];
294
284
 
295
285
  test('"OR" simple', () => {
@@ -313,9 +303,20 @@ describe('nodester Query Language', () => {
313
303
 
314
304
  expect(result).toMatchObject(expected);
315
305
  });
306
+ });
307
+
308
+ describe('operators:not', () => {
309
+ const queryStrings = [
310
+ // Not simple.
311
+ 'key=not(main)',
312
+ // Not short.
313
+ 'key=!(main)',
314
+ // NOT inside include.
315
+ 'includes=comments(id=not(7))'
316
+ ];
316
317
 
317
318
  test('"NOT" simple', () => {
318
- const lexer = new QueryLexer( queryStrings[2] );
319
+ const lexer = new QueryLexer( queryStrings[0] );
319
320
  const result = lexer.query;
320
321
 
321
322
  const tree = new ModelsTree();
@@ -326,7 +327,7 @@ describe('nodester Query Language', () => {
326
327
  });
327
328
 
328
329
  test('"NOT" short', () => {
329
- const lexer = new QueryLexer( queryStrings[3] );
330
+ const lexer = new QueryLexer( queryStrings[1] );
330
331
  const result = lexer.query;
331
332
 
332
333
  const tree = new ModelsTree();
@@ -337,7 +338,7 @@ describe('nodester Query Language', () => {
337
338
  });
338
339
 
339
340
  test('"NOT" inside includes', () => {
340
- const lexer = new QueryLexer( queryStrings[4] );
341
+ const lexer = new QueryLexer( queryStrings[2] );
341
342
  const result = lexer.query;
342
343
 
343
344
  const tree = new ModelsTree();
@@ -348,8 +349,21 @@ describe('nodester Query Language', () => {
348
349
  expect(result).toMatchObject(expected);
349
350
  });
350
351
 
352
+ });
353
+
354
+ describe('operators:like', () => {
355
+ const queryStrings = [
356
+ // Like simple.
357
+ 'title=like(some_text)',
358
+
359
+ // Not like simple.
360
+ 'title=notLike(some_text)',
361
+ // Not like short.
362
+ 'title=!like(some_text)',
363
+ ];
364
+
351
365
  test('"Like" simple', () => {
352
- const lexer = new QueryLexer( queryStrings[5] );
366
+ const lexer = new QueryLexer( queryStrings[0] );
353
367
  const result = lexer.query;
354
368
 
355
369
  const tree = new ModelsTree();
@@ -358,6 +372,141 @@ describe('nodester Query Language', () => {
358
372
 
359
373
  expect(result).toMatchObject(expected);
360
374
  });
375
+
376
+ test('"NotLike" simple', () => {
377
+ const lexer = new QueryLexer( queryStrings[1] );
378
+ const result = lexer.query;
379
+
380
+ const tree = new ModelsTree();
381
+ tree.node.addWhere({ title: { notLike: ['some_text'] }});
382
+ const expected = tree.root.toObject();
383
+
384
+ expect(result).toMatchObject(expected);
385
+ });
386
+
387
+ test('"NotLike" short', () => {
388
+ const lexer = new QueryLexer( queryStrings[2] );
389
+ const result = lexer.query;
390
+
391
+ const tree = new ModelsTree();
392
+ tree.node.addWhere({ title: { notLike: ['some_text'] }});
393
+ const expected = tree.root.toObject();
394
+
395
+ expect(result).toMatchObject(expected);
396
+ });
397
+ });
398
+
399
+ describe('operators:in', () => {
400
+ const queryStrings = [
401
+ // IN simple.
402
+ 'status=[REVIEWED,ANSWERED]',
403
+
404
+ // IN and limit clause.
405
+ 'status=[REVIEWED,ANSWERED]&limit=3',
406
+ ];
407
+
408
+ test('"IN" simple', () => {
409
+ const lexer = new QueryLexer( queryStrings[0] );
410
+ const result = lexer.query;
411
+
412
+ const tree = new ModelsTree();
413
+ tree.node.addWhere({ status: { in: ['REVIEWED', 'ANSWERED'] }});
414
+ const expected = tree.root.toObject();
415
+
416
+ expect(result).toMatchObject(expected);
417
+ });
418
+
419
+ test('"IN" and "limit" clause', () => {
420
+ const lexer = new QueryLexer( queryStrings[1] );
421
+ const result = lexer.query;
422
+
423
+ const tree = new ModelsTree();
424
+ tree.node.limit = 3;
425
+ tree.node.addWhere({ status: { in: ['REVIEWED', 'ANSWERED'] }});
426
+ const expected = tree.root.toObject();
427
+
428
+ expect(result).toMatchObject(expected);
429
+ });
361
430
  });
362
431
 
432
+ describe('operators:inequality', () => {
433
+ const queryStrings = [
434
+ // Greater than.
435
+ 'created_at=gt(2022)',
436
+
437
+ // Greater than or equal to.
438
+ 'created_at=gte(2023-12-08)',
439
+
440
+ // Lower than.
441
+ 'index=lt(10)',
442
+
443
+ // Lower than or equal to.
444
+ 'index=lte(9)',
445
+
446
+ // Greater than in subinclude.
447
+ 'in=comments.likes(index=gt(60))'
448
+ ];
449
+
450
+ test('Greater than', () => {
451
+ const lexer = new QueryLexer( queryStrings[0] );
452
+ const result = lexer.query;
453
+
454
+
455
+ const tree = new ModelsTree();
456
+ tree.node.addWhere({ created_at: { gt: ['2022'] }});
457
+ const expected = tree.root.toObject();
458
+
459
+ expect(result).toMatchObject(expected);
460
+ });
461
+
462
+ test('Greater than or equal to', () => {
463
+ const lexer = new QueryLexer( queryStrings[1] );
464
+ const result = lexer.query;
465
+
466
+
467
+ const tree = new ModelsTree();
468
+ tree.node.addWhere({ created_at: { gte: ['2023-12-08'] }});
469
+ const expected = tree.root.toObject();
470
+
471
+ expect(result).toMatchObject(expected);
472
+ });
473
+
474
+ test('Lower than', () => {
475
+ const lexer = new QueryLexer( queryStrings[2] );
476
+ const result = lexer.query;
477
+
478
+
479
+ const tree = new ModelsTree();
480
+ tree.node.addWhere({ index: { lt: ['10'] }});
481
+ const expected = tree.root.toObject();
482
+
483
+ expect(result).toMatchObject(expected);
484
+ });
485
+
486
+ test('Lower than or equal to', () => {
487
+ const lexer = new QueryLexer( queryStrings[3] );
488
+ const result = lexer.query;
489
+
490
+
491
+ const tree = new ModelsTree();
492
+ tree.node.addWhere({ index: { lte: ['9'] }});
493
+ const expected = tree.root.toObject();
494
+
495
+ expect(result).toMatchObject(expected);
496
+ });
497
+
498
+ test('Greater than in subinclude', () => {
499
+ const lexer = new QueryLexer( queryStrings[4] );
500
+ const result = lexer.query;
501
+
502
+
503
+ const tree = new ModelsTree();
504
+ tree.include('comments').use('comments');
505
+ tree.include('likes').use('likes');
506
+ tree.node.addWhere({ index: { gt: ['60'] }});
507
+ const expected = tree.root.toObject();
508
+
509
+ expect(result).toMatchObject(expected);
510
+ });
511
+ });
363
512
  });