anote-server-libs 0.12.0 → 0.12.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.
- package/.vscode/settings.json +2 -2
- package/models/ApiCall.ts +40 -40
- package/models/Migration.ts +43 -43
- package/models/repository/CryptModelDao.js +82 -0
- package/models/repository/MemoryCache.ts +87 -87
- package/models/repository/ModelDao.ts +268 -268
- package/package.json +35 -35
- package/services/WithBody.ts +65 -65
- package/services/WithTransaction.ts +260 -260
- package/services/utils.js +25 -24
- package/services/utils.ts +289 -288
- package/tsconfig.json +29 -29
package/services/utils.js
CHANGED
|
@@ -147,22 +147,22 @@ function idempotent(repo, debug, logger) {
|
|
|
147
147
|
};
|
|
148
148
|
}
|
|
149
149
|
function sendSelfPostableMessage(res, code, messageType, err) {
|
|
150
|
-
res.type('text/html').status(code).write(`
|
|
151
|
-
<!DOCTYPE HTML>
|
|
152
|
-
<html>
|
|
153
|
-
<head>
|
|
154
|
-
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
|
155
|
-
</head>
|
|
156
|
-
<body>
|
|
157
|
-
<script type="text/javascript">
|
|
158
|
-
window.parent.postMessage({
|
|
159
|
-
type: '${messageType}',
|
|
160
|
-
confirm: ${!err},
|
|
161
|
-
error: JSON.parse('${JSON.stringify(err) || 'null'}')
|
|
162
|
-
}, '*');
|
|
163
|
-
</script>
|
|
164
|
-
</body>
|
|
165
|
-
</html>
|
|
150
|
+
res.type('text/html').status(code).write(`
|
|
151
|
+
<!DOCTYPE HTML>
|
|
152
|
+
<html>
|
|
153
|
+
<head>
|
|
154
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
|
155
|
+
</head>
|
|
156
|
+
<body>
|
|
157
|
+
<script type="text/javascript">
|
|
158
|
+
window.parent.postMessage({
|
|
159
|
+
type: '${messageType}',
|
|
160
|
+
confirm: ${!err},
|
|
161
|
+
error: JSON.parse('${JSON.stringify(err) || 'null'}')
|
|
162
|
+
}, '*');
|
|
163
|
+
</script>
|
|
164
|
+
</body>
|
|
165
|
+
</html>
|
|
166
166
|
`);
|
|
167
167
|
res.end();
|
|
168
168
|
}
|
|
@@ -236,6 +236,7 @@ function getHtml(template, translationKeyValue) {
|
|
|
236
236
|
const replacer = (_, key) => replacePlaceholders(translationKeyValue, key.split('.'), 0) ?? key;
|
|
237
237
|
template = template.replace(pattern, replacer);
|
|
238
238
|
template = template.replace(pattern, replacer);
|
|
239
|
+
template = template.replace(pattern, replacer);
|
|
239
240
|
template = template.replace(new RegExp('\\[\\[\\s*([a-zA-Z._0-9:]+)\\s*\\]\\]', 'g'), (_, key) => {
|
|
240
241
|
const keyParts = key.split(':');
|
|
241
242
|
return (replacePlaceholders(translationKeyValue, keyParts[1].split('.'), 0) || [])
|
|
@@ -261,15 +262,15 @@ function getHtmlReplaced(config, templates, key, lang, translations, data = {},
|
|
|
261
262
|
};
|
|
262
263
|
let html = getHtml(templates[key], merged);
|
|
263
264
|
if (newPage) {
|
|
264
|
-
html = html.replace('</head>', `<style>
|
|
265
|
-
@media print {
|
|
266
|
-
.new-page {
|
|
267
|
-
page-break-before: always;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
</style>
|
|
265
|
+
html = html.replace('</head>', `<style>
|
|
266
|
+
@media print {
|
|
267
|
+
.new-page {
|
|
268
|
+
page-break-before: always;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
</style>
|
|
271
272
|
</head>`);
|
|
272
|
-
html = html.replace('</body>', `<p class="new-page">Provided by Satoris</p>
|
|
273
|
+
html = html.replace('</body>', `<p class="new-page">Provided by Satoris</p>
|
|
273
274
|
</body>`);
|
|
274
275
|
}
|
|
275
276
|
return html;
|
package/services/utils.ts
CHANGED
|
@@ -1,288 +1,289 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import {NextFunction, Request, Response} from 'express';
|
|
3
|
-
import {Logger} from 'winston';
|
|
4
|
-
import {BaseModelRepository} from '../models/repository/BaseModelRepository';
|
|
5
|
-
|
|
6
|
-
interface MinimalConfiguration {frontend: string, app: {product: string, name: string}}
|
|
7
|
-
|
|
8
|
-
export function atob(str: string): string {
|
|
9
|
-
return Buffer.from(str, 'base64').toString('binary');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function btoa(str: string): string {
|
|
13
|
-
return Buffer.from(str).toString('base64');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function clientErrorHandle(this: Logger, err: any, dbClient: any) {
|
|
17
|
-
this.error('SQL [Client %d] ERROR: %j', dbClient.processID, err);
|
|
18
|
-
}
|
|
19
|
-
export const utils: {[id: string]: any} = {
|
|
20
|
-
clientErrorHandler: undefined
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
function gcdTwo(a: number, b: number): number {
|
|
24
|
-
if(a === 0)
|
|
25
|
-
return b;
|
|
26
|
-
return gcdTwo(b % a, a);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function gcd(values: number[]): number {
|
|
30
|
-
let result = values[0];
|
|
31
|
-
for(let i = 1; i < values.length; i++)
|
|
32
|
-
result = gcdTwo(values[i], result);
|
|
33
|
-
return result;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function lcm(values: number[]): number {
|
|
37
|
-
let l = 1, divisor = 2;
|
|
38
|
-
while(true) {
|
|
39
|
-
let counter = 0;
|
|
40
|
-
let divisible = false;
|
|
41
|
-
for(let i = 0; i < values.length; i++) {
|
|
42
|
-
if(values[i] === 0) {
|
|
43
|
-
return 0;
|
|
44
|
-
} else if(values[i] < 0) {
|
|
45
|
-
values[i] = values[i] * (-1);
|
|
46
|
-
}
|
|
47
|
-
if(values[i] === 1) {
|
|
48
|
-
counter++;
|
|
49
|
-
}
|
|
50
|
-
if(values[i] % divisor === 0) {
|
|
51
|
-
divisible = true;
|
|
52
|
-
values[i] = values[i] / divisor;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
if(divisible) {
|
|
56
|
-
l = l * divisor;
|
|
57
|
-
} else {
|
|
58
|
-
divisor++;
|
|
59
|
-
}
|
|
60
|
-
if(counter === values.length) {
|
|
61
|
-
return l;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function jsonStringify(obj: any): string {
|
|
67
|
-
const cache: any = {};
|
|
68
|
-
return JSON.stringify(obj, function(_, value) {
|
|
69
|
-
if(typeof value === 'object' && value !== null) {
|
|
70
|
-
if(cache[value] !== -1) {
|
|
71
|
-
try {
|
|
72
|
-
return JSON.parse(JSON.stringify(value));
|
|
73
|
-
} catch(error) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
cache[value] = true;
|
|
78
|
-
}
|
|
79
|
-
return value;
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function idempotent(repo: BaseModelRepository, debug: boolean, logger: Logger) {
|
|
84
|
-
return function(req: Request, res: Response, next: NextFunction) {
|
|
85
|
-
let idempotenceKey = req.header('x-idempotent-key');
|
|
86
|
-
if(idempotenceKey) {
|
|
87
|
-
idempotenceKey = idempotenceKey.substring(0, 40);
|
|
88
|
-
repo.ApiCall.get(idempotenceKey).then(call => {
|
|
89
|
-
if(!call) {
|
|
90
|
-
const jsonTerminator = res.json;
|
|
91
|
-
const endTerminator = res.end;
|
|
92
|
-
const writeTerminator = res.write;
|
|
93
|
-
let response = '';
|
|
94
|
-
res.json = (function(obj: any) {
|
|
95
|
-
repo.ApiCall.create({
|
|
96
|
-
id: idempotenceKey,
|
|
97
|
-
responseCode: res.statusCode,
|
|
98
|
-
responseJson: jsonStringify(obj),
|
|
99
|
-
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000)
|
|
100
|
-
}).then(() => jsonTerminator.call(res, obj), err => {
|
|
101
|
-
if(err) logger.warn('Cannot save idempotent key: %j', err);
|
|
102
|
-
jsonTerminator.call(res, obj);
|
|
103
|
-
});
|
|
104
|
-
}).bind(res);
|
|
105
|
-
res.end = (function(buf: string) {
|
|
106
|
-
if(buf) response = response + buf.toString();
|
|
107
|
-
repo.ApiCall.create({
|
|
108
|
-
id: idempotenceKey,
|
|
109
|
-
responseCode: res.statusCode,
|
|
110
|
-
responseJson: response,
|
|
111
|
-
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000)
|
|
112
|
-
}).then(() => endTerminator.call(res, buf), err => {
|
|
113
|
-
if(err) logger.warn('Cannot save idempotent key: %j', err);
|
|
114
|
-
endTerminator.call(res, buf);
|
|
115
|
-
});
|
|
116
|
-
}).bind(res);
|
|
117
|
-
res.write = (function(buf: string) {
|
|
118
|
-
if(buf) response = response + buf.toString();
|
|
119
|
-
writeTerminator.call(res, buf);
|
|
120
|
-
}).bind(res);
|
|
121
|
-
next();
|
|
122
|
-
} else res.status(417).json({
|
|
123
|
-
responseCode: call.responseCode,
|
|
124
|
-
responseBody: call.responseJson
|
|
125
|
-
});
|
|
126
|
-
}, err => res.status(500).json({
|
|
127
|
-
error: {
|
|
128
|
-
errorKey: 'internal.db',
|
|
129
|
-
additionalInfo: {message: err.message, stack: debug && err.stack}
|
|
130
|
-
}
|
|
131
|
-
}));
|
|
132
|
-
} else next();
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function sendSelfPostableMessage(res: Response, code: number, messageType: string, err?: any) {
|
|
137
|
-
res.type('text/html').status(code).write(`
|
|
138
|
-
<!DOCTYPE HTML>
|
|
139
|
-
<html>
|
|
140
|
-
<head>
|
|
141
|
-
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
|
142
|
-
</head>
|
|
143
|
-
<body>
|
|
144
|
-
<script type="text/javascript">
|
|
145
|
-
window.parent.postMessage({
|
|
146
|
-
type: '${messageType}',
|
|
147
|
-
confirm: ${!err},
|
|
148
|
-
error: JSON.parse('${JSON.stringify(err) || 'null'}')
|
|
149
|
-
}, '*');
|
|
150
|
-
</script>
|
|
151
|
-
</body>
|
|
152
|
-
</html>
|
|
153
|
-
`);
|
|
154
|
-
res.end();
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function fpEuros(n: number) {
|
|
158
|
-
return Math.round(n * 100) / 100;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function digitize(value: number | string, opts?: {[key: string]: any}): string {
|
|
162
|
-
if(value === undefined || value === null) return 'undefined';
|
|
163
|
-
if(typeof value === 'number') {
|
|
164
|
-
if(isNaN(value)) return '-';
|
|
165
|
-
if(!isFinite(value)) return 'Infinite';
|
|
166
|
-
value = String(value);
|
|
167
|
-
} else if(typeof value === 'string') return value;
|
|
168
|
-
|
|
169
|
-
const parts = value.split('.');
|
|
170
|
-
const initialLength = parts[0].length;
|
|
171
|
-
for(let i = initialLength - 3; i > 0; i -= 3) {
|
|
172
|
-
parts[0] = parts[0].slice(0, i) + ',' + parts[0].slice(i);
|
|
173
|
-
}
|
|
174
|
-
if(parts[0].startsWith('-,'))
|
|
175
|
-
parts[0] = '-' + parts[0].slice(2);
|
|
176
|
-
if(parts[1]) {
|
|
177
|
-
const expDecimals = parts[1].split(/[eE]-/);
|
|
178
|
-
if (expDecimals.length > 1 && parseInt(expDecimals[1], 10) > 2) return '0.00';
|
|
179
|
-
let decimals = fpEuros(parseFloat('0.' + parts[1])).toString().substr(2, 2);
|
|
180
|
-
if (decimals.length === 1) decimals += '0';
|
|
181
|
-
return parts[0] + '.' + decimals;
|
|
182
|
-
}
|
|
183
|
-
return (opts && opts.currency)? (parts[0] + '.00') : parts[0];
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function loadEmailTemplates(path: string): Record<string, string> | undefined {
|
|
187
|
-
if(!path || !fs.existsSync(path)) return undefined;
|
|
188
|
-
const loadedEmailTemplates: {[id: string]: string} = {};
|
|
189
|
-
const fileNames = fs.readdirSync(path);
|
|
190
|
-
fileNames.forEach(fileName => {
|
|
191
|
-
if(!fileName.startsWith('template-') || !fileName.endsWith('.html')) return;
|
|
192
|
-
const templateIdentifier = fileName.slice('template-'.length, -'.html'.length);
|
|
193
|
-
loadedEmailTemplates[templateIdentifier] = fs.readFileSync(path + '/' + fileName).toString('utf8');
|
|
194
|
-
});
|
|
195
|
-
return loadedEmailTemplates;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function readEmailTemplates(path: string, config: MinimalConfiguration): {[id: string]: string} | undefined {
|
|
199
|
-
const {product, name} = config.app;
|
|
200
|
-
if(!product || !name) return loadEmailTemplates(path);
|
|
201
|
-
const mainPath = path + '/' + product;
|
|
202
|
-
const overridePath = product !== name ? path + `/${product}/${name}` : undefined;
|
|
203
|
-
const defaultTemplates = loadEmailTemplates(mainPath);
|
|
204
|
-
const overrideTemplates = overridePath ? loadEmailTemplates(overridePath) : {};
|
|
205
|
-
return {...defaultTemplates, ...overrideTemplates};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function replacePlaceholders(data: Record<string, any>, keys: string[], depth: number): any {
|
|
209
|
-
if(!keys || depth >= keys.length) return undefined;
|
|
210
|
-
if(depth === keys.length - 1) return (data || {})[keys[depth]];
|
|
211
|
-
return replacePlaceholders((data || {})[keys[depth]], keys, depth + 1);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function getHtml(template: string, translationKeyValue: Record<string, any>): string | undefined {
|
|
215
|
-
if(!template) return undefined;
|
|
216
|
-
const pattern = new RegExp('{{\\s*([^{}]+?)\\s*}}', 'g');
|
|
217
|
-
const replacer = (_: string, key: string) => replacePlaceholders(translationKeyValue, key.split('.'), 0) ?? key;
|
|
218
|
-
template = template.replace(pattern, replacer);
|
|
219
|
-
template = template.replace(pattern, replacer);
|
|
220
|
-
template = template.replace(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
...(translations.
|
|
247
|
-
...(translations.
|
|
248
|
-
...(
|
|
249
|
-
...
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
</
|
|
262
|
-
|
|
263
|
-
</body
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
...(translations.
|
|
280
|
-
...(translations.
|
|
281
|
-
...(
|
|
282
|
-
...
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import {NextFunction, Request, Response} from 'express';
|
|
3
|
+
import {Logger} from 'winston';
|
|
4
|
+
import {BaseModelRepository} from '../models/repository/BaseModelRepository';
|
|
5
|
+
|
|
6
|
+
interface MinimalConfiguration {frontend: string, app: {product: string, name: string}}
|
|
7
|
+
|
|
8
|
+
export function atob(str: string): string {
|
|
9
|
+
return Buffer.from(str, 'base64').toString('binary');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function btoa(str: string): string {
|
|
13
|
+
return Buffer.from(str).toString('base64');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function clientErrorHandle(this: Logger, err: any, dbClient: any) {
|
|
17
|
+
this.error('SQL [Client %d] ERROR: %j', dbClient.processID, err);
|
|
18
|
+
}
|
|
19
|
+
export const utils: {[id: string]: any} = {
|
|
20
|
+
clientErrorHandler: undefined
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function gcdTwo(a: number, b: number): number {
|
|
24
|
+
if(a === 0)
|
|
25
|
+
return b;
|
|
26
|
+
return gcdTwo(b % a, a);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function gcd(values: number[]): number {
|
|
30
|
+
let result = values[0];
|
|
31
|
+
for(let i = 1; i < values.length; i++)
|
|
32
|
+
result = gcdTwo(values[i], result);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function lcm(values: number[]): number {
|
|
37
|
+
let l = 1, divisor = 2;
|
|
38
|
+
while(true) {
|
|
39
|
+
let counter = 0;
|
|
40
|
+
let divisible = false;
|
|
41
|
+
for(let i = 0; i < values.length; i++) {
|
|
42
|
+
if(values[i] === 0) {
|
|
43
|
+
return 0;
|
|
44
|
+
} else if(values[i] < 0) {
|
|
45
|
+
values[i] = values[i] * (-1);
|
|
46
|
+
}
|
|
47
|
+
if(values[i] === 1) {
|
|
48
|
+
counter++;
|
|
49
|
+
}
|
|
50
|
+
if(values[i] % divisor === 0) {
|
|
51
|
+
divisible = true;
|
|
52
|
+
values[i] = values[i] / divisor;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if(divisible) {
|
|
56
|
+
l = l * divisor;
|
|
57
|
+
} else {
|
|
58
|
+
divisor++;
|
|
59
|
+
}
|
|
60
|
+
if(counter === values.length) {
|
|
61
|
+
return l;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function jsonStringify(obj: any): string {
|
|
67
|
+
const cache: any = {};
|
|
68
|
+
return JSON.stringify(obj, function(_, value) {
|
|
69
|
+
if(typeof value === 'object' && value !== null) {
|
|
70
|
+
if(cache[value] !== -1) {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(JSON.stringify(value));
|
|
73
|
+
} catch(error) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
cache[value] = true;
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function idempotent(repo: BaseModelRepository, debug: boolean, logger: Logger) {
|
|
84
|
+
return function(req: Request, res: Response, next: NextFunction) {
|
|
85
|
+
let idempotenceKey = req.header('x-idempotent-key');
|
|
86
|
+
if(idempotenceKey) {
|
|
87
|
+
idempotenceKey = idempotenceKey.substring(0, 40);
|
|
88
|
+
repo.ApiCall.get(idempotenceKey).then(call => {
|
|
89
|
+
if(!call) {
|
|
90
|
+
const jsonTerminator = res.json;
|
|
91
|
+
const endTerminator = res.end;
|
|
92
|
+
const writeTerminator = res.write;
|
|
93
|
+
let response = '';
|
|
94
|
+
res.json = (function(obj: any) {
|
|
95
|
+
repo.ApiCall.create({
|
|
96
|
+
id: idempotenceKey,
|
|
97
|
+
responseCode: res.statusCode,
|
|
98
|
+
responseJson: jsonStringify(obj),
|
|
99
|
+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000)
|
|
100
|
+
}).then(() => jsonTerminator.call(res, obj), err => {
|
|
101
|
+
if(err) logger.warn('Cannot save idempotent key: %j', err);
|
|
102
|
+
jsonTerminator.call(res, obj);
|
|
103
|
+
});
|
|
104
|
+
}).bind(res);
|
|
105
|
+
res.end = (function(buf: string) {
|
|
106
|
+
if(buf) response = response + buf.toString();
|
|
107
|
+
repo.ApiCall.create({
|
|
108
|
+
id: idempotenceKey,
|
|
109
|
+
responseCode: res.statusCode,
|
|
110
|
+
responseJson: response,
|
|
111
|
+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000)
|
|
112
|
+
}).then(() => endTerminator.call(res, buf), err => {
|
|
113
|
+
if(err) logger.warn('Cannot save idempotent key: %j', err);
|
|
114
|
+
endTerminator.call(res, buf);
|
|
115
|
+
});
|
|
116
|
+
}).bind(res);
|
|
117
|
+
res.write = (function(buf: string) {
|
|
118
|
+
if(buf) response = response + buf.toString();
|
|
119
|
+
writeTerminator.call(res, buf);
|
|
120
|
+
}).bind(res);
|
|
121
|
+
next();
|
|
122
|
+
} else res.status(417).json({
|
|
123
|
+
responseCode: call.responseCode,
|
|
124
|
+
responseBody: call.responseJson
|
|
125
|
+
});
|
|
126
|
+
}, err => res.status(500).json({
|
|
127
|
+
error: {
|
|
128
|
+
errorKey: 'internal.db',
|
|
129
|
+
additionalInfo: {message: err.message, stack: debug && err.stack}
|
|
130
|
+
}
|
|
131
|
+
}));
|
|
132
|
+
} else next();
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function sendSelfPostableMessage(res: Response, code: number, messageType: string, err?: any) {
|
|
137
|
+
res.type('text/html').status(code).write(`
|
|
138
|
+
<!DOCTYPE HTML>
|
|
139
|
+
<html>
|
|
140
|
+
<head>
|
|
141
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
|
142
|
+
</head>
|
|
143
|
+
<body>
|
|
144
|
+
<script type="text/javascript">
|
|
145
|
+
window.parent.postMessage({
|
|
146
|
+
type: '${messageType}',
|
|
147
|
+
confirm: ${!err},
|
|
148
|
+
error: JSON.parse('${JSON.stringify(err) || 'null'}')
|
|
149
|
+
}, '*');
|
|
150
|
+
</script>
|
|
151
|
+
</body>
|
|
152
|
+
</html>
|
|
153
|
+
`);
|
|
154
|
+
res.end();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function fpEuros(n: number) {
|
|
158
|
+
return Math.round(n * 100) / 100;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function digitize(value: number | string, opts?: {[key: string]: any}): string {
|
|
162
|
+
if(value === undefined || value === null) return 'undefined';
|
|
163
|
+
if(typeof value === 'number') {
|
|
164
|
+
if(isNaN(value)) return '-';
|
|
165
|
+
if(!isFinite(value)) return 'Infinite';
|
|
166
|
+
value = String(value);
|
|
167
|
+
} else if(typeof value === 'string') return value;
|
|
168
|
+
|
|
169
|
+
const parts = value.split('.');
|
|
170
|
+
const initialLength = parts[0].length;
|
|
171
|
+
for(let i = initialLength - 3; i > 0; i -= 3) {
|
|
172
|
+
parts[0] = parts[0].slice(0, i) + ',' + parts[0].slice(i);
|
|
173
|
+
}
|
|
174
|
+
if(parts[0].startsWith('-,'))
|
|
175
|
+
parts[0] = '-' + parts[0].slice(2);
|
|
176
|
+
if(parts[1]) {
|
|
177
|
+
const expDecimals = parts[1].split(/[eE]-/);
|
|
178
|
+
if (expDecimals.length > 1 && parseInt(expDecimals[1], 10) > 2) return '0.00';
|
|
179
|
+
let decimals = fpEuros(parseFloat('0.' + parts[1])).toString().substr(2, 2);
|
|
180
|
+
if (decimals.length === 1) decimals += '0';
|
|
181
|
+
return parts[0] + '.' + decimals;
|
|
182
|
+
}
|
|
183
|
+
return (opts && opts.currency)? (parts[0] + '.00') : parts[0];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function loadEmailTemplates(path: string): Record<string, string> | undefined {
|
|
187
|
+
if(!path || !fs.existsSync(path)) return undefined;
|
|
188
|
+
const loadedEmailTemplates: {[id: string]: string} = {};
|
|
189
|
+
const fileNames = fs.readdirSync(path);
|
|
190
|
+
fileNames.forEach(fileName => {
|
|
191
|
+
if(!fileName.startsWith('template-') || !fileName.endsWith('.html')) return;
|
|
192
|
+
const templateIdentifier = fileName.slice('template-'.length, -'.html'.length);
|
|
193
|
+
loadedEmailTemplates[templateIdentifier] = fs.readFileSync(path + '/' + fileName).toString('utf8');
|
|
194
|
+
});
|
|
195
|
+
return loadedEmailTemplates;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function readEmailTemplates(path: string, config: MinimalConfiguration): {[id: string]: string} | undefined {
|
|
199
|
+
const {product, name} = config.app;
|
|
200
|
+
if(!product || !name) return loadEmailTemplates(path);
|
|
201
|
+
const mainPath = path + '/' + product;
|
|
202
|
+
const overridePath = product !== name ? path + `/${product}/${name}` : undefined;
|
|
203
|
+
const defaultTemplates = loadEmailTemplates(mainPath);
|
|
204
|
+
const overrideTemplates = overridePath ? loadEmailTemplates(overridePath) : {};
|
|
205
|
+
return {...defaultTemplates, ...overrideTemplates};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function replacePlaceholders(data: Record<string, any>, keys: string[], depth: number): any {
|
|
209
|
+
if(!keys || depth >= keys.length) return undefined;
|
|
210
|
+
if(depth === keys.length - 1) return (data || {})[keys[depth]];
|
|
211
|
+
return replacePlaceholders((data || {})[keys[depth]], keys, depth + 1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function getHtml(template: string, translationKeyValue: Record<string, any>): string | undefined {
|
|
215
|
+
if(!template) return undefined;
|
|
216
|
+
const pattern = new RegExp('{{\\s*([^{}]+?)\\s*}}', 'g');
|
|
217
|
+
const replacer = (_: string, key: string) => replacePlaceholders(translationKeyValue, key.split('.'), 0) ?? key;
|
|
218
|
+
template = template.replace(pattern, replacer);
|
|
219
|
+
template = template.replace(pattern, replacer);
|
|
220
|
+
template = template.replace(pattern, replacer);
|
|
221
|
+
template = template.replace(new RegExp('\\[\\[\\s*([a-zA-Z._0-9:]+)\\s*\\]\\]', 'g'), (_, key) => {
|
|
222
|
+
const keyParts = key.split(':');
|
|
223
|
+
return (replacePlaceholders(translationKeyValue, keyParts[1].split('.'), 0) || [])
|
|
224
|
+
.map((subContext: any) => getHtml(keyParts[0], subContext)).join('');
|
|
225
|
+
});
|
|
226
|
+
template = template.replace(new RegExp('<<\\s*([a-zA-Z._0-9:]+)\\s*>>', 'g'), (_, key) => {
|
|
227
|
+
const keyParts = key.split(':');
|
|
228
|
+
if(replacePlaceholders(translationKeyValue, keyParts[1].split('.'), 0))
|
|
229
|
+
return getHtml(keyParts[0], translationKeyValue);
|
|
230
|
+
return '';
|
|
231
|
+
});
|
|
232
|
+
return template;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function getHtmlReplaced(config: MinimalConfiguration, templates: Record<string, string>, key: string, lang: number,
|
|
236
|
+
translations: {
|
|
237
|
+
common: Record<number, Record<string, string>>,
|
|
238
|
+
byProduct: Record<string, Record<number, Record<string, string>>>,
|
|
239
|
+
byEnv: Record<string, Record<number, Record<string, string>>>
|
|
240
|
+
},
|
|
241
|
+
data: Record<string, Record<string, string | number>> = {},
|
|
242
|
+
dynamicData: Record<string, string | number> = {},
|
|
243
|
+
newPage?: boolean): string {
|
|
244
|
+
const {product, name} = config.app;
|
|
245
|
+
const merged = {
|
|
246
|
+
...(translations.common[lang] ?? translations.common[0] ?? {}),
|
|
247
|
+
...(translations.byProduct[product]?.[lang] ?? translations.byProduct[product]?.[0] ?? {}),
|
|
248
|
+
...(translations.byEnv[name]?.[lang] ?? translations.byEnv[name]?.[0] ?? {}),
|
|
249
|
+
...(data[name] ?? {}),
|
|
250
|
+
...dynamicData,
|
|
251
|
+
domain: config.frontend
|
|
252
|
+
};
|
|
253
|
+
let html = getHtml(templates[key], merged);
|
|
254
|
+
if(newPage) {
|
|
255
|
+
html = html.replace('</head>', `<style>
|
|
256
|
+
@media print {
|
|
257
|
+
.new-page {
|
|
258
|
+
page-break-before: always;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
</style>
|
|
262
|
+
</head>`);
|
|
263
|
+
html = html.replace('</body>', `<p class="new-page">Provided by Satoris</p>
|
|
264
|
+
</body>`);
|
|
265
|
+
}
|
|
266
|
+
return html;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function getEmailOneSimpleValue(config: MinimalConfiguration, key: string, lang: number,
|
|
270
|
+
translations: {
|
|
271
|
+
common: Record<number, Record<string, string>>,
|
|
272
|
+
byProduct: Record<string, Record<number, Record<string, string>>>,
|
|
273
|
+
byEnv: Record<string, Record<number, Record<string, string>>>
|
|
274
|
+
},
|
|
275
|
+
data: Record<string, Record<string, string | number>> = {},
|
|
276
|
+
dynamicData: Record<string, string | number> = {}): string | undefined {
|
|
277
|
+
const {product, name} = config.app;
|
|
278
|
+
const merged: Record<string, string | number> = {
|
|
279
|
+
...(translations.common[lang] ?? translations.common[0] ?? {}),
|
|
280
|
+
...(translations.byProduct[product]?.[lang] ?? translations.byProduct[product]?.[0] ?? {}),
|
|
281
|
+
...(translations.byEnv[name]?.[lang] ?? translations.byEnv[name]?.[0] ?? {}),
|
|
282
|
+
...(data[name] ?? {}),
|
|
283
|
+
...dynamicData
|
|
284
|
+
};
|
|
285
|
+
const pattern = new RegExp('{{\\s*([a-zA-Z._0-9]+)\\s*}}', 'g');
|
|
286
|
+
return (merged[key] as string | undefined)?.replace(pattern, (_: string, placeholderKey: string) =>
|
|
287
|
+
String(merged[placeholderKey] ?? placeholderKey)
|
|
288
|
+
);
|
|
289
|
+
}
|