5htp-core 0.2.4 → 0.2.5-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.
@@ -1,8 +1,8 @@
1
1
  /*----------------------------------
2
2
  - DEPENDANCES
3
3
  ----------------------------------*/
4
-
5
4
  // Npm
5
+ import { v4 as uuid } from 'uuid';
6
6
  import { Logger, ILogObject } from "tslog";
7
7
  import { format as formatSql } from 'sql-formatter';
8
8
  import highlight from 'cli-highlight';
@@ -11,10 +11,10 @@ import highlight from 'cli-highlight';
11
11
  import Application, { Service, TPriority } from '@server/app';
12
12
  import context from '@server/context';
13
13
  import type ServerRequest from '@server/services/router/request';
14
+ import { SqlError } from '@server/services/database/debug';
14
15
 
15
16
  // Specific
16
17
  import logToHTML from './html';
17
- import BugReporter from "./bugReporter";
18
18
 
19
19
  /*----------------------------------
20
20
  - SERVICE CONFIG
@@ -23,6 +23,8 @@ import BugReporter from "./bugReporter";
23
23
  type TLogProfile = 'silly' | 'info' | 'warn' | 'error'
24
24
 
25
25
  export type Config = {
26
+ debug?: boolean,
27
+ bufferLimit: number,
26
28
  dev: {
27
29
  level: TLogProfile,
28
30
  },
@@ -77,14 +79,35 @@ export type TQueryLogs = ChannelInfos & {
77
79
  time: number,
78
80
  }
79
81
 
80
- export type TLog = ChannelInfos & {
82
+ export type TLog = ILogObject & ChannelInfos
83
+
84
+ /*----------------------------------
85
+ - TYPES: BUG REPORT
86
+ ----------------------------------*/
87
+ export type ServerBug = {
88
+ // Context
89
+ hash: string,
90
+ date: Date, // Timestamp
91
+ channelType?: string,
92
+ channelId?: string,
81
93
 
94
+ user: string | null | undefined,
95
+ ip: string | null | undefined,
96
+
97
+ // Error
98
+ error: Error,
99
+ stacktrace: string,
100
+ logs: string,
82
101
  }
83
102
 
84
103
  /*----------------------------------
85
104
  - CONST
86
105
  ----------------------------------*/
87
106
 
107
+ const LogPrefix = '[console]'
108
+
109
+ const errorMailInterval = (1 * 60 * 60 * 1000); // 1 hour
110
+
88
111
  const logFields = [
89
112
  'date',
90
113
  'logLevelId',
@@ -99,7 +122,7 @@ const logFields = [
99
122
  'lineNumber',
100
123
  'argumentsArray',
101
124
  'stack',
102
- ]
125
+ ] as const
103
126
 
104
127
  /*----------------------------------
105
128
  - LOGGER
@@ -111,13 +134,14 @@ export default class Console extends Service<Config, Hooks, Application> {
111
134
 
112
135
  // Services
113
136
  public logger!: Logger;
114
- public bugReport = new BugReporter(this);
115
137
 
116
138
  // Buffers
117
139
  public logs: TLog[] = [];
118
140
  public clients: TGuestLogs[] = [];
119
141
  public requests: TRequestLogs[] = [];
120
142
  public sqlQueries: TQueryLogs[] = [];
143
+ // Bug ID => Timestamp latest send
144
+ private sentBugs: {[bugId: string]: number} = {};
121
145
 
122
146
  // Adapters
123
147
  public log = console.log;
@@ -159,20 +183,75 @@ export default class Console extends Service<Config, Hooks, Application> {
159
183
  fatal: this.logEntry.bind(this),
160
184
  }, envConfig.level);
161
185
 
162
- setInterval(() => this.clean(), 60000);
186
+ setInterval(() => this.clean(), 10000);
163
187
 
164
188
  // Send email report
165
- this.app.on('error', (error: Error, request?: ServerRequest) => this.bugReport.server(error, request));
189
+ this.app.on('error', this.createBugReport.bind(this));
166
190
  }
167
191
 
168
192
  private clean() {
169
- // Clean memory from old logs
193
+ this.config.debug && console.log(LogPrefix, `Clean logs buffer. Current size:`, this.logs.length, '/', this.config.bufferLimit);
194
+ const bufferOverflow = this.logs.length - this.config.bufferLimit;
195
+ if (bufferOverflow > 0)
196
+ this.logs = this.logs.slice(bufferOverflow);
170
197
  }
171
198
 
172
199
  /*----------------------------------
173
200
  - LOGGING
174
201
  ----------------------------------*/
