@adaptivestone/framework 3.4.3 → 4.1.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 +37 -3
- 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 +40 -30
- 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 +20 -16
- 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 +162 -42
- 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
|
@@ -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;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const PrepareAppInfo = require('./PrepareAppInfo');
|
|
2
|
+
|
|
3
|
+
describe('prepareAppInfo methods', () => {
|
|
4
|
+
it('have description fields', async () => {
|
|
5
|
+
expect.assertions(1);
|
|
6
|
+
const middleware = new PrepareAppInfo(global.server.app);
|
|
7
|
+
expect(middleware.constructor.description).toBeDefined();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('middleware that works', async () => {
|
|
11
|
+
expect.assertions(3);
|
|
12
|
+
const middleware = new PrepareAppInfo(global.server.app);
|
|
13
|
+
const nextFunction = jest.fn(() => {});
|
|
14
|
+
const req = {};
|
|
15
|
+
await middleware.middleware(req, {}, nextFunction);
|
|
16
|
+
expect(nextFunction).toHaveBeenCalledWith();
|
|
17
|
+
expect(req.appInfo).toBeDefined();
|
|
18
|
+
req.appInfo.test = 5;
|
|
19
|
+
await middleware.middleware(req, {}, nextFunction);
|
|
20
|
+
expect(req.appInfo.test).toBe(5);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const AbstractMiddleware = require('./AbstractMiddleware');
|
|
2
|
+
|
|
3
|
+
class RequestLogger extends AbstractMiddleware {
|
|
4
|
+
static get description() {
|
|
5
|
+
return 'Log info about the request';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async middleware(req, res, next) {
|
|
9
|
+
const startTime = Date.now();
|
|
10
|
+
const text = `Request is [${req.method}] ${req.url}`;
|
|
11
|
+
this.logger.info(text);
|
|
12
|
+
res.on('finish', () => {
|
|
13
|
+
const duration = Date.now() - startTime;
|
|
14
|
+
this.logger.info(
|
|
15
|
+
`Finished ${text}. Status: ${res.statusCode}. Duration ${duration} ms`,
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
next();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = RequestLogger;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const formidable = require('formidable');
|
|
2
|
+
|
|
3
|
+
const AbstractMiddleware = require('./AbstractMiddleware');
|
|
4
|
+
|
|
5
|
+
class RequestParser extends AbstractMiddleware {
|
|
6
|
+
static get description() {
|
|
7
|
+
return 'Parses incoming request. Based on Formidable library';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async middleware(req, res, next) {
|
|
11
|
+
const time = Date.now();
|
|
12
|
+
this.logger.verbose(`Parsing request`);
|
|
13
|
+
// TODO update this to https://github.com/node-formidable/formidable/issues/412#issuecomment-1367914268 in node v20 (in 2023?)
|
|
14
|
+
|
|
15
|
+
const form = formidable(this.params); // not in construstor as reuse formidable affects performance
|
|
16
|
+
form.parse(req, (err, fields, files) => {
|
|
17
|
+
this.logger.verbose(
|
|
18
|
+
`Parsing multipart/formdata request DONE ${Date.now() - time}ms`,
|
|
19
|
+
);
|
|
20
|
+
if (err) {
|
|
21
|
+
this.logger.error(`Parsing failed ${err}`);
|
|
22
|
+
return next(err);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
req.body = {
|
|
26
|
+
// todo avoid body in next versions
|
|
27
|
+
...req.body,
|
|
28
|
+
...fields,
|
|
29
|
+
...files,
|
|
30
|
+
};
|
|
31
|
+
return next();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = RequestParser;
|
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { promisify } = require('node:util');
|
|
3
4
|
const nodemailer = require('nodemailer');
|
|
4
5
|
const sendMail = require('nodemailer-sendmail-transport');
|
|
5
6
|
const stub = require('nodemailer-stub-transport');
|
|
7
|
+
const pug = require('pug');
|
|
8
|
+
const juice = require('juice');
|
|
9
|
+
const { convert } = require('html-to-text');
|
|
6
10
|
|
|
7
11
|
const mailTransports = {
|
|
8
12
|
sendMail,
|
|
9
13
|
stub,
|
|
10
14
|
smtp: (data) => data,
|
|
11
15
|
};
|
|
12
|
-
const path = require('path');
|
|
13
16
|
const Base = require('../../../modules/Base');
|
|
14
17
|
|
|
15
|
-
// const i18next = require('i18next');
|
|
16
|
-
|
|
17
18
|
class Mail extends Base {
|
|
18
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Construct mail class
|
|
21
|
+
* @param {object} app
|
|
22
|
+
* @param {string} template template name
|
|
23
|
+
* @param {object} [templateData={}] data to render in template. Object with value that available inside template
|
|
24
|
+
* @param {object} [i18n] data to render in template
|
|
25
|
+
*/
|
|
26
|
+
constructor(app, template, templateData = {}, i18n = null) {
|
|
19
27
|
super(app);
|
|
20
28
|
if (!path.isAbsolute(template)) {
|
|
21
29
|
if (
|
|
@@ -38,60 +46,172 @@ class Mail extends Base {
|
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
48
|
this.templateData = templateData;
|
|
41
|
-
this.i18n = i18n
|
|
42
|
-
|
|
49
|
+
this.i18n = i18n ?? {
|
|
50
|
+
t: (str) => str,
|
|
51
|
+
locale: 'en', // todo change it to config
|
|
52
|
+
};
|
|
53
|
+
this.locale = this.i18n?.language;
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
/**
|
|
46
|
-
*
|
|
47
|
-
* @param
|
|
48
|
-
* @param
|
|
57
|
+
* Render template
|
|
58
|
+
* @param {object} type and fullpath
|
|
59
|
+
* @param {object} templateData
|
|
60
|
+
* @returns string
|
|
61
|
+
*/
|
|
62
|
+
static async #renderTemplateFile({ type, fullPath } = {}, templateData = {}) {
|
|
63
|
+
if (!type) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
switch (type) {
|
|
68
|
+
case 'html':
|
|
69
|
+
case 'text':
|
|
70
|
+
case 'css':
|
|
71
|
+
return fs.promises.readFile(fullPath, { encoding: 'utf8' });
|
|
72
|
+
case 'pug': {
|
|
73
|
+
const compiledFunction = pug.compileFile(fullPath);
|
|
74
|
+
return compiledFunction(templateData);
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
throw new Error(`Template type ${type} is not supported`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Render template
|
|
49
83
|
* @return {Promise}
|
|
50
84
|
*/
|
|
51
|
-
async
|
|
85
|
+
async renderTemplate() {
|
|
86
|
+
const files = await fs.promises.readdir(this.template);
|
|
87
|
+
const templates = {};
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
const [name, extension] = file.split('.');
|
|
90
|
+
templates[name] = {
|
|
91
|
+
type: extension,
|
|
92
|
+
fullPath: path.join(this.template, file),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!templates.html || !templates.subject) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
'Template HTML and Subject must be provided. Please follow documentation for details https://framework.adaptivestone.com/docs/email',
|
|
99
|
+
);
|
|
100
|
+
}
|
|
52
101
|
const mailConfig = this.app.getConfig('mail');
|
|
102
|
+
|
|
103
|
+
const templateDataToRender = {
|
|
104
|
+
locale: this.locale,
|
|
105
|
+
t: this.i18n.t.bind(this.i18n),
|
|
106
|
+
...mailConfig.globalVariablesToTemplates,
|
|
107
|
+
...this.templateData,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const [htmlRendered, subjectRendered, textRendered, extraCss] =
|
|
111
|
+
await Promise.all([
|
|
112
|
+
this.constructor.#renderTemplateFile(
|
|
113
|
+
templates.html,
|
|
114
|
+
templateDataToRender,
|
|
115
|
+
),
|
|
116
|
+
this.constructor.#renderTemplateFile(
|
|
117
|
+
templates.subject,
|
|
118
|
+
templateDataToRender,
|
|
119
|
+
),
|
|
120
|
+
this.constructor.#renderTemplateFile(
|
|
121
|
+
templates.text,
|
|
122
|
+
templateDataToRender,
|
|
123
|
+
),
|
|
124
|
+
this.constructor.#renderTemplateFile(templates.style),
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
juice.tableElements = ['TABLE'];
|
|
128
|
+
|
|
129
|
+
const juiceResourcesAsync = promisify(juice.juiceResources);
|
|
130
|
+
|
|
131
|
+
const inlinedHTML = await juiceResourcesAsync(htmlRendered, {
|
|
132
|
+
preserveImportant: true,
|
|
133
|
+
webResources: mailConfig.webResources,
|
|
134
|
+
extraCss,
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
htmlRaw: htmlRendered,
|
|
138
|
+
subject: subjectRendered,
|
|
139
|
+
text: textRendered,
|
|
140
|
+
inlinedHTML,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Send email
|
|
146
|
+
* @param {string} to email send to
|
|
147
|
+
* @param {string} [from = mailConfig.from]
|
|
148
|
+
* @param {object} [aditionalNodemailerOptions = {}] additional option to nodemailer
|
|
149
|
+
* @return {Promise}
|
|
150
|
+
*/
|
|
151
|
+
async send(to, from = null, aditionalNodemailerOptions = {}) {
|
|
152
|
+
const { subject, text, inlinedHTML } = await this.renderTemplate();
|
|
153
|
+
|
|
154
|
+
return this.constructor.sendRaw(
|
|
155
|
+
this.app,
|
|
156
|
+
to,
|
|
157
|
+
subject,
|
|
158
|
+
inlinedHTML,
|
|
159
|
+
text,
|
|
160
|
+
from,
|
|
161
|
+
aditionalNodemailerOptions,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Send provided text (html) to email. Low level function. All data should be prepared before sending (like inline styles)
|
|
167
|
+
* @param {objetc} app application
|
|
168
|
+
* @param {string} to send to
|
|
169
|
+
* @param {string} subject email topic
|
|
170
|
+
* @param {string} html hmlt body of emain
|
|
171
|
+
* @param {string} [text] if not provided will be generated from html string
|
|
172
|
+
* @param {string} [from = mailConfig.from] from. If not provided will be grabbed from config
|
|
173
|
+
* @param {object} [additionalNodeMailerOption = {}] any otipns to pass to nodemailer https://nodemailer.com/message/
|
|
174
|
+
*/
|
|
175
|
+
static async sendRaw(
|
|
176
|
+
app,
|
|
177
|
+
to,
|
|
178
|
+
subject,
|
|
179
|
+
html,
|
|
180
|
+
text = null,
|
|
181
|
+
from = null,
|
|
182
|
+
additionalNodeMailerOption = {},
|
|
183
|
+
) {
|
|
184
|
+
if (!app || !to || !subject || !html) {
|
|
185
|
+
throw new Error('App, to, subject and html is required fields.');
|
|
186
|
+
}
|
|
187
|
+
const mailConfig = app.getConfig('mail');
|
|
53
188
|
if (!from) {
|
|
54
189
|
// eslint-disable-next-line no-param-reassign
|
|
55
190
|
from = mailConfig.from;
|
|
56
191
|
}
|
|
57
|
-
|
|
192
|
+
|
|
193
|
+
if (!text) {
|
|
194
|
+
// eslint-disable-next-line no-param-reassign
|
|
195
|
+
text = convert(html, {
|
|
196
|
+
selectors: [{ selector: 'img', format: 'skip' }],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
58
199
|
const transportConfig = mailConfig.transports[mailConfig.transport];
|
|
59
200
|
const transport = mailTransports[mailConfig.transport];
|
|
60
201
|
const transporter = nodemailer.createTransport(transport(transportConfig));
|
|
61
202
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
juiceResources: {
|
|
70
|
-
webResources: mailConfig.webResources,
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
return email.send({
|
|
75
|
-
template: this.template,
|
|
76
|
-
message: {
|
|
77
|
-
to,
|
|
78
|
-
},
|
|
79
|
-
locals: {
|
|
80
|
-
locale: this.locale,
|
|
81
|
-
serverDomain: mailConfig.myDomain,
|
|
82
|
-
siteDomain,
|
|
83
|
-
t: this.i18n.t.bind(this.i18n),
|
|
84
|
-
...this.templateData,
|
|
85
|
-
},
|
|
203
|
+
return transporter.sendMail({
|
|
204
|
+
from,
|
|
205
|
+
to,
|
|
206
|
+
subject,
|
|
207
|
+
text,
|
|
208
|
+
html,
|
|
209
|
+
...additionalNodeMailerOption,
|
|
86
210
|
});
|
|
87
211
|
}
|
|
88
212
|
|
|
89
|
-
render() {
|
|
90
|
-
// TODO for debug
|
|
91
|
-
}
|
|
92
|
-
|
|
93
213
|
static get loggerGroup() {
|
|
94
|
-
return '
|
|
214
|
+
return 'email_';
|
|
95
215
|
}
|
|
96
216
|
}
|
|
97
217
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This folder for you resources to inline
|