@adaptivestone/framework 3.4.3 → 4.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.
- package/CHANGELOG.md +30 -0
- package/LICENCE +21 -0
- package/cluster.js +3 -3
- package/commands/CreateUser.js +27 -0
- package/commands/Documentation.js +1 -1
- package/commands/GetOpenApiJson.js +53 -23
- package/commands/migration/Create.js +2 -2
- package/config/auth.js +1 -1
- package/config/i18n.js +4 -3
- package/config/mail.js +5 -1
- package/controllers/Home.js +2 -2
- package/controllers/Home.test.js +11 -0
- package/controllers/index.js +15 -15
- package/folderConfig.js +1 -1
- package/helpers/yup.js +24 -0
- package/index.js +8 -0
- package/models/User.js +38 -27
- package/models/User.test.js +68 -18
- package/modules/AbstractController.js +144 -208
- package/modules/AbstractModel.js +2 -1
- package/modules/Base.js +3 -2
- package/modules/BaseCli.js +6 -2
- package/package.json +18 -14
- package/server.d.ts +1 -1
- package/server.js +25 -8
- package/services/cache/Cache.d.ts +3 -3
- package/services/cache/Cache.js +17 -3
- package/services/documentation/DocumentationGenerator.js +171 -0
- package/services/http/HttpServer.js +16 -96
- package/services/http/middleware/AbstractMiddleware.js +20 -0
- package/services/http/middleware/GetUserByToken.js +4 -0
- package/services/http/middleware/I18n.js +119 -0
- package/services/http/middleware/I18n.test.js +77 -0
- package/services/http/middleware/Pagination.js +56 -0
- package/services/http/middleware/PrepareAppInfo.test.js +22 -0
- package/services/http/middleware/{Middlewares.test.js → RateLimiter.test.js} +1 -1
- package/services/http/middleware/RequestLogger.js +22 -0
- package/services/http/middleware/RequestParser.js +36 -0
- package/services/messaging/email/index.js +141 -41
- package/services/messaging/email/resources/.gitkeep +1 -0
- package/services/validate/ValidateService.js +161 -0
- package/services/validate/ValidateService.test.js +105 -0
- package/services/validate/drivers/AbstractValidator.js +37 -0
- package/services/validate/drivers/CustomValidator.js +52 -0
- package/services/validate/drivers/YupValidator.js +103 -0
- package/tests/setup.js +2 -0
- package/services/messaging/email/templates/emptyTemplate/style.less +0 -0
- package/services/messaging/email/templates/password/html.handlebars +0 -13
- package/services/messaging/email/templates/password/style.less +0 -0
- package/services/messaging/email/templates/password/subject.handlebars +0 -1
- package/services/messaging/email/templates/password/text.handlebars +0 -1
- package/services/messaging/email/templates/verification/style.less +0 -0
|
@@ -11,7 +11,7 @@ declare class Cache extends Base {
|
|
|
11
11
|
* Function return new key with added namespace
|
|
12
12
|
* @param key key to add namespace
|
|
13
13
|
*/
|
|
14
|
-
getKeyWithNameSpace(key:
|
|
14
|
+
getKeyWithNameSpace(key: string): string;
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Get value from cache. Set and get if not eists
|
|
@@ -20,7 +20,7 @@ declare class Cache extends Base {
|
|
|
20
20
|
* @param storeTime how long we should store value on cache
|
|
21
21
|
*/
|
|
22
22
|
getSetValue(
|
|
23
|
-
key:
|
|
23
|
+
key: string,
|
|
24
24
|
onNotFound: () => Promise<any>,
|
|
25
25
|
storeTime: number,
|
|
26
26
|
): Promise<any>;
|
|
@@ -29,7 +29,7 @@ declare class Cache extends Base {
|
|
|
29
29
|
* Remove key from cache
|
|
30
30
|
* @param key key to remove
|
|
31
31
|
*/
|
|
32
|
-
removeKey(key:
|
|
32
|
+
removeKey(key: string): Promise<number>;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export = Cache;
|
package/services/cache/Cache.js
CHANGED
|
@@ -66,13 +66,27 @@ class Cache extends Base {
|
|
|
66
66
|
return Promise.reject(e);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
this.redisClient.setEx(
|
|
69
|
+
this.redisClient.setEx(
|
|
70
|
+
key,
|
|
71
|
+
storeTime,
|
|
72
|
+
JSON.stringify(result, (jsonkey, value) =>
|
|
73
|
+
typeof value === 'bigint' ? `${value}n` : value,
|
|
74
|
+
),
|
|
75
|
+
);
|
|
70
76
|
} else {
|
|
71
77
|
this.logger.verbose(
|
|
72
|
-
`getSetValueFromCache FROM CACHE key ${key}, value ${result
|
|
78
|
+
`getSetValueFromCache FROM CACHE key ${key}, value ${result.substring(
|
|
79
|
+
0,
|
|
80
|
+
100,
|
|
81
|
+
)}`,
|
|
73
82
|
);
|
|
74
83
|
try {
|
|
75
|
-
result = JSON.parse(result)
|
|
84
|
+
result = JSON.parse(result, (jsonkey, value) => {
|
|
85
|
+
if (typeof value === 'string' && /^\d+n$/.test(value)) {
|
|
86
|
+
return BigInt(value.slice(0, value.length - 1));
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
});
|
|
76
90
|
} catch (e) {
|
|
77
91
|
this.logger.warn(
|
|
78
92
|
'Not able to parse json from redis cache. That can be a normal in case you store string here',
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const Base = require('../../modules/Base');
|
|
2
|
+
const ValidateService = require('../validate/ValidateService');
|
|
3
|
+
|
|
4
|
+
class DocumentationGenerator extends Base {
|
|
5
|
+
static processingFields(fieldsByRoute) {
|
|
6
|
+
const fields = [];
|
|
7
|
+
if (!fieldsByRoute) {
|
|
8
|
+
return fields;
|
|
9
|
+
}
|
|
10
|
+
const entries = Object.entries(fieldsByRoute);
|
|
11
|
+
entries.forEach(([key, value]) => {
|
|
12
|
+
const field = {};
|
|
13
|
+
field.name = key;
|
|
14
|
+
field.type = value.type;
|
|
15
|
+
if (value.exclusiveTests) {
|
|
16
|
+
field.required = value.exclusiveTests.required;
|
|
17
|
+
}
|
|
18
|
+
if (value?.innerType) {
|
|
19
|
+
field.innerType = value?.innerType?.type;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (value.fields) {
|
|
23
|
+
field.fields = [];
|
|
24
|
+
// eslint-disable-next-line no-shadow
|
|
25
|
+
const entries = Object.entries(value.fields);
|
|
26
|
+
// eslint-disable-next-line no-shadow
|
|
27
|
+
entries.forEach(([key, value]) => {
|
|
28
|
+
field.fields.push({
|
|
29
|
+
name: key,
|
|
30
|
+
type: value.type,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
fields.push(field);
|
|
35
|
+
});
|
|
36
|
+
return fields;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static selectUniqueFields(fields) {
|
|
40
|
+
return Array.from(
|
|
41
|
+
new Map(fields.map((item) => [item.name, item])).values(),
|
|
42
|
+
).reduce((uniqueArray, item) => {
|
|
43
|
+
const existingItem = uniqueArray.find(
|
|
44
|
+
(uniqueItem) => uniqueItem.name === item.name,
|
|
45
|
+
);
|
|
46
|
+
if (!existingItem) {
|
|
47
|
+
uniqueArray.push(item);
|
|
48
|
+
} else if (item.required) {
|
|
49
|
+
existingItem.required = true;
|
|
50
|
+
}
|
|
51
|
+
return uniqueArray;
|
|
52
|
+
}, []);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static groupFieldsFromSchemas(schemas) {
|
|
56
|
+
const result = [];
|
|
57
|
+
schemas.forEach((schema) => {
|
|
58
|
+
const convertedSchema = new ValidateService(this.app, schema).validator;
|
|
59
|
+
|
|
60
|
+
for (const [key, value] of Object.entries(
|
|
61
|
+
convertedSchema?.fieldsInJsonFormat,
|
|
62
|
+
)) {
|
|
63
|
+
result.push({
|
|
64
|
+
name: key,
|
|
65
|
+
type: value.type,
|
|
66
|
+
required: value.required,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static convertDataToDocumentationElement(
|
|
75
|
+
controllerName,
|
|
76
|
+
routesInfo,
|
|
77
|
+
middlewaresInfo,
|
|
78
|
+
routeMiddlewaresReg,
|
|
79
|
+
) {
|
|
80
|
+
return {
|
|
81
|
+
contollerName: controllerName,
|
|
82
|
+
routesInfo: routesInfo.map((route) => {
|
|
83
|
+
const middlewareQueryParams = ValidateService.getMiddlewareParams(
|
|
84
|
+
middlewaresInfo,
|
|
85
|
+
routeMiddlewaresReg,
|
|
86
|
+
{
|
|
87
|
+
method: route.method.toLowerCase(),
|
|
88
|
+
path: route.fullPath,
|
|
89
|
+
},
|
|
90
|
+
).query;
|
|
91
|
+
|
|
92
|
+
const middlewareRequestParams = ValidateService.getMiddlewareParams(
|
|
93
|
+
middlewaresInfo,
|
|
94
|
+
routeMiddlewaresReg,
|
|
95
|
+
{
|
|
96
|
+
method: route.method.toLowerCase(),
|
|
97
|
+
path: route.fullPath,
|
|
98
|
+
},
|
|
99
|
+
).request;
|
|
100
|
+
|
|
101
|
+
const queryParams = this.groupFieldsFromSchemas(middlewareQueryParams);
|
|
102
|
+
|
|
103
|
+
const requestParams = this.groupFieldsFromSchemas(
|
|
104
|
+
middlewareRequestParams,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
[route.fullPath]: {
|
|
109
|
+
method: route.method,
|
|
110
|
+
name: route.name,
|
|
111
|
+
description: route?.description,
|
|
112
|
+
fields: this.selectUniqueFields([
|
|
113
|
+
...this.processingFields(route.fields),
|
|
114
|
+
...requestParams,
|
|
115
|
+
]),
|
|
116
|
+
queryFields: this.selectUniqueFields([
|
|
117
|
+
...this.processingFields(route.queryFields),
|
|
118
|
+
...queryParams,
|
|
119
|
+
]),
|
|
120
|
+
routeMiddlewares: routeMiddlewaresReg
|
|
121
|
+
.map((middleware) => {
|
|
122
|
+
const routeFullPath = route.fullPath.toUpperCase();
|
|
123
|
+
const middlewareFullPath = middleware.fullPath.toUpperCase();
|
|
124
|
+
if (
|
|
125
|
+
route.method.toLowerCase() ===
|
|
126
|
+
middleware.method.toLowerCase() &&
|
|
127
|
+
(middlewareFullPath === routeFullPath ||
|
|
128
|
+
middlewareFullPath === `${routeFullPath}*`)
|
|
129
|
+
) {
|
|
130
|
+
return {
|
|
131
|
+
name: middleware.name,
|
|
132
|
+
params: middleware.params,
|
|
133
|
+
authParams: middleware.authParams,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
})
|
|
138
|
+
.filter(Boolean),
|
|
139
|
+
controllerMiddlewares: [
|
|
140
|
+
...new Set(
|
|
141
|
+
middlewaresInfo
|
|
142
|
+
.filter((middleware) => {
|
|
143
|
+
const routeFullPath = route.fullPath.toUpperCase();
|
|
144
|
+
const middlewareFullPath =
|
|
145
|
+
middleware.fullPath.toUpperCase();
|
|
146
|
+
const middlewareFullPathWithSliced = middleware.fullPath
|
|
147
|
+
.toUpperCase()
|
|
148
|
+
.slice(0, -1);
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
middlewareFullPath === routeFullPath ||
|
|
152
|
+
middlewareFullPath === `${routeFullPath}*` ||
|
|
153
|
+
routeFullPath?.indexOf(middlewareFullPathWithSliced) !==
|
|
154
|
+
-1
|
|
155
|
+
);
|
|
156
|
+
})
|
|
157
|
+
.map(({ name, params, authParams }) => ({
|
|
158
|
+
name,
|
|
159
|
+
params,
|
|
160
|
+
authParams,
|
|
161
|
+
})),
|
|
162
|
+
),
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = DocumentationGenerator;
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
+
const http = require('node:http');
|
|
2
|
+
const path = require('node:path');
|
|
1
3
|
const express = require('express');
|
|
2
|
-
const http = require('http');
|
|
3
|
-
const path = require('path');
|
|
4
4
|
const cors = require('cors');
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const BackendFS = require('i18next-fs-backend');
|
|
9
|
-
const Backend = require('i18next-chained-backend');
|
|
6
|
+
const RequestLoggerMiddleware = require('./middleware/RequestLogger');
|
|
7
|
+
const I18nMiddleware = require('./middleware/I18n');
|
|
10
8
|
const PrepareAppInfoMiddleware = require('./middleware/PrepareAppInfo');
|
|
9
|
+
const RequestParserMiddleware = require('./middleware/RequestParser');
|
|
11
10
|
|
|
12
11
|
const Base = require('../../modules/Base');
|
|
13
12
|
|
|
@@ -15,25 +14,19 @@ const Base = require('../../modules/Base');
|
|
|
15
14
|
* HTTP server based on Express
|
|
16
15
|
*/
|
|
17
16
|
class HttpServer extends Base {
|
|
18
|
-
constructor(app
|
|
17
|
+
constructor(app) {
|
|
19
18
|
super(app);
|
|
20
19
|
this.express = express();
|
|
20
|
+
this.express.disable('x-powered-by');
|
|
21
21
|
this.express.set('views', [
|
|
22
|
-
|
|
22
|
+
this.app.foldersConfig.views,
|
|
23
23
|
path.join(__dirname, '../../views'),
|
|
24
24
|
]);
|
|
25
25
|
this.express.set('view engine', 'pug');
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
res.on('finish', () => {
|
|
31
|
-
const duration = Date.now() - startTime;
|
|
32
|
-
this.logger.info(`Finished ${text}. Duration ${duration} ms`);
|
|
33
|
-
});
|
|
34
|
-
next();
|
|
35
|
-
});
|
|
36
|
-
this.enableI18N(folderConfig);
|
|
26
|
+
|
|
27
|
+
this.express.use(new PrepareAppInfoMiddleware(this.app).getMiddleware());
|
|
28
|
+
this.express.use(new RequestLoggerMiddleware(this.app).getMiddleware());
|
|
29
|
+
this.express.use(new I18nMiddleware(this.app).getMiddleware());
|
|
37
30
|
|
|
38
31
|
const httpConfig = this.app.getConfig('http');
|
|
39
32
|
this.express.use(
|
|
@@ -41,17 +34,16 @@ class HttpServer extends Base {
|
|
|
41
34
|
origin: httpConfig.corsDomains,
|
|
42
35
|
}),
|
|
43
36
|
); // todo whitelist
|
|
44
|
-
this.express.use(express.
|
|
45
|
-
this.express.use(express.json({ limit: '50mb' }));
|
|
46
|
-
this.express.use(express.static(folderConfig.folders.public));
|
|
37
|
+
this.express.use(express.static(this.app.foldersConfig.public));
|
|
47
38
|
this.express.use(express.static('./public'));
|
|
48
39
|
|
|
49
|
-
this.express.use(new
|
|
40
|
+
this.express.use(new RequestParserMiddleware(this.app).getMiddleware());
|
|
50
41
|
|
|
51
42
|
// As exprress will check numbersof arguments
|
|
52
43
|
// eslint-disable-next-line no-unused-vars
|
|
53
44
|
this.express.use((err, req, res, next) => {
|
|
54
45
|
// error handling
|
|
46
|
+
// eslint-disable-next-line no-console
|
|
55
47
|
console.error(err.stack);
|
|
56
48
|
// TODO
|
|
57
49
|
res.status(500).send('Something broke!');
|
|
@@ -79,85 +71,13 @@ class HttpServer extends Base {
|
|
|
79
71
|
);
|
|
80
72
|
}
|
|
81
73
|
|
|
82
|
-
/**
|
|
83
|
-
* Enable support for i18n
|
|
84
|
-
* @param {object} folderConfig config
|
|
85
|
-
* @param {object} folderConfig.folders folder config
|
|
86
|
-
* @param {string} folderConfig.folders.config path to folder with config files
|
|
87
|
-
* @param {string} folderConfig.folders.models path to folder with moidels files
|
|
88
|
-
* @param {string} folderConfig.folders.controllers path to folder with controllers files
|
|
89
|
-
* @param {string} folderConfig.folders.views path to folder with view files
|
|
90
|
-
* @param {string} folderConfig.folders.public path to folder with public files
|
|
91
|
-
* @param {string} folderConfig.folders.locales path to folder with locales files
|
|
92
|
-
* @param {string} folderConfig.folders.emails path to folder with emails files
|
|
93
|
-
*/
|
|
94
|
-
enableI18N(folderConfig) {
|
|
95
|
-
const I18NConfig = this.app.getConfig('i18n');
|
|
96
|
-
if (!I18NConfig.enabled) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
const lngDetector = new i18nextMiddleware.LanguageDetector();
|
|
100
|
-
lngDetector.addDetector({
|
|
101
|
-
name: 'xLang',
|
|
102
|
-
// eslint-disable-next-line no-unused-vars
|
|
103
|
-
lookup: (req, res, options) => {
|
|
104
|
-
const lng = req.get('X-Lang');
|
|
105
|
-
if (lng) {
|
|
106
|
-
return lng;
|
|
107
|
-
}
|
|
108
|
-
return false;
|
|
109
|
-
},
|
|
110
|
-
// eslint-disable-next-line no-unused-vars
|
|
111
|
-
cacheUserLanguage: (req, res, lng, options) => {},
|
|
112
|
-
});
|
|
113
|
-
this.logger.info('Enabling i18n support');
|
|
114
|
-
i18next
|
|
115
|
-
.use(Backend)
|
|
116
|
-
.use(lngDetector)
|
|
117
|
-
.init({
|
|
118
|
-
backend: {
|
|
119
|
-
backends: [
|
|
120
|
-
BackendFS,
|
|
121
|
-
// BackendFS,
|
|
122
|
-
],
|
|
123
|
-
backendOptions: [
|
|
124
|
-
// {
|
|
125
|
-
// loadPath: __dirname + '/../../locales/{{lng}}/{{ns}}.json',
|
|
126
|
-
// addPath: __dirname + '/../../locales/{{lng}}/{{ns}}.missing.json'
|
|
127
|
-
// },
|
|
128
|
-
{
|
|
129
|
-
loadPath: `${folderConfig.folders.locales}/{{lng}}/{{ns}}.json`,
|
|
130
|
-
addPath: `${folderConfig.folders.locales}/{{lng}}/{{ns}}.missing.json`,
|
|
131
|
-
},
|
|
132
|
-
],
|
|
133
|
-
},
|
|
134
|
-
fallbackLng: I18NConfig.fallbackLng,
|
|
135
|
-
preload: I18NConfig.preload,
|
|
136
|
-
saveMissing: I18NConfig.saveMissing,
|
|
137
|
-
debug: I18NConfig.debug,
|
|
138
|
-
detection: {
|
|
139
|
-
// caches: ['cookie'],
|
|
140
|
-
order: I18NConfig.langDetectionOders || ['xLang'],
|
|
141
|
-
lookupQuerystring: I18NConfig.lookupQuerystring,
|
|
142
|
-
},
|
|
143
|
-
});
|
|
144
|
-
this.express.use(i18nextMiddleware.handle(i18next));
|
|
145
|
-
this.express.use((req, res, next) => {
|
|
146
|
-
// f ix ru-Ru, en-US, etc
|
|
147
|
-
if (res.locals.language.length !== 2) {
|
|
148
|
-
[res.locals.language] = res.locals.language.split('-');
|
|
149
|
-
}
|
|
150
|
-
next();
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
74
|
/**
|
|
155
75
|
* Add handle for 404 error
|
|
156
76
|
*/
|
|
157
77
|
add404Page() {
|
|
158
78
|
this.express.use((req, res) => {
|
|
159
79
|
// error handling
|
|
160
|
-
res.status(404).
|
|
80
|
+
res.status(404).json({ message: '404' });
|
|
161
81
|
});
|
|
162
82
|
}
|
|
163
83
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const yup = require('yup');
|
|
1
2
|
const Base = require('../../../modules/Base');
|
|
2
3
|
|
|
3
4
|
class AbstractMiddleware extends Base {
|
|
@@ -14,6 +15,25 @@ class AbstractMiddleware extends Base {
|
|
|
14
15
|
return [];
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
// eslint-disable-next-line class-methods-use-this
|
|
19
|
+
get relatedQueryParameters() {
|
|
20
|
+
// For example yup.object().shape({page: yup.number().required(),limit: yup.number()})
|
|
21
|
+
return yup.object().shape({});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// eslint-disable-next-line class-methods-use-this
|
|
25
|
+
get relatedRequestParameters() {
|
|
26
|
+
// For example yup.object().shape({page: yup.number().required(),limit: yup.number()})
|
|
27
|
+
return yup.object().shape({});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get relatedReqParameters() {
|
|
31
|
+
return {
|
|
32
|
+
request: this.relatedRequestParameters,
|
|
33
|
+
query: this.relatedQueryParameters,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
async middleware(req, res, next) {
|
|
18
38
|
this.logger.warn('Middleware is not implemented');
|
|
19
39
|
next();
|
|
@@ -17,6 +17,10 @@ class GetUserByToken extends AbstractMiddleware {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
async middleware(req, res, next) {
|
|
20
|
+
if (req.appInfo.user) {
|
|
21
|
+
this.logger.warn('You call GetUserByToken more then once');
|
|
22
|
+
return next();
|
|
23
|
+
}
|
|
20
24
|
let { token } = req.body;
|
|
21
25
|
this.logger.verbose(
|
|
22
26
|
`GetUserByToken token in BODY ${token}. Token if Authorization header ${req.get(
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const i18next = require('i18next');
|
|
2
|
+
const BackendFS = require('i18next-fs-backend');
|
|
3
|
+
const Backend = require('i18next-chained-backend');
|
|
4
|
+
|
|
5
|
+
const AbstractMiddleware = require('./AbstractMiddleware');
|
|
6
|
+
|
|
7
|
+
class I18n extends AbstractMiddleware {
|
|
8
|
+
constructor(app, params) {
|
|
9
|
+
super(app, params);
|
|
10
|
+
const I18NConfig = this.app.getConfig('i18n');
|
|
11
|
+
this.i18n = {
|
|
12
|
+
t: (text) => text,
|
|
13
|
+
language: I18NConfig.fallbackLng,
|
|
14
|
+
};
|
|
15
|
+
this.cache = {};
|
|
16
|
+
|
|
17
|
+
if (I18NConfig.enabled) {
|
|
18
|
+
this.logger.info('Enabling i18n support');
|
|
19
|
+
this.i18n = i18next;
|
|
20
|
+
i18next.use(Backend).init({
|
|
21
|
+
backend: {
|
|
22
|
+
backends: [
|
|
23
|
+
BackendFS,
|
|
24
|
+
// BackendFS,
|
|
25
|
+
],
|
|
26
|
+
backendOptions: [
|
|
27
|
+
// {
|
|
28
|
+
// loadPath: __dirname + '/../../locales/{{lng}}/{{ns}}.json',
|
|
29
|
+
// addPath: __dirname + '/../../locales/{{lng}}/{{ns}}.missing.json'
|
|
30
|
+
// },
|
|
31
|
+
{
|
|
32
|
+
loadPath: `${this.app.foldersConfig.locales}/{{lng}}/{{ns}}.json`,
|
|
33
|
+
addPath: `${this.app.foldersConfig.locales}/{{lng}}/{{ns}}.missing.json`,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
fallbackLng: I18NConfig.fallbackLng,
|
|
38
|
+
preload: I18NConfig.preload,
|
|
39
|
+
saveMissing: I18NConfig.saveMissing,
|
|
40
|
+
debug: I18NConfig.debug,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.enabled = I18NConfig.enabled;
|
|
45
|
+
this.lookupQuerystring = I18NConfig.lookupQuerystring;
|
|
46
|
+
this.supportedLngs = I18NConfig.supportedLngs;
|
|
47
|
+
this.fallbackLng = I18NConfig.fallbackLng;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static get description() {
|
|
51
|
+
return 'Provide language detection and translation';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async middleware(req, res, next) {
|
|
55
|
+
let i18n;
|
|
56
|
+
|
|
57
|
+
if (this.enabled) {
|
|
58
|
+
let lang = this.detectLang(req);
|
|
59
|
+
if (!lang || this.supportedLngs.indexOf(lang) === -1) {
|
|
60
|
+
this.logger.verbose(
|
|
61
|
+
`Language "${lang}" is not supported or not detected. Using fallback on ${this.fallbackLng}`,
|
|
62
|
+
);
|
|
63
|
+
lang = this.fallbackLng;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!this.cache[lang]) {
|
|
67
|
+
this.cache[lang] = i18next.cloneInstance({
|
|
68
|
+
initImmediate: false,
|
|
69
|
+
lng: lang,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
i18n = this.cache[lang];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!i18n) {
|
|
76
|
+
i18n = this.i18n;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
req.appInfo.i18n = i18n;
|
|
80
|
+
req.i18n = new Proxy(req.appInfo.i18n, {
|
|
81
|
+
get: (target, prop) => {
|
|
82
|
+
this.logger.warn('Please not use "req.i18n" Use "req.appInfo.i18n"');
|
|
83
|
+
return target[prop];
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return next();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
detectors = {
|
|
91
|
+
XLang: (req) => req.get('X-Lang'), // grab from header
|
|
92
|
+
query: (req) => (req.query ? req.query[this.lookupQuerystring] : false), // grab from query
|
|
93
|
+
user: (req) => req.appInfo?.user?.locale, // what if we have a user and user have a defined locale?
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
detectorOrder = ['XLang', 'query', 'user'];
|
|
97
|
+
|
|
98
|
+
detectLang(req, isUseShortCode = true) {
|
|
99
|
+
let lang = '';
|
|
100
|
+
for (const detectorName of this.detectorOrder) {
|
|
101
|
+
const lng = this.detectors[detectorName](req);
|
|
102
|
+
if (!lng) {
|
|
103
|
+
// eslint-disable-next-line no-continue
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (i18next.services.languageUtils.isSupportedCode(lng)) {
|
|
107
|
+
if (isUseShortCode) {
|
|
108
|
+
lang = i18next.services.languageUtils.getLanguagePartFromCode(lng);
|
|
109
|
+
} else {
|
|
110
|
+
lang = lng;
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return lang;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = I18n;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const I18n = require('./I18n');
|
|
2
|
+
|
|
3
|
+
describe('i18n middleware methods', () => {
|
|
4
|
+
let middleware;
|
|
5
|
+
beforeAll(() => {
|
|
6
|
+
middleware = new I18n(global.server.app);
|
|
7
|
+
});
|
|
8
|
+
it('have description fields', async () => {
|
|
9
|
+
expect.assertions(1);
|
|
10
|
+
expect(middleware.constructor.description).toBeDefined();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('detectors should works correctly', async () => {
|
|
14
|
+
expect.assertions(5);
|
|
15
|
+
const request = {
|
|
16
|
+
get: () => 'en',
|
|
17
|
+
query: {
|
|
18
|
+
[middleware.lookupQuerystring]: 'es',
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
let lang = await middleware.detectLang(request);
|
|
22
|
+
expect(lang).toBe('en');
|
|
23
|
+
|
|
24
|
+
request.appInfo = {
|
|
25
|
+
user: {
|
|
26
|
+
locale: 'be',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
lang = await middleware.detectLang(request);
|
|
30
|
+
expect(lang).toBe('en');
|
|
31
|
+
request.get = () => null;
|
|
32
|
+
lang = await middleware.detectLang(request);
|
|
33
|
+
expect(lang).toBe('es');
|
|
34
|
+
|
|
35
|
+
delete request.query;
|
|
36
|
+
lang = await middleware.detectLang(request);
|
|
37
|
+
expect(lang).toBe('be');
|
|
38
|
+
|
|
39
|
+
request.query = {
|
|
40
|
+
[middleware.lookupQuerystring]: 'en-GB',
|
|
41
|
+
};
|
|
42
|
+
lang = await middleware.detectLang(request);
|
|
43
|
+
expect(lang).toBe('en');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('middleware that works', async () => {
|
|
47
|
+
expect.assertions(4);
|
|
48
|
+
const nextFunction = jest.fn(() => {});
|
|
49
|
+
const req = {
|
|
50
|
+
get: () => 'en',
|
|
51
|
+
appInfo: {},
|
|
52
|
+
};
|
|
53
|
+
await middleware.middleware(req, {}, nextFunction);
|
|
54
|
+
expect(nextFunction).toHaveBeenCalledWith();
|
|
55
|
+
expect(req.appInfo.i18n).toBeDefined();
|
|
56
|
+
expect(req.appInfo.i18n.t('aaaaa')).toBe('aaaaa');
|
|
57
|
+
expect(req.i18n.t('aaaaa')).toBe('aaaaa'); // proxy test
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('middleware disabled', async () => {
|
|
61
|
+
expect.assertions(4);
|
|
62
|
+
global.server.app.updateConfig('i18n', { enabled: false });
|
|
63
|
+
middleware = new I18n(global.server.app);
|
|
64
|
+
|
|
65
|
+
const nextFunction = jest.fn(() => {});
|
|
66
|
+
const req = {
|
|
67
|
+
get: () => 'en',
|
|
68
|
+
appInfo: {},
|
|
69
|
+
};
|
|
70
|
+
await middleware.middleware(req, {}, nextFunction);
|
|
71
|
+
expect(nextFunction).toHaveBeenCalledWith();
|
|
72
|
+
expect(req.appInfo.i18n).toBeDefined();
|
|
73
|
+
expect(req.appInfo.i18n.t('aaaaa')).toBe('aaaaa');
|
|
74
|
+
expect(req.i18n.t('aaaaa')).toBe('aaaaa'); // proxy test
|
|
75
|
+
global.server.app.updateConfig('i18n', { enabled: true });
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const yup = require('yup');
|
|
2
|
+
const AbstractMiddleware = require('./AbstractMiddleware');
|
|
3
|
+
/**
|
|
4
|
+
* Middleware for reusing pagination
|
|
5
|
+
*/
|
|
6
|
+
class Pagination extends AbstractMiddleware {
|
|
7
|
+
static get description() {
|
|
8
|
+
return 'Pagination middleware. You can use limit=10 and maxLimit=100 parameters';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// eslint-disable-next-line class-methods-use-this
|
|
12
|
+
get relatedQueryParameters() {
|
|
13
|
+
return yup.object().shape({
|
|
14
|
+
page: yup.number(),
|
|
15
|
+
limit: yup.number(),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async middleware(req, res, next) {
|
|
20
|
+
let { limit, maxLimit } = this.params;
|
|
21
|
+
|
|
22
|
+
limit = typeof limit === 'number' ? parseInt(limit, 10) : 10;
|
|
23
|
+
maxLimit = typeof maxLimit === 'number' ? parseInt(maxLimit, 10) : 100;
|
|
24
|
+
|
|
25
|
+
req.appInfo.pagination = {};
|
|
26
|
+
req.appInfo.pagination.page =
|
|
27
|
+
typeof req?.query?.page === 'string'
|
|
28
|
+
? parseInt(req?.query?.page, 10) || 1
|
|
29
|
+
: 1;
|
|
30
|
+
|
|
31
|
+
req.appInfo.pagination.limit =
|
|
32
|
+
typeof req?.query?.limit === 'string'
|
|
33
|
+
? parseInt(req?.query?.limit, 10) || 0
|
|
34
|
+
: limit;
|
|
35
|
+
|
|
36
|
+
if (req.appInfo.pagination.limit > maxLimit) {
|
|
37
|
+
req.appInfo.pagination.limit = maxLimit;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (req.appInfo.pagination.page < 1) {
|
|
41
|
+
req.appInfo.pagination.page = 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (req.appInfo.pagination.limit < 0) {
|
|
45
|
+
req.appInfo.pagination.limit = 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
req.appInfo.pagination.skip =
|
|
49
|
+
req.appInfo.pagination.page * req.appInfo.pagination.limit -
|
|
50
|
+
req.appInfo.pagination.limit;
|
|
51
|
+
|
|
52
|
+
return next();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = Pagination;
|