diff --git a/db/knex_migrations/2024-12-10-2237-add-database-query-handle-empty-result.js b/db/knex_migrations/2024-12-10-2237-add-database-query-handle-empty-result.js new file mode 100644 index 000000000..9d70fcd5d --- /dev/null +++ b/db/knex_migrations/2024-12-10-2237-add-database-query-handle-empty-result.js @@ -0,0 +1,13 @@ +exports.up = function (knex) { + return knex.schema + .alterTable("monitor", function (table) { + table.boolean("database_query_handle_empty_as_failure").notNullable().defaultTo(false); + }); +}; + +exports.down = function (knex) { + return knex.schema + .alterTable("monitor", function (table) { + table.dropColumn("database_query_handle_empty_as_failure"); + }); +}; diff --git a/server/model/monitor.js b/server/model/monitor.js index 3ad8cfafc..442faebe0 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -129,6 +129,7 @@ class Monitor extends BeanModel { mqttSuccessMessage: this.mqttSuccessMessage, mqttCheckType: this.mqttCheckType, databaseQuery: this.databaseQuery, + databaseQueryHandleEmptyAsFailure: this.isDatabaseQueryHandleEmptyAsFailure(), authMethod: this.authMethod, grpcUrl: this.grpcUrl, grpcProtobuf: this.grpcProtobuf, @@ -271,6 +272,14 @@ class Monitor extends BeanModel { return Boolean(this.invertKeyword); } + /** + * Parse to boolean + * @returns {boolean} Is sql query empty result handled as failure? + */ + isDatabaseQueryHandleEmptyAsFailure() { + return Boolean(this.databaseQueryHandleEmptyAsFailure); + } + /** * Parse to boolean * @returns {boolean} Enable TLS for gRPC? @@ -760,7 +769,7 @@ class Monitor extends BeanModel { } else if (this.type === "sqlserver") { let startTime = dayjs().valueOf(); - await mssqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1"); + await mssqlQuery(this.databaseConnectionString, this.databaseQueryHandleEmptyAsFailure, this.databaseQuery || "SELECT 1"); bean.msg = ""; bean.status = UP; @@ -799,7 +808,7 @@ class Monitor extends BeanModel { } else if (this.type === "postgres") { let startTime = dayjs().valueOf(); - await postgresQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1"); + await postgresQuery(this.databaseConnectionString, this.databaseQueryHandleEmptyAsFailure, this.databaseQuery || "SELECT 1"); bean.msg = ""; bean.status = UP; @@ -811,7 +820,7 @@ class Monitor extends BeanModel { // TODO: rename `radius_password` to `password` later for general use let mysqlPassword = this.radiusPassword; - bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1", mysqlPassword); + bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQueryHandleEmptyAsFailure, this.databaseQuery || "SELECT 1", mysqlPassword); bean.status = UP; bean.ping = dayjs().valueOf() - startTime; } else if (this.type === "radius") { diff --git a/server/server.js b/server/server.js index ec5ad49f6..6e5da348b 100644 --- a/server/server.js +++ b/server/server.js @@ -837,6 +837,7 @@ let needSetup = false; bean.mqttCheckType = monitor.mqttCheckType; bean.databaseConnectionString = monitor.databaseConnectionString; bean.databaseQuery = monitor.databaseQuery; + bean.databaseQueryHandleEmptyAsFailure = monitor.databaseQueryHandleEmptyAsFailure; bean.authMethod = monitor.authMethod; bean.authWorkstation = monitor.authWorkstation; bean.authDomain = monitor.authDomain; diff --git a/server/util-server.js b/server/util-server.js index 5ebc62ac5..0b3737cce 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -318,11 +318,12 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) { /** * Run a query on SQL Server * @param {string} connectionString The database connection string + * @param {boolean} handleEmptyResult Should empty results be handled as an error * @param {string} query The query to validate the database with * @returns {Promise<(string[] | object[] | object)>} Response from * server */ -exports.mssqlQuery = async function (connectionString, query) { +exports.mssqlQuery = async function (connectionString, handleEmptyResult, query) { let pool; try { pool = new mssql.ConnectionPool(connectionString); @@ -330,7 +331,12 @@ exports.mssqlQuery = async function (connectionString, query) { if (!query) { query = "SELECT 1"; } - await pool.request().query(query); + const result = await pool.request().query(query); + if (Array.isArray(result.recordset)) { + if (handleEmptyResult && result.recordset.length === 0) { + throw new Error("No rows returned"); + } + } pool.close(); } catch (e) { if (pool) { @@ -343,11 +349,12 @@ exports.mssqlQuery = async function (connectionString, query) { /** * Run a query on Postgres * @param {string} connectionString The database connection string + * @param {boolean} handleEmptyResult Should empty results be handled as an error * @param {string} query The query to validate the database with * @returns {Promise<(string[] | object[] | object)>} Response from * server */ -exports.postgresQuery = function (connectionString, query) { +exports.postgresQuery = function (connectionString, handleEmptyResult, query) { return new Promise((resolve, reject) => { const config = postgresConParse(connectionString); @@ -384,7 +391,13 @@ exports.postgresQuery = function (connectionString, query) { if (err) { reject(err); } else { - resolve(res); + if (Array.isArray(res.rows)) { + if (handleEmptyResult && res.rows.length === 0) { + reject(new Error("No rows returned")); + } else { + resolve(res); + } + } } client.end(); }); @@ -401,11 +414,12 @@ exports.postgresQuery = function (connectionString, query) { /** * Run a query on MySQL/MariaDB * @param {string} connectionString The database connection string + * @param {boolean} handleEmptyResult Should empty results be handled as an error * @param {string} query The query to validate the database with * @param {?string} password The password to use * @returns {Promise<(string)>} Response from server */ -exports.mysqlQuery = function (connectionString, query, password = undefined) { +exports.mysqlQuery = function (connectionString, handleEmptyResult, query, password = undefined) { return new Promise((resolve, reject) => { const connection = mysql.createConnection({ uri: connectionString, @@ -421,7 +435,11 @@ exports.mysqlQuery = function (connectionString, query, password = undefined) { reject(err); } else { if (Array.isArray(res)) { - resolve("Rows: " + res.length); + if (handleEmptyResult && res.length === 0) { + reject(new Error("No rows returned")); + } else { + resolve("Rows: " + res.length); + } } else { resolve("No Error, but the result is not an array. Type: " + typeof res); } diff --git a/src/lang/en.json b/src/lang/en.json index e215f1031..701674b2b 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1051,5 +1051,7 @@ "RabbitMQ Password": "RabbitMQ Password", "rabbitmqHelpText": "To use the monitor, you will need to enable the Management Plugin in your RabbitMQ setup. For more information, please consult the {rabitmq_documentation}.", "SendGrid API Key": "SendGrid API Key", - "Separate multiple email addresses with commas": "Separate multiple email addresses with commas" + "Separate multiple email addresses with commas": "Separate multiple email addresses with commas", + "Handle empty result as error": "Handle empty result as error", + "handleEmptyResultDescription": "If the query returns no results, the monitor will be treated as down." } diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index a83f91cab..e2c8b185a 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -539,6 +539,19 @@ + + +