diff --git a/README.md b/README.md index 58a37cf..650256d 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ for (let table of largeArray) { } ``` -## Prepared Statements in Postgre +## Prepared Statements in Postgres Postgre requires prepared statements to be named, otherwise the parameters will be escaped and replaced on the client side. You can still use SQL template strings though, you just need to assign a name to the query before using it: ```js @@ -73,9 +73,48 @@ pg.query(query) pg.query(_.assign(SQL`SELECT author FROM books WHERE name = ${book}`, {name: 'my_query'})) ``` +## Postgres Keyword +You can enforce that a value is a Postgres keyword. If it fails, an exception `SQL.InvalidValue` is thrown. + +```js +let order = 'desc' +pg.query(SQL`SELECT author FROM books ORDER BY author ${PG.keyword(order)}`) +// SELECT author FROM books ORDER BY author DESC +``` + +You can narrow your options farther with a second `limiter` argument. +```js +let order = 'TABLE_NAME' +pg.query(SQL`SELECT author FROM books ORDER BY author ${PG.keyword(order, ['DESC', 'ASC')}`) +// throws an error, although TABLE_NAME is a Postgresql keyword. +``` + +## Postgres Identifier +Postgres identifiers can only be made of alphanumeric, diacritical marked letters, numbers, underscores, and dollar signs. +`SQL.PG.identifier` will properly quote your identifier and ensure that it only contains valid characters. + +```js +let field = 'author' +pg.query(SQL`SELECT ${PG.identifier(field)} FROM books` +// SELECT "author" FROM books +``` + +```js +let field = 'cheese sandwich' +pg.query(SQL`SELECT ${PG.identifier(field)} FROM books` +// throws SQL.InvalidValue because there is a space +``` + +You can also limit the possible values. +```js +let field = 'phone' +pg.query(SQL`SELECT ${PG.identifier(field, ['author', 'id')} FROM books` +// throws SQL.InvalidValue phone isn't one of the valid values +``` + ## Contributing - Tests are written using [mocha](https://www.npmjs.com/package/mocha) (BDD style) and [chai](https://www.npmjs.com/package/chai) (expect style) - This module follows [standard](https://www.npmjs.com/package/standard) coding style - You can use `npm test` to run the tests and check coding style - Since this module is only compatible with ES6 versions of node anyway, use all the ES6 goodies - - Pull requests are welcome :) \ No newline at end of file + - Pull requests are welcome :) diff --git a/index.js b/index.js index ef7abd5..2926a40 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,104 @@ 'use strict' +let pgKeywords = new Set(['A', 'ABORT', 'ABS', 'ABSENT', 'ABSOLUTE', 'ACCESS', + 'ACCORDING', 'ACTION', 'ADA', 'ADD', 'ADMIN', 'AFTER', 'AGGREGATE', 'ALL', + 'ALLOCATE', 'ALSO', 'ALTER', 'ALWAYS', 'ANALYSE', 'ANALYZE', 'AND', 'ANY', + 'ARE', 'ARRAY', 'ARRAYAGG', 'ARRAYMAXCARDINALITY', 'AS', 'ASC', 'ASENSITIVE', + 'ASSERTION', 'ASSIGNMENT', 'ASYMMETRIC', 'AT', 'ATOMIC', 'ATTRIBUTE', + 'ATTRIBUTES', 'AUTHORIZATION', 'AVG', 'BACKWARD', 'BASE', 'BEFORE', 'BEGIN', + 'BEGINFRAME', 'BEGINPARTITION', 'BERNOULLI', 'BETWEEN', 'BIGINT', 'BINARY', + 'BIT', 'BITLENGTH', 'BLOB', 'BLOCKED', 'BOM', 'BOOLEAN', 'BOTH', 'BREADTH', + 'BY', 'C', 'CACHE', 'CALL', 'CALLED', 'CARDINALITY', 'CASCADE', 'CASCADED', + 'CASE', 'CAST', 'CATALOG', 'CATALOGNAME', 'CEIL', 'CEILING', 'CHAIN', 'CHAR', + 'CHARACTER', 'CHARACTERISTICS', 'CHARACTERS', 'CHARACTERLENGTH', + 'CHARACTERSETCATALOG', 'CHARACTERSETNAME', 'CHARACTERSETSCHEMA', 'CHARLENGTH', + 'CHECK', 'CHECKPOINT', 'CLASS', 'CLASSORIGIN', 'CLOB', 'CLOSE', 'CLUSTER', + 'COALESCE', 'COBOL', 'COLLATE', 'COLLATION', 'COLLATIONCATALOG', + 'COLLATIONNAME', 'COLLATIONSCHEMA', 'COLLECT', 'COLUMN', 'COLUMNS', + 'COLUMNNAME', 'COMMANDFUNCTION', 'COMMANDFUNCTIONCODE', 'COMMENT', 'COMMENTS', + 'COMMIT', 'COMMITTED', 'CONCURRENTLY', 'CONDITION', 'CONDITIONNUMBER', + 'CONFIGURATION', 'CONFLICT', 'CONNECT', 'CONNECTION', 'CONNECTIONNAME', + 'CONSTRAINT', 'CONSTRAINTS', 'CONSTRAINTCATALOG', 'CONSTRAINTNAME', + 'CONSTRAINTSCHEMA', 'CONSTRUCTOR', 'CONTAINS', 'CONTENT', 'CONTINUE', + 'CONTROL', 'CONVERSION', 'CONVERT', 'COPY', 'CORR', 'CORRESPONDING', 'COST', + 'COUNT', 'COVARPOP', 'COVARSAMP', 'CREATE', 'CROSS', 'CSV', 'CUBE', + 'CUMEDIST', 'CURRENT', 'CURRENTCATALOG', 'CURRENTDATE', + 'CURRENTDEFAULTTRANSFORMGROUP', 'CURRENTPATH', 'CURRENTROLE', 'CURRENTROW', + 'CURRENTSCHEMA', 'CURRENTTIME', 'CURRENTTIMESTAMP', + 'CURRENTTRANSFORMGROUPFORTYPE', 'CURRENTUSER', 'CURSOR', 'CURSORNAME', + 'CYCLE', 'DATA', 'DATABASE', 'DATALINK', 'DATE', 'DATETIMEINTERVALCODE', + 'DATETIMEINTERVALPRECISION', 'DAY', 'DB', 'DEALLOCATE', 'DEC', 'DECIMAL', + 'DECLARE', 'DEFAULT', 'DEFAULTS', 'DEFERRABLE', 'DEFERRED', 'DEFINED', + 'DEFINER', 'DEGREE', 'DELETE', 'DELIMITER', 'DELIMITERS', 'DENSERANK', + 'DEPTH', 'DEREF', 'DERIVED', 'DESC', 'DESCRIBE', 'DESCRIPTOR', + 'DETERMINISTIC', 'DIAGNOSTICS', 'DICTIONARY', 'DISABLE', 'DISCARD', + 'DISCONNECT', 'DISPATCH', 'DISTINCT', 'DLNEWCOPY', 'DLPREVIOUSCOPY', + 'DLURLCOMPLETE', 'DLURLCOMPLETEONLY', 'DLURLCOMPLETEWRITE', 'DLURLPATH', + 'DLURLPATHONLY', 'DLURLPATHWRITE', 'DLURLSCHEME', 'DLURLSERVER', 'DLVALUE', + 'DO', 'DOCUMENT', 'DOMAIN', 'DOUBLE', 'DROP', 'DYNAMIC', 'DYNAMICFUNCTION', + 'DYNAMICFUNCTIONCODE', 'EACH', 'ELEMENT', 'ELSE', 'EMPTY', 'ENABLE', + 'ENCODING', 'ENCRYPTED', 'END', 'ENDEXEC', 'ENDFRAME', 'ENDPARTITION', + 'ENFORCED', 'ENUM', 'EQUALS', 'ESCAPE', 'EVENT', 'EVERY', 'EXCEPT', + 'EXCEPTION', 'EXCLUDE', 'EXCLUDING', 'EXCLUSIVE', 'EXEC', 'EXECUTE', 'EXISTS', + 'EXP', 'EXPLAIN', 'EXPRESSION', 'EXTENSION', 'EXTERNAL', 'EXTRACT', 'FALSE', + 'FAMILY', 'FETCH', 'FILE', 'FILTER', 'FINAL', 'FIRST', 'FIRSTVALUE', 'FLAG', + 'FLOAT', 'FLOOR', 'FOLLOWING', 'FOR', 'FORCE', 'FOREIGN', 'FORTRAN', + 'FORWARD', 'FOUND', 'FRAMEROW', 'FREE', 'FREEZE', 'FROM', 'FS', 'FULL', + 'FUNCTION', 'FUNCTIONS', 'FUSION', 'G', 'GENERAL', 'GENERATED', 'GET', + 'GLOBAL', 'GO', 'GOTO', 'GRANT', 'GRANTED', 'GREATEST', 'GROUP', 'GROUPING', + 'GROUPS', 'HANDLER', 'HAVING', 'HEADER', 'HEX', 'HIERARCHY', 'HOLD', 'HOUR', + 'ID', 'IDENTITY', 'IF', 'IGNORE', 'ILIKE', 'IMMEDIATE', 'IMMEDIATELY', + 'IMMUTABLE', 'IMPLEMENTATION', 'IMPLICIT', 'IMPORT', 'IN', 'INCLUDING', + 'INCREMENT', 'INDENT', 'INDEX', 'INDEXES', 'INDICATOR', 'INHERIT', 'INHERITS', + 'INITIALLY', 'INLINE', 'INNER', 'INOUT', 'INPUT', 'INSENSITIVE', 'INSERT', + 'INSTANCE', 'INSTANTIABLE', 'INSTEAD', 'INT', 'INTEGER', 'INTEGRITY', + 'INTERSECT', 'INTERSECTION', 'INTERVAL', 'INTO', 'INVOKER', 'IS', 'ISNULL', + 'ISOLATION', 'JOIN', 'K', 'KEY', 'KEYMEMBER', 'KEYTYPE', 'LABEL', 'LAG', + 'LANGUAGE', 'LARGE', 'LAST', 'LASTVALUE', 'LATERAL', 'LCCOLLATE', 'LCCTYPE', + 'LEAD', 'LEADING', 'LEAKPROOF', 'LEAST', 'LEFT', 'LENGTH', 'LEVEL', 'LIBRARY', + 'LIKE', 'LIKEREGEX', 'LIMIT', 'LINK', 'LISTEN', 'LN', 'LOAD', 'LOCAL', + 'LOCALTIME', 'LOCALTIMESTAMP', 'LOCATION', 'LOCATOR', 'LOCK', 'LOWER', 'M', + 'MAP', 'MAPPING', 'MATCH', 'MATCHED', 'MATERIALIZED', 'MAX', 'MAXVALUE', + 'MAXCARDINALITY', 'MEMBER', 'MERGE', 'MESSAGELENGTH', 'MESSAGEOCTETLENGTH', + 'MESSAGETEXT', 'METHOD', 'MIN', 'MINUTE', 'MINVALUE', 'MOD', 'MODE', + 'MODIFIES', 'MODULE', 'MONTH', 'MORE', 'MOVE', 'MULTISET', 'MUMPS', 'NAME', + 'NAMES', 'NAMESPACE', 'NATIONAL', 'NATURAL', 'NCHAR', 'NCLOB', 'NESTING', + 'NEW', 'NEXT', 'NFC', 'NFD', 'NFKC', 'NFKD', 'NIL', 'NO', 'NONE', 'NORMALIZE', + 'NORMALIZED', 'NOT', 'NOTHING', 'NOTIFY', 'NOTNULL', 'NOWAIT', 'NTHVALUE', + 'NTILE', 'NULL', 'NULLABLE', 'NULLIF', 'NULLS', 'NUMBER', 'NUMERIC', 'OBJECT', + 'OCCURRENCESREGEX', 'OCTETS', 'OCTETLENGTH', 'OF', 'OFF', 'OFFSET', 'OIDS', + 'OLD', 'ON', 'ONLY', 'OPEN', 'OPERATOR', 'OPTION', 'OPTIONS', 'OR', 'ORDER', + 'ORDERING', 'ORDINALITY', 'OTHERS', 'OUT', 'OUTER', 'OUTPUT', 'OVER', + 'OVERLAPS', 'OVERLAY', 'OVERRIDING', 'OWNED', 'OWNER', 'P', 'PAD', + 'PARAMETER', 'PARAMETERMODE', 'PARAMETERNAME', 'PARAMETERORDINALPOSITION', + 'PARAMETERSPECIFICCATALOG', 'PARAMETERSPECIFICNAME', + 'PARAMETERSPECIFICSCHEMA', 'PARSER', 'PARTIAL', 'PARTITION', 'PASCAL', + 'PASSING', 'PASSTHROUGH', 'PASSWORD', 'PATH', 'PERCENT', 'PERCENTILECONT', + 'PERCENTILEDISC', 'PERCENTRANK', 'PERIOD', 'PERMISSION', 'PLACING', 'PLANS', + 'PLI', 'POLICY', 'PORTION', 'POSITION', 'POSITIONREGEX', 'POWER', 'PRECEDES', + 'PRECEDING', 'PRECISION', 'PREPARE', 'PREPARED', 'PRESERVE', 'PRIMARY', + 'PRIOR', 'PRIVILEGES', 'PROCEDURAL', 'PROCEDURE', 'PROGRAM', 'PUBLIC', + 'QUOTE', 'RANGE', 'RANK', 'READ', 'READS', 'REAL', 'REASSIGN', 'RECHECK', + 'RECOVERY', 'RECURSIVE', 'REF', 'REFERENCES', 'REFERENCING', 'REFRESH', + 'REGRAVGX', 'REGRAVGY', 'REGRCOUNT', 'REGRINTERCEPT', 'REGRR', 'REGRSLOPE', + 'REGRSXX', 'REGRSXY', 'REGRSYY', 'REINDEX', 'RELATIVE', 'RELEASE', 'RENAME', + 'REPEATABLE', 'REPLACE', 'REPLICA', 'REQUIRING', 'RESET', 'RESPECT', + 'RESTART', 'RESTORE', 'RESTRICT', 'RESULT', 'RETURN', 'RETURNEDCARDINALITY', + 'RETURNEDLENGTH', 'RETURNEDOCTETLENGTH', 'RETURNEDSQLSTATE', 'RETURNING', + 'RETURNS', 'REVOKE', 'RIGHT', 'ROLE', 'ROLLBACK', 'ROLLUP', 'ROUTINE', + 'ROUTINECATALOG', 'ROUTINENAME', 'ROUTINESCHEMA', 'ROW', 'ROWS', 'ROWCOUNT', + 'ROWNUMBER', 'RULE', 'SAVEPOINT', 'SCALE', 'SCHEMA', 'SCHEMANAME', 'SCOPE', + 'SCOPECATALOG', 'SCOPENAME', 'SCOPESCHEMA', 'SCROLL', 'SEARCH', 'SECOND', + 'SECTION', 'SECURITY', 'SELECT', 'SELECTIVE', 'SELF', 'SENSITIVE', 'SEQUENCE', + 'SEQUENCES', 'SERIALIZABLE', 'SERVER', 'SERVERNAME', 'SESSION', 'SESSIONUSER', + 'SET', 'SETOF', 'SETS', 'SHARE', 'SHOW', 'SIMILAR', 'SIMPLE', 'SIZE', + 'SMALLINT', 'SNAPSHOT', 'SOME', 'SOURCE', 'SPACE', 'SPECIFIC', 'SPECIFICTYPE', + 'SPECIFICNAME', 'SQL', 'SQLCODE', 'SQLERROR', 'SQLEXCEPTION', 'SQLSTATE', + 'SQLWARNING', 'SQRT', 'STABLE', 'STANDALONE', 'START', 'STATE', 'STATEMENT', + 'STATIC', 'STATISTICS', 'STDDEVPOP', 'STDDEVSAMP', 'STDIN', 'STDOUT', + 'STORAGE', 'STRICT', 'STRIP']) +let pgIdentReg = /^[A-zÀ-ÿ_$]+$/ + function SQL (strings) { let args = Array.from(arguments).slice(1) let sql = '' // for mysql/mysql2 @@ -24,4 +123,40 @@ SQL.raw = function (value) { return {value, raw: true} } +SQL.InvalidValue = function (msg) { + Error.call(this, msg) + this.name = 'SQLTemplateInvalidValue' +} + +SQL.InvalidValue.prototoype = Object.create(Error.prototype) + +SQL.PG = { + keyword: function (value, subset) { + value = value.toUpperCase() + if (subset) { + subset = new Set(subset) + if (!subset.has(value)) { + throw new SQL.InvalidValue() + } + } + if (!pgKeywords.has(value)) { + throw new SQL.InvalidValue() + } + return {value, raw: true} + }, + + identifier: function (value, subset) { + if (subset) { + subset = new Set(subset) + if (!subset.has(value.toLowerCase())) { + throw new SQL.InvalidValue() + } + } + if (!pgIdentReg.test(value)) { + throw new SQL.InvalidValue() + } + return {value: `"${value}"`, raw: true} + } +} + module.exports = SQL diff --git a/test/index.js b/test/index.js index 1fc5681..b62d7d2 100644 --- a/test/index.js +++ b/test/index.js @@ -47,4 +47,52 @@ describe('SQL', function () { values: [column, value, column] }) }) + it('should accept PG keyword', function () { + let order = 'desc' + expect(SQL`SELECT * FROM table ORDER BY blah ${SQL.PG.keyword(order)}`).to.deep.equal({ + sql: 'SELECT * FROM table ORDER BY blah DESC', + text: 'SELECT * FROM table ORDER BY blah DESC', + values: [] + }) + }) + it('should accept narrowed PG keyword', function () { + let order = 'desc' + expect(SQL`SELECT * FROM table ORDER BY blah ${SQL.PG.keyword(order, ['ASC', 'DESC'])}`).to.deep.equal({ + sql: 'SELECT * FROM table ORDER BY blah DESC', + text: 'SELECT * FROM table ORDER BY blah DESC', + values: [] + }) + }) + it('should accept narrowed outside PG keyword', function () { + let order = 'desc' + expect(() => SQL`SELECT * FROM table ORDER BY blah ${SQL.PG.keyword(order, ['STRICT', 'REPLACE'])}`).to.throw(SQL.InvalidValue) + }) + it('should not accept PG non-keyword', function () { + let order = 'descr' + expect(() => SQL`SELECT * FROM table ORDER BY blah ${SQL.PG.keyword(order)}`).to.throw(SQL.InvalidValue) + }) + it('should accept keyword', function () { + let order = 'desc' + expect(SQL`SELECT * FROM table ORDER BY blah ${SQL.PG.keyword(order)}`).to.deep.equal({ + sql: 'SELECT * FROM table ORDER BY blah DESC', + text: 'SELECT * FROM table ORDER BY blah DESC', + values: [] + }) + }) + it('should accept PG identifier', function () { + let field = 'ham' + expect(SQL`SELECT * FROM table WHERE ${SQL.PG.identifier(field)}=1`).to.deep.equal({ + sql: 'SELECT * FROM table WHERE "ham"=1', + text: 'SELECT * FROM table WHERE "ham"=1', + values: [] + }) + }) + it('should not accept PG invalid identifier', function () { + let field = 'ham cheese' + expect(() => SQL`SELECT * FROM table WHERE ${SQL.PG.identifier(field)}=1`).to.throw(SQL.InvalidValue) + }) + it('should not accept PG valid excluded identifier', function () { + let field = 'ham' + expect(() => SQL`SELECT * FROM table WHERE ${SQL.PG.identifier(field, ['turkey', 'swiss'])}=1`).to.throw(SQL.InvalidValue) + }) })