nodester 0.6.71 → 0.6.72

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
@@ -12,7 +12,24 @@
12
12
  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.
13
13
 
14
14
  Building an application which allows users to build their own REST queries raises huge security concerns.
15
- 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.
15
+ That's why **nodester** was not developed 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.
16
+
17
+ ## Quick Example
18
+
19
+ With NQL, a single endpoint can handle complex queries:
20
+
21
+ ```http
22
+ GET /api/v1/countries?includes=cities(limit=5&order_by=population&order=desc).areas&name=like(Bel)&fn=count(cities)
23
+ ```
24
+
25
+ This query:
26
+ - Filters countries by name containing "Bel"
27
+ - Includes cities (limited to 5, ordered by population)
28
+ - Includes areas for each city
29
+ - Counts total cities per country
30
+
31
+ All with proper security through [Filters →](docs/Filter.md) that control what users can query.
32
+
16
33
  Check out [core concepts documentation →](docs/CoreConcepts.md) for more info.
17
34
 
18
35
 
@@ -25,8 +42,34 @@ npm install -S nodester
25
42
  ```
26
43
 
27
44
 
45
+ ## Or kickstart your project with the boilerplate
46
+
47
+ ```shell
48
+ npx degit https://github.com/MarkKhramko/nodester/examples/boilerplate my-app
49
+
50
+ cd my-app
51
+
52
+ npm i
53
+
54
+ npm run bootstrap
55
+ ```
56
+
57
+ ## Features
58
+
59
+ - **🔍 Powerful Query Language (NQL)**: Extend REST with SQL-like querying capabilities
60
+ - **🔒 Security First**: Fine-grained control over what users can query through Filters
61
+ - **🌳 Hierarchical Associations**: Query nested relationships with ease
62
+ - **⚡ Built on Sequelize**: Leverage the power of Sequelize ORM
63
+ - **🎯 Flexible Architecture**: Controller → Facade → Model pattern for clean separation
64
+ - **🛡️ Request Validation**: Automatic validation and bounds checking
65
+ - **📊 Aggregate Functions**: Built-in support for count, avg, and more
66
+ - **🔧 Extensible**: Easy to extend and customize for your needs
67
+
28
68
  ## Table of Contents
29
69
 
70
+ - [Quick Example](#quick-example)
71
+ - [Features](#features)
72
+ - [Installation](#installation)
30
73
  - [Usage](#usage)
31
74
  - [Documentation](#documentation)
32
75
  - [Philosophy](#philosophy)
@@ -81,8 +124,13 @@ process.once('SIGTERM', () => {
81
124
  ### Queries & querying - nodester Query Language (NQL)
82
125
  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 powerful tool for expressive and efficient data querying.
83
126
 
84
- Read more about it in the documentation:
85
- [NQL documentaion →](docs/nql/Introduction.md)
127
+ **NQL Documentation:**
128
+ - [Introduction →](docs/nql/Introduction.md) - Get started with NQL
129
+ - [Syntax →](docs/nql/Syntax.md) - Understand query syntax and parsing
130
+ - [Parameters →](docs/nql/Parameters.md) - Complete parameter reference
131
+ - [Subqueries →](docs/nql/Subqueries.md) - Advanced nested queries
132
+ - [Operators →](docs/nql/Operators.md) - Filtering operators (like, in, gt, etc.)
133
+ - [Functions →](docs/nql/Functions.md) - Aggregate functions (count, avg)
86
134
 
87
135
 
88
136
  ### Database
@@ -109,7 +157,7 @@ The Philosophy of `nodester` is to provide a developer with a tool that can buil
109
157
 
110
158
  ### Goal
111
159
 
112
- The goal of `nodester` is to be a robust and flexible framework that makes development in iteratations easy, while laying the foundation for seamless scalability in the future.
160
+ The goal of `nodester` is to be a robust and flexible framework that makes development in iterations easy, while laying the foundation for seamless scalability in the future.
113
161
 
114
162
 
115
163
  ## License
@@ -33,9 +33,13 @@ const PARAM_TOKENS = new Enum({
33
33
  const OP_TOKENS = new Enum({
34
34
  AND: 'and',
35
35
 
36
+ // NOTE: BETWEEN tokens are defined but not implemented.
37
+ // They are reserved for future use.
36
38
  BETWEEN: 'between',
39
+ BETWEEN_SHORT: '~',
37
40
  NOT_BETWEEN: 'notBetween',
38
- BETWEEN_MARK: '~',
41
+ NOT_BETWEEN_SHORT: '!~',
42
+ // NOTE\
39
43
 
40
44
  OR: 'or',
41
45
  OR_SHORT: '|',
@@ -46,11 +50,11 @@ const OP_TOKENS = new Enum({
46
50
 
47
51
  IN: 'in',
48
52
  NOT_IN: 'notIn',
49
-
53
+
50
54
  LIKE: 'like',
51
55
  NOT_LIKE: 'notLike',
52
56
  NOT_LIKE_SHORT: '!like',
53
-
57
+
54
58
  GREATER: 'gt',
55
59
  GREATER_OR_EQUAL: 'gte',
56
60
  LOWER: 'lt',
@@ -72,12 +76,12 @@ const FN_TOKENS = new Enum({
72
76
  * @access public
73
77
  */
74
78
  module.exports = class QueryLexer {
75
- constructor(queryString='') {
79
+ constructor(queryString = '') {
76
80
  this.tree = new ModelsTree();
77
81
  this.queryString = queryString;
78
82
  }
79
83
 
80
- async parse(queryString=this.queryString, tree=this.tree) {
84
+ async parse(queryString = this.queryString, tree = this.tree) {
81
85
  if (typeof queryString !== 'string') {
82
86
  const err = new TypeError(`Invalid 'queryString'.`);
83
87
  return Promise.reject(err);
@@ -91,7 +95,7 @@ module.exports = class QueryLexer {
91
95
  return Promise.resolve(this.tree.root.toObject());
92
96
  }
93
97
 
94
- async parseIsolatedQuery(queryString='', startAt=0, tree) {
98
+ async parseIsolatedQuery(queryString = '', startAt = 0, tree) {
95
99
  const isSubQuery = tree.node.model !== 'root';
96
100
 
97
101
  // Token is a String, accumulated char-by-char.
@@ -101,7 +105,7 @@ module.exports = class QueryLexer {
101
105
  // Model, that was active before a cursor went up in the tree.
102
106
  let previousActive = null;
103
107
 
104
- for (let i=startAt; i < queryString.length; i++) {
108
+ for (let i = startAt; i < queryString.length; i++) {
105
109
  const char = queryString[i];
106
110
 
107
111
  // ( can mean:
@@ -118,7 +122,7 @@ module.exports = class QueryLexer {
118
122
  token = '';
119
123
  continue;
120
124
  }
121
-
125
+
122
126
  // If FN token:
123
127
  if (FN_TOKENS.asArray.indexOf(token) > -1) {
124
128
  // Set function token.
@@ -134,7 +138,7 @@ module.exports = class QueryLexer {
134
138
 
135
139
  // Process subquery:
136
140
  i++;
137
- const [ charsCount ] = await this.parseIsolatedQuery(queryString, i, tree);
141
+ const [charsCount] = await this.parseIsolatedQuery(queryString, i, tree);
138
142
  i += charsCount;
139
143
 
140
144
  previousActive = model;
@@ -234,7 +238,7 @@ module.exports = class QueryLexer {
234
238
  if (token.length > 0) {
235
239
  this.setNodeParam(tree.node, token, value);
236
240
  }
237
-
241
+
238
242
  // Reset:
239
243
  tree.node.resetActiveParam();
240
244
  tree.node.resetOP();
@@ -243,7 +247,7 @@ module.exports = class QueryLexer {
243
247
  tree.up();
244
248
  }
245
249
  const numberOfProcessedChars = i - startAt;
246
- return [ numberOfProcessedChars ];
250
+ return [numberOfProcessedChars];
247
251
  }
248
252
 
249
253
  // , can mean:
@@ -379,15 +383,15 @@ module.exports = class QueryLexer {
379
383
 
380
384
  // If any OP at all:
381
385
  if (!!tree.node.op) {
382
- const err = MissingCharError(i+1, ')');
386
+ const err = MissingCharError(i + 1, ')');
383
387
  return Promise.reject(err);
384
388
  }
385
389
 
386
390
  // If end of a key=value pair:
387
391
  if (!!tree.node.activeParam
388
- && tree.node.activeParam !== PARAM_TOKENS.FUNCTIONS
389
- && tree.node.activeParam !== PARAM_TOKENS.INCLUDES
390
- ) {
392
+ && tree.node.activeParam !== PARAM_TOKENS.FUNCTIONS
393
+ && tree.node.activeParam !== PARAM_TOKENS.INCLUDES
394
+ ) {
391
395
  if (token.length > 0) {
392
396
  // Set value.
393
397
  this.setNodeParam(tree.node, token, value);
@@ -403,7 +407,7 @@ module.exports = class QueryLexer {
403
407
  // If token has some chars,
404
408
  // then it's a syntactic error:
405
409
  if (token.length > 0) {
406
- const err = new NodesterQueryError(`unrecognized char at position ${ i }: Unknown token '${ token }'`);
410
+ const err = new NodesterQueryError(`unrecognized char at position ${i}: Unknown token '${token}'`);
407
411
  return Promise.reject(err);
408
412
  }
409
413
 
@@ -433,7 +437,7 @@ module.exports = class QueryLexer {
433
437
  // Reset:
434
438
  token = '';
435
439
  value = [];
436
- continue;
440
+ continue;
437
441
  }
438
442
 
439
443
  // If end of subquery:
@@ -454,7 +458,7 @@ module.exports = class QueryLexer {
454
458
  const err = UnexpectedCharError(i, char);
455
459
  return Promise.reject(err);
456
460
  }
457
-
461
+
458
462
  // [ can mean:
459
463
  // • start of 'in' / 'notIn'
460
464
  if (char === '[') {
@@ -505,13 +509,13 @@ module.exports = class QueryLexer {
505
509
  token = '';
506
510
  continue;
507
511
  }
508
-
512
+
509
513
  // = can only mean the end of a param name:
510
514
  if (char === '=') {
511
515
  const param = this.parseParamFromToken(token);
512
516
 
513
517
  if (isSubQuery === true && param === PARAM_TOKENS.INCLUDES) {
514
- const err = new NodesterQueryError(`'include' is forbidden inside subquery (position ${ i }). Use: 'model.submodel' or 'model.submodel1+submodel2'.`);
518
+ const err = new NodesterQueryError(`'include' is forbidden inside subquery (position ${i}). Use: 'model.submodel' or 'model.submodel1+submodel2'.`);
515
519
  return Promise.reject(err);
516
520
  }
517
521
 
@@ -522,45 +526,45 @@ module.exports = class QueryLexer {
522
526
 
523
527
  // Continue accumulating token.
524
528
  token += char;
525
-
529
+
526
530
  // If last char:
527
- if (i === queryString.length-1) {
531
+ if (i === queryString.length - 1) {
528
532
  debug('last char', { token, node: tree.node });
529
-
533
+
530
534
  // haven't up from 'in':
531
535
  if (tree.node.op === 'in') {
532
- const err = MissingCharError(i+1, ']');
536
+ const err = MissingCharError(i + 1, ']');
533
537
  return Promise.reject(err);
534
538
  }
535
539
 
536
540
  // If any Function:
537
541
  if (!!tree.node.fn) {
538
- const err = MissingCharError(i+1, ')');
542
+ const err = MissingCharError(i + 1, ')');
539
543
  return Promise.reject(err);
540
544
  }
541
545
 
542
546
  // If any OP at all:
543
547
  if (!!tree.node.op) {
544
- const err = MissingCharError(i+1, ')');
548
+ const err = MissingCharError(i + 1, ')');
545
549
  return Promise.reject(err);
546
550
  }
547
-
551
+
548
552
  this.setNodeParam(tree.node, token, value);
549
553
 
550
554
  // If end of subquery:
551
555
  if (isSubQuery === true) {
552
- const numberOfProcessedChars = i+1 - startAt;
553
- return [ numberOfProcessedChars ];
556
+ const numberOfProcessedChars = i + 1 - startAt;
557
+ return [numberOfProcessedChars];
554
558
  }
555
559
  }
556
560
  }
557
561
 
558
562
  // Must return it's portion of chars.
559
- return Promise.resolve([ queryString.length - startAt ]);
563
+ return Promise.resolve([queryString.length - startAt]);
560
564
  }
561
565
 
562
566
  parseParamFromToken(token) {
563
- switch(token) {
567
+ switch (token) {
564
568
  case 'attributes':
565
569
  case 'a':
566
570
  return PARAM_TOKENS.ATTRIBUTES;
@@ -570,6 +574,7 @@ module.exports = class QueryLexer {
570
574
  return PARAM_TOKENS.FUNCTIONS;
571
575
 
572
576
  case 'includes':
577
+ case 'include':
573
578
  case 'in':
574
579
  return PARAM_TOKENS.INCLUDES;
575
580
 
@@ -604,7 +609,7 @@ module.exports = class QueryLexer {
604
609
 
605
610
  debug(`set param`, { param, token, value });
606
611
 
607
- switch(param) {
612
+ switch (param) {
608
613
  case PARAM_TOKENS.ATTRIBUTES:
609
614
  if (token) value.push(token);
610
615
  treeNode.attributes = value;
@@ -649,7 +654,7 @@ module.exports = class QueryLexer {
649
654
  }
650
655
 
651
656
  parseOP(opToken) {
652
- switch(opToken) {
657
+ switch (opToken) {
653
658
  case '|':
654
659
  case 'or':
655
660
  return OP_TOKENS.OR;
@@ -674,11 +679,11 @@ module.exports = class QueryLexer {
674
679
 
675
680
 
676
681
  function UnexpectedCharError(index, char) {
677
- const err = new NodesterQueryError(`Unexpected '${ char }' at position ${ index }`);
682
+ const err = new NodesterQueryError(`Unexpected '${char}' at position ${index}`);
678
683
  return err;
679
684
  }
680
685
 
681
686
  function MissingCharError(index, char) {
682
- const err = new NodesterQueryError(`Missing '${ char }' at position ${ index }`);
687
+ const err = new NodesterQueryError(`Missing '${char}' at position ${index}`);
683
688
  return err;
684
689
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodester",
3
- "version": "0.6.71",
3
+ "version": "0.6.72",
4
4
  "description": "A versatile REST framework for Node.js",
5
5
  "directories": {
6
6
  "docs": "docs",