From c44839e0c36364f56a57f07e849be6ce753762c1 Mon Sep 17 00:00:00 2001 From: Durlabh Jain Date: Tue, 2 Jun 2026 14:48:06 -0400 Subject: [PATCH 1/5] feat: enhance BusinessBase with customizable where clause and operation modes --- lib/business/business-base.mjs | 126 ++++++++++++++++++++++++++------- 1 file changed, 101 insertions(+), 25 deletions(-) diff --git a/lib/business/business-base.mjs b/lib/business/business-base.mjs index b45d41b..f1c2787 100644 --- a/lib/business/business-base.mjs +++ b/lib/business/business-base.mjs @@ -19,6 +19,12 @@ const filterFields = { ModifiedByUser: "Modified_" } +const OperationMode = { + load: 'load', + list: 'list', + lookupList: 'lookupList', +}; + const dateTypeFields = ["date", "dateTime"]; const IsDeletedColumn = "IsDeleted"; @@ -254,17 +260,31 @@ class BusinessBase { return BusinessBase.businessObject.sql.createRequest(this.logger); } - createWhere({ alias = "Main", filterDeleted = true } = {}) { + /** + * createWhere - Generates a WHERE clause object based on the current business object's configuration and optional parameters. + * @param {Object} alias - The alias to use for table references in the WHERE clause (default is "Main"). + * @returns {Object} A WHERE clause object that can be used in SQL queries, including client-based filtering and soft delete conditions if applicable. + */ + async createWhere({ alias = "Main", ...options } = {}) { const where = {}; if (this.clientBased && this.user.scopeId) { where[`${alias}.ClientId`] = this.user.scopeId; } - if (filterDeleted && this.softDelete !== false) { + if (options.isStandard && this.softDelete !== false) { where[`${alias}.IsDeleted`] = 0; } + if (typeof this.customizeWhere === 'function') { + await this.customizeWhere({ where, alias, ...options }); + } return where; } + /** + * customizeWhere - Optional hook for customizing the WHERE clause in SQL queries. + * @param {Object} param0 - An object containing the current WHERE clause, alias, and additional options. + * @returns {Promise} A promise that resolves when the customization is complete. + */ + pluralize(str) { return str + 's'; } @@ -278,7 +298,7 @@ class BusinessBase { let query = this.getSelectStatement(); - const where = await this.createWhere({ filterDeleted: this.standardTable }); + const where = await this.createWhere({ isStandard: this.standardTable, operationMode: OperationMode.load }); where[keyField] = id; const request = this.createRequest(); @@ -620,8 +640,10 @@ class BusinessBase { return await sql.insertUpdate({ tableName: this.getTableName(), keyField, id, json: values, update: true, logger: this.logger }); } - getListStatement() { - return this.listStatement || (this.standardTable && this.useView !== false ? `SELECT * FROM vw${this.getTableName()}List Main` : this.getSelectStatement()); + async getListStatement({ scopeId, forLookup = false, ...otherListParameters } = {}) { + const listStatement = this.listStatement || (this.standardTable && this.useView !== false ? `SELECT * FROM vw${this.getTableName()}List Main` : this.getSelectStatement()); + const isStandard = this.standardTable === true && listStatement.indexOf("vw") === -1; + return { listStatement, isStandard }; } async lookupList({ scopeId }) { @@ -634,8 +656,7 @@ class BusinessBase { } const sql = BusinessBase.businessObject.sql; - let listStatement = this.getListStatement(); - const isStandard = this.standardTable === true && listStatement.indexOf("vw") === -1; + let { listStatement, isStandard } = await this.getListStatement({ scopeId, forLookup: true }); const labelField = displayField || sort || this.sort; if (!labelField) { this.logger.error('No displayField or sort field defined for lookupList label.'); @@ -644,7 +665,7 @@ class BusinessBase { listStatement = listStatement.replace(/^.+ FROM/i, `SELECT [${keyField}] value, [${labelField}] label FROM `); let query = listStatement; - const where = await this.createWhere({ filterDeleted: isStandard, tableName }); + const where = await this.createWhere({ isStandard, tableName, operationMode: OperationMode.lookupList }); if (!clientBased && scopeId) { where.ScopeId = scopeId; } @@ -660,7 +681,23 @@ class BusinessBase { return result.recordset; } - async list({ start = 0, limit = 100, sort, filter, groupBy, include, exclude, returnCount = true }) { + /** + * addAdditionalColumns - Optional hook for adding custom JOINs and columns to the list SQL statement. + * @param {Object} param0 - An object containing the current SQL statement, request, and additional parameters for context. + * @returns {Promise} An object that can include a modified listStatement and an array of additionalColumns to be added to the SELECT clause. + */ + + /** + * customizeList - Optional hook for customizing the list results + * @param {Object} param0 - An object containing the current SQL statement, request, and additional parameters for context. + * @returns {Promise} A promise that resolves when the customization is complete. + */ + + /** + * List records with optional hooks for extensibility + * Supports hooks: customizeWhere, addAdditionalColumns,customizeList + */ + async list({ start = 0, limit = 100, sort, filter, groupBy, include, exclude, returnCount = true, ...options }) { sort = sort || this.defaultSortOrder; const request = this.createRequest(); const { keyField } = this; @@ -669,9 +706,32 @@ class BusinessBase { let totalStatement = "SELECT COUNT(1) AS TotalCount"; const { relations = [] } = this; - let listStatement = this.getListStatement(); - const isStandard = this.standardTable === true && listStatement.indexOf("vw") === -1; + + const hookParameters = { + ...options, + forLookup: false, + sql, + request, + keyField, + start, + limit, + sort, + filter, + groupBy, + include, + exclude, + returnCount, + relations, + operationMode: OperationMode.list + }; + + let { listStatement, isStandard } = await this.getListStatement(hookParameters); const isDataFromView = listStatement.indexOf("vw") > -1; + + hookParameters.listStatement = listStatement; + hookParameters.isStandard = isStandard; + hookParameters.isDataFromView = isDataFromView; + const additionalColumns = []; if (isStandard) { listStatement += '\r\n LEFT OUTER JOIN (SELECT UserId Created_UserId, UserName as CreatedByUser FROM Security_User) Created_ ON Created_.Created_UserId = Main.CreatedByUserId' @@ -697,12 +757,26 @@ class BusinessBase { } } + + // Hook: addAdditionalColumns - Allow adding custom JOINs and columns + if (typeof this.addAdditionalColumns === 'function' ) { + const result = await this.addAdditionalColumns(hookParameters); + + if (result) { + listStatement = result.listStatement || listStatement; + if (result.additionalColumns && result.additionalColumns.length > 0) { + additionalColumns.push(...result.additionalColumns); + } + } + } + if (additionalColumns.length > 0) { listStatement = listStatement.replace(/ from /i, ', ' + additionalColumns.join(', ') + ' FROM '); } let query = listStatement; - const where = this.createWhere({ filterDeleted: isStandard }); + const where = await this.createWhere(hookParameters); + if (typeof include === 'string') { include = include.split(',').map(item => Number(item)); } @@ -782,25 +856,27 @@ class BusinessBase { const result = await request.query(query); - if (returnCount) { - let recordCount; + const listResult = { + records: result.recordset + }; + if (returnCount) { if (limit > 0) { - recordCount = result.recordsets[1][0].TotalCount; + listResult.recordCount = result.recordsets[1][0].TotalCount; } else { - recordCount = result.rowsAffected[0]; + listResult.recordCount = result.rowsAffected[0]; } + } - return { - records: result.recordset, - recordCount - } - } else { - return { - records: result.recordset - } + hookParameters.listResult = listResult; + + // Hook: customizeList - Allow result post-processing + if (typeof this.customizeList === 'function') { + await this.customizeList(hookParameters); } + return hookParameters.listResult; + } static async handleMultiSelectValues({ multiSelectValues, multiSelectColumns, getTableName, keyField, id, user, sql, isUpdate, softDelete }) { @@ -883,6 +959,6 @@ const classMap = { } }; -export { RelationshipTypes, BusinessBase, classMap }; +export { RelationshipTypes, BusinessBase, classMap, OperationMode }; export default BusinessBase; \ No newline at end of file From 5317a12ed34b3d7e953daf80dfebbf5b31585733 Mon Sep 17 00:00:00 2001 From: Durlabh Jain Date: Tue, 2 Jun 2026 15:24:21 -0400 Subject: [PATCH 2/5] fix for getListStatement --- lib/business/business-base.mjs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/business/business-base.mjs b/lib/business/business-base.mjs index f1c2787..3cdc882 100644 --- a/lib/business/business-base.mjs +++ b/lib/business/business-base.mjs @@ -646,6 +646,16 @@ class BusinessBase { return { listStatement, isStandard }; } + normalizeListStatement(result) { + if(typeof result === 'string') { + return { + listStatement: result, + isStandard: this.standardTable === true && result.indexOf("vw") === -1 + } + } + return result; + } + async lookupList({ scopeId }) { const request = this.createRequest(); const { keyField, lookupSortOrder, defaultSortOrder, displayField, clientBased, lookupListStatement = '', tableName } = this; @@ -656,7 +666,7 @@ class BusinessBase { } const sql = BusinessBase.businessObject.sql; - let { listStatement, isStandard } = await this.getListStatement({ scopeId, forLookup: true }); + let { listStatement, isStandard } = this.normalizeListStatement(await this.getListStatement({ scopeId, forLookup: true })); const labelField = displayField || sort || this.sort; if (!labelField) { this.logger.error('No displayField or sort field defined for lookupList label.'); @@ -725,7 +735,7 @@ class BusinessBase { operationMode: OperationMode.list }; - let { listStatement, isStandard } = await this.getListStatement(hookParameters); + let { listStatement, isStandard } = this.normalizeListStatement(await this.getListStatement(hookParameters)); const isDataFromView = listStatement.indexOf("vw") > -1; hookParameters.listStatement = listStatement; From 6e65f3371c437c06f81b1b293955f8e54a51136d Mon Sep 17 00:00:00 2001 From: Durlabh Jain Date: Tue, 2 Jun 2026 15:35:17 -0400 Subject: [PATCH 3/5] normalizeListStatement --- lib/business/business-base.mjs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/business/business-base.mjs b/lib/business/business-base.mjs index 3cdc882..2e4727f 100644 --- a/lib/business/business-base.mjs +++ b/lib/business/business-base.mjs @@ -640,7 +640,7 @@ class BusinessBase { return await sql.insertUpdate({ tableName: this.getTableName(), keyField, id, json: values, update: true, logger: this.logger }); } - async getListStatement({ scopeId, forLookup = false, ...otherListParameters } = {}) { + async getListStatement(listParameters) { const listStatement = this.listStatement || (this.standardTable && this.useView !== false ? `SELECT * FROM vw${this.getTableName()}List Main` : this.getSelectStatement()); const isStandard = this.standardTable === true && listStatement.indexOf("vw") === -1; return { listStatement, isStandard }; @@ -666,7 +666,20 @@ class BusinessBase { } const sql = BusinessBase.businessObject.sql; - let { listStatement, isStandard } = this.normalizeListStatement(await this.getListStatement({ scopeId, forLookup: true })); + let { listStatement, isStandard } = this.normalizeListStatement( + await this.getListStatement({ + scopeId, + operationMode: OperationMode.lookupList, + keyField, + lookupSortOrder, + defaultSortOrder, + displayField, + clientBased, + lookupListStatement, + tableName, + sort + }) + ); const labelField = displayField || sort || this.sort; if (!labelField) { this.logger.error('No displayField or sort field defined for lookupList label.'); @@ -719,7 +732,6 @@ class BusinessBase { const hookParameters = { ...options, - forLookup: false, sql, request, keyField, From 6230778f2ffc014eb340e67618e02f84ca4bd848 Mon Sep 17 00:00:00 2001 From: durlabhjain Date: Tue, 2 Jun 2026 15:45:25 -0400 Subject: [PATCH 4/5] JSDoc fix Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/business/business-base.mjs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/business/business-base.mjs b/lib/business/business-base.mjs index 2e4727f..116689f 100644 --- a/lib/business/business-base.mjs +++ b/lib/business/business-base.mjs @@ -261,9 +261,11 @@ class BusinessBase { } /** - * createWhere - Generates a WHERE clause object based on the current business object's configuration and optional parameters. - * @param {Object} alias - The alias to use for table references in the WHERE clause (default is "Main"). - * @returns {Object} A WHERE clause object that can be used in SQL queries, including client-based filtering and soft delete conditions if applicable. + * Generates a WHERE clause object based on the current business object's configuration and optional parameters. + * @param {Object} [options] + * @param {string} [options.alias="Main"] - The alias to use for table references in the WHERE clause. + * @param {boolean} [options.isStandard] - When true, apply soft-delete filtering (IsDeleted = 0) if enabled. + * @returns {Promise} A WHERE clause object that can be passed to sql.addParameters({ forWhere: true }). */ async createWhere({ alias = "Main", ...options } = {}) { const where = {}; From 9290da6eaba9f0a0e8216346c9f62b0505ea9e76 Mon Sep 17 00:00:00 2001 From: Durlabh Jain Date: Tue, 2 Jun 2026 15:49:09 -0400 Subject: [PATCH 5/5] fix - update listStatement --- lib/business/business-base.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/business/business-base.mjs b/lib/business/business-base.mjs index 116689f..208ca93 100644 --- a/lib/business/business-base.mjs +++ b/lib/business/business-base.mjs @@ -798,8 +798,9 @@ class BusinessBase { listStatement = listStatement.replace(/ from /i, ', ' + additionalColumns.join(', ') + ' FROM '); } - let query = listStatement; + hookParameters.listStatement = listStatement; const where = await this.createWhere(hookParameters); + let query = hookParameters.listStatement; if (typeof include === 'string') { include = include.split(',').map(item => Number(item));