nodester 0.6.0 → 0.6.1

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.
@@ -5,15 +5,10 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const {
9
- NODESTER_QUERY_ERROR,
10
- } = require('nodester/constants/ErrorCodes');
11
8
  const {
12
9
  NOT_ACCEPTABLE
13
10
  } = require('nodester/http/codes');
14
11
 
15
- const NodesterError = require('./NodesterError');
16
-
17
12
 
18
13
  /**
19
14
  * @class
@@ -22,11 +17,12 @@ const NodesterError = require('./NodesterError');
22
17
  *
23
18
  * @access public
24
19
  */
25
- module.exports = class NodesterQueryError extends NodesterError {
26
- constructor(message) {
20
+ module.exports = class NodesterQueryError extends Error {
21
+ constructor(message, status) {
27
22
  super(message);
28
23
 
29
- this.status = NOT_ACCEPTABLE;
24
+ this.name = this.constructor.name;
25
+ this.status = status ?? NOT_ACCEPTABLE;
30
26
 
31
27
  Error.captureStackTrace(this, this.constructor);
32
28
  }
@@ -17,21 +17,25 @@ class ModelsTreeNode {
17
17
  constructor(model, parent=null, opts={}) {
18
18
  this.model = model;
19
19
  this.parent = parent;
20
+
20
21
  this.activeParam = null;
21
22
  this.op = null;
22
23
  this.fn = null;
23
24
 
24
- // for override:
25
25
  this._attributes = [];
26
26
  this._where = {};
27
27
  this._functions = [];
28
+
29
+ // Clauses:
30
+ this.group_by = opts.group_by ?? undefined;
28
31
  this.skip = 0;
29
32
  this.limit = -1; // No limit
30
-
31
- this._includes = opts.includes ?? [];
32
-
33
+
33
34
  this.order = opts.order ?? undefined;
34
35
  this.order_by = opts.order_by ?? undefined;
36
+ // Clauses\
37
+
38
+ this._includes = opts.includes ?? [];
35
39
  }
36
40
 
37
41
  // Getters:
@@ -107,6 +111,7 @@ class ModelsTreeNode {
107
111
 
108
112
  where: this.where,
109
113
 
114
+ group_by: this.group_by,
110
115
  skip: this.skip,
111
116
  limit: this.limit,
112
117
  order: this.order,
@@ -9,7 +9,7 @@ const Enum = require('nodester/enum');
9
9
 
10
10
  const { ModelsTree, ModelsTreeNode } = require('./ModelsTree');
11
11
 
12
- const NodesterQueryError = require('./NodesterQueryError');
12
+ const { NodesterQueryError } = require('nodester/errors');
13
13
 
14
14
  const util = require('util');
15
15
  const debug = require('debug')('nodester:interpreter:QueryLexer');
@@ -22,6 +22,7 @@ const PARAM_TOKENS = new Enum({
22
22
  INCLUDES: Symbol('includes'),
23
23
 
24
24
  // Clauses:
25
+ GROUP_BY: Symbol('group_by'),
25
26
  LIMIT: Symbol('limit'),
26
27
  ORDER: Symbol('order'),
27
28
  ORDER_BY: Symbol('order_by'),
@@ -187,12 +188,7 @@ module.exports = class QueryLexer {
187
188
 
188
189
  // If end of FN token:
189
190
  if (!!tree.node.fn) {
190
- // If token is empty, error:
191
- if (token === '') {
192
- const err = UnexpectedCharError(i, char);
193
- throw err;
194
- }
195
-
191
+ // Token is the param of this fn:
196
192
  const fnParams = {
197
193
  fn: tree.node.fn
198
194
  };
@@ -547,6 +543,10 @@ module.exports = class QueryLexer {
547
543
  case 'in':
548
544
  return PARAM_TOKENS.INCLUDES;
549
545
 
546
+ // Clauses:
547
+ case 'group_by':
548
+ return PARAM_TOKENS.GROUP_BY;
549
+
550
550
  case 'limit':
551
551
  case 'l':
552
552
  return PARAM_TOKENS.LIMIT;
@@ -562,6 +562,7 @@ module.exports = class QueryLexer {
562
562
  case 'skip':
563
563
  case 's':
564
564
  return PARAM_TOKENS.SKIP;
565
+ // Clauses\
565
566
 
566
567
  default:
567
568
  return token;
@@ -589,6 +590,10 @@ module.exports = class QueryLexer {
589
590
  break;
590
591
 
591
592
  // Clauses:
593
+ case PARAM_TOKENS.GROUP_BY:
594
+ treeNode.group_by = token;
595
+ break;
596
+
592
597
  case PARAM_TOKENS.LIMIT:
593
598
  treeNode.limit = parseInt(token);
594
599
  break;
@@ -12,10 +12,14 @@ const httpCodes = require('nodester/http/codes');
12
12
 
13
13
  const { ensure } = require('nodester/validators/arguments');
14
14
 
15
+ // Mappers & parsers:
16
+ const mapCOUNT = require('./mappers/functions/count');
17
+
15
18
  const {
16
19
  parseValue,
17
20
  parseWhereEntry,
18
21
  } = require('./parsers');
22
+ // Mappers & parsers\
19
23
 
20
24
  const {
21
25
  disassembleQueryNode,
@@ -26,6 +30,8 @@ const {
26
30
  getModelAssociationProps
27
31
  } = require('../../utils/modelAssociations.util');
28
32
 
33
+ const consl = require('nodester/loggers/console');
34
+
29
35
 
30
36
  module.exports = traverse;
31
37
 
@@ -114,8 +120,13 @@ function traverse(queryNode, filter=null, model=null, association=null) {
114
120
  }
115
121
  }
116
122
 
117
- // At least 1 attribute is mandatory:
118
- if (newQuery.attributes.length === 0) {
123
+ // At least 1 attribute is mandatory
124
+ // or "functions" must be set:
125
+ if (
126
+ functions.length === 0
127
+ &&
128
+ newQuery.attributes.length === 0
129
+ ) {
119
130
  const err = new NodesterQueryError(`No attributes were selected.`);
120
131
  Error.captureStackTrace(err, traverse);
121
132
  throw err;
@@ -124,57 +135,28 @@ function traverse(queryNode, filter=null, model=null, association=null) {
124
135
 
125
136
  // Functions:
126
137
  for (const fnParams of functions) {
138
+ const fnName = fnParams.fn;
139
+
140
+ if (typeof filter.functions[fnName] === 'undefined') {
141
+ const err = new NodesterQueryError(`Function '${ fnName }' is not allowed.`);
142
+ Error.captureStackTrace(err, traverse);
143
+ throw err;
144
+ }
127
145
 
128
- switch(fnParams.fn) {
146
+ switch(fnName) {
129
147
  // SQL COUNT():
130
148
  case 'count': {
131
- const countParams = fnParams.args;
132
-
133
- const [ countTarget ] = countParams;
134
- // Count can be requested for this model,
135
- // or for any of the available uncludes.
136
- const isForRootModel = countTarget === rootModelName.plural.toLowerCase();
137
-
138
- // Compile request:
139
- // Example of desired SQL:
140
- // `(SELECT COUNT(*) FROM comments WHERE comments.post_id=Post.id)`
141
- //
142
- let rawSQL = '(SELECT COUNT(*) FROM ';
143
- let countAttribute = 'count';
144
-
145
- // If request to count one of the includes:
146
- if (!isForRootModel) {
147
- // Check if it's available:
148
- if (
149
- !filter
150
- ||
151
- !filter?.includes[countTarget]
152
- ||
153
- rootModelAssociations[countTarget] === undefined
154
- ) {
155
- const err = new NodesterQueryError(`Count for '${ countTarget }' is not available.`);
156
- Error.captureStackTrace(err, traverse);
157
- throw err;
158
- }
159
-
160
- const {
161
- as,
162
- target,
163
- foreignKey,
164
- sourceKey
165
- } = rootModelAssociations[countTarget];
166
- const { tableName } = target;
167
-
168
- rawSQL += `${ tableName } where ${ tableName }.${ foreignKey }=${ rootModelName.singular }.${ sourceKey })`;
169
- countAttribute = `${ as }_count`;
170
- }
149
+ mapCOUNT(
150
+ fnParams,
151
+ _model,
152
+ filter?.includes,
171
153
 
172
- newQuery.attributes.push(
173
- [sequelize.literal(rawSQL), countAttribute]
154
+ newQuery
174
155
  );
175
156
  }
176
- // Unknow function:
157
+ // Any other function:
177
158
  default:
159
+ consl.warn(`function ${ fnName }() is not supported`);
178
160
  break;
179
161
  }
180
162
  }
@@ -194,6 +176,22 @@ function traverse(queryNode, filter=null, model=null, association=null) {
194
176
  }
195
177
 
196
178
  switch(clauseName) {
179
+ case 'group_by': {
180
+ // Check if this value is a valid attribute:
181
+ if (typeof value === 'undefined') {
182
+ continue;
183
+ }
184
+
185
+ if (typeof _model.tableAttributes[value] === 'undefined') {
186
+ const err = new NodesterQueryError(`group_by '${ value }' is not allowed.`);
187
+ Error.captureStackTrace(err, traverse);
188
+ throw err;
189
+ }
190
+
191
+ newQuery.group = value;
192
+ continue;
193
+ }
194
+
197
195
  case 'limit': {
198
196
  const _value = _setValueWithBounds(value, 'number', filter.bounds.clauses.limit);
199
197
 
@@ -241,6 +239,10 @@ function traverse(queryNode, filter=null, model=null, association=null) {
241
239
  const [ clauseName, staticClauseValue ] = entry;
242
240
 
243
241
  switch(clauseName) {
242
+ case 'group_by':
243
+ newQuery.group = staticClauseValue;
244
+ continue;
245
+
244
246
  case 'limit':
245
247
  newQuery.limit = staticClauseValue;
246
248
  continue;
@@ -0,0 +1,76 @@
1
+ const { NodesterQueryError } = require('nodester/errors');
2
+
3
+
4
+ module.exports = function mapCOUNT(
5
+ fnParams,
6
+ rootModel,
7
+ filterIncludes,
8
+
9
+ sequelizeQuery
10
+ ) {
11
+ try {
12
+
13
+ const { sequelize } = rootModel;
14
+ const rootModelName = rootModel.options.name;
15
+
16
+ const countParams = fnParams.args;
17
+ const [ countTarget ] = countParams;
18
+
19
+ // COUNT can be requested for this model,
20
+ // or for any of the available includes.
21
+ const associations = rootModel.associations;
22
+
23
+ const isForAssociation = typeof associations[countTarget] !== 'undefined';
24
+ const isForRootModel = countTarget === '';
25
+
26
+ // If request to count one of the includes:
27
+ if (isForAssociation) {
28
+ // Check if it's available:
29
+ if (!filterIncludes[countTarget]) {
30
+ const err = new NodesterQueryError(`Count for '${ countTarget }' is not available.`);
31
+ Error.captureStackTrace(err, mapCOUNT);
32
+ throw err;
33
+ }
34
+
35
+ // Unwrap desired association info:
36
+ const {
37
+ as,
38
+ target,
39
+ foreignKey,
40
+ sourceKey
41
+ } = associations[countTarget];
42
+ const { tableName } = target;
43
+
44
+ // Compile request:
45
+ // Example of desired SQL:
46
+ // `(SELECT COUNT(*) FROM comments WHERE comments.post_id=Post.id)`
47
+ const rawSQL = `(SELECT COUNT(*) FROM ${ tableName } where ${ tableName }.${ foreignKey }=${ rootModelName.singular }.${ sourceKey })`;
48
+
49
+ const countAttributeName = `${ as }_count`;
50
+ sequelizeQuery.attributes.push(
51
+ [sequelize.literal(rawSQL), countAttributeName]
52
+ );
53
+ }
54
+
55
+ // If request to COUNT root model:
56
+ else if (isForRootModel) {
57
+ const firstAttribute = Object.keys(rootModel.tableAttributes)[0];
58
+
59
+ const countAttributeName = `${ rootModelName.plural.toLowerCase() }_count`;
60
+ sequelizeQuery.attributes.push(
61
+ [sequelize.fn('COUNT', firstAttribute), countAttributeName]
62
+ );
63
+ }
64
+
65
+ // Unknown attribute:
66
+ else {
67
+ const err = new NodesterQueryError(`Count for '${ countTarget }' is not available.`);
68
+ Error.captureStackTrace(err, mapCOUNT);
69
+ throw err;
70
+ }
71
+ }
72
+ catch(error) {
73
+ Error.captureStackTrace(error, mapCOUNT);
74
+ throw error;
75
+ }
76
+ }
@@ -44,6 +44,8 @@ module.exports = class NodesterFilter {
44
44
 
45
45
  this._attributes = [];
46
46
  this._clauses = [];
47
+ this._functions = [];
48
+
47
49
  this._includes = {};
48
50
 
49
51
  this._bounds = {
@@ -74,21 +76,47 @@ module.exports = class NodesterFilter {
74
76
  const {
75
77
  attributes,
76
78
  clauses,
79
+ functions,
77
80
  includes,
78
81
  bounds,
79
82
  statics,
80
83
  } = options;
81
84
 
82
85
 
83
- // If attributes are array:
84
- if (Array.isArray(attributes)) {
86
+ // Check attributes type:
87
+ if (!!attributes) {
88
+ if (!Array.isArray(attributes)) {
89
+ const err = new TypeError(`[NodesterFilter]: 'attributes' parameter must be an array.`);
90
+ throw err;
91
+ }
92
+
85
93
  this._attributes = attributes;
86
94
  }
87
95
 
88
- if (Array.isArray(clauses)) {
96
+ // Check clauses type:
97
+ if (!!clauses) {
98
+ if (!Array.isArray(clauses)) {
99
+ const err = new TypeError(`[NodesterFilter]: 'clauses' parameter must be an array.`);
100
+ throw err;
101
+ }
102
+
89
103
  this._clauses = clauses;
90
104
  }
91
105
 
106
+ // If functions are set:
107
+ if (!!functions) {
108
+ if (
109
+ typeof functions !== 'object'
110
+ ||
111
+ Array.isArray(functions)
112
+ ) {
113
+ const err = new TypeError(`[NodesterFilter]: 'functions' parameter must be an object.`);
114
+ throw err;
115
+ }
116
+
117
+ this._functions = functions;
118
+ }
119
+
92
120
  // Includes:
93
121
  if (typeof includes === 'object') {
94
122
  const { associations } = this.model;
@@ -152,6 +180,10 @@ module.exports = class NodesterFilter {
152
180
  return this._clauses;
153
181
  }
154
182
 
183
+ get functions() {
184
+ return this._functions;
185
+ }
186
+
155
187
  get includes() {
156
188
  return this._includes;
157
189
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodester",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "A versatile REST framework for Node.js",
5
5
  "directories": {
6
6
  "docs": "docs",
@@ -1,9 +0,0 @@
1
-
2
- class NodesterQueryError extends Error {
3
- constructor(message) {
4
- super(message);
5
- this.name = 'NodesterQueryError';
6
- }
7
- }
8
-
9
- module.exports = NodesterQueryError;