Comparing version 1.0.0-beta.7 to 1.0.0-beta.8
307
lib/index.js
@@ -7,3 +7,9 @@ "use strict"; | ||
const debug = require("debug")("pg-sql2"); | ||
var isSymbol = function isSymbol(sym) { | ||
return typeof sym === "symbol"; | ||
}; | ||
var isNil = function isNil(o) { | ||
return o === null || o === undefined; | ||
}; | ||
var debug = require("debug")("pg-sql2"); | ||
@@ -15,3 +21,3 @@ function debugError(err) { | ||
const $$trusted = Symbol("trusted"); | ||
var $$trusted = Symbol("trusted"); | ||
/*:: | ||
@@ -41,41 +47,38 @@ type SQLRawNode = { | ||
function makeRawNode(text /*: string */) /*: SQLRawNode */{ | ||
if (typeof text !== "string") { | ||
throw new Error("Invalid argument to makeRawNode - expected string"); | ||
} | ||
// $FlowFixMe: flow doesn't like symbols | ||
return { type: "RAW", text, [$$trusted]: true }; | ||
function makeTrustedNode /*:: <Node>*/(node /*: Node */) /*: Node */{ | ||
Object.defineProperty(node, $$trusted, { | ||
enumerable: false, | ||
configurable: false, | ||
value: true | ||
}); | ||
return node; | ||
} | ||
function isStringOrSymbol(val) { | ||
return typeof val === "string" || typeof val === "symbol"; | ||
function makeRawNode(text /*: string */) /*: SQLRawNode */{ | ||
return makeTrustedNode({ type: "RAW", text }); | ||
} | ||
function makeIdentifierNode(names /*: Array<string | Symbol> */ | ||
function makeIdentifierNode(names /*: Array<mixed> */ | ||
) /*: SQLIdentifierNode */{ | ||
if (!Array.isArray(names) || !names.every(isStringOrSymbol)) { | ||
throw new Error("Invalid argument to makeIdentifierNode - expected array of strings/symbols"); | ||
} | ||
// $FlowFixMe | ||
return { type: "IDENTIFIER", names, [$$trusted]: true }; | ||
return makeTrustedNode({ type: "IDENTIFIER", names }); | ||
} | ||
function makeValueNode(value /*: mixed */) /*: SQLValueNode */{ | ||
// $FlowFixMe | ||
return { type: "VALUE", value, [$$trusted]: true }; | ||
return makeTrustedNode({ type: "VALUE", value }); | ||
} | ||
function ensureNonEmptyArray /*:: <T>*/(array /*: Array<T>*/ | ||
, allowZeroLength = false) /*: Array<T> */{ | ||
function ensureNonEmptyArray(array) { | ||
var allowZeroLength = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; | ||
if (!Array.isArray(array)) { | ||
throw debugError(new Error("Expected array")); | ||
} | ||
if (!allowZeroLength && array.length < 1) { | ||
if (array.length < 1 && !allowZeroLength) { | ||
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])}`)); | ||
array.forEach(function (entry, idx) { | ||
if (entry == null) { | ||
throw debugError(new Error(`Array index ${idx} is ${String(entry)}`)); | ||
} | ||
} | ||
}); | ||
return array; | ||
@@ -86,3 +89,3 @@ } | ||
// Join this to generate the SQL query | ||
const sqlFragments = []; | ||
var sqlFragments = []; | ||
@@ -92,3 +95,3 @@ // Values hold the JavaScript values that are represented in the query | ||
// compile time. | ||
const values = []; | ||
var values = []; | ||
@@ -98,30 +101,35 @@ // When we come accross a symbol in our identifier, we create a unique | ||
// sanity when constructing large Sql queries with many aliases. | ||
let nextSymbolId = 0; | ||
const symbolToIdentifier = new Map(); | ||
var nextSymbolId = 0; | ||
var symbolToIdentifier = new Map(); | ||
const items = Array.isArray(sql) ? sql : [sql]; | ||
var items = Array.isArray(sql) ? sql : [sql]; | ||
for (let i = 0, l = items.length; i < l; i++) { | ||
const rawItem = items[i]; | ||
const item /*: SQLNode */ = enforceValidNode(rawItem); | ||
switch (item.type) { | ||
case "RAW": | ||
if (typeof item.text !== "string") { | ||
throw new Error("RAW node expected string"); | ||
} | ||
sqlFragments.push(item.text); | ||
break; | ||
case "IDENTIFIER": | ||
if (item.names.length === 0) throw new Error("Identifier must have a name"); | ||
var _iteratorNormalCompletion = true; | ||
var _didIteratorError = false; | ||
var _iteratorError = undefined; | ||
sqlFragments.push(item.names.map(rawName => { | ||
if (typeof rawName === "string") { | ||
const name /*: string */ = rawName; | ||
return escapeSqlIdentifier(name); | ||
// $FlowFixMe: flow doesn't like symbols | ||
} else if (typeof rawName === "symbol") { | ||
const name /*: Symbol */ = /*:: (*/rawName /*: any) */; | ||
try { | ||
for (var _iterator = items[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { | ||
var rawItem = _step.value; | ||
var item /*: SQLNode */ = enforceValidNode(rawItem); | ||
switch (item.type) { | ||
case "RAW": | ||
sqlFragments.push(item.text); | ||
break; | ||
case "IDENTIFIER": | ||
if (item.names.length === 0) throw new Error("Identifier must have a name"); | ||
sqlFragments.push(item.names.map(function (rawName) { | ||
if (typeof rawName === "string") { | ||
var _name /*: string */ = rawName; | ||
return escapeSqlIdentifier(_name); | ||
} | ||
if (!isSymbol(rawName)) { | ||
throw debugError(new Error(`Expected string or symbol, received '${String(rawName)}'`)); | ||
} | ||
var name /*: Symbol */ = /*:: (*/rawName /*: any) */; | ||
// Get the correct identifier string for this symbol. | ||
let identifier = symbolToIdentifier.get(name); | ||
var identifier = symbolToIdentifier.get(name); | ||
@@ -137,16 +145,27 @@ // If there is no identifier, create one and set it. | ||
return identifier; | ||
} else { | ||
throw debugError(new Error(`Expected string or symbol, received '${String(rawName)}'`)); | ||
} | ||
}).join(".")); | ||
break; | ||
case "VALUE": | ||
values.push(item.value); | ||
sqlFragments.push(`$${values.length}`); | ||
break; | ||
default: | ||
}).join(".")); | ||
break; | ||
case "VALUE": | ||
values.push(item.value); | ||
sqlFragments.push(`$${values.length}`); | ||
break; | ||
default: | ||
} | ||
} | ||
} catch (err) { | ||
_didIteratorError = true; | ||
_iteratorError = err; | ||
} finally { | ||
try { | ||
if (!_iteratorNormalCompletion && _iterator.return) { | ||
_iterator.return(); | ||
} | ||
} finally { | ||
if (_didIteratorError) { | ||
throw _iteratorError; | ||
} | ||
} | ||
} | ||
const text = sqlFragments.join(""); | ||
var text = sqlFragments.join(""); | ||
return { | ||
@@ -159,6 +178,15 @@ text, | ||
function enforceValidNode(node /*: mixed */) /*: SQLNode */{ | ||
// $FlowFixMe: flow doesn't like symbols | ||
if (node !== null && typeof node === "object" && node[$$trusted] === true) { | ||
// $FlowFixMe: this has been validated | ||
return node; | ||
if (node != null && typeof node === "object") { | ||
var isRaw = node.type === "RAW" && typeof node.text === "string"; | ||
var isIdentifier = node.type === "IDENTIFIER" && Array.isArray(node.names) && node.names.every(function (name) { | ||
return typeof name === "string" || typeof name === "symbol"; | ||
}); | ||
var isValue = node.type === "VALUE"; | ||
// $FlowFixMe: flow doesn't like symbols here? | ||
var isTrusted = node[$$trusted] === true; | ||
if ((isRaw || isIdentifier || isValue) && isTrusted) { | ||
// $FlowFixMe: this has been validated | ||
return node; | ||
} | ||
} | ||
@@ -176,29 +204,29 @@ throw new Error(`Expected SQL item, instead received '${String(node)}'.`); | ||
*/ | ||
function query(strings /*: Array<string> */ | ||
, ...values /*: Array<SQL> */ | ||
) /*: SQLQuery */{ | ||
function query(strings /*: mixed */ | ||
) /*: Array<mixed> */ | ||
/*: SQLQuery */{ | ||
for (var _len = arguments.length, values = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { | ||
values[_key - 1] = arguments[_key]; | ||
} | ||
if (!Array.isArray(strings)) { | ||
throw new Error("sql.query should be used as a template literal, not a function call!"); | ||
} | ||
const items = []; | ||
for (let i = 0, l = strings.length; i < l; i++) { | ||
const text = strings[i]; | ||
return strings.reduce(function (items, text, 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 value = values[i]; | ||
if (Array.isArray(value)) { | ||
const nodes /*: SQLQuery */ = value.map(enforceValidNode); | ||
items.push(...nodes); | ||
if (!values[i]) { | ||
return items.concat(makeRawNode(text)); | ||
} else { | ||
var _value = values[i]; | ||
if (Array.isArray(_value)) { | ||
var nodes /*: SQLQuery */ = _value.map(enforceValidNode); | ||
return items.concat(makeRawNode(text), nodes); | ||
} else { | ||
const node /*: SQLNode */ = enforceValidNode(value); | ||
items.push(node); | ||
var node /*: SQLNode */ = enforceValidNode(_value); | ||
return items.concat(makeRawNode(text), node); | ||
} | ||
} | ||
} | ||
return items; | ||
}, []); | ||
} | ||
@@ -211,5 +239,5 @@ | ||
*/ | ||
function raw(text /*: string */) /*: SQLNode */{ | ||
var raw = function raw(text /*: mixed */) { | ||
return makeRawNode(String(text)); | ||
} | ||
}; | ||
@@ -221,6 +249,11 @@ /** | ||
*/ | ||
function identifier(...names /*: Array<string | Symbol> */) /*: SQLNode */{ | ||
return makeIdentifierNode(ensureNonEmptyArray(names)); | ||
} | ||
var identifier = function identifier() { | ||
for (var _len2 = arguments.length, names = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { | ||
names[_key2] = arguments[_key2]; | ||
} | ||
return (/*: Array<mixed> */makeIdentifierNode(ensureNonEmptyArray(names)) | ||
); | ||
}; | ||
/** | ||
@@ -230,10 +263,6 @@ * Creates a Sql item for a value that will be included in our final query. | ||
*/ | ||
function value(val /*: mixed */) /*: SQLNode */{ | ||
var value = function value(val /*: mixed */) { | ||
return makeValueNode(val); | ||
} | ||
}; | ||
const trueNode = raw(`TRUE`); | ||
const falseNode = raw(`FALSE`); | ||
const nullNode = raw(`NULL`); | ||
/** | ||
@@ -243,3 +272,3 @@ * If the value is simple will inline it into the query, otherwise will defer | ||
*/ | ||
function literal(val /*: mixed */) /*: SQLNode */{ | ||
var literal = function literal(val /*: mixed */) { | ||
if (typeof val === "string" && val.match(/^[a-zA-Z0-9_-]*$/)) { | ||
@@ -254,9 +283,13 @@ return raw(`'${val}'`); | ||
} else if (typeof val === "boolean") { | ||
return val ? trueNode : falseNode; | ||
} else if (val == null) { | ||
return nullNode; | ||
if (val) { | ||
return raw(`TRUE`); | ||
} else { | ||
return raw(`FALSE`); | ||
} | ||
} else if (isNil(val)) { | ||
return raw(`NULL`); | ||
} else { | ||
return makeValueNode(val); | ||
} | ||
} | ||
}; | ||
@@ -267,27 +300,27 @@ /** | ||
*/ | ||
function join(items /*: Array<SQL> */ | ||
, rawSeparator /*: string */ = "") /*: SQLQuery */{ | ||
ensureNonEmptyArray(items, true); | ||
var join = function join(rawItems /*: mixed */) { | ||
var rawSeparator /*: mixed */ = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; | ||
if (!Array.isArray(rawItems)) { | ||
throw new Error("Items to join must be an array"); | ||
} | ||
var items = rawItems; | ||
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 /*: SQL */ = items[i]; | ||
let itemsToAppend /*: SQLNode | SQLQuery */; | ||
var separator = rawSeparator; | ||
return ensureNonEmptyArray(items, true).reduce(function (currentItems, rawItem, i) { | ||
var item = void 0 /*: SQLNode | SQLQuery */; | ||
if (Array.isArray(rawItem)) { | ||
itemsToAppend = rawItem.map(enforceValidNode); | ||
item = rawItem.map(enforceValidNode); | ||
} else { | ||
itemsToAppend = [enforceValidNode(rawItem)]; | ||
item = enforceValidNode(rawItem); | ||
} | ||
if (i === 0 || !separator) { | ||
currentItems.push(...itemsToAppend); | ||
return currentItems.concat(item); | ||
} else { | ||
currentItems.push(sepNode, ...itemsToAppend); | ||
return currentItems.concat(makeRawNode(separator), item); | ||
} | ||
} | ||
return currentItems; | ||
} | ||
}, []); | ||
}; | ||
@@ -299,3 +332,3 @@ // Copied from https://github.com/brianc/node-postgres/blob/860cccd53105f7bc32fed8b1de69805f0ecd12eb/lib/client.js#L285-L302 | ||
for (var i = 0, l = str.length; i < l; i++) { | ||
for (var i = 0; i < str.length; i++) { | ||
var c = str[i]; | ||
@@ -314,19 +347,45 @@ if (c === '"') { | ||
exports.query = query; | ||
// The types we export are stricter so people get the right hinting | ||
exports.query = function sqlQuery(strings /*: string[] */ | ||
) /*: Array<SQL> */ | ||
/*: SQLQuery */{ | ||
for (var _len3 = arguments.length, values = Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { | ||
values[_key3 - 1] = arguments[_key3]; | ||
} | ||
return query.apply(undefined, [strings].concat(values)); | ||
}; | ||
exports.fragment = exports.query; | ||
exports.raw = raw; | ||
exports.raw = function sqlRaw(text /*: string */) /*: SQLNode */{ | ||
return raw(text); | ||
}; | ||
exports.identifier = identifier; | ||
exports.identifier = function sqlIdentifier() /*: Array<string | Symbol> */ | ||
/*: SQLNode */{ | ||
return identifier.apply(undefined, arguments); | ||
}; | ||
exports.value = value; | ||
exports.value = function sqlValue(val /*: mixed */) /*: SQLNode */{ | ||
return value(val); | ||
}; | ||
exports.literal = literal; | ||
exports.literal = function sqlLiteral(val /*: mixed */) /*: SQLNode */{ | ||
return literal(val); | ||
}; | ||
exports.join = join; | ||
exports.join = function sqlJoin(items /*: Array<SQL> */ | ||
) /*: SQLQuery */{ | ||
var separator /*: string */ = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; | ||
exports.compile = compile; | ||
return join(items, separator); | ||
}; | ||
exports.null = nullNode; | ||
exports.compile = function sqlCompile(sql /*: SQLQuery */) /*: QueryConfig */{ | ||
return compile(sql); | ||
}; | ||
exports.null = exports.literal(null); | ||
exports.blank = exports.query``; |
{ | ||
"name": "pg-sql2", | ||
"version": "1.0.0-beta.7", | ||
"version": "1.0.0-beta.8", | ||
"description": "Generate safe Postgres-compliant SQL with tagged template literals", | ||
"main": "lib/index.js", | ||
"main": "index.js", | ||
"scripts": { | ||
@@ -10,4 +10,3 @@ "flow": "flow", | ||
"lint": "eslint .", | ||
"test": "node src/index.js && eslint . && flow check && jest && markdown-doctest", | ||
"test:u": "node src/index.js && eslint . && flow check && jest -u && markdown-doctest", | ||
"test": "node index.js && eslint . && flow check && jest && markdown-doctest", | ||
"test:docs": "markdown-doctest", | ||
@@ -45,7 +44,7 @@ "prepublish": "babel --out-dir lib src" | ||
"eslint-plugin-jest": "^20.0.3", | ||
"eslint-plugin-prettier": "2.6.0", | ||
"flow-bin": "0.66.0", | ||
"eslint-plugin-prettier": "^2.1.2", | ||
"flow-bin": "^0.52.0", | ||
"jest": "20.0.4", | ||
"markdown-doctest": "^0.9.1", | ||
"prettier": "1.11.0" | ||
"prettier": "^1.5.3" | ||
}, | ||
@@ -62,6 +61,3 @@ "jest": { | ||
"index.js" | ||
], | ||
"engines": { | ||
"node": ">=8.6" | ||
} | ||
] | ||
} |
@@ -1,2 +0,3 @@ | ||
# pg-sql2 | ||
pg-sql2 | ||
======= | ||
@@ -7,3 +8,3 @@ Create SQL in a powerful and flexible manner without opening yourself to SQL | ||
```js | ||
const sql = require("pg-sql2"); | ||
const sql = require('pg-sql2'); | ||
// or import sql from 'pg-sql2'; | ||
@@ -23,10 +24,8 @@ | ||
// statement, to ensure that no SQL injection can occur. | ||
const sqlConditions = sql.query`created_at > NOW() - interval '3 years' and age > ${sql.value( | ||
22 | ||
)}`; | ||
const sqlConditions = | ||
sql.query`created_at > NOW() - interval '3 years' and age > ${sql.value(22)}`; | ||
// This could be a full query, but we're going to embed it in another query safely | ||
const innerQuery = sql.query`select ${sqlFields} from ${sql.identifier( | ||
tableName | ||
)} where ${sqlConditions}`; | ||
const innerQuery = | ||
sql.query`select ${sqlFields} from ${sql.identifier(tableName)} where ${sqlConditions}`; | ||
@@ -60,5 +59,6 @@ // Symbols are automatically assigned unique identifiers | ||
## API | ||
API | ||
--- | ||
### `` sql.query`...` `` | ||
### ``sql.query`...` `` | ||
@@ -68,5 +68,4 @@ Builds part of (or the whole of) an SQL query, safely interpretting the embedded expressions. If a non `sql.*` expression is passed in, e.g.: | ||
<!-- skip-example --> | ||
```js | ||
sql.query`select ${1}`; | ||
sql.query`select ${1}` | ||
``` | ||
@@ -97,15 +96,11 @@ | ||
```js | ||
const arrayOfSqlFields = ["a", "b", "c", "d"].map(n => sql.identifier(n)); | ||
sql.query`select ${sql.join(arrayOfSqlFields, ", ")}`; // -> select "a", "b", "c", "d" | ||
const arrayOfSqlFields = ['a', 'b', 'c', 'd'].map(n => sql.identifier(n)); | ||
sql.query`select ${sql.join(arrayOfSqlFields, ', ')}` // -> select "a", "b", "c", "d" | ||
const arrayOfSqlConditions = [ | ||
sql.query`a = 1`, | ||
sql.query`b = 2`, | ||
sql.query`c = 3` | ||
]; | ||
sql.query`where (${sql.join(arrayOfSqlConditions, ") and (")})`; // -> where (a = 1) and (b = 2) and (c = 3) | ||
const arrayOfSqlConditions = [sql.query`a = 1`, sql.query`b = 2`, sql.query`c = 3`]; | ||
sql.query`where (${sql.join(arrayOfSqlConditions, ') and (')})` // -> where (a = 1) and (b = 2) and (c = 3) | ||
const fragments = [ | ||
{ alias: "name", sqlFragment: sql.identifier("user", "name") }, | ||
{ alias: "age", sqlFragment: sql.identifier("user", "age") } | ||
{alias: 'name', sqlFragment: sql.identifier('user', 'name')}, | ||
{alias: 'age', sqlFragment: sql.identifier('user', 'age')}, | ||
]; | ||
@@ -125,5 +120,5 @@ sql.query` | ||
sql.query`inner join bar on (bar.foo_id = foo.id)`, | ||
sql.query`inner join baz on (baz.bar_id = bar.id)` | ||
sql.query`inner join baz on (baz.bar_id = bar.id)`, | ||
]; | ||
sql.query`select * from foo ${sql.join(arrayOfSqlInnerJoins, " ")}`; | ||
sql.query`select * from foo ${sql.join(arrayOfSqlInnerJoins, " ")}` | ||
// select * from foo inner join bar on (bar.foo_id = foo.id) inner join baz on (baz.bar_id = bar.id) | ||
@@ -143,3 +138,4 @@ ``` | ||
## History | ||
History | ||
------- | ||
@@ -152,14 +148,11 @@ This is a replacement for [@calebmer's | ||
* Better development experience for people not using Flow/TypeScript (throws | ||
- Better development experience for people not using Flow/TypeScript (throws | ||
errors a lot earlier allowing you to catch issues at the source) | ||
* Slightly more helpful error messages | ||
* Uses a symbol-key on the query nodes to protect against an object | ||
accidentally being inserted verbatim and being treated as valid (because | ||
every Symbol is unique an attacker would need control of the code to get a | ||
reference to the Symbol in order to set it on an object (it cannot be | ||
serialised/deserialised via JSON or any other medium), and if the attacker | ||
has control of the code then you've already lost) | ||
* Adds `sql.literal` which is similar to `sql.value` but when used with simple | ||
- Slightly more helpful error messages | ||
- Uses a hidden non-enumerable symbol as the type of the query nodes to protect | ||
against an object accidentally being inserted verbatim and being treated as | ||
valid | ||
- Adds `sql.literal` which is similar to `sql.value` but when used with simple | ||
values can write the valid direct to the SQL statement. **USE WITH CAUTION**. | ||
The purpose for this is if you are using _trusted_ values (e.g. for the keys | ||
The purpose for this is if you are using *trusted* values (e.g. for the keys | ||
to | ||
@@ -166,0 +159,0 @@ [`json_build_object(...)`](https://www.postgresql.org/docs/9.6/static/functions-json.html)) |
233
src/index.js
@@ -10,2 +10,4 @@ "use strict"; | ||
const isSymbol = sym => typeof sym === "symbol"; | ||
const isNil = o => o === null || o === undefined; | ||
const debug = require("debug")("pg-sql2"); | ||
@@ -43,48 +45,37 @@ | ||
function makeRawNode(text /*: string */) /*: SQLRawNode */ { | ||
if (typeof text !== "string") { | ||
throw new Error("Invalid argument to makeRawNode - expected string"); | ||
} | ||
// $FlowFixMe: flow doesn't like symbols | ||
return { type: "RAW", text, [$$trusted]: true }; | ||
function makeTrustedNode /*:: <Node>*/(node /*: Node */) /*: Node */ { | ||
Object.defineProperty(node, $$trusted, { | ||
enumerable: false, | ||
configurable: false, | ||
value: true, | ||
}); | ||
return node; | ||
} | ||
function isStringOrSymbol(val) { | ||
return typeof val === "string" || typeof val === "symbol"; | ||
function makeRawNode(text /*: string */) /*: SQLRawNode */ { | ||
return makeTrustedNode({ type: "RAW", text }); | ||
} | ||
function makeIdentifierNode( | ||
names /*: Array<string | Symbol> */ | ||
names /*: Array<mixed> */ | ||
) /*: SQLIdentifierNode */ { | ||
if (!Array.isArray(names) || !names.every(isStringOrSymbol)) { | ||
throw new Error( | ||
"Invalid argument to makeIdentifierNode - expected array of strings/symbols" | ||
); | ||
} | ||
// $FlowFixMe | ||
return { type: "IDENTIFIER", names, [$$trusted]: true }; | ||
return makeTrustedNode({ type: "IDENTIFIER", names }); | ||
} | ||
function makeValueNode(value /*: mixed */) /*: SQLValueNode */ { | ||
// $FlowFixMe | ||
return { type: "VALUE", value, [$$trusted]: true }; | ||
return makeTrustedNode({ type: "VALUE", value }); | ||
} | ||
function ensureNonEmptyArray /*:: <T>*/( | ||
array /*: Array<T>*/, | ||
allowZeroLength = false | ||
) /*: Array<T> */ { | ||
function ensureNonEmptyArray(array, allowZeroLength = false) { | ||
if (!Array.isArray(array)) { | ||
throw debugError(new Error("Expected array")); | ||
} | ||
if (!allowZeroLength && array.length < 1) { | ||
if (array.length < 1 && !allowZeroLength) { | ||
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])}`) | ||
); | ||
array.forEach((entry, idx) => { | ||
if (entry == null) { | ||
throw debugError(new Error(`Array index ${idx} is ${String(entry)}`)); | ||
} | ||
} | ||
}); | ||
return array; | ||
@@ -110,10 +101,6 @@ } | ||
for (let i = 0, l = items.length; i < l; i++) { | ||
const rawItem = items[i]; | ||
for (const rawItem of items) { | ||
const item /*: SQLNode */ = enforceValidNode(rawItem); | ||
switch (item.type) { | ||
case "RAW": | ||
if (typeof item.text !== "string") { | ||
throw new Error("RAW node expected string"); | ||
} | ||
sqlFragments.push(item.text); | ||
@@ -131,19 +118,4 @@ break; | ||
return escapeSqlIdentifier(name); | ||
// $FlowFixMe: flow doesn't like symbols | ||
} else if (typeof rawName === "symbol") { | ||
const name /*: Symbol */ = /*:: (*/ rawName /*: any) */; | ||
// Get the correct identifier string for this symbol. | ||
let identifier = symbolToIdentifier.get(name); | ||
// If there is no identifier, create one and set it. | ||
if (!identifier) { | ||
identifier = `__local_${nextSymbolId++}__`; | ||
symbolToIdentifier.set(name, identifier); | ||
} | ||
// Return the identifier. Since we create it, we won’t have to | ||
// escape it because we know all of the characters are safe. | ||
return identifier; | ||
} else { | ||
} | ||
if (!isSymbol(rawName)) { | ||
throw debugError( | ||
@@ -155,2 +127,16 @@ new Error( | ||
} | ||
const name /*: Symbol */ = /*:: (*/ rawName /*: any) */; | ||
// Get the correct identifier string for this symbol. | ||
let identifier = symbolToIdentifier.get(name); | ||
// If there is no identifier, create one and set it. | ||
if (!identifier) { | ||
identifier = `__local_${nextSymbolId++}__`; | ||
symbolToIdentifier.set(name, identifier); | ||
} | ||
// Return the identifier. Since we create it, we won’t have to | ||
// escape it because we know all of the characters are safe. | ||
return identifier; | ||
}) | ||
@@ -176,6 +162,18 @@ .join(".") | ||
function enforceValidNode(node /*: mixed */) /*: SQLNode */ { | ||
// $FlowFixMe: flow doesn't like symbols | ||
if (node !== null && typeof node === "object" && node[$$trusted] === true) { | ||
// $FlowFixMe: this has been validated | ||
return node; | ||
if (node != null && typeof node === "object") { | ||
const isRaw = node.type === "RAW" && typeof node.text === "string"; | ||
const isIdentifier = | ||
node.type === "IDENTIFIER" && | ||
Array.isArray(node.names) && | ||
node.names.every( | ||
name => typeof name === "string" || typeof name === "symbol" | ||
); | ||
const isValue = node.type === "VALUE"; | ||
// $FlowFixMe: flow doesn't like symbols here? | ||
const isTrusted = node[$$trusted] === true; | ||
if ((isRaw || isIdentifier || isValue) && isTrusted) { | ||
// $FlowFixMe: this has been validated | ||
return node; | ||
} | ||
} | ||
@@ -194,4 +192,4 @@ throw new Error(`Expected SQL item, instead received '${String(node)}'.`); | ||
function query( | ||
strings /*: Array<string> */, | ||
...values /*: Array<SQL> */ | ||
strings /*: mixed */, | ||
...values /*: Array<mixed> */ | ||
) /*: SQLQuery */ { | ||
@@ -203,5 +201,3 @@ if (!Array.isArray(strings)) { | ||
} | ||
const items = []; | ||
for (let i = 0, l = strings.length; i < l; i++) { | ||
const text = strings[i]; | ||
return strings.reduce((items, text, i) => { | ||
if (typeof text !== "string") { | ||
@@ -212,17 +208,15 @@ throw new Error( | ||
} | ||
if (text.length > 0) { | ||
items.push(makeRawNode(text)); | ||
} | ||
if (values[i]) { | ||
if (!values[i]) { | ||
return items.concat(makeRawNode(text)); | ||
} else { | ||
const value = values[i]; | ||
if (Array.isArray(value)) { | ||
const nodes /*: SQLQuery */ = value.map(enforceValidNode); | ||
items.push(...nodes); | ||
return items.concat(makeRawNode(text), nodes); | ||
} else { | ||
const node /*: SQLNode */ = enforceValidNode(value); | ||
items.push(node); | ||
return items.concat(makeRawNode(text), node); | ||
} | ||
} | ||
} | ||
return items; | ||
}, []); | ||
} | ||
@@ -235,5 +229,3 @@ | ||
*/ | ||
function raw(text /*: string */) /*: SQLNode */ { | ||
return makeRawNode(String(text)); | ||
} | ||
const raw = (text /*: mixed */) => makeRawNode(String(text)); | ||
@@ -245,5 +237,4 @@ /** | ||
*/ | ||
function identifier(...names /*: Array<string | Symbol> */) /*: SQLNode */ { | ||
return makeIdentifierNode(ensureNonEmptyArray(names)); | ||
} | ||
const identifier = (...names /*: Array<mixed> */) => | ||
makeIdentifierNode(ensureNonEmptyArray(names)); | ||
@@ -254,10 +245,4 @@ /** | ||
*/ | ||
function value(val /*: mixed */) /*: SQLNode */ { | ||
return makeValueNode(val); | ||
} | ||
const value = (val /*: mixed */) => makeValueNode(val); | ||
const trueNode = raw(`TRUE`); | ||
const falseNode = raw(`FALSE`); | ||
const nullNode = raw(`NULL`); | ||
/** | ||
@@ -267,3 +252,3 @@ * If the value is simple will inline it into the query, otherwise will defer | ||
*/ | ||
function literal(val /*: mixed */) /*: SQLNode */ { | ||
const literal = (val /*: mixed */) => { | ||
if (typeof val === "string" && val.match(/^[a-zA-Z0-9_-]*$/)) { | ||
@@ -278,9 +263,13 @@ return raw(`'${val}'`); | ||
} else if (typeof val === "boolean") { | ||
return val ? trueNode : falseNode; | ||
} else if (val == null) { | ||
return nullNode; | ||
if (val) { | ||
return raw(`TRUE`); | ||
} else { | ||
return raw(`FALSE`); | ||
} | ||
} else if (isNil(val)) { | ||
return raw(`NULL`); | ||
} else { | ||
return makeValueNode(val); | ||
} | ||
} | ||
}; | ||
@@ -291,7 +280,7 @@ /** | ||
*/ | ||
function join( | ||
items /*: Array<SQL> */, | ||
rawSeparator /*: string */ = "" | ||
) /*: SQLQuery */ { | ||
ensureNonEmptyArray(items, true); | ||
const join = (rawItems /*: mixed */, rawSeparator /*: mixed */ = "") => { | ||
if (!Array.isArray(rawItems)) { | ||
throw new Error("Items to join must be an array"); | ||
} | ||
const items = rawItems; | ||
if (typeof rawSeparator !== "string") { | ||
@@ -301,20 +290,16 @@ 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 /*: SQL */ = items[i]; | ||
let itemsToAppend /*: SQLNode | SQLQuery */; | ||
return ensureNonEmptyArray(items, true).reduce((currentItems, rawItem, i) => { | ||
let item /*: SQLNode | SQLQuery */; | ||
if (Array.isArray(rawItem)) { | ||
itemsToAppend = rawItem.map(enforceValidNode); | ||
item = rawItem.map(enforceValidNode); | ||
} else { | ||
itemsToAppend = [enforceValidNode(rawItem)]; | ||
item = enforceValidNode(rawItem); | ||
} | ||
if (i === 0 || !separator) { | ||
currentItems.push(...itemsToAppend); | ||
return currentItems.concat(item); | ||
} else { | ||
currentItems.push(sepNode, ...itemsToAppend); | ||
return currentItems.concat(makeRawNode(separator), item); | ||
} | ||
} | ||
return currentItems; | ||
} | ||
}, []); | ||
}; | ||
@@ -326,3 +311,3 @@ // Copied from https://github.com/brianc/node-postgres/blob/860cccd53105f7bc32fed8b1de69805f0ecd12eb/lib/client.js#L285-L302 | ||
for (var i = 0, l = str.length; i < l; i++) { | ||
for (var i = 0; i < str.length; i++) { | ||
var c = str[i]; | ||
@@ -341,19 +326,43 @@ if (c === '"') { | ||
exports.query = query; | ||
// The types we export are stricter so people get the right hinting | ||
exports.query = function sqlQuery( | ||
strings /*: string[] */, | ||
...values /*: Array<SQL> */ | ||
) /*: SQLQuery */ { | ||
return query(strings, ...values); | ||
}; | ||
exports.fragment = exports.query; | ||
exports.raw = raw; | ||
exports.raw = function sqlRaw(text /*: string */) /*: SQLNode */ { | ||
return raw(text); | ||
}; | ||
exports.identifier = identifier; | ||
exports.identifier = function sqlIdentifier( | ||
...names /*: Array<string | Symbol> */ | ||
) /*: SQLNode */ { | ||
return identifier(...names); | ||
}; | ||
exports.value = value; | ||
exports.value = function sqlValue(val /*: mixed */) /*: SQLNode */ { | ||
return value(val); | ||
}; | ||
exports.literal = literal; | ||
exports.literal = function sqlLiteral(val /*: mixed */) /*: SQLNode */ { | ||
return literal(val); | ||
}; | ||
exports.join = join; | ||
exports.join = function sqlJoin( | ||
items /*: Array<SQL> */, | ||
separator /*: string */ = "" | ||
) /*: SQLQuery */ { | ||
return join(items, separator); | ||
}; | ||
exports.compile = compile; | ||
exports.compile = function sqlCompile(sql /*: SQLQuery */) /*: QueryConfig */ { | ||
return compile(sql); | ||
}; | ||
exports.null = nullNode; | ||
exports.null = exports.literal(null); | ||
exports.blank = exports.query``; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
29153
6
638
154