@scaleleap/pg-format
Version:
A fully typed TypeScript and Node.js implementation of PostgreSQL format() to safely create dynamic SQL queries. SQL identifiers and literals are escaped to help prevent SQL injection.
253 lines • 7.65 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.format = exports.withArray = exports.config = exports.string = exports.literal = exports.ident = void 0;
const reserved_1 = require("./reserved");
const FMT_PATTERN_CONFIG = {
ident: 'I',
literal: 'L',
string: 's',
};
// convert to Postgres default ISO 8601 format
function formatDate(date) {
return date.replace('T', ' ').replace('Z', '+00');
}
function isReserved(value) {
if (reserved_1.POSTGRESQL_RESERVED_WORDS.has(value.toUpperCase())) {
return true;
}
return false;
}
function arrayToList(useSpace, array, formatter) {
let sql = '';
sql += useSpace ? ' (' : '(';
for (const [index, element] of array.entries()) {
sql += (index === 0 ? '' : ', ') + formatter(element);
}
sql += ')';
return sql;
}
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
// eslint-disable-next-line radar/cognitive-complexity
function ident(value) {
if (value === undefined || value === null) {
throw new Error('SQL identifier cannot be null or undefined');
}
else if (value === false) {
return '"f"';
}
else if (value === true) {
return '"t"';
}
else if (value instanceof Date) {
return `"${formatDate(value.toISOString())}"`;
}
else if (value instanceof Buffer) {
throw new TypeError('SQL identifier cannot be a buffer');
}
else if (Array.isArray(value)) {
const temporary = [];
for (const element of value) {
if (Array.isArray(element) === true) {
throw new TypeError('Nested array to grouped list conversion is not supported for SQL identifier');
}
else {
temporary.push(ident(element));
}
}
return temporary.toString();
}
else if (value === Object(value)) {
throw new Error('SQL identifier cannot be an object');
}
const tident = String(value).slice(0); // create copy
// do not quote a valid, unquoted identifier
if (/^[_a-z][\d$_a-z]*$/.test(tident) === true && isReserved(tident) === false) {
return tident;
}
let quoted = '"';
for (const c of tident) {
quoted += c === '"' ? c + c : c;
}
quoted += '"';
return quoted;
}
exports.ident = ident;
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
// eslint-disable-next-line radar/cognitive-complexity
function literal(value) {
let tliteral = '';
let explicitCast;
if (value === undefined || value === null) {
return 'NULL';
}
if (typeof value === 'bigint') {
return BigInt(value).toString();
}
if (value === Number.POSITIVE_INFINITY) {
return "'Infinity'";
}
if (value === Number.NEGATIVE_INFINITY) {
return "'-Infinity'";
}
if (Number.isNaN(value)) {
return "'NaN'";
}
if (typeof value === 'number') {
// Test must be AFTER other special case number tests
return Number(value).toString();
}
if (value === false) {
return "'f'";
}
if (value === true) {
return "'t'";
}
if (value instanceof Date) {
return `'${formatDate(value.toISOString())}'`;
}
if (value instanceof Buffer) {
return `E'\\\\x${value.toString('hex')}'`;
}
if (Array.isArray(value)) {
const temporary = [];
for (const [index, element] of value.entries()) {
if (Array.isArray(element) === true) {
temporary.push(arrayToList(index !== 0, element, literal));
}
else {
temporary.push(literal(element));
}
}
return temporary.toString();
}
if (value === Object(value)) {
explicitCast = 'jsonb';
tliteral = JSON.stringify(value);
}
else {
tliteral = String(value).slice(0); // create copy
}
let hasBackslash = false;
let quoted = "'";
for (const c of tliteral) {
if (c === "'") {
quoted += c + c;
}
else if (c === '\\') {
quoted += c + c;
hasBackslash = true;
}
else {
quoted += c;
}
}
quoted += "'";
if (hasBackslash === true) {
quoted = `E${quoted}`;
}
if (explicitCast) {
quoted += `::${explicitCast}`;
}
return quoted;
}
exports.literal = literal;
// eslint-disable-next-line radar/cognitive-complexity
function string(value) {
if (value === undefined || value === null) {
return '';
}
if (value === false) {
return 'f';
}
if (value === true) {
return 't';
}
if (value instanceof Date) {
return formatDate(value.toISOString());
}
if (value instanceof Buffer) {
return `\\x${value.toString('hex')}`;
}
if (Array.isArray(value)) {
const temporary = [];
for (const [index, element] of value.entries()) {
if (element !== null && element !== undefined) {
if (Array.isArray(element) === true) {
temporary.push(arrayToList(index !== 0, element, string));
}
else {
temporary.push(string(element));
}
}
}
return temporary.toString();
}
if (value === Object(value)) {
return JSON.stringify(value);
}
return String(value).toString().slice(0); // return copy
}
exports.string = string;
function config(cfg) {
// default
FMT_PATTERN_CONFIG.ident = 'I';
FMT_PATTERN_CONFIG.literal = 'L';
FMT_PATTERN_CONFIG.string = 's';
if (cfg && cfg.pattern) {
if (cfg.pattern.ident) {
FMT_PATTERN_CONFIG.ident = cfg.pattern.ident;
}
if (cfg.pattern.literal) {
FMT_PATTERN_CONFIG.literal = cfg.pattern.literal;
}
if (cfg.pattern.string) {
FMT_PATTERN_CONFIG.string = cfg.pattern.string;
}
}
}
exports.config = config;
function withArray(fmt, parameters) {
let index = 0;
let reText = '%(%|(\\d+\\$)?[';
reText += FMT_PATTERN_CONFIG.ident;
reText += FMT_PATTERN_CONFIG.literal;
reText += FMT_PATTERN_CONFIG.string;
reText += '])';
const re = new RegExp(reText, 'g');
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return fmt.replace(re, (_, type) => {
if (type === '%') {
return '%';
}
let position = index;
const tokens = type.split('$');
if (tokens.length > 1) {
position = Number.parseInt(tokens[0], 10) - 1;
// eslint-disable-next-line no-param-reassign, prefer-destructuring
type = tokens[1];
}
if (position < 0) {
throw new Error('specified argument 0 but arguments start at 1');
}
else if (position > parameters.length - 1) {
throw new Error('too few arguments');
}
index = position + 1;
if (type === FMT_PATTERN_CONFIG.ident) {
return ident(parameters[position]);
}
if (type === FMT_PATTERN_CONFIG.literal) {
return literal(parameters[position]);
}
if (type === FMT_PATTERN_CONFIG.string) {
return string(parameters[position]);
}
});
}
exports.withArray = withArray;
function format(fmt, ...arguments_) {
return withArray(fmt, arguments_);
}
exports.format = format;
//# sourceMappingURL=pg-format.js.map