nodester 0.1.0 → 0.1.4
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 +3 -2
- package/lib/application/index.js +1 -1
- package/lib/constants/ErrorCodes.js +19 -0
- package/lib/constants/Operations.js +1 -1
- package/lib/controllers/mixins/index.js +59 -20
- package/lib/facades/methods/index.js +10 -3
- package/lib/factories/errors/CustomError.js +7 -0
- package/lib/factories/errors/NodesterQueryError.js +23 -0
- package/lib/factories/errors/index.js +10 -3
- package/lib/loggers/dev.js +28 -0
- package/lib/middlewares/ql/sequelize/interpreter/ModelsTree.js +24 -1
- package/lib/middlewares/ql/sequelize/interpreter/QueryLexer.js +64 -12
- package/lib/queries/Colander.js +25 -2
- package/lib/queries/NodesterQueryParams.js +6 -0
- package/lib/queries/traverse.js +77 -7
- package/lib/router/index.js +1 -1
- package/lib/stacks/MarkersStack.js +1 -1
- package/lib/stacks/MiddlewareStack.js +1 -1
- package/lib/utils/models.js +14 -0
- package/package.json +18 -1
- package/lib/preprocessors/IncludesPreprocessor.js +0 -55
- package/lib/preprocessors/QueryPreprocessor.js +0 -64
- /package/lib/{logger → loggers}/console.js +0 -0
package/Readme.md
CHANGED
|
@@ -30,12 +30,13 @@ app.listen(8080, function() {
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
### Core concepts
|
|
33
|
-
[
|
|
33
|
+
[Core concepts documentation ➡️](docs/CoreConcepts.md)
|
|
34
|
+
|
|
34
35
|
|
|
35
36
|
### Queries & Querying - Nodester Query Language (NQR)
|
|
36
37
|
One of the main power points of nodester is it's query language. It's an extension of a REST API syntaxis for a broader integration with a database SQL. Read more about it in the documentation:
|
|
37
38
|
|
|
38
|
-
[
|
|
39
|
+
[NQR documentaion ➡️](docs/Queries.md)
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
### Database
|
package/lib/application/index.js
CHANGED
|
@@ -27,7 +27,7 @@ const {
|
|
|
27
27
|
} = require('../database/utils');
|
|
28
28
|
|
|
29
29
|
const { merge } = require('../utils/objects.util');
|
|
30
|
-
const consl = require('
|
|
30
|
+
const consl = require('nodester/loggers/console');
|
|
31
31
|
const debug = require('debug')('nodester:application');
|
|
32
32
|
|
|
33
33
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const Enum = require('nodester/enum');
|
|
2
|
+
|
|
3
|
+
const NODESTER_QUERY_ERROR = 'NodesterQueryError';
|
|
4
|
+
const UNAUTHORIZED_ERROR = 'Unauthorized';
|
|
5
|
+
const NOT_FOUND_ERROR = 'NotFound';
|
|
6
|
+
const VALIDATION_ERROR = 'ValidationError';
|
|
7
|
+
const CONFLICT_ERROR = 'ConflictError';
|
|
8
|
+
const SEQUELIZE_UNIQUE_CONSTRAINT_ERROR = 'SequelizeUniqueConstraintError';
|
|
9
|
+
const INTERNAL_VALIDATION_ERROR = 'InternalValidationError';
|
|
10
|
+
|
|
11
|
+
module.exports = new Enum({
|
|
12
|
+
NODESTER_QUERY_ERROR,
|
|
13
|
+
UNAUTHORIZED_ERROR,
|
|
14
|
+
NOT_FOUND_ERROR,
|
|
15
|
+
VALIDATION_ERROR,
|
|
16
|
+
CONFLICT_ERROR,
|
|
17
|
+
SEQUELIZE_UNIQUE_CONSTRAINT_ERROR,
|
|
18
|
+
INTERNAL_VALIDATION_ERROR,
|
|
19
|
+
});
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
const {
|
|
2
|
+
NODESTER_QUERY_ERROR,
|
|
3
|
+
UNAUTHORIZED_ERROR,
|
|
4
|
+
NOT_FOUND_ERROR,
|
|
5
|
+
VALIDATION_ERROR,
|
|
6
|
+
CONFLICT_ERROR,
|
|
7
|
+
SEQUELIZE_UNIQUE_CONSTRAINT_ERROR,
|
|
8
|
+
INTERNAL_VALIDATION_ERROR
|
|
9
|
+
} = require('nodester/constants/ErrorCodes');
|
|
10
|
+
|
|
1
11
|
const {
|
|
2
12
|
getOne,
|
|
3
13
|
getMany,
|
|
@@ -10,6 +20,8 @@ const {
|
|
|
10
20
|
module.exports = {
|
|
11
21
|
withDefaultCRUD: _withDefaultCRUD,
|
|
12
22
|
withDefaultErrorProcessing: _withDefaultErrorProcessing,
|
|
23
|
+
|
|
24
|
+
setFacade: _setFacade,
|
|
13
25
|
setMethod: _setMethod
|
|
14
26
|
}
|
|
15
27
|
|
|
@@ -42,18 +54,7 @@ function _withDefaultCRUD(controller, opts={}) {
|
|
|
42
54
|
throw err;
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
const err = new TypeError(`'opts.facade' argument is invalid.`);
|
|
47
|
-
throw err;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Set main facade:
|
|
51
|
-
if (facade.constructor.name === 'Function') {
|
|
52
|
-
controller.facade = new facade();
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
controller.facade = facade;
|
|
56
|
-
}
|
|
57
|
+
_setFacade(controller, facade);
|
|
57
58
|
|
|
58
59
|
// Set model info:
|
|
59
60
|
const model = facade.model;
|
|
@@ -143,34 +144,39 @@ function _withDefaultErrorProcessing(controller, opts={}) {
|
|
|
143
144
|
let errorResponse = {};
|
|
144
145
|
|
|
145
146
|
switch(error.name) {
|
|
146
|
-
case
|
|
147
|
+
case UNAUTHORIZED_ERROR: {
|
|
147
148
|
statusCode = 401;
|
|
148
149
|
errorResponse.details = { message: 'Unauthorized' };
|
|
149
150
|
break;
|
|
150
151
|
}
|
|
151
|
-
case
|
|
152
|
+
case NOT_FOUND_ERROR: {
|
|
152
153
|
statusCode = 404;
|
|
153
154
|
errorResponse.details = { message: errorMessage };
|
|
154
155
|
break;
|
|
155
156
|
}
|
|
156
|
-
case
|
|
157
|
-
statusCode =
|
|
157
|
+
case NODESTER_QUERY_ERROR: {
|
|
158
|
+
statusCode = 422;
|
|
159
|
+
errorResponse.details = { message: errorMessage };
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case VALIDATION_ERROR: {
|
|
163
|
+
statusCode = 422;
|
|
158
164
|
errorResponse.details = error?.details;
|
|
159
165
|
break;
|
|
160
166
|
}
|
|
161
|
-
case
|
|
167
|
+
case CONFLICT_ERROR: {
|
|
162
168
|
statusCode = 409;
|
|
163
169
|
errorResponse.details = error?.details ?? error?.message;
|
|
164
170
|
break;
|
|
165
171
|
}
|
|
166
|
-
case
|
|
172
|
+
case SEQUELIZE_UNIQUE_CONSTRAINT_ERROR: {
|
|
167
173
|
statusCode = 409;
|
|
168
174
|
errorResponse.details = error?.errors;
|
|
169
175
|
break;
|
|
170
176
|
}
|
|
171
|
-
case
|
|
177
|
+
case INTERNAL_VALIDATION_ERROR: {
|
|
172
178
|
statusCode = 500;
|
|
173
|
-
errorResponse.details = { message:
|
|
179
|
+
errorResponse.details = { message: errorMessage };
|
|
174
180
|
break;
|
|
175
181
|
}
|
|
176
182
|
default: {
|
|
@@ -201,6 +207,39 @@ function _withDefaultErrorProcessing(controller, opts={}) {
|
|
|
201
207
|
}
|
|
202
208
|
|
|
203
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Sets one of or all of CRUD methods to Controller.
|
|
212
|
+
*
|
|
213
|
+
* @param {Function|Object} controller
|
|
214
|
+
* @param {Function|Object} facade
|
|
215
|
+
* @param {String} facadeName
|
|
216
|
+
*
|
|
217
|
+
* @return {Function|Object} controller
|
|
218
|
+
*
|
|
219
|
+
* @api public
|
|
220
|
+
* @alias setFacade
|
|
221
|
+
*/
|
|
222
|
+
function _setFacade(controller, facade, facadeName=null) {
|
|
223
|
+
if (!controller) {
|
|
224
|
+
const err = new TypeError(`'controller' argument is not provided.`);
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!facade) {
|
|
229
|
+
const err = new TypeError(`'facade' argument is invalid.`);
|
|
230
|
+
throw err;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Set main facade:
|
|
234
|
+
if (facade.constructor.name === 'Function') {
|
|
235
|
+
controller[facadeName ?? 'facade'] = new facade();
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
controller[facadeName ?? 'facade'] = facade;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
204
243
|
/**
|
|
205
244
|
* Sets one of CRUD methods to Controller.
|
|
206
245
|
*
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const Params = require('nodester/params');
|
|
2
2
|
|
|
3
|
+
const log = require('nodester/loggers/dev');
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
module.exports = {
|
|
5
7
|
getOne: _getOne,
|
|
@@ -29,11 +31,12 @@ async function _getOne(params) {
|
|
|
29
31
|
|
|
30
32
|
const result = {
|
|
31
33
|
[this.modelName.singular]: instance,
|
|
32
|
-
count: 0 + instance !== null
|
|
34
|
+
count: 0 + (instance !== null)
|
|
33
35
|
}
|
|
34
36
|
return Promise.resolve(result);
|
|
35
37
|
}
|
|
36
38
|
catch(error) {
|
|
39
|
+
log.error(`${ [this.modelName.singular] }Facade.getOne error:`, error);
|
|
37
40
|
return Promise.reject(error);
|
|
38
41
|
}
|
|
39
42
|
}
|
|
@@ -63,6 +66,7 @@ async function _getMany(params) {
|
|
|
63
66
|
return Promise.resolve(result);
|
|
64
67
|
}
|
|
65
68
|
catch(error) {
|
|
69
|
+
log.error(`${ [this.modelName.singular] }Facade.getMany error:`, error);
|
|
66
70
|
return Promise.reject(error);
|
|
67
71
|
}
|
|
68
72
|
}
|
|
@@ -96,7 +100,7 @@ async function _createOne(params) {
|
|
|
96
100
|
|
|
97
101
|
const result = {
|
|
98
102
|
[this.modelName.singular]: instance,
|
|
99
|
-
count: instance
|
|
103
|
+
count: 0 + (instance !== null)
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
// Call after create.
|
|
@@ -105,6 +109,7 @@ async function _createOne(params) {
|
|
|
105
109
|
return Promise.resolve(result);
|
|
106
110
|
}
|
|
107
111
|
catch(error) {
|
|
112
|
+
log.error(`${ [this.modelName.singular] }Facade.createOne error:`, error);
|
|
108
113
|
return Promise.reject(error);
|
|
109
114
|
}
|
|
110
115
|
}
|
|
@@ -134,11 +139,12 @@ async function _updateOne(params) {
|
|
|
134
139
|
const result = {
|
|
135
140
|
success: isNewRecord === false,
|
|
136
141
|
[this.modelName.singular]: instance,
|
|
137
|
-
count:
|
|
142
|
+
count: 0 + (instance !== null)
|
|
138
143
|
}
|
|
139
144
|
return Promise.resolve(result);
|
|
140
145
|
}
|
|
141
146
|
catch(error) {
|
|
147
|
+
log.error(`${ [this.modelName.singular] }Facade.updateOne error:`, error);
|
|
142
148
|
return Promise.reject(error);
|
|
143
149
|
}
|
|
144
150
|
}
|
|
@@ -168,6 +174,7 @@ async function _deleteOne(params) {
|
|
|
168
174
|
return Promise.resolve(result);
|
|
169
175
|
}
|
|
170
176
|
catch(error) {
|
|
177
|
+
log.error(`${ [this.modelName.singular] }Facade.deleteOne error:`, error);
|
|
171
178
|
return Promise.reject(error);
|
|
172
179
|
}
|
|
173
180
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* /nodester
|
|
3
|
+
* MIT Licensed
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
NODESTER_QUERY_ERROR,
|
|
10
|
+
} = require('nodester/constants/ErrorCodes');
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
module.exports = class NodesterQueryError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
|
|
17
|
+
this.name = NODESTER_QUERY_ERROR;
|
|
18
|
+
this.status = 422;
|
|
19
|
+
|
|
20
|
+
// Remove constructor info from stack.
|
|
21
|
+
Error.captureStackTrace(this, this.constructor);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
1
|
+
/*!
|
|
2
|
+
* /nodester
|
|
3
|
+
* MIT Licensed
|
|
3
4
|
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
|
|
4
9
|
const Err = require('./CustomError');
|
|
10
|
+
const NodesterQueryError = require('./NodesterQueryError');
|
|
5
11
|
|
|
6
12
|
|
|
7
13
|
module.exports = {
|
|
8
|
-
Err
|
|
14
|
+
Err,
|
|
15
|
+
NodesterQueryError
|
|
9
16
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* /nodester
|
|
3
|
+
* MIT Licensed
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
exports = module.exports = {
|
|
10
|
+
error: _error
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Log error using console.error.
|
|
15
|
+
*
|
|
16
|
+
* @param {Array} args
|
|
17
|
+
*
|
|
18
|
+
* @alias error
|
|
19
|
+
* @public
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
function _error(...args) {
|
|
23
|
+
const activeEnv = process.env.NODE_ENV
|
|
24
|
+
|
|
25
|
+
if (activeEnv === 'development' || activeEnv === 'testing') {
|
|
26
|
+
console.error(...args);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* /nodester
|
|
3
|
+
* MIT Licensed
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
1
7
|
const debug = require('debug')('nodester:interpreter:ModelsTree');
|
|
2
8
|
|
|
3
9
|
|
|
@@ -7,10 +13,12 @@ class ModelsTreeNode {
|
|
|
7
13
|
this.parent = parent;
|
|
8
14
|
this.activeParam = null;
|
|
9
15
|
this.op = null;
|
|
16
|
+
this.fn = null;
|
|
10
17
|
|
|
11
18
|
// for override:
|
|
12
19
|
this.fields = [];
|
|
13
20
|
this._where = {};
|
|
21
|
+
this._functions = [];
|
|
14
22
|
this.skip = 0;
|
|
15
23
|
this.limit = -1; // No limit
|
|
16
24
|
|
|
@@ -35,6 +43,10 @@ class ModelsTreeNode {
|
|
|
35
43
|
return this._where;
|
|
36
44
|
}
|
|
37
45
|
|
|
46
|
+
get functions() {
|
|
47
|
+
return this._functions;
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
resetActiveParam() {
|
|
39
51
|
this.activeParam = null;
|
|
40
52
|
}
|
|
@@ -43,6 +55,10 @@ class ModelsTreeNode {
|
|
|
43
55
|
this.op = null;
|
|
44
56
|
}
|
|
45
57
|
|
|
58
|
+
resetFN() {
|
|
59
|
+
this.fn = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
addWhere(condition={}) {
|
|
47
63
|
this._where = {
|
|
48
64
|
...this.where,
|
|
@@ -50,6 +66,10 @@ class ModelsTreeNode {
|
|
|
50
66
|
}
|
|
51
67
|
}
|
|
52
68
|
|
|
69
|
+
addFunction(fnParams={}) {
|
|
70
|
+
this._functions.push(fnParams);
|
|
71
|
+
}
|
|
72
|
+
|
|
53
73
|
include(modelTreeNode) {
|
|
54
74
|
modelTreeNode.parent = this;
|
|
55
75
|
this.includes.push(modelTreeNode);
|
|
@@ -60,13 +80,16 @@ class ModelsTreeNode {
|
|
|
60
80
|
return {
|
|
61
81
|
model: this.model,
|
|
62
82
|
|
|
83
|
+
fields: this.fields,
|
|
84
|
+
functions: this.functions,
|
|
85
|
+
|
|
63
86
|
where: this.where,
|
|
87
|
+
|
|
64
88
|
skip: this.skip,
|
|
65
89
|
limit: this.limit,
|
|
66
90
|
order: this.order,
|
|
67
91
|
order_by: this.order_by,
|
|
68
92
|
|
|
69
|
-
fields: this.fields,
|
|
70
93
|
|
|
71
94
|
includes: this.includes.map(i => i.toObject())
|
|
72
95
|
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* /nodester
|
|
3
|
+
* MIT Licensed
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
|
|
1
8
|
const Enum = require('../../../../enums/Enum');
|
|
2
9
|
const { ModelsTree, ModelsTreeNode } = require('./ModelsTree');
|
|
3
10
|
const util = require('util');
|
|
@@ -21,6 +28,10 @@ const OP_TOKENS = new Enum({
|
|
|
21
28
|
LOWER_OR_EQUAL: 'lte'
|
|
22
29
|
});
|
|
23
30
|
|
|
31
|
+
const FN_TOKENS = new Enum({
|
|
32
|
+
COUNT: 'count',
|
|
33
|
+
});
|
|
34
|
+
|
|
24
35
|
|
|
25
36
|
module.exports = class QueryLexer {
|
|
26
37
|
constructor(queryString='') {
|
|
@@ -71,22 +82,29 @@ module.exports = class QueryLexer {
|
|
|
71
82
|
token = '';
|
|
72
83
|
continue;
|
|
73
84
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
|
|
86
|
+
// If FN token:
|
|
87
|
+
if (FN_TOKENS.asArray.indexOf(token) > -1) {
|
|
88
|
+
// Set function token.
|
|
89
|
+
tree.node.fn = token;
|
|
78
90
|
token = '';
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
79
93
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
// If model subquery:
|
|
95
|
+
const model = token;
|
|
96
|
+
tree.use(model) ?? tree.include(model).use(model);
|
|
97
|
+
token = '';
|
|
84
98
|
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
// Process subquery:
|
|
100
|
+
i++;
|
|
101
|
+
const [ charsCount ] = this.parseIsolatedQuery(queryString, i, tree);
|
|
102
|
+
i += charsCount;
|
|
87
103
|
|
|
88
|
-
|
|
89
|
-
|
|
104
|
+
previousActive = model;
|
|
105
|
+
tree.up();
|
|
106
|
+
|
|
107
|
+
continue;
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
// ) can mean end of OP token params,
|
|
@@ -126,6 +144,40 @@ module.exports = class QueryLexer {
|
|
|
126
144
|
continue;
|
|
127
145
|
}
|
|
128
146
|
|
|
147
|
+
// If end of FN token:
|
|
148
|
+
if (!!tree.node.fn) {
|
|
149
|
+
// If token is empty, error:
|
|
150
|
+
if (token === '') {
|
|
151
|
+
const err = UnexpectedCharError(i, char);
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let fnParams = {};
|
|
156
|
+
switch (tree.node.fn) {
|
|
157
|
+
case 'count':
|
|
158
|
+
fnParams = {
|
|
159
|
+
fn: 'count',
|
|
160
|
+
args: [token]
|
|
161
|
+
};
|
|
162
|
+
break;
|
|
163
|
+
default:
|
|
164
|
+
fnParams = {
|
|
165
|
+
fn: [tree.node.fn],
|
|
166
|
+
args: [token]
|
|
167
|
+
};
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
tree.node.addFunction(fnParams);
|
|
172
|
+
|
|
173
|
+
// Reset:
|
|
174
|
+
tree.node.resetFN();
|
|
175
|
+
tree.node.activeParam = 'includes';
|
|
176
|
+
token = '';
|
|
177
|
+
value = [];
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
129
181
|
// If end of subquery:
|
|
130
182
|
if (!!tree.node.activeParam && tree.node.activeParam !== 'includes') {
|
|
131
183
|
// Set value.
|
package/lib/queries/Colander.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* /nodester
|
|
3
|
+
* MIT Licensed
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
1
7
|
const CLAUSES = ['limit', 'skip', 'order', 'order_by'];
|
|
2
8
|
|
|
9
|
+
const { isModel } = require('../utils/models');
|
|
10
|
+
|
|
3
11
|
|
|
4
12
|
module.exports = class Colander {
|
|
5
13
|
|
|
6
14
|
/*
|
|
7
15
|
*
|
|
8
16
|
* @param {Object|Model} optsOrModelDefinition
|
|
17
|
+
@ - @param {Model} model
|
|
9
18
|
* - @param {Array} fields
|
|
10
19
|
* - @param {Array} clauses
|
|
11
20
|
* - @param {Object} includes
|
|
@@ -13,8 +22,10 @@ module.exports = class Colander {
|
|
|
13
22
|
* -- @param {Object} attributes
|
|
14
23
|
* -- @param {Object} clauses
|
|
15
24
|
*
|
|
25
|
+
* @param {Boolean} noLimit
|
|
26
|
+
*
|
|
16
27
|
*/
|
|
17
|
-
constructor(optsOrModelDefinition) {
|
|
28
|
+
constructor(optsOrModelDefinition, noLimit=false) {
|
|
18
29
|
this._fields = [];
|
|
19
30
|
this._clauses = [];
|
|
20
31
|
this._includes = {};
|
|
@@ -25,24 +36,36 @@ module.exports = class Colander {
|
|
|
25
36
|
limit: 3
|
|
26
37
|
}
|
|
27
38
|
}
|
|
39
|
+
if (noLimit === true) {
|
|
40
|
+
delete this._statics.clauses.limit;
|
|
41
|
+
}
|
|
28
42
|
|
|
29
43
|
// If model:
|
|
30
|
-
if (
|
|
44
|
+
if (isModel(optsOrModelDefinition)) {
|
|
31
45
|
this._fields = Object.keys(optsOrModelDefinition.tableAttributes);
|
|
32
46
|
this._clauses = CLAUSES;
|
|
33
47
|
}
|
|
34
48
|
// If options:
|
|
35
49
|
else {
|
|
36
50
|
const {
|
|
51
|
+
model,
|
|
37
52
|
fields,
|
|
38
53
|
clauses,
|
|
39
54
|
includes,
|
|
40
55
|
statics,
|
|
41
56
|
} = optsOrModelDefinition;
|
|
42
57
|
|
|
58
|
+
|
|
59
|
+
// If fields are array:
|
|
43
60
|
if (Array.isArray(fields)) {
|
|
44
61
|
this._fields = fields;
|
|
45
62
|
}
|
|
63
|
+
// If fields were not provided,
|
|
64
|
+
// but we have full model definition:
|
|
65
|
+
else if (isModel(model)) {
|
|
66
|
+
this._fields = Object.keys(model.tableAttributes);
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
|
|
47
70
|
if (Array.isArray(clauses)) {
|
|
48
71
|
this._clauses = clauses;
|
package/lib/queries/traverse.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* /nodester
|
|
3
|
+
* MIT Licensed
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
1
7
|
const { Op } = require('sequelize');
|
|
8
|
+
const NQueryError = require('../factories/errors/NodesterQueryError');
|
|
2
9
|
|
|
3
10
|
|
|
4
11
|
module.exports = traverse;
|
|
5
12
|
|
|
6
13
|
function traverse(queryNode, colander=null, model) {
|
|
7
14
|
|
|
15
|
+
const sequelize = model.sequelize;
|
|
8
16
|
const fieldsAvailable = Object.keys(model.tableAttributes);
|
|
9
17
|
const includesAvailable = model.getIncludesList();
|
|
10
18
|
|
|
@@ -18,6 +26,7 @@ function traverse(queryNode, colander=null, model) {
|
|
|
18
26
|
where,
|
|
19
27
|
includes,
|
|
20
28
|
fields,
|
|
29
|
+
functions,
|
|
21
30
|
clauses,
|
|
22
31
|
} = _disassembleQueryNode(queryNode);
|
|
23
32
|
|
|
@@ -67,6 +76,55 @@ function traverse(queryNode, colander=null, model) {
|
|
|
67
76
|
}
|
|
68
77
|
// Fields\
|
|
69
78
|
|
|
79
|
+
// Functions:
|
|
80
|
+
for (const fnParams of functions) {
|
|
81
|
+
|
|
82
|
+
// If COUNT() is requested:
|
|
83
|
+
if (fnParams.fn === 'count') {
|
|
84
|
+
const countParams = fnParams.args;
|
|
85
|
+
|
|
86
|
+
const [ countTarget ] = countParams;
|
|
87
|
+
const RootModelName = model.options.name;
|
|
88
|
+
// Count can be requested for this model,
|
|
89
|
+
// or for any of the available uncludes.
|
|
90
|
+
const isForRootModel = countTarget === RootModelName.plural.toLowerCase();
|
|
91
|
+
|
|
92
|
+
// Compile request:
|
|
93
|
+
// Example:
|
|
94
|
+
// `(SELECT COUNT(*) FROM comments WHERE comments.morph_id=Morph.id)`
|
|
95
|
+
|
|
96
|
+
// Params for attribute:
|
|
97
|
+
let rawSQL = '(SELECT COUNT(*) FROM ';
|
|
98
|
+
let countAttribute = '_count';
|
|
99
|
+
|
|
100
|
+
// If request to count one of includes:
|
|
101
|
+
if (!isForRootModel) {
|
|
102
|
+
// Check if it's available:
|
|
103
|
+
if (
|
|
104
|
+
!colander
|
|
105
|
+
||
|
|
106
|
+
!colander?.includes[countTarget]
|
|
107
|
+
||
|
|
108
|
+
model.associations[countTarget] === undefined
|
|
109
|
+
) {
|
|
110
|
+
const err = new NQueryError(`Count for ${ countTarget } is not available.`);
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const {
|
|
115
|
+
foreignKey,
|
|
116
|
+
sourceKey
|
|
117
|
+
} = model.associations[countTarget];
|
|
118
|
+
rawSQL += `${ countTarget } where ${ countTarget }.${ foreignKey }=${ RootModelName.singular }.${ sourceKey })`;
|
|
119
|
+
countAttribute = `${ countTarget }_count`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
newQuery.attributes.push(
|
|
123
|
+
[sequelize.literal(rawSQL), countAttribute]
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Functions\
|
|
70
128
|
|
|
71
129
|
// Clauses:
|
|
72
130
|
const order = {};
|
|
@@ -87,6 +145,7 @@ function traverse(queryNode, colander=null, model) {
|
|
|
87
145
|
|
|
88
146
|
newQuery.limit = value;
|
|
89
147
|
continue;
|
|
148
|
+
|
|
90
149
|
case 'skip':
|
|
91
150
|
// Do not set if 0:
|
|
92
151
|
if (value === 0)
|
|
@@ -94,12 +153,15 @@ function traverse(queryNode, colander=null, model) {
|
|
|
94
153
|
|
|
95
154
|
newQuery.offset = value;
|
|
96
155
|
continue;
|
|
156
|
+
|
|
97
157
|
case 'order':
|
|
98
158
|
order.order = value;
|
|
99
159
|
continue;
|
|
160
|
+
|
|
100
161
|
case 'order_by':
|
|
101
162
|
order.by = value;
|
|
102
163
|
continue;
|
|
164
|
+
|
|
103
165
|
default:
|
|
104
166
|
continue;
|
|
105
167
|
}
|
|
@@ -107,7 +169,10 @@ function traverse(queryNode, colander=null, model) {
|
|
|
107
169
|
|
|
108
170
|
// "statics" override or set any query Clause:
|
|
109
171
|
if (colander !== null) {
|
|
110
|
-
|
|
172
|
+
const staticClausesEntries = Object.entries(colander.statics.clauses);
|
|
173
|
+
for (let entry of staticClausesEntries) {
|
|
174
|
+
const [clauseName, staticClauseValue] = entry;
|
|
175
|
+
|
|
111
176
|
switch(clauseName) {
|
|
112
177
|
case 'limit':
|
|
113
178
|
newQuery.limit = staticClauseValue;
|
|
@@ -130,7 +195,6 @@ function traverse(queryNode, colander=null, model) {
|
|
|
130
195
|
|
|
131
196
|
|
|
132
197
|
// Order:
|
|
133
|
-
const sequelize = model.sequelize;
|
|
134
198
|
if ( ['rand', 'random'].indexOf(order.order) > -1) {
|
|
135
199
|
newQuery.order = sequelize.random();
|
|
136
200
|
}
|
|
@@ -150,6 +214,11 @@ function traverse(queryNode, colander=null, model) {
|
|
|
150
214
|
break;
|
|
151
215
|
// MAX/MIN\
|
|
152
216
|
|
|
217
|
+
case null:
|
|
218
|
+
case undefined:
|
|
219
|
+
newQuery.order = [ ['id', 'desc'] ];
|
|
220
|
+
break;
|
|
221
|
+
|
|
153
222
|
default:
|
|
154
223
|
newQuery.order = [ [order.by, order.order] ];
|
|
155
224
|
break;
|
|
@@ -189,7 +258,6 @@ function traverse(queryNode, colander=null, model) {
|
|
|
189
258
|
}
|
|
190
259
|
// Where\
|
|
191
260
|
|
|
192
|
-
|
|
193
261
|
return newQuery;
|
|
194
262
|
}
|
|
195
263
|
|
|
@@ -252,9 +320,9 @@ function _addAssociationQuery(associationQuery, includeName, resultQuery) {
|
|
|
252
320
|
|
|
253
321
|
function _parseWhereEntry(attribute, value, whereHolder, staticAttributes) {
|
|
254
322
|
let _value = value;
|
|
255
|
-
const
|
|
323
|
+
const staticAttribute = staticAttributes[attribute];
|
|
256
324
|
|
|
257
|
-
// If attribute is Op (like, or,
|
|
325
|
+
// If attribute is Op (not, like, or, etc.):
|
|
258
326
|
if (attribute in Op) {
|
|
259
327
|
// Parse value:
|
|
260
328
|
_value = _parseValue(_value, attribute);
|
|
@@ -265,8 +333,8 @@ function _parseWhereEntry(attribute, value, whereHolder, staticAttributes) {
|
|
|
265
333
|
}
|
|
266
334
|
|
|
267
335
|
// Static value overrides any other:
|
|
268
|
-
if (!!
|
|
269
|
-
whereHolder[attribute] =
|
|
336
|
+
if (!!staticAttribute) {
|
|
337
|
+
whereHolder[attribute] = staticAttribute;
|
|
270
338
|
return;
|
|
271
339
|
}
|
|
272
340
|
|
|
@@ -279,6 +347,7 @@ function _disassembleQueryNode(queryNode) {
|
|
|
279
347
|
where,
|
|
280
348
|
includes,
|
|
281
349
|
fields,
|
|
350
|
+
functions,
|
|
282
351
|
...clauses
|
|
283
352
|
} = queryNode;
|
|
284
353
|
// delete queryNode.model;
|
|
@@ -287,6 +356,7 @@ function _disassembleQueryNode(queryNode) {
|
|
|
287
356
|
where: where ?? {},
|
|
288
357
|
includes: includes ?? [],
|
|
289
358
|
fields: fields ?? [],
|
|
359
|
+
functions: functions ?? [],
|
|
290
360
|
clauses: clauses ?? []
|
|
291
361
|
};
|
|
292
362
|
}
|
package/lib/router/index.js
CHANGED
|
@@ -22,7 +22,7 @@ const Path = require('path');
|
|
|
22
22
|
const fs = require('fs');
|
|
23
23
|
const commonExtensions = require('common-js-file-extensions');
|
|
24
24
|
// Debug & console:
|
|
25
|
-
const consl = require('
|
|
25
|
+
const consl = require('nodester/loggers/console');
|
|
26
26
|
const debug = require('debug')('nodester:router');
|
|
27
27
|
|
|
28
28
|
|
package/package.json
CHANGED
|
@@ -1,29 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodester",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "A boilerplate framework for Node.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./lib/application/index.js",
|
|
7
|
+
|
|
8
|
+
"./constants/ErrorCodes": "./lib/constants/ErrorCodes.js",
|
|
9
|
+
|
|
7
10
|
"./controllers/methods": "./lib/controllers/methods/index.js",
|
|
8
11
|
"./controllers/mixins": "./lib/controllers/mixins/index.js",
|
|
12
|
+
|
|
9
13
|
"./database/connection": "./lib/database/connection.js",
|
|
10
14
|
"./database/migration": "./lib/database/migration.js",
|
|
11
15
|
"./database/utils": "./lib/database/utils.js",
|
|
16
|
+
|
|
12
17
|
"./enum": "./lib/enums/Enum.js",
|
|
18
|
+
|
|
13
19
|
"./facades/methods": "./lib/facades/methods/index.js",
|
|
14
20
|
"./facades/mixins": "./lib/facades/mixins/index.js",
|
|
21
|
+
|
|
15
22
|
"./factories/errors": "./lib/factories/errors/index.js",
|
|
16
23
|
"./factories/responses/rest": "./lib/factories/responses/rest/index.js",
|
|
24
|
+
|
|
17
25
|
"./http/codes": "./lib/http/codes/index.js",
|
|
26
|
+
"./loggers/console": "./lib/loggers/console.js",
|
|
27
|
+
"./loggers/dev": "./lib/loggers/dev.js",
|
|
28
|
+
|
|
18
29
|
"./middlewares/formidable": "./lib/middlewares/formidable/index.js",
|
|
30
|
+
|
|
19
31
|
"./models/associate": "./lib/models/associate.js",
|
|
20
32
|
"./models/define": "./lib/models/define.js",
|
|
33
|
+
|
|
21
34
|
"./params": "./lib/params/Params.js",
|
|
35
|
+
|
|
22
36
|
"./ql/sequelize": "./lib/middlewares/ql/sequelize",
|
|
23
37
|
"./queries/Colander": "./lib/queries/Colander.js",
|
|
24
38
|
"./queries/traverse": "./lib/queries/traverse.js",
|
|
39
|
+
|
|
25
40
|
"./route": "./lib/router/route.js",
|
|
26
41
|
"./router": "./lib/router/index.js",
|
|
42
|
+
|
|
43
|
+
"./utils/sql": "./lib/utils/sql.util.js",
|
|
27
44
|
"./utils/strings": "./lib/utils/strings.util.js"
|
|
28
45
|
},
|
|
29
46
|
"directories": {
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
// Constants:
|
|
2
|
-
const VISITOR = 'visitor';
|
|
3
|
-
const DefaultAvailableParamsForRoles = { [VISITOR]: [] };
|
|
4
|
-
const DefaultStaticParamsForRoles = { [VISITOR]: [] };
|
|
5
|
-
|
|
6
|
-
// Custom error.
|
|
7
|
-
const { Err } = require('nodester/factories/errors');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
module.exports = class IncludesPreprocessor {
|
|
11
|
-
|
|
12
|
-
constructor(
|
|
13
|
-
availableParamsForRoles,
|
|
14
|
-
staticParamsForRoles,
|
|
15
|
-
customProcessFunction=()=>{}
|
|
16
|
-
) {
|
|
17
|
-
this.availableParamsForRoles = availableParamsForRoles ?? DefaultAvailableParamsForRoles;
|
|
18
|
-
this.staticParamsForRoles = staticParamsForRoles ?? DefaultStaticParamsForRoles;
|
|
19
|
-
|
|
20
|
-
this.customProcessFunction = customProcessFunction ? customProcessFunction : ()=>{};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async extract(
|
|
24
|
-
req,
|
|
25
|
-
role
|
|
26
|
-
) {
|
|
27
|
-
const requestIncludes = req.query?.includes ?? [];
|
|
28
|
-
|
|
29
|
-
if (!requestQuery || typeof requestQuery !== 'object') {
|
|
30
|
-
const err = new Err();
|
|
31
|
-
err.name = 'ValidationError';
|
|
32
|
-
throw err;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Get role or set "visitor"
|
|
36
|
-
const _role = typeof role === 'string' && role.length > 1 ? role : VISITOR;
|
|
37
|
-
|
|
38
|
-
const resultIncludes = [];
|
|
39
|
-
|
|
40
|
-
const params = this.availableParamsForRoles[_role] ?? [];
|
|
41
|
-
const staticValues = this.staticParamsForRoles[_role] ?? [];
|
|
42
|
-
|
|
43
|
-
params.forEach((param) => {
|
|
44
|
-
// If such param is set in query:
|
|
45
|
-
if (requestIncludes.indexOf(param) !== -1) {
|
|
46
|
-
resultIncludes.push(param);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// Make further preprocessing using custom defined function.
|
|
51
|
-
await this.customProcessFunction.call(this, req, role, resultIncludes);
|
|
52
|
-
|
|
53
|
-
return resultIncludes;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
// Constants:
|
|
2
|
-
const VISITOR = 'visitor';
|
|
3
|
-
const DefaultAvailableParamsForRoles = { [VISITOR]: [ 'skip', 'limit', 'order' ] };
|
|
4
|
-
const DefaultStaticParamsForRoles = { [VISITOR]: { limit: 50 } };
|
|
5
|
-
|
|
6
|
-
// Custom error.
|
|
7
|
-
const { Err } = require('nodester/factories/errors');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
module.exports = class QueryPreprocessor {
|
|
11
|
-
|
|
12
|
-
constructor(
|
|
13
|
-
availableParamsForRoles,
|
|
14
|
-
staticParamsForRoles,
|
|
15
|
-
customProcessFunction
|
|
16
|
-
) {
|
|
17
|
-
this.availableParamsForRoles = availableParamsForRoles ?? DefaultAvailableParamsForRoles;
|
|
18
|
-
this.staticParamsForRoles = staticParamsForRoles ?? DefaultStaticParamsForRoles;
|
|
19
|
-
|
|
20
|
-
this.customProcessFunction = customProcessFunction ? customProcessFunction : ()=>{};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async extract(
|
|
24
|
-
req,
|
|
25
|
-
role
|
|
26
|
-
) {
|
|
27
|
-
try {
|
|
28
|
-
const requestQuery = req.query;
|
|
29
|
-
|
|
30
|
-
if (!requestQuery || typeof requestQuery !== 'object') {
|
|
31
|
-
const err = new Err();
|
|
32
|
-
err.name = 'ValidationError';
|
|
33
|
-
throw err;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Get role or set "visitor"
|
|
37
|
-
const _role = typeof role === 'string' && role.length > 1 ? role : [VISITOR];
|
|
38
|
-
|
|
39
|
-
const resultQuery = {};
|
|
40
|
-
|
|
41
|
-
const params = this.availableParamsForRoles[_role] ?? [];
|
|
42
|
-
const staticValues = this.staticParamsForRoles[_role] ?? {};
|
|
43
|
-
|
|
44
|
-
params.forEach((param) => {
|
|
45
|
-
// If such param is set in query:
|
|
46
|
-
if (!!requestQuery[param]) {
|
|
47
|
-
resultQuery[param] = staticValues[param] ?? requestQuery[param];
|
|
48
|
-
}
|
|
49
|
-
// If such param is not set, but we have a "static" for it:
|
|
50
|
-
else if (!requestQuery[param] && !!staticValues[param]) {
|
|
51
|
-
resultQuery[param] = staticValues[param];
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Make further preprocessing using customly defined function.
|
|
56
|
-
await this.customProcessFunction.call(this, req, role, resultQuery);
|
|
57
|
-
|
|
58
|
-
return Promise.resolve(resultQuery);
|
|
59
|
-
}
|
|
60
|
-
catch(error) {
|
|
61
|
-
return Promise.reject(error);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
File without changes
|