175
202
 
203
+ public async createBugReport( error: Error, request?: ServerRequest ) {
204
+
205
+ // Print the error so it's accessible via logs
206
+ if (error instanceof SqlError) {
207
+ let printedQuery: string;
208
+ try {
209
+ printedQuery = this.printSql( error.query );
210
+ } catch (error) {
211
+ printedQuery = 'Failed to print query:' + (error || 'unknown error');
212
+ }
213
+ console.error(`Error caused by this query:`, printedQuery);
214
+ }
215
+ console.error(LogPrefix, `Sending bug report for the following error:`, error);
216
+
217
+ // Prevent spamming the mailbox if infinite loop
218
+ const bugId = ['server', request?.user?.name, undefined, error.message].filter(e => !!e).join('::');
219
+ const lastSending = this.sentBugs[bugId];
220
+ this.sentBugs[bugId] = Date.now();
221
+ const shouldSendReport = lastSending === undefined || lastSending < Date.now() - errorMailInterval;
222
+ if (!shouldSendReport)
223
+ return;
224
+
225
+ // Get context
226
+ const now = new Date();
227
+ const hash = uuid();
228
+ const { channelType, channelId } = this.getChannel();
229
+
230
+ // On envoi l'email avant l'insertion dans bla bdd
231
+ // Car cette denrière a plus de chances de provoquer une erreur
232
+ const logsHtml = this.printHtml(
233
+ this.logs.filter(e => e.channelId === channelId),
234
+ true
235
+ );
236
+
237
+ const bugReport: ServerBug = {
238
+ // Context
239
+ hash: hash,
240
+ date: now,
241
+ channelType,
242
+ channelId,
243
+ // User
244
+ user: request?.user?.email,
245
+ ip: request?.ip,
246
+ // Error
247
+ error,
248
+ stacktrace: error.stack || error.message,
249
+ logs: logsHtml
250
+ }
251
+
252
+ await this.runHook('bugReport', bugReport);
253
+ }
254
+
176
255
  public getChannel() {
177
256
  return context.getStore() || {
178
257
  channelType: 'master',
@@ -182,6 +261,7 @@ export default class Console extends Service<Config, Hooks, Application> {
182
261
 
183
262
  private logEntry(entry: ILogObject) {
184
263
 
264
+ // Don't keep logs from the admin sashboard
185
265
  const [channelType, channelId] = entry.requestId?.split(':') || ['master'];
186
266
  if (entry.requestId === 'admin')
187
267
  return;
@@ -192,22 +272,6 @@ export default class Console extends Service<Config, Hooks, Application> {
192
272
  for (const k of logFields)
193
273
  miniLog[k] = entry[k];
194
274
 
195
- // remove webpack path
196
- if (miniLog.filePath !== undefined) {
197
-
198
- const appPrefix = '/webpack:/' + this.app.pkg.name + '/src/';
199
- const appPrefixIndex = miniLog.filePath.indexOf(appPrefix);
200
-
201
- const corePrefix = '/webpack:/' + this.app.pkg.name + '/node_modules/5htp-core/src/';
202
- const corePrefixIndex = miniLog.filePath.indexOf(corePrefix);
203
-
204
- if (appPrefixIndex !== -1)
205
- miniLog.filePath = '@/' + miniLog.filePath.substring(appPrefixIndex + appPrefix.length);
206
- else if (corePrefixIndex !== -1)
207
- miniLog.filePath = '@' + miniLog.filePath.substring(corePrefixIndex + corePrefix.length);
208
-
209
- }
210
-
211
275
  this.logs.push(miniLog as TLog);
212
276
  }
213
277
 
@@ -285,11 +349,30 @@ export default class Console extends Service<Config, Hooks, Application> {
285
349
  if (channelId !== undefined)
286
350
  filters.channelId = channelId;
287
351
 
288
- const fromBuffer = this.logs.filter(
289
- e => e.channelId === channelId && e.channelType === channelType
290
- ).reverse();
352
+ const entries: TLog[] = []
353
+ for (const log of this.logs) {
354
+
355
+ // Filters
356
+ if (!(log.channelId === channelId && log.channelType === channelType))
357
+ continue;
358
+
359
+ // Remove path prefixs
360
+ if (log.filePath !== undefined) {
361
+
362
+ const appPrefix = '/webpack:/' + this.app.pkg.name + '/src/';
363
+ const appPrefixIndex = log.filePath.indexOf(appPrefix);
364
+
365
+ const corePrefix = '/webpack:/' + this.app.pkg.name + '/node_modules/5htp-core/src/';
366
+ const corePrefixIndex = log.filePath.indexOf(corePrefix);
367
+
368
+ if (appPrefixIndex !== -1)
369
+ log.filePath = '@/' + log.filePath.substring(appPrefixIndex + appPrefix.length);
370
+ else if (corePrefixIndex !== -1)
371
+ log.filePath = '@' + log.filePath.substring(corePrefixIndex + corePrefix.length);
372
+ }
373
+ }
291
374
 
292
- return this.printHtml(fromBuffer);
375
+ return this.printHtml( entries.reverse() );
293
376
  }
294
377
 
295
378
  public printHtml(logs: TLog[], full: boolean = false): string {
@@ -8,6 +8,7 @@ import mysql from 'mysql2/promise';
8
8
  // Core: general
9
9
  import Application from '@server/app';
10
10
  import Service from '@server/app/service';
11
+ import { Anomaly } from '@common/errors';
11
12
 
12
13
  // Core: specific
13
14
  import { SqlError } from './debug';
@@ -26,20 +27,17 @@ const LogPrefix = '[database][connection]';
26
27
  - SERVICE CONFIG
27
28
  ----------------------------------*/
28
29
 
30
+ type ConnectionConfig = {
31
+ name: string,
32
+ databases: string[],
33
+ host: string,
34
+ port: number,
35
+ login: string,
36
+ password: string,
37
+ }
38
+
29
39
  export type DatabaseServiceConfig = {
30
- list: string[],
31
- dev: {
32
- host: string,
33
- port: number,
34
- login: string,
35
- password: string,
36
- },
37
- prod: {
38
- host: string,
39
- port: number,
40
- login: string,
41
- password: string,
42
- }
40
+ connections: ConnectionConfig[]
43
41
  }
44
42
 
45
43
  export type THooks = {
@@ -71,10 +69,11 @@ type TSelectQueryResult = any;
71
69
  /*----------------------------------
72
70
  - SERVICES
73
71
  ----------------------------------*/
74
- export default class DatabaseConnection extends Service<DatabaseServiceConfig, THooks, Application> {
72
+ export default class DatabaseManager extends Service<DatabaseServiceConfig, THooks, Application> {
75
73
 
76
74
  private initialized = false;
77
75
  public connection!: mysql.Pool;
76
+ public connectionConfig?: ConnectionConfig;
78
77
 
79
78
  public tables: TDatabasesList = {};
80
79
  public metas = new MetadataParser(this);
@@ -92,37 +91,47 @@ export default class DatabaseConnection extends Service<DatabaseServiceConfig, T
92
91
 
93
92
  this.initialized = false;
94
93
 
95
- this.app.on('cleanup', () => this.cleanup());
94
+ // Try to connect to one of the databases
95
+ const connectionErrors: string[] = []
96
+ for (const connectionConfig of this.config.connections){
97
+ try {
98
+ await this.connect(connectionConfig)
99
+ break;
100
+ } catch (error) {
101
+ console.warn(LogPrefix, `Failed to connect to ${connectionConfig.name}: ` + error);
102
+ connectionErrors.push(connectionConfig.name + ': ' + error);
103
+ }
104
+ }
96
105
 
97
- this.connection = await this.connect();
106
+ // Coudnt connect to any database
107
+ if (this.connectionConfig === undefined)
108
+ throw new Anomaly(`Couldnt connect to any database.`, { connectionErrors });
98
109
 
99
- this.tables = await this.metas.load( this.config.list );
110
+ // Disconnect from the database when the app is terminated
111
+ this.app.on('cleanup', () => this.disconnect());
100
112
 
113
+ // Ready to make queries
101
114
  this.initialized = true;
102
-
103
115
  }
104
116
 
105
- public async cleanup() {
117
+ public async disconnect() {
106
118
  return this.connection.end();
107
119
  }
108
120
 
109
121
  /*----------------------------------
110
122
  - INIT
111
123
  ----------------------------------*/
112
- public async connect() {
124
+ public async connect(config: ConnectionConfig) {
113
125
 
114
- console.info(LogPrefix, `Connecting to databases ...`);
115
-
116
- const creds = this.config[ this.app.env.profile ];
117
-
118
- return await mysql.createPool({
126
+ console.info(LogPrefix, `Trying to connect to ${config.name} ...`);
127
+ this.connection = mysql.createPool({
119
128
 
120
129
  // Identification
121
- host: creds.host,
122
- port: creds.port,
123
- user: creds.login,
124
- password: creds.password,
125
- database: this.config.list[0],
130
+ host: config.host,
131
+ port: config.port,
132
+ user: config.login,
133
+ password: config.password,
134
+ database: config.databases[0],
126
135
 
127
136
  // Pool
128
137
  waitForConnections: true,
@@ -147,7 +156,12 @@ export default class DatabaseConnection extends Service<DatabaseServiceConfig, T
147
156
  //console.info(LogPrefix, 'queryFormat', query);
148
157
  return query;
149
158
  }
150
- });
159
+ })
160
+
161
+ this.connectionConfig = config;
162
+
163
+ this.tables = await this.metas.load( config.databases );
164
+ console.info(LogPrefix, `Successfully connected to ${config.name}.`);
151
165
  }
152
166
 
153
167
  private typeCast( field: mysql.Field, next: Function ) {
@@ -226,13 +240,16 @@ export default class DatabaseConnection extends Service<DatabaseServiceConfig, T
226
240
 
227
241
  public getTable( path: string ): TMetasTable {
228
242
 
243
+ if (this.connectionConfig === undefined)
244
+ throw new Error(`No connection has been initialised.`);
245
+
229
246
  // Parse path
230
247
  let db: string, table: string;
231
248
  if (path.includes('.'))
232
249
  ([db, table] = path.split('.'));
233
250
  else {
234
251
  // Only the table = use the main database (first of the list in the config)
235
- db = this.config.list[0];
252
+ db = this.connectionConfig.databases[0];
236
253
  table = path;
237
254
  }
238
255
 
@@ -27,7 +27,7 @@ import BaseRouter, {
27
27
  TRouteOptions, defaultOptions
28
28
  } from '@common/router';
29
29
  import { buildRegex, getRegisterPageArgs } from '@common/router/register';
30
- import { layoutsList } from '@common/router/layouts';
30
+ import { layoutsList, getLayout } from '@common/router/layouts';
31
31
  import { TFetcherList, TFetcher } from '@common/router/request/api';
32
32
  import type { TFrontRenderer } from '@common/router/response/page';
33
33
  import type { TSsrUnresolvedRoute, TRegisterPageArgs } from '@client/services/router';
@@ -147,7 +147,6 @@ export default class ServerRouter<
147
147
  // Since route registering requires all services to be ready,
148
148
  // We load routes only when all services are ready
149
149
  this.app.on('ready', async () => {
150
-
151
150
  // Use require to avoid circular references
152
151
  this.registerRoutes([
153
152
  ...require("metas:@/server/routes/**/*.ts"),
@@ -228,9 +227,13 @@ export default class ServerRouter<
228
227
  options: TRoute["options"],
229
228
  renderer: TFrontRenderer<{}, { message: string }>
230
229
  ) {
230
+
231
+ // Automatic layout form the nearest _layout folder
232
+ const layout = getLayout('Error ' + code, options);
233
+
231
234
  this.errors[code] = {
232
235
  code,
233
- controller: (context: TRouterContext<this>) => new Page(null, renderer, context),
236
+ controller: (context: TRouterContext<this>) => new Page(null, renderer, context, layout),
234
237
  options
235
238
  };
236
239
  }
@@ -503,7 +506,7 @@ declare type Routes = {
503
506
  return response;
504
507
  }
505
508
 
506
- throw new NotFound(`The requested endpoint was not found.`);
509
+ throw new NotFound();
507
510
  }
508
511
 
509
512
  private async resolveApiBatch( fetchers: TFetcherList, request: ServerRequest<this> ) {
@@ -542,9 +545,14 @@ declare type Routes = {
542
545
  // Rapport / debug
543
546
  if (code === 500) {
544
547
 
548
+ // Report error
545
549
  await this.app.runHook('error', e, request);
546
550
 
547
- // Pour déboguer les erreurs HTTP
551
+ // Don't exose technical errors to users
552
+ if (this.app.env.profile === 'prod')
553
+ e.message = "We encountered an internal error, and our team has just been notified. Sorry for the inconvenience.";
554
+
555
+ // Pour déboguer les erreurs HTTP
548
556
  } else if (this.app.env.profile === "dev")
549
557
  console.warn(e);
550
558
 
@@ -1,13 +0,0 @@
1
- #page {
2
- transition: opacity 5s cubic-bezier(0,.3,0,.99);
3
- animation: showMain 1s ease-out;
4
- @keyframes showMain {
5
- 0% { opacity: 0.3; }
6
- 100% { opacity: 1; }
7
- }
8
-
9
- body.loading & {
10
- cursor: wait;
11
- opacity: 0.1;
12
- }
13
- }
@@ -1,5 +0,0 @@
1
- body {
2
-
3
-
4
-
5
- }
@@ -1,55 +0,0 @@
1
- /*----------------------------------
2
- - DEPENDANCES
3
- ----------------------------------*/
4
-
5
- // Npm
6
- import React from 'react';
7
-
8
- // Core
9
-
10
- // Core components
11
- import Button, { Props as ButtonProps } from '@client/components/button';
12
- import useHeader from '@client/pages/useHeader';
13
-
14
- /*----------------------------------
15
- - CONTROLEUR
16
- ----------------------------------*/
17
- import './index.less';
18
- export default ({ headline, subtitle, cta, preview }: {
19
-
20
- headline: string,
21
- subtitle: string,
22
- cta: ButtonProps,
23
- preview: [string, string]
24
-
25
- }) => {
26
-
27
- useHeader({
28
- title: headline,
29
- subtitle: subtitle,
30
- });
31
-
32
- /*----------------------------------
33
- - RENDER
34
- ----------------------------------*/
35
- return (
36
- <div id="landing">
37
-
38
- <header>
39
- <div class="text card">
40
- <h1>{headline}</h1>
41
- <p>
42
- {subtitle}
43
- </p>
44
- <Button type="guide" {...cta} />
45
- </div>
46
-
47
- <figure>
48
- <img class="image" src={preview[0]} />
49
- <legend>{preview[1]}</legend>
50
- </figure>
51
- </header>
52
-
53
- </div>
54
- )
55
- }