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