Selaa lähdekoodia

Server: PUT /services/{:serverId} für ServiceCheck-Create/Update; SQLite ErrorHandling;

Christian Kahlau 3 vuotta sitten
vanhempi
commit
79901287da

+ 76 - 10
server/src/ctrl/database.class.ts

@@ -6,12 +6,18 @@ import { Database as SQLiteDB, OPEN_CREATE, OPEN_READWRITE } from 'sqlite3';
 
 import { ServiceConfig, validateParamType } from '../../../common/interfaces/service-config.interface';
 import { Logger } from '../../../common/util/logger.class';
-import { arrayDiff } from '../../../common/util/object-utils';
 
+import { ValidationException } from '../lib/validation-exception.class';
 import { DBMigration } from './db-migration.class';
 import { SQLiteController } from './sqlite-controller.base';
 
 export class Database extends SQLiteController {
+  public set onError(listener: (error: any) => void) {
+    this._onError = listener;
+  }
+
+  private _onError: (error: any) => void = err => console.error('[DB.ONERROR]', err);
+
   public async open(migrate = false) {
     try {
       const DATA_DIR = process.env.DATA_DIR || 'data';
@@ -23,6 +29,7 @@ export class Database extends SQLiteController {
 
       await new Promise<void>((res, rej) => {
         this.db = new SQLiteDB(DATA_FILE, OPEN_READWRITE | OPEN_CREATE, err => (err ? rej(err) : res()));
+        this.db.on('error', this._onError);
       });
       Logger.info('[INFO]', exists ? 'Opened' : 'Created', 'SQLite3 Database file', DATA_FILE);
 
@@ -246,7 +253,7 @@ export class Database extends SQLiteController {
     return (await this.getHealthCheckConfigs(serverID)).map(this.httpCheckConfigFrom);
   }
 
-  private async getHealtCheckConfigByID(serverID: number, configID: number) {
+  private async getHealthCheckConfigByID(serverID: number, configID: number) {
     if (!serverID && !configID) return null;
 
     const res = await this.stmt(
@@ -257,7 +264,7 @@ export class Database extends SQLiteController {
         HealthCheckParams.Value as '_ParamValue'
         FROM HealthCheckConfig
         LEFT OUTER JOIN HealthCheckParams ON HealthCheckConfig.ID = HealthCheckParams.ConfigID
-        WHERE HealtCheckConfig.ID = ?
+        WHERE HealthCheckConfig.ID = ?
         AND HealthCheckConfig.ServerID = ?
         ORDER BY HealthCheckConfig.Title, _ParamType, _ParamKey`,
       [configID, serverID]
@@ -271,10 +278,13 @@ export class Database extends SQLiteController {
   }
 
   public async getHttpCheckConfigByID(serverID: number, configID: number) {
-    return this.httpCheckConfigFrom(await this.getHealtCheckConfigByID(serverID, configID));
+    return this.httpCheckConfigFrom(await this.getHealthCheckConfigByID(serverID, configID));
   }
 
   public async saveHttpCheckConfig(serverID: number, conf: HttpCheckConfig) {
+    const validationErrors = this.validateHttpCheckConfig(conf);
+    if (validationErrors) throw new ValidationException('Validation of HttpCheckConfig object failed', validationErrors);
+
     conf.serverId = serverID;
 
     const oldConf = await this.getHttpCheckConfigByID(serverID, conf.id);
@@ -286,20 +296,63 @@ export class Database extends SQLiteController {
           await this.stmt('UPDATE HealthCheckConfig SET Title = ?', [conf.title]);
         }
 
-        const sql = `UPDATE HealthCheckParams SET Value = ? WHERE ConfigID = ? AND Key = ?;`;
-
-        const updValues: any[][] = [];
+        let updValues: any[][] = [];
         if (oldConf.interval !== conf.interval) updValues.push([conf.interval, conf.id, 'interval']);
         if (oldConf.url !== conf.url) updValues.push([conf.url, conf.id, 'url']);
-        const checksDiff = arrayDiff(oldConf.checks, conf.checks);
+        if (updValues.length) {
+          for (const data of updValues) {
+            await this.run(`UPDATE HealthCheckParams SET Value = ? WHERE ConfigID = ? AND Key = ?;`, data);
+          }
+        }
+
+        const res = await this.stmt('SELECT * FROM HealthCheckParams WHERE ConfigID = ? and Key = "check";', [conf.id]);
+        updValues = [];
+        const delIDs: any[][] = [];
+        res.rows.forEach((row, i) => {
+          if (i < conf.checks.length) {
+            updValues.push([conf.checks[i], row['ID']]);
+          } else {
+            delIDs.push(row['ID']);
+          }
+        });
+
+        if (delIDs.length) {
+          const delSql = 'DELETE FROM HealthCheckParams WHERE ID IN (?);';
+          await this.run(delSql, [delIDs]);
+        }
+
+        if (updValues.length) {
+          for (const data of updValues) {
+            await this.run('UPDATE HealthCheckParams SET Value = ? WHERE ID = ?;', data);
+          }
+        }
+        const insValues = conf.checks.filter((c, i) => i > res.rows.length - 1).map(c => [conf.id, 'regexp', 'check', c]);
+        if (insValues.length) {
+          for (const data of insValues) {
+            await this.run('INSERT INTO HealthCheckParams(ConfigID, Type, Key, Value) VALUES(?, ?, ?, ?);', data);
+          }
+        }
       } else {
         // INSERT
+        const res = await this.run('INSERT INTO HealthCheckConfig(ServerID, Type, Title) VALUES(?, ?, ?);', [serverID, 'http', conf.title]);
+
+        const insCheckValues = conf.checks.map(c => [res.lastID, 'regexp', 'check', c]);
+        await this.run(
+          `INSERT INTO HealthCheckParams(ConfigID, Type, Key, Value) VALUES
+          (?, ?, ?, ?),
+          (?, ?, ?, ?,)${conf.checks.length ? `,${insCheckValues.map(() => '(?, ?, ?, ?)').join(',')}` : ''}`,
+          [
+            ...[res.lastID, 'text', 'url', conf.url],
+            ...[res.lastID, 'number', 'interval', conf.interval],
+            ...conf.checks.reduce((res, arr) => [...res, ...arr], [] as any[])
+          ]
+        );
       }
 
-      this.commit();
+      await this.commit();
       return conf;
     } catch (err) {
-      this.rollback();
+      await this.rollback();
       throw err;
     }
   }
@@ -362,4 +415,17 @@ export class Database extends SQLiteController {
       ...params
     };
   }
+
+  private validateHttpCheckConfig(conf: Partial<HttpCheckConfig>): { [key: string]: string } | null {
+    const errors = {} as any;
+    if (!conf) return { null: 'Object was null or undefined' };
+    if (!conf.title?.trim().length) errors['required|title'] = `Field 'title' is required.`;
+    if (!conf.url?.trim().length) errors['required|url'] = `Field 'url' is required.`;
+    if ((!conf.interval && conf.interval !== 0) || Number.isNaN(Number(conf.interval))) errors['required|interval'] = `Field 'interval' is required.`;
+
+    if (!conf.checks || !Array.isArray(conf.checks))
+      errors['required|checks'] = `Field 'checks' is required and must be an array of check expressions.`;
+
+    return Object.keys(errors).length ? errors : null;
+  }
 }

+ 27 - 10
server/src/ctrl/sqlite-controller.base.ts

@@ -1,9 +1,9 @@
-import { Database, RunResult } from 'sqlite3';
+import { Database, RunResult, Statement } from 'sqlite3';
 
 export abstract class SQLiteController {
   protected db!: Database;
 
-  public async run(sql: string, params: any): Promise<RunResult> {
+  public run(sql: string, params: any): Promise<RunResult> {
     return new Promise<RunResult>((res, rej) => {
       if (!this.db) return rej(new Error('Database not opened.'));
       this.db.run(sql, params, function (err) {
@@ -13,18 +13,35 @@ export abstract class SQLiteController {
     });
   }
 
-  public async stmt(sql: string, params: any[]) {
-    return new Promise<{ result: RunResult; rows: any[] }>((res, rej) => {
+  public stmt(sql: string, params: any[]) {
+    return new Promise<{ result: RunResult; rows: any[] }>(async (res, rej) => {
       if (!this.db) return rej(new Error('Database not opened.'));
-      const stmt = this.db.prepare(sql);
-      stmt.all(params, function (err, rows) {
-        if (err) return rej(err);
-        res({ result: this, rows });
-      });
+      try {
+        const stmt = await this.prepare(sql);
+        stmt.all(params, function (err, rows) {
+          if (err) return rej(err);
+          res({ result: this, rows });
+        });
+      } catch (err) {
+        rej(err);
+      }
+    });
+  }
+
+  public prepare(sql: string) {
+    return new Promise<Statement>((res, rej) => {
+      try {
+        this.db.prepare(sql, function (err) {
+          if (err) return rej(err);
+          res(this);
+        });
+      } catch (err) {
+        rej(err);
+      }
     });
   }
 
-  public async exec(script: string) {
+  public exec(script: string) {
     return new Promise<void>((res, rej) => {
       if (!this.db) return rej(new Error('Database not opened.'));
       this.db.exec(script, err => (err ? rej(err) : res()));

+ 7 - 0
server/src/lib/validation-exception.class.ts

@@ -0,0 +1,7 @@
+export class ValidationException extends Error {
+  constructor(msg: string, public errors: ValidationErrors) {
+    super(msg);
+  }
+}
+
+export type ValidationErrors = { [key: string]: string };

+ 27 - 8
server/src/webserver.class.ts

@@ -5,6 +5,7 @@ import { HttpStatusException } from '../../common/lib/http-status.exception';
 import { Logger } from '../../common/util/logger.class';
 
 import { ControllerPool } from './ctrl/controller-pool.interface';
+import { ValidationException } from './lib/validation-exception.class';
 import { ServerAPIHandler } from './webhdl/server-api-handler.class';
 import { ServicesAPIHandler } from './webhdl/services-api-handler.class';
 
@@ -23,17 +24,35 @@ export class Webserver {
 
     this.app.use('**', express.static(path.join(process.env.STATIC_DIR || 'public', 'index.html')));
 
-    this.app.use((err: any, req: Request, res: Response, next: NextFunction) => {
-      if (err instanceof HttpStatusException) {
-        res.status(err.statusCode).send(err.message);
-      } else {
-        Logger.error('[ERROR] Webservice ErrorHandler caught:', err);
-        res.status(500).send(JSON.stringify(err));
-      }
-    });
+    this.app.use(this.handleError.bind(this));
+    this.app.on('error', err => console.error('APP.ONERROR', err));
+    process.on('uncaughtException', err => console.error('PROCESS.UNCAUGHT', err));
 
     this.app.listen(this.port, () => {
       Logger.info(`[INFO] Monitoring Webserver started at http://localhost:${this.port}`);
     });
   }
+
+  handleError = (err: any, req: Request, res: Response, next: NextFunction) => {
+    let log = false;
+    if (err instanceof HttpStatusException) {
+      res.status(err.statusCode).send(err.message);
+    } else if (err instanceof ValidationException) {
+      res.status(400).send(err);
+    } else {
+      const keys = Object.keys(err);
+      if (keys.includes('errno') && keys.includes('code')) {
+        // SQLITE DATABASE EXCEPTIONS
+        log = true;
+        res.status(500).send({
+          ...err,
+          message: new String(err)
+        });
+      } else {
+        log = true;
+        res.status(500).send(JSON.stringify(err));
+      }
+    }
+    if (log) Logger.error('[ERROR] Webservice ErrorHandler caught:', err);
+  };
 }