• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

errsole / errsole-mysql / #14

19 Feb 2025 07:08AM UTC coverage: 91.612% (-2.1%) from 93.74%
#14

push

venki91
refactor: remove comments

185 of 217 branches covered (85.25%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

17 existing lines in 1 file now uncovered.

372 of 391 relevant lines covered (95.14%)

10.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.61
/lib/index.js
1
/**
2
 * @typedef {Object} Log
3
 * @property {number} [id]
4
 * @property {number} [errsole_id]
5
 * @property {Date} timestamp
6
 * @property {string} hostname
7
 * @property {string} source
8
 * @property {string} level
9
 * @property {string} message
10
 * @property {string} [meta]
11
 */
12

13
/**
14
 * @typedef {Object} LogFilter
15
 * @property {number} [lt_id]
16
 * @property {number} [gt_id]
17
 * @property {number} [errsole_id]
18
 * @property {Date} [lte_timestamp]
19
 * @property {Date} [gte_timestamp]
20
 * @property {string[]} [hostnames]
21
 * @property {{source: string, level: string}[]} [level_json]
22
 * @property {number} [limit=100]
23
 */
24

25
/**
26
 * @typedef {Object} Config
27
 * @property {number} id
28
 * @property {string} key
29
 * @property {string} value
30
 */
31

32
/**
33
 * @typedef {Object} User
34
 * @property {number} id
35
 * @property {string} name
36
 * @property {string} email
37
 * @property {string} role
38
 */
39

40
/**
41
 * @typedef {Object} Notification
42
 * @property {number} [id]
43
 * @property {number} [errsole_id]
44
 * @property {string} hostname
45
 * @property {string} hashed_message
46
 * @property {Date} [created_at]
47
 * @property {Date} [updated_at]
48
 */
49

50
const bcrypt = require('bcryptjs');
1✔
51
const { EventEmitter } = require('events');
1✔
52
const mysql = require('mysql2');
1✔
53
const cron = require('node-cron');
1✔
54

55
class ErrsoleMySQL extends EventEmitter {
56
  constructor (options = {}) {
×
57
    super();
101✔
58

59
    let { tablePrefix, ...mysqlOptions } = options;
101✔
60
    tablePrefix = tablePrefix ? `errsole_${tablePrefix.toLowerCase().replace(/[^a-z0-9]/g, '')}` : 'errsole';
101!
61

62
    this.isConnectionInProgress = true;
101✔
63
    this.pool = mysql.createPool(mysqlOptions);
101✔
64
    this.logsTable = `${tablePrefix}_logs_v3`;
101✔
65
    this.usersTable = `${tablePrefix}_users`;
101✔
66
    this.configTable = `${tablePrefix}_config`;
101✔
67
    this.notificationsTable = `${tablePrefix}_notifications`;
101✔
68

69
    this.name = require('../package.json').name;
101✔
70
    this.version = require('../package.json').version || '0.0.0';
101!
71

72
    this.pendingLogs = [];
101✔
73
    this.batchSize = 100;
101✔
74
    this.flushInterval = 1000;
101✔
75

76
    this.initialize();
101✔
77
  }
78

79
  async initialize () {
80
    await this.checkConnection();
102✔
81
    await this.setBufferSize();
102✔
82
    await this.createTables();
1✔
83
    await this.ensureLogsTTL();
1✔
84
    this.emit('ready');
1✔
85
    setInterval(() => this.flushLogs(), this.flushInterval);
1✔
86
    cron.schedule('0 * * * *', () => {
1✔
87
      this.deleteExpiredLogs();
×
88
      this.deleteExpiredNotificationItems();
×
89
    });
90
  }
91

92
  async checkConnection () {
93
    return new Promise((resolve, reject) => {
104✔
94
      this.pool.getConnection((err, connection) => {
104✔
95
        if (err) {
104✔
96
          err = new Error(err.message || err.toString());
1!
97
          return reject(err);
1✔
98
        }
99
        connection.release();
103✔
100
        resolve();
103✔
101
      });
102
    });
103
  }
104

105
  async setBufferSize () {
106
    const DESIRED_SORT_BUFFER_SIZE = 8 * 1024 * 1024;
104✔
107
    const currentSize = await this.getBufferSize();
104✔
108

109
    if (currentSize < DESIRED_SORT_BUFFER_SIZE) {
3✔
110
      const query = `SET SESSION sort_buffer_size = ${DESIRED_SORT_BUFFER_SIZE}`;
1✔
111
      return new Promise((resolve, reject) => {
1✔
112
        this.pool.query(query, err => {
1✔
113
          if (err) {
1!
UNCOV
114
            err = new Error(err.message || err.toString());
×
UNCOV
115
            return reject(err);
×
116
          }
117
          resolve();
1✔
118
        });
119
      });
120
    }
121
  }
122

123
  async getBufferSize () {
124
    const query = "SHOW VARIABLES LIKE 'sort_buffer_size'";
104✔
125
    return new Promise((resolve, reject) => {
104✔
126
      this.pool.query(query, (err, results) => {
104✔
127
        if (err) {
3✔
128
          err = new Error(err.message || err.toString());
1!
129
          return reject(err);
1✔
130
        }
131
        resolve(parseInt(results[0].Value, 10));
2✔
132
      });
133
    });
134
  }
135

136
  async createTables () {
137
    const tableQueries = [
3✔
138
      `CREATE TABLE IF NOT EXISTS \`${this.logsTable}\` (
139
        \`id\` BIGINT PRIMARY KEY AUTO_INCREMENT,
140
        \`hostname\` VARCHAR(63),
141
        \`pid\` INT,
142
        \`source\` VARCHAR(31),
143
        \`timestamp\` TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
144
        \`level\` VARCHAR(31) DEFAULT 'info',
145
        \`message\` TEXT,
146
        \`meta\` TEXT,
147
        \`errsole_id\` BIGINT,
148
        INDEX (\`hostname\`, \`source\`, \`level\`, \`id\`),
149
        INDEX (\`hostname\`, \`id\`),
150
        INDEX (\`source\`, \`level\`, \`id\`),
151
        INDEX (\`hostname\`, \`source\`, \`level\`, \`timestamp\`, \`id\`),
152
        INDEX (\`hostname\`, \`timestamp\`, \`id\`),
153
        INDEX (\`source\`, \`level\`, \`timestamp\`, \`id\`),
154
        INDEX (\`timestamp\`, \`id\`),
155
        INDEX (\`errsole_id\`),
156
        FULLTEXT INDEX (\`message\`)
157
      )`,
158
      `CREATE TABLE IF NOT EXISTS \`${this.usersTable}\` (
159
        \`id\` BIGINT PRIMARY KEY AUTO_INCREMENT,
160
        \`name\` VARCHAR(255),
161
        \`email\` VARCHAR(255) UNIQUE NOT NULL,
162
        \`hashed_password\` VARCHAR(255) NOT NULL,
163
        \`role\` VARCHAR(255) NOT NULL
164
      )`,
165
      `CREATE TABLE IF NOT EXISTS \`${this.configTable}\` (
166
        \`id\` BIGINT PRIMARY KEY AUTO_INCREMENT,
167
        \`key\` VARCHAR(255) UNIQUE NOT NULL,
168
        \`value\` TEXT NOT NULL
169
      )`,
170
      `CREATE TABLE IF NOT EXISTS \`${this.notificationsTable}\` (
171
        \`id\` BIGINT PRIMARY KEY AUTO_INCREMENT,
172
        \`errsole_id\` BIGINT,
173
        \`hostname\` VARCHAR(255),
174
        \`hashed_message\` VARCHAR(255),
175
        \`created_at\` TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3),
176
        \`updated_at\` TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
177
        INDEX (\`hostname\`, \`hashed_message\`, \`created_at\`),
178
        INDEX (\`created_at\`)
179
      )`
180
    ];
181

182
    const alterQueries = [
3✔
183
      `ALTER TABLE \`${this.configTable}\` MODIFY COLUMN \`value\` TEXT NOT NULL;`
184
    ];
185

186
    await Promise.all(tableQueries.map(query => {
3✔
187
      return new Promise((resolve, reject) => {
12✔
188
        this.pool.query(query, (err, results) => {
12✔
189
          if (err) {
12✔
190
            err = new Error(err.message || err.toString());
4!
191
            return reject(err);
4✔
192
          }
193
          resolve(results);
8✔
194
        });
195
      });
196
    }));
197

198
    await Promise.all(alterQueries.map(query => {
2✔
199
      return new Promise((resolve, reject) => {
2✔
200
        this.pool.query(query, (err, results) => {
2✔
201
          if (err) {
2!
UNCOV
202
            err = new Error(err.message || err.toString());
×
UNCOV
203
            return reject(err);
×
204
          }
205
          resolve(results);
2✔
206
        });
207
      });
208
    }));
209

210
    this.isConnectionInProgress = false;
2✔
211
  }
212

213
  async ensureLogsTTL () {
214
    const DEFAULT_LOGS_TTL = 30 * 24 * 60 * 60 * 1000;
5✔
215
    const configResult = await this.getConfig('logsTTL');
5✔
216
    if (!configResult.item) {
4✔
217
      await this.setConfig('logsTTL', DEFAULT_LOGS_TTL.toString());
2✔
218
    }
219
  }
220

221
  /**
222
   * Retrieves a configuration entry from the database.
223
   *
224
   * @async
225
   * @function getConfig
226
   * @param {string} key - The key of the configuration entry to retrieve.
227
   * @returns {Promise<{item: Config}>} - A promise that resolves with an object containing the configuration item.
228
   * @throws {Error} - Throws an error if the operation fails.
229
   */
230
  async getConfig (key) {
231
    const query = `SELECT * FROM ${this.configTable} WHERE \`key\` = ?`;
3✔
232
    return new Promise((resolve, reject) => {
3✔
233
      this.pool.query(query, [key], (err, results) => {
3✔
234
        if (err) return reject(err);
3✔
235
        resolve({ item: results[0] });
2✔
236
      });
237
    });
238
  }
239

240
  /**
241
   * Updates or adds a configuration entry in the database.
242
   *
243
   * @async
244
   * @function setConfig
245
   * @param {string} key - The key of the configuration entry.
246
   * @param {string} value - The value to be stored for the configuration entry.
247
   * @returns {Promise<{item: Config}>} - A promise that resolves with an object containing the updated or added configuration item.
248
   * @throws {Error} - Throws an error if the operation fails.
249
   */
250
  async setConfig (key, value) {
251
    const query = `INSERT INTO ${this.configTable} (\`key\`, \`value\`) VALUES (?, ?) ON DUPLICATE KEY UPDATE \`value\` = VALUES(\`value\`)`;
3✔
252

253
    return new Promise((resolve, reject) => {
3✔
254
      this.pool.query(query, [key, value], err => {
3✔
255
        if (err) return reject(err);
3✔
256
        this.getConfig(key).then(resolve).catch(reject);
2✔
257
      });
258
    });
259
  }
260

261
  /**
262
   * Deletes a configuration entry from the database.
263
   *
264
   * @async
265
   * @function deleteConfig
266
   * @param {string} key - The key of the configuration entry to be deleted.
267
   * @returns {Promise<{}>} - A Promise that resolves with an empty object upon successful deletion of the configuration.
268
   * @throws {Error} - Throws an error if the operation fails.
269
   */
270
  async deleteConfig (key) {
271
    const query = `DELETE FROM ${this.configTable} WHERE \`key\` = ?`;
2✔
272
    return new Promise((resolve, reject) => {
2✔
273
      this.pool.query(query, [key], (err, results) => {
2✔
274
        if (err) return reject(err);
2!
275
        if (results.affectedRows === 0) return reject(new Error('Configuration not found.'));
2✔
276
        resolve({});
1✔
277
      });
278
    });
279
  }
280

281
  /**
282
   * Adds log entries to the pending logs and flushes them if the batch size is reached.
283
   *
284
   * @param {Log[]} logEntries - An array of log entries to be added to the pending logs.
285
   * @returns {Object} - An empty object.
286
   */
287
  postLogs (logEntries) {
288
    this.pendingLogs.push(...logEntries);
2✔
289
    if (this.pendingLogs.length >= this.batchSize) {
2✔
290
      this.flushLogs();
1✔
291
    }
292
    return {};
2✔
293
  }
294

295
  /**
296
   * Flushes pending logs to the database.
297
   *
298
   * @async
299
   * @function flushLogs
300
   * @returns {Promise<{}>} - A Promise that resolves with an empty object.
301
   * @throws {Error} - Throws an error if the operation fails.
302
   */
303
  async flushLogs () {
UNCOV
304
    while (this.isConnectionInProgress) {
×
UNCOV
305
      await new Promise(resolve => setTimeout(resolve, 100));
×
306
    }
307

UNCOV
308
    const logsToPost = this.pendingLogs.splice(0, this.pendingLogs.length);
×
UNCOV
309
    if (logsToPost.length === 0) {
×
UNCOV
310
      return {}; // No logs to post
×
311
    }
312

UNCOV
313
    const values = logsToPost.map(logEntry => [
×
314
      new Date(logEntry.timestamp),
315
      logEntry.hostname,
316
      logEntry.pid,
317
      logEntry.source,
318
      logEntry.level,
319
      logEntry.message,
320
      logEntry.meta,
321
      logEntry.errsole_id
322
    ]);
323

UNCOV
324
    return await new Promise((resolve, reject) => {
×
UNCOV
325
      this.pool.getConnection((err, connection) => {
×
UNCOV
326
        if (err) return reject(err);
×
UNCOV
327
        connection.query(`INSERT IGNORE INTO ${this.logsTable} (timestamp, hostname, pid, source, level, message, meta, errsole_id) VALUES ?`, [values], () => {
×
UNCOV
328
          connection.release();
×
UNCOV
329
          if (err) return reject(err);
×
UNCOV
330
          resolve({});
×
331
        });
332
      });
333
    });
334
  }
335

336
  /**
337
   * Retrieves unique hostnames from the database.
338
   *
339
   * @async
340
   * @function getHostnames
341
   * @returns {Promise<{items: string[]}>} - A Promise that resolves with an object containing an array of unique hostnames.
342
   * @throws {Error} - Throws an error if the operation fails.
343
   */
344
  async getHostnames () {
345
    const query = `
3✔
346
    SELECT DISTINCT hostname 
347
    FROM ${this.logsTable} 
348
    WHERE hostname IS NOT NULL AND hostname != ''
349
    `;
350

351
    return new Promise((resolve, reject) => {
3✔
352
      this.pool.query(query, (err, results) => {
3✔
353
        if (err) return reject(err);
3✔
354
        const hostnames = results.map(row => row.hostname).sort();
3✔
355
        resolve({ items: hostnames });
2✔
356
      });
357
    });
358
  }
359

360
  /**
361
   * Retrieves log entries from the database based on specified filters.
362
   *
363
   * @async
364
   * @function getLogs
365
   * @param {LogFilter} [filters] - Filters to apply for log retrieval.
366
   * @returns {Promise<{items: Log[]}>} - A Promise that resolves with an object containing log items.
367
   * @throws {Error} - Throws an error if the operation fails.
368
   */
369
  async getLogs (filters = {}) {
2✔
370
    const DEFAULT_LOGS_LIMIT = 100;
9✔
371
    filters.limit = filters.limit || DEFAULT_LOGS_LIMIT;
9✔
372

373
    const whereClauses = [];
9✔
374
    const values = [];
9✔
375
    let orderBy = 'id DESC';
9✔
376
    let shouldReverse = true;
9✔
377

378
    // Apply filters
379
    if (filters.hostnames && filters.hostnames.length > 0) {
9✔
380
      whereClauses.push('hostname IN (?)');
1✔
381
      values.push(filters.hostnames);
1✔
382
    }
383
    if (filters.level_json || filters.errsole_id) {
9✔
384
      const orConditions = [];
3✔
385

386
      if (filters.level_json && filters.level_json.length > 0) {
3✔
387
        const levelConditions = filters.level_json.map(levelObj => '(source = ? AND level = ?)');
2✔
388
        orConditions.push(`(${levelConditions.join(' OR ')})`);
2✔
389
        filters.level_json.forEach(levelObj => {
2✔
390
          values.push(levelObj.source, levelObj.level);
2✔
391
        });
392
      }
393

394
      if (filters.errsole_id) {
3✔
395
        orConditions.push('errsole_id = ?');
2✔
396
        values.push(filters.errsole_id);
2✔
397
      }
398

399
      whereClauses.push(`(${orConditions.join(' OR ')})`);
3✔
400
    }
401
    if (filters.lt_id) {
9✔
402
      whereClauses.push('id < ?');
1✔
403
      values.push(filters.lt_id);
1✔
404
      orderBy = 'id DESC';
1✔
405
      shouldReverse = true;
1✔
406
    } else if (filters.gt_id) {
8✔
407
      whereClauses.push('id > ?');
1✔
408
      values.push(filters.gt_id);
1✔
409
      orderBy = 'id ASC';
1✔
410
      shouldReverse = false;
1✔
411
    } else if (filters.lte_timestamp || filters.gte_timestamp) {
7✔
412
      if (filters.lte_timestamp) {
1!
413
        whereClauses.push('timestamp <= ?');
1✔
414
        values.push(new Date(filters.lte_timestamp));
1✔
415
        orderBy = 'timestamp DESC, id DESC';
1✔
416
        shouldReverse = true;
1✔
417
      }
418
      if (filters.gte_timestamp) {
1!
419
        whereClauses.push('timestamp >= ?');
1✔
420
        values.push(new Date(filters.gte_timestamp));
1✔
421
        orderBy = 'timestamp ASC, id ASC';
1✔
422
        shouldReverse = false;
1✔
423
      }
424
    }
425

426
    const whereClause = whereClauses.length ? `WHERE ${whereClauses.join(' AND ')}` : '';
9✔
427
    const query = `SELECT id, hostname, pid, source, timestamp, level, message, errsole_id  FROM ${this.logsTable}  ${whereClause} ORDER BY ${orderBy} LIMIT ?`;
9✔
428
    values.push(filters.limit);
9✔
429

430
    return new Promise((resolve, reject) => {
9✔
431
      this.pool.query(query, values, (err, results) => {
9✔
432
        if (err) return reject(err);
9✔
433
        if (shouldReverse) results.reverse();
8✔
434
        resolve({ items: results });
8✔
435
      });
436
    });
437
  }
438

439
  /**
440
   * Retrieves log entries from the database based on specified search terms and filters.
441
   *
442
   * @async
443
   * @function searchLogs
444
   * @param {string[]} searchTerms - An array of search terms.
445
   * @param {LogFilter} [filters] - Filters to refine the search.
446
   * @returns {Promise<{items: Log[], filters: LogFilter[]}>} - A promise that resolves with an object containing an array of log items and the applied filters.
447
   * @throws {Error} - Throws an error if the operation fails.
448
   */
449
  async searchLogs (searchTerms, filters = {}) {
2✔
450
    const DEFAULT_LOGS_LIMIT = 100;
11✔
451
    filters.limit = filters.limit || DEFAULT_LOGS_LIMIT;
11✔
452

453
    const whereClauses = [];
11✔
454
    const values = [];
11✔
455
    let orderBy = 'id DESC';
11✔
456
    let shouldReverse = true;
11✔
457

458
    if (searchTerms.length > 0) {
11✔
459
      whereClauses.push('MATCH(message) AGAINST (? IN BOOLEAN MODE)');
2✔
460
      values.push(searchTerms.map(term => `+"${term}"`).join(' '));
3✔
461
    }
462

463
    // Apply filters
464
    if (filters.hostnames && filters.hostnames.length > 0) {
11✔
465
      whereClauses.push('hostname IN (?)');
1✔
466
      values.push(filters.hostnames);
1✔
467
    }
468
    if (filters.level_json || filters.errsole_id) {
11✔
469
      const orConditions = [];
2✔
470

471
      if (filters.level_json && filters.level_json.length > 0) {
2✔
472
        const levelConditions = filters.level_json.map(levelObj => '(source = ? AND level = ?)');
2✔
473
        orConditions.push(`(${levelConditions.join(' OR ')})`);
1✔
474
        filters.level_json.forEach(levelObj => {
1✔
475
          values.push(levelObj.source, levelObj.level);
2✔
476
        });
477
      }
478

479
      if (filters.errsole_id) {
2✔
480
        orConditions.push('errsole_id = ?');
1✔
481
        values.push(filters.errsole_id);
1✔
482
      }
483

484
      whereClauses.push(`(${orConditions.join(' OR ')})`);
2✔
485
    }
486
    if (filters.lt_id) {
11✔
487
      whereClauses.push('id < ?');
2✔
488
      values.push(filters.lt_id);
2✔
489
      orderBy = 'id DESC';
2✔
490
      shouldReverse = true;
2✔
491
    }
492
    if (filters.gt_id) {
11✔
493
      whereClauses.push('id > ?');
1✔
494
      values.push(filters.gt_id);
1✔
495
      orderBy = 'id ASC';
1✔
496
      shouldReverse = false;
1✔
497
    }
498
    if (filters.lte_timestamp || filters.gte_timestamp) {
11✔
499
      if (filters.lte_timestamp) {
3✔
500
        whereClauses.push('timestamp <= ?');
2✔
501
        values.push(new Date(filters.lte_timestamp));
2✔
502
        orderBy = 'timestamp DESC, id DESC';
2✔
503
        shouldReverse = true;
2✔
504
      }
505
      if (filters.gte_timestamp) {
3✔
506
        whereClauses.push('timestamp >= ?');
2✔
507
        values.push(new Date(filters.gte_timestamp));
2✔
508
        orderBy = 'timestamp ASC, id ASC';
2✔
509
        shouldReverse = false;
2✔
510
      }
511
      if (filters.lte_timestamp && !filters.gte_timestamp) {
3✔
512
        filters.lte_timestamp = new Date(filters.lte_timestamp);
1✔
513
        const gteTimestamp = new Date(filters.lte_timestamp.getTime() - 24 * 60 * 60 * 1000);
1✔
514
        whereClauses.push('timestamp >= ?');
1✔
515
        values.push(gteTimestamp);
1✔
516
        filters.gte_timestamp = gteTimestamp;
1✔
517
      }
518
      if (filters.gte_timestamp && !filters.lte_timestamp) {
3✔
519
        filters.gte_timestamp = new Date(filters.gte_timestamp);
1✔
520
        const lteTimestamp = new Date(filters.gte_timestamp.getTime() + 24 * 60 * 60 * 1000);
1✔
521
        whereClauses.push('timestamp <= ?');
1✔
522
        values.push(lteTimestamp);
1✔
523
        filters.lte_timestamp = lteTimestamp;
1✔
524
      }
525
    }
526

527
    const whereClause = whereClauses.length ? `WHERE ${whereClauses.join(' AND ')}` : '';
11!
528
    const query = `SELECT id, hostname, pid, source, timestamp, level, message, errsole_id FROM ${this.logsTable} ${whereClause} ORDER BY ${orderBy} LIMIT ?`;
11✔
529
    values.push(filters.limit);
11✔
530

531
    return new Promise((resolve, reject) => {
11✔
532
      this.pool.query(query, values, (err, results) => {
11✔
533
        if (err) return reject(err);
11✔
534
        if (shouldReverse) results.reverse();
10✔
535
        resolve({ items: results, filters });
10✔
536
      });
537
    });
538
  }
539

540
  /**
541
   * Retrieves the meta data of a log entry.
542
   *
543
   * @async
544
   * @function getMeta
545
   * @param {number} id - The unique ID of the log entry.
546
   * @returns {Promise<{item: id, meta}>}  - A Promise that resolves with an object containing the log ID and its associated metadata.
547
   * @throws {Error} - Throws an error if the log entry is not found or the operation fails.
548
   */
549
  async getMeta (id) {
550
    const query = `SELECT id, meta FROM ${this.logsTable} WHERE id = ?`;
3✔
551
    return new Promise((resolve, reject) => {
3✔
552
      this.pool.query(query, [id], (err, results) => {
3✔
553
        if (err) return reject(err);
3✔
554
        if (!results.length) return reject(new Error('Log entry not found.'));
2✔
555
        resolve({ item: results[0] });
1✔
556
      });
557
    });
558
  }
559

560
  /**
561
   * Deletes expired logs based on TTL configuration.
562
   *
563
   * @async
564
   * @function deleteExpiredLogs
565
   */
566
  async deleteExpiredLogs () {
567
    if (this.deleteExpiredLogsRunning) return;
5!
568

569
    this.deleteExpiredLogsRunning = true;
5✔
570

571
    const DEFAULT_LOGS_TTL = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds
5✔
572

573
    try {
5✔
574
      let logsTTL = DEFAULT_LOGS_TTL;
5✔
575
      const configResult = await this.getConfig('logsTTL');
5✔
576
      if (configResult.item) {
5✔
577
        const parsedTTL = parseInt(configResult.item.value, 10);
4✔
578
        logsTTL = isNaN(parsedTTL) ? DEFAULT_LOGS_TTL : parsedTTL;
4✔
579
      }
580
      let expirationTime = new Date(Date.now() - logsTTL);
5✔
581
      expirationTime = new Date(expirationTime).toISOString().slice(0, 19).replace('T', ' ');
5✔
582
      let deletedRowCount;
583
      do {
5✔
584
        deletedRowCount = await new Promise((resolve, reject) => {
8✔
585
          this.pool.query(
8✔
586
            `DELETE FROM ${this.logsTable} WHERE timestamp < ? LIMIT 1000`,
587
            [expirationTime],
588
            (err, results) => {
589
              if (err) return reject(err);
8✔
590
              resolve(results.affectedRows);
7✔
591
            }
592
          );
593
        });
594
        await new Promise(resolve => setTimeout(resolve, 10000));
7✔
595
      } while (deletedRowCount > 0);
596
    } catch (err) {
597
      console.error(err);
1✔
598
    } finally {
599
      this.deleteExpiredLogsRunning = false;
5✔
600
    }
601
  }
602

603
  /**
604
   * Deletes all logs from the logs table.
605
   *
606
   * @async
607
   * @function deleteAllLogs
608
   * @returns {Promise<{}>} - A Promise that resolves with an empty object upon successful deletion of the logs.
609
   * @throws {Error} - Throws an error if the operation fails.
610
   */
611
  async deleteAllLogs () {
612
    const query = `TRUNCATE TABLE ${this.logsTable}`;
2✔
613
    return new Promise((resolve, reject) => {
2✔
614
      this.pool.query(query, (err, results) => {
2✔
615
        if (err) return reject(err);
2✔
616
        resolve({});
1✔
617
      });
618
    });
619
  }
620

621
  /**
622
   * Inserts a notification, counts today's notifications, and retrieves the previous notification.
623
   * @param {Notification} notification - The notification to be inserted.
624
   * @returns {Promise<Object>} - Returns today's notification count and the previous notification.
625
   */
626
  async insertNotificationItem (notification = {}) {
×
627
    const errsoleId = notification.errsole_id;
9✔
628
    const hostname = notification.hostname;
9✔
629
    const hashedMessage = notification.hashed_message;
9✔
630

631
    const connection = await new Promise((resolve, reject) => {
9✔
632
      this.pool.getConnection((err, connection) => {
9✔
633
        if (err) return reject(err);
9!
634
        resolve(connection);
9✔
635
      });
636
    });
637

638
    try {
9✔
639
      await new Promise((resolve, reject) => {
9✔
640
        connection.beginTransaction((err) => {
9✔
641
          if (err) return reject(err);
9✔
642
          resolve();
8✔
643
        });
644
      });
645

646
      const fetchPreviousNotificationQuery = `
8✔
647
      SELECT * FROM ${this.notificationsTable}
648
      WHERE hostname = ? AND hashed_message = ?
649
      ORDER BY created_at DESC
650
      LIMIT 1;
651
      `;
652
      const previousNotificationItem = await new Promise((resolve, reject) => {
8✔
653
        connection.query(fetchPreviousNotificationQuery, [hostname, hashedMessage], (err, results) => {
8✔
654
          if (err) return reject(err);
8✔
655
          resolve(results[0]);
7✔
656
        });
657
      });
658

659
      const insertNotificationQuery = `
7✔
660
      INSERT INTO ${this.notificationsTable} (errsole_id, hostname, hashed_message)
661
      VALUES (?, ?, ?);
662
      `;
663
      await new Promise((resolve, reject) => {
7✔
664
        connection.query(insertNotificationQuery, [errsoleId, hostname, hashedMessage], (err) => {
7✔
665
          if (err) return reject(err);
7✔
666
          resolve();
6✔
667
        });
668
      });
669

670
      const startOfDayUTC = new Date();
6✔
671
      startOfDayUTC.setUTCHours(0, 0, 0, 0);
6✔
672
      const endOfDayUTC = new Date();
6✔
673
      endOfDayUTC.setUTCHours(23, 59, 59, 999);
6✔
674
      const countTodayNotificationsQuery = `
6✔
675
      SELECT COUNT(*) AS notificationCount 
676
      FROM ${this.notificationsTable}
677
      WHERE hashed_message = ? AND created_at BETWEEN ? AND ?;
678
      `;
679
      const todayNotificationCount = await new Promise((resolve, reject) => {
6✔
680
        connection.query(countTodayNotificationsQuery, [hashedMessage, startOfDayUTC, endOfDayUTC], (err, result) => {
6✔
681
          if (err) return reject(err);
6✔
682
          resolve(result[0].notificationCount);
5✔
683
        });
684
      });
685

686
      await new Promise((resolve, reject) => {
5✔
687
        connection.commit((err) => {
5✔
688
          if (err) return reject(err);
5✔
689
          resolve();
3✔
690
        });
691
      });
692

693
      return {
3✔
694
        previousNotificationItem,
695
        todayNotificationCount
696
      };
697
    } catch (err) {
698
      await new Promise((resolve) => {
6✔
699
        connection.rollback(() => resolve());
6✔
700
      });
701
      throw err;
6✔
702
    } finally {
703
      connection.release();
9✔
704
    }
705
  }
706

707
  /**
708
   * Deletes expired notifications based on TTL configuration.
709
   *
710
   * @async
711
   * @function deleteExpiredNotificationItems
712
   */
713
  async deleteExpiredNotificationItems () {
714
    if (this.deleteExpiredNotificationItemsRunning) return;
9✔
715

716
    this.deleteExpiredNotificationItemsRunning = true;
8✔
717

718
    const DEFAULT_NOTIFICATIONS_TTL = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds
8✔
719

720
    try {
8✔
721
      let notificationsTTL = DEFAULT_NOTIFICATIONS_TTL;
8✔
722
      const configResult = await this.getConfig('logsTTL');
8✔
723
      if (configResult.item) {
8✔
724
        const parsedTTL = parseInt(configResult.item.value, 10);
2✔
725
        notificationsTTL = isNaN(parsedTTL) ? DEFAULT_NOTIFICATIONS_TTL : parsedTTL;
2✔
726
      }
727
      let expirationTime = new Date(Date.now() - notificationsTTL);
8✔
728
      expirationTime = new Date(expirationTime).toISOString().slice(0, 19).replace('T', ' ');
8✔
729
      let deletedRowCount;
730
      do {
8✔
731
        deletedRowCount = await new Promise((resolve, reject) => {
10✔
732
          this.pool.query(
10✔
733
            `DELETE FROM ${this.notificationsTable} WHERE created_at < ? LIMIT 1000`,
734
            [expirationTime],
735
            (err, results) => {
736
              if (err) return reject(err);
10✔
737
              resolve(results.affectedRows);
8✔
738
            }
739
          );
740
        });
741
        await new Promise(resolve => setTimeout(resolve, 10000));
8✔
742
      } while (deletedRowCount > 0);
743
    } catch (err) {
744
      console.error(err);
2✔
745
    } finally {
746
      this.deleteExpiredNotificationItemsRunning = false;
8✔
747
    }
748
  }
749

750
  /**
751
   * Creates a new user record in the database.
752
   *
753
   * @async
754
   * @function createUser
755
   * @param {Object} user - The user data.
756
   * @param {string} user.name - The name of the user.
757
   * @param {string} user.email - The email address of the user.
758
   * @param {string} user.password - The password of the user.
759
   * @param {string} user.role - The role of the user.
760
   * @returns {Promise<{item: User}>} - A promise that resolves with an object containing the new user item.
761
   * @throws {Error} - Throws an error if the user creation fails due to duplicate email or other database issues.
762
   */
763
  async createUser (user) {
764
    const SALT_ROUNDS = 10;
3✔
765
    const hashedPassword = await bcrypt.hash(user.password, SALT_ROUNDS);
3✔
766
    const query = `INSERT INTO ${this.usersTable} (name, email, hashed_password, role) VALUES (?, ?, ?, ?)`;
3✔
767
    return new Promise((resolve, reject) => {
3✔
768
      this.pool.query(query, [user.name, user.email, hashedPassword, user.role], (err, results) => {
3✔
769
        if (err) {
3✔
770
          if (err.code === 'ER_DUP_ENTRY') {
2✔
771
            return reject(new Error('A user with the provided email already exists.'));
1✔
772
          }
773
          return reject(err);
1✔
774
        }
775
        resolve({ item: { id: results.insertId, name: user.name, email: user.email, role: user.role } });
1✔
776
      });
777
    });
778
  }
779

780
  /**
781
   * Verifies a user's credentials against stored records.
782
   *
783
   * @async
784
   * @function verifyUser
785
   * @param {string} email - The email address of the user.
786
   * @param {string} password - The password of the user
787
   * @returns {Promise<{item: User}>} - A promise that resolves with an object containing the user item upon successful verification.
788
   * @throws {Error} - Throws an error if the operation fails.
789
   */
790
  async verifyUser (email, password) {
791
    if (!email || !password) {
6✔
792
      throw new Error('Both email and password are required for verification.');
2✔
793
    }
794

795
    const query = `SELECT * FROM ${this.usersTable} WHERE email = ?`;
4✔
796
    return new Promise((resolve, reject) => {
4✔
797
      this.pool.query(query, [email], async (err, results) => {
4✔
798
        if (err) return reject(err);
4✔
799
        if (!results.length) return reject(new Error('User not found.'));
3✔
800

801
        const user = results[0];
2✔
802
        const isPasswordCorrect = await bcrypt.compare(password, user.hashed_password);
2✔
803
        if (!isPasswordCorrect) return reject(new Error('Incorrect password.'));
2✔
804

805
        delete user.hashed_password;
1✔
806
        resolve({ item: user });
1✔
807
      });
808
    });
809
  }
810

811
  /**
812
   * Retrieves the total count of users from the database.
813
   *
814
   * @async
815
   * @function getUserCount
816
   * @returns {Promise<{count: number}>} - A promise that resolves with an object containing the count of users.
817
   * @throws {Error} - Throws an error if the operation fails.
818
   */
819
  async getUserCount () {
820
    const query = `SELECT COUNT(*) as count FROM ${this.usersTable}`;
1✔
821
    return new Promise((resolve, reject) => {
1✔
822
      this.pool.query(query, (err, results) => {
1✔
823
        if (err) return reject(err);
1!
824
        resolve({ count: results[0].count });
1✔
825
      });
826
    });
827
  }
828

829
  /**
830
   * Retrieves all user records from the database.
831
   *
832
   * @async
833
   * @function getAllUsers
834
   * @returns {Promise<{items: User[]}>} - A promise that resolves with an object containing an array of user items.
835
   * @throws {Error} - Throws an error if the operation fails.
836
   */
837
  async getAllUsers () {
838
    const query = `SELECT id, name, email, role FROM ${this.usersTable}`;
1✔
839
    return new Promise((resolve, reject) => {
1✔
840
      this.pool.query(query, (err, results) => {
1✔
841
        if (err) return reject(err);
1!
842
        resolve({ items: results });
1✔
843
      });
844
    });
845
  }
846

847
  /**
848
   * Retrieves a user record from the database based on the provided email.
849
   *
850
   * @async
851
   * @function getUserByEmail
852
   * @param {string} email - The email address of the user.
853
   * @returns {Promise<{item: User}>} - A Promise that resolves with an object containing the user item.
854
   * @throws {Error} - Throws an error if no user matches the email address.
855
   */
856
  async getUserByEmail (email) {
857
    if (!email) throw new Error('Email is required.');
2!
858

859
    const query = `SELECT id, name, email, role FROM ${this.usersTable} WHERE email = ?`;
2✔
860
    return new Promise((resolve, reject) => {
2✔
861
      this.pool.query(query, [email], (err, results) => {
2✔
862
        if (err) return reject(err);
2!
863
        if (!results.length) return reject(new Error('User not found.'));
2✔
864
        resolve({ item: results[0] });
1✔
865
      });
866
    });
867
  }
868

869
  /**
870
   * Updates a user's record in the database based on the provided email.
871
   *
872
   * @async
873
   * @function updateUserByEmail
874
   * @param {string} email - The email address of the user to be updated.
875
   * @param {Object} updates - The updates to be applied to the user record.
876
   * @returns {Promise<{item: User}>} - A Promise that resolves with an object containing the updated user item.
877
   * @throws {Error} - Throws an error if no updates could be applied or the user is not found.
878
   */
879
  async updateUserByEmail (email, updates) {
880
    if (!email) throw new Error('Email is required.');
5✔
881
    if (!updates || Object.keys(updates).length === 0) throw new Error('No updates provided.');
4✔
882

883
    const restrictedFields = ['id', 'hashed_password'];
3✔
884
    restrictedFields.forEach(field => delete updates[field]);
6✔
885

886
    const setClause = Object.keys(updates).map(key => `${key} = ?`).join(', ');
3✔
887
    const values = [...Object.values(updates), email];
3✔
888

889
    const query = `UPDATE ${this.usersTable} SET ${setClause} WHERE email = ?`;
3✔
890
    return new Promise((resolve, reject) => {
3✔
891
      this.pool.query(query, values, async (err, results) => {
3✔
892
        if (err) return reject(err);
3✔
893
        if (results.affectedRows === 0) return reject(new Error('No updates applied.'));
2✔
894
        this.getUserByEmail(email).then(resolve).catch(reject);
1✔
895
      });
896
    });
897
  }
898

899
  /**
900
   * Updates a user's password in the database.
901
   *
902
   * @async
903
   * @function updatePassword
904
   * @param {string} email - The email address of the user whose password is to be updated.
905
   * @param {string} currentPassword - The current password of the user for verification.
906
   * @param {string} newPassword - The new password to replace the current one.
907
   * @returns {Promise<{item: User}>} - A Promise that resolves with an object containing the updated user item (excluding sensitive information).
908
   * @throws {Error} - If the user is not found, if the current password is incorrect, or if the password update fails.
909
   */
910
  async updatePassword (email, currentPassword, newPassword) {
911
    if (!email || !currentPassword || !newPassword) {
8✔
912
      throw new Error('Email, current password, and new password are required.');
3✔
913
    }
914

915
    const query = `SELECT * FROM ${this.usersTable} WHERE email = ?`;
5✔
916
    return new Promise((resolve, reject) => {
5✔
917
      this.pool.query(query, [email], async (err, results) => {
5✔
918
        if (err) return reject(err);
5!
919
        if (!results.length) return reject(new Error('User not found.'));
5✔
920

921
        const user = results[0];
4✔
922
        const isPasswordCorrect = await bcrypt.compare(currentPassword, user.hashed_password);
4✔
923
        if (!isPasswordCorrect) return reject(new Error('Current password is incorrect.'));
4✔
924

925
        const hashedPassword = await bcrypt.hash(newPassword, 10);
3✔
926
        const updateQuery = `UPDATE ${this.usersTable} SET hashed_password = ? WHERE email = ?`;
3✔
927
        this.pool.query(updateQuery, [hashedPassword, email], (err, updateResults) => {
3✔
928
          if (err) return reject(err);
3✔
929
          if (updateResults.affectedRows === 0) return reject(new Error('Password update failed.'));
2✔
930
          delete user.hashed_password;
1✔
931
          resolve({ item: user });
1✔
932
        });
933
      });
934
    });
935
  }
936

937
  /**
938
   * Deletes a user record from the database.
939
   *
940
   * @async
941
   * @function deleteUser
942
   * @param {number} id - The unique ID of the user to be deleted.
943
   * @returns {Promise<{}>} - A Promise that resolves with an empty object upon successful deletion of the user.
944
   * @throws {Error} - Throws an error if no user is found with the given ID or if the database operation fails.
945
   */
946
  async deleteUser (id) {
947
    if (!id) throw new Error('User ID is required.');
2!
948

949
    const query = `DELETE FROM ${this.usersTable} WHERE id = ?`;
2✔
950
    return new Promise((resolve, reject) => {
2✔
951
      this.pool.query(query, [id], (err, results) => {
2✔
952
        if (err) return reject(err);
2!
953
        if (results.affectedRows === 0) return reject(new Error('User not found.'));
2✔
954
        resolve({});
1✔
955
      });
956
    });
957
  }
958
}
959

960
module.exports = ErrsoleMySQL;
1✔
961
module.exports.default = ErrsoleMySQL;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc