Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 73 additions & 20 deletions lib/business/business-base.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import mssql from 'mssql';
import SqlHelper from './sql-helper.mjs';
import { sqlErrorMapper } from './error-mapper.mjs';
import ConcatenatedColumns from './concatenated-columns.mjs';

const enums = {
startDateTime: '00:00:00',
Expand Down Expand Up @@ -256,8 +257,10 @@ class BusinessBase {
return this.selectStatement || `SELECT ${alias}.* FROM ${tableName} ${alias}`;
}

createRequest() {
return BusinessBase.businessObject.sql.createRequest(this.logger);
dbAdapter = 'sql';

getDatabaseAdapter() {
return BusinessBase.businessObject[this.dbAdapter];
}

/**
Expand Down Expand Up @@ -302,9 +305,8 @@ class BusinessBase {

const where = await this.createWhere({ isStandard: this.standardTable, operationMode: OperationMode.load });
where[keyField] = id;
const request = this.createRequest();

const sql = BusinessBase.businessObject.sql;
const sql = this.getDatabaseAdapter();
const request = sql.createRequest();

query = sql.addParameters({ query, request, parameters: where, forWhere: true });
Comment on lines +308 to 311

Expand Down Expand Up @@ -402,7 +404,7 @@ class BusinessBase {
}
const tableName = this.getTableName();
const isUpdate = id ? parseInt(id) !== 0 : false;
const sql = BusinessBase.businessObject.sql;
const sql = this.getDatabaseAdapter();
const clientId = user.scopeId;

// todo: Client check
Expand Down Expand Up @@ -642,14 +644,24 @@ class BusinessBase {
return await sql.insertUpdate({ tableName: this.getTableName(), keyField, id, json: values, update: true, logger: this.logger });
}

/**
* queryFileName - Optional property to specify a SQL file for the list statement. If provided, this file will be used instead of the default listStatement or generated SELECT statement.
*/
queryFileName = null;

async getListStatement(listParameters) {
const listStatement = this.listStatement || (this.standardTable && this.useView !== false ? `SELECT * FROM vw${this.getTableName()}List Main` : this.getSelectStatement());
let listStatement;
if (this.queryFileName) {
listStatement = await listParameters.sql.getQuery(this.queryFileName);
} else {
listStatement = this.listStatement || (this.standardTable && this.useView !== false ? `SELECT * FROM vw${this.getTableName()}List Main` : this.getSelectStatement());
}
Comment thread
durlabhjain marked this conversation as resolved.
const isStandard = this.standardTable === true && listStatement.indexOf("vw") === -1;
return { listStatement, isStandard };
}

normalizeListStatement(result) {
if(typeof result === 'string') {
if (typeof result === 'string') {
return {
listStatement: result,
isStandard: this.standardTable === true && result.indexOf("vw") === -1
Expand All @@ -659,14 +671,14 @@ class BusinessBase {
}

async lookupList({ scopeId }) {
const request = this.createRequest();
const sql = this.getDatabaseAdapter();
const request = sql.createRequest();
const { keyField, lookupSortOrder, defaultSortOrder, displayField, clientBased, lookupListStatement = '', tableName } = this;
const sort = lookupSortOrder || defaultSortOrder;
if (lookupListStatement) {
const result = await request.query(lookupListStatement);
return result.recordset;
}
Comment on lines 679 to 681
const sql = BusinessBase.businessObject.sql;

let { listStatement, isStandard } = this.normalizeListStatement(
await this.getListStatement({
Expand Down Expand Up @@ -724,9 +736,9 @@ class BusinessBase {
*/
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;
const sql = BusinessBase.businessObject.sql;
const sql = this.getDatabaseAdapter();
const request = sql.createRequest();
const { keyField, concatenatedColumns = [] } = this;
const whereArr = this.parseJson(filter, []);
let totalStatement = "SELECT COUNT(1) AS TotalCount";

Expand All @@ -746,6 +758,7 @@ class BusinessBase {
exclude,
returnCount,
relations,
concatenatedColumns,
operationMode: OperationMode.list
};

Expand Down Expand Up @@ -783,7 +796,8 @@ class BusinessBase {
}

// Hook: addAdditionalColumns - Allow adding custom JOINs and columns
if (typeof this.addAdditionalColumns === 'function' ) {
if (typeof this.addAdditionalColumns === 'function') {
hookParameters.listStatement = listStatement;
const result = await this.addAdditionalColumns(hookParameters);

if (result) {
Expand Down Expand Up @@ -819,8 +833,39 @@ class BusinessBase {
}
if (whereArr.length) {
whereArr.forEach((ele) => {
if (!ele) return; // guard against null/ undefined
const { operator, field, value, type } = ele;
if (!field) return; // guard against filter objects with missing field name
const filterValue = compareLookups[operator]({ v: value, field, type });
const concatIdx = concatenatedColumns.findIndex(c => c.DisplayColumn === field) ?? -1;
if (concatIdx !== -1) {
const concatenatedColumn = concatenatedColumns[concatIdx];
const paramName = `concat_${index}`;
if (typeof filterValue === 'string') {
where[`filter_${index}`] = {
statement: ConcatenatedColumns.applyStringFilter({
sql,
value,
concatenatedColumn,
request,
paramName,
operator: sql.normalizeOperator(operator)
})
};
return;
} else if (filterValue) {
Comment thread
durlabhjain marked this conversation as resolved.
where[`filter_${index}`] = {
statement: ConcatenatedColumns.applyStringFilter({
value: filterValue.value,
concatenatedColumn,
request,
paramName,
operator: filterValue.operator
})
};
Comment on lines +857 to +865
}
return;
}
let fieldName = isDataFromView ? field : `Main.${field}`;
if (filterFields[field]) {
fieldName = `${filterFields[field]}.${field}`;
Expand Down Expand Up @@ -860,12 +905,11 @@ class BusinessBase {
}

if (limit > 0) {
query += ' OFFSET @_start ROWS FETCH NEXT @_limit ROWS ONLY';
query = BusinessBase.businessObject.sql.addParameters({ query: query, request, parameters: { _start: start, _limit: limit }, forWhere: false });
query = sql.addPaging({ query, request, start, limit });
}

if (groupBy) {
let groupByFields = groupBy.split(',');
let groupByFields = Array.isArray(groupBy) ? groupBy : groupBy.split(',');
groupByFields = groupByFields.map(field => SqlHelper.sanitizeField(field));
const groupByStatement = ' GROUP BY ' + SqlHelper.sanitizeField(groupByFields.join(', '));
query += groupByStatement;
Expand All @@ -879,17 +923,26 @@ class BusinessBase {
}


const result = await request.query(query);
const result = await sql.runQuery({ request, type: "query", query });
if (result.err) throw result.err;

const listResult = {
records: result.recordset
records: result.recordsets[0]
};

if (listResult.records?.length && concatenatedColumns.length) {
const resultValue = await ConcatenatedColumns.addColumns({ records: listResult.records, sql, columns: concatenatedColumns });
if (resultValue !== null && resultValue !== undefined) {
listResult.records = resultValue.recordset;
}
}


if (returnCount) {
if (limit > 0) {
listResult.recordCount = result.recordsets[1][0].TotalCount;
} else {
listResult.recordCount = result.rowsAffected[0];
listResult.recordCount = result.rowsAffected ? result.rowsAffected[0] : 0;
}
}
Comment thread
durlabhjain marked this conversation as resolved.

Expand Down
105 changes: 105 additions & 0 deletions lib/business/concatenated-columns.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
class ConcatenatedColumns {
static async addColumns({ records, sql, maxVariables = 1500, columns }) {
if (!records || !records.length || !columns || columns.length === 0) {
return;
}
const keys = {};
for (const colConfig of columns) {
const parentKeyColumn = colConfig.ParentColumn;
const joinColumn = colConfig.JoinColumn || parentKeyColumn;
const colName = colConfig.ColumnName;
const displayColumn = colConfig.DisplayColumn;
if (!keys[parentKeyColumn]) {
keys[parentKeyColumn] = records.reduce((ids, dr) => {
if (dr[parentKeyColumn] !== null) {
ids.push(dr[parentKeyColumn]);
}
return ids;
}, []);
Comment thread
durlabhjain marked this conversation as resolved.
}
const ids = keys[parentKeyColumn];
if (records && records.length && !records[0].hasOwnProperty(colName)) {
records.forEach(dr => dr[colName] = null);
}
if (ids.length > 0) {
let results = [];
if (colConfig.listMethod) {
results = await colConfig.listMethod(ids);
} else {
let startIndex = 0;
while (startIndex < ids.length) {
let idsForQuery;

if (startIndex === 0 && ids.length < maxVariables) {
idsForQuery = ids;
startIndex = ids.length;
} else {
let count = ids.length - startIndex;

if (count > maxVariables) {
count = maxVariables;
}
const idArray = ids.slice(startIndex, startIndex + count);
idsForQuery = idArray;
startIndex += count;
}
let query = colConfig.Query;
const where = { [joinColumn]: { value: idsForQuery, operator: 'in' } };
const request = sql.createRequest();
query = sql.addParameters({ request, query, parameters: where, forWhere: true });
const { data: tempDt } = await sql.execute({ query, request });
results = results.concat(tempDt);
}
}
const infoParser = colConfig.infoParser || ConcatenatedColumns.defaultInfoParser;
const resultsByParentId = results.reduce((lookup, row) => {
const parentId = row[joinColumn];
if (!lookup.has(parentId)) {
lookup.set(parentId, []);
}
lookup.get(parentId).push(row);
return lookup;
}, new Map());
records.forEach(dr => {
if (dr[parentKeyColumn] !== null) {
const parentId = dr[parentKeyColumn];
const childRows = resultsByParentId.get(parentId) || [];
const rowInfo = childRows.reduce((infot, childRow) => {
const infoToAppend = infoParser({ row: childRow, displayColumn });
if (infoToAppend !== null && infoToAppend?.length > 0) {
infot.push(infoToAppend);
}
return infot;
}, [])
dr[colName] = rowInfo.join(', ');
}
});
}
}
return records;
}

static defaultInfoParser({ row, displayColumn }) {
return row[displayColumn] !== null ? row[displayColumn] : null;
}

static applyStringFilter({ sql, value, concatenatedColumn, request, paramName = null, operator = 'LIKE' }) {
let childQuery = concatenatedColumn.Query;
const fieldName = concatenatedColumn.FilterColumn || concatenatedColumn.DisplayColumn;
const parentColumn = concatenatedColumn.ParentColumn;
const joinColumn = concatenatedColumn.JoinColumn || parentColumn;
// Use caller-supplied paramName (unique per filter index) to avoid collisions;
// fall back to fieldName only when called outside the standard filter pipeline.
const uniqueParam = paramName || fieldName;
childQuery = sql.addParameters({
query: childQuery,
request,
parameters: { [uniqueParam]: { fieldName, operator, value } },
forWhere: true,
});
Comment thread
durlabhjain marked this conversation as resolved.
const subQuery = `SELECT ${joinColumn} FROM (${childQuery}) ConcatenatedSubQuery`;
return `${parentColumn} IN (${subQuery})`;
}
}

export default ConcatenatedColumns;
21 changes: 16 additions & 5 deletions lib/mysql.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,14 @@ class Mysql extends Sql {
try {
const startTime = Date.now();
const result = await request[type](query, request.params);
let recordsets = result[0];
this.logSlowQuery({ startTime, query, type, request });
return { success: true, data: result[0], ...result };
if (Array.isArray(recordsets) && recordsets.length > 0 && Array.isArray(recordsets[0])) {
// nothing
} else {
recordsets = [recordsets];
}
return { success: true, data: recordsets[0], recordset: recordsets[0], recordsets, ...result };
} catch (err) {
const loggerToUse = request._logger || this.logger;
loggerToUse.error({ err, query, parameters: request.params, type });
Expand All @@ -94,10 +100,10 @@ class Mysql extends Sql {

createRequest(logger) {
const loggerToUse = logger || this.logger;
const queryLogger = createQueryLogger({
queryLogThreshold: this.queryLogThreshold,
timeoutLogLevel: this.timeoutLogLevel,
logger: loggerToUse
const queryLogger = createQueryLogger({
queryLogThreshold: this.queryLogThreshold,
timeoutLogLevel: this.timeoutLogLevel,
logger: loggerToUse
});
const pool = this.pool;
const request = {
Expand Down Expand Up @@ -140,6 +146,11 @@ class Mysql extends Sql {
if (value === undefined) value = null;
this.params[name] = value;
}

addPaging({ query, request, start, limit }) {
query += ' LIMIT :z_start, :z_limit';
return this.addParameters({ query, request, parameters: { z_start: start, z_limit: limit }, forWhere: false });
}
}

export default Mysql;
Loading