@adaptivestone/framework 2.17.0 → 3.0.0

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.
@@ -1,16 +1,15 @@
1
+ /* eslint-disable array-callback-return */
1
2
  /* eslint-disable no-restricted-syntax */
2
3
  /* eslint-disable guard-for-in */
3
4
  const express = require('express');
4
- const validator = require('validator');
5
- const cloneDeep = require('lodash/cloneDeep');
5
+ const merge = require('deepmerge');
6
6
 
7
7
  const Base = require('./Base');
8
- const PrepareAppInfo = require('../services/http/middleware/PrepareAppInfo');
9
8
  const GetUserByToken = require('../services/http/middleware/GetUserByToken');
10
9
  const Auth = require('../services/http/middleware/Auth');
11
10
 
12
11
  /**
13
- * Abstract controller. You shoul extend any controller from them.
12
+ * Abstract controller. You should extend any controller from them.
14
13
  * Place you cintroller into controller folder and it be inited in auto way.
15
14
  * By default name of route will be controller name not file name. But please name it in same ways.
16
15
  * You can overwrite base controllers byt creating controllers with tha same file name (yes file name, not class name)
@@ -25,80 +24,106 @@ class AbstractController extends Base {
25
24
  const { routes } = this;
26
25
  const expressPath = this.getExpressPath();
27
26
 
28
-
29
- const allMiddlewares = this.constructor.middleware;
30
- Object.entries(routes).forEach( ([method, methodRoutes]) => {
27
+ /**
28
+ * Grab route middleware onlo one Map
29
+ */
30
+ const routeMiddlewares = new Map();
31
+ Object.entries(routes).forEach(([method, methodRoutes]) => {
31
32
  Object.entries(methodRoutes).forEach(([route, routeParam]) => {
32
- if (routeParam.middleware) {
33
- const fullRoute = method.toUpperCase() + route
34
- if (!allMiddlewares.has(fullRoute)) {
35
- allMiddlewares.set(fullRoute, routeParam.middleware)
36
- } else {
37
- allMiddlewares.set(fullRoute, [...allMiddlewares.get(fullRoute), ...routeParam.middleware])
33
+ if (routeParam?.middleware) {
34
+ const fullRoute = method.toUpperCase() + route;
35
+
36
+ if (!routeMiddlewares.has(fullRoute)) {
37
+ routeMiddlewares.set(fullRoute, []);
38
38
  }
39
- }
40
- })
41
- })
42
39
 
43
- const middlewaresInfo = [];
44
- const routesInfo = [];
45
- let routeObjectClone = {}
40
+ routeMiddlewares.set(fullRoute, [
41
+ ...routeMiddlewares.get(fullRoute),
42
+ ...routeParam.middleware,
43
+ ]);
44
+ }
45
+ });
46
+ });
46
47
 
