├── .travis.yml ├── LICENSE ├── README.md ├── example.js ├── index.js ├── package.json └── test.js /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - lts/* 5 | jobs: 6 | include: 7 | - stage: npm release 8 | node_js: node 9 | script: echo "Deploying to npm ..." 10 | deploy: 11 | provider: npm 12 | email: 13 | secure: "PMRYjacyycR2jWqR7h7C4GxCkVgG+tQ4V8WDoqVLttHmU56geiYi8nGAzrvwNWSozoc/YaTkAX29+3kGVwsIIm5C6klXtUMkzMdFEAfyi9+ys8G1bWn3w2nBiwVSc8r95hGyyfecQCZmQTG/9dzL8LN2JxoNSAo63EoQGkn7Ww1k1oduvp60ha1LKhI0kAbkgam7fhn6CzREZlGCK/KogDedEXuzCKGnsy5JI6GyyMSlJY8jHrwdLbTUNHJkgBbIao97hgbtZLcaWYp0EJGs5L7h5qXYVjm4Awi7zZZoGWC1miYLDxN4xbK6EwvKTkpL+qDhv5EWr/alwkthobr7bQ+3NSCDPO7fzCFzvkO0Ce3c41npHD/hYCedXbUuSAZaL2gzWM4btqSwwa+xSfcgH6s69/w3M/tLLutkiFPi0o94SBhAtsMoUNznLQx7LBbr+LSn8yaF/3cfI7vvsiHb2fJ39JEJ90GCVBr7sETddgDKvnWh7Rq4mqws7YqnECkl4U95LqKUrVkO9eBTaXpml0uQQuzEBb519pUyUi9L5P8PKq4zQta2JO5vjJ88yWg8gOim3sLjZZUWpyetqPLmov9Al75Bt6DsqMK3MQ+W1LhgwwO8vn49DVlZpUTJ5ka0JFc31aqnxUjpT8LBH1DnNpeRPbjiMi/CIp+ompQxfTg=" 14 | api_key: # fcb1 15 | secure: "mlGe3YGLBjA/f+lW4bw4Vdhb9PYyold3jLKNQWkoScrATXUGVHPvvJpLBDKLPv3y3MRFpUeoMQyDGRDhpbp6lE1uD93sNa5Xrw8FVBly1M0DNioRR4Uq2Xo4UN0SCosLe1oj1YOD9y3KG3JHRuvq0QfXdCpKzmE+Sz8LuwUStrYrMDWitNRzjMkKZxxuGXwLdoDGi5ph55mmNYuLHML0Op830b2RNExGeiFpPQrxSmTB7MPOHBZvoZeUnHuqtdW12RRL9XrPBju0+ykOADJSdul5C8HWCWoh6FA6xEEDMXR/IeEsw7wI6ygBYlaQFRoDw2Oa4XOQ8CEqLqrrBxfjELdDksYwBw7KggsU0X3Y3pVgcGCyGBQZ5dr0WoeqVLzZYuIV2nwGKMsopAcob2I1T3uyBNVsFZXpEgCwZBhHM/qktc73N+J0LsAhYwJymQ97KfFRpMzl1GiTkzlgG+yodexXaIlJgGQ5Vf36pQPfV8OqwbYUQnI5eQcXwTHhVz+5VGkDGe6V3fU2PdfLx+7fQa2UT48osiDXyBZXFiLLqM8J5eh72efDwrIU7Gygn6MmeKaYMOs0GszfU22OQ1dMgsAVReC0ANZT8J9kiB4oEYrXgwhYXi0iNQlqyKulaHNb7491ZnXegDBvyUIC4VY5qn4tTszgAFsHK+mJBgDScLY=" 16 | on: 17 | tags: true 18 | node: node 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Hyperdivision ApS 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `prepare-sql` 2 | 3 | [![Build Status](https://travis-ci.org/hyperdivision/prepare-sql.svg?branch=master)](https://travis-ci.org/hyperdivision/prepare-sql) 4 | 5 | > SQL template strings 6 | 7 | ## Usage 8 | 9 | ```js 10 | var SQL = require('prepare-sql') 11 | 12 | const username = 'emilbayes' 13 | // Query with template string 14 | const select = SQL`SELECT * FROM users WHERE username = ${username}` 15 | 16 | const insertRows = ['mafintosh', 'chm-diederichs'] 17 | .map(u => SQL`(${u}, ${Date.now()})`) 18 | 19 | // SQL.join can combine queries 20 | const insert = SQL`INSERT INTO users (username, create_at) 21 | VALUES ${SQL.join(insertRows)}` 22 | 23 | // You can also nest queries inside each other 24 | const statement = SQL`DO $$ 25 | BEGIN 26 | ${select} 27 | ${insert} 28 | END $$` 29 | 30 | // Or use append to perform a mix of safe and unsafe query building 31 | var updateKey = 'username' 32 | var updateValue = 'emil' 33 | 34 | const update = SQL`UPDATE users SET ` 35 | .append(`${updateKey} = `) // Unsafe! 36 | .append(SQL`${updateValue}`) // Safe 37 | .append(SQL`WHERE username = ${username}::text`) // Also safe 38 | ``` 39 | 40 | ## API 41 | 42 | ### ``var prepared = SQL`statement``` 43 | 44 | ### `prepared.text` 45 | 46 | Returns a prepared query statement in PostgreSQL format (eg. `$1` for placeholders). 47 | Works directly with `pg` 48 | 49 | ```js 50 | const { Client } = require('pg') 51 | const SQL = require('prepare-sql') 52 | const client = new Client() 53 | 54 | client.connect() 55 | 56 | client.query(SQL`SELECT ${'Hello world!'}::text as message`, (err, res) => { 57 | console.log(err ? err.stack : res.rows[0].message) // Hello World! 58 | client.end() 59 | }) 60 | ``` 61 | 62 | ### `prepared.sql` 63 | 64 | Returns a prepared query statement in MySQL format (eg. `?` for placeholders). 65 | Works directly with `mysql`: 66 | 67 | ```js 68 | connection.query( 69 | SQL`SELECT * FROM books WHERE author = ${'David'}`, 70 | function (error, results, fields) { 71 | // error will be an Error if one occurred during the query 72 | // results will contain the results of the query 73 | // fields will contain information about the returned results fields (if any) 74 | }); 75 | ``` 76 | 77 | ### `prepared.values` 78 | 79 | List of extracted values from the template string. Note that nested queries will 80 | be collapsed, eg. 81 | ``SQL`SELECT * FROM users WHERE name = ${'emil'} AND ${SQLusername = ${'emilbayes'}`}`` 82 | will result in `['emil', 'emilbayes']` despite them being nested. 83 | 84 | ### `prepared.append(sqlOrString)` 85 | 86 | Append to the original query. Can be either a string for unsafe query parts eg. 87 | static SQL, or another SQL tagged template. 88 | 89 | ### `SQL.join(queriesArray, [separator = ','])` 90 | 91 | Join together multiple SQL tagged templates. Useful with key-values in `UPDATE` 92 | statements or combining many rows for `INSERT`. Default separator is `','` 93 | (like `Array.prototype.join`), but can be any string. 94 | 95 | ## Install 96 | 97 | ```sh 98 | npm install prepare-sql 99 | ``` 100 | 101 | ## License 102 | 103 | [ISC](LICENSE) 104 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var SQL = require('prepare-sql') 2 | 3 | const username = 'emilbayes' 4 | // Query with template string 5 | const select = SQL`SELECT * FROM users WHERE username = ${username}` 6 | 7 | const insertRows = ['mafintosh', 'chm-diederichs'] 8 | .map(u => SQL`(${u}, ${Date.now()})`) 9 | 10 | // SQL.join can combine queries 11 | const insert = SQL`INSERT INTO users (username, create_at) 12 | VALUES ${SQL.join(insertRows)}` 13 | 14 | // You can also nest queries inside each other 15 | const statement = SQL`DO $$ 16 | BEGIN 17 | ${select} 18 | ${insert} 19 | END $$` 20 | 21 | // Or use append to perform a mix of safe and unsafe query building 22 | var updateKey = 'username' 23 | var updateValue = 'emil' 24 | 25 | const update = SQL`UPDATE users SET ` 26 | .append(`${updateKey} = `) // Unsafe! 27 | .append(SQL`${updateValue}`) // Safe 28 | .append(SQL`WHERE username = ${username}::text`) // Also safe 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const assert = require('nanoassert') 2 | module.exports = SQL 3 | 4 | function SQL (strings, ...values) { 5 | return new Query(strings, values) 6 | } 7 | 8 | SQL.join = function (queries, separator = ', ') { 9 | assert(Array.isArray(queries), 'queries must be an array of SQL tagged templates') 10 | assert(typeof separator === 'string', 'separator must be string') 11 | 12 | var joinedQuery = new Query() 13 | 14 | for (var i = 0; i < queries.length; i++) { 15 | var query = queries[i] 16 | assert(query instanceof Query, 'Can only SQL.join on SQL tagged templates') 17 | var isLast = i === queries.length - 1 18 | var isFirst = i === 0 19 | var prevLength = joinedQuery.pieces.length - 1 20 | 21 | joinedQuery.pieces.push(...query.pieces) 22 | 23 | // Join together last part of previous items, separator and first part of new items 24 | // eg [')', ',', '('] becomes ['),('] 25 | if (isFirst === false) mergeAdjecent(joinedQuery.pieces, prevLength, 1, 1) 26 | // Only add separator between items 27 | if (isLast === false) joinedQuery.pieces.push(separator) 28 | 29 | joinedQuery.values.push(...query.values) 30 | } 31 | 32 | return joinedQuery 33 | } 34 | 35 | class Query { 36 | constructor (pieces = [], values = []) { 37 | // Store the pieces as an array where each "space" represents a placeholder 38 | // for a parameters. This means that manipulation of the query itself 39 | // sometimes invovles fusing query pieces together 40 | this.pieces = Array.from(pieces) 41 | this.values = values 42 | 43 | // To supported nested queries we need to look at all values form the 44 | // template string and find the ones that are Query's themselves and fuse 45 | // them into the parent query and add their values correctly 46 | // 47 | // i tracks the next item to process. This might be a far forward when nested 48 | // values are spliced in 49 | // k tracks the 50 | for (var i = 0, j = 1, k = 0; i < values.length; i++, j++, k++) { 51 | var val = values[i] 52 | if (val instanceof Query) { 53 | // Remove the Query from values 54 | this.values.splice(k, 1) 55 | 56 | // Empty SQL statement 57 | if (val.pieces.length === 0) { 58 | mergeAdjecent(this.pieces, j, 1) 59 | continue 60 | } 61 | 62 | // Add in the text pieces from the query 63 | this.pieces.splice(j, 0, ...val.pieces) 64 | 65 | // Merge front of nested query with adjecent part of parent query 66 | mergeAdjecent(this.pieces, j, 1, 0) 67 | // Merge end of nested query with adjecent part of parent query 68 | mergeAdjecent(this.pieces, j + val.pieces.length - 2, 0, 1) 69 | 70 | // Add in the values 71 | this.values.splice(k, 0, ...val.values) 72 | j += val.pieces.length 73 | k += val.values.length 74 | i += val.values.length 75 | } 76 | } 77 | } 78 | 79 | get text () { 80 | return this._stringify('pg') 81 | } 82 | 83 | get sql () { 84 | return this._stringify('mysql') 85 | } 86 | 87 | _stringify (type = 'pg') { 88 | var text = '' 89 | for (var i = 0; i < this.pieces.length; i++) { 90 | text += this.pieces[i] 91 | if (i === this.pieces.length - 1) break 92 | text += type === 'pg' ? '$' + (i + 1) : '?' 93 | } 94 | 95 | return text 96 | } 97 | 98 | // [Symbol.for('nodejs.util.inspect.custom')] () { 99 | // return { text: this.text, sql: this.sql, values: this.values } 100 | // } 101 | 102 | append (query) { 103 | // if normal string, concat the query onto the last pieces of the existing 104 | // query 105 | if (typeof query === 'string') { 106 | this.pieces[this.pieces.length - 1] += query 107 | return this 108 | } 109 | 110 | assert(query instanceof Query, 'append must be string or SQL tagged template') 111 | this.pieces[this.pieces.length - 1] += query.pieces[0] 112 | var [_, ...pieces] = query.pieces // eslint-disable-line 113 | this.pieces.push(...pieces) 114 | this.values.push(...query.values) 115 | 116 | return this 117 | } 118 | } 119 | 120 | function mergeAdjecent (arr, i, before = 0, after = 0) { 121 | assert(i >= 0 && i < arr.length, 'out of range') 122 | assert(i - before >= 0, 'underflow') 123 | assert(i + after + 1 <= arr.length, 'overflow') 124 | 125 | arr.splice(i - before, before + 1 + after, [...arr.slice(i - before, i), arr[i], ...arr.slice(i + 1, i + 1 + after)].join('')) 126 | return arr 127 | } 128 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prepare-sql", 3 | "version": "1.0.1", 4 | "description": "SQL template strings", 5 | "main": "index.js", 6 | "dependencies": { 7 | "nanoassert": "^2.0.0" 8 | }, 9 | "devDependencies": { 10 | "nyc": "^14.0.0", 11 | "standard": "^12.0.1", 12 | "tape": "^4.8.0" 13 | }, 14 | "scripts": { 15 | "pretest": "standard", 16 | "test": "tape test.js", 17 | "coverage": "nyc npm test" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/hyperdivision/prepare-sql.git" 22 | }, 23 | "keywords": [ 24 | "pg", 25 | "sql", 26 | "postgres", 27 | "postgresql", 28 | "mysql", 29 | "mariadb", 30 | "aurora" 31 | ], 32 | "author": "Emil Bay ", 33 | "license": "ISC", 34 | "bugs": { 35 | "url": "https://github.com/hyperdivision/prepare-sql/issues" 36 | }, 37 | "homepage": "https://github.com/hyperdivision/prepare-sql#readme" 38 | } 39 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const SQL = require('.') 2 | const test = require('tape') 3 | 4 | test('base case', function (assert) { 5 | var q1 = SQL`` 6 | assert.same(q1.text, '') 7 | assert.same(q1.sql, '') 8 | assert.same(q1.values, []) 9 | 10 | var q2 = SQL`SELECT *` 11 | assert.same(q2.text, 'SELECT *') 12 | assert.same(q2.sql, 'SELECT *') 13 | assert.same(q2.values, []) 14 | 15 | var q3 = SQL`UPDATE t SET v = ${'k'}` 16 | assert.same(q3.text, 'UPDATE t SET v = $1') 17 | assert.same(q3.sql, 'UPDATE t SET v = ?') 18 | assert.same(q3.values, ['k']) 19 | 20 | assert.end() 21 | }) 22 | 23 | test('nested statements', function (assert) { 24 | var q1 = SQL`${SQL``}` 25 | assert.same(q1.text, '') 26 | assert.same(q1.sql, '') 27 | assert.same(q1.values, []) 28 | 29 | var q2 = SQL`SELECT ${SQL`*`}` 30 | assert.same(q2.text, 'SELECT *') 31 | assert.same(q2.sql, 'SELECT *') 32 | assert.same(q2.values, []) 33 | 34 | var q3 = SQL`UPDATE ${SQL`t SET v = ${'k'}`}` 35 | assert.same(q3.text, 'UPDATE t SET v = $1') 36 | assert.same(q3.sql, 'UPDATE t SET v = ?') 37 | assert.same(q3.values, ['k']) 38 | 39 | var q4 = SQL`INSERT INTO t VALUES ${SQL`(a, ${'b'}, ${'c'})`}` 40 | assert.same(q4.text, 'INSERT INTO t VALUES (a, $1, $2)') 41 | assert.same(q4.sql, 'INSERT INTO t VALUES (a, ?, ?)') 42 | assert.same(q4.values, ['b', 'c']) 43 | assert.end() 44 | }) 45 | 46 | test.only('join', function (assert) { 47 | var q0 = SQL`${SQL.join([])}` 48 | assert.same(q0.text, '') 49 | assert.same(q0.sql, '') 50 | assert.same(q0.values, []) 51 | 52 | var q00 = SQL`${SQL.join([SQL``])}` 53 | assert.same(q00.text, '') 54 | assert.same(q00.sql, '') 55 | assert.same(q00.values, []) 56 | 57 | var q1 = SQL`UPDATE t SET ${SQL.join([SQL`a = b`, SQL`k = ${'v'}`])}` 58 | assert.same(q1.text, 'UPDATE t SET a = b, k = $1') 59 | assert.same(q1.sql, 'UPDATE t SET a = b, k = ?') 60 | assert.same(q1.values, ['v']) 61 | 62 | var q2 = SQL`INSERT INTO t VALUES ${SQL.join([SQL`(a, ${'b'}, ${'c'})`])}` 63 | assert.same(q2.text, 'INSERT INTO t VALUES (a, $1, $2)') 64 | assert.same(q2.sql, 'INSERT INTO t VALUES (a, ?, ?)') 65 | assert.same(q2.values, ['b', 'c']) 66 | 67 | var q3 = SQL`INSERT INTO t VALUES ${SQL.join([SQL`(a, ${'b'}, ${'c'})`, SQL`(d, ${'e'}, ${'f'})`])}` 68 | assert.same(q3.text, 'INSERT INTO t VALUES (a, $1, $2), (d, $3, $4)') 69 | assert.same(q3.sql, 'INSERT INTO t VALUES (a, ?, ?), (d, ?, ?)') 70 | assert.same(q3.values, ['b', 'c', 'e', 'f']) 71 | assert.end() 72 | }) 73 | 74 | test('append', function (assert) { 75 | var query = SQL`INSERT INTO t` 76 | .append(' VALUES ') // append string 77 | .append(`(a, ${'b'}, ${'c'}), `) // append untagget template string 78 | .append(SQL`(a, ${'b'}, ${'c'})`) // append tagged template string 79 | 80 | assert.same(query.text, 'INSERT INTO t VALUES (a, b, c), (a, $1, $2)') 81 | assert.same(query.sql, 'INSERT INTO t VALUES (a, b, c), (a, ?, ?)') 82 | assert.same(query.values, ['b', 'c']) 83 | assert.end() 84 | }) 85 | --------------------------------------------------------------------------------