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.
- package/lib/errors/NodesterQueryError.js +4 -8
- package/lib/middlewares/ql/sequelize/interpreter/ModelsTree.js +9 -4
- package/lib/middlewares/ql/sequelize/interpreter/QueryLexer.js +12 -7
- package/lib/query/traverse/index.js +48 -46
- package/lib/query/traverse/mappers/functions/count.js +76 -0
- package/lib/structures/Filter.js +35 -3
- package/package.json +1 -1
- package/lib/middlewares/ql/sequelize/interpreter/NodesterQueryError.js +0 -9
|
@@ -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
|
|
26
|
-
constructor(message) {
|
|
20
|
+
module.exports = class NodesterQueryError extends Error {
|
|
21
|
+
constructor(message, status) {
|
|
27
22
|
super(message);
|
|
28
23
|
|
|
29
|
-
this.
|
|
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('
|
|
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
|
-
//
|
|
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
|
-
//
|
|
118
|
-
|
|
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(
|
|
146
|
+
switch(fnName) {
|
|
129
147
|
// SQL COUNT():
|
|
130
148
|
case 'count': {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
173
|
-
[sequelize.literal(rawSQL), countAttribute]
|
|
154
|
+
newQuery
|
|
174
155
|
);
|
|
175
156
|
}
|
|
176
|
-
//
|
|
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
|
+
}
|
package/lib/structures/Filter.js
CHANGED
|
@@ -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
|
-
//
|
|
84
|
-
if (
|
|
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
|
-
|
|
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