diff --git a/.eslintrc b/.eslintrc index cee55ae8..41e6da41 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,5 +3,14 @@ "canonical", "canonical/flowtype" ], - "root": true + "root": true, + "rules": { + "no-restricted-syntax": 0, + "no-unused-expressions": [ + 2, + { + "allowTaggedTemplates": true + } + ] + } } diff --git a/README.md b/README.md index 5c85ebf4..db7dee79 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A PostgreSQL client with strict types and assertions. * [A value set](#a-value-set) * [Multiple value sets](#multiple-value-sets) * [Named placeholders](#named-placeholders) + * [Template literals](#template-literals) * [Query methods](#query-methods) * [`any`](#any) * [`insert`](#insert) @@ -131,6 +132,20 @@ SELECT $1 ``` +### Template literals + +Query methods can be executed using [tagged template literals](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Template_literals#Tagged_template_literals) syntax, e.g. + +```js +connection.query`INSERT INTO reservation_ticket (reservation_id, ticket_id) VALUES ${values}`; + +``` + +> Is it safe? Absolutely! +> `foo\`bar${baz}qux\`` is equivalent to `foo(['bar', 'qux'], baz)`. +> +> Mightyql recognises tagged template literal invocation and uses it to construct a query with anonymous value placeholders. +> Execution of the resulting query is delegated to the `pg` module. ## Query methods diff --git a/src/index.js b/src/index.js index 45cb97f3..e2285525 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,7 @@ import { NotFoundError } from './errors'; import { + mapTaggedTemplateLiteralInvocation, normalizeAnonymousValuePlaceholders, normalizeNamedValuePlaceholders } from './utilities'; @@ -187,10 +188,10 @@ const createConnection = async ( return ended; }, - many: many.bind(null, connection, clientConfiguration), - maybeOne: maybeOne.bind(null, connection, clientConfiguration), - one: one.bind(null, connection, clientConfiguration), - query: query.bind(null, connection) + many: mapTaggedTemplateLiteralInvocation(many).bind(null, connection, clientConfiguration), + maybeOne: mapTaggedTemplateLiteralInvocation(maybeOne).bind(null, connection, clientConfiguration), + one: mapTaggedTemplateLiteralInvocation(one).bind(null, connection, clientConfiguration), + query: mapTaggedTemplateLiteralInvocation(query).bind(null, connection) }; }; @@ -201,11 +202,11 @@ const createPool = ( const pool = new pg.Pool(typeof connectionConfiguration === 'string' ? parseConnectionString(connectionConfiguration) : connectionConfiguration); return { - any: any.bind(null, pool, clientConfiguration), - many: many.bind(null, pool, clientConfiguration), - maybeOne: maybeOne.bind(null, pool, clientConfiguration), - one: one.bind(null, pool, clientConfiguration), - query: query.bind(null, pool) + any: mapTaggedTemplateLiteralInvocation(any).bind(null, pool, clientConfiguration), + many: mapTaggedTemplateLiteralInvocation(many).bind(null, pool, clientConfiguration), + maybeOne: mapTaggedTemplateLiteralInvocation(maybeOne).bind(null, pool, clientConfiguration), + one: mapTaggedTemplateLiteralInvocation(one).bind(null, pool, clientConfiguration), + query: mapTaggedTemplateLiteralInvocation(query).bind(null, pool) }; }; diff --git a/src/utilities/index.js b/src/utilities/index.js index 955598d6..cc6ec0bd 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -1,4 +1,5 @@ // @flow +export {default as mapTaggedTemplateLiteralInvocation} from './mapTaggedTemplateLiteralInvocation'; export {default as normalizeAnonymousValuePlaceholders} from './normalizeAnonymousValuePlaceholders'; export {default as normalizeNamedValuePlaceholders} from './normalizeNamedValuePlaceholders'; diff --git a/src/utilities/mapTaggedTemplateLiteralInvocation.js b/src/utilities/mapTaggedTemplateLiteralInvocation.js new file mode 100644 index 00000000..96f7cba8 --- /dev/null +++ b/src/utilities/mapTaggedTemplateLiteralInvocation.js @@ -0,0 +1,15 @@ +// @flow + +// eslint-disable-next-line flowtype/no-weak-types +export default (targetMethod: Function) => { + // eslint-disable-next-line flowtype/no-weak-types + return (maybeQuery: string | Array, ...args: any) => { + if (typeof maybeQuery === 'string') { + return targetMethod(maybeQuery, args[0]); + } else { + const strings = maybeQuery; + + return targetMethod(strings.join('?'), args); + } + }; +}; diff --git a/test/utilities/mapTaggedTemplateLiteralInvocation.js b/test/utilities/mapTaggedTemplateLiteralInvocation.js new file mode 100644 index 00000000..90ff9211 --- /dev/null +++ b/test/utilities/mapTaggedTemplateLiteralInvocation.js @@ -0,0 +1,29 @@ +// @flow + +import test from 'ava'; +import sinon from 'sinon'; +import { + mapTaggedTemplateLiteralInvocation +} from '../../src/utilities'; + +test('works with regular invocation', (t) => { + const spy = sinon.spy(); + + const method = mapTaggedTemplateLiteralInvocation(spy); + + method('query', 'parameters'); + + t.true(spy.calledWith('query', 'parameters')); +}); + +test('works with tagged template literals', (t) => { + const spy = sinon.spy(); + + const method = mapTaggedTemplateLiteralInvocation(spy); + + const bar = 'BAR'; + + method`foo${bar}baz`; + + t.true(spy.calledWith('foo?baz', ['BAR'])); +});