├── .gitignore ├── .prettierignore ├── renovate.json ├── .prettierrc ├── .editorconfig ├── .eslintrc.json ├── typings.json ├── codecov.yml ├── test ├── integration │ ├── mysql.js │ ├── sequelize.js │ ├── pg.js │ └── mysql2.js └── unit.js ├── LICENSE.txt ├── .travis.yml ├── package.json ├── index.d.ts ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | typings 3 | typedoc 4 | coverage 5 | .nyc_output 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | package.json 4 | package-lock.json 5 | typedoc/ 6 | *.md 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "rebaseStalePrs": true, 4 | "pinVersions": false 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "bracketSpacing": true, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "semi": false, 7 | "trailingComma": "es5", 8 | "proseWrap": "preserve" 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | [*] 3 | end_of_line = lf 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 8 | 9 | [*.md] 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true, 5 | "mocha": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 6, 9 | "sourceType": "module" 10 | }, 11 | "extends": ["eslint:recommended", "eslint-config-prettier"] 12 | } 13 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-template-strings", 3 | "version": "2.0.0", 4 | "main": "typings.d.ts", 5 | "dependencies": {}, 6 | "ambientDevDependencies": { 7 | "mocha": "registry:dt/mocha#2.2.5+20160317120654", 8 | "node": "registry:dt/node#4.0.0+20160501135006" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | 2 | coverage: 3 | status: 4 | project: 5 | default: 6 | target: auto 7 | threshold: null 8 | base: auto 9 | 10 | comment: 11 | layout: "header, diff, tree, changes" 12 | behavior: default 13 | require_changes: false # if true: only post the comment if coverage changes 14 | branches: null 15 | flags: null 16 | paths: null 17 | -------------------------------------------------------------------------------- /test/integration/mysql.js: -------------------------------------------------------------------------------- 1 | const SQL = require('../..') 2 | const mysql = require('mysql') 3 | const assert = require('assert') 4 | 5 | describe('mysql', function() { 6 | this.timeout(10000) 7 | for (const test of ['Connection', 'Pool']) { 8 | describe(test, () => { 9 | it('should work with a simple query', done => { 10 | const connection = mysql['create' + test](process.env.MYSQL_CONN) 11 | connection.query(SQL`SELECT ${1} + 1 AS result`, (err, rows) => { 12 | if (err) { 13 | return done(err) 14 | } 15 | assert.equal(rows[0].result, 2) 16 | done() 17 | }) 18 | }) 19 | }) 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2016, Felix Frederick Becker 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /test/integration/sequelize.js: -------------------------------------------------------------------------------- 1 | const SQL = require('../..') 2 | const Sequelize = require('sequelize') 3 | const assert = require('assert') 4 | 5 | describe('sequelize', function() { 6 | this.timeout(10000) 7 | it('should work with a simple query', () => { 8 | const sequelize = new Sequelize(process.env.PG_CONN, { logging: false }) 9 | return sequelize.query(SQL`SELECT ${1} + 1 as result`, { type: Sequelize.QueryTypes.SELECT }).then(rows => { 10 | assert.equal(rows[0].result, 2) 11 | }) 12 | }) 13 | it('should work with a bound statement', () => { 14 | const sequelize = new Sequelize(process.env.PG_CONN, { logging: false }) 15 | return sequelize 16 | .query(SQL`SELECT ${1} + 1 as result`.useBind(true), { type: Sequelize.QueryTypes.SELECT }) 17 | .then(rows => { 18 | assert.equal(rows[0].result, 2) 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/integration/pg.js: -------------------------------------------------------------------------------- 1 | const SQL = require('../..') 2 | const pg = require('pg') 3 | const assert = require('assert') 4 | 5 | describe('pg', function() { 6 | this.timeout(10000) 7 | it('should work with a simple query', done => { 8 | const client = new pg.Client(process.env.PG_CONN) 9 | client.connect(err => { 10 | if (err) { 11 | return done(err) 12 | } 13 | client.query(SQL`SELECT ${1} + 1 as result`, (err, result) => { 14 | if (err) { 15 | return done(err) 16 | } 17 | assert.equal(result.rows[0].result, 2) 18 | done() 19 | }) 20 | }) 21 | }) 22 | it('should work with a named statement', done => { 23 | const client = new pg.Client(process.env.PG_CONN) 24 | client.connect(err => { 25 | if (err) { 26 | return done(err) 27 | } 28 | client.query(SQL`SELECT ${1} + 1 as result`.setName('my_query'), (err, result) => { 29 | if (err) { 30 | return done(err) 31 | } 32 | assert.equal(result.rows[0].result, 2) 33 | done() 34 | }) 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '8' 5 | - '6' 6 | 7 | cache: 8 | directories: 9 | - ~/.npmrc 10 | 11 | env: 12 | global: 13 | - MYSQL_CONN=mysql://root@localhost/ 14 | - PG_CONN=postgres://postgres@localhost/postgres 15 | 16 | services: 17 | - mysql 18 | - postgresql 19 | 20 | before_install: 21 | - npm install -g npm@6.0.0 22 | 23 | install: 24 | - npm ci 25 | 26 | script: 27 | - npm run lint 28 | - npm run cover 29 | - nyc report --reporter=json 30 | - bash <(curl -s https://codecov.io/bash) -f coverage/coverage-final.json 31 | 32 | jobs: 33 | include: 34 | - stage: release 35 | node_js: '8' 36 | services: [] 37 | script: 38 | - npm run semantic-release 39 | before_deploy: 40 | - npm run typedoc 41 | deploy: 42 | skip_cleanup: true 43 | provider: surge 44 | project: ./typedoc/ 45 | domain: node-sql-template-strings.surge.sh 46 | 47 | stages: 48 | - test 49 | - name: release 50 | if: branch = master AND type = push AND fork = false 51 | 52 | branches: 53 | only: 54 | - master 55 | -------------------------------------------------------------------------------- /test/integration/mysql2.js: -------------------------------------------------------------------------------- 1 | const SQL = require('../..') 2 | const mysql2 = require('mysql2') 3 | const mysql2Promise = require('mysql2/promise') 4 | const assert = require('assert') 5 | 6 | describe('mysql2', function() { 7 | this.timeout(10000) 8 | for (const test of ['Connection', 'Pool']) { 9 | describe(test, () => { 10 | for (const method of ['query', 'execute']) { 11 | describe(method, () => { 12 | it('should work with a simple query', done => { 13 | const connection = mysql2['create' + test](process.env.MYSQL_CONN) 14 | connection[method](SQL`SELECT ${1} + 1 AS result`, (err, rows) => { 15 | if (err) { 16 | return done(err) 17 | } 18 | assert.equal(rows[0].result, 2) 19 | done() 20 | }) 21 | }) 22 | }) 23 | } 24 | }) 25 | } 26 | }) 27 | 28 | describe('mysql2/promise', () => { 29 | for (const test of ['Connection', 'Pool']) { 30 | describe(test, () => { 31 | for (const method of ['query', 'execute']) { 32 | describe(method, () => { 33 | it('should work with a simple query', () => { 34 | return Promise.resolve(mysql2Promise['create' + test](process.env.MYSQL_CONN)) 35 | .then(connection => { 36 | return connection[method](SQL`SELECT ${1} + 1 AS result`) 37 | }) 38 | .then(rowsAndFields => { 39 | assert.equal(rowsAndFields[0][0].result, 2) 40 | }) 41 | }) 42 | }) 43 | } 44 | }) 45 | } 46 | }) 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-template-strings", 3 | "version": "2.2.2", 4 | "description": "ES6 tagged template strings for prepared statements with mysql and postgres", 5 | "main": "index.js", 6 | "files": [ 7 | "index.d.ts", 8 | "index.js", 9 | "LICENSE.txt", 10 | "README.md" 11 | ], 12 | "typings": "index.d.ts", 13 | "engines": { 14 | "node": ">=4.0.0" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/felixfbecker/node-sql-template-strings" 19 | }, 20 | "scripts": { 21 | "lint": "eslint index.js test && npm run prettier", 22 | "prettier": "prettier --write --list-different '**/*.{js?(on),.d.ts}'", 23 | "test": "mocha \"test/**/*.js\" --exit", 24 | "cover": "nyc --all mocha \"test/**/*.js\" --exit", 25 | "typedoc": "typedoc --module es2015 --target es2015 --includeDeclarations --excludeExternals --mode file --readme none --out typedoc index.d.ts", 26 | "semantic-release": "semantic-release", 27 | "commitmsg": "validate-commit-msg" 28 | }, 29 | "keywords": [ 30 | "mysql", 31 | "mysql2", 32 | "postgres", 33 | "pg", 34 | "prepared", 35 | "statements", 36 | "placeholder", 37 | "es6", 38 | "tagged", 39 | "template", 40 | "strings" 41 | ], 42 | "author": "Felix Becker", 43 | "license": "ISC", 44 | "devDependencies": { 45 | "cz-conventional-changelog": "^2.0.0", 46 | "eslint": "^4.1.0", 47 | "eslint-config-prettier": "^2.8.0", 48 | "husky": "^0.14.1", 49 | "mocha": "^5.0.0", 50 | "mysql": "^2.12.0", 51 | "mysql2": "^1.1.2", 52 | "nyc": "^12.0.0", 53 | "pg": "^7.1.0", 54 | "prettier": "1.13.3", 55 | "semantic-release": "^15.0.0", 56 | "sequelize": "^4.0.0", 57 | "typedoc": "^0.11.0", 58 | "validate-commit-msg": "^2.12.2" 59 | }, 60 | "nyc": { 61 | "include": [ 62 | "index.js" 63 | ], 64 | "exclude": [ 65 | "test/**/*.js" 66 | ] 67 | }, 68 | "config": { 69 | "commitizen": { 70 | "path": "./node_modules/cz-conventional-changelog" 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export class SQLStatement { 2 | private strings: string[] 3 | 4 | /** 5 | * The SQL Statement for [node-postgres](https://www.npmjs.com/package/pg) 6 | */ 7 | text: string 8 | 9 | /** 10 | * The SQL Statement for [Sequelize](https://www.npmjs.com/package/sequelize) 11 | */ 12 | query: string 13 | 14 | /** 15 | * The SQL Statement for [mysql](https://www.npmjs.com/package/mysql) 16 | */ 17 | sql: string 18 | 19 | /** 20 | * The values to be inserted for the placeholders 21 | */ 22 | values: any[] 23 | 24 | /** 25 | * The name for postgres prepared statements, if set 26 | */ 27 | name: string 28 | 29 | /** 30 | * Appends a string or another statement 31 | * 32 | * ```ts 33 | * query.append(SQL`AND genre = ${genre}`).append(' ORDER BY rating') 34 | * query.text // => 'SELECT author FROM books WHERE name = $1 AND author = $2 AND genre = $3 ORDER BY rating' 35 | * query.sql // => 'SELECT author FROM books WHERE name = ? AND author = ? AND genre = ? ORDER BY rating' 36 | * query.values // => ['harry potter', 'J. K. Rowling', 'Fantasy'] ORDER BY rating` 37 | * 38 | * const query = SQL`SELECT * FROM books` 39 | * if (params.name) { 40 | * query.append(SQL` WHERE name = ${params.name}`) 41 | * } 42 | * query.append(SQL` LIMIT 10 OFFSET ${params.offset || 0}`) 43 | * ``` 44 | */ 45 | append(statement: SQLStatement | string | number): this 46 | 47 | /** 48 | * Sets the name property of this statement for prepared statements in postgres 49 | * 50 | * ```ts 51 | * pg.query(SQL`SELECT author FROM books WHERE name = ${book}`.setName('my_query')) 52 | * ``` 53 | */ 54 | setName(name: string): this 55 | 56 | /** 57 | * Use a prepared statement with Sequelize. 58 | * Makes `query` return a query with `$n` syntax instead of `?` and switches the `values` key name to `bind` 59 | * If omitted, `value` defaults to `true`. 60 | */ 61 | useBind(value?: boolean): this 62 | } 63 | 64 | /** 65 | * The template string tag 66 | * 67 | * ```ts 68 | * import {SQL} from 'sql-template-strings'; 69 | * 70 | * pg.query(SQL`SELECT author FROM books WHERE name = ${book} AND author = ${author}`) 71 | * ``` 72 | */ 73 | export function SQL(strings: any, ...values: any[]): SQLStatement 74 | export default SQL 75 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class SQLStatement { 4 | /** 5 | * @param {string[]} strings 6 | * @param {any[]} values 7 | */ 8 | constructor(strings, values) { 9 | this.strings = strings 10 | this.values = values 11 | } 12 | 13 | /** Returns the SQL Statement for Sequelize */ 14 | get query() { 15 | return this.bind ? this.text : this.sql 16 | } 17 | 18 | /** Returns the SQL Statement for node-postgres */ 19 | get text() { 20 | return this.strings.reduce((prev, curr, i) => prev + '$' + i + curr) 21 | } 22 | 23 | /** 24 | * @param {SQLStatement|string} statement 25 | * @returns {this} 26 | */ 27 | append(statement) { 28 | if (statement instanceof SQLStatement) { 29 | this.strings[this.strings.length - 1] += statement.strings[0] 30 | this.strings.push.apply(this.strings, statement.strings.slice(1)) 31 | const list = this.values || this.bind 32 | list.push.apply(list, statement.values) 33 | } else { 34 | this.strings[this.strings.length - 1] += statement 35 | } 36 | return this 37 | } 38 | 39 | /** 40 | * Use a prepared statement with Sequelize. 41 | * Makes `query` return a query with `$n` syntax instead of `?` and switches the `values` key name to `bind` 42 | * @param {boolean} [value=true] value If omitted, defaults to `true` 43 | * @returns this 44 | */ 45 | useBind(value) { 46 | if (value === undefined) { 47 | value = true 48 | } 49 | if (value && !this.bind) { 50 | this.bind = this.values 51 | delete this.values 52 | } else if (!value && this.bind) { 53 | this.values = this.bind 54 | delete this.bind 55 | } 56 | return this 57 | } 58 | 59 | /** 60 | * @param {string} name 61 | * @returns {this} 62 | */ 63 | setName(name) { 64 | this.name = name 65 | return this 66 | } 67 | } 68 | 69 | /** Returns the SQL Statement for mysql */ 70 | Object.defineProperty(SQLStatement.prototype, 'sql', { 71 | enumerable: true, 72 | get() { 73 | return this.strings.join('?') 74 | }, 75 | }) 76 | 77 | /** 78 | * @param {string[]} strings 79 | * @param {...any} values 80 | * @returns {SQLStatement} 81 | */ 82 | function SQL(strings) { 83 | return new SQLStatement(strings.slice(0), Array.from(arguments).slice(1)) 84 | } 85 | 86 | module.exports = SQL 87 | module.exports.SQL = SQL 88 | module.exports.default = SQL 89 | module.exports.SQLStatement = SQLStatement 90 | -------------------------------------------------------------------------------- /test/unit.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | let assert = require('assert') 3 | let SQL = require('..') 4 | 5 | describe('SQL', () => { 6 | it('should work with a simple query', () => { 7 | const query = SQL`SELECT * FROM table` 8 | assert.equal(query.sql, 'SELECT * FROM table') 9 | assert.equal(query.text, 'SELECT * FROM table') 10 | assert.equal(query.query, 'SELECT * FROM table') 11 | assert.deepEqual(query.values, []) 12 | }) 13 | 14 | it('should work with a query with values', () => { 15 | const value = 1234 16 | const query = SQL`SELECT * FROM table WHERE column = ${value}` 17 | assert.equal(query.sql, 'SELECT * FROM table WHERE column = ?') 18 | assert.equal(query.query, 'SELECT * FROM table WHERE column = ?') 19 | assert.equal(query.text, 'SELECT * FROM table WHERE column = $1') 20 | assert.deepEqual(query.values, [value]) 21 | }) 22 | 23 | it('should work with falsy values', () => { 24 | const value1 = false 25 | const value2 = null 26 | const query = SQL`SELECT * FROM table WHERE column1 = ${value1} AND column2 = ${value2}` 27 | assert.equal(query.sql, 'SELECT * FROM table WHERE column1 = ? AND column2 = ?') 28 | assert.equal(query.query, 'SELECT * FROM table WHERE column1 = ? AND column2 = ?') 29 | assert.equal(query.text, 'SELECT * FROM table WHERE column1 = $1 AND column2 = $2') 30 | assert.deepEqual(query.values, [value1, value2]) 31 | }) 32 | 33 | it('should expose "sql" as an enumerable property', () => { 34 | const query = SQL`SELECT * FROM table` 35 | for (const key in query) { 36 | if (key === 'sql') { 37 | return 38 | } 39 | } 40 | throw new assert.AssertionError({ message: 'expected enumerable property "sql"' }) 41 | }) 42 | 43 | describe('append()', () => { 44 | it('should return this', () => { 45 | const query = SQL`SELECT * FROM table` 46 | assert.strictEqual(query, query.append('whatever')) 47 | }) 48 | 49 | it('should append a second SQLStatement', () => { 50 | const value1 = 1234 51 | const value2 = 5678 52 | const query = SQL`SELECT * FROM table WHERE column = ${value1}`.append(SQL` AND other_column = ${value2}`) 53 | assert.equal(query.sql, 'SELECT * FROM table WHERE column = ? AND other_column = ?') 54 | assert.equal(query.text, 'SELECT * FROM table WHERE column = $1 AND other_column = $2') 55 | assert.deepEqual(query.values, [value1, value2]) 56 | }) 57 | 58 | it('should append a string', () => { 59 | const value = 1234 60 | const query = SQL`SELECT * FROM table WHERE column = ${value}`.append(' ORDER BY other_column') 61 | assert.equal(query.sql, 'SELECT * FROM table WHERE column = ? ORDER BY other_column') 62 | assert.equal(query.text, 'SELECT * FROM table WHERE column = $1 ORDER BY other_column') 63 | assert.deepEqual(query.values, [value]) 64 | }) 65 | 66 | it('should work with a bound statement', () => { 67 | const value = 1234 68 | const statement = SQL`SELECT * FROM table WHERE column = ${value}`.useBind(true).append(' ORDER BY other_column') 69 | assert.equal(statement.sql, 'SELECT * FROM table WHERE column = ? ORDER BY other_column') 70 | assert.equal(statement.text, 'SELECT * FROM table WHERE column = $1 ORDER BY other_column') 71 | assert.strictEqual(statement.query, 'SELECT * FROM table WHERE column = $1 ORDER BY other_column') 72 | assert.strictEqual(statement.values, undefined) 73 | assert.strictEqual('values' in statement, false) 74 | assert.deepStrictEqual(statement.bind, [1234]) 75 | }) 76 | }) 77 | 78 | describe('setName()', () => { 79 | it('should set the name and return this', () => { 80 | assert.equal(SQL`SELECT * FROM table`.setName('my_query').name, 'my_query') 81 | }) 82 | }) 83 | 84 | describe('useBind()', () => { 85 | it('should change query to $n syntax and swap values with bind', () => { 86 | const value = 123 87 | const statement = SQL`SELECT * FROM table WHERE column = ${value}`.useBind(true) 88 | assert.strictEqual(statement.query, 'SELECT * FROM table WHERE column = $1') 89 | assert.strictEqual(statement.values, undefined) 90 | assert.strictEqual('values' in statement, false) 91 | assert.deepStrictEqual(statement.bind, [123]) 92 | }) 93 | 94 | it('should allow to omit the parameter', () => { 95 | const value = 123 96 | const statement = SQL`SELECT * FROM table WHERE column = ${value}`.useBind() 97 | assert.strictEqual(statement.query, 'SELECT * FROM table WHERE column = $1') 98 | assert.strictEqual(statement.values, undefined) 99 | assert.strictEqual('values' in statement, false) 100 | assert.deepStrictEqual(statement.bind, [123]) 101 | }) 102 | 103 | it('should be idempotent', () => { 104 | const value = 123 105 | const statement = SQL`SELECT * FROM table WHERE column = ${value}`.useBind(true).useBind(true) 106 | assert.strictEqual(statement.query, 'SELECT * FROM table WHERE column = $1') 107 | assert.strictEqual(statement.values, undefined) 108 | assert.strictEqual('values' in statement, false) 109 | assert.deepStrictEqual(statement.bind, [123]) 110 | }) 111 | 112 | it('should be reversable', () => { 113 | const value = 123 114 | const statement = SQL`SELECT * FROM table WHERE column = ${value}`.useBind(true).useBind(false) 115 | assert.strictEqual(statement.query, 'SELECT * FROM table WHERE column = ?') 116 | assert.strictEqual(statement.bind, undefined) 117 | assert.strictEqual('bind' in statement, false) 118 | assert.deepStrictEqual(statement.values, [123]) 119 | }) 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQL Template Strings 2 | 3 | [![npm](https://img.shields.io/npm/v/sql-template-strings.svg?maxAge=2592000)](https://www.npmjs.com/package/sql-template-strings) 4 | [![downloads](https://img.shields.io/npm/dm/sql-template-strings.svg?maxAge=2592000)](https://www.npmjs.com/package/sql-template-strings) 5 | [![build](https://travis-ci.org/felixfbecker/node-sql-template-strings.svg?branch=master)](https://travis-ci.org/felixfbecker/node-sql-template-strings) 6 | [![codecov](https://codecov.io/gh/felixfbecker/node-sql-template-strings/branch/master/graph/badge.svg)](https://codecov.io/gh/felixfbecker/node-sql-template-strings) 7 | [![dependencies](https://david-dm.org/felixfbecker/node-sql-template-strings.svg)](https://david-dm.org/felixfbecker/node-sql-template-strings) 8 | ![node](http://img.shields.io/node/v/sql-template-strings.svg) 9 | [![license](https://img.shields.io/npm/l/sql-template-strings.svg?maxAge=2592000)](https://github.com/felixfbecker/node-sql-template-strings/blob/master/LICENSE.txt) 10 | [![chat: on gitter](https://badges.gitter.im/felixfbecker/node-sql-template-strings.svg)](https://gitter.im/felixfbecker/node-sql-template-strings?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 11 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 12 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 13 | 14 | [API Documentation](http://node-sql-template-strings.surge.sh/) 15 | 16 | A simple yet powerful module to allow you to use ES6 tagged template strings for prepared/escaped statements. 17 | Works with [mysql](https://www.npmjs.com/package/mysql), [mysql2](https://www.npmjs.com/package/mysql2), [postgres](https://www.npmjs.com/package/pg) and [sequelize](https://www.npmjs.com/package/sequelize). 18 | 19 | Example for escaping queries (callbacks omitted): 20 | 21 | ```js 22 | const SQL = require('sql-template-strings') 23 | 24 | const book = 'harry potter' 25 | const author = 'J. K. Rowling' 26 | 27 | // mysql: 28 | mysql.query('SELECT author FROM books WHERE name = ? AND author = ?', [book, author]) 29 | // is equivalent to 30 | mysql.query(SQL`SELECT author FROM books WHERE name = ${book} AND author = ${author}`) 31 | 32 | // postgres: 33 | pg.query('SELECT author FROM books WHERE name = $1 AND author = $2', [book, author]) 34 | // is equivalent to 35 | pg.query(SQL`SELECT author FROM books WHERE name = ${book} AND author = ${author}`) 36 | 37 | // sequelize: 38 | sequelize.query('SELECT author FROM books WHERE name = ? AND author = ?', {replacements: [book, author]}) 39 | // is equivalent to 40 | sequelize.query(SQL`SELECT author FROM books WHERE name = ${book} AND author = ${author}`) 41 | ``` 42 | 43 | This might not seem like a big deal, but when you do an INSERT with a lot columns writing all the placeholders becomes a nightmare: 44 | 45 | ```js 46 | db.query( 47 | 'INSERT INTO books (name, author, isbn, category, recommended_age, pages, price) VALUES (?, ?, ?, ?, ?, ?, ?)', 48 | [name, author, isbn, category, recommendedAge, pages, price] 49 | ) 50 | // is better written as 51 | db.query(SQL` 52 | INSERT 53 | INTO books 54 | (name, author, isbn, category, recommended_age, pages, price) 55 | VALUES (${name}, ${author}, ${isbn}, ${category}, ${recommendedAge}, ${pages}, ${price}) 56 | `) 57 | ``` 58 | Also template strings support line breaks, while normal strings do not. 59 | 60 | ## How it works 61 | The SQL template string tag transforms the template string and returns an _object_ that is understood by both mysql and postgres: 62 | 63 | ```js 64 | const query = SQL`SELECT author FROM books WHERE name = ${book} AND author = ${author}` 65 | typeof query // => 'object' 66 | query.text // => 'SELECT author FROM books WHERE name = $1 AND author = $2' 67 | query.sql // => 'SELECT author FROM books WHERE name = ? AND author = ?' 68 | query.values // => ['harry potter', 'J. K. Rowling'] 69 | ``` 70 | 71 | ## Building complex queries with `append()` 72 | It is also possible to build queries by appending another query or a string with the `append()` method (returns `this` for chaining): 73 | 74 | ```js 75 | query.append(SQL`AND genre = ${genre}`).append(' ORDER BY rating') 76 | query.text // => 'SELECT author FROM books WHERE name = $1 AND author = $2 AND genre = $3 ORDER BY rating' 77 | query.sql // => 'SELECT author FROM books WHERE name = ? AND author = ? AND genre = ? ORDER BY rating' 78 | query.values // => ['harry potter', 'J. K. Rowling', 'Fantasy'] ORDER BY rating 79 | ``` 80 | 81 | This allows you to build complex queries without having to care about the placeholder index or the values array: 82 | 83 | ```js 84 | const query = SQL`SELECT * FROM books` 85 | if (params.name) { 86 | query.append(SQL` WHERE name = ${params.name}`) 87 | } 88 | query.append(SQL` LIMIT 10 OFFSET ${params.offset || 0}`) 89 | ``` 90 | 91 | ## Raw values 92 | Some values cannot be replaced by placeholders in prepared statements, like table names. 93 | `append()` replaces the `SQL.raw()` syntax from version 1, just pass a string and it will get appended raw. 94 | 95 | > Please note that when inserting raw values, you are responsible for quoting and escaping these values with proper escaping functions first if they come from user input (E.g. `mysql.escapeId()` and `pg.escapeIdentifier()`). 96 | > Also, executing many prepared statements with changing raw values in a loop will quickly overflow the prepared statement buffer (and destroy their performance benefit), so be careful. 97 | 98 | ```js 99 | const table = 'books' 100 | const order = 'DESC' 101 | const column = 'author' 102 | 103 | db.query(SQL`SELECT * FROM "`.append(table).append(SQL`" WHERE author = ${author} ORDER BY ${column} `).append(order)) 104 | 105 | // escape user input manually 106 | mysql.query(SQL`SELECT * FROM `.append(mysql.escapeId(someUserInput)).append(SQL` WHERE name = ${book} ORDER BY ${column} `).append(order)) 107 | pg.query(SQL`SELECT * FROM `.append(pg.escapeIdentifier(someUserInput)).append(SQL` WHERE name = ${book} ORDER BY ${column} `).append(order)) 108 | ``` 109 | 110 | ## Binding Arrays 111 | 112 | To bind the array dynamically as a parameter use ANY (PostgreSQL only): 113 | ```js 114 | const authors = ['J. K. Rowling', 'J. R. R. Tolkien'] 115 | const query = SQL`SELECT name FROM books WHERE author = ANY(${authors})` 116 | query.text // => 'SELECT name FROM books WHERE author = ANY($1)' 117 | query.values // => ['J. K. Rowling', 'J. R. R. Tolkien'] 118 | ``` 119 | 120 | ## Named Prepared Statements in Postgres 121 | Postgres has the option of naming prepared statements, which allows parsing and other work to be reused (and requires the SQL associated with the name to stay the same, with only the parameters changing). 122 | You can set the name with the `setName()` method: 123 | 124 | ```js 125 | // old way 126 | pg.query({name: 'my_query', text: 'SELECT author FROM books WHERE name = $1', values: [book]}) 127 | 128 | // with template strings 129 | pg.query(SQL`SELECT author FROM books WHERE name = ${book}`.setName('my_query')) 130 | ``` 131 | You can also set the name property on the statement object directly or use `Object.assign()`. 132 | 133 | ## Bound Statements in sequelize 134 | By default, Sequelize will escape replacements on the client. 135 | To switch to using a bound statement in Sequelize, call `useBind()`. 136 | The boolean parameter defaults to `true`. 137 | Like all methods, returns `this` for chaining. 138 | Please note that as long as the bound mode is active, the statement object only supports Sequelize, not the other drivers. 139 | 140 | ```js 141 | // old way 142 | sequelize.query('SELECT author FROM books WHERE name = ? AND author = ?', {bind: [book, author]}) 143 | 144 | // with template strings 145 | sequelize.query(SQL`SELECT author FROM books WHERE name = ${book}`.useBind(true)) 146 | sequelize.query(SQL`SELECT author FROM books WHERE name = ${book}`.useBind()) // the same 147 | 148 | // works with append (you can call useBind at any time) 149 | const query = SQL`SELECT * FROM books`.useBind(true) 150 | if (params.name) { 151 | query.append(SQL` WHERE name = ${params.name}`) 152 | } 153 | query.append(SQL` LIMIT 10 OFFSET ${params.offset || 0}`) 154 | ``` 155 | 156 | ## Editor Integration 157 | 158 | - **Sublime Text**: [javascript-sql-sublime-syntax](https://github.com/AsterisqueDigital/javascript-sql-sublime-syntax) 159 | - **Vim**: [vim-javascript-sql](https://github.com/statico/vim-javascript-sql) 160 | 161 | ## Contributing 162 | - Tests are written using [mocha](https://www.npmjs.com/package/mocha) 163 | - You can use `npm test` to run the tests and check coding style 164 | - Since this module is only compatible with ES6 versions of node anyway, use all the ES6 goodies 165 | - Pull requests are welcome :) 166 | --------------------------------------------------------------------------------