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
|
|
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
|
-
|
|
85
|
-
[
|
|
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
|
|
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
|
-
|
|
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 [
|
|
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 [
|
|
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
|
-
|
|
389
|
-
|
|
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 ${
|
|
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 ${
|
|
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 [
|
|
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([
|
|
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 '${
|
|
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 '${
|
|
687
|
+
const err = new NodesterQueryError(`Missing '${char}' at position ${index}`);
|
|
683
688
|
return err;
|
|
684
689
|
}
|