From c986d93a399714bfb2958424e25ea3ee6b3143a8 Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Tue, 29 Sep 2015 14:06:01 -0700 Subject: [PATCH 1/2] added pg functions keyword and identifier, updated README --- README.md | 43 +++++++++++++++- index.js | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++ test/index.js | 48 ++++++++++++++++++ 3 files changed, 224 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 58a37cf..4a904ab 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.identier` will properly quote your identifier and ensure that it only contains valid characters. + +```js +let field = 'author' +pg.query(SQL`SELECT ${PG.keyword(field)} FROM books` +// SELECT "author" FROM books +``` + +```js +let field = 'cheese sandwich' +pg.query(SQL`SELECT ${PG.keyword(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.keyword(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) + }) }) From 89c927f81423e68ac13fd3170c340bbb941cd9d8 Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Tue, 29 Sep 2015 14:12:17 -0700 Subject: [PATCH 2/2] readme bugs --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4a904ab..650256d 100644 --- a/README.md +++ b/README.md @@ -91,24 +91,24 @@ pg.query(SQL`SELECT author FROM books ORDER BY author ${PG.keyword(order, ['DESC ## Postgres Identifier Postgres identifiers can only be made of alphanumeric, diacritical marked letters, numbers, underscores, and dollar signs. -`SQL.PG.identier` will properly quote your identifier and ensure that it only contains valid characters. +`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.keyword(field)} FROM books` +pg.query(SQL`SELECT ${PG.identifier(field)} FROM books` // SELECT "author" FROM books ``` ```js let field = 'cheese sandwich' -pg.query(SQL`SELECT ${PG.keyword(field)} FROM books` +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.keyword(field, ['author', 'id')} FROM books` +pg.query(SQL`SELECT ${PG.identifier(field, ['author', 'id')} FROM books` // throws SQL.InvalidValue phone isn't one of the valid values ```