47
- // eslint-disable-next-line prefer-const
48
- for (let [path, middleware] of allMiddlewares) {
49
- if (!Array.isArray(middleware)) {
50
- middleware = [middleware];
51
- }
52
- for (const M of middleware) {
53
- let method = 'all';
54
- let realPath = path;
55
- if (typeof realPath !== 'string') {
56
- this.logger.error(`Path not a string ${realPath}. Please check it`);
57
- // eslint-disable-next-line no-continue
58
- continue;
48
+ /**
49
+ * Parse middlewares to be an object.
50
+ */
51
+ const parseMiddlewares = (middlewareMap) => {
52
+ const middlewaresInfo = [];
53
+ // eslint-disable-next-line prefer-const
54
+ for (let [path, middleware] of middlewareMap) {
55
+ if (!Array.isArray(middleware)) {
56
+ middleware = [middleware];
59
57
  }
60
- if (!realPath.startsWith('/')) {
61
- method = realPath.split('/')[0]?.toLowerCase();
62
- if (!method) {
63
- this.logger.error(`Method not found for ${realPath}`);
58
+ for (const M of middleware) {
59
+ let method = 'all';
60
+ let realPath = path;
61
+ if (typeof realPath !== 'string') {
62
+ this.logger.error(`Path not a string ${realPath}. Please check it`);
64
63
  // eslint-disable-next-line no-continue
65
64
  continue;
66
65
  }
67
- realPath = realPath.substring(method.length);
68
- }
69
- if (typeof this.router[method] !== 'function') {
70
- this.logger.error(
71
- `Method ${method} not exist for middleware. Please check your codebase`,
72
- );
73
- // eslint-disable-next-line no-continue
74
- continue;
75
- }
76
- const fullPath = `/${expressPath}/${realPath.toUpperCase()}`
77
- .split('//')
78
- .join('/')
79
- .split('//')
80
- .join('/');
81
- let MiddlewareFunction = M;
82
- let middlewareParams = {};
83
- if (Array.isArray(M)) {
84
- [MiddlewareFunction, middlewareParams] = M;
66
+ if (!realPath.startsWith('/')) {
67
+ method = realPath.split('/')[0]?.toLowerCase();
68
+ if (!method) {
69
+ this.logger.error(`Method not found for ${realPath}`);
70
+ // eslint-disable-next-line no-continue
71
+ continue;
72
+ }
73
+ realPath = realPath.substring(method.length);
74
+ }
75
+ if (typeof this.router[method] !== 'function') {
76
+ this.logger.error(
77
+ `Method ${method} not exist for middleware. Please check your codebase`,
78
+ );
79
+ // eslint-disable-next-line no-continue
80
+ continue;
81
+ }
82
+ const fullPath = `/${expressPath}/${realPath.toUpperCase()}`
83
+ .split('//')
84
+ .join('/')
85
+ .split('//')
86
+ .join('/');
87
+ let MiddlewareFunction = M;
88
+ let middlewareParams = {};
89
+ if (Array.isArray(M)) {
90
+ [MiddlewareFunction, middlewareParams] = M;
91
+ }
92
+ middlewaresInfo.push({
93
+ name: MiddlewareFunction.name,
94
+ method,
95
+ path: realPath,
96
+ fullPath,
97
+ params: middlewareParams,
98
+ MiddlewareFunction,
99
+ });
85
100
  }
86
- middlewaresInfo.push({
87
- name: MiddlewareFunction.name,
88
- method: method.toUpperCase(),
89
- path: realPath,
90
- fullPath,
91
- });
92
-
93
- this.router[method](
94
- realPath,
95
- new MiddlewareFunction(this.app, middlewareParams).getMiddleware(),
96
- );
97
101
  }
98
- }
102
+ return middlewaresInfo;
103
+ };
99
104
 
105
+ const routeMiddlewaresReg = parseMiddlewares(routeMiddlewares);
106
+ const middlewaresInfo = parseMiddlewares(this.constructor.middleware);
100
107
 
108
+ const routesInfo = [];
109
+ let routeObjectClone = {};
110
+
111
+ /**
112
+ * Register controller middleware
113
+ */
114
+ for (const middleware of middlewaresInfo) {
115
+ this.router[middleware.method](
116
+ middleware.path,
117
+ new middleware.MiddlewareFunction(
118
+ this.app,
119
+ middleware.params,
120
+ ).getMiddleware(),
121
+ );
122
+ }
101
123
 
124
+ /**
125
+ * Register routes itself
126
+ */
102
127
  for (const verb in routes) {
103
128
  if (typeof this.router[verb] !== 'function') {
104
129
  this.logger.error(
@@ -108,22 +133,18 @@ class AbstractController extends Base {
108
133
  continue;
109
134
  }
110
135
  for (const path in routes[verb]) {
136
+ const routeAdditionalMiddlewares = routeMiddlewaresReg.filter(
137
+ (middleware) => middleware.path === path,
138
+ );
111
139
  let routeObject = routes[verb][path];
112
- routeObjectClone = cloneDeep(routeObject);
140
+ routeObjectClone = merge({}, routeObject);
113
141
  if (Object.prototype.toString.call(routeObject) !== '[object Object]') {
114
142
  routeObject = {
115
143
  handler: routeObject,
116
144
  request: null,
117
- middleware: null
145
+ middleware: null,
118
146
  };
119
147
 
120
- if (typeof routeObject.handler === 'string') {
121
- routeObject.handler = this[routeObject];
122
- this.logger.warn(
123
- 'Using string as a controller callback deprecated. Please use function instead',
124
- );
125
- }
126
-
127
148
  if (typeof routeObject.handler !== 'function') {
128
149
  this.logger.error(
129
150
  `Can't resolve function '${
@@ -156,211 +177,175 @@ class AbstractController extends Base {
156
177
  // `Controller '${this.getConstructorName()}' register function '${fnName}' for method '${verb}' and path '${path}' Full path '${fullPath}'`,
157
178
  // );
158
179
 
159
- this.router[verb](path, async (req, res, next) => {
160
- if (routeObject.request) {
161
- if (typeof routeObject.request.validate !== 'function') {
162
- this.logger.error('request.validate should be a function');
163
- }
164
- if (typeof routeObject.request.cast !== 'function') {
165
- this.logger.error('request.cast should be a function');
166
- }
180
+ let additionalMiddlewares;
181
+
182
+ if (routeAdditionalMiddlewares.length > 0) {
183
+ additionalMiddlewares = Array.from(
184
+ routeAdditionalMiddlewares,
185
+ ({ MiddlewareFunction, params }) =>
186
+ new MiddlewareFunction(this.app, params).getMiddleware(),
187
+ );
188
+ }
167
189
 
168
- try {
169
- await routeObject.request.validate(req.body);
170
- } catch (e) {
171
- // translate it
172
- const errors = e.errors.map((err) => req.i18n.t(err));
173
- this.logger.error(`Request validation failed: ${errors}`);
174
-
175
- return res.status(400).json({
176
- errors: {
177
- [e.path]: errors,
178
- },
190
+ this.router[verb](
191
+ path,
192
+ additionalMiddlewares || [],
193
+ async (req, res, next) => {
194
+ if (routeObject.request) {
195
+ if (typeof routeObject.request.validate !== 'function') {
196
+ this.logger.error('request.validate should be a function');
197
+ }
198
+ if (typeof routeObject.request.cast !== 'function') {
199
+ this.logger.error('request.cast should be a function');
200
+ }
201
+ const bodyAndQuery = merge(req.query, req.body);
202
+
203
+ try {
204
+ await routeObject.request.validate(bodyAndQuery);
205
+ } catch (e) {
206
+ let { errors } = e;
207
+ // translate it
208
+ if (req.i18n) {
209
+ errors = e.errors.map((err) => req.i18n.t(err));
210
+ }
211
+ this.logger.error(`Request validation failed: ${errors}`);
212
+
213
+ return res.status(400).json({
214
+ errors: {
215
+ [e.path]: errors,
216
+ },
217
+ });
218
+ }
219
+ req.appInfo.request = routeObject.request.cast(bodyAndQuery, {
220
+ stripUnknown: true,
179
221
  });
180
222
  }
181
- req.appInfo.request = routeObject.request.cast(req.body, {
182
- stripUnknown: true,
223
+ req.body = new Proxy(req.body, {
224
+ get: (target, prop) => {
225
+ this.logger.warn(
226
+ 'Please not use "req.body" directly. Implement "request" and use "req.appInfo.request" ',
227
+ );
228
+ return target[prop];
229
+ },
183
230
  });
184
- }
185
- req.body = new Proxy(req.body, {
186
- get: (target, prop) => {
187
- this.logger.warn(
188
- 'Please not use "req.body" directly. Implement "request" and use "req.appInfo.request" ',
189
- );
190
- return target[prop];
191
- },
192
- });
193
231
 
194
- if (routeObject.handler.constructor.name !== 'AsyncFunction') {
195
- const error =
196
- "Handler should be AsyncFunction. Perhabs you miss 'async' of function declaration?";
197
- this.logger.error(error);
198
- return res.status(500).json({
199
- succes: false,
200
- message: 'Platform error. Please check later or contact support',
201
- });
202
- }
203
- return routeObject.handler.call(this, req, res, next).catch((e) => {
204
- this.logger.error(e.message);
205
- console.error(e);
206
- return res.status(500).json({
207
- succes: false,
208
- message: 'Platform error. Please check later or contact support',
232
+ if (routeObject.handler.constructor.name !== 'AsyncFunction') {
233
+ const error =
234
+ "Handler should be AsyncFunction. Perhabs you miss 'async' of function declaration?";
235
+ this.logger.error(error);
236
+ return res.status(500).json({
237
+ message:
238
+ 'Platform error. Please check later or contact support',
239
+ });
240
+ }
241
+ return routeObject.handler.call(this, req, res, next).catch((e) => {
242
+ this.logger.error(e.message);
243
+ console.error(e);
244
+ return res.status(500).json({
245
+ message:
246
+ 'Platform error. Please check later or contact support',
247
+ });
209
248
  });
210
- });
211
- });
249
+ },
250
+ );
212
251
  }
213
252
  }
214
253
 
215
- const text = [
216
- '',
217
- `Controller '${this.getConstructorName()}' registered.`,
218
- 'Middlewares:',
219
- ];
220
-
221
- middlewaresInfo.forEach((m) => {
222
- text.push(
223
- `Path:'${m.path}'. Full path: '${m.fullPath}'. Method: '${m.method}'. Function: '${m.name}'`,
224
- );
225
- });
226
- text.push('Callbacks:');
254
+ /**
255
+ * Generate text info
256
+ */
257
+ const text = ['', `Controller '${this.getConstructorName()}' registered.`];
258
+
259
+ const reports = {
260
+ 'Middlewares:': middlewaresInfo,
261
+ 'Route middlewares:': routeMiddlewaresReg,
262
+ 'Callbacks:': routesInfo,
263
+ };
264
+ for (const key in reports) {
265
+ text.push(`${key}`);
266
+ for (const item of reports[key]) {
267
+ text.push(
268
+ `Path:'${item.path}'. Full path: '${
269
+ item.fullPath
270
+ }'. Method: '${item.method.toUpperCase()}'. Function: '${item.name}'`,
271
+ );
272
+ }
273
+ }
227
274
 
228
- routesInfo.forEach((m) => {
229
- text.push(
230
- `Path:'${m.path}'. Full path: '${m.fullPath}'. Method: '${m.method}'. Callback: '${m.name}'`,
231
- );
232
- });
233
275
  text.push(`Time: ${Date.now() - time} ms`);
234
276
 
235
277
  this.logger.verbose(text.join('\n'));
236
- if (!this.app.httpServer) {
237
278
 
238
- const fields = []
279
+ /**
280
+ * Generate documentation
281
+ */
282
+ if (!this.app.httpServer) {
283
+ const fields = [];
239
284
  if (routeObjectClone.request) {
240
- const reqFields = routeObjectClone.request.fields
285
+ const reqFields = routeObjectClone.request.fields;
241
286
  const entries = Object.entries(reqFields);
242
287
  entries.forEach(([key, value]) => {
243
- const field = {}
244
- field.name = key
245
- field.type = value.type
246
- if (value.exclusiveTests) {
247
- field.isRequired = value.exclusiveTests.required
248
- }
249
-
250
- if (value.fields) {
251
- field.fields = []
288
+ const field = {};
289
+ field.name = key;
290
+ field.type = value.type;
291
+ if (value.exclusiveTests) {
292
+ field.isRequired = value.exclusiveTests.required;
293
+ }
294
+
295
+ if (value.fields) {
296
+ field.fields = [];
297
+ // eslint-disable-next-line no-shadow
252
298
  const entries = Object.entries(value.fields);
299
+ // eslint-disable-next-line no-shadow
253
300
  entries.forEach(([key, value]) => {
254
301
  field.fields.push({
255
302
  name: key,
256
- type: value.type
257
- })
258
- }
259
- )
303
+ type: value.type,
304
+ });
305
+ });
260
306
  }
261
- fields.push(field)
262
- })
307
+ fields.push(field);
308
+ });
263
309
  }
264
310
 
265
311
  this.app.documentation.push({
266
312
  contollerName: this.getConstructorName(),
267
- routesInfo: routesInfo.map( route => ({
313
+ routesInfo: routesInfo.map((route) => ({
268
314
  [route.fullPath]: {
269
- method: route.method,
270
- name: route.name,
271
- fields,
272
- middlewares: [...new Set(middlewaresInfo.map( middleware => middleware.name))]
273
- }
315
+ method: route.method,
316
+ name: route.name,
317
+ fields,
318
+ routeMiddlewares: routeMiddlewaresReg
319
+ // eslint-disable-next-line consistent-return
320
+ .map((middleware) => {
321
+ if (
322
+ route.fullPath.toUpperCase() ===
323
+ middleware.fullPath.toUpperCase()
324
+ ) {
325
+ return {
326
+ name: middleware.name,
327
+ params: middleware.params,
328
+ };
329
+ }
330
+ })
331
+ .filter(Boolean),
332
+ controllerMiddlewares: [
333
+ ...new Set(
334
+ middlewaresInfo
335
+ .filter(
336
+ (middleware) =>
337
+ middleware.fullPath.toUpperCase() ===
338
+ route.fullPath.toUpperCase(),
339
+ )
340
+ .map(({ name, params }) => ({ name, params })),
341
+ ),
342
+ ],
343
+ },
274
344
  })),
275
- })
276
- }
277
- else {
345
+ });
346
+ } else {
278
347
  this.app.httpServer.express.use(expressPath, this.router);
279
348
  }
280
-
281
- }
282
-
283
-
284
-
285
- /**
286
- * Internal validation method for params validation.
287
- * You can pass own function or use validator.js functions
288
- * From own function you can return a bool then will be treater as rule pass or not. At that case error message will be used from default error. But you also can provide error as output. Where only one arrya element will be an error message
289
- * @param {object} obj object with params to validate
290
- * @param {object} rules validation rules. rule name should match parameter name
291
- * @deprecated
292
- * @example
293
- * // We can pass own function
294
- * validate({
295
- * someKey:10
296
- * },{
297
- * 'someKey':[
298
- * (val)=>val>10,
299
- * 'Error message'
300
- * ]
301
- * })
302
- * @example
303
- * // We can pass function to validator.js
304
- * validate({
305
- * someKey: 'test_at_test.com'
306
- * },{
307
- * 'someKey':[
308
- * 'isEmail',
309
- * 'Please provide valid email'
310
- * ]
311
- * })
312
- * @example
313
- * // We can pass function to validator.js with params
314
- * validate({
315
- * someKey: 'test_at_test.com'
316
- * },{
317
- * 'someKey':[
318
- * ['isEmail',{'require_tld':false}],
319
- * 'Please provide valid email'
320
- * ]
321
- * })
322
- */
323
- validate(obj, rules) {
324
- this.logger.warn(
325
- 'Validate deprecated. Please do not use it. Will be revomed it future release',
326
- );
327
- const errors = {};
328
- for (const name in rules) {
329
- let validationResult = false;
330
- if (typeof rules[name][0] === 'function') {
331
- validationResult = rules[name][0](obj[name]);
332
- if (
333
- Object.prototype.toString.call(validationResult) === '[object Array]'
334
- ) {
335
- [errors[name]] = validationResult;
336
- validationResult = false;
337
- }
338
- } else if (typeof validator[rules[name][0]] === 'function') {
339
- // use from validator then
340
- validationResult = validator[rules[name][0]](obj[name]);
341
- } else if (
342
- Object.prototype.toString.call(rules[name][0]) === '[object Array]' &&
343
- typeof validator[rules[name][0][0]] === 'function'
344
- ) {
345
- // use from validator then
346
- validationResult = validator[rules[name][0][0]](
347
- `${obj[name]}`,
348
- rules[name][0][1],
349
- );
350
- } else {
351
- this.logger.warn(
352
- `No rule found for ${name}. Swith to existing checking`,
353
- );
354
- validationResult = !!obj[name];
355
- }
356
- if (!validationResult && !errors[name]) {
357
- [, errors[name]] = rules[name];
358
- }
359
- }
360
- if (Object.entries(errors).length === 0 && errors.constructor === Object) {
361
- return false;
362
- }
363
- return errors;
364
349
  }
365
350
 
366
351
  /**
@@ -370,24 +355,14 @@ class AbstractController extends Base {
370
355
  * Be default path apply to ANY' method, but you can preattach 'METHOD' into patch to scope patch to this METHOD
371
356
  * @example
372
357
  * return new Map([
373
- * ['/*', [PrepareAppInfo, GetUserByToken]] // for any method for this controller
358
+ * ['/*', [GetUserByToken]] // for any method for this controller
374
359
  * ['POST/', [Auth]] // for POST method
375
360
  * ['/superSecretMethod', [OnlySuperSecretUsers]] // route with ANY method
376
361
  * ['PUT/superSecretMathod', [OnlySuperSecretAdmin]] // route with PUT method
377
362
  * ]);
378
363
  */
379
364
  static get middleware() {
380
- return new Map([['/*', [PrepareAppInfo, GetUserByToken, Auth]]]);
381
- }
382
-
383
- /**
384
- * Part of abstract contorller.
385
- * When you do not need controller name to append in route then return false here.
386
- * Useful for home(root) controllers
387
- * @deprecated please use getExpressPath instead
388
- */
389
- static get isUseControllerNameForRouting() {
390
- return true;
365
+ return new Map([['/*', [GetUserByToken, Auth]]]);
391
366
  }
392
367
 
393
368
  /**
@@ -406,12 +381,6 @@ class AbstractController extends Base {
406
381
  * Get express path with inheritance of path
407
382
  */
408
383
  getExpressPath() {
409
- if (!this.constructor.isUseControllerNameForRouting) {
410
- console.warn(
411
- 'isUseControllerNameForRouting is DEPRECATED. Please use getExpressPath instead',
412
- );
413
- return '/';
414
- }
415
384
  return `/${this.getConstructorName().toLowerCase()}`.replace('//', '/');
416
385
  }
417
386
 
@@ -20,28 +20,21 @@ class AbstractModel extends Base {
20
20
  );
21
21
  if (!mongoose.connection.readyState) {
22
22
  // do not connect on test
23
- mongoose
24
- .connect(this.app.getConfig('mongo').connectionString, {
25
- useNewUrlParser: true,
26
- useCreateIndex: true,
27
- useUnifiedTopology: true,
28
- useFindAndModify: false,
29
- })
30
- .then(
31
- () => {
32
- this.logger.info('Mongo connection success');
33
- this.app.events.on('die', async () => {
34
- for (const c of mongoose.connections) {
35
- c.close(true);
36
- }
37
- // await mongoose.disconnect(); // TODO it have problems with replica-set
38
- });
39
- callback();
40
- },
41
- (error) => {
42
- this.logger.error("Can't install mongodb connection", error);
43
- },
44
- );
23
+ mongoose.connect(this.app.getConfig('mongo').connectionString, {}).then(
24
+ () => {
25
+ this.logger.info('Mongo connection success');
26
+ this.app.events.on('die', async () => {
27
+ for (const c of mongoose.connections) {
28
+ c.close(true);
29
+ }
30
+ // await mongoose.disconnect(); // TODO it have problems with replica-set
31
+ });
32
+ callback();
33
+ },
34
+ (error) => {
35
+ this.logger.error("Can't install mongodb connection", error);
36
+ },
37
+ );
45
38
  } else {
46
39
  callback();
47
40
  }
package/modules/Base.d.ts CHANGED
@@ -23,11 +23,6 @@ declare class Base {
23
23
  */
24
24
  getLogger(label: string): winston.Logger;
25
25
 
26
- loadFilesWithInheritance(
27
- internalFolder: string,
28
- externalFolder: string,
29
- ): Promise<string[]>;
30
-
31
26
  getFilesPathWithInheritance(
32
27
  internalFolder: string,
33
28
  externalFolder: string,
package/modules/Base.js CHANGED
@@ -95,15 +95,6 @@ class Base {
95
95
  });
96
96
  }
97
97
 
98
- async loadFilesWithInheritance(internalFolder, externalFolder) {
99
- this.logger.warn(
100
- 'Method "loadFilesWithInheritance" deprecated. Please use "getFilesPathWithInheritance"',
101
- );
102
- return (
103
- await this.getFilesPathWithInheritance(internalFolder, externalFolder)
104
- ).map((file) => file.path);
105
- }
106
-
107
98
  async getFilesPathWithInheritance(internalFolder, externalFolder) {
108
99
  async function rreaddir(dir, allFiles = []) {
109
100
  const files = (await fs.readdir(dir)).map((f) => join(dir, f));