5htp-core 0.6.0 → 0.6.1-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/app/component.tsx +1 -0
- package/client/assets/css/colors.less +46 -25
- package/client/assets/css/components/button.less +14 -5
- package/client/assets/css/components/card.less +5 -10
- package/client/assets/css/components/mantine.less +6 -5
- package/client/assets/css/components/table.less +1 -1
- package/client/assets/css/text/icons.less +1 -1
- package/client/assets/css/text/text.less +4 -0
- package/client/assets/css/utils/borders.less +1 -1
- package/client/assets/css/utils/layouts.less +8 -5
- package/client/components/Button.tsx +20 -17
- package/client/components/Checkbox.tsx +6 -1
- package/client/components/ConnectedInput.tsx +34 -0
- package/client/components/DropDown.tsx +21 -4
- package/client/components/Input.tsx +2 -2
- package/client/components/Rte/Editor.tsx +23 -9
- package/client/components/Rte/ToolbarPlugin/ElementFormat.tsx +1 -1
- package/client/components/Rte/ToolbarPlugin/index.tsx +272 -183
- package/client/components/Rte/currentEditor.ts +31 -2
- package/client/components/Rte/index.tsx +3 -0
- package/client/components/Rte/plugins/FloatingTextFormatToolbarPlugin/index.tsx +4 -1
- package/client/components/Select.tsx +29 -16
- package/client/components/Table/index.tsx +27 -11
- package/client/components/containers/Popover/index.tsx +21 -4
- package/client/components/index.ts +4 -2
- package/client/services/router/index.tsx +7 -5
- package/common/errors/index.tsx +28 -3
- package/common/router/index.ts +4 -1
- package/common/utils/rte.ts +183 -0
- package/package.json +3 -2
- package/server/app/container/console/index.ts +92 -62
- package/server/app/container/index.ts +4 -0
- package/server/app/service/index.ts +4 -2
- package/server/services/auth/index.ts +28 -14
- package/server/services/auth/router/index.ts +1 -1
- package/server/services/auth/router/request.ts +4 -4
- package/server/services/email/index.ts +8 -51
- package/server/services/prisma/Facet.ts +118 -0
- package/server/services/prisma/index.ts +24 -0
- package/server/services/router/http/index.ts +0 -2
- package/server/services/router/index.ts +220 -86
- package/server/services/router/response/index.ts +0 -15
- package/server/utils/rte.ts +21 -132
- package/types/global/utils.d.ts +4 -22
- package/types/icons.d.ts +1 -1
- package/server/services/email/service.json +0 -6
- package/server/services/email/templates.ts +0 -49
- package/server/services/email/transporter.ts +0 -31
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// Node
|
|
6
6
|
import { serialize } from 'v8';
|
|
7
7
|
import { formatWithOptions } from 'util';
|
|
8
|
+
import md5 from 'md5';
|
|
9
|
+
import dayjs from 'dayjs';
|
|
8
10
|
import Youch from 'youch';
|
|
9
11
|
import forTerminal from 'youch-terminal';
|
|
10
12
|
|
|
@@ -17,7 +19,7 @@ import Ansi2Html from 'ansi-to-html';
|
|
|
17
19
|
// Core libs
|
|
18
20
|
import type ApplicationContainer from '..';
|
|
19
21
|
import context from '@server/context';
|
|
20
|
-
import type { ServerBug,
|
|
22
|
+
import type { ServerBug, TCatchedError, CoreError } from '@common/errors';
|
|
21
23
|
import type ServerRequest from '@server/services/router/request';
|
|
22
24
|
import { SqlError } from '@server/services/database/debug';
|
|
23
25
|
|
|
@@ -49,6 +51,11 @@ export type Services = {
|
|
|
49
51
|
export type ChannelInfos = {
|
|
50
52
|
channelType: 'cron' | 'master' | 'request' | 'socket',
|
|
51
53
|
channelId?: string,
|
|
54
|
+
|
|
55
|
+
method?: string,
|
|
56
|
+
path?: string,
|
|
57
|
+
|
|
58
|
+
user?: string
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
export type TGuestLogs = {
|
|
@@ -99,8 +106,6 @@ export type TJsonLog = {
|
|
|
99
106
|
|
|
100
107
|
const LogPrefix = '[console]'
|
|
101
108
|
|
|
102
|
-
const errorMailInterval = (1 * 60 * 60 * 1000); // 1 hour
|
|
103
|
-
|
|
104
109
|
const logLevels = {
|
|
105
110
|
'log': 0,
|
|
106
111
|
'info': 3,
|
|
@@ -144,13 +149,12 @@ export default class Console {
|
|
|
144
149
|
public logger!: Logger<ILogObj>;
|
|
145
150
|
// Buffers
|
|
146
151
|
public logs: TJsonLog[] = [];
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
public sqlQueries: TDbQueryLog[] = [];*/
|
|
152
|
+
private reported: {
|
|
153
|
+
[hash: string]: {
|
|
154
|
+
times: number,
|
|
155
|
+
last: Date,
|
|
156
|
+
}
|
|
157
|
+
} = {};
|
|
154
158
|
|
|
155
159
|
/*----------------------------------
|
|
156
160
|
- LIFECYCLE
|
|
@@ -283,71 +287,86 @@ export default class Console {
|
|
|
283
287
|
this.logs = this.logs.slice(bufferOverflow);
|
|
284
288
|
}
|
|
285
289
|
|
|
286
|
-
|
|
290
|
+
// We don't prevent duplicates because we want to receive all variants of the same error
|
|
291
|
+
public async createBugReport( error: TCatchedError, request?: ServerRequest ) {
|
|
287
292
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const jsonResponse = await youchRes.toJSON()
|
|
292
|
-
console.log( forTerminal(jsonResponse, {
|
|
293
|
-
// Defaults to false
|
|
294
|
-
displayShortPath: false,
|
|
293
|
+
const application = this.container.application;
|
|
294
|
+
if (application === undefined)
|
|
295
|
+
return console.error(LogPrefix, "Can't send bug report because the application is not instanciated");
|
|
295
296
|
|
|
296
|
-
|
|
297
|
-
|
|
297
|
+
// Get context
|
|
298
|
+
const now = new Date();
|
|
299
|
+
const { channelType, channelId } = this.getChannel();
|
|
298
300
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
+
// On envoi l'email avant l'insertion dans bla bdd
|
|
302
|
+
// Car cette denrière a plus de chances de provoquer une erreur
|
|
303
|
+
//const logs = this.logs.filter(e => e.channel.channelId === channelId).slice(-100);
|
|
304
|
+
const stacktraces: string[] = [];
|
|
305
|
+
const context: object[] = [];
|
|
301
306
|
|
|
302
|
-
|
|
303
|
-
|
|
307
|
+
let currentError: TCatchedError | undefined = error;
|
|
308
|
+
let title: string | undefined;
|
|
309
|
+
while (currentError !== undefined) {
|
|
304
310
|
|
|
305
|
-
|
|
306
|
-
|
|
311
|
+
if (title === undefined)
|
|
312
|
+
title = currentError.message;
|
|
307
313
|
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
314
|
+
// Stacktrace
|
|
315
|
+
this.logger.error(LogPrefix, `Sending bug report for the following error:`, currentError);
|
|
316
|
+
stacktraces.push(currentError.stack || currentError.message);
|
|
311
317
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
318
|
+
// Context
|
|
319
|
+
if (('dataForDebugging' in currentError) && currentError.dataForDebugging !== undefined) {
|
|
320
|
+
console.error(LogPrefix, `More data about the error:`, currentError.dataForDebugging);
|
|
321
|
+
context.push(currentError.dataForDebugging || {});
|
|
322
|
+
}
|
|
315
323
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
324
|
+
// Print the error so it's accessible via logs
|
|
325
|
+
if (currentError instanceof SqlError) {
|
|
326
|
+
let printedQuery: string;
|
|
327
|
+
try {
|
|
328
|
+
printedQuery = this.printSql( currentError.query );
|
|
329
|
+
} catch (error) {
|
|
330
|
+
printedQuery = 'Failed to print query:' + (error || 'unknown error');
|
|
331
|
+
}
|
|
332
|
+
console.error(`Error caused by this query:`, printedQuery);
|
|
323
333
|
}
|
|
324
|
-
|
|
334
|
+
|
|
335
|
+
// Go deeper
|
|
336
|
+
currentError = 'originalError' in currentError
|
|
337
|
+
? currentError.originalError
|
|
338
|
+
: undefined
|
|
325
339
|
}
|
|
326
340
|
|
|
327
|
-
|
|
328
|
-
|
|
341
|
+
// Genertae unique error hash
|
|
342
|
+
const hash = md5( stacktraces.join('\n') );
|
|
329
343
|
|
|
330
|
-
//
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const shouldSendReport = lastSending === undefined || lastSending < Date.now() - errorMailInterval;
|
|
335
|
-
if (!shouldSendReport)
|
|
336
|
-
return;
|
|
344
|
+
// Don't send the same error twice in a row (avoid email spamming)
|
|
345
|
+
const lastReport = this.reported[hash];
|
|
346
|
+
let isDuplicate = false;
|
|
347
|
+
if (lastReport === undefined) {
|
|
337
348
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
349
|
+
this.reported[hash] = {
|
|
350
|
+
times: 0,
|
|
351
|
+
last: new Date()
|
|
352
|
+
}
|
|
342
353
|
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
354
|
+
// If error older than 1 day
|
|
355
|
+
} else if (dayjs(now).diff( dayjs(lastReport.last), 'day' ) > 1) {
|
|
356
|
+
|
|
357
|
+
lastReport.times++;
|
|
358
|
+
lastReport.last = now;
|
|
359
|
+
|
|
360
|
+
} else {
|
|
361
|
+
|
|
362
|
+
isDuplicate = true;
|
|
363
|
+
}
|
|
346
364
|
|
|
347
365
|
const bugReport: ServerBug = {
|
|
348
366
|
|
|
349
367
|
// Context
|
|
350
368
|
hash: hash,
|
|
369
|
+
isDuplicate,
|
|
351
370
|
date: now,
|
|
352
371
|
channelType,
|
|
353
372
|
channelId,
|
|
@@ -371,9 +390,9 @@ export default class Console {
|
|
|
371
390
|
} : {}),
|
|
372
391
|
|
|
373
392
|
// Error
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
393
|
+
title,
|
|
394
|
+
stacktraces,
|
|
395
|
+
context
|
|
377
396
|
}
|
|
378
397
|
|
|
379
398
|
await application.runHook('bug', bugReport);
|
|
@@ -391,12 +410,23 @@ export default class Console {
|
|
|
391
410
|
----------------------------------*/
|
|
392
411
|
|
|
393
412
|
public bugToHtml( report: ServerBug ) {
|
|
413
|
+
|
|
394
414
|
return `
|
|
395
415
|
<b>Channel</b>: ${report.channelType} (${report.channelId})<br />
|
|
396
416
|
<b>User</b>: ${report.user ? (report.user.name + ' (' + report.user.email + ')') : 'Unknown'}<br />
|
|
397
417
|
<b>IP</b>: ${report.ip}<br />
|
|
398
|
-
|
|
399
|
-
${
|
|
418
|
+
|
|
419
|
+
${report.stacktraces.map((stacktrace, index) => `
|
|
420
|
+
<hr />
|
|
421
|
+
<b>Error ${index + 1}</b>:
|
|
422
|
+
${this.printHtml(stacktrace)}<br />
|
|
423
|
+
`).join('')}
|
|
424
|
+
|
|
425
|
+
${report.context.map((context, index) => `
|
|
426
|
+
<hr />
|
|
427
|
+
<b>Context ${index + 1}</b>: ${this.jsonToHTML(context)}<br />
|
|
428
|
+
`).join('')}
|
|
429
|
+
|
|
400
430
|
${report.request ? `
|
|
401
431
|
<hr />
|
|
402
432
|
<b>Request</b>: ${report.request.method} ${report.request.url}<br />
|
|
@@ -432,7 +462,7 @@ Logs: ${this.config.enable ? `<br/>` + this.logsToHTML(report.logs) : 'Logs coll
|
|
|
432
462
|
});
|
|
433
463
|
|
|
434
464
|
// Print args as ANSI
|
|
435
|
-
const logArgsAndErrorsMarkup = this.logger
|
|
465
|
+
const logArgsAndErrorsMarkup = this.logger["runtime"].prettyFormatLogObj(log.args, this.logger.settings);
|
|
436
466
|
const logErrors = logArgsAndErrorsMarkup.errors;
|
|
437
467
|
const logArgs = logArgsAndErrorsMarkup.args;
|
|
438
468
|
const logErrorsStr = (logErrors.length > 0 && logArgs.length > 0 ? "\n" : "") + logErrors.join("\n");
|
|
@@ -42,7 +42,7 @@ export type StartedServicesIndex = {
|
|
|
42
42
|
|
|
43
43
|
export type TServiceArgs<TService extends AnyService> = [
|
|
44
44
|
parent: AnyService | 'self',
|
|
45
|
-
getConfig: (instance: TService) => {},
|
|
45
|
+
getConfig: null | undefined | ((instance: TService) => {}),
|
|
46
46
|
app: TService['app'] | 'self'
|
|
47
47
|
]
|
|
48
48
|
|
|
@@ -94,7 +94,8 @@ export function Route(options: Omit<TControllerDefinition, 'controller'> = {}) {
|
|
|
94
94
|
export default abstract class Service<
|
|
95
95
|
TConfig extends {},
|
|
96
96
|
THooks extends THooksList,
|
|
97
|
-
TApplication extends Application
|
|
97
|
+
TApplication extends Application,
|
|
98
|
+
TParent extends AnyService | Application = Application
|
|
98
99
|
> {
|
|
99
100
|
|
|
100
101
|
public started?: Promise<void>;
|
|
@@ -104,6 +105,7 @@ export default abstract class Service<
|
|
|
104
105
|
public metas!: TServiceMetas;
|
|
105
106
|
public bindings: string[] = []
|
|
106
107
|
|
|
108
|
+
public parent: TParent;
|
|
107
109
|
public app: TApplication;
|
|
108
110
|
public config: TConfig = {} as TConfig;
|
|
109
111
|
|
|
@@ -41,7 +41,7 @@ export type TConfig = {
|
|
|
41
41
|
jwt: {
|
|
42
42
|
// 2048 bits
|
|
43
43
|
key: string,
|
|
44
|
-
expiration:
|
|
44
|
+
expiration: number,
|
|
45
45
|
},
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -173,7 +173,9 @@ export default abstract class AuthService<
|
|
|
173
173
|
|
|
174
174
|
this.config.debug && console.info(LogPrefix, `Generated JWT token for session:` + token);
|
|
175
175
|
|
|
176
|
-
request.res.cookie('authorization', token
|
|
176
|
+
request.res.cookie('authorization', token, {
|
|
177
|
+
maxAge: this.config.jwt.expiration,
|
|
178
|
+
});
|
|
177
179
|
|
|
178
180
|
return token;
|
|
179
181
|
}
|
|
@@ -187,13 +189,30 @@ export default abstract class AuthService<
|
|
|
187
189
|
request.res.clearCookie('authorization');
|
|
188
190
|
}
|
|
189
191
|
|
|
190
|
-
public check(
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
public check(
|
|
193
|
+
request: TRequest,
|
|
194
|
+
role: TUserRole,
|
|
195
|
+
motivation?: string,
|
|
196
|
+
dataForDebug?: { [key: string]: any }
|
|
197
|
+
): TUser;
|
|
198
|
+
|
|
199
|
+
public check(
|
|
200
|
+
request: TRequest,
|
|
201
|
+
role: false,
|
|
202
|
+
motivation?: string,
|
|
203
|
+
dataForDebug?: { [key: string]: any }
|
|
204
|
+
): null;
|
|
205
|
+
|
|
206
|
+
public check(
|
|
207
|
+
request: TRequest,
|
|
208
|
+
role: TUserRole | false = 'USER',
|
|
209
|
+
motivation?: string,
|
|
210
|
+
dataForDebug?: { [key: string]: any }
|
|
211
|
+
): TUser | null {
|
|
193
212
|
|
|
194
213
|
const user = request.user;
|
|
195
214
|
|
|
196
|
-
this.config.debug && console.warn(LogPrefix, `Check auth, role = ${role}. Current user =`, user?.name);
|
|
215
|
+
this.config.debug && console.warn(LogPrefix, `Check auth, role = ${role}. Current user =`, user?.name, motivation);
|
|
197
216
|
|
|
198
217
|
if (user === undefined) {
|
|
199
218
|
|
|
@@ -208,23 +227,18 @@ export default abstract class AuthService<
|
|
|
208
227
|
} else if (user === null) {
|
|
209
228
|
|
|
210
229
|
this.config.debug && console.warn(LogPrefix, "Refusé pour anonyme (" + request.ip + ")");
|
|
211
|
-
throw new AuthRequired('Please login to continue');
|
|
212
|
-
|
|
213
|
-
} else if (user.type !== entity) {
|
|
214
|
-
|
|
215
|
-
this.config.debug && console.warn(LogPrefix, `User type mismatch: ${user.type} (user) vs ${entity} (expected) (${request.ip})`);
|
|
216
|
-
throw new AuthRequired("Your account type doesn't have access to the requested content.");
|
|
230
|
+
throw new AuthRequired('Please login to continue', motivation, dataForDebug);
|
|
217
231
|
|
|
218
232
|
// Insufficient permissions
|
|
219
233
|
} else if (!user.roles.includes(role)) {
|
|
220
234
|
|
|
221
|
-
console.warn(LogPrefix, "Refusé: " + role + " pour " + user.name + " (" + (user.roles || 'role inconnu') + ")");
|
|
235
|
+
this.config.debug && console.warn(LogPrefix, "Refusé: " + role + " pour " + user.name + " (" + (user.roles || 'role inconnu') + ")");
|
|
222
236
|
|
|
223
237
|
throw new Forbidden("You do not have sufficient permissions to access this resource.");
|
|
224
238
|
|
|
225
239
|
} else {
|
|
226
240
|
|
|
227
|
-
console.warn(LogPrefix, "Autorisé " + role + " pour " + user.name + " (" + user.roles + ")");
|
|
241
|
+
this.config.debug && console.warn(LogPrefix, "Autorisé " + role + " pour " + user.name + " (" + user.roles + ")");
|
|
228
242
|
|
|
229
243
|
}
|
|
230
244
|
|
|
@@ -66,7 +66,7 @@ export default class AuthenticationRouterService<
|
|
|
66
66
|
if (route.options.auth !== undefined) {
|
|
67
67
|
|
|
68
68
|
// Basic auth check
|
|
69
|
-
this.users.check(request,
|
|
69
|
+
this.users.check(request, route.options.auth);
|
|
70
70
|
|
|
71
71
|
// Redirect to logged page
|
|
72
72
|
if (route.options.auth === false && request.user && route.options.redirectLogged)
|
|
@@ -45,9 +45,9 @@ export default class UsersRequestService<
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// TODO: return user type according to entity
|
|
48
|
-
public check(
|
|
49
|
-
public check(
|
|
50
|
-
public check(
|
|
51
|
-
return this.users.check( this.request,
|
|
48
|
+
public check(role: TUserRole, motivation?: string, dataForDebug?: {}): TUser;
|
|
49
|
+
public check(role: false, motivation?: string, dataForDebug?: {}): null;
|
|
50
|
+
public check(role: TUserRole | boolean = 'USER', motivation?: string, dataForDebug?: {}): TUser | null {
|
|
51
|
+
return this.users.check( this.request, role, motivation, dataForDebug );
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
- DEPENDANCES
|
|
3
3
|
----------------------------------*/
|
|
4
4
|
|
|
5
|
-
/* NOTE: On évite d'utiliser les alias ici,
|
|
6
|
-
Afin que l'envoi des rapports de bug fonctionne même en cas d'erreur avec les alias
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
5
|
// Core
|
|
10
6
|
import type { Application } from '@server/app';
|
|
11
7
|
import Service from '@server/app/service';
|
|
@@ -13,7 +9,6 @@ import markdown from '@common/data/markdown';
|
|
|
13
9
|
|
|
14
10
|
// Speciic
|
|
15
11
|
import { jsonToHtml } from './utils';
|
|
16
|
-
import type { Transporter } from './transporter';
|
|
17
12
|
|
|
18
13
|
/*----------------------------------
|
|
19
14
|
- SERVICE CONFIG
|
|
@@ -25,16 +20,12 @@ export type Config = {
|
|
|
25
20
|
debug: boolean,
|
|
26
21
|
simulateWhenLocal: boolean,
|
|
27
22
|
default: {
|
|
28
|
-
transporter: string,
|
|
29
23
|
from: TPerson
|
|
30
24
|
},
|
|
31
25
|
bugReport: {
|
|
32
26
|
from: TPerson,
|
|
33
27
|
to: TPerson
|
|
34
28
|
},
|
|
35
|
-
transporters: {
|
|
36
|
-
[transporterId: string]: Transporter
|
|
37
|
-
}
|
|
38
29
|
}
|
|
39
30
|
|
|
40
31
|
export type Hooks = {
|
|
@@ -49,9 +40,7 @@ export type Services = {
|
|
|
49
40
|
- TYPES: EMAILS
|
|
50
41
|
----------------------------------*/
|
|
51
42
|
|
|
52
|
-
export
|
|
53
|
-
|
|
54
|
-
export type TEmail = THtmlEmail | TMarkdownEmail// | TTemplateEmail;
|
|
43
|
+
export type TEmail = THtmlEmail | TMarkdownEmail;
|
|
55
44
|
|
|
56
45
|
type TPerson = {
|
|
57
46
|
name?: string,
|
|
@@ -74,11 +63,6 @@ export type TMarkdownEmail = TBaseEmail & {
|
|
|
74
63
|
markdown: string,
|
|
75
64
|
}
|
|
76
65
|
|
|
77
|
-
/*export type TTemplateEmail = TBaseEmail & {
|
|
78
|
-
template: keyof typeof templates,
|
|
79
|
-
data?: TObjetDonnees
|
|
80
|
-
}*/
|
|
81
|
-
|
|
82
66
|
export type TCompleteEmail = With<THtmlEmail, {
|
|
83
67
|
to: TPerson[],
|
|
84
68
|
from: TPerson,
|
|
@@ -109,14 +93,15 @@ type TOptions = {
|
|
|
109
93
|
/*----------------------------------
|
|
110
94
|
- FONCTIONS
|
|
111
95
|
----------------------------------*/
|
|
112
|
-
export default class Email extends
|
|
113
|
-
|
|
114
|
-
private transporters = this.config.transporters;
|
|
96
|
+
export default abstract class Email<TConfig extends Config>
|
|
97
|
+
extends Service<TConfig, Hooks, Application> {
|
|
115
98
|
|
|
116
99
|
/*----------------------------------
|
|
117
100
|
- ACTIONS
|
|
118
101
|
----------------------------------*/
|
|
119
102
|
|
|
103
|
+
protected abstract sendNow( emails: TCompleteEmail[] ): Promise<void>;
|
|
104
|
+
|
|
120
105
|
public async send( to: string, subject: string, markdown: string, options?: TOptions );
|
|
121
106
|
public async send( emails: TEmail | TEmail[], options?: TOptions ): Promise<void>;
|
|
122
107
|
public async send( ...args: TEmailSendArgs ): Promise<void> {
|
|
@@ -162,30 +147,7 @@ export default class Email extends Service<Config, Hooks, Application> {
|
|
|
162
147
|
? email.to
|
|
163
148
|
: [email.to];
|
|
164
149
|
|
|
165
|
-
|
|
166
|
-
// TODO: Restore templates feature
|
|
167
|
-
/*if ('template' in email) {
|
|
168
|
-
|
|
169
|
-
const template = templates[email.template];
|
|
170
|
-
|
|
171
|
-
if (template === undefined)
|
|
172
|
-
throw new Error(`Impossible de charger la template email ${email.template} depuis le cache (NotFound).`);
|
|
173
|
-
|
|
174
|
-
const txt = template(email.data || {})
|
|
175
|
-
|
|
176
|
-
const delimTitre = txt.indexOf('\n\n');
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
...email,
|
|
180
|
-
// Vire le "> " au début
|
|
181
|
-
subject: txt.substring(2, delimTitre),
|
|
182
|
-
html: htmlWarning + txt.substring(delimTitre + 2),
|
|
183
|
-
from,
|
|
184
|
-
to,
|
|
185
|
-
cc
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
} else */if ('markdown' in email) {
|
|
150
|
+
if ('markdown' in email) {
|
|
189
151
|
|
|
190
152
|
return {
|
|
191
153
|
...email,
|
|
@@ -209,11 +171,7 @@ export default class Email extends Service<Config, Hooks, Application> {
|
|
|
209
171
|
|
|
210
172
|
});
|
|
211
173
|
|
|
212
|
-
|
|
213
|
-
if (transporterName === undefined)
|
|
214
|
-
throw new Error(`Please define at least one mail transporter.`);
|
|
215
|
-
|
|
216
|
-
console.info(LogPrefix, `Sending ${emailsToSend.length} emails via transporter "${transporterName}"`, emailsToSend[0].subject);
|
|
174
|
+
console.info(LogPrefix, `Sending ${emailsToSend.length} emails:`, emailsToSend[0].subject);
|
|
217
175
|
|
|
218
176
|
// Pas d'envoi d'email quand local
|
|
219
177
|
if (this.app.env.name === 'local' && this.config.simulateWhenLocal === true) {
|
|
@@ -224,8 +182,7 @@ export default class Email extends Service<Config, Hooks, Application> {
|
|
|
224
182
|
return;
|
|
225
183
|
}
|
|
226
184
|
|
|
227
|
-
|
|
228
|
-
await transporter.send(emailsToSend);
|
|
185
|
+
await this.sendNow(emailsToSend);
|
|
229
186
|
|
|
230
187
|
}
|
|
231
188
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { Prisma, PrismaClient } from '@models/types';
|
|
2
|
+
import * as runtime from '@/var/prisma/runtime/library.js';
|
|
3
|
+
|
|
4
|
+
export type TWithStats = {
|
|
5
|
+
$table: string,
|
|
6
|
+
$key: string
|
|
7
|
+
} & {
|
|
8
|
+
[key: string]: string // key => SQL
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type TSubset = (...a: any[]) => Prisma.ProspectContactLeadFindFirstArgs & {
|
|
12
|
+
withStats?: TWithStats
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default class Facet<
|
|
16
|
+
D extends {
|
|
17
|
+
findMany(args?: any): Promise<any>
|
|
18
|
+
findFirst(args?: any): Promise<any>
|
|
19
|
+
},
|
|
20
|
+
S extends TSubset,
|
|
21
|
+
R
|
|
22
|
+
> {
|
|
23
|
+
constructor(
|
|
24
|
+
|
|
25
|
+
private readonly prisma: PrismaClient,
|
|
26
|
+
|
|
27
|
+
private readonly delegate: D,
|
|
28
|
+
private readonly subset: S,
|
|
29
|
+
|
|
30
|
+
/* the **ONLY** line that changed ↓↓↓ */
|
|
31
|
+
private readonly transform?: (
|
|
32
|
+
row: runtime.Types.Result.GetResult<
|
|
33
|
+
Prisma.$ProspectContactLeadPayload,
|
|
34
|
+
ReturnType<S>,
|
|
35
|
+
'findMany'
|
|
36
|
+
>[number]
|
|
37
|
+
) => R,
|
|
38
|
+
) { }
|
|
39
|
+
|
|
40
|
+
public async findMany(
|
|
41
|
+
...args: Parameters<S>
|
|
42
|
+
): Promise<R[]> {
|
|
43
|
+
|
|
44
|
+
const { withStats, ...subset } = this.subset(...args);
|
|
45
|
+
|
|
46
|
+
const results = await this.delegate.findMany(subset);
|
|
47
|
+
|
|
48
|
+
// Load stats
|
|
49
|
+
const stats = withStats
|
|
50
|
+
? await this.fetchStats( withStats, results )
|
|
51
|
+
: [];
|
|
52
|
+
|
|
53
|
+
return results.map(row => this.transformResult(row, stats, withStats));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public async findFirst(
|
|
57
|
+
...args: Parameters<S>
|
|
58
|
+
): Promise<R | null> {
|
|
59
|
+
|
|
60
|
+
const { withStats, ...subset } = this.subset(...args);
|
|
61
|
+
|
|
62
|
+
const result = await this.delegate.findFirst(subset);
|
|
63
|
+
|
|
64
|
+
const stats = withStats
|
|
65
|
+
? await this.fetchStats( withStats, [result] )
|
|
66
|
+
: [];
|
|
67
|
+
|
|
68
|
+
return result ? this.transformResult(result, stats, withStats) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private async fetchStats(
|
|
72
|
+
{ $table, $key, ...withStats }: TWithStats,
|
|
73
|
+
results: any[]
|
|
74
|
+
): Promise<any[]> {
|
|
75
|
+
|
|
76
|
+
const select = Object.entries(withStats).map(([key, sql]) =>
|
|
77
|
+
`(COALESCE((
|
|
78
|
+
${sql}
|
|
79
|
+
), 0)) as ${key}`
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const stats = await this.prisma.$queryRawUnsafe(`
|
|
83
|
+
SELECT ${$key}, ${select.join(', ')}
|
|
84
|
+
FROM ${$table}
|
|
85
|
+
WHERE ${$key} IN (
|
|
86
|
+
${results.map(r => "'" + r[ $key ] + "'").join(',')}
|
|
87
|
+
)
|
|
88
|
+
`);
|
|
89
|
+
|
|
90
|
+
for (const stat of stats) {
|
|
91
|
+
for (const key in stat) {
|
|
92
|
+
|
|
93
|
+
if (key === $key)
|
|
94
|
+
continue;
|
|
95
|
+
|
|
96
|
+
stat[key] = stat[key] ? parseInt(stat[key]) : 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return stats;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private transformResult( result: any, stats: any[], withStats?: TWithStats ) {
|
|
104
|
+
|
|
105
|
+
// Transform stats
|
|
106
|
+
const resultStats = withStats
|
|
107
|
+
? stats.find(stat => stat[withStats.$key] === result[withStats.$key]) || {}
|
|
108
|
+
: {};
|
|
109
|
+
|
|
110
|
+
if (this.transform)
|
|
111
|
+
result = this.transform(result);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
...result,
|
|
115
|
+
...resultStats
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -9,6 +9,9 @@ import { PrismaClient } from '@/var/prisma';
|
|
|
9
9
|
import type { Application } from '@server/app';
|
|
10
10
|
import Service from '@server/app/service';
|
|
11
11
|
|
|
12
|
+
// Specific
|
|
13
|
+
import Facet, { TSubset } from './Facet';
|
|
14
|
+
|
|
12
15
|
/*----------------------------------
|
|
13
16
|
- TYPES
|
|
14
17
|
----------------------------------*/
|
|
@@ -37,10 +40,31 @@ export type Services = {
|
|
|
37
40
|
export default class ModelsManager extends Service<Config, Hooks, Application> {
|
|
38
41
|
|
|
39
42
|
public client = new PrismaClient();
|
|
43
|
+
|
|
44
|
+
public async ready() {
|
|
45
|
+
|
|
46
|
+
await this.client.$executeRaw`SET time_zone = '+00:00'`;
|
|
47
|
+
|
|
48
|
+
}
|
|
40
49
|
|
|
41
50
|
public async shutdown() {
|
|
42
51
|
await this.client.$disconnect()
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
public Facet<
|
|
55
|
+
D extends {
|
|
56
|
+
findMany(args?: any): Promise<any>
|
|
57
|
+
findFirst(args?: any): Promise<any>
|
|
58
|
+
},
|
|
59
|
+
S extends TSubset,
|
|
60
|
+
R
|
|
61
|
+
>(...args: [D, S, R]) {
|
|
62
|
+
|
|
63
|
+
return new Facet(
|
|
64
|
+
this.client,
|
|
65
|
+
...args
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
45
69
|
|
|
46
70
|
}
|