nodester 0.0.9 → 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.
Files changed (48) hide show
  1. package/Readme.md +16 -2
  2. package/lib/application/index.js +29 -8
  3. package/lib/constants/ErrorCodes.js +19 -0
  4. package/lib/constants/Operations.js +1 -1
  5. package/lib/controllers/methods/index.js +34 -10
  6. package/lib/controllers/mixins/index.js +72 -24
  7. package/lib/database/connection.js +34 -0
  8. package/lib/database/migration.js +42 -0
  9. package/lib/database/utils.js +19 -0
  10. package/lib/facades/methods/index.js +180 -0
  11. package/lib/facades/mixins/index.js +111 -0
  12. package/lib/factories/errors/CustomError.js +7 -0
  13. package/lib/factories/errors/NodesterQueryError.js +23 -0
  14. package/lib/factories/errors/index.js +10 -3
  15. package/lib/loggers/dev.js +28 -0
  16. package/lib/middlewares/formidable/index.js +37 -0
  17. package/lib/middlewares/ql/sequelize/interpreter/ModelsTree.js +26 -3
  18. package/lib/middlewares/ql/sequelize/interpreter/QueryLexer.js +67 -15
  19. package/lib/models/define.js +49 -1
  20. package/lib/models/mixins.js +76 -67
  21. package/lib/params/Params.js +10 -7
  22. package/lib/queries/Colander.js +107 -0
  23. package/lib/queries/NodesterQueryParams.js +6 -0
  24. package/lib/queries/traverse.js +381 -0
  25. package/lib/router/handlers.util.js +22 -2
  26. package/lib/router/index.js +97 -76
  27. package/lib/router/markers.js +78 -0
  28. package/lib/router/route.js +4 -4
  29. package/lib/router/routes.util.js +35 -5
  30. package/lib/router/utils.js +30 -0
  31. package/lib/stacks/MarkersStack.js +1 -1
  32. package/lib/stacks/MiddlewareStack.js +1 -1
  33. package/lib/utils/models.js +14 -0
  34. package/package.json +36 -7
  35. package/tests/nql.test.js +3 -3
  36. package/lib/_/n_controllers/Controller.js +0 -474
  37. package/lib/_/n_controllers/JWTController.js +0 -240
  38. package/lib/_/n_controllers/ServiceController.js +0 -109
  39. package/lib/_/n_controllers/WebController.js +0 -75
  40. package/lib/_facades/Facade.js +0 -388
  41. package/lib/_facades/FacadeParams.js +0 -11
  42. package/lib/_facades/ServiceFacade.js +0 -17
  43. package/lib/_facades/jwt.facade.js +0 -273
  44. package/lib/models/Extractor.js +0 -320
  45. package/lib/preprocessors/IncludesPreprocessor.js +0 -55
  46. package/lib/preprocessors/QueryPreprocessor.js +0 -64
  47. package/lib/utils/forms.util.js +0 -22
  48. /package/lib/{logger → loggers}/console.js +0 -0
