@adaptivestone/framework 2.15.4 → 3.0.1

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,175 @@ 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) {
209
+ errors = e.errors.map((err) => req.i18n.t(err));
210
+ }
211
+ this.logger.error(`Request validation failed: ${errors}`);
148
212
 
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
- },
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,
160
221
  });
161
222
  }
162
- req.appInfo.request = routeObject.request.cast(req.body, {
163
- 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
+ },
164
230
  });
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
231
 
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',
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
+ });
190
248
  });
191
- });
192
- });
249
+ },
250
+ );
193
251
  }
194
252
  }
195
253
 
196
- const text = [
197
- '',
198
- `Controller '${this.getConstructorName()}' registered.`,
199
- 'Middlewares:',
200
- ];
254
+ /**
255
+ * Generate text info
256
+ */
257
+ const text = ['', `Controller '${this.getConstructorName()}' registered.`];
201
258
 
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:');
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
+ }
208
274
 
209
- routesInfo.forEach((m) => {
210
- text.push(
211
- `Path:'${m.path}'. Full path: '${m.fullPath}'. Method: '${m.method}'. Callback: '${m.name}'`,
212
- );
213
- });
214
275
  text.push(`Time: ${Date.now() - time} ms`);
215
276
 
216
277
  this.logger.verbose(text.join('\n'));
217
278
 
218
- this.app.httpServer.express.use(expressPath, this.router);
219
- }
279
+ /**
280
+ * Generate documentation
281
+ */
282
+ if (!this.app.httpServer) {
283
+ const fields = [];
284
+ if (routeObjectClone.request) {
285
+ const reqFields = routeObjectClone.request.fields;
286
+ const entries = Object.entries(reqFields);
287
+ entries.forEach(([key, value]) => {
288
+ const field = {};
289
+ field.name = key;
290
+ field.type = value.type;
291
+ if (value.exclusiveTests) {
292
+ field.isRequired = value.exclusiveTests.required;
293
+ }
220
294
 
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];
295
+ if (value.fields) {
296
+ field.fields = [];
297
+ // eslint-disable-next-line no-shadow
298
+ const entries = Object.entries(value.fields);
299
+ // eslint-disable-next-line no-shadow
300
+ entries.forEach(([key, value]) => {
301
+ field.fields.push({
302
+ name: key,
303
+ type: value.type,
304
+ });
305
+ });
306
+ }
307
+ fields.push(field);
308
+ });
294
309
  }
310
+
311
+ this.app.documentation.push({
312
+ contollerName: this.getConstructorName(),
313
+ routesInfo: routesInfo.map((route) => ({
314
+ [route.fullPath]: {
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
+ },
344
+ })),
345
+ });
346
+ } else {
347
+ this.app.httpServer.express.use(expressPath, this.router);
295
348
  }
296
- if (Object.entries(errors).length === 0 && errors.constructor === Object) {
297
- return false;
298
- }
299
- return errors;
300
349
  }
301
350
 
302
351
  /**
@@ -306,24 +355,14 @@ class AbstractController extends Base {
306
355
  * Be default path apply to ANY' method, but you can preattach 'METHOD' into patch to scope patch to this METHOD
307
356
  * @example
308
357
  * return new Map([
309
- * ['/*', [PrepareAppInfo, GetUserByToken]] // for any method for this controller
358
+ * ['/*', [GetUserByToken]] // for any method for this controller
310
359
  * ['POST/', [Auth]] // for POST method
311
360
  * ['/superSecretMethod', [OnlySuperSecretUsers]] // route with ANY method
312
361
  * ['PUT/superSecretMathod', [OnlySuperSecretAdmin]] // route with PUT method
313
362
  * ]);
314
363
  */
315
364
  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;
365
+ return new Map([['/*', [GetUserByToken, Auth]]]);
327
366
  }
328
367
 
329
368
  /**
@@ -342,12 +381,6 @@ class AbstractController extends Base {
342
381
  * Get express path with inheritance of path
343
382
  */
344
383
  getExpressPath() {
345
- if (!this.constructor.isUseControllerNameForRouting) {
346
- console.warn(
347
- 'isUseControllerNameForRouting is DEPRECATED. Please use getExpressPath instead',
348
- );
349
- return '/';
350
- }
351
384
  return `/${this.getConstructorName().toLowerCase()}`.replace('//', '/');
352
385
  }
353
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
  }
@@ -0,0 +1,36 @@
1
+ import winston from 'winston';
2
+ import Server from '../server';
3
+
4
+ declare class Base {
5
+ app: Server['app'];
6
+ _realLogger: null;
7
+
8
+ constructor(app: Server['app']);
9
+
10
+ /**
11
+ * In case of logging sometimes we might need to replace name
12
+ */
13
+ getConstructorName(): string;
14
+
15
+ /**
16
+ * Optimzation to lazy load logger. It will be inited only on request
17
+ */
18
+ get logger(): winston.Logger;
19
+
20
+ /**
21
+ * Get winston loger for given label
22
+ * @param label name of logger
23
+ */
24
+ getLogger(label: string): winston.Logger;
25
+
26
+ getFilesPathWithInheritance(
27
+ internalFolder: string,
28
+ externalFolder: string,
29
+ ): Promise<string[]>;
30
+
31
+ /**
32
+ * Return logger group. Just to have all logs groupped logically
33
+ */
34
+ static get loggerGroup(): string;
35
+ }
36
+ export = Base;
package/modules/Base.js CHANGED
@@ -3,9 +3,6 @@ const fs = require('fs').promises;
3
3
  const { join } = require('path');
4
4
 
5
5
  class Base {
6
- /**
7
- * @param {import('../Server')} app //TODO change to *.d.ts as this is a Server, not app
8
- */
9
6
  constructor(app) {
10
7
  this.app = app;
11
8
  this._realLogger = null;
@@ -98,15 +95,6 @@ class Base {
98
95
  });
99
96
  }
100
97
 
101
- async loadFilesWithInheritance(internalFolder, externalFolder) {
102
- this.logger.warn(
103
- 'Method "loadFilesWithInheritance" deprecated. Please use "getFilesPathWithInheritance"',
104
- );
105
- return (
106
- await this.getFilesPathWithInheritance(internalFolder, externalFolder)
107
- ).map((file) => file.path);
108
- }
109
-
110
98
  async getFilesPathWithInheritance(internalFolder, externalFolder) {
111
99
  async function rreaddir(dir, allFiles = []) {
112
100
  const files = (await fs.readdir(dir)).map((f) => join(dir, f));