@demmings/gssql
Version:
Google Sheets QUERY function replacement using real SQL select syntax.
244 lines (201 loc) • 7.64 kB
JavaScript
/* *** DEBUG START ***
// Remove comments for testing in NODE
import { GasSql } from "./Sql.js";
export { Select2Object };
// *** DEBUG END ***/
/**
* @classdesc - Executes a SELECT statement on sheet data. Returned data will be any array of objects,
* where each item is one row of data. The property values in the object are the column names.
* The column names will be in lower case. If more than one table is referenced, the column name will be:
* "table.column", otherwise it will just be the column name. Spaces in the column name use the underscore, so
* something like "Transaction Date" would be referenced as "transaction_date".
*/
class Select2Object { // skipcq: JS-0128
constructor() {
this.tables = [];
this.bindVariables = [];
}
/**
*
* @param {String} tableName - table name referenced in SELECT statement.
* @param {*} data - double array or string. If string it must reference A1 notation, named range or sheet name.
* @returns {Select2Object}
*/
addTableData(tableName, data) {
const table = { tableName, data };
this.tables.push(table);
return this;
}
/**
* If bind variables are used in SELECT statement, this are added here.
* Ordering is important. The first one added will be '?1' in the select, second is '?2' in select...
* @param {any} bindVar
* @returns {Select2Object}
*/
addBindVariable(bindVar) {
this.bindVariables.push(bindVar);
return this;
}
/**
* Query any sheet range using standard SQL SELECT syntax and return array of table info with column names as properties.
* @example
* gsSQL("select * from expenses where type = ?1")
*
* @param {String} statement - SQL string
* @returns {Object[]} - array of object data.
*/
execute(statement) { // skipcq: JS-0128
const parms = [];
// Add the table name and range.
for (const tab of this.tables) {
parms.push(tab.tableName);
parms.push(tab.data);
}
// Add column output indicator.
parms.push(true); // We want column names returned.
// Add bind data.
this.bindVariables.forEach(bind => parms.push(bind));
const tableDataArray = GasSql.execute(statement, parms);
if (tableDataArray === null || tableDataArray.length === 0) {
return null;
}
// First item in return array is an array of column names.
const columnNames = Select2Object.cleanupColumnNames(tableDataArray[0]);
return Select2Object.createTableObjectArray(columnNames, tableDataArray);
}
/**
* Return column names in lower case and remove table name when only one table.
* @param {String[]} cols
* @returns {String[]}
*/
static cleanupColumnNames(cols) {
const newColumns = cols.map(v => v.toLowerCase());
const noTableColumns = [];
const uniqueTables = new Set();
for (const col of newColumns) {
const splitColumn = col.split(".");
if (splitColumn.length > 1) {
uniqueTables.add(splitColumn[0]);
noTableColumns.push(splitColumn[1]);
}
else {
noTableColumns.push(splitColumn[0]);
}
}
// Leave the table name in the column since we have two or more tables.
if (uniqueTables.size > 1) {
return newColumns;
}
return noTableColumns;
}
/**
* First row MUST be column names.
* @param {any[][]} tableDataArray
* @returns {Object[]}
*/
static convertTableArrayToObjectArray(tableDataArray) {
// First item in return array is an array of column names.
const propertyNames = Select2Object.convertColumnTitleToPropertyName(tableDataArray[0]);
return Select2Object.createTableObjectArray(propertyNames, tableDataArray);
}
/**
*
* @param {Object[]} objectArray
* @param {String[]} columnTitles
* @param {Boolean} outputTitleRow
* @returns {any[][]}
*/
static convertObjectArrayToTableArray(objectArray, columnTitles, outputTitleRow = true) {
const propertyNames = Select2Object.convertColumnTitleToPropertyName(columnTitles);
const tableArray = [];
if (outputTitleRow)
tableArray.push(columnTitles);
for (const objectRow of objectArray) {
const row = [];
for (const prop of propertyNames) {
row.push(objectRow[prop]);
}
tableArray.push(row);
}
return tableArray;
}
/**
*
* @param {Object} object
* @param {String[]} columnTitles
* @returns {String[]}
*/
static convertObjectToArray(object, columnTitles) {
const propertyNames = Select2Object.convertColumnTitleToPropertyName(columnTitles);
const row = [];
for (const prop of propertyNames) {
row.push(object[prop]);
}
return row;
}
/**
* Convert a sheet column name into format used for property name (spaces to underscore && lowercase)
* @param {String[]} columnTitles
* @returns {String[]}
*/
static convertColumnTitleToPropertyName(columnTitles) {
const columnNames = [...columnTitles];
const srcColumns = columnNames.map(col => col.trim()).map(col => col.toLowerCase()).map(col => col.replaceAll(' ', '_'));
return srcColumns;
}
/**
* Get column number - starting at 1 in object.
* @param {Object} object
* @param {String} columnTitle
* @returns {Number}
*/
static getColumnNumber(object, columnTitle) {
const prop = Select2Object.convertColumnTitleToPropertyName([columnTitle])[0];
let col = 1;
for (const propName in object) { // skipcq: JS-0051
if (propName === prop) {
return col;
}
col++;
}
return -1;
}
/**
*
* @param {String[]} columnNames
* @param {any[]} tableDataArray
* @returns {Object[]}
*/
static createTableObjectArray(columnNames, tableDataArray) {
// Create empty table record object.
const emptyTableRecord = Select2Object.createEmptyRecordObject(columnNames);
// Create table array with record data stored in an object.
const tableData = [];
for (let i = 1; i < tableDataArray.length; i++) {
const newRecord = {};
Object.assign(newRecord, emptyTableRecord);
columnNames.forEach((col, j) => {newRecord[col] = tableDataArray[i][j]});
tableData.push(newRecord);
}
return tableData;
}
/**
* Creates an empty object where each column name is a property in the object.
* @param {String[]} columnNames
* @returns {Object}
*/
static createEmptyRecordObject(columnNames) {
// Create empty table record object.
const dataObject = {};
columnNames.forEach(col => {dataObject[col] = ''});
dataObject.get = function (columnTitle) {
const prop = Select2Object.convertColumnTitleToPropertyName([columnTitle])[0];
return this[prop];
};
dataObject.set = function (columnTitle, value) {
const prop = Select2Object.convertColumnTitleToPropertyName([columnTitle])[0];
this[prop] = value;
}
return dataObject;
}
}