nodester 0.2.0 → 0.2.2
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 +9 -6
- package/lib/application/index.js +12 -1
- package/lib/controllers/mixins/index.js +1 -1
- package/lib/database/migration.js +5 -6
- package/lib/errors/NodesterError.js +18 -0
- package/lib/{factories/errors → errors}/NodesterQueryError.js +7 -4
- package/lib/{factories/errors → errors}/index.js +2 -0
- package/lib/facades/methods/index.js +28 -6
- package/lib/facades/mixins/index.js +3 -2
- package/lib/factories/responses/rest.js +62 -33
- package/lib/http/codes/index.js +2 -1
- package/lib/middlewares/cookies/index.js +10 -11
- package/lib/middlewares/formidable/index.js +11 -10
- package/lib/middlewares/ql/sequelize/index.js +10 -4
- package/lib/middlewares/ql/sequelize/interpreter/ModelsTree.js +15 -10
- package/lib/middlewares/ql/sequelize/interpreter/QueryLexer.js +9 -2
- package/lib/query/traverse.js +13 -14
- package/lib/router/index.js +1 -1
- package/lib/router/route.js +1 -1
- package/lib/stacks/MarkersStack.js +1 -1
- package/lib/stacks/MiddlewaresStack.js +4 -4
- package/lib/structures/Filter.js +3 -2
- package/lib/tools/nql.tool.js +45 -0
- package/lib/utils/objects.util.js +1 -1
- package/lib/validators/arguments.js +7 -3
- package/package.json +15 -7
- package/tests/ast.test.js +16 -0
- package/tests/index.test.js +4 -4
- package/tests/nql.test.js +18 -1
- /package/lib/{factories/errors → errors}/CustomError.js +0 -0
package/Readme.md
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# nodester
|
|
2
|
-
> A robust and flexible boilerplate framework that makes iterative development easy.
|
|
3
2
|
|
|
4
3
|
[](https://www.npmjs.com/package/nodester)
|
|
5
4
|
[](https://www.npmjs.com/package/nodester)
|
|
6
5
|
|
|
6
|
+
> **nodester** is a modern and versatile Node.js framework designed to streamline the development of robust and scalable web applications.
|
|
7
|
+
|
|
8
|
+
The main reason of nodester's existence is the [nodester Query Language (NQL)](docs/Queries.md), an extension of standard REST API syntax, it lets you craft complex queries with hierarchical associations.
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
## Installation
|
|
9
12
|
|
|
@@ -18,7 +21,6 @@ npm install -S nodester
|
|
|
18
21
|
|
|
19
22
|
- [Usage](#usage)
|
|
20
23
|
- [Documentation](#documentation)
|
|
21
|
-
- [Extending App](#extending-application-functionality)
|
|
22
24
|
- [Philosophy](#philosophy)
|
|
23
25
|
- [License](#license)
|
|
24
26
|
- [Copyright](#copyright)
|
|
@@ -47,10 +49,11 @@ app.listen(8080, function() {
|
|
|
47
49
|
[Core concepts documentation ➡️](docs/CoreConcepts.md)
|
|
48
50
|
|
|
49
51
|
|
|
50
|
-
### Queries & Querying - Nodester Query Language (
|
|
51
|
-
|
|
52
|
+
### Queries & Querying - Nodester Query Language (NQL)
|
|
53
|
+
The true strength of nodester lies in its query language. Serving as an extension of standard REST API syntax, it brings many aspects of SQL into REST requests, providing developers with a simple yet potent tool for expressive and efficient data querying.
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
Read more about it in the documentation:
|
|
56
|
+
[NQL documentaion ➡️](docs/Queries.md)
|
|
54
57
|
|
|
55
58
|
|
|
56
59
|
### Database
|
|
@@ -70,7 +73,7 @@ The Philosophy of `nodester` is to provide a developer with a tool that can buil
|
|
|
70
73
|
|
|
71
74
|
### Goal
|
|
72
75
|
|
|
73
|
-
The goal of `nodester` is to be a robust and flexible framework that makes development in iteratations easy,
|
|
76
|
+
The goal of `nodester` is to be a robust and flexible framework that makes development in iteratations easy, while laying the foundation for seamless scalability in the future.
|
|
74
77
|
|
|
75
78
|
|
|
76
79
|
## LICENSE
|
package/lib/application/index.js
CHANGED
|
@@ -30,7 +30,7 @@ const {
|
|
|
30
30
|
const { merge } = require('../utils/objects.util');
|
|
31
31
|
|
|
32
32
|
// Arguments validator.
|
|
33
|
-
const { ensure } = require('
|
|
33
|
+
const { ensure } = require('nodester/validators/arguments');
|
|
34
34
|
|
|
35
35
|
// Console:
|
|
36
36
|
const consl = require('nodester/loggers/console');
|
|
@@ -134,6 +134,17 @@ module.exports = class NodesterApplication extends Emitter {
|
|
|
134
134
|
return this._router.isLocked;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Indicates whether app is awaiting requests.
|
|
139
|
+
*
|
|
140
|
+
* @return {Boolean} isListening
|
|
141
|
+
*
|
|
142
|
+
* @api public
|
|
143
|
+
*/
|
|
144
|
+
get isListening() {
|
|
145
|
+
return this._isListening;
|
|
146
|
+
}
|
|
147
|
+
|
|
137
148
|
// Getters\
|
|
138
149
|
|
|
139
150
|
/*
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
const { associateModels } = require('nodester/models/associate');
|
|
9
9
|
|
|
10
|
+
// Arguments validator.
|
|
11
|
+
const { ensure } = require('nodester/validators/arguments');
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
module.exports = {
|
|
12
15
|
migrate: _migrate
|
|
@@ -14,11 +17,7 @@ module.exports = {
|
|
|
14
17
|
|
|
15
18
|
async function _migrate(databaseConnection, force=false) {
|
|
16
19
|
try {
|
|
17
|
-
|
|
18
|
-
if (typeof force !== 'boolean') {
|
|
19
|
-
const err = new Error('Wrong "force" parameter; must be boolean.');
|
|
20
|
-
throw err;
|
|
21
|
-
}
|
|
20
|
+
ensure(force, 'boolean', 'force');
|
|
22
21
|
|
|
23
22
|
// Test connection.
|
|
24
23
|
await databaseConnection.authenticate();
|
|
@@ -44,7 +43,7 @@ async function _migrate(databaseConnection, force=false) {
|
|
|
44
43
|
return Promise.resolve(output);
|
|
45
44
|
}
|
|
46
45
|
catch(error) {
|
|
47
|
-
console.error('
|
|
46
|
+
console.error('Migration failed!');
|
|
48
47
|
console.error(error);
|
|
49
48
|
return Promise.reject(error);
|
|
50
49
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* /nodester
|
|
3
|
+
* MIT Licensed
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
module.exports = class NodesterError extends Error {
|
|
10
|
+
constructor(message, status) {
|
|
11
|
+
super(message);
|
|
12
|
+
|
|
13
|
+
this.name = this.constructor.name;
|
|
14
|
+
this.status = status;
|
|
15
|
+
|
|
16
|
+
Error.captureStackTrace(this, this.constructor);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -8,16 +8,19 @@
|
|
|
8
8
|
const {
|
|
9
9
|
NODESTER_QUERY_ERROR,
|
|
10
10
|
} = require('nodester/constants/ErrorCodes');
|
|
11
|
+
const {
|
|
12
|
+
NOT_ACCEPTABLE
|
|
13
|
+
} = require('nodester/http/codes');
|
|
14
|
+
|
|
15
|
+
const NodesterError = require('./NodesterError');
|
|
11
16
|
|
|
12
17
|
|
|
13
|
-
module.exports = class NodesterQueryError extends
|
|
18
|
+
module.exports = class NodesterQueryError extends NodesterError {
|
|
14
19
|
constructor(message) {
|
|
15
20
|
super(message);
|
|
16
21
|
|
|
17
|
-
this.
|
|
18
|
-
this.status = 422;
|
|
22
|
+
this.status = NOT_ACCEPTABLE;
|
|
19
23
|
|
|
20
|
-
// Remove constructor info from stack.
|
|
21
24
|
Error.captureStackTrace(this, this.constructor);
|
|
22
25
|
}
|
|
23
26
|
}
|
|
@@ -21,7 +21,8 @@ module.exports = {
|
|
|
21
21
|
|
|
22
22
|
/*
|
|
23
23
|
*
|
|
24
|
-
* @param {Object} params
|
|
24
|
+
* @param {Object} [params]
|
|
25
|
+
* @param {Object} params.query
|
|
25
26
|
*
|
|
26
27
|
* @alias getOne
|
|
27
28
|
* @api public
|
|
@@ -40,6 +41,10 @@ async function _getOne(params) {
|
|
|
40
41
|
[this.outputName.singular]: instance,
|
|
41
42
|
count: 0 + (instance !== null)
|
|
42
43
|
}
|
|
44
|
+
|
|
45
|
+
// Hook (checkout facades/mixins).
|
|
46
|
+
await this.afterGetOne(instance, params, result);
|
|
47
|
+
|
|
43
48
|
return Promise.resolve(result);
|
|
44
49
|
}
|
|
45
50
|
catch(error) {
|
|
@@ -51,7 +56,8 @@ async function _getOne(params) {
|
|
|
51
56
|
|
|
52
57
|
/*
|
|
53
58
|
*
|
|
54
|
-
* @param {Object} params
|
|
59
|
+
* @param {Object} [params]
|
|
60
|
+
* @param {Object} params.query
|
|
55
61
|
*
|
|
56
62
|
* @alias getMany
|
|
57
63
|
* @api public
|
|
@@ -70,6 +76,10 @@ async function _getMany(params) {
|
|
|
70
76
|
[this.outputName.plural]: instances,
|
|
71
77
|
count: instances.length
|
|
72
78
|
}
|
|
79
|
+
|
|
80
|
+
// Hook (checkout facades/mixins).
|
|
81
|
+
await this.afterGetMany(instances, params, result);
|
|
82
|
+
|
|
73
83
|
return Promise.resolve(result);
|
|
74
84
|
}
|
|
75
85
|
catch(error) {
|
|
@@ -81,7 +91,8 @@ async function _getMany(params) {
|
|
|
81
91
|
|
|
82
92
|
/*
|
|
83
93
|
*
|
|
84
|
-
* @param {Object} params
|
|
94
|
+
* @param {Object} [params]
|
|
95
|
+
* @param {Object} params.data
|
|
85
96
|
*
|
|
86
97
|
* @alias createOne
|
|
87
98
|
* @api public
|
|
@@ -103,7 +114,7 @@ async function _createOne(params) {
|
|
|
103
114
|
count: 0 + (instance !== null)
|
|
104
115
|
}
|
|
105
116
|
|
|
106
|
-
//
|
|
117
|
+
// Hook (checkout facades/mixins).
|
|
107
118
|
await this.afterCreateOne(instance, params, result);
|
|
108
119
|
|
|
109
120
|
return Promise.resolve(result);
|
|
@@ -117,7 +128,9 @@ async function _createOne(params) {
|
|
|
117
128
|
|
|
118
129
|
/*
|
|
119
130
|
*
|
|
120
|
-
* @param {Object} params
|
|
131
|
+
* @param {Object} [params]
|
|
132
|
+
* @param {Object} params.query
|
|
133
|
+
* @param {Object} params.data
|
|
121
134
|
*
|
|
122
135
|
* @alias updateOne
|
|
123
136
|
* @api public
|
|
@@ -141,6 +154,10 @@ async function _updateOne(params) {
|
|
|
141
154
|
[this.outputName.singular]: instance,
|
|
142
155
|
count: 0 + (instance !== null)
|
|
143
156
|
}
|
|
157
|
+
|
|
158
|
+
// Hook (checkout facades/mixins).
|
|
159
|
+
await this.afterUpdateOne(instance, params, result);
|
|
160
|
+
|
|
144
161
|
return Promise.resolve(result);
|
|
145
162
|
}
|
|
146
163
|
catch(error) {
|
|
@@ -152,7 +169,8 @@ async function _updateOne(params) {
|
|
|
152
169
|
|
|
153
170
|
/*
|
|
154
171
|
*
|
|
155
|
-
* @param {Object} params
|
|
172
|
+
* @param {Object} [params]
|
|
173
|
+
* @param {Object} params.query
|
|
156
174
|
*
|
|
157
175
|
* @alias deleteOne
|
|
158
176
|
* @api public
|
|
@@ -171,6 +189,10 @@ async function _deleteOne(params) {
|
|
|
171
189
|
success: count > 0,
|
|
172
190
|
count: count
|
|
173
191
|
};
|
|
192
|
+
|
|
193
|
+
// Hook (checkout facades/mixins).
|
|
194
|
+
await this.afterDeleteOne(null, params, result);
|
|
195
|
+
|
|
174
196
|
return Promise.resolve(result);
|
|
175
197
|
}
|
|
176
198
|
catch(error) {
|
|
@@ -18,7 +18,7 @@ const { Sequelize } = require('sequelize');
|
|
|
18
18
|
const { lowerCaseFirstLetter } = require('nodester/utils/strings');
|
|
19
19
|
|
|
20
20
|
// Arguments validator.
|
|
21
|
-
const { ensure } = require('
|
|
21
|
+
const { ensure } = require('nodester/validators/arguments');
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
module.exports = {
|
|
@@ -70,6 +70,7 @@ function _withDefaultCRUD(facade, options={}) {
|
|
|
70
70
|
},
|
|
71
71
|
writable: false
|
|
72
72
|
});
|
|
73
|
+
// Model info\
|
|
73
74
|
|
|
74
75
|
// Set name of this facade:
|
|
75
76
|
Object.defineProperty(facade, 'name', {
|
|
@@ -138,7 +139,7 @@ function _withDefaultCRUD(facade, options={}) {
|
|
|
138
139
|
facade.afterGetMany = async () => {};
|
|
139
140
|
facade.afterCreateOne = async () => {};
|
|
140
141
|
facade.afterUpdateOne = async () => {};
|
|
141
|
-
facade.
|
|
142
|
+
facade.afterDeleteOne = async () => {};
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
return facade;
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
/*
|
|
9
9
|
* REST response factory.
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
const
|
|
11
|
+
const Params = require('nodester/params');
|
|
12
|
+
const { NodesterError } = require('nodester/errors');
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
module.exports = {
|
|
@@ -26,52 +26,83 @@ module.exports = {
|
|
|
26
26
|
* }
|
|
27
27
|
* Status code is sent in header.
|
|
28
28
|
*
|
|
29
|
-
* If error is not present, error
|
|
29
|
+
* If error is not present, error must be null.
|
|
30
30
|
* If error is present, content can be null (But it's not required).
|
|
31
31
|
*
|
|
32
32
|
* @param {ServerResponse} res
|
|
33
|
-
* @param {Object} options
|
|
33
|
+
* @param {Object} [options]
|
|
34
34
|
* @param {Object} options.error
|
|
35
35
|
* @param {Object} options.content (optional)
|
|
36
36
|
* @param {Int} options.status
|
|
37
|
-
* @param {String} options.format
|
|
38
37
|
*
|
|
39
38
|
* @alias createGenericResponse
|
|
40
39
|
* @api public
|
|
41
40
|
*/
|
|
42
|
-
function _createGenericResponse(
|
|
43
|
-
res,
|
|
44
|
-
options = {
|
|
45
|
-
status: 200,
|
|
46
|
-
content: {},
|
|
47
|
-
error: null,
|
|
48
|
-
format: ResponseFormats.JSON
|
|
49
|
-
}
|
|
50
|
-
) {
|
|
41
|
+
function _createGenericResponse(res, options) {
|
|
51
42
|
try {
|
|
43
|
+
let {
|
|
44
|
+
status,
|
|
45
|
+
content,
|
|
46
|
+
error
|
|
47
|
+
} = Params(options, {
|
|
48
|
+
status: 200,
|
|
49
|
+
content: {},
|
|
50
|
+
error: null
|
|
51
|
+
});
|
|
52
|
+
|
|
52
53
|
const data = {
|
|
53
54
|
content: options?.content ?? null,
|
|
54
|
-
error:
|
|
55
|
+
error: null,
|
|
55
56
|
};
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
if (!!error) {
|
|
59
|
+
const details = {
|
|
60
|
+
message: error?.message
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
|
|
63
|
+
switch(error.name) {
|
|
64
|
+
case 'Unauthorized': {
|
|
65
|
+
statusCode = 401;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'NotFound': {
|
|
69
|
+
statusCode = 404;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case 'ValidationError': {
|
|
73
|
+
statusCode = 422;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case 'ConflictError': {
|
|
77
|
+
statusCode = 409;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case 'SequelizeUniqueConstraintError': {
|
|
81
|
+
statusCode = 409;
|
|
82
|
+
details.errors = error?.errors;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
default:
|
|
86
|
+
statusCode = status;
|
|
87
|
+
|
|
88
|
+
if (!!error?.errors) {
|
|
89
|
+
details.errors = error?.errors;
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
64
92
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
93
|
+
|
|
94
|
+
data.error = {
|
|
95
|
+
details: details,
|
|
96
|
+
code: error.name
|
|
68
97
|
}
|
|
69
98
|
}
|
|
99
|
+
|
|
100
|
+
res.status(status);
|
|
101
|
+
return res.json(data);
|
|
70
102
|
}
|
|
71
103
|
catch(error) {
|
|
72
|
-
const err = new
|
|
73
|
-
err
|
|
74
|
-
err.code = error?.code;
|
|
104
|
+
const err = new NodesterError(`Could not create generic response: ${ error.message }`);
|
|
105
|
+
Error.captureStackTrace(err, _createGenericResponse);
|
|
75
106
|
throw err;
|
|
76
107
|
}
|
|
77
108
|
}
|
|
@@ -82,8 +113,9 @@ function _createGenericResponse(
|
|
|
82
113
|
* Should be called on all successful respones.
|
|
83
114
|
*
|
|
84
115
|
* @param {ServerResponse} res
|
|
116
|
+
* @param {Object} [options]
|
|
85
117
|
* @param {Object} options.content (optional)
|
|
86
|
-
* @param {
|
|
118
|
+
* @param {Object} options.status (optional)
|
|
87
119
|
*
|
|
88
120
|
* @alias createOKResponse
|
|
89
121
|
* @api public
|
|
@@ -92,8 +124,7 @@ function _createOKResponse(res, options={}) {
|
|
|
92
124
|
|
|
93
125
|
return this.createGenericResponse(res, {
|
|
94
126
|
...options,
|
|
95
|
-
status: 200,
|
|
96
|
-
format: options?.format ?? ResponseFormats.JSON
|
|
127
|
+
status: options?.status ?? 200,
|
|
97
128
|
});
|
|
98
129
|
}
|
|
99
130
|
|
|
@@ -103,11 +134,10 @@ function _createOKResponse(res, options={}) {
|
|
|
103
134
|
* Should be called on all failed respones.
|
|
104
135
|
*
|
|
105
136
|
* @param {ServerResponse} res
|
|
106
|
-
* @param {Object} options
|
|
137
|
+
* @param {Object} [options]
|
|
107
138
|
* @param {Object} options.error
|
|
108
139
|
* @param {Object} options.content (optional)
|
|
109
140
|
* @param {Int} options.status
|
|
110
|
-
* @param {String} options.format
|
|
111
141
|
*
|
|
112
142
|
* @alias createErrorResponse
|
|
113
143
|
* @api public
|
|
@@ -117,6 +147,5 @@ function _createErrorResponse(res, options) {
|
|
|
117
147
|
return this.createGenericResponse(res, {
|
|
118
148
|
...options,
|
|
119
149
|
status: options?.status ?? 500,
|
|
120
|
-
format: options?.format ?? ResponseFormats.JSON
|
|
121
150
|
});
|
|
122
151
|
}
|
package/lib/http/codes/index.js
CHANGED
|
@@ -5,9 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
+
const {
|
|
9
|
+
HTTP_CODE_UNPROCESSABLE_ENTITY
|
|
10
|
+
} = require('nodester/http/codes');
|
|
11
|
+
|
|
8
12
|
const cookie = require('cookie');
|
|
9
13
|
const cookieSignature = require('cookie-signature');
|
|
10
14
|
|
|
15
|
+
const { createErrorResponse } = require('nodester/factories/responses/rest');
|
|
16
|
+
|
|
11
17
|
|
|
12
18
|
module.exports = function initCookiesMiddleware(options={}) {
|
|
13
19
|
const context = {
|
|
@@ -26,19 +32,12 @@ function cookiesHandle(req, res, next) {
|
|
|
26
32
|
const cookies = cookie.parse(req.headers.cookie);
|
|
27
33
|
req.cookies = cookies;
|
|
28
34
|
|
|
29
|
-
next();
|
|
35
|
+
return next();
|
|
30
36
|
}
|
|
31
37
|
catch(error) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
res.status(statusCode);
|
|
36
|
-
res.json({
|
|
37
|
-
error: {
|
|
38
|
-
message: error.message,
|
|
39
|
-
code: statusCode
|
|
40
|
-
},
|
|
41
|
-
status: statusCode
|
|
38
|
+
return createErrorResponse(res, {
|
|
39
|
+
error: error,
|
|
40
|
+
status: error.status ?? HTTP_CODE_UNPROCESSABLE_ENTITY
|
|
42
41
|
});
|
|
43
42
|
}
|
|
44
43
|
}
|
|
@@ -5,8 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
+
const {
|
|
9
|
+
HTTP_CODE_NOT_ACCEPTABLE
|
|
10
|
+
} = require('nodester/http/codes');
|
|
11
|
+
|
|
8
12
|
const { formidable } = require('formidable');
|
|
9
13
|
|
|
14
|
+
const { createErrorResponse } = require('nodester/factories/responses/rest');
|
|
15
|
+
|
|
10
16
|
|
|
11
17
|
module.exports = function initFormidableMiddleware(formidableOptions={}) {
|
|
12
18
|
const context = {
|
|
@@ -26,17 +32,12 @@ async function formidableHandle(req, res, next) {
|
|
|
26
32
|
files
|
|
27
33
|
};
|
|
28
34
|
|
|
29
|
-
next();
|
|
35
|
+
return next();
|
|
30
36
|
}
|
|
31
37
|
catch(error) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
message: error.message,
|
|
37
|
-
code: statusCode
|
|
38
|
-
},
|
|
39
|
-
status: statusCode
|
|
40
|
-
});
|
|
38
|
+
return createErrorResponse(res, {
|
|
39
|
+
error: error,
|
|
40
|
+
status: error.status ?? HTTP_CODE_NOT_ACCEPTABLE
|
|
41
|
+
})
|
|
41
42
|
}
|
|
42
43
|
}
|
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
+
const {
|
|
9
|
+
HTTP_CODE_UNPROCESSABLE_ENTITY
|
|
10
|
+
} = require('nodester/http/codes');
|
|
11
|
+
|
|
8
12
|
const QueryLexer = require('./interpreter/QueryLexer');
|
|
9
|
-
const
|
|
13
|
+
const { createErrorResponse } = require('nodester/factories/responses/rest');
|
|
10
14
|
|
|
11
15
|
|
|
12
16
|
module.exports = function initNodesterQL() {
|
|
@@ -34,10 +38,12 @@ async function nqlHandle(req, res, next) {
|
|
|
34
38
|
|
|
35
39
|
// Go on!
|
|
36
40
|
req.nquery = lexer.query;
|
|
37
|
-
next();
|
|
41
|
+
return next();
|
|
38
42
|
}
|
|
39
43
|
catch(error) {
|
|
40
|
-
res
|
|
41
|
-
|
|
44
|
+
return createErrorResponse(res, {
|
|
45
|
+
error: error,
|
|
46
|
+
status: error.status ?? HTTP_CODE_UNPROCESSABLE_ENTITY
|
|
47
|
+
});
|
|
42
48
|
}
|
|
43
49
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* /nodester
|
|
3
3
|
* MIT Licensed
|
|
4
4
|
*/
|
|
5
|
+
|
|
5
6
|
'use strict';
|
|
6
7
|
|
|
7
8
|
const debug = require('debug')('nodester:interpreter:ModelsTree');
|
|
@@ -22,15 +23,27 @@ class ModelsTreeNode {
|
|
|
22
23
|
this.skip = 0;
|
|
23
24
|
this.limit = -1; // No limit
|
|
24
25
|
|
|
25
|
-
this.
|
|
26
|
+
this._includes = opts.includes ?? [];
|
|
26
27
|
this.order = opts.order ?? 'asc';
|
|
27
28
|
this.order_by = opts.order_by ?? 'id';
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
get where() {
|
|
32
|
+
return this._where;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get functions() {
|
|
36
|
+
return this._functions;
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
get hasParent() {
|
|
31
40
|
return this.parent !== null;
|
|
32
41
|
}
|
|
33
42
|
|
|
43
|
+
get includes() {
|
|
44
|
+
return this._includes;
|
|
45
|
+
}
|
|
46
|
+
|
|
34
47
|
get includesCount() {
|
|
35
48
|
return Object.values(this.includes).length;
|
|
36
49
|
}
|
|
@@ -39,14 +52,6 @@ class ModelsTreeNode {
|
|
|
39
52
|
return this.includesCount > 0;
|
|
40
53
|
}
|
|
41
54
|
|
|
42
|
-
get where() {
|
|
43
|
-
return this._where;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
get functions() {
|
|
47
|
-
return this._functions;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
55
|
resetActiveParam() {
|
|
51
56
|
this.activeParam = null;
|
|
52
57
|
}
|
|
@@ -72,7 +77,7 @@ class ModelsTreeNode {
|
|
|
72
77
|
|
|
73
78
|
include(modelTreeNode) {
|
|
74
79
|
modelTreeNode.parent = this;
|
|
75
|
-
this.
|
|
80
|
+
this._includes.push(modelTreeNode);
|
|
76
81
|
return modelTreeNode;
|
|
77
82
|
}
|
|
78
83
|
|
|
@@ -222,6 +222,13 @@ module.exports = class QueryLexer {
|
|
|
222
222
|
const model = token;
|
|
223
223
|
tree.use(model) ?? tree.include(model);
|
|
224
224
|
|
|
225
|
+
// Last token (model) was included,
|
|
226
|
+
// now jump to root and proceed to collect next token (model).
|
|
227
|
+
tree.node.resetActiveParam();
|
|
228
|
+
tree.upToRoot();
|
|
229
|
+
|
|
230
|
+
tree.node.activeParam = 'includes';
|
|
231
|
+
|
|
225
232
|
token = '';
|
|
226
233
|
continue;
|
|
227
234
|
}
|
|
@@ -376,7 +383,7 @@ module.exports = class QueryLexer {
|
|
|
376
383
|
const param = this.parseParamFromToken(token);
|
|
377
384
|
|
|
378
385
|
if (isSubQuery === true && param === 'includes') {
|
|
379
|
-
const err = new TypeError(`'include' is forbidden inside subquery (position ${ i }). Use: 'model.
|
|
386
|
+
const err = new TypeError(`'include' is forbidden inside subquery (position ${ i }). Use: 'model.submodel' or 'model.submodel1+submodel2'.`);
|
|
380
387
|
throw err;
|
|
381
388
|
}
|
|
382
389
|
|
|
@@ -403,7 +410,7 @@ module.exports = class QueryLexer {
|
|
|
403
410
|
const err = MissingCharError(i+1, ')');
|
|
404
411
|
throw err;
|
|
405
412
|
}
|
|
406
|
-
|
|
413
|
+
|
|
407
414
|
this.setNodeParam(tree.node, token, value);
|
|
408
415
|
|
|
409
416
|
// If end of subquery:
|
package/lib/query/traverse.js
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
const BOUNDS = require('../constants/Bounds');
|
|
9
9
|
|
|
10
10
|
const { Op } = require('sequelize');
|
|
11
|
-
const
|
|
11
|
+
const { NodesterQueryError } = require('nodester/errors');
|
|
12
12
|
const httpCodes = require('nodester/http/codes');
|
|
13
13
|
|
|
14
|
-
const { ensure } = require('
|
|
14
|
+
const { ensure } = require('nodester/validators/arguments');
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
module.exports = traverse;
|
|
@@ -73,8 +73,8 @@ function traverse(queryNode, filter=null, model=null) {
|
|
|
73
73
|
// put them through Filter:
|
|
74
74
|
for (let field of filter.fields) {
|
|
75
75
|
if (fieldsAvailable.indexOf(field) === -1) {
|
|
76
|
-
const err = new
|
|
77
|
-
err
|
|
76
|
+
const err = new NodesterQueryError(`Field '${ field }' is not present in model.`);
|
|
77
|
+
Error.captureStackTrace(err, traverse);
|
|
78
78
|
throw err;
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -93,8 +93,8 @@ function traverse(queryNode, filter=null, model=null) {
|
|
|
93
93
|
|
|
94
94
|
// At least 1 field is mandatory:
|
|
95
95
|
if (newQuery.attributes.length === 0) {
|
|
96
|
-
const err = new
|
|
97
|
-
err
|
|
96
|
+
const err = new NodesterQueryError(`No fields were selected.`);
|
|
97
|
+
Error.captureStackTrace(err, traverse);
|
|
98
98
|
throw err;
|
|
99
99
|
}
|
|
100
100
|
// Fields\
|
|
@@ -119,7 +119,7 @@ function traverse(queryNode, filter=null, model=null) {
|
|
|
119
119
|
let rawSQL = '(SELECT COUNT(*) FROM ';
|
|
120
120
|
let countAttribute = '_count';
|
|
121
121
|
|
|
122
|
-
// If request to count one of includes:
|
|
122
|
+
// If request to count one of the includes:
|
|
123
123
|
if (!isForRootModel) {
|
|
124
124
|
// Check if it's available:
|
|
125
125
|
if (
|
|
@@ -129,8 +129,8 @@ function traverse(queryNode, filter=null, model=null) {
|
|
|
129
129
|
||
|
|
130
130
|
rootModelAssociations[countTarget] === undefined
|
|
131
131
|
) {
|
|
132
|
-
const err = new
|
|
133
|
-
err
|
|
132
|
+
const err = new NodesterQueryError(`Count for '${ countTarget }' is not available.`);
|
|
133
|
+
Error.captureStackTrace(err, traverse);
|
|
134
134
|
throw err;
|
|
135
135
|
}
|
|
136
136
|
|
|
@@ -261,12 +261,12 @@ function traverse(queryNode, filter=null, model=null) {
|
|
|
261
261
|
|
|
262
262
|
|
|
263
263
|
// Includes:
|
|
264
|
-
//
|
|
264
|
+
// Validate, if requested includes are available:
|
|
265
265
|
for (let include of includes) {
|
|
266
266
|
const includeName = include.model;
|
|
267
|
+
|
|
267
268
|
if (rootModelAssociations[includeName] === undefined) {
|
|
268
|
-
const err = new
|
|
269
|
-
err.status = httpCodes.NOT_ACCEPTABLE;
|
|
269
|
+
const err = new NodesterQueryError(`No include named '${ includeName }'`);
|
|
270
270
|
Error.captureStackTrace(err, traverse);
|
|
271
271
|
throw err;
|
|
272
272
|
}
|
|
@@ -300,8 +300,7 @@ function _traverseIncludes(includes, model, filter, resultQuery) {
|
|
|
300
300
|
|
|
301
301
|
// If no such association:
|
|
302
302
|
if (!association) {
|
|
303
|
-
const err = new
|
|
304
|
-
err.status = httpCodes.NOT_ACCEPTABLE;
|
|
303
|
+
const err = new NodesterQueryError(`No include named '${ includeName }'`);
|
|
305
304
|
Error.captureStackTrace(err, _traverseIncludes);
|
|
306
305
|
throw err;
|
|
307
306
|
}
|
package/lib/router/index.js
CHANGED
|
@@ -25,7 +25,7 @@ const fs = require('fs');
|
|
|
25
25
|
const commonExtensions = require('common-js-file-extensions');
|
|
26
26
|
|
|
27
27
|
// Arguments validator.
|
|
28
|
-
const { ensure } = require('
|
|
28
|
+
const { ensure } = require('nodester/validators/arguments');
|
|
29
29
|
|
|
30
30
|
// Console:
|
|
31
31
|
const consl = require('nodester/loggers/console');
|
package/lib/router/route.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
const finalhandler = require('finalhandler');
|
|
9
9
|
|
|
10
10
|
// Arguments validator.
|
|
11
|
-
const { ensure } = require('
|
|
11
|
+
const { ensure } = require('nodester/validators/arguments');
|
|
12
12
|
|
|
13
13
|
// Console:
|
|
14
14
|
const consl = require('nodester/loggers/console');
|
|
@@ -175,17 +175,17 @@ module.exports = class MiddlewaresStack {
|
|
|
175
175
|
process(req, res, next) {
|
|
176
176
|
let middlewareOffset = -1;
|
|
177
177
|
|
|
178
|
-
const _next = (...args) => {
|
|
178
|
+
const _next = async (...args) => {
|
|
179
179
|
middlewareOffset += 1;
|
|
180
180
|
const fn = this._middlewares[middlewareOffset];
|
|
181
181
|
|
|
182
182
|
try {
|
|
183
183
|
if (!fn && !!next) {
|
|
184
|
-
//
|
|
184
|
+
// Middlewares stack is finished:
|
|
185
185
|
return next.call(null, req, res, next, ...args);
|
|
186
186
|
}
|
|
187
187
|
else if (!!fn) {
|
|
188
|
-
return fn.call(null, req, res, _next, ...args);
|
|
188
|
+
return await fn.call(null, req, res, _next, ...args);
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
catch(error) {
|
package/lib/structures/Filter.js
CHANGED
|
@@ -9,7 +9,7 @@ const BOUNDS = require('../constants/Bounds');
|
|
|
9
9
|
const CLAUSES = require('../constants/Clauses');
|
|
10
10
|
|
|
11
11
|
const { isModel } = require('../utils/models');
|
|
12
|
-
const { ensure } = require('
|
|
12
|
+
const { ensure } = require('nodester/validators/arguments');
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
module.exports = class NodesterFilter {
|
|
@@ -97,7 +97,8 @@ module.exports = class NodesterFilter {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// If singular association:
|
|
100
|
-
|
|
100
|
+
// Fix of "Only HasMany associations support include.separate"
|
|
101
|
+
if ('HasMany' !== association.associationType) {
|
|
101
102
|
// Empty bounds.
|
|
102
103
|
includeFilter.noBounds = true;
|
|
103
104
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* /nodester
|
|
3
|
+
* MIT Licensed
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
toAST_ModelsTreeNode: _toAST_ModelsTreeNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function _toAST_ModelsTreeNode(node, spacing=0) {
|
|
14
|
+
let spaces = '';
|
|
15
|
+
for (let i = 0; i < spacing; i++) {
|
|
16
|
+
spaces += ' ';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let ast = `${ spaces }[TreeNode]\n`;
|
|
20
|
+
|
|
21
|
+
spaces += ' ';
|
|
22
|
+
|
|
23
|
+
ast += `${ spaces }model: ${ node.model }\n\n`;
|
|
24
|
+
|
|
25
|
+
ast += `${ spaces }fields: [\n${ node.fields.map(f => ` • ${ f },\n`) }`;
|
|
26
|
+
ast += `${ spaces }]\n\n`;
|
|
27
|
+
|
|
28
|
+
ast += `${ spaces }functions: [\n${ node.functions.map(f => ` • ${ f },\n`) }`;
|
|
29
|
+
ast += `${ spaces }]\n\n`;
|
|
30
|
+
|
|
31
|
+
ast += `${ spaces }where: ${ JSON.stringify(node.where) }\n\n`;
|
|
32
|
+
|
|
33
|
+
['skip','limit','order','order_by'].map(
|
|
34
|
+
c => ast += `${ spaces }${ c }: ${ node[c] }\n\n`
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
ast += `${ spaces }includes: [\n`
|
|
38
|
+
node.includes.map(n => ast += _toAST_ModelsTreeNode(n, spacing + 2));
|
|
39
|
+
ast += `${ spaces }]\n`;
|
|
40
|
+
|
|
41
|
+
spaces.slice(-1);
|
|
42
|
+
ast += `${ spaces }[TreeNode END]\n\n`;
|
|
43
|
+
|
|
44
|
+
return ast;
|
|
45
|
+
}
|
|
@@ -17,7 +17,7 @@ module.exports = {
|
|
|
17
17
|
* @param {String} rules
|
|
18
18
|
* @param {String} argumentName
|
|
19
19
|
*
|
|
20
|
-
* @api
|
|
20
|
+
* @api public
|
|
21
21
|
* @alias ensure
|
|
22
22
|
*/
|
|
23
23
|
function _ensure(argument, rules, argumentName) {
|
|
@@ -40,7 +40,9 @@ function _ensure(argument, rules, argumentName) {
|
|
|
40
40
|
|
|
41
41
|
if (rule === 'required') {
|
|
42
42
|
if (argument === undefined || argument === null) {
|
|
43
|
-
|
|
43
|
+
const err = new TypeError(`${ name } is required.`);
|
|
44
|
+
Error.captureStackTrace(err, _ensure);
|
|
45
|
+
throw err;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
isRequired = true;
|
|
@@ -61,7 +63,9 @@ function _ensure(argument, rules, argumentName) {
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
if (mismatchedTypesCount === types.length && argument !== undefined) {
|
|
64
|
-
|
|
66
|
+
const err = new TypeError(`${ name } must be of type ${ types.join('|') }.`);
|
|
67
|
+
Error.captureStackTrace(err, _ensure);
|
|
68
|
+
throw err;
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
return true;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodester",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "A
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "A versatile REST framework for Node.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./lib/application/index.js",
|
|
7
7
|
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
|
|
19
19
|
"./enum": "./lib/structures/Enum.js",
|
|
20
20
|
|
|
21
|
+
"./errors": "./lib/errors/index.js",
|
|
22
|
+
|
|
21
23
|
"./facades/methods": "./lib/facades/methods/index.js",
|
|
22
24
|
"./facades/mixins": "./lib/facades/mixins/index.js",
|
|
23
25
|
|
|
24
|
-
"./factories/
|
|
25
|
-
"./factories/responses/rest": "./lib/factories/responses/rest/index.js",
|
|
26
|
+
"./factories/responses/rest": "./lib/factories/responses/rest.js",
|
|
26
27
|
|
|
27
28
|
"./filter": "./lib/structures/Filter.js",
|
|
28
29
|
|
|
@@ -53,10 +54,14 @@
|
|
|
53
54
|
|
|
54
55
|
"./utils/sql": "./lib/utils/sql.util.js",
|
|
55
56
|
"./utils/strings": "./lib/utils/strings.util.js",
|
|
56
|
-
"./utils/sanitizations": "./lib/utils/sanitizations.util.js"
|
|
57
|
+
"./utils/sanitizations": "./lib/utils/sanitizations.util.js",
|
|
58
|
+
|
|
59
|
+
"./validators/arguments": "./lib/validators/arguments.js"
|
|
57
60
|
},
|
|
58
61
|
"directories": {
|
|
59
|
-
"
|
|
62
|
+
"docs": "docs",
|
|
63
|
+
"lib": "lib",
|
|
64
|
+
"tests": "tests"
|
|
60
65
|
},
|
|
61
66
|
"source": [
|
|
62
67
|
"lib"
|
|
@@ -67,7 +72,10 @@
|
|
|
67
72
|
},
|
|
68
73
|
"author": "Mark Khramko <markkhramko@gmail.com>",
|
|
69
74
|
"license": "MIT",
|
|
70
|
-
"repository":
|
|
75
|
+
"repository": {
|
|
76
|
+
"type": "git",
|
|
77
|
+
"url": "git+https://github.com/MarkKhramko/nodester.git"
|
|
78
|
+
},
|
|
71
79
|
"private": false,
|
|
72
80
|
"bugs": {
|
|
73
81
|
"url": "https://github.com/MarkKhramko/nodester/issues"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { ModelsTree } = require('../lib/middlewares/ql/sequelize/interpreter/ModelsTree');
|
|
2
|
+
const { toAST_ModelsTreeNode } = require('../lib/tools/nql.tool');
|
|
3
|
+
|
|
4
|
+
const tree = new ModelsTree();
|
|
5
|
+
tree.node.addWhere({ id: ['1000'] });
|
|
6
|
+
tree.include('comments').use('comments');
|
|
7
|
+
tree.node.order = 'desc';
|
|
8
|
+
tree.up();
|
|
9
|
+
tree.include('users');
|
|
10
|
+
tree.include('likes') && tree.use('likes');
|
|
11
|
+
tree.node.order = 'rand';
|
|
12
|
+
tree.up();
|
|
13
|
+
tree.include('reposts');
|
|
14
|
+
|
|
15
|
+
console.debug(toAST_ModelsTreeNode(tree.root));
|
|
16
|
+
|
package/tests/index.test.js
CHANGED
|
@@ -27,13 +27,13 @@ describe('nodester application', () => {
|
|
|
27
27
|
test('Application start', () => {
|
|
28
28
|
app.listen(PORT, function() {
|
|
29
29
|
expect(app.port).toBe(PORT);
|
|
30
|
-
expect(app.
|
|
31
|
-
expect(app.
|
|
30
|
+
expect(app.isLocked).toBe(true);
|
|
31
|
+
expect(app.isListening).toBe(true);
|
|
32
|
+
expect(app.middlewaresStack.length).toBe(4);
|
|
32
33
|
|
|
33
34
|
app.stop();
|
|
34
35
|
|
|
35
|
-
expect(app.
|
|
36
|
-
expect(app.router._middlewares.isLocked).toBe(false);
|
|
36
|
+
expect(app.isLocked).toBe(false);
|
|
37
37
|
expect(app.isListening).toBe(false);
|
|
38
38
|
});
|
|
39
39
|
});
|
package/tests/nql.test.js
CHANGED
|
@@ -49,6 +49,9 @@ describe('nodester Query Language', () => {
|
|
|
49
49
|
|
|
50
50
|
// Like simple.
|
|
51
51
|
'title=like(some_text)',
|
|
52
|
+
|
|
53
|
+
// Subinclude and isolated Horizontal.
|
|
54
|
+
'in=comments.user,likes',
|
|
52
55
|
];
|
|
53
56
|
|
|
54
57
|
it('query "Simple where"', () => {
|
|
@@ -262,7 +265,6 @@ describe('nodester Query Language', () => {
|
|
|
262
265
|
|
|
263
266
|
expect(result).toMatchObject(expected);
|
|
264
267
|
});
|
|
265
|
-
|
|
266
268
|
|
|
267
269
|
test('Token "Like" simple', () => {
|
|
268
270
|
const lexer = new QueryLexer( queryStrings[14] );
|
|
@@ -274,4 +276,19 @@ describe('nodester Query Language', () => {
|
|
|
274
276
|
|
|
275
277
|
expect(result).toMatchObject(expected);
|
|
276
278
|
});
|
|
279
|
+
|
|
280
|
+
it('query "Subinclude and isolated Horizontal"', () => {
|
|
281
|
+
const lexer = new QueryLexer( queryStrings[15] );
|
|
282
|
+
result = lexer.query;
|
|
283
|
+
|
|
284
|
+
const tree = new ModelsTree();
|
|
285
|
+
tree.include('comments').use('comments');
|
|
286
|
+
tree.include('user');
|
|
287
|
+
tree.up();
|
|
288
|
+
tree.include('likes');
|
|
289
|
+
const expected = tree.root.toObject();
|
|
290
|
+
|
|
291
|
+
expect(result).toMatchObject(expected);
|
|
292
|
+
});
|
|
293
|
+
|
|
277
294
|
});
|
|
File without changes
|