Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added pg functions keyword and identifier, updated README #2

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 41 additions & 2 deletions README.md
Expand Up @@ -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
Expand All @@ -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 :)
- Pull requests are welcome :)
135 changes: 135 additions & 0 deletions 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
Expand All @@ -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
48 changes: 48 additions & 0 deletions test/index.js
Expand Up @@ -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)
})
})