nodester 0.5.0 → 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
@@ -30,13 +30,14 @@ function extract(body, filter=null, model) {
30
30
 
31
31
  const bodyEntries = Object.entries(body);
32
32
 
33
+ // Unwrap statics:
33
34
  const { statics } = filter;
34
-
35
+ const staticAttributes = statics?.attributes ?? {};
35
36
 
36
37
  // Result object.
37
38
  const filteredBody = {};
38
39
 
39
- for (const [key, value] of bodyEntries) {
40
+ for (const [ key, value ] of bodyEntries) {
40
41
  const isInclude = availableIncludes.indexOf(key) > -1;
41
42
  const isAttribute = modelAttributes.indexOf(key) > -1;
42
43
 
@@ -47,6 +48,14 @@ function extract(body, filter=null, model) {
47
48
  }
48
49
 
49
50
  if (isAttribute) {
51
+ // If this attribute must have a static value,
52
+ // skip it.
53
+ // The static value will be set later:
54
+ if (typeof staticAttributes[key] !== 'undefined') {
55
+ continue;
56
+ }
57
+
58
+ // Sanitize value from the body:
50
59
  const column = model.rawAttributes[key];
51
60
  const typeName = column.type.constructor.name;
52
61
  // Optional validation.
@@ -99,6 +108,12 @@ function extract(body, filter=null, model) {
99
108
  err.status = httpCodes.NOT_ACCEPTABLE;
100
109
  throw err;
101
110
  }
111
+
112
+ // Set all static attributes:
113
+ const staticAttributeEntries = Object.entries(staticAttributes);
114
+ for (const [ key, value ] of staticAttributeEntries) {
115
+ filteredBody[key] = staticAttributes[key];
116
+ }
102
117
 
103
118
  return filteredBody;
104
119
  }
@@ -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
  }
@@ -64,7 +64,7 @@ function traverse(queryNode, filter=null, model=null, association=null) {
64
64
  where: {},
65
65
  include: []
66
66
  };
67
-
67
+
68
68
  const {
69
69
  attributes,
70
70
  clauses,
@@ -184,18 +184,19 @@ function traverse(queryNode, filter=null, model=null, association=null) {
184
184
  const order = {};
185
185
 
186
186
  const clausesEntries = Object.entries(clauses);
187
- for (let [clauseName, value] of clausesEntries) {
187
+ for (const [ clauseName, value ] of clausesEntries) {
188
188
 
189
189
  // If clause is not available:
190
190
  if (filter != null) {
191
- if (filter.clauses.indexOf(clauseName) === -1)
191
+ if (filter.clauses.indexOf(clauseName) === -1) {
192
192
  continue;
193
+ }
193
194
  }
194
195
 
195
196
  switch(clauseName) {
196
197
  case 'limit': {
197
198
  const _value = _setValueWithBounds(value, 'number', filter.bounds.clauses.limit);
198
-
199
+
199
200
  // Do not set if -1:
200
201
  if (_value === -1)
201
202
  continue;
@@ -226,7 +227,7 @@ function traverse(queryNode, filter=null, model=null, association=null) {
226
227
 
227
228
  order.by = value;
228
229
  continue;
229
-
230
+
230
231
  default:
231
232
  continue;
232
233
  }
@@ -243,7 +244,7 @@ function traverse(queryNode, filter=null, model=null, association=null) {
243
244
  case 'limit':
244
245
  newQuery.limit = staticClauseValue;
245
246
  continue;
246
-
247
+
247
248
  case 'skip':
248
249
  newQuery.offset = staticClauseValue;
249
250
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodester",
3
- "version": "0.5.0",
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
  });