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 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
@@ -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 modelFields = Object.keys(model.tableAttributes);
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 isField = modelFields.indexOf(key) > -1;
41
+ const isAttribute = modelAttributes.indexOf(key) > -1;
42
42
 
43
- if ((!isField || filter.fields.indexOf(key) === -1) && !isInclude) {
44
- const err = new Error(`Field '${ key }' is not available.`);
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 (isField) {
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 field '${ key }'.`);
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.fields = [];
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
- fields: this.fields,
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
- FIELDS: Symbol('fields'),
16
- INCLUDES: Symbol('includes'),
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
- OR_MARK: '|',
33
+ OR_SHORT: '|',
30
34
  XOR: 'xor',
35
+
31
36
  NOT: 'not',
32
- NOT_MARK: '!',
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 'fields=id,text').
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 'o_by':
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);
@@ -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 fieldsAvailable = Object.keys(_model.tableAttributes);
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
- // Fields:
56
+ // Attribute:
56
57
  //
57
58
  // If Filter is not set,
58
- // use every available field:
59
+ // use every available attribute:
59
60
  if (filter === null) {
60
- for (let field of fieldsAvailable) {
61
- // If no query filter or field is requested:
62
- if (fields.length === 0 || fields.indexOf(field) > -1) {
63
- newQuery.attributes.push(field);
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 fields were set,
71
+ // If no query attributes were set,
71
72
  // use the ones from Filter,
72
- // If query fields were set,
73
+ // If query attributes were set,
73
74
  // put them through Filter:
74
- for (let field of filter.fields) {
75
- if (fieldsAvailable.indexOf(field) === -1) {
76
- const err = new NodesterQueryError(`Field '${ field }' is not present in model.`);
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 field is not in available set:
82
- // if (filter.fields.indexOf(field) === -1) {
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 field is requested:
87
- if (fields.length === 0 || fields.indexOf(field) > -1) {
88
- newQuery.attributes.push(field);
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 field is mandatory:
95
+ // At least 1 attribute is mandatory:
95
96
  if (newQuery.attributes.length === 0) {
96
- const err = new NodesterQueryError(`No fields were selected.`);
97
+ const err = new NodesterQueryError(`No attributes were selected.`);
97
98
  Error.captureStackTrace(err, traverse);
98
99
  throw err;
99
100
  }
100
- // Fields\
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
 
@@ -18,12 +18,12 @@ module.exports = class NodesterFilter {
18
18
  *
19
19
  * @param {Model} model
20
20
  * @param {Object} options
21
- * @param {Array} options.fields
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.fields
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._fields = [];
40
+ this._attributes = [];
41
41
  this._clauses = [];
42
42
  this._includes = {};
43
43
 
44
44
  this._bounds = {
45
- fields: {},
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._fields = Object.keys(this.model.tableAttributes);
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
- fields,
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 fields are array:
79
- if (Array.isArray(fields)) {
80
- this._fields = fields;
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 fields() {
128
- return this._fields;
127
+ get attributes() {
128
+ return this._attributes;
129
129
  }
130
130
 
131
131
  get clauses() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodester",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "A versatile REST framework for Node.js",
5
5
  "exports": {
6
6
  ".": "./lib/application/index.js",
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&fields=id,content,position,created_at',
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
- test('All possible params', () => {
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.fields = [ 'id', 'content', 'position', 'created_at' ];
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&fields=id,content,position)',
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(fields=id,content)',
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.fields = [ 'id', 'content', 'position' ];
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.fields = [ 'id', 'content' ];
187
+ tree.node.attributes = [ 'id', 'content' ];
175
188
  const expected = tree.root.toObject();
176
189
 
177
190
  expect(result).toMatchObject(expected);
File without changes