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)
|
|
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
|
|
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
|
|
70
|
+
[Core concepts documentation →](docs/CoreConcepts.md)
|
|
54
71
|
|
|
55
72
|
|
|
56
|
-
### Queries &
|
|
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
|
|
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
|
|
90
|
+
[Application documentation →](docs/App.md)
|
|
72
91
|
|
|
73
92
|
|
|
74
93
|
### Comments
|
|
@@ -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
|
-
|
|
196
|
+
const fnParams = {
|
|
197
|
+
fn: tree.node.fn
|
|
198
|
+
};
|
|
189
199
|
switch (tree.node.fn) {
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
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
|
|
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 '
|
|
527
|
-
case '
|
|
528
|
-
return PARAM_TOKENS.
|
|
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
|
|
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
|
|
647
|
+
const err = new NodesterQueryError(`Missing '${ char }' at position ${ index }`);
|
|
606
648
|
return err;
|
|
607
649
|
}
|
package/package.json
CHANGED
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
|
});
|