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.
Files changed (48) hide show
  1. package/client/app/component.tsx +1 -0
  2. package/client/assets/css/colors.less +46 -25
  3. package/client/assets/css/components/button.less +14 -5
  4. package/client/assets/css/components/card.less +5 -10
  5. package/client/assets/css/components/mantine.less +6 -5
  6. package/client/assets/css/components/table.less +1 -1
  7. package/client/assets/css/text/icons.less +1 -1
  8. package/client/assets/css/text/text.less +4 -0
  9. package/client/assets/css/utils/borders.less +1 -1
  10. package/client/assets/css/utils/layouts.less +8 -5
  11. package/client/components/Button.tsx +20 -17
  12. package/client/components/Checkbox.tsx +6 -1
  13. package/client/components/ConnectedInput.tsx +34 -0
  14. package/client/components/DropDown.tsx +21 -4
  15. package/client/components/Input.tsx +2 -2
  16. package/client/components/Rte/Editor.tsx +23 -9
  17. package/client/components/Rte/ToolbarPlugin/ElementFormat.tsx +1 -1
  18. package/client/components/Rte/ToolbarPlugin/index.tsx +272 -183
  19. package/client/components/Rte/currentEditor.ts +31 -2
  20. package/client/components/Rte/index.tsx +3 -0
  21. package/client/components/Rte/plugins/FloatingTextFormatToolbarPlugin/index.tsx +4 -1
  22. package/client/components/Select.tsx +29 -16
  23. package/client/components/Table/index.tsx +27 -11
  24. package/client/components/containers/Popover/index.tsx +21 -4
  25. package/client/components/index.ts +4 -2
  26. package/client/services/router/index.tsx +7 -5
  27. package/common/errors/index.tsx +28 -3
  28. package/common/router/index.ts +4 -1
  29. package/common/utils/rte.ts +183 -0
  30. package/package.json +3 -2
  31. package/server/app/container/console/index.ts +92 -62
  32. package/server/app/container/index.ts +4 -0
  33. package/server/app/service/index.ts +4 -2
  34. package/server/services/auth/index.ts +28 -14
  35. package/server/services/auth/router/index.ts +1 -1
  36. package/server/services/auth/router/request.ts +4 -4
  37. package/server/services/email/index.ts +8 -51
  38. package/server/services/prisma/Facet.ts +118 -0
  39. package/server/services/prisma/index.ts +24 -0
  40. package/server/services/router/http/index.ts +0 -2
  41. package/server/services/router/index.ts +220 -86
  42. package/server/services/router/response/index.ts +0 -15
  43. package/server/utils/rte.ts +21 -132
  44. package/types/global/utils.d.ts +4 -22
  45. package/types/icons.d.ts +1 -1
  46. package/server/services/email/service.json +0 -6
  47. package/server/services/email/templates.ts +0 -49
  48. 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, Anomaly, CoreError } from '@common/errors';
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
- // Bug ID => Timestamp latest send
148
- private sentBugs: {[bugId: string]: number} = {};
149
-
150
- // Old (still useful???)
151
- /*public clients: TGuestLogs[] = [];
152
- public requests: TRequestLogs[] = [];
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
- public async createBugReport( error: Error | CoreError | Anomaly, request?: ServerRequest ) {
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
- // Print error
289
- this.logger.error(LogPrefix, `Sending bug report for the following error:`, error);
290
- /*const youchRes = new Youch(error, {});
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
- // Defaults to single whitspace
297
- prefix: ' ',
297
+ // Get context
298
+ const now = new Date();
299
+ const { channelType, channelId } = this.getChannel();
298
300
 
299
- // Defaults to false
300
- hideErrorTitle: false,
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
- // Defaults to false
303
- hideMessage: false,
307
+ let currentError: TCatchedError | undefined = error;
308
+ let title: string | undefined;
309
+ while (currentError !== undefined) {
304
310
 
305
- // Defaults to false
306
- displayMainFrameOnly: false,
311
+ if (title === undefined)
312
+ title = currentError.message;
307
313
 
308
- // Defaults to 3
309
- framesMaxLimit: 3,
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
- const application = this.container.application;
313
- if (application === undefined)
314
- return console.error(LogPrefix, "Can't send bug report because the application is not instanciated");
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
- // Print the error so it's accessible via logs
317
- if (error instanceof SqlError) {
318
- let printedQuery: string;
319
- try {
320
- printedQuery = this.printSql( error.query );
321
- } catch (error) {
322
- printedQuery = 'Failed to print query:' + (error || 'unknown error');
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
- console.error(`Error caused by this query:`, printedQuery);
334
+
335
+ // Go deeper
336
+ currentError = 'originalError' in currentError
337
+ ? currentError.originalError
338
+ : undefined
325
339
  }
326
340
 
327
- if (('dataForDebugging' in error) && error.dataForDebugging !== undefined)
328
- console.error(LogPrefix, `More data about the error:`, error.dataForDebugging);
341
+ // Genertae unique error hash
342
+ const hash = md5( stacktraces.join('\n') );
329
343
 
330
- // Prevent spamming the mailbox if infinite loop
331
- const bugId = ['server', request?.user?.name, undefined, error.message].filter(e => !!e).join('::');
332
- const lastSending = this.sentBugs[bugId];
333
- this.sentBugs[bugId] = Date.now();
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
- // Get context
339
- const now = new Date();
340
- const hash = uuid();
341
- const { channelType, channelId } = this.getChannel();
349
+ this.reported[hash] = {
350
+ times: 0,
351
+ last: new Date()
352
+ }
342
353
 
343
- // On envoi l'email avant l'insertion dans bla bdd
344
- // Car cette denrière a plus de chances de provoquer une erreur
345
- const logs = this.logs.filter(e => e.channel.channelId === channelId).slice(-100);
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
- error,
375
- stacktrace: error.stack || error.message,
376
- logs
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
- <b>Error</b>: ${report.error.message}<br />
399
- ${this.printHtml(report.stacktrace)}<br />
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.runtime.prettyFormatLogObj(log.args, this.logger.settings);
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");
@@ -2,6 +2,10 @@
2
2
  - DEPENDANCES
3
3
  ----------------------------------*/
4
4
 
5
+ // Set timezone
6
+ process.env.TZ = 'UTC';
7
+ import 'source-map-support/register';
8
+
5
9
  // Npm
6
10
  import path from 'path';
7
11
 
@@ -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: string,
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( request: TRequest, entity: string, role: TUserRole): TUser;
191
- public check( request: TRequest, entity: string, role: false): null;
192
- public check( request: TRequest, entity: string, role: TUserRole | false = 'USER'): TUser | null {
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, 'User', route.options.auth);
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( entity: string, role: TUserRole, motivation?: string): TUser;
49
- public check( entity: string, role: false, motivation?: string): null;
50
- public check( entity: string, role: TUserRole | boolean = 'USER', motivation?: string): TUser | null {
51
- return this.users.check( this.request, entity, role, motivation );
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 { Transporter } from './transporter';
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 Service<Config, Hooks, Application> {
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
- // Via template
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
- const transporterName = options.transporter || this.config.default.transporter;
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
- const transporter = this.transporters[ transporterName ];
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
  }