@@ -1,273 +0,0 @@
1
- // Access to database.
2
- const Sequelize = require('sequelize');
3
- // JWT service.
4
- const JWT = require('nodester/services/jwt.service');
5
- // Utils:
6
- const { lowerCaseFirstLetter } = require('nodester/utils/strings.util');
7
- const { addSeconds } = require('nodester/utils/dates.util');
8
- const Params = require('nodester/facades/FacadeParams');
9
-
10
-
11
- module.exports = class JWTFacade {
12
- constructor(
13
- disabledRefreshToken,
14
- roleModels=[],
15
- accessTokenConfigs=null,
16
- refreshTokenConfigs=null
17
- ) {
18
- if (!disabledRefreshToken)
19
- throw new Erroror('"disabledRefreshToken" argument is invalid.');
20
-
21
- this.disabledRefreshToken = disabledRefreshToken;
22
-
23
- this.roleModels = {};
24
- roleModels.forEach(model => {
25
- const name = lowerCaseFirstLetter(model?.options?.name?.singular);
26
- this.roleModels[name] = model;
27
- });
28
-
29
- this.service = new JWT(accessTokenConfigs, refreshTokenConfigs);
30
- }
31
-
32
- async login(params) {
33
- try {
34
- const {
35
- email,
36
- password,
37
- role
38
- } = Params(params, {
39
- email: null,
40
- password: null,
41
- role: null
42
- });
43
-
44
- if (Object.keys(this.roleModels).indexOf(role) === -1)
45
- throw new Error();
46
-
47
- // Extract model difinition of this type.
48
- const modelDefinition = this.roleModels[role];
49
- const instance = await modelDefinition?.findOneByEmail(email);
50
-
51
- if (!instance)
52
- throw new Error();
53
-
54
- const compareResult = await instance?.comparePasswords(password);
55
-
56
- if (compareResult === false)
57
- throw new Error();
58
-
59
- const result = {
60
- role: role,
61
- [role]: instance.toJSON(),
62
- tokens: await this.issueTokens({ modelInstance: instance, modelName: role })
63
- }
64
-
65
- // Send output.
66
- return Promise.resolve(result);
67
- }
68
- catch(error) {
69
- console.error(error);
70
-
71
- // Unauthorized on any error:
72
- const err = new Error();
73
- err.name = 'Unauthorized';
74
- return Promise.reject(err);
75
- }
76
- }
77
-
78
- async issueAccessToken(params) {
79
- try {
80
- const {
81
- parsedRefreshToken,
82
- modelInstance,
83
- modelName
84
- } = Params(params, {
85
- parsedRefreshToken: null,
86
- modelInstance: null,
87
- modelName: null,
88
- });
89
-
90
- let newAccessToken = null;
91
-
92
- if (!modelName && !parsedRefreshToken.role) {
93
- const err = new Error('No "modelName" provided for JWT issue.');
94
- err.name = "ValidationError";
95
- err.status = 403;
96
- throw err;
97
- }
98
-
99
- // If parsed refresh token was provided:
100
- if (!!parsedRefreshToken) {
101
- const payload = {
102
- id: parsedRefreshToken?.id,
103
- role: parsedRefreshToken.role,
104
- };
105
- newAccessToken = await this.service.issueAccessToken(payload);
106
- }
107
- // If modelInstance was provided:
108
- else if (!!modelInstance) {
109
- const payload = {
110
- id: modelInstance?.id,
111
- role: modelName
112
- };
113
- newAccessToken = await this.service.issueAccessToken(payload);
114
- }
115
- else {
116
- const err = new Error('No "modelInstance" or "parsedRefreshToken" provided for JWT issue.');
117
- err.name = "ValidationError";
118
- err.status = 403;
119
- throw err;
120
- }
121
-
122
- // Check if issue was successful.
123
- if (!newAccessToken) {
124
- const err = new Error("Could not issue new access token.");
125
- err.status = 401;
126
- throw err;
127
- }
128
-
129
- // Send output.
130
- return Promise.resolve(newAccessToken);
131
- }
132
- catch(error) {
133
- return Promise.reject(error);
134
- }
135
- }
136
-
137
- async issueTokens(params) {
138
- try {
139
- const {
140
- modelInstance,
141
- modelName,
142
- noAccessToken,
143
- noRefreshToken
144
- } = Params(params, {
145
- modelInstance: null,
146
- modelName: null,
147
- noAccessToken: false,
148
- noRefreshToken: false
149
- });
150
-
151
- // Prepare payload container.
152
- let payload = {};
153
-
154
- if (!!modelInstance && !!modelName) {
155
- payload = {
156
- id: modelInstance?.id,
157
- role: modelName
158
- };
159
- }
160
- else {
161
- const err = new Error('No "modelInstance" and "modelName" provided for JWT issue.');
162
- err.name = "ValidationError";
163
- err.status = 403;
164
- throw err;
165
- }
166
-
167
- const accessToken = await this.service.issueAccessToken(payload);
168
- const refreshToken = await this.service.issueRefreshToken(payload);
169
-
170
- // Prepare output,
171
- const tokens = {
172
- accessToken,
173
- refreshToken
174
- };
175
-
176
- if (noAccessToken)
177
- delete tokens.accessToken;
178
-
179
- if (noRefreshToken)
180
- delete tokens.refreshToken;
181
-
182
- // Send output.
183
- return Promise.resolve(tokens);
184
- }
185
- catch(error) {
186
- return Promise.reject(error);
187
- }
188
- }
189
-
190
- async refreshAccessToken(params) {
191
- try {
192
- const {
193
- refreshToken,
194
- parsedRefreshToken
195
- } = Params(params, {
196
- refreshToken: null,
197
- parsedRefreshToken: null
198
- });
199
-
200
- // Check if token is not blocked:
201
- const isActive = await this.isRefreshTokenActive({ refreshToken });
202
- if (!isActive) {
203
- const err = new Error('Invalid Token!');
204
- err.name = 'InvalidToken';
205
- throw err;
206
- }
207
-
208
- // Issue new access token, based on refresh token:
209
- const accessToken = await this.issueAccessToken({ parsedRefreshToken });
210
-
211
- // Send output.
212
- return Promise.resolve(accessToken);
213
- }
214
- catch(error) {
215
- return Promise.reject(error);
216
- }
217
- }
218
-
219
- async isRefreshTokenActive(params) {
220
- try {
221
- const {
222
- refreshToken
223
- } = Params(params, {
224
- refreshToken: null
225
- });
226
-
227
- const foundTokens = await this.disabledRefreshToken.selectAll({ token: refreshToken });
228
-
229
- // Prepare output. Check if provided token was not disabled.
230
- const isActive = foundTokens.length === 0;
231
-
232
- // Send output.
233
- return Promise.resolve(isActive);
234
- }
235
- catch(error) {
236
- return Promise.reject(error);
237
- }
238
- }
239
-
240
- async disableRefreshToken(params) {
241
- try {
242
- const {
243
- refreshToken,
244
- parsedToken
245
- } = Params(params, {
246
- refreshToken: null,
247
- parsedToken: null
248
- });
249
-
250
- // Unwrap nessessary data.
251
- const { id, role } = parsedToken;
252
-
253
- // Find or create:
254
- const cofParams = {
255
- [`${ role }_id`]: id,
256
- token: refreshToken
257
- };
258
- const [ disabledRefreshToken, created ] = await this.disabledRefreshToken.createOrFind(cofParams);
259
-
260
- // Check result,
261
- const createdStatus = created === true || !!disabledRefreshToken;
262
-
263
- // Send output.
264
- const result = {
265
- status: createdStatus
266
- }
267
- return Promise.resolve(result);
268
- }
269
- catch(error) {
270
- return Promise.reject(error);
271
- }
272
- }
273
- }
@@ -1,320 +0,0 @@
1
- /*
2
- * ModelDataExtractor (/models/Extractor.js)
3
- * Reads data from Express's request and filters it,
4
- * based on model parameters and always-present query parameters (limit, skip, etc.).
5
- */
6
- // Constants:
7
- const { DataTypes } = require('sequelize');
8
- const IGNORED_KEYS = [
9
- // Sequilize keys:
10
- 'createdAt',
11
- 'updatedAt',
12
- 'deletedAt',
13
-
14
- 'created_at',
15
- 'updated_at',
16
- 'deleted_at',
17
- // Procedural keys.
18
- 'should_delete',
19
- ];
20
- const PROCEDURAL_KEYS = {
21
- 'should_delete': new DataTypes.BOOLEAN()
22
- };
23
- // Utils:
24
- const sntz = require('nodester/utils/sanitizations.util');
25
- const Params = require('nodester/facades/FacadeParams');
26
-
27
-
28
- module.exports = class ModelDataExtractor {
29
-
30
- constructor(
31
- modelDefinition,
32
- options,
33
- ) {
34
- if (!modelDefinition) {
35
- throw new Error('"modelDefinition" attribute is invalid.');
36
- }
37
-
38
- const {
39
- withFiles
40
- } = Params(options, {
41
- withFiles: false
42
- });
43
-
44
- this.model = modelDefinition;
45
- this.attributes = modelDefinition.tableAttributes;
46
- this.options = {
47
- withFiles: !!withFiles,
48
- fileInstanceName: 'file',
49
- filesArrayName: 'files',
50
- };
51
- }
52
-
53
- extractData(
54
- modelDefinition,
55
- parentModelDefinition=null,
56
- holder={},
57
- includes=[],
58
- skipIdValidation=false,
59
- skipValidation=false,
60
- ) {
61
- if (!modelDefinition) {
62
- const err = new Error('"modelDefinition" attribute is invalid.');
63
- err.name = 'InternalValidationError';
64
- throw err;
65
- }
66
-
67
- const data = {};
68
- const errors = {};
69
-
70
- const attributes = modelDefinition.tableAttributes;
71
-
72
- // Table attributes minus "special" keys.
73
- const keysToCheck = Object.keys(attributes)
74
- .filter(key => !IGNORED_KEYS.includes(key));
75
-
76
- keysToCheck.forEach((key) => {
77
- const {
78
- type,
79
- allowNull,
80
- defaultValue,
81
- primaryKey,
82
- references
83
- } = attributes[key];
84
-
85
- // Extract raw value from passed object.
86
- const dataValue = holder[key];
87
-
88
- if (!!primaryKey && !!skipIdValidation) {
89
- // Just put id to data object:
90
- if (!!dataValue)
91
- data[key] = dataValue;
92
-
93
- // Skip further validation.
94
- return;
95
- }
96
-
97
- // If this field references some other model:
98
- if (!!references) {
99
- const modelReferenced = references.model;
100
-
101
- // If referenced model is the same as parentModel,
102
- // skip (Sequilize will handle it):
103
- if (modelReferenced === parentModelDefinition?.tableName) {
104
- // Skip further validation.
105
- return;
106
- }
107
- }
108
-
109
- // If value is undefined, and null is allowed skip:
110
- if (dataValue === undefined && allowNull) {
111
- // Skip further validation.
112
- return;
113
- }
114
-
115
- // If value is undefined, and null is allowed,
116
- // set and skip:
117
- if (dataValue === null && allowNull) {
118
- data[key] = null;
119
- // Skip further validation.
120
- return;
121
- }
122
-
123
- if (allowNull === false && (dataValue === undefined || dataValue === null)) {
124
-
125
- // If default value can be set,
126
- // or we're allowed to skip validation,
127
- // skip:
128
- if (defaultValue !== undefined || (!primaryKey && skipValidation === true)) {
129
- // Skip further validation.
130
- return;
131
- }
132
-
133
- return errors[key] = { message: `Field "${key}" can not be null.` };
134
- }
135
-
136
- data[key] = _sanitizeValue(dataValue, type);
137
- });
138
-
139
- // Check procedural keys:
140
- for (const [ key, dataType ] of Object.entries(PROCEDURAL_KEYS)) {
141
- // Extract raw value from passed object.
142
- const dataValue = holder[key];
143
-
144
- // If value is defined:
145
- if (dataValue !== undefined) {
146
- data[key] = _sanitizeValue(dataValue, dataType);
147
- }
148
- };
149
-
150
- // If model has associations:
151
- if (Object.keys(modelDefinition?.associations)?.length > 0) {
152
-
153
- // Go through association entries:
154
- const associationEntries = Object.entries( modelDefinition.associations );
155
- associationEntries.forEach(([
156
- associationName,
157
- associationDefinition
158
- ]) => {
159
-
160
- // If data of this association is present:
161
- if (!!holder[associationName]) {
162
-
163
- const associatedModel = associationDefinition.target;
164
-
165
- const isSingleInstance = Array.isArray( holder[associationName] ) === false;
166
-
167
- // If single instance of associated model:
168
- if (isSingleInstance) {
169
- data[associationName] = this.extractData(
170
- associatedModel,
171
- modelDefinition,
172
- holder[associationName],
173
- [], // Includes
174
- skipIdValidation,
175
- skipValidation
176
- );
177
- }
178
- // If multiple instances of associated model:
179
- else {
180
- data[associationName] = holder[associationName].map((associationData) => {
181
- return this.extractData(
182
- associatedModel,
183
- modelDefinition,
184
- associationData,
185
- [], // Includes
186
- skipIdValidation,
187
- skipValidation
188
- );
189
- });
190
- }
191
- }
192
- });
193
- }
194
-
195
- // If options for "withFiles" is true,
196
- // also check files in object.
197
- if (this.options.withFiles === true) {
198
- // Check for single file & array of files.
199
- const keysToCheck = [
200
- this.options.fileInstanceName,
201
- this.options.filesArrayName,
202
- ];
203
-
204
- keysToCheck.filter(key => holder[key] !== undefined)
205
- .forEach(key => data[key] = holder[key]);
206
- }
207
-
208
- // If errors were set, throw ValidationError:
209
- if (Object.keys(errors).length > 0) {
210
- const err = new Error('');
211
- err.name = 'ValidationError';
212
- err.details = { ...errors };
213
- throw err;
214
- }
215
-
216
- return data;
217
- }
218
-
219
- extractInstanceDataFromObject(
220
- holder={},
221
- includes,
222
- options,
223
- ) {
224
- const {
225
- skipIdValidation,
226
- skipValidation
227
- } = Params(options, {
228
- skipIdValidation: false,
229
- skipValidation: false,
230
- });
231
-
232
- return this.extractData(
233
- this.model,
234
- null,
235
- holder,
236
- includes,
237
- skipIdValidation,
238
- skipValidation
239
- );
240
- }
241
-
242
- /* ! Warning !
243
- * Not finished method
244
- * Do not use!
245
- */
246
- extractArrayDataFromObject(
247
- holder={},
248
- includes,
249
- options,
250
- ) {
251
- const {
252
- skipIdValidation,
253
- skipValidation
254
- } = Params(options, {
255
- skipIdValidation: false,
256
- skipValidation: false,
257
- });
258
-
259
- const { instances } = holder;
260
-
261
- // All model instances must be in an array by key "instances".
262
- // If "instances" is not array, throw error:
263
- if (Array.isArray(instances) === false) {
264
- const err = new Error('');
265
- err.name = 'ValidationError';
266
- err.details = { message: 'Field "instances" must be an array' };
267
- throw err;
268
- }
269
-
270
- const results = instances.map((instance) =>
271
- this.extractData(
272
- this.model,
273
- null,
274
- instance,
275
- skipIdValidation,
276
- skipValidation
277
- )
278
- );
279
-
280
- return results;
281
- }
282
- }
283
-
284
- function _sanitizeValue(
285
- value,
286
- dataType,
287
- fallback=null,
288
- ) {
289
- let result = null;
290
-
291
- if (dataType instanceof DataTypes.INTEGER) {
292
- result = sntz.INT(value, { fallback });
293
- }
294
- else if (dataType instanceof DataTypes.DECIMAL) {
295
- result = sntz.NUMBER(value, { fallback });
296
- }
297
- else if (dataType instanceof DataTypes.FLOAT) {
298
- result = sntz.NUMBER(value, { fallback });
299
- }
300
- else if (dataType instanceof DataTypes.STRING) {
301
- result = sntz.STRING(value, { fallback });
302
- }
303
- else if (dataType instanceof DataTypes.TEXT) {
304
- result = sntz.STRING(value, { fallback });
305
- }
306
- else if (dataType instanceof DataTypes.ENUM) {
307
- result = sntz.STRING(value, { fallback });
308
- }
309
- else if (dataType instanceof DataTypes.JSON) {
310
- result = sntz.JSON(value, { fallback });
311
- }
312
- else if (dataType instanceof DataTypes.BOOLEAN) {
313
- result = sntz.BOOLEAN(value, { fallback });
314
- }
315
- else if (dataType instanceof DataTypes.DATE || dataType instanceof DataTypes.DATEONLY) {
316
- result = sntz.DATE(value, { fallback });
317
- }
318
-
319
- return result;
320
- }
@@ -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
- }