pg-sql2
Version:
Generate safe Postgres-compliant SQL with tagged template literals
287 lines • 10.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.FALSE = exports.TRUE = exports.NULL = exports.null = exports.fragment = exports.blank = exports.escapeSqlIdentifier = exports.join = exports.literal = exports.value = exports.identifier = exports.raw = exports.query = exports.compile = void 0;
const debugFactory = require("debug");
const lru_1 = require("@graphile/lru");
const debug = debugFactory("pg-sql2");
function debugError(err) {
debug(err);
return err;
}
const $$trusted = Symbol("trusted");
const CACHE_RAW_NODES = new lru_1.default({ maxLength: 2000 });
function makeRawNode(text) {
const n = CACHE_RAW_NODES.get(text);
if (n) {
return n;
}
if (typeof text !== "string") {
throw new Error("Invalid argument to makeRawNode - expected string");
}
const newNode = { type: "RAW", text, [$$trusted]: true };
CACHE_RAW_NODES.set(text, newNode);
return newNode;
}
function isStringOrSymbol(val) {
return typeof val === "string" || typeof val === "symbol";
}
function makeIdentifierNode(names) {
if (!Array.isArray(names) ||
names.length === 0 ||
!names.every(isStringOrSymbol)) {
throw new Error("Invalid argument to makeIdentifierNode - expected array of strings/symbols");
}
return { type: "IDENTIFIER", names, [$$trusted]: true };
}
function makeValueNode(rawValue) {
return { type: "VALUE", value: rawValue, [$$trusted]: true };
}
function ensureNonEmptyArray(array, allowZeroLength = false) {
if (!Array.isArray(array)) {
throw debugError(new Error("Expected array"));
}
if (!allowZeroLength && array.length < 1) {
throw debugError(new Error("Expected non-empty array"));
}
for (let idx = 0, l = array.length; idx < l; idx++) {
if (array[idx] == null) {
throw debugError(new Error(`Array index ${idx} is ${String(array[idx])}`));
}
}
return array;
}
function compile(sql) {
const items = Array.isArray(sql) ? sql : [sql];
const itemCount = items.length;
// Join this to generate the SQL query
const sqlFragments = [];
// Values hold the JavaScript values that are represented in the query
// string by placeholders. They are eager because they were provided before
// compile time.
const values = [];
// If we use the exact same `sql.value` node more than once, we should use
// the same placeholder for both (for performance and efficiency)
const valueNodeToPlaceholder = new Map();
// When we come accross a symbol in our identifier, we create a unique
// alias for it that shouldn’t be in the users schema. This helps maintain
// sanity when constructing large Sql queries with many aliases.
let nextSymbolId = 0;
const symbolToIdentifier = new Map();
for (let itemIndex = 0; itemIndex < itemCount; itemIndex++) {
const item = enforceValidNode(items[itemIndex]);
switch (item.type) {
case "RAW":
sqlFragments.push(item.text);
break;
case "IDENTIFIER": {
const nameCount = item.names.length;
const mappedNames = [];
for (let nameIndex = 0; nameIndex < nameCount; nameIndex++) {
const name = item.names[nameIndex];
if (typeof name === "string") {
mappedNames.push(escapeSqlIdentifier(name));
}
else if (typeof name === "symbol") {
// Get the correct identifier string for this symbol.
let identifierForSymbol = symbolToIdentifier.get(name);
// If there is no identifier, create one and set it.
if (!identifierForSymbol) {
identifierForSymbol = `__local_${nextSymbolId++}__`;
symbolToIdentifier.set(name, identifierForSymbol);
}
// Return the identifier. Since we create it, we won’t have to
// escape it because we know all of the characters are safe.
mappedNames.push(identifierForSymbol);
}
else {
throw debugError(new Error(`Expected string or symbol, received '${String(name)}'`));
}
}
sqlFragments.push(nameCount === 1 ? mappedNames[0] : mappedNames.join("."));
break;
}
case "VALUE": {
let placeholder = valueNodeToPlaceholder.get(item);
// If there's no placeholder for this value node, create one
if (!placeholder) {
values.push(item.value);
placeholder = `$${values.length}`;
valueNodeToPlaceholder.set(item, placeholder);
}
sqlFragments.push(placeholder);
break;
}
default:
// This cannot happen
}
}
const text = sqlFragments.join("");
if (values.length > 65535) {
throw new Error("PostgreSQL allows the use of up to 65535 placeholders; but your statement wants to use ${values.length} placeholders. To solve this issue you could split the statement into multiple statements, or pass more values into a single placeholder by using JSON, arrays, or similar techniques.");
}
return {
text,
values,
};
}
exports.compile = compile;
function enforceValidNode(node) {
if (node !== null && node[$$trusted] === true) {
return node;
}
throw new Error(`Expected SQL item, instead received '${String(node)}'.`);
}
/**
* A template string tag that creates a `Sql` query out of some strings and
* some values. Use this to construct all PostgreSQL queries to avoid SQL
* injection.
*
* Note that using this function, the user *must* specify if they are injecting
* raw text. This makes a SQL injection vulnerability harder to create.
*/
// LRU not necessary
const CACHE_SIMPLE_FRAGMENTS = new Map();
function query(strings, ...values) {
if (!Array.isArray(strings)) {
throw new Error("sql.query should be used as a template literal, not a function call!");
}
const first = strings[0];
// Reduce memory churn with a cache
if (strings.length === 1 && typeof first === "string" && first.length < 20) {
const cached = CACHE_SIMPLE_FRAGMENTS.get(first);
if (cached) {
return cached;
}
const node = [makeRawNode(first)];
CACHE_SIMPLE_FRAGMENTS.set(first, node);
return node;
}
const items = [];
for (let i = 0, l = strings.length; i < l; i++) {
const text = strings[i];
if (typeof text !== "string") {
throw new Error("sql.query should be used as a template literal, not a function call.");
}
if (text.length > 0) {
items.push(makeRawNode(text));
}
if (values[i]) {
const val = values[i];
if (Array.isArray(val)) {
const nodes = val.map(enforceValidNode);
for (const item of nodes) {
items.push(item);
}
}
else {
const node = enforceValidNode(val);
items.push(node);
}
}
}
return items;
}
exports.query = query;
exports.fragment = query;
/**
* Creates a Sql item for some raw Sql text. Just plain ol‘ raw Sql. This
* method is dangerous though because it involves no escaping, so proceed
* with caution!
*/
function raw(text) {
return makeRawNode(String(text));
}
exports.raw = raw;
/**
* Creates a Sql item for a Sql identifier. A Sql identifier is anything like
* a table, schema, or column name. An identifier may also have a namespace,
* thus why many names are accepted.
*/
function identifier(...names) {
return makeIdentifierNode(ensureNonEmptyArray(names));
}
exports.identifier = identifier;
/**
* Creates a Sql item for a value that will be included in our final query.
* This value will be added in a way which avoids Sql injection.
*/
function value(val) {
return makeValueNode(val);
}
exports.value = value;
const trueNode = raw(`TRUE`);
exports.TRUE = trueNode;
const falseNode = raw(`FALSE`);
exports.FALSE = falseNode;
const nullNode = raw(`NULL`);
exports.null = nullNode;
exports.NULL = nullNode;
/**
* If the value is simple will inline it into the query, otherwise will defer
* to value.
*/
function literal(val) {
if (typeof val === "string" && val.match(/^[-a-zA-Z0-9_@! ]*$/)) {
return raw(`'${val}'`);
}
else if (typeof val === "number" && Number.isFinite(val)) {
if (Number.isInteger(val)) {
return raw(String(val));
}
else {
return raw(`'${0 + val}'::float`);
}
}
else if (typeof val === "boolean") {
return val ? trueNode : falseNode;
}
else if (val == null) {
return nullNode;
}
else {
return makeValueNode(val);
}
}
exports.literal = literal;
/**
* Join some Sql items together seperated by a string. Useful when dealing
* with lists of Sql items that doesn’t make sense as a Sql query.
*/
function join(items, rawSeparator = "") {
ensureNonEmptyArray(items, true);
if (typeof rawSeparator !== "string") {
throw new Error("Invalid separator - must be a string");
}
const separator = rawSeparator;
const currentItems = [];
const sepNode = makeRawNode(separator);
for (let i = 0, l = items.length; i < l; i++) {
const rawItem = items[i];
const itemsToAppend = Array.isArray(rawItem)
? rawItem.map(enforceValidNode)
: [enforceValidNode(rawItem)];
if (i === 0 || !separator) {
for (const itemToAppend of itemsToAppend) {
currentItems.push(itemToAppend);
}
}
else {
currentItems.push(sepNode);
for (const item of itemsToAppend) {
currentItems.push(item);
}
}
}
return currentItems;
}
exports.join = join;
// Copied from https://github.com/brianc/node-postgres/blob/860cccd53105f7bc32fed8b1de69805f0ecd12eb/lib/client.js#L285-L302
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
// Trivial performance optimisations by Benjie.
// Replaced with regexp because it's 11x faster by Benjie.
function escapeSqlIdentifier(str) {
return `"${str.replace(/"/g, '""')}"`;
}
exports.escapeSqlIdentifier = escapeSqlIdentifier;
exports.blank = query ``;
//# sourceMappingURL=index.js.map