nodester 0.5.1 → 0.6.0

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
@@ -5,11 +5,11 @@
5
5
 
6
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/nql/Introduction.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
  Building an application which allows users to build their own REST queries raises huge security concerns.
11
11
  That's why **nodester** was not developped as a middleware. It's a framework equipped with a set of technologies enabling you to fully customize the request-response flow down to the specific user and a database column.
12
- Check out [core concepts documentation ➡️](docs/CoreConcepts.md) for more info.
12
+ Check out [core concepts documentation ](docs/CoreConcepts.md) for more info.
13
13
 
14
14
 
15
15
  ## Installation
@@ -39,36 +39,55 @@ const db = require('#db');
39
39
  const app = new nodester();
40
40
  app.set.database(db);
41
41
 
42
+ // Optional beforeStart hook:
43
+ app.beforeStart(async ()=>{
44
+ // Do any asynchronous initializations
45
+ // before app.listen
46
+ // ...
47
+ });
48
+
49
+ // Start the http server:
42
50
  app.listen(8080, function() {
43
51
  console.log('listening on port', app.port);
44
52
  });
53
+
54
+ // Gracefully shut down:
55
+ process.once('SIGTERM', () => {
56
+ app.stop(() => {
57
+ const pid = process.pid;
58
+ console.info('Process', pid, 'terminated\n');
59
+ process.exit(0);
60
+ });
61
+ });
62
+
45
63
  ```
46
- [How to setup "db" ➡️](docs/App.md#with-database)
47
64
 
48
65
 
49
66
  ## Documentation
50
67
 
51
68
 
52
69
  ### Core concepts
53
- [Core concepts documentation ➡️](docs/CoreConcepts.md)
70
+ [Core concepts documentation ](docs/CoreConcepts.md)
54
71
 
55
72
 
56
- ### Queries & Querying - nodester Query Language (NQL)
73
+ ### Queries & querying - nodester Query Language (NQL)
57
74
  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.
58
75
 
59
76
  Read more about it in the documentation:
60
- [NQL documentaion ➡️](docs/nql/Introduction.md)
77
+ [NQL documentaion ](docs/nql/Introduction.md)
61
78
 
62
79
 
63
80
  ### Database
64
- Nodester is built upon a powerful [Sequelize](https://sequelize.org/).
81
+ Nodester is built upon a powerful [Sequelize](https://sequelize.org/).<br/>
65
82
  Supported drivers:
66
83
  - MySQL
67
84
  - PostgreSQL
68
85
 
86
+ [How to setup a database →](docs/App.md#with-database)
87
+
69
88
 
70
89
  ### Application
71
- [Application documentation ➡️](docs/App.md)
90
+ [Application documentation ](docs/App.md)
72
91
 
73
92
 
74
93
  ### Comments
@@ -0,0 +1,9 @@
1
+
2
+ class NodesterQueryError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'NodesterQueryError';
6
+ }
7
+ }
8
+
9
+ module.exports = NodesterQueryError;
@@ -8,18 +8,25 @@
8
8
  const Enum = require('nodester/enum');
9
9
 
10
10
  const { ModelsTree, ModelsTreeNode } = require('./ModelsTree');
11
+
12
+ const NodesterQueryError = require('./NodesterQueryError');
13
+
11
14
  const util = require('util');
12
15
  const debug = require('debug')('nodester:interpreter:QueryLexer');
13
16
 
14
17
  const PARAM_TOKENS = new Enum({
15
18
  ATTRIBUTES: Symbol('attributes'),
16
-
19
+
20
+ FUNCTIONS: Symbol('functions'),
21
+
22
+ INCLUDES: Symbol('includes'),
23
+
24
+ // Clauses:
17
25
  LIMIT: Symbol('limit'),
18
26
  ORDER: Symbol('order'),
19
27
  ORDER_BY: Symbol('order_by'),
20
28
  SKIP: Symbol('skip'),
21
-
22
- INCLUDES: Symbol('includes'),
29
+ // Clauses\
23
30
  });
24
31
 
25
32
  const OP_TOKENS = new Enum({
@@ -50,6 +57,7 @@ const OP_TOKENS = new Enum({
50
57
  });
51
58
 
52
59
  const FN_TOKENS = new Enum({
60
+ AVG: 'avg',
53
61
  COUNT: 'count',
54
62
  });
55
63
 
@@ -185,19 +193,14 @@ module.exports = class QueryLexer {
185
193
  throw err;
186
194
  }
187
195
 
188
- let fnParams = {};
196
+ const fnParams = {
197
+ fn: tree.node.fn
198
+ };
189
199
  switch (tree.node.fn) {
190
- case 'count':
191
- fnParams = {
192
- fn: 'count',
193
- args: [token]
194
- };
195
- break;
200
+ // ToDo: cases with multiple args.
201
+
196
202
  default:
197
- fnParams = {
198
- fn: [tree.node.fn],
199
- args: [token]
200
- };
203
+ fnParams.args = [token];
201
204
  break;
202
205
  }
203
206
 
@@ -205,7 +208,6 @@ module.exports = class QueryLexer {
205
208
 
206
209
  // Reset:
207
210
  tree.node.resetFN();
208
- tree.node.activeParam = PARAM_TOKENS.INCLUDES;
209
211
  token = '';
210
212
  value = [];
211
213
  continue;
@@ -251,6 +253,13 @@ module.exports = class QueryLexer {
251
253
  continue;
252
254
  }
253
255
 
256
+ // If new function:
257
+ if (tree.node.activeParam === PARAM_TOKENS.FUNCTIONS) {
258
+ // Prepare for new function:
259
+ tree.node.resetFN();
260
+ continue;
261
+ }
262
+
254
263
  // If param value:
255
264
  if (tree.node.activeParam !== PARAM_TOKENS.INCLUDES) {
256
265
  value.push(token);
@@ -354,7 +363,10 @@ module.exports = class QueryLexer {
354
363
  }
355
364
 
356
365
  // If end of key=value pair:
357
- if (!!tree.node.activeParam && tree.node.activeParam !== PARAM_TOKENS.INCLUDES) {
366
+ if (!!tree.node.activeParam
367
+ && tree.node.activeParam !== PARAM_TOKENS.FUNCTIONS
368
+ && tree.node.activeParam !== PARAM_TOKENS.INCLUDES
369
+ ) {
358
370
  // Set value.
359
371
  this.setNodeParam(tree.node, token, value);
360
372
  // Reset:
@@ -363,6 +375,20 @@ module.exports = class QueryLexer {
363
375
  value = [];
364
376
  continue;
365
377
  }
378
+ else if (tree.node.activeParam === PARAM_TOKENS.FUNCTIONS) {
379
+ // If token has some chars,
380
+ // then it's a syntactic error:
381
+ if (token.length > 0) {
382
+ const err = new NodesterQueryError(`unrecognized char at position ${ i }: Unknown token '${ token }'`);
383
+ throw err;
384
+ }
385
+
386
+ // Reset:
387
+ tree.node.resetActiveParam();
388
+ token = '';
389
+ value = [];
390
+ continue;
391
+ }
366
392
  else if (tree.node.activeParam === PARAM_TOKENS.INCLUDES) {
367
393
  // If token has some chars,
368
394
  // then it's include of a new model:
@@ -460,7 +486,7 @@ module.exports = class QueryLexer {
460
486
  const param = this.parseParamFromToken(token);
461
487
 
462
488
  if (isSubQuery === true && param === PARAM_TOKENS.INCLUDES) {
463
- const err = new TypeError(`'include' is forbidden inside subquery (position ${ i }). Use: 'model.submodel' or 'model.submodel1+submodel2'.`);
489
+ const err = new NodesterQueryError(`'include' is forbidden inside subquery (position ${ i }). Use: 'model.submodel' or 'model.submodel1+submodel2'.`);
464
490
  throw err;
465
491
  }
466
492
 
@@ -482,6 +508,12 @@ module.exports = class QueryLexer {
482
508
  throw err;
483
509
  }
484
510
 
511
+ // If any Function:
512
+ if (!!tree.node.fn) {
513
+ const err = MissingCharError(i+1, ')');
514
+ throw err;
515
+ }
516
+
485
517
  // If any OP at all:
486
518
  if (!!tree.node.op) {
487
519
  const err = MissingCharError(i+1, ')');
@@ -507,14 +539,18 @@ module.exports = class QueryLexer {
507
539
  case 'a':
508
540
  return PARAM_TOKENS.ATTRIBUTES;
509
541
 
542
+ case 'functions':
543
+ case 'fn':
544
+ return PARAM_TOKENS.FUNCTIONS;
545
+
546
+ case 'includes':
547
+ case 'in':
548
+ return PARAM_TOKENS.INCLUDES;
549
+
510
550
  case 'limit':
511
551
  case 'l':
512
552
  return PARAM_TOKENS.LIMIT;
513
553
 
514
- case 'skip':
515
- case 's':
516
- return PARAM_TOKENS.SKIP;
517
-
518
554
  case 'order':
519
555
  case 'o':
520
556
  return PARAM_TOKENS.ORDER;
@@ -523,9 +559,9 @@ module.exports = class QueryLexer {
523
559
  case 'oby':
524
560
  return PARAM_TOKENS.ORDER_BY;
525
561
 
526
- case 'includes':
527
- case 'in':
528
- return PARAM_TOKENS.INCLUDES;
562
+ case 'skip':
563
+ case 's':
564
+ return PARAM_TOKENS.SKIP;
529
565
 
530
566
  default:
531
567
  return token;
@@ -543,6 +579,16 @@ module.exports = class QueryLexer {
543
579
  treeNode.attributes = value;
544
580
  break;
545
581
 
582
+ case PARAM_TOKENS.FUNCTIONS:
583
+ treeNode.addFunction(value);
584
+ break;
585
+
586
+ case PARAM_TOKENS.INCLUDES:
587
+ const node = new ModelsTreeNode(token);
588
+ treeNode.include(node);
589
+ break;
590
+
591
+ // Clauses:
546
592
  case PARAM_TOKENS.LIMIT:
547
593
  treeNode.limit = parseInt(token);
548
594
  break;
@@ -558,11 +604,7 @@ module.exports = class QueryLexer {
558
604
  case PARAM_TOKENS.ORDER_BY:
559
605
  treeNode.order_by = token;
560
606
  break;
561
-
562
- case PARAM_TOKENS.INCLUDES:
563
- const node = new ModelsTreeNode(token);
564
- treeNode.include(node);
565
- break;
607
+ // Clauses\
566
608
 
567
609
  default:
568
610
  if (token) value.push(token);
@@ -597,11 +639,11 @@ module.exports = class QueryLexer {
597
639
 
598
640
 
599
641
  function UnexpectedCharError(index, char) {
600
- const err = new TypeError(`Unexpected ${ char } at position ${ index }`);
642
+ const err = new NodesterQueryError(`Unexpected '${ char }' at position ${ index }`);
601
643
  return err;
602
644
  }
603
645
 
604
646
  function MissingCharError(index, char) {
605
- const err = new TypeError(`Missing ${ char } at position ${ index }`);
647
+ const err = new NodesterQueryError(`Missing '${ char }' at position ${ index }`);
606
648
  return err;
607
649
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodester",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "A versatile REST framework for Node.js",
5
5
  "directories": {
6
6
  "docs": "docs",
package/tests/nql.test.js CHANGED
@@ -575,4 +575,60 @@ describe('nodester Query Language', () => {
575
575
  expect(result).toMatchObject(expected);
576
576
  });
577
577
  });
578
+
579
+
580
+ describe('functions', () => {
581
+ const queryStrings = {
582
+ count_long: '?functions=count(comments)',
583
+ count_short: '?fn=count(comments)',
584
+
585
+ count_and_includes: '?fn=count(comments)&in=comments',
586
+ }
587
+
588
+ test('Count (full key name)', () => {
589
+ const lexer = new QueryLexer( queryStrings.count_long );
590
+ const result = lexer.query;
591
+
592
+
593
+ const tree = new ModelsTree();
594
+ tree.node.addFunction({
595
+ fn: 'count',
596
+ args: ['comments']
597
+ })
598
+ const expected = tree.root.toObject();
599
+
600
+ expect(result).toMatchObject(expected);
601
+ });
602
+
603
+ test('Count (short key name)', () => {
604
+ const lexer = new QueryLexer( queryStrings.count_long );
605
+ const result = lexer.query;
606
+
607
+
608
+ const tree = new ModelsTree();
609
+ tree.node.addFunction({
610
+ fn: 'count',
611
+ args: ['comments']
612
+ })
613
+ const expected = tree.root.toObject();
614
+
615
+ expect(result).toMatchObject(expected);
616
+ });
617
+
618
+ test('Count and includes', () => {
619
+ const lexer = new QueryLexer( queryStrings.count_and_includes );
620
+ const result = lexer.query;
621
+
622
+
623
+ const tree = new ModelsTree();
624
+ tree.node.addFunction({
625
+ fn: 'count',
626
+ args: ['comments']
627
+ })
628
+ tree.include('comments');
629
+ const expected = tree.root.toObject();
630
+
631
+ expect(result).toMatchObject(expected);
632
+ });
633
+ });
578
634
  });