From 0bcd105de5c68225cebc2505ffc9b7da86861ced Mon Sep 17 00:00:00 2001 From: cB-Guru-Sharan-Kumar-Ram Date: Wed, 11 Feb 2026 18:36:04 +0530 Subject: [PATCH 1/8] feat: Add PostgreSQL support --- index.js | 50 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/index.js b/index.js index 0fb0894..c199c7a 100644 --- a/index.js +++ b/index.js @@ -9,28 +9,39 @@ let SqlString = require('sqlstring'); class PagiHelp { constructor(options) { if(options) { - let { columnNameConverter } = options; + let { columnNameConverter, dialect } = options; if (columnNameConverter) this.columnNameConverter = columnNameConverter; + this.dialect = dialect || "mysql"; + } else { + this.dialect = "mysql"; } } columnNameConverter = (x) => x; + dialect = "mysql"; + + escapeIdentifier = (identifier) => { + if (this.dialect === "postgresql") { + return `"${identifier}"`; + } + return `\`${identifier}\``; + }; columNames = (arr) => arr.map((a) => { if (a.prefix) { if (a.alias) return ( - a.prefix + "." + this.columnNameConverter(a.name) + " AS " + a.alias + this.escapeIdentifier(a.prefix) + "." + this.escapeIdentifier(this.columnNameConverter(a.name)) + " AS " + a.alias ); - return a.prefix + "." + this.columnNameConverter(a.name); + return this.escapeIdentifier(a.prefix) + "." + this.escapeIdentifier(this.columnNameConverter(a.name)); } if (a.statement) { if (a.alias) return a.statement + " AS " + a.alias; return a.statement; } - if (a.alias) return this.columnNameConverter(a.name) + " AS " + a.alias; - return this.columnNameConverter(a.name); + if (a.alias) return this.escapeIdentifier(this.columnNameConverter(a.name)) + " AS " + a.alias; + return this.escapeIdentifier(this.columnNameConverter(a.name)); }); tupleCreator = (tuple, replacements, asItIs = false) => { @@ -40,6 +51,9 @@ class PagiHelp { } let field = tuple[0]; if (operator === "JSON_CONTAINS" || operator === "JSON_OVERLAPS") { + if (this.dialect === "postgresql") { + throw `${operator} is not supported in PostgreSQL. Use JSONB operators instead.`; + } let query = `${operator}(${field}, ?)`; if (tuple[2] && typeof tuple[2] === "object") { replacements.push(JSON.stringify(tuple[2])); @@ -49,6 +63,9 @@ class PagiHelp { return query; } if (operator === "FIND_IN_SET") { + if (this.dialect === "postgresql") { + throw "FIND_IN_SET is not supported in PostgreSQL. Use ANY/array operators instead."; + } let query = `FIND_IN_SET(?, ${field})`; replacements.push(tuple[2]); return query; @@ -147,9 +164,9 @@ class PagiHelp { if (column.statement) { fieldName = column.statement; } else if (column.prefix) { - fieldName = `${column.prefix}.${column.name}`; + fieldName = `${this.escapeIdentifier(column.prefix)}.${this.escapeIdentifier(column.name)}`; } else { - fieldName = column.name; + fieldName = this.escapeIdentifier(column.name); } return [[fieldName, operator, value]]; @@ -171,24 +188,21 @@ class PagiHelp { let query = "SELECT " + columnList.join(",") + - " FROM `" + - tableName + - "`" + + " FROM " + + this.escapeIdentifier(tableName) + joinQuery; let countQuery = "SELECT " + columnList.join(",") + - " FROM `" + - tableName + - "`" + + " FROM " + + this.escapeIdentifier(tableName) + joinQuery; let totalCountQuery = "SELECT COUNT(*) AS countValue " + - " FROM `" + - tableName + - "`" + + " FROM " + + this.escapeIdentifier(tableName) + joinQuery; let replacements = []; @@ -315,8 +329,8 @@ class PagiHelp { orderByQuery = orderByQuery + "" + - this.columnNameConverter(SqlString.escapeId(sort.attributes[i])) + - "" + + this.columnNameConverter(this.escapeIdentifier(sort.attributes[i])) + + " " + sort.sorts[i] + ","; } From 2e2e9d8dfd7368a4ff7fa65fcc2c0d5ac450a784 Mon Sep 17 00:00:00 2001 From: cB-Guru-Sharan-Kumar-Ram Date: Wed, 11 Feb 2026 19:48:16 +0530 Subject: [PATCH 2/8] feat: Add sort column whitelist validation --- index.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index c199c7a..1198a6d 100644 --- a/index.js +++ b/index.js @@ -52,7 +52,7 @@ class PagiHelp { let field = tuple[0]; if (operator === "JSON_CONTAINS" || operator === "JSON_OVERLAPS") { if (this.dialect === "postgresql") { - throw `${operator} is not supported in PostgreSQL. Use JSONB operators instead.`; + throw `${operator} is not supported in PostgreSQL.`; } let query = `${operator}(${field}, ?)`; if (tuple[2] && typeof tuple[2] === "object") { @@ -64,7 +64,7 @@ class PagiHelp { } if (operator === "FIND_IN_SET") { if (this.dialect === "postgresql") { - throw "FIND_IN_SET is not supported in PostgreSQL. Use ANY/array operators instead."; + throw "FIND_IN_SET is not supported in PostgreSQL."; } let query = `FIND_IN_SET(?, ${field})`; replacements.push(tuple[2]); @@ -326,10 +326,26 @@ class PagiHelp { sort.sorts[i] = sort.sorts[i].toUpperCase() } for (let i = 0; i < sort.attributes.length; i++) { + const sortAttr = sort.attributes[i]; + let isValidColumn = false; + for (let option of options) { + const cols = option.columnList || [{ name: "*" }]; + isValidColumn = cols.some(col => + col.alias === sortAttr || + col.name === sortAttr || + (col.prefix && `${col.prefix}.${col.name}` === sortAttr) + ); + if (isValidColumn) break; + } + + if (sortAttr !== "id" && !isValidColumn) { + throw `Invalid sort attribute: ${sortAttr}`; + } + orderByQuery = orderByQuery + "" + - this.columnNameConverter(this.escapeIdentifier(sort.attributes[i])) + + this.columnNameConverter(this.escapeIdentifier(sortAttr)) + " " + sort.sorts[i] + ","; From 92f1144dae377e2523c56e8cc1747f1970647d8f Mon Sep 17 00:00:00 2001 From: cB-Guru-Sharan-Kumar-Ram Date: Wed, 11 Feb 2026 20:03:17 +0530 Subject: [PATCH 3/8] feat: Add sort column whitelist validation --- index.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index 1198a6d..ce7e646 100644 --- a/index.js +++ b/index.js @@ -328,13 +328,21 @@ class PagiHelp { for (let i = 0; i < sort.attributes.length; i++) { const sortAttr = sort.attributes[i]; let isValidColumn = false; + + const toCamelCase = (str) => { + return str.replace(/_([a-zA-Z0-9])/g, (_, char) => { + return /[a-zA-Z]/.test(char) ? char.toUpperCase() : char; + }); + }; + for (let option of options) { const cols = option.columnList || [{ name: "*" }]; - isValidColumn = cols.some(col => - col.alias === sortAttr || - col.name === sortAttr || - (col.prefix && `${col.prefix}.${col.name}` === sortAttr) - ); + isValidColumn = cols.some(col => { + if (col.alias === sortAttr || col.name === sortAttr) return true; + if (col.alias && toCamelCase(col.alias) === toCamelCase(sortAttr)) return true; + if (col.prefix && `${col.prefix}.${col.name}` === sortAttr) return true; + return false; + }); if (isValidColumn) break; } From ecc9c5d6332163648e0fb423ffd72298a46f5780 Mon Sep 17 00:00:00 2001 From: cB-Guru-Sharan-Kumar-Ram Date: Wed, 11 Feb 2026 21:48:47 +0530 Subject: [PATCH 4/8] fix(security): properly escape identifier delimiters and fix column conversion order --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index ce7e646..9ed20d9 100644 --- a/index.js +++ b/index.js @@ -22,9 +22,9 @@ class PagiHelp { escapeIdentifier = (identifier) => { if (this.dialect === "postgresql") { - return `"${identifier}"`; + return `"${identifier.replace(/"/g, '""')}"`; } - return `\`${identifier}\``; + return `\`${identifier.replace(/`/g, '``')}\``; }; columNames = (arr) => @@ -353,7 +353,7 @@ class PagiHelp { orderByQuery = orderByQuery + "" + - this.columnNameConverter(this.escapeIdentifier(sortAttr)) + + this.escapeIdentifier(this.columnNameConverter(sortAttr)) + " " + sort.sorts[i] + ","; From 6a7e9b6559e0eede25be308b606fbc55401c63e4 Mon Sep 17 00:00:00 2001 From: cB-Guru-Sharan-Kumar-Ram Date: Thu, 12 Feb 2026 01:08:00 +0530 Subject: [PATCH 5/8] fix: escape countValue and totalCounts aliases for PostgreSQL compatibility --- index.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 9ed20d9..353e05f 100644 --- a/index.js +++ b/index.js @@ -200,7 +200,7 @@ class PagiHelp { joinQuery; let totalCountQuery = - "SELECT COUNT(*) AS countValue " + + "SELECT COUNT(*) AS " + this.escapeIdentifier("countValue") + " " + " FROM " + this.escapeIdentifier(tableName) + joinQuery; @@ -311,9 +311,9 @@ class PagiHelp { countQuery = countQuery.replace(/(UNION ALL\s*)$/i, ""); if (totalCountQueries.length > 1) { - totalCountQuery = `SELECT SUM(countValue) AS countValue FROM ( ${totalCountQueries.join( + totalCountQuery = `SELECT SUM(${this.escapeIdentifier("countValue")}) AS ${this.escapeIdentifier("countValue")} FROM ( ${totalCountQueries.join( " UNION ALL " - )} ) AS totalCounts`; + )} ) AS ${this.escapeIdentifier("totalCounts")}`; } else { totalCountQuery = totalCountQueries[0]; } @@ -350,10 +350,18 @@ class PagiHelp { throw `Invalid sort attribute: ${sortAttr}`; } + let orderByPart; + if (sortAttr.includes(".")) { + const [prefix, colName] = sortAttr.split("."); + orderByPart = `${this.escapeIdentifier(prefix)}.${this.escapeIdentifier(this.columnNameConverter(colName))}`; + } else { + orderByPart = this.escapeIdentifier(this.columnNameConverter(sortAttr)); + } + orderByQuery = orderByQuery + "" + - this.escapeIdentifier(this.columnNameConverter(sortAttr)) + + orderByPart + " " + sort.sorts[i] + ","; From b1a89406b33adb53564d655869eddba07342e08e Mon Sep 17 00:00:00 2001 From: cB-Guru-Sharan-Kumar-Ram Date: Thu, 12 Feb 2026 01:29:50 +0530 Subject: [PATCH 6/8] fix: escape column aliases in columNames for PostgreSQL case preservation --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 353e05f..d538efc 100644 --- a/index.js +++ b/index.js @@ -32,15 +32,15 @@ class PagiHelp { if (a.prefix) { if (a.alias) return ( - this.escapeIdentifier(a.prefix) + "." + this.escapeIdentifier(this.columnNameConverter(a.name)) + " AS " + a.alias + this.escapeIdentifier(a.prefix) + "." + this.escapeIdentifier(this.columnNameConverter(a.name)) + " AS " + this.escapeIdentifier(a.alias) ); return this.escapeIdentifier(a.prefix) + "." + this.escapeIdentifier(this.columnNameConverter(a.name)); } if (a.statement) { - if (a.alias) return a.statement + " AS " + a.alias; + if (a.alias) return a.statement + " AS " + this.escapeIdentifier(a.alias); return a.statement; } - if (a.alias) return this.escapeIdentifier(this.columnNameConverter(a.name)) + " AS " + a.alias; + if (a.alias) return this.escapeIdentifier(this.columnNameConverter(a.name)) + " AS " + this.escapeIdentifier(a.alias); return this.escapeIdentifier(this.columnNameConverter(a.name)); }); From ef49d95a513cd633950c69d0bbbbb4534d76c0f2 Mon Sep 17 00:00:00 2001 From: cB-Guru-Sharan-Kumar-Ram Date: Thu, 12 Feb 2026 02:42:08 +0530 Subject: [PATCH 7/8] refactor: simplify sort logic --- index.js | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/index.js b/index.js index d538efc..39b97da 100644 --- a/index.js +++ b/index.js @@ -327,28 +327,6 @@ class PagiHelp { } for (let i = 0; i < sort.attributes.length; i++) { const sortAttr = sort.attributes[i]; - let isValidColumn = false; - - const toCamelCase = (str) => { - return str.replace(/_([a-zA-Z0-9])/g, (_, char) => { - return /[a-zA-Z]/.test(char) ? char.toUpperCase() : char; - }); - }; - - for (let option of options) { - const cols = option.columnList || [{ name: "*" }]; - isValidColumn = cols.some(col => { - if (col.alias === sortAttr || col.name === sortAttr) return true; - if (col.alias && toCamelCase(col.alias) === toCamelCase(sortAttr)) return true; - if (col.prefix && `${col.prefix}.${col.name}` === sortAttr) return true; - return false; - }); - if (isValidColumn) break; - } - - if (sortAttr !== "id" && !isValidColumn) { - throw `Invalid sort attribute: ${sortAttr}`; - } let orderByPart; if (sortAttr.includes(".")) { From 8f865f063f9965364222eabadf8443619fb8fe5c Mon Sep 17 00:00:00 2001 From: cB-Guru-Sharan-Kumar-Ram Date: Thu, 12 Feb 2026 02:58:18 +0530 Subject: [PATCH 8/8] fix: add PostgreSQL pagination support with LIMIT OFFSET syntax --- index.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 39b97da..29a7751 100644 --- a/index.js +++ b/index.js @@ -352,11 +352,21 @@ class PagiHelp { let offset = (paginationObject.pageNo - 1) * paginationObject.itemsPerPage; - query = query + " LIMIT ?,?"; - replacements.push(offset, paginationObject.itemsPerPage); + if (this.dialect === "postgresql") { + query = query + " LIMIT ? OFFSET ?"; + replacements.push(paginationObject.itemsPerPage, offset); + } else { + query = query + " LIMIT ?,?"; + replacements.push(offset, paginationObject.itemsPerPage); + } } else if(paginationObject.offset && paginationObject.limit ) { - query = query + " LIMIT ?,?"; - replacements.push(paginationObject.offset,paginationObject.limit) + if (this.dialect === "postgresql") { + query = query + " LIMIT ? OFFSET ?"; + replacements.push(paginationObject.limit, paginationObject.offset); + } else { + query = query + " LIMIT ?,?"; + replacements.push(paginationObject.offset, paginationObject.limit); + } } return {