nodester 0.2.5 → 0.2.6
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 +5 -1
- package/lib/body/extract.js +6 -6
- package/lib/middlewares/ql/sequelize/interpreter/ModelsTree.js +13 -2
- package/lib/middlewares/ql/sequelize/interpreter/QueryLexer.js +20 -16
- package/lib/query/traverse.js +29 -29
- package/lib/structures/Filter.js +11 -11
- package/package.json +1 -1
- package/tests/nql.test.js +20 -7
- /package/lib/constants/{Operations.js → Operators.js} +0 -0
package/Readme.md
CHANGED
|
@@ -5,7 +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
|
+
|
|
10
|
+
Building an application which allows users to build their own REST queries raises huge security concerns.
|
|
11
|
+
That's why nodester was not developped as a middleware. It's a framework equipped which set of technologies enabling you to fully customize the request-response flow down to the specific user to only give them access to the data you intended.
|
|
12
|
+
Check out [core concepts documentation ➡️](docs/CoreConcepts.md) for more info.
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
## Installation
|
package/lib/body/extract.js
CHANGED
|
@@ -25,7 +25,7 @@ module.exports = extract;
|
|
|
25
25
|
function extract(body, filter=null, model) {
|
|
26
26
|
|
|
27
27
|
const sequelize = model.sequelize;
|
|
28
|
-
const
|
|
28
|
+
const modelAttributes = Object.keys(model.tableAttributes);
|
|
29
29
|
const availableIncludes = Object.keys(model.associations);
|
|
30
30
|
|
|
31
31
|
const bodyEntries = Object.entries(body);
|
|
@@ -38,15 +38,15 @@ function extract(body, filter=null, model) {
|
|
|
38
38
|
|
|
39
39
|
for (const [key, value] of bodyEntries) {
|
|
40
40
|
const isInclude = availableIncludes.indexOf(key) > -1;
|
|
41
|
-
const
|
|
41
|
+
const isAttribute = modelAttributes.indexOf(key) > -1;
|
|
42
42
|
|
|
43
|
-
if ((!
|
|
44
|
-
const err = new Error(`
|
|
43
|
+
if ((!isAttribute || filter.attributes.indexOf(key) === -1) && !isInclude) {
|
|
44
|
+
const err = new Error(`Attribute '${ key }' is not available.`);
|
|
45
45
|
err.status = httpCodes.NOT_ACCEPTABLE;
|
|
46
46
|
throw err;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
if (
|
|
49
|
+
if (isAttribute) {
|
|
50
50
|
const column = model.rawAttributes[key];
|
|
51
51
|
const typeName = column.type.constructor.name;
|
|
52
52
|
// Optional validation.
|
|
@@ -80,7 +80,7 @@ function extract(body, filter=null, model) {
|
|
|
80
80
|
continue;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
const err = new Error(`Unknown
|
|
83
|
+
const err = new Error(`Unknown attribute '${ key }'.`);
|
|
84
84
|
err.status = httpCodes.NOT_ACCEPTABLE;
|
|
85
85
|
throw err;
|
|
86
86
|
}
|
|
@@ -17,7 +17,7 @@ class ModelsTreeNode {
|
|
|
17
17
|
this.fn = null;
|
|
18
18
|
|
|
19
19
|
// for override:
|
|
20
|
-
this.
|
|
20
|
+
this._attributes = [];
|
|
21
21
|
this._where = {};
|
|
22
22
|
this._functions = [];
|
|
23
23
|
this.skip = 0;
|
|
@@ -28,6 +28,11 @@ class ModelsTreeNode {
|
|
|
28
28
|
this.order_by = opts.order_by ?? 'id';
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Getters:
|
|
32
|
+
get attributes() {
|
|
33
|
+
return this._attributes;
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
get where() {
|
|
32
37
|
return this._where;
|
|
33
38
|
}
|
|
@@ -51,6 +56,12 @@ class ModelsTreeNode {
|
|
|
51
56
|
get hasIncludes() {
|
|
52
57
|
return this.includesCount > 0;
|
|
53
58
|
}
|
|
59
|
+
// Getters\
|
|
60
|
+
|
|
61
|
+
// Setters:
|
|
62
|
+
set attributes(array) {
|
|
63
|
+
this._attributes = array;
|
|
64
|
+
}
|
|
54
65
|
|
|
55
66
|
resetActiveParam() {
|
|
56
67
|
this.activeParam = null;
|
|
@@ -85,7 +96,7 @@ class ModelsTreeNode {
|
|
|
85
96
|
return {
|
|
86
97
|
model: this.model,
|
|
87
98
|
|
|
88
|
-
|
|
99
|
+
attributes: this.attributes,
|
|
89
100
|
functions: this.functions,
|
|
90
101
|
|
|
91
102
|
where: this.where,
|
|
@@ -12,24 +12,29 @@ const util = require('util');
|
|
|
12
12
|
const debug = require('debug')('nodester:interpreter:QueryLexer');
|
|
13
13
|
|
|
14
14
|
const PARAM_TOKENS = new Enum({
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
ATTRIBUTES: Symbol('attributes'),
|
|
16
|
+
|
|
17
17
|
LIMIT: Symbol('limit'),
|
|
18
18
|
ORDER: Symbol('order'),
|
|
19
19
|
ORDER_BY: Symbol('order_by'),
|
|
20
20
|
SKIP: Symbol('skip'),
|
|
21
|
+
|
|
22
|
+
INCLUDES: Symbol('includes'),
|
|
21
23
|
});
|
|
22
24
|
|
|
23
25
|
const OP_TOKENS = new Enum({
|
|
24
26
|
AND: 'and',
|
|
27
|
+
|
|
25
28
|
BETWEEN: 'between',
|
|
26
29
|
NOT_BETWEEN: 'notBetween',
|
|
27
30
|
BETWEEN_MARK: '~',
|
|
31
|
+
|
|
28
32
|
OR: 'or',
|
|
29
|
-
|
|
33
|
+
OR_SHORT: '|',
|
|
30
34
|
XOR: 'xor',
|
|
35
|
+
|
|
31
36
|
NOT: 'not',
|
|
32
|
-
|
|
37
|
+
NOT_SHORT: '!',
|
|
33
38
|
|
|
34
39
|
IN: 'in',
|
|
35
40
|
NOT_IN: 'notIn',
|
|
@@ -77,7 +82,7 @@ module.exports = class QueryLexer {
|
|
|
77
82
|
|
|
78
83
|
// Token is a String, accumulated char-by-char.
|
|
79
84
|
let token = '';
|
|
80
|
-
// Value of param ('id=10' OR '
|
|
85
|
+
// Value of param ('id=10' OR 'attributes=id,text').
|
|
81
86
|
let value = [];
|
|
82
87
|
// Model, that was active before a cursor went up in the tree.
|
|
83
88
|
let previousActive = null;
|
|
@@ -489,13 +494,16 @@ module.exports = class QueryLexer {
|
|
|
489
494
|
|
|
490
495
|
parseParamFromToken(token) {
|
|
491
496
|
switch(token) {
|
|
497
|
+
case 'attributes':
|
|
498
|
+
case 'a':
|
|
499
|
+
return PARAM_TOKENS.ATTRIBUTES;
|
|
500
|
+
|
|
492
501
|
case 'limit':
|
|
493
502
|
case 'l':
|
|
494
503
|
return PARAM_TOKENS.LIMIT;
|
|
495
504
|
|
|
496
505
|
case 'skip':
|
|
497
506
|
case 's':
|
|
498
|
-
case 'offset':
|
|
499
507
|
return PARAM_TOKENS.SKIP;
|
|
500
508
|
|
|
501
509
|
case 'order':
|
|
@@ -503,13 +511,9 @@ module.exports = class QueryLexer {
|
|
|
503
511
|
return PARAM_TOKENS.ORDER;
|
|
504
512
|
|
|
505
513
|
case 'order_by':
|
|
506
|
-
case '
|
|
514
|
+
case 'oby':
|
|
507
515
|
return PARAM_TOKENS.ORDER_BY;
|
|
508
516
|
|
|
509
|
-
case 'fields':
|
|
510
|
-
case 'f':
|
|
511
|
-
return PARAM_TOKENS.FIELDS;
|
|
512
|
-
|
|
513
517
|
case 'includes':
|
|
514
518
|
case 'in':
|
|
515
519
|
return PARAM_TOKENS.INCLUDES;
|
|
@@ -525,6 +529,11 @@ module.exports = class QueryLexer {
|
|
|
525
529
|
debug(`set param`, { param, token, value });
|
|
526
530
|
|
|
527
531
|
switch(param) {
|
|
532
|
+
case PARAM_TOKENS.ATTRIBUTES:
|
|
533
|
+
if (token) value.push(token);
|
|
534
|
+
treeNode.attributes = value;
|
|
535
|
+
break;
|
|
536
|
+
|
|
528
537
|
case PARAM_TOKENS.LIMIT:
|
|
529
538
|
treeNode.limit = parseInt(token);
|
|
530
539
|
break;
|
|
@@ -541,11 +550,6 @@ module.exports = class QueryLexer {
|
|
|
541
550
|
treeNode.order_by = token;
|
|
542
551
|
break;
|
|
543
552
|
|
|
544
|
-
case PARAM_TOKENS.FIELDS:
|
|
545
|
-
if (token) value.push(token);
|
|
546
|
-
treeNode.fields = value;
|
|
547
|
-
break;
|
|
548
|
-
|
|
549
553
|
case PARAM_TOKENS.INCLUDES:
|
|
550
554
|
const node = new ModelsTreeNode(token);
|
|
551
555
|
treeNode.include(node);
|
package/lib/query/traverse.js
CHANGED
|
@@ -35,7 +35,7 @@ function traverse(queryNode, filter=null, model=null) {
|
|
|
35
35
|
const rootModelName = _model.options.name;
|
|
36
36
|
const rootModelAssociations = _model.associations;
|
|
37
37
|
const { sequelize } = _model;
|
|
38
|
-
const
|
|
38
|
+
const attributesAvailable = Object.keys(_model.tableAttributes);
|
|
39
39
|
|
|
40
40
|
const newQuery = {
|
|
41
41
|
attributes: [],
|
|
@@ -44,60 +44,61 @@ function traverse(queryNode, filter=null, model=null) {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
const {
|
|
47
|
+
attributes,
|
|
48
|
+
clauses,
|
|
49
|
+
functions,
|
|
47
50
|
where,
|
|
51
|
+
|
|
48
52
|
includes,
|
|
49
|
-
fields,
|
|
50
|
-
functions,
|
|
51
|
-
clauses,
|
|
52
53
|
} = _disassembleQueryNode(queryNode);
|
|
53
54
|
|
|
54
55
|
|
|
55
|
-
//
|
|
56
|
+
// Attribute:
|
|
56
57
|
//
|
|
57
58
|
// If Filter is not set,
|
|
58
|
-
// use every available
|
|
59
|
+
// use every available attribute:
|
|
59
60
|
if (filter === null) {
|
|
60
|
-
for (let
|
|
61
|
-
// If no query filter or
|
|
62
|
-
if (
|
|
63
|
-
newQuery.attributes.push(
|
|
61
|
+
for (let attribute of attributesAvailable) {
|
|
62
|
+
// If no query filter or attribute is requested:
|
|
63
|
+
if (attributes.length === 0 || attributes.indexOf(attribute) > -1) {
|
|
64
|
+
newQuery.attributes.push(attribute);
|
|
64
65
|
continue;
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
// Filter is present:
|
|
69
70
|
else {
|
|
70
|
-
// If no query
|
|
71
|
+
// If no query attributes were set,
|
|
71
72
|
// use the ones from Filter,
|
|
72
|
-
// If query
|
|
73
|
+
// If query attributes were set,
|
|
73
74
|
// put them through Filter:
|
|
74
|
-
for (let
|
|
75
|
-
if (
|
|
76
|
-
const err = new NodesterQueryError(`Field '${
|
|
75
|
+
for (let attribute of filter.attributes) {
|
|
76
|
+
if (attributesAvailable.indexOf(attribute) === -1) {
|
|
77
|
+
const err = new NodesterQueryError(`Field '${ attribute }' is not present in model.`);
|
|
77
78
|
Error.captureStackTrace(err, traverse);
|
|
78
79
|
throw err;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
// If
|
|
82
|
-
// if (filter.
|
|
82
|
+
// If attribute is not in available set:
|
|
83
|
+
// if (filter.attributes.indexOf(attribute) === -1) {
|
|
83
84
|
// continue;
|
|
84
85
|
// }
|
|
85
86
|
|
|
86
|
-
// If no query filter or
|
|
87
|
-
if (
|
|
88
|
-
newQuery.attributes.push(
|
|
87
|
+
// If no query filter or attribute is requested:
|
|
88
|
+
if (attributes.length === 0 || attributes.indexOf(attribute) > -1) {
|
|
89
|
+
newQuery.attributes.push(attribute);
|
|
89
90
|
continue;
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
// At least 1
|
|
95
|
+
// At least 1 attribute is mandatory:
|
|
95
96
|
if (newQuery.attributes.length === 0) {
|
|
96
|
-
const err = new NodesterQueryError(`No
|
|
97
|
+
const err = new NodesterQueryError(`No attributes were selected.`);
|
|
97
98
|
Error.captureStackTrace(err, traverse);
|
|
98
99
|
throw err;
|
|
99
100
|
}
|
|
100
|
-
//
|
|
101
|
+
// Attribute\
|
|
101
102
|
|
|
102
103
|
// Functions:
|
|
103
104
|
for (const fnParams of functions) {
|
|
@@ -377,20 +378,19 @@ function _parseWhereEntry(attribute, value, whereHolder, staticAttributes) {
|
|
|
377
378
|
function _disassembleQueryNode(queryNode) {
|
|
378
379
|
// Disassemble current query node:
|
|
379
380
|
const {
|
|
381
|
+
attributes,
|
|
382
|
+
functions,
|
|
380
383
|
where,
|
|
381
384
|
includes,
|
|
382
|
-
fields,
|
|
383
|
-
functions,
|
|
384
385
|
...clauses
|
|
385
386
|
} = queryNode;
|
|
386
|
-
// delete queryNode.model;
|
|
387
387
|
|
|
388
388
|
return {
|
|
389
|
+
attributes: attributes ?? [],
|
|
390
|
+
clauses: clauses ?? [],
|
|
391
|
+
functions: functions ?? [],
|
|
389
392
|
where: where ?? {},
|
|
390
393
|
includes: includes ?? [],
|
|
391
|
-
fields: fields ?? [],
|
|
392
|
-
functions: functions ?? [],
|
|
393
|
-
clauses: clauses ?? []
|
|
394
394
|
};
|
|
395
395
|
}
|
|
396
396
|
|
package/lib/structures/Filter.js
CHANGED
|
@@ -18,12 +18,12 @@ module.exports = class NodesterFilter {
|
|
|
18
18
|
*
|
|
19
19
|
* @param {Model} model
|
|
20
20
|
* @param {Object} options
|
|
21
|
-
* @param {Array} options.
|
|
21
|
+
* @param {Array} options.attributes
|
|
22
22
|
* @param {Array} options.clauses
|
|
23
23
|
* @param {Object} options.includes
|
|
24
24
|
*
|
|
25
25
|
* @param {Object} options.bounds
|
|
26
|
-
* @param {Object} options.bounds.
|
|
26
|
+
* @param {Object} options.bounds.attributes
|
|
27
27
|
* @param {Object} options.bounds.clauses
|
|
28
28
|
*
|
|
29
29
|
* @param {Object} options.statics
|
|
@@ -37,12 +37,12 @@ module.exports = class NodesterFilter {
|
|
|
37
37
|
|
|
38
38
|
this._model = model;
|
|
39
39
|
|
|
40
|
-
this.
|
|
40
|
+
this._attributes = [];
|
|
41
41
|
this._clauses = [];
|
|
42
42
|
this._includes = {};
|
|
43
43
|
|
|
44
44
|
this._bounds = {
|
|
45
|
-
|
|
45
|
+
attributes: {},
|
|
46
46
|
clauses: {}
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -53,7 +53,7 @@ module.exports = class NodesterFilter {
|
|
|
53
53
|
|
|
54
54
|
// If model is present:
|
|
55
55
|
if (isModel(this.model)) {
|
|
56
|
-
this.
|
|
56
|
+
this._attributes = Object.keys(this.model.tableAttributes);
|
|
57
57
|
this._clauses = CLAUSES.asArray;
|
|
58
58
|
|
|
59
59
|
// ...and no 'bounds' and 'statics' are provided,
|
|
@@ -67,7 +67,7 @@ module.exports = class NodesterFilter {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const {
|
|
70
|
-
|
|
70
|
+
attributes,
|
|
71
71
|
clauses,
|
|
72
72
|
includes,
|
|
73
73
|
bounds,
|
|
@@ -75,9 +75,9 @@ module.exports = class NodesterFilter {
|
|
|
75
75
|
} = options;
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
// If
|
|
79
|
-
if (Array.isArray(
|
|
80
|
-
this.
|
|
78
|
+
// If attributes are array:
|
|
79
|
+
if (Array.isArray(attributes)) {
|
|
80
|
+
this._attributes = attributes;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
if (Array.isArray(clauses)) {
|
|
@@ -124,8 +124,8 @@ module.exports = class NodesterFilter {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
// Getters:
|
|
127
|
-
get
|
|
128
|
-
return this.
|
|
127
|
+
get attributes() {
|
|
128
|
+
return this._attributes;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
get clauses() {
|
package/package.json
CHANGED
package/tests/nql.test.js
CHANGED
|
@@ -17,8 +17,10 @@ describe('nodester Query Language', () => {
|
|
|
17
17
|
const queryStrings = [
|
|
18
18
|
// Simple where.
|
|
19
19
|
'id=10',
|
|
20
|
+
// Only certain attributes.
|
|
21
|
+
'a=id,text',
|
|
20
22
|
// All possible params.
|
|
21
|
-
'id=10&position=4&limit=3&skip=10&order=desc&order_by=index&
|
|
23
|
+
'id=10&position=4&limit=3&skip=10&order=desc&order_by=index&a=id,content,position,created_at',
|
|
22
24
|
];
|
|
23
25
|
|
|
24
26
|
it('Simple where', () => {
|
|
@@ -32,14 +34,25 @@ describe('nodester Query Language', () => {
|
|
|
32
34
|
expect(result).toMatchObject(expected);
|
|
33
35
|
});
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
it('Only certain attributes', () => {
|
|
36
38
|
const lexer = new QueryLexer( queryStrings[1] );
|
|
37
39
|
const result = lexer.query;
|
|
38
40
|
|
|
41
|
+
const tree = new ModelsTree();
|
|
42
|
+
tree.node.attributes = [ 'id', 'text' ];
|
|
43
|
+
const expected = tree.root.toObject();
|
|
44
|
+
|
|
45
|
+
expect(result).toMatchObject(expected);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('All possible params', () => {
|
|
49
|
+
const lexer = new QueryLexer( queryStrings[2] );
|
|
50
|
+
const result = lexer.query;
|
|
51
|
+
|
|
39
52
|
|
|
40
53
|
const tree = new ModelsTree();
|
|
41
54
|
tree.node.addWhere({ id: ['10'], position: ['4'] });
|
|
42
|
-
tree.node.
|
|
55
|
+
tree.node.attributes = [ 'id', 'content', 'position', 'created_at' ];
|
|
43
56
|
tree.node.limit = 3;
|
|
44
57
|
tree.node.skip = 10;
|
|
45
58
|
tree.node.order = 'desc';
|
|
@@ -55,7 +68,7 @@ describe('nodester Query Language', () => {
|
|
|
55
68
|
// Simple includes.
|
|
56
69
|
'includes=comments&id=7',
|
|
57
70
|
// Include with All possible params.
|
|
58
|
-
'includes=comments(id=10&position=4&limit=3&skip=10&order=desc&order_by=index&
|
|
71
|
+
'includes=comments(id=10&position=4&limit=3&skip=10&order=desc&order_by=index&a=id,content,position)',
|
|
59
72
|
|
|
60
73
|
// 2 horizontals
|
|
61
74
|
'includes=comments,users&id=1000',
|
|
@@ -66,7 +79,7 @@ describe('nodester Query Language', () => {
|
|
|
66
79
|
'in=reactions,comments(user_id=gte(4)&skip=10&limit=2).users,likes,reposts',
|
|
67
80
|
|
|
68
81
|
// Separated includes.
|
|
69
|
-
'includes=comments(order=rand)&id=7&limit=3&includes=users(
|
|
82
|
+
'includes=comments(order=rand)&id=7&limit=3&includes=users(a=id,content)',
|
|
70
83
|
];
|
|
71
84
|
|
|
72
85
|
test('Simple includes', () => {
|
|
@@ -89,7 +102,7 @@ describe('nodester Query Language', () => {
|
|
|
89
102
|
const tree = new ModelsTree();
|
|
90
103
|
tree.include('comments').use('comments');
|
|
91
104
|
tree.node.addWhere({ id: ['10'], position: ['4'] });
|
|
92
|
-
tree.node.
|
|
105
|
+
tree.node.attributes = [ 'id', 'content', 'position' ];
|
|
93
106
|
tree.node.limit = 3;
|
|
94
107
|
tree.node.skip = 10;
|
|
95
108
|
tree.node.order = 'desc';
|
|
@@ -171,7 +184,7 @@ describe('nodester Query Language', () => {
|
|
|
171
184
|
tree.node.order = 'rand';
|
|
172
185
|
tree.up();
|
|
173
186
|
tree.include('users').use('users');
|
|
174
|
-
tree.node.
|
|
187
|
+
tree.node.attributes = [ 'id', 'content' ];
|
|
175
188
|
const expected = tree.root.toObject();
|
|
176
189
|
|
|
177
190
|
expect(result).toMatchObject(expected);
|
|
File without changes
|