From dfc66d7519d17e858c14c8e0638e0f1c470615ce Mon Sep 17 00:00:00 2001 From: Johnny Hausman Date: Wed, 5 Nov 2025 17:59:52 +0700 Subject: [PATCH 1/4] [wip] reqService queryWhereCondition() now supports more conditions --- utils/reqService.js | 84 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/utils/reqService.js b/utils/reqService.js index 6fc0555..179c862 100644 --- a/utils/reqService.js +++ b/utils/reqService.js @@ -720,17 +720,93 @@ class ABRequestService extends EventEmitter { if (cond) { var params = []; Object.keys(cond).forEach((key) => { - values.push(cond[key]); - if (Array.isArray(cond[key])) { - if (cond[key].length > 0) { + const val = cond[key]; + const keyLower = String(key).toLowerCase(); + // Support logical OR: { or: [ {condA}, {condB} ] } + if (keyLower === "or" && Array.isArray(val)) { + const subParams = []; + val.forEach((sub) => { + const { condition: subCond, values: subVals } = + this.queryWhereCondition(sub); + if (subCond) { + subParams.push(subCond); + values = values.concat(subVals); + } + }); + if (subParams.length > 0) { + params.push(`( ${subParams.join(" OR ")} )`); + } else { + // empty OR list evaluates to false + params.push(` 1 = 0 `); + } + return; // handled this key + } + if (Array.isArray(val)) { + if (val.length > 0) { params.push(`${key} IN ( ? )`); + values.push(val); } else { // if an empty array then we falsify this condition: - values.pop(); // remove pushed value above params.push(` 1 = 0 `); } + } else if (val && typeof val === "object") { + // Support operator objects like { "<": 50 }, { "!=": "blue" }, + // as well as Waterline-style: in, nin, contains, startsWith, endsWith + Object.keys(val).forEach((op) => { + const rawValue = val[op]; + const opLower = String(op).toLowerCase(); + + if (opLower === "in") { + params.push(`${key} IN ( ? )`); + values.push(rawValue); + return; + } + if (opLower === "nin") { + params.push(`${key} NOT IN ( ? )`); + values.push(rawValue); + return; + } + if (opLower === "contains") { + params.push(`${key} LIKE ?`); + values.push(`%${rawValue}%`); + return; + } + if (opLower === "startswith") { + params.push(`${key} LIKE ?`); + values.push(`${rawValue}%`); + return; + } + if (opLower === "endswith") { + params.push(`${key} LIKE ?`); + values.push(`%${rawValue}`); + return; + } + + // default comparison operators + if (op === "!=" && Array.isArray(rawValue)) { + // NOT IN array form: { field: { '!=': [a,b] } } + params.push(`${key} NOT IN ( ? )`); + values.push(rawValue); + return; + } + + const opMap = { + "=": "=", + "!=": "!=", + "<": "<", + "<=": "<=", + ">": ">", + ">=": ">=", + like: "LIKE", + LIKE: "LIKE", + }; + const sqlOp = opMap[op] || op; + params.push(`${key} ${sqlOp} ?`); + values.push(rawValue); + }); } else { params.push(`${key} = ?`); + values.push(val); } }); condition = `${params.join(" AND ")}`; From fbfdc511914cc1fb05fdd06d88a7624b1a07334b Mon Sep 17 00:00:00 2001 From: Johnny Hausman Date: Wed, 5 Nov 2025 18:01:05 +0700 Subject: [PATCH 2/4] [wip] async versions of our query() and queryIsolate() methods --- utils/reqService.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/utils/reqService.js b/utils/reqService.js index 179c862..9ee3e86 100644 --- a/utils/reqService.js +++ b/utils/reqService.js @@ -658,6 +658,31 @@ class ABRequestService extends EventEmitter { }); } + /** + * perform an sql query directly on our dbConn, returning a Promise. + * @param {string} query the sql query to perform. Use "?" for placeholders. + * @param {array} values the array of values that correspond to the + * placeholders in the sql + * @param {MySQL} [dbConn] the DB Connection to use for this request. If not + * provided the common dbConnection() will be used. + * @returns {Promise<{results, fields}>} + */ + queryAsync(query, values, dbConn) { + return new Promise((resolve, reject) => { + this.query( + query, + values, + (err, results, fields) => { + if (err) { + return reject(err); + } + resolve({ results, fields }); + }, + dbConn, + ); + }); + } + /** * Perform a query on it's own DB Connection. Not shared with other requests. * @param {string} query the sql query to perform. Use "?" for placeholders. @@ -673,6 +698,25 @@ class ABRequestService extends EventEmitter { this.query(query, values, cb, this.___isoDB); } + /** + * Perform a query on it's own DB Connection, returning a Promise. Not shared + * with other requests. + * @param {string} query the sql query to perform. Use "?" for placeholders. + * @param {array} values the array of values that correspond to the + * placeholders in the sql + * @returns {Promise<{results, fields}>} + */ + queryIsolateAsync(query, values) { + return new Promise((resolve, reject) => { + this.queryIsolate(query, values, (err, results, fields) => { + if (err) { + return reject(err); + } + resolve({ results, fields }); + }); + }); + } + /** * Ensure the temporary isolated db connection is closed out properly. * This method is intended to be used after all your desired queryIsolate() From 200ff6dd48a9f1c0dee647e7d1b9f97b9d7583e6 Mon Sep 17 00:00:00 2001 From: Johnny Hausman Date: Wed, 5 Nov 2025 18:01:26 +0700 Subject: [PATCH 3/4] [wip] eslint changes --- utils/reqService.js | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/utils/reqService.js b/utils/reqService.js index 9ee3e86..a63dff2 100644 --- a/utils/reqService.js +++ b/utils/reqService.js @@ -45,7 +45,7 @@ function deCircular(args, o, context, level = 1) { args.push( `${context ? context : ""}${ context ? "." : "" - }${k}: ${JSON.stringify(o[k].toObj())}` + }${k}: ${JSON.stringify(o[k].toObj())}`, ); } else { if (!o[k].____deCircular) { @@ -54,14 +54,14 @@ function deCircular(args, o, context, level = 1) { args, o[k], (context ? context + "->" : "") + k, - level + 1 + level + 1, ); } } } else { if (typeof o[k] != "function") { args.push( - `${context ? context : ""}${context ? "." : ""}${k}: ${o[k]}` + `${context ? context : ""}${context ? "." : ""}${k}: ${o[k]}`, ); } } @@ -328,7 +328,7 @@ class ABRequestService extends EventEmitter { return; } resolve(); - } + }, ); }); }; @@ -373,7 +373,7 @@ class ABRequestService extends EventEmitter { return; } resolve(); - } + }, ); }); }; @@ -420,7 +420,7 @@ class ABRequestService extends EventEmitter { return; } resolve(); - } + }, ); }); }; @@ -530,8 +530,8 @@ class ABRequestService extends EventEmitter { args.push( // FIX: TypeError: Do not know how to serialize a BigInt JSON.stringify(a, (key, value) => - typeof value === "bigint" ? value.toString() + "n" : value - ) + typeof value === "bigint" ? value.toString() + "n" : value, + ), ); } catch (e) { if (e.toString().indexOf("circular") > -1) { @@ -646,7 +646,6 @@ class ABRequestService extends EventEmitter { return reject(error); } resolve({ results, fields }); - // cb(error, results, fields); }); }); }) @@ -732,20 +731,25 @@ class ABRequestService extends EventEmitter { * return the tenantDB value for this req object. * this is a helper function that simplifies the error handling if no * tenantDB is found. - * @param {Promise.reject} reject a reject() handler to be called if a - * tenantDB is not found. - * @return {false|string} false if tenantDB not found, otherwise the tenantDB - * name (string). + * @param {Promise.reject} [reject] a reject() handler to be called if a + * tenantDB is not found. If not provided, an error will be thrown. + * @return {false|string} false if tenantDB not found and reject is provided, + * otherwise the tenantDB name (string). + * @throws {Error} if tenantDB not found and reject is not provided. */ queryTenantDB(reject) { let tenantDB = this.tenantDB(); if (tenantDB == "") { let errorNoTenant = new Error( - `Unable to find tenant information for tenantID[${this.tenantID()}]` + `Unable to find tenant information for tenantID[${this.tenantID()}]`, ); errorNoTenant.code = "ENOTENANT"; - reject(errorNoTenant); - return false; + if (reject) { + reject(errorNoTenant); + return false; + } else { + throw errorNoTenant; + } } return tenantDB; } @@ -1029,7 +1033,7 @@ class ABRequestService extends EventEmitter { jobID: `ABFactory(${this._tenantID})`, tenantID: this._tenantID, }, - this.controller + this.controller, ); ABReq._DBConn = this._DBConn; ABReq._Model = this._Model; @@ -1045,7 +1049,7 @@ class ABRequestService extends EventEmitter { ["jobID", "_tenantID", "_user", "_userReal", "serviceKey"].forEach( (f) => { obj[f] = this[f]; - } + }, ); return obj; } From af3ca6d9b2b42ae5efc5fef2e8c7184ae8d1ca77 Mon Sep 17 00:00:00 2001 From: Johnny Hausman Date: Sat, 8 Nov 2025 16:58:23 +0700 Subject: [PATCH 4/4] [wip] include unit tests for queryAsync() and queryIsolateAsync() --- test/unit/reqService.test.js | 264 +++++++++++++++++++++++++++++++++++ 1 file changed, 264 insertions(+) diff --git a/test/unit/reqService.test.js b/test/unit/reqService.test.js index e3ec908..4964bd2 100644 --- a/test/unit/reqService.test.js +++ b/test/unit/reqService.test.js @@ -304,4 +304,268 @@ describe("ABRequestAPI", () => { assert.equal(await result, 3); }); }); + + describe(".queryAsync()", () => { + let mockDbConn; + let queryStub; + + beforeEach(() => { + queryStub = sinon.stub(); + mockDbConn = { + query: queryStub, + }; + sinon.replace(req, "dbConnection", sinon.fake.returns(mockDbConn)); + }); + + it("returns a Promise that resolves with results and fields on success", async () => { + const mockResults = [{ id: 1, name: "test" }]; + const mockFields = [{ name: "id" }, { name: "name" }]; + const query = "SELECT * FROM test WHERE id = ?"; + const values = [1]; + + const queryObj = { sql: query }; + queryStub.callsFake((sql, vals, callback) => { + // Call callback asynchronously to simulate real behavior + setImmediate(() => { + callback(null, mockResults, mockFields); + }); + return queryObj; + }); + + const result = await req.queryAsync(query, values); + + assert(queryStub.calledOnce); + assert.equal(queryStub.firstCall.args[0], query); + assert.deepEqual(queryStub.firstCall.args[1], values); + assert.isFunction(queryStub.firstCall.args[2]); + assert.deepEqual(result.results, mockResults); + assert.deepEqual(result.fields, mockFields); + }); + + it("uses default dbConnection when not provided", async () => { + const mockResults = [{ id: 1 }]; + const mockFields = []; + const query = "SELECT * FROM test"; + const values = []; + + const queryObj = { sql: query }; + queryStub.callsFake((sql, vals, callback) => { + setImmediate(() => { + callback(null, mockResults, mockFields); + }); + return queryObj; + }); + + await req.queryAsync(query, values); + + assert(queryStub.calledOnce); + }); + + it("uses provided dbConnection when specified", async () => { + const query = "SELECT * FROM test"; + const values = []; + const queryObj = { sql: query }; + const customDbConn = { + query: sinon.stub().callsFake((sql, vals, callback) => { + setImmediate(() => { + callback(null, [{ id: 2 }], []); + }); + return queryObj; + }), + }; + + await req.queryAsync(query, values, customDbConn); + + assert(customDbConn.query.calledOnce); + assert(queryStub.notCalled); + }); + + it("rejects Promise when query returns an error", async () => { + const query = "SELECT * FROM test"; + const values = []; + const mockError = new Error("Database error"); + mockError.code = "ER_SYNTAX_ERROR"; + + const queryObj = { sql: query }; + queryStub.callsFake((sql, vals, callback) => { + // Call callback asynchronously to simulate real behavior + setImmediate(() => { + callback(mockError, null, null); + }); + return queryObj; + }); + + try { + await req.queryAsync(query, values); + assert.fail("Should have thrown an error"); + } catch (error) { + assert.equal(error.message, mockError.message); + assert.equal(error.code, mockError.code); + assert.equal(error.sql, query); + } + }); + + it("propagates errors from the underlying query method", async () => { + const query = "SELECT * FROM test"; + const values = []; + const mockError = new Error("Invalid query syntax"); + mockError.code = "ER_PARSE_ERROR"; // Non-retryable error + + const queryObj = { sql: query }; + queryStub.callsFake((sql, vals, callback) => { + // Call callback asynchronously to simulate real behavior + setImmediate(() => { + callback(mockError, null, null); + }); + return queryObj; + }); + + try { + await req.queryAsync(query, values); + assert.fail("Should have thrown an error"); + } catch (error) { + assert.equal(error.message, mockError.message); + assert.equal(error.code, mockError.code); + } + }); + }); + + describe(".queryIsolateAsync()", () => { + let mockDbConn; + let queryStub; + let dbConnectionStub; + + beforeEach(() => { + // Clean up any existing isolated connection + req.___isoDB = undefined; + queryStub = sinon.stub(); + mockDbConn = { + query: queryStub, + }; + dbConnectionStub = sinon.stub(req, "dbConnection").returns(mockDbConn); + }); + + afterEach(() => { + // Clean up isolated connection after each test + req.___isoDB = undefined; + }); + + it("creates an isolated DB connection on first call", async () => { + const mockResults = [{ id: 1, name: "test" }]; + const mockFields = [{ name: "id" }, { name: "name" }]; + const query = "SELECT * FROM test WHERE id = ?"; + const values = [1]; + + const queryObj = { sql: query }; + queryStub.callsFake((sql, vals, callback) => { + setImmediate(() => { + callback(null, mockResults, mockFields); + }); + return queryObj; + }); + + await req.queryIsolateAsync(query, values); + + assert(dbConnectionStub.calledOnce); + assert.equal(dbConnectionStub.firstCall.args[0], false); + assert.equal(dbConnectionStub.firstCall.args[1], true); + }); + + it("reuses the same isolated DB connection on subsequent calls", async () => { + const mockResults = [{ id: 1 }]; + const mockFields = []; + const query = "SELECT * FROM test"; + const values = []; + + const queryObj = { sql: query }; + queryStub.callsFake((sql, vals, callback) => { + setImmediate(() => { + callback(null, mockResults, mockFields); + }); + return queryObj; + }); + + await req.queryIsolateAsync(query, values); + await req.queryIsolateAsync(query, values); + + assert(dbConnectionStub.calledOnce); + assert.isDefined(req.___isoDB); + assert.equal(req.___isoDB, mockDbConn); + }); + + it("returns a Promise that resolves with results and fields on success", async () => { + const mockResults = [{ id: 1, name: "test" }]; + const mockFields = [{ name: "id" }, { name: "name" }]; + const query = "SELECT * FROM test WHERE id = ?"; + const values = [1]; + + const queryObj = { sql: query }; + queryStub.callsFake((sql, vals, callback) => { + setImmediate(() => { + callback(null, mockResults, mockFields); + }); + return queryObj; + }); + + const result = await req.queryIsolateAsync(query, values); + + assert(queryStub.calledOnce); + assert.equal(queryStub.firstCall.args[0], query); + assert.deepEqual(queryStub.firstCall.args[1], values); + assert.isFunction(queryStub.firstCall.args[2]); + assert.deepEqual(result.results, mockResults); + assert.deepEqual(result.fields, mockFields); + }); + + it("rejects Promise when query returns an error", async () => { + const query = "SELECT * FROM test"; + const values = []; + const mockError = new Error("Database error"); + mockError.code = "ER_SYNTAX_ERROR"; + + const queryObj = { sql: query }; + queryStub.callsFake((sql, vals, callback) => { + // Call callback asynchronously to simulate real behavior + setImmediate(() => { + callback(mockError, null, null); + }); + return queryObj; + }); + + try { + await req.queryIsolateAsync(query, values); + assert.fail("Should have thrown an error"); + } catch (error) { + assert.equal(error.message, mockError.message); + assert.equal(error.code, mockError.code); + assert.equal(error.sql, query); + } + }); + + it("calls queryIsolate internally which uses the isolated connection", async () => { + const mockResults = [{ id: 1 }]; + const mockFields = []; + const query = "SELECT * FROM test"; + const values = []; + + const queryObj = { sql: query }; + queryStub.callsFake((sql, vals, callback) => { + setImmediate(() => { + callback(null, mockResults, mockFields); + }); + return queryObj; + }); + + const queryIsolateStub = sinon.spy(req, "queryIsolate"); + + await req.queryIsolateAsync(query, values); + + assert(queryIsolateStub.calledOnce); + assert.equal(queryIsolateStub.firstCall.args[0], query); + assert.deepEqual(queryIsolateStub.firstCall.args[1], values); + assert.isFunction(queryIsolateStub.firstCall.args[2]); + + queryIsolateStub.restore(); + }); + }); });