├── .nvmrc ├── .gitignore ├── =11.5.1 ├── .prettierrc ├── .github ├── dependabot.yml └── workflows │ ├── notify-release.yml │ ├── check-linked-issues.yml │ ├── release.yml │ └── ci.yml ├── docker-compose.yml ├── sqlmap ├── config.js ├── table.js ├── server.js ├── injection-endpoints.json ├── users.js ├── db-init.js └── sqlmap.js ├── quoteIdentifier.js ├── LICENSE ├── benchmark ├── README.md └── index.js ├── SQL.test-d.ts ├── quoteIdentifier.test.js ├── package.json ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── SQL.d.ts ├── SQL.js ├── README.md └── SQL.test.js /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | .DS_Store 4 | 5 | *.log 6 | .nyc_output 7 | 8 | node_modules 9 | 10 | package-lock.json 11 | -------------------------------------------------------------------------------- /=11.5.1: -------------------------------------------------------------------------------- 1 | 2 | removed 9 packages, and changed 42 packages in 9s 3 | 4 | 25 packages are looking for funding 5 | run `npm fund` for details 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "arrowParens": "avoid", 5 | "trailingComma": "none", 6 | "bracketSpacing": true 7 | } 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | services: 3 | db: 4 | image: postgres:9-alpine 5 | environment: 6 | POSTGRES_PASSWORD: postgres 7 | POSTGRES_USER: postgres 8 | POSTGRES_DB: sqlmap 9 | ports: 10 | - 5432:5432 11 | -------------------------------------------------------------------------------- /sqlmap/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | user: process.env.PGUSER || 'postgres', 3 | host: process.env.PGHOST || 'localhost', 4 | database: process.env.PGDB || 'sqlmap', 5 | password: process.env.PGPASS || '', 6 | port: process.env.PGPORT || 5432 7 | } 8 | -------------------------------------------------------------------------------- /quoteIdentifier.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function quoteIdentifier (value, type) { 4 | const quote = type === 'mysql' ? '`' : '"' 5 | 6 | const quoted = value.replace(new RegExp(quote, 'g'), `${quote}${quote}`) 7 | 8 | return `${quote}${quoted}${quote}` 9 | } 10 | 11 | module.exports = quoteIdentifier 12 | -------------------------------------------------------------------------------- /.github/workflows/notify-release.yml: -------------------------------------------------------------------------------- 1 | name: notify-release 2 | 'on': 3 | workflow_dispatch: 4 | schedule: 5 | - cron: 30 8 * * * 6 | release: 7 | types: 8 | - published 9 | issues: 10 | types: 11 | - closed 12 | jobs: 13 | run: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | issues: write 17 | contents: read 18 | steps: 19 | - name: Notify release 20 | uses: nearform-actions/github-action-notify-release@v1 21 | -------------------------------------------------------------------------------- /sqlmap/table.js: -------------------------------------------------------------------------------- 1 | const SQL = require('../SQL') 2 | const quoteIdent = SQL.quoteIdent 3 | 4 | module.exports = async function (fastify) { 5 | fastify.post('/table', async request => { 6 | const { tableName, field1, field2 } = request.body 7 | 8 | await fastify.pg.query( 9 | SQL`CREATE TABLE ${quoteIdent(tableName)} ( 10 | ${quoteIdent(field1)} SERIAL PRIMARY KEY, 11 | ${quoteIdent(field2)} VARCHAR (30) 12 | )` 13 | ) 14 | 15 | return fastify.pg.query(SQL`DROP TABLE ${quoteIdent(tableName)} `) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 nearForm 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /.github/workflows/check-linked-issues.yml: -------------------------------------------------------------------------------- 1 | name: Check Linked Issues 2 | 'on': 3 | pull_request_target: 4 | types: 5 | - opened 6 | - edited 7 | - reopened 8 | - synchronize 9 | jobs: 10 | check_pull_requests: 11 | runs-on: ubuntu-latest 12 | name: Check linked issues 13 | permissions: 14 | issues: read 15 | pull-requests: write 16 | steps: 17 | - uses: nearform-actions/github-action-check-linked-issues@v1 18 | with: 19 | github-token: ${{ secrets.GITHUB_TOKEN }} 20 | exclude-branches: release/**, dependabot/** 21 | -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | # Benchmark 2 | We're using https://www.npmjs.com/package/benchmark module to do the benchmarking. Below you can see results and how to run it locally 3 | 4 | You can check the code in `index.js` file. We're comparing the speed with https://www.npmjs.com/package/sql-template-strings module. 5 | 6 | ## Usage 7 | 8 | ```bash 9 | npm run benchmark 10 | ``` 11 | 12 | ## Results 13 | ```bash 14 | ➜ sql git:(master) ✗ npm run benchmark 15 | @nearform/sql x 9,621,960 ops/sec ±1.57% (82 runs sampled) 16 | sql-template-strings x 286,461 ops/sec ±1.74% (81 runs sampled) 17 | The fastest is @nearform/sql 18 | ``` 19 | 20 | The module can execute 9,621,960 operations per second. 21 | -------------------------------------------------------------------------------- /sqlmap/server.js: -------------------------------------------------------------------------------- 1 | const users = require('./users') 2 | const table = require('./table') 3 | 4 | const fastify = require('fastify')({ 5 | logger: false 6 | }) 7 | 8 | fastify.register(require('@fastify/postgres'), require('./config')) 9 | fastify.register(users) 10 | fastify.register(table) 11 | 12 | const start = async () => { 13 | try { 14 | await fastify.listen({ port: 8080 }) 15 | // it's important to write to stdout as the sqlmap script relies on 16 | // a message from the server to be printed on stdout to start the checks 17 | console.log('Server started') 18 | } catch (err) { 19 | fastify.log.error(err) 20 | process.exit(1) 21 | } 22 | } 23 | 24 | start() 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | semver: 6 | description: The semver to use 7 | required: true 8 | default: patch 9 | type: choice 10 | options: 11 | - patch 12 | - minor 13 | - major 14 | pull_request: 15 | types: [closed] 16 | 17 | jobs: 18 | release: 19 | permissions: 20 | contents: write 21 | issues: write 22 | pull-requests: write 23 | id-token: write 24 | environment: production 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: nearform-actions/optic-release-automation-action@v4 28 | with: 29 | publish-mode: oidc 30 | semver: ${{ github.event.inputs.semver }} 31 | commit-message: 'chore: release {version}' 32 | -------------------------------------------------------------------------------- /sqlmap/injection-endpoints.json: -------------------------------------------------------------------------------- 1 | { 2 | "dbms": "postgresql", 3 | "level": 1, 4 | "risk": 1, 5 | "verbose": 0, 6 | "timeout": 5, 7 | "urls": [ 8 | { 9 | "url": "http://localhost:8080/users?page=1&limit=10", 10 | "method": "GET", 11 | "params": "page,limit" 12 | }, 13 | { 14 | "url": "http://localhost:8080/users", 15 | "headers": "Content-Type:application/json", 16 | "params": "username,password,email", 17 | "data": "{\"username\":\"user\",\"password\":\"12345\",\"email\":\"user@email.com\"}" 18 | }, 19 | { 20 | "url": "http://localhost:8080/table", 21 | "headers": "Content-Type:application/json", 22 | "params": "tableName,field1,field2", 23 | "data": "{\"tableName\":\"some_table\",\"field1\":\"id\",\"field2\":\"username\"}" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | const Benchmark = require('benchmark') 2 | const suite = new Benchmark.Suite() 3 | 4 | const SQL1 = require('../SQL') 5 | const SQL2 = require('sql-template-strings') 6 | 7 | const username = 'user' 8 | const email = 'user@email.com' 9 | const password = 'Password1' 10 | 11 | suite 12 | .add('@nearform/sql', function () { 13 | SQL1`INSERT INTO users (username, email, password) VALUES (${username},${email},${password})` 14 | }) 15 | .add('sql-template-strings', function () { 16 | SQL2`INSERT INTO users (username, email, password) VALUES (${username},${email},${password})` 17 | }) 18 | .on('cycle', function (event) { 19 | console.log(String(event.target)) 20 | }) 21 | .on('complete', function () { 22 | console.log('The fastest is ' + this.filter('fastest').map('name')) 23 | }) 24 | .run({ async: true }) 25 | -------------------------------------------------------------------------------- /SQL.test-d.ts: -------------------------------------------------------------------------------- 1 | import SQL from '.' 2 | import { glue, map, SqlStatement } from '.' 3 | import { expectType, expectError } from 'tsd' 4 | 5 | expectType(SQL`SELECT 1`) 6 | expectType(SQL`SELECT 1`) 7 | expectType(SQL`SELECT `.append(SQL`1`)) 8 | expectType(SQL`SELECT `.append(SQL`1`)) 9 | expectType(glue([SQL`SELECT`, SQL`1`], ' ')) 10 | expectType(SQL.glue([SQL`SELECT`, SQL`1`], ' ')) 11 | expectType(SQL.map([1,2,3])) 12 | expectType(SQL.map([1,2,3], x => x**2)) 13 | expectType(map([1,2,3])) 14 | expectType(map([1,2,3], x => x**2)) 15 | expectType(SQL`SELECT 1`.debug) 16 | expectType(SQL`SELECT 1`.sql) 17 | expectType(SQL`SELECT 1`.text) 18 | expectType<{ value: string }>(SQL.unsafe('string')) 19 | expectType<{ value: number }>(SQL.unsafe(1)) 20 | expectType<{ value: string }>(SQL.quoteIdent('string')) 21 | expectError(SQL`SELECT `.append(`1`)) 22 | -------------------------------------------------------------------------------- /sqlmap/users.js: -------------------------------------------------------------------------------- 1 | const SQL = require('../SQL') 2 | const quoteIdent = SQL.quoteIdent 3 | 4 | const tableName = 'users' 5 | 6 | module.exports = async function (fastify) { 7 | fastify.get('/users', async request => { 8 | const { limit = 10, page = 1 } = request.query 9 | 10 | const { rows: users } = await fastify.pg.query( 11 | SQL`SELECT * FROM ${SQL.quoteIdent(tableName)} LIMIT ${limit} OFFSET ${ 12 | limit * (page - 1) 13 | }` 14 | ) 15 | 16 | return users 17 | }) 18 | 19 | fastify.post('/users', async (request, reply) => { 20 | const { username, password, email } = request.body 21 | 22 | const result = await fastify.pg.query( 23 | SQL`INSERT INTO ${quoteIdent( 24 | tableName 25 | )} (username, email, password) VALUES (${username},${email},${password})` 26 | ) 27 | 28 | if (result.command !== 'INSERT' || result.rowCount !== 1) { 29 | throw new Error('User was not inserted') 30 | } 31 | 32 | return reply.code(201).send() 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /quoteIdentifier.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { describe, test } = require('node:test') 4 | const assert = require('node:assert') 5 | const quoteIdentifier = require('./quoteIdentifier') 6 | 7 | describe('quoteIdentifier', () => { 8 | describe('pg', () => { 9 | test('simple', async t => { 10 | assert.deepStrictEqual(quoteIdentifier('identifier', 'pg'), '"identifier"') 11 | }) 12 | 13 | test('with quotes', async t => { 14 | assert.deepStrictEqual(quoteIdentifier('"quotes"', 'pg'), '"""quotes"""') 15 | }) 16 | }) 17 | 18 | describe('mysql', () => { 19 | test('simple', t => { 20 | assert.deepStrictEqual(quoteIdentifier('identifier', 'mysql'), '`identifier`') 21 | }) 22 | 23 | test('with quotes', t => { 24 | assert.deepStrictEqual(quoteIdentifier('`quotes`', 'mysql'), '```quotes```') 25 | }) 26 | }) 27 | 28 | describe('without type', () => { 29 | test('simple', t => { 30 | assert.deepStrictEqual(quoteIdentifier('identifier'), '"identifier"') 31 | }) 32 | 33 | test('with quotes', t => { 34 | assert.deepStrictEqual(quoteIdentifier('"quotes"'), '"""quotes"""') 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nearform/sql", 3 | "version": "1.10.7", 4 | "description": "SQL injection protection module", 5 | "main": "./SQL.js", 6 | "types": "./SQL.d.ts", 7 | "scripts": { 8 | "test": "node --test *.test.js", 9 | "posttest": "tsd", 10 | "test:security": "node ./sqlmap/sqlmap.js", 11 | "test:typescript": "tsd", 12 | "pretest:security": "napa https://github.com/sqlmapproject/sqlmap && node ./sqlmap/db-init.js", 13 | "lint": "standard", 14 | "benchmark": "node benchmark/index.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/nearform/sql.git" 19 | }, 20 | "author": "NearForm Ltd", 21 | "license": "Apache-2.0", 22 | "bugs": { 23 | "url": "https://github.com/nearform/sql/issues" 24 | }, 25 | "homepage": "https://github.com/nearform/sql#readme", 26 | "devDependencies": { 27 | "@fastify/postgres": "^5.2.0", 28 | "async": "^3.2.0", 29 | "benchmark": "^2.1.4", 30 | "fastify": "^4.0.1", 31 | "jsonfile": "^6.1.0", 32 | "napa": "^3.0.0", 33 | "pg": "^8.6.0", 34 | "sql-template-strings": "^2.2.2", 35 | "standard": "^17.0.0", 36 | "tsd": "^0.33.0" 37 | }, 38 | "standard": { 39 | "ignore": [ 40 | "docs/*" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | build-lint-test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | node-version: 15 | - 20 16 | - 22 17 | - 24 18 | os: 19 | - ubuntu-latest 20 | services: 21 | postgres: 22 | image: postgres:9-alpine 23 | env: 24 | POSTGRES_PASSWORD: postgres 25 | POSTGRES_USER: postgres 26 | POSTGRES_DB: sqlmap 27 | ports: 28 | - 5432:5432 29 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s 30 | --health-retries 5 31 | env: 32 | PGPASS: postgres 33 | PGUSER: postgres 34 | PGDB: sqlmap 35 | PGHOST: localhost 36 | steps: 37 | - name: Checkout source code 38 | uses: actions/checkout@v6 39 | - name: Use Node.js 40 | uses: actions/setup-node@v5 41 | with: 42 | node-version: ${{ matrix.node-version }} 43 | - name: Install 44 | run: npm install 45 | - name: Lint 46 | run: npm run lint 47 | - name: Test 48 | run: npm test 49 | - name: Test Security 50 | run: npm run test:security 51 | automerge: 52 | needs: build-lint-test 53 | runs-on: ubuntu-latest 54 | permissions: 55 | pull-requests: write 56 | contents: write 57 | steps: 58 | - uses: fastify/github-action-merge-dependabot@v3 59 | -------------------------------------------------------------------------------- /sqlmap/db-init.js: -------------------------------------------------------------------------------- 1 | const pg = require('pg') 2 | const async = require('async') 3 | const config = require('./config') 4 | 5 | config.database = 'postgres' 6 | let client = new pg.Client(config) 7 | 8 | function connect (next) { 9 | client.connect(next) 10 | } 11 | 12 | function dropDb (next) { 13 | client.query('DROP DATABASE IF EXISTS sqlmap', function (err) { 14 | if (err) return next(err) 15 | 16 | next() 17 | }) 18 | } 19 | 20 | function createDb (next) { 21 | client.query('CREATE DATABASE sqlmap', function (err) { 22 | if (err) return next(err) 23 | 24 | next() 25 | }) 26 | } 27 | 28 | function connectToCorrectTable (next) { 29 | client.end(function () { 30 | config.database = 'sqlmap' 31 | client = new pg.Client(config) 32 | client.connect(next) 33 | }) 34 | } 35 | 36 | function initTable (next) { 37 | client.query('CREATE TABLE users (\n' + 38 | ' id SERIAL PRIMARY KEY,\n' + 39 | ' username VARCHAR(30) NOT NULL,\n' + 40 | ' email VARCHAR(30) NOT NULL,\n' + 41 | ' password VARCHAR(30) NOT NULL\n' + 42 | ')', function (err) { 43 | if (err) return next(err) 44 | 45 | next() 46 | }) 47 | } 48 | 49 | function init (cb) { 50 | async.series([ 51 | connect, 52 | dropDb, 53 | createDb, 54 | connectToCorrectTable, 55 | initTable 56 | ], 57 | function (err1) { 58 | if (err1) console.error(err1) 59 | client.end(function (err2) { 60 | cb(err1 || err2) 61 | cb() 62 | }) 63 | }) 64 | } 65 | 66 | module.exports = init 67 | 68 | if (require.main === module) { 69 | init((err) => { 70 | if (err) throw err 71 | else console.log('Db init: done') 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome to SQL! 2 | 3 | Please take a second to read over this before opening an issue. Providing complete information upfront will help us address any issue (and ship new features!) faster. 4 | 5 | We greatly appreciate bug fixes, documentation improvements and new features. However, when contributing a new major feature, it is a good idea to first open an issue. This is to make sure that the feature fits with the goal of the project, so we don't waste your or our time. 6 | 7 | ## Bug Reports 8 | 9 | A perfect bug report would have the following: 10 | 11 | 1. Summary of the issue you are experiencing. 12 | 2. Details on what versions of node you have (`node -v`). 13 | 3. A simple repeatable test case for us to run. Please try to run through it 2-3 times to ensure it is completely repeatable. 14 | 15 | We would like to avoid issues that require follow up questions to identify the bug. These follow ups are difficult to do unless we have a repeatable test case. 16 | 17 | ## For Developers 18 | 19 | All contributions should fit the [standard](https://github.com/standard/standard) linter, and pass the tests. 20 | You can test this by running: 21 | 22 | ``` 23 | npm test 24 | ``` 25 | 26 | In addition, make sure to add tests for any new features. 27 | You can test the test coverage by running: 28 | 29 | ``` 30 | npm run coverage 31 | ``` 32 | 33 | ## For Collaborators 34 | 35 | Make sure to get a `:thumbsup:`, `+1` or `LGTM` from another collaborator before merging a PR. If you aren't sure if a release should happen, open an issue. 36 | 37 | Release process: 38 | 39 | - `npm test` 40 | - `npm version ` 41 | - `git push && git push --tags` 42 | - `npm publish` 43 | 44 | ----------------------------------------- 45 | 46 | 47 | ## Developer's Certificate of Origin 1.1 48 | 49 | By making a contribution to this project, I certify that: 50 | 51 | * (a) The contribution was created in whole or in part by me and I 52 | have the right to submit it under the open source license 53 | indicated in the file; or 54 | 55 | * (b) The contribution is based upon previous work that, to the best 56 | of my knowledge, is covered under an appropriate open source 57 | license and I have the right under that license to submit that 58 | work with modifications, whether created in whole or in part 59 | by me, under the same open source license (unless I am 60 | permitted to submit under a different license), as indicated 61 | in the file; or 62 | 63 | * (c) The contribution was provided directly to me by some other 64 | person who certified (a), (b) or (c) and I have not modified 65 | it. 66 | 67 | * (d) I understand and agree that this project and the contribution 68 | are public and that a record of the contribution (including all 69 | personal information I submit with it, including my sign-off) is 70 | maintained indefinitely and may be redistributed consistent with 71 | this project or the open source license(s) involved. -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource@nearform.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /sqlmap/sqlmap.js: -------------------------------------------------------------------------------- 1 | const jsonfile = require('jsonfile') 2 | const spawn = require('child_process').spawn 3 | const exec = require('child_process').exec 4 | const path = require('path') 5 | const source = path.join(__dirname, 'injection-endpoints.json') 6 | const async = require('async') 7 | const chalk = require('chalk') 8 | 9 | const endpoints = jsonfile.readFileSync(source, { throws: false }) 10 | 11 | if (!endpoints) { 12 | console.error('⚠️ Invalid JSON file.') 13 | process.exit(1) 14 | } 15 | 16 | const findPython3 = (pythonCommand, done) => { 17 | return exec(`${pythonCommand} --version`, function (err, stdout, stderr) { 18 | if (err) { 19 | return done(err) 20 | } 21 | 22 | if (stdout.indexOf('Python 3.') >= 0) { 23 | console.log(chalk.green(`✅ '${pythonCommand}' is a valid Python3`)) 24 | return done(null, pythonCommand) 25 | } 26 | return done(null, false) 27 | }) 28 | } 29 | 30 | const sqlmapChalk = chalk.blue('sqlmap') 31 | 32 | const executeMap = (command, config, urlDescription, done) => { 33 | console.log('⏳ Python command that will be used:', command) 34 | 35 | const params = [ 36 | './node_modules/sqlmap/sqlmap.py', 37 | `--url=${urlDescription.url}`, 38 | `--level=${config.level}`, 39 | `--risk=${config.risk}`, 40 | `--dbms=${config.dbms}`, 41 | `--timeout=${config.timeout}`, 42 | '-v', 43 | `${config.verbose}`, 44 | '--flush-session', 45 | '--batch' 46 | ] 47 | 48 | if (urlDescription.method) { 49 | params.push(`--method=${urlDescription.method}`) 50 | } 51 | 52 | if (urlDescription.headers) { 53 | params.push(`--headers=${urlDescription.headers}`) 54 | } 55 | 56 | if (urlDescription.params) { 57 | params.push('-p') 58 | params.push(`${urlDescription.params}`) 59 | } 60 | if (urlDescription.data) { 61 | params.push(`--data=${urlDescription.data}`) 62 | } 63 | 64 | console.log( 65 | chalk.green( 66 | '⏳ executing sqlmap with: ', 67 | ['' + command].concat(params).join(' ') 68 | ) 69 | ) 70 | 71 | const sql = spawn(command, params) 72 | sql.stdin.end() 73 | let vulnerabilities = false 74 | 75 | sql.stdout.on('data', data => { 76 | if (data.length > 1) { 77 | console.log(`${sqlmapChalk} ${data}`) 78 | } 79 | if (data.indexOf('identified the following injection') >= 0) { 80 | vulnerabilities = true 81 | } 82 | }) 83 | 84 | sql.stderr.on('data', data => { 85 | done(data) 86 | }) 87 | 88 | sql.on('error', error => { 89 | console.log(`${sqlmapChalk} ${chalk.red(`⚠️ ${error}`)}`) 90 | done(new Error('failed to start child process')) 91 | }) 92 | 93 | sql.on('close', code => { 94 | if (code !== 0) { 95 | console.log(`${sqlmapChalk} ${chalk.red(`⚠️ exited with code ${code}`)}`) 96 | return process.exit(1) 97 | } 98 | done(null, vulnerabilities) 99 | }) 100 | } 101 | 102 | const fastifyChalk = chalk.yellow('fastify') 103 | 104 | const fastify = spawn('node', ['sqlmap/server.js']) 105 | 106 | fastify.on('close', code => { 107 | if (code === 0) return 108 | console.log( 109 | `\n${fastifyChalk} ${chalk.red(`⚠️ server exited with code ${code}`)}` 110 | ) 111 | process.exit(1) 112 | }) 113 | 114 | fastify.stdout.on('data', data => { 115 | console.log(`${fastifyChalk} ${data}`) 116 | }) 117 | 118 | fastify.stderr.on('data', data => { 119 | console.log(`${fastifyChalk} ${chalk.red(data)}`) 120 | }) 121 | 122 | async.detect(['python3'], findPython3, function (err, python) { 123 | if (err) { 124 | return console.error(chalk.red(err)) 125 | } 126 | 127 | fastify.stdout.once('data', data => { 128 | async.everySeries( 129 | endpoints.urls, 130 | (urlDescription, done) => { 131 | executeMap( 132 | python, 133 | endpoints, 134 | urlDescription, 135 | (err, vulnerabilities) => { 136 | if (err) { 137 | console.error(chalk.red(err)) 138 | return done(err, false) 139 | } 140 | 141 | done(null, !vulnerabilities) 142 | } 143 | ) 144 | }, 145 | (err, result) => { 146 | if (err) { 147 | console.error(chalk.red(err)) 148 | return process.exit(1) 149 | } 150 | 151 | console.log('\n\n') 152 | fastify.kill() 153 | if (result) { 154 | console.log( 155 | `\n${sqlmapChalk} ${chalk.green( 156 | '✅ no injection vulnerabilities found\n\n' 157 | )}` 158 | ) 159 | console.log() 160 | return process.exit(0) 161 | } else { 162 | console.log( 163 | `\n${sqlmapChalk} ${chalk.red( 164 | '⚠️ FOUND injection vulnerabilities\n\n' 165 | )}` 166 | ) 167 | return process.exit(1) 168 | } 169 | } 170 | ) 171 | }) 172 | }) 173 | -------------------------------------------------------------------------------- /SQL.d.ts: -------------------------------------------------------------------------------- 1 | /** A tagged template containing strings and values */ 2 | interface StatementLike { 3 | strings: string[] 4 | values: any[] 5 | } 6 | 7 | interface StatementOptions { 8 | unsafe?: boolean 9 | } 10 | 11 | /** 12 | * An SQL statement tagged template 13 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals 14 | */ 15 | declare class SqlStatement implements StatementLike { 16 | constructor(strings: string[], values: any[]) 17 | 18 | /** The string components of this tagged template */ 19 | strings: string[] 20 | 21 | /** 22 | * Safely glues multiple SQL statements together 23 | * @param pieces the statements to be glued 24 | * @param separator the glue separator placed between each statement 25 | * @example 26 | * const sql = SQL`SELECT id FROM customers WHERE ` 27 | * sql.glue([ 28 | * sql, 29 | * SQL`email = ${email}` 30 | * ]) 31 | */ 32 | glue(pieces: StatementLike[], separator: string): SqlStatement 33 | 34 | /** 35 | * Safely glues multiple SQL statements together 36 | * @param pieces the statements to be glued 37 | * @param separator the glue separator placed between each statement 38 | * @example 39 | * SQL.glue([ 40 | * SQL`SELECT id FROM customers WHERE `, 41 | * SQL`email = ${email}` 42 | * ]) 43 | * ) 44 | */ 45 | static glue(pieces: StatementLike[], separator: string): SqlStatement 46 | 47 | /** 48 | * A function that accepts an array of objects and a mapper function 49 | * It returns a clean SQL format using the object properties defined in the mapper function 50 | * @param array the items to be mapped over 51 | * @param mapFunc a function to transform the items in `array` before being added to the SqlStatement 52 | * @example 53 | * SQL`SELECT ${SQL.map([1,2,3])}` 54 | * @example 55 | * SQL`SELECT ${SQL.map([1,2,3], x => x ** 2)}` 56 | */ 57 | map(array: T[], mapFunc?: (item: T) => unknown): SqlStatement 58 | 59 | /** 60 | * A function that accepts an array of objects and a mapper function 61 | * It returns a clean SQL format using the object properties defined in the mapper function 62 | * @param array the items to be mapped over 63 | * @param mapFunc a function to transform the items in `array` before being added to the SqlStatement 64 | * @example 65 | * SQL`SELECT ${SQL.map([1,2,3])}` 66 | * @example 67 | * SQL`SELECT ${SQL.map([1,2,3], x => x ** 2)}` 68 | */ 69 | static map(array: T[], mapFunc?: (item: T) => unknown): SqlStatement 70 | 71 | /** Returns a formatted but unsafe statement of strings and values, useful for debugging */ 72 | get debug(): string 73 | 74 | /** Returns a formatted statement suitable for use in PostgreSQL */ 75 | get text(): string 76 | 77 | /** Returns a formatted statement suitable for use in MySQL */ 78 | get sql(): string 79 | 80 | /** The value components of this tagged template */ 81 | get values(): any[] 82 | 83 | /** 84 | * Appends another statement onto this statement 85 | * @deprecated Please append within template literals, e.g. SQL`SELECT * ${sql}` 86 | * @param statement a statement to be appended onto this existing statement 87 | * @param options allows disabling the safe template escaping while appending 88 | * @example 89 | * SQL`UPDATE users SET name = ${username}, email = ${email} ` 90 | * .append(SQL`SET ${dynamicName} = '2'`, { unsafe: true }) 91 | * .append(SQL`WHERE id = ${userId}`) 92 | */ 93 | append(statement: StatementLike, options?: StatementOptions): SqlStatement 94 | } 95 | 96 | declare namespace SQL { 97 | export { SqlStatement } 98 | 99 | /** 100 | * Safely glues multiple SQL statements together 101 | * @param pieces the statements to be glued 102 | * @param separator the glue separator placed between each statement 103 | * @example 104 | * SQL.glue([ 105 | * SQL`SELECT id FROM customers WHERE `, 106 | * SQL`email = ${email}` 107 | * ]) 108 | * ) 109 | */ 110 | export function glue(pieces: StatementLike[], separator: string): SqlStatement 111 | 112 | /** 113 | * A function that accepts an array of objects and a mapper function 114 | * It returns a clean SQL format using the object properties defined in the mapper function 115 | * @param array the items to be mapped over 116 | * @param mapFunc a function to transform the items in `array` before being added to the SqlStatement 117 | * @example 118 | * SQL`SELECT ${SQL.map([1,2,3])}` 119 | * @example 120 | * SQL`SELECT ${SQL.map([1,2,3], x => x ** 2)}` 121 | */ 122 | export function map(array: T[], mapFunc?: (item: T) => unknown): SqlStatement 123 | 124 | export function unsafe(value: T): { value: T } 125 | export function quoteIdent(value: string): { value: string } 126 | } 127 | 128 | /** 129 | * Create an SQL statement tagged template 130 | * @param strings template literal string components 131 | * @param values template literal value components 132 | * @example 133 | * SQL`SELECT id FROM customers WHERE name = ${userInput}` 134 | */ 135 | declare function SQL(strings: any, ...values: any[]): SqlStatement 136 | 137 | export = SQL 138 | -------------------------------------------------------------------------------- /SQL.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const inspect = Symbol.for('nodejs.util.inspect.custom') 3 | const wrapped = Symbol('wrapped') 4 | 5 | const quoteIdentifier = require('./quoteIdentifier') 6 | 7 | class SqlStatement { 8 | constructor (strings, values) { 9 | if (values.some(value => value === undefined)) { 10 | throw new Error( 11 | 'SQL`...` strings cannot take `undefined` as values as this can generate invalid sql.' 12 | ) 13 | } 14 | this.strings = strings 15 | this._values = values 16 | } 17 | 18 | glue (pieces, separator) { 19 | const result = { strings: [], values: [] } 20 | let carryover 21 | for (let i = 0; i < pieces.length; i++) { 22 | const strings = Array.from(pieces[i].strings) 23 | if (i > 0) { 24 | strings[0] = carryover + separator + strings[0] 25 | } 26 | carryover = strings.splice(-1)[0] 27 | result.strings.push.apply(result.strings, strings) 28 | result.values.push.apply(result.values, pieces[i]._values) 29 | } 30 | 31 | result.strings.push(carryover) 32 | 33 | result.strings[result.strings.length - 1] += ' ' 34 | 35 | return new SqlStatement(result.strings, result.values) 36 | } 37 | 38 | /** 39 | * A function that accepts an array of objects and a mapper function 40 | * It returns a clean SQL format using the object properties defined in the mapper function 41 | */ 42 | map (array, mapFunc = i => i) { 43 | if ((mapFunc instanceof Function) && array?.length > 0) { 44 | return this.glue( 45 | array.map(mapFunc).map((item) => SQL`${item}`), 46 | ',' 47 | ) 48 | } 49 | return null 50 | } 51 | 52 | _generateString (type, namedValueOffset = 0) { 53 | let text = this.strings[0] 54 | let valueOffset = 0 55 | const values = [...this._values] 56 | 57 | for (let i = 1; i < this.strings.length; i++) { 58 | const valueIndex = i - 1 + valueOffset 59 | const valueContainer = values[valueIndex] 60 | 61 | if (valueContainer && valueContainer[wrapped]) { 62 | text += `${valueContainer.transform(type)}${this.strings[i]}` 63 | values.splice(valueIndex, 1) 64 | valueOffset-- 65 | } else if (valueContainer instanceof SqlStatement) { 66 | text += `${valueContainer._generateString( 67 | type, 68 | valueIndex + namedValueOffset 69 | )}${this.strings[i]}` 70 | valueOffset += valueContainer.values.length - 1 71 | values.splice(valueIndex, 1, ...valueContainer.values) 72 | } else { 73 | let delimiter = '?' 74 | if (type === 'pg') { 75 | delimiter = '$' + (i + valueOffset + namedValueOffset) 76 | } 77 | 78 | text += delimiter + this.strings[i] 79 | } 80 | } 81 | 82 | return text.replace(/\s+$/gm, ' ').replace(/^\s+|\s+$/gm, '') 83 | } 84 | 85 | get debug () { 86 | let text = this.strings[0] 87 | 88 | for (let i = 1; i < this.strings.length; i++) { 89 | let data = this._values[i - 1] 90 | let quote = "'" 91 | if (data && data[wrapped]) { 92 | data = data.transform() 93 | quote = '' 94 | } else if (data instanceof SqlStatement) { 95 | data = data.debug 96 | quote = '' 97 | } 98 | typeof data === 'string' ? (text += quote + data + quote) : (text += data) 99 | text += this.strings[i] 100 | } 101 | 102 | return text.replace(/\s+$/gm, ' ').replace(/^\s+|\s+$/gm, '') 103 | } 104 | 105 | [inspect] () { 106 | return `SQL << ${this.debug} >>` 107 | } 108 | 109 | get text () { 110 | return this._generateString('pg') 111 | } 112 | 113 | get sql () { 114 | return this._generateString('mysql') 115 | } 116 | 117 | get values () { 118 | return this._values 119 | .filter(v => !v || !v[wrapped]) 120 | .reduce((acc, v) => { 121 | if (v instanceof SqlStatement) { 122 | acc.push(...v.values) 123 | } else { 124 | acc.push(v) 125 | } 126 | return acc 127 | }, []) 128 | } 129 | 130 | /** 131 | * @deprecated Please append within template literals, e.g. SQL`SELECT * ${sql}` 132 | */ 133 | append (statement, options) { 134 | if (!statement) { 135 | return this 136 | } 137 | 138 | if (!(statement instanceof SqlStatement)) { 139 | throw new Error( 140 | '"append" accepts only template string prefixed with SQL (SQL`...`)' 141 | ) 142 | } 143 | 144 | if (options && options.unsafe === true) { 145 | const text = statement.strings.reduce((acc, string, i) => { 146 | acc = `${acc}${string}${statement.values[i] ? statement.values[i] : ''}` 147 | return acc 148 | }, '') 149 | 150 | const strings = this.strings.slice(0) 151 | strings[this.strings.length - 1] += text 152 | 153 | this.strings = strings 154 | 155 | return this 156 | } 157 | 158 | const last = this.strings[this.strings.length - 1] 159 | const [first, ...rest] = statement.strings 160 | 161 | this.strings = [...this.strings.slice(0, -1), last + first, ...rest] 162 | 163 | this._values.push.apply(this._values, statement._values) 164 | 165 | return this 166 | } 167 | } 168 | 169 | function SQL (strings, ...values) { 170 | return new SqlStatement(strings, values) 171 | } 172 | 173 | SQL.glue = SqlStatement.prototype.glue 174 | SQL.map = SqlStatement.prototype.map 175 | 176 | module.exports = SQL 177 | module.exports.SQL = SQL 178 | module.exports.default = SQL 179 | module.exports.unsafe = value => ({ 180 | transform () { 181 | return value 182 | }, 183 | [wrapped]: true 184 | }) 185 | module.exports.quoteIdent = value => ({ 186 | transform (type) { 187 | return quoteIdentifier(value, type) 188 | }, 189 | [wrapped]: true 190 | }) 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQL 2 | 3 | A simple SQL injection protection module that allows you to use ES6 template strings for escaped statements. Works with [pg](https://www.npmjs.com/package/pg), [mysql](https://www.npmjs.com/package/mysql) and [mysql2](https://www.npmjs.com/package/mysql2) library. 4 | 5 | [![npm version][1]][2] [![build status][3]][4] [![js-standard-style][5]][6] 6 | 7 | 1. [Install](#install) 8 | 2. [Usage](#usage) 9 | 1. [Linting](#linting) 10 | 3. [Methods](#methods) 11 | 1. [glue](#gluepieces-separator) 12 | 2. [map](#maparray-mapperfunction) 13 | 2. (deprecated) [append](#deprecated-appendstatement-options) 14 | 4. [Utilities](#utilities) 15 | 1. [unsafe](#unsafevalue) 16 | 2. [quoteIdent](#quoteidentvalue) 17 | 5. [How it works?](#how-it-works) 18 | 6. [Undefined values and nullable fields](#undefined-values-and-nullable-fields) 19 | 7. [Testing, linting, & coverage](#testing-linting--coverage) 20 | 8. [Benchmark](#benchmark) 21 | 9. [License](#license) 22 | 23 | ## Install 24 | 25 | ```sh 26 | npm install @nearform/sql 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```js 32 | const SQL = require('@nearform/sql') 33 | 34 | const username = 'user' 35 | const email = 'user@email.com' 36 | const password = 'Password1' 37 | 38 | // generate SQL query 39 | const sql = SQL` 40 | INSERT INTO users (username, email, password) 41 | VALUES (${username},${email},${password}) 42 | ` 43 | 44 | pg.query(sql) // execute query in pg 45 | 46 | mysql.query(sql) // execute query in mysql 47 | 48 | mysql2.query(sql) // execute query in mysql2 49 | ``` 50 | 51 | ### Linting 52 | 53 | We recommend using [eslint-plugin-sql](https://github.com/gajus/eslint-plugin-sql#eslint-plugin-sql-rules-no-unsafe-query) to prevent cases in which the SQL tag is forgotten to be added in front of template strings. Eslint will fail if you write SQL queries without `sql` tag in front of the string. 54 | 55 | ```sql 56 | `SELECT 1` 57 | // fails - Message: Use "sql" tag 58 | 59 | sql`SELECT 1` 60 | // passes 61 | ``` 62 | 63 | ## Methods 64 | 65 | > ⚠️ **Warning** 66 | > 67 | > The `unsafe` option interprets the interpolated values as literals and it should be used carefully to avoid introducing SQL injection vulnerabilities. 68 | 69 | ### glue(pieces, separator) 70 | 71 | ```js 72 | const username = 'user1' 73 | const email = 'user1@email.com' 74 | const userId = 1 75 | 76 | const updates = [] 77 | updates.push(SQL`name = ${username}`) 78 | updates.push(SQL`email = ${email}`) 79 | 80 | const sql = SQL`UPDATE users SET ${SQL.glue(updates, ' , ')} WHERE id = ${userId}` 81 | ``` 82 | 83 | or also 84 | 85 | ```js 86 | const ids = [1, 2, 3] 87 | const value = 'test' 88 | const sql = SQL` 89 | UPDATE users 90 | SET property = ${value} 91 | WHERE id 92 | IN (${SQL.glue(ids.map(id => SQL`${id}`), ' , ')}) 93 | ` 94 | ``` 95 | 96 | Glue can also be used statically: 97 | 98 | ```js 99 | const ids = [1, 2, 3] 100 | const idsSqls = ids.map(id => SQL`(${id})`) 101 | SQL.glue(idsSqls, ' , ') 102 | ``` 103 | 104 | Glue can also be used to generate batch operations: 105 | 106 | ```js 107 | const users = [ 108 | { id: 1, name: 'something' }, 109 | { id: 2, name: 'something-else' }, 110 | { id: 3, name: 'something-other' } 111 | ] 112 | 113 | const sql = SQL`INSERT INTO users (id, name) VALUES 114 | ${SQL.glue( 115 | users.map(user => SQL`(${user.id},${user.name}})`), 116 | ' , ' 117 | )} 118 | ` 119 | ``` 120 | 121 | ### map(array, mapperFunction) 122 | 123 | Using the default mapperFunction which is just an iteration over the array elements 124 | ```js 125 | const ids = [1, 2, 3] 126 | 127 | const values = SQL.map(ids) 128 | const sql = SQL`INSERT INTO users (id) VALUES (${values})` 129 | ``` 130 | 131 | Using an array of objects which requires a mapper function 132 | 133 | ```js 134 | const objArray = [{ 135 | id: 1, 136 | name: 'name1' 137 | }, 138 | { 139 | id: 2, 140 | name: 'name2' 141 | }, 142 | { 143 | id: 3, 144 | name: 'name3' 145 | }] 146 | 147 | const mapperFunction = (objItem) => objItem.id 148 | const values = SQL.map(objArray, mapperFunction) 149 | 150 | const sql = SQL`INSERT INTO users (id) VALUES (${values})` 151 | ``` 152 | 153 | ### (deprecated) append(statement[, options]) 154 | 155 | Append has been deprecated in favour of using template literals: 156 | 157 | ```js 158 | const from = SQL`FROM table` 159 | const sql = SQL`SELECT * ${from}` 160 | ``` 161 | 162 | For now, you can still use append as follows: 163 | 164 | ```js 165 | const username = 'user1' 166 | const email = 'user1@email.com' 167 | const userId = 1 168 | 169 | const sql = SQL`UPDATE users SET name = ${username}, email = ${email}` 170 | sql.append(SQL`, ${dynamicName} = 'dynamicValue'`, { unsafe: true }) 171 | sql.append(SQL`WHERE id = ${userId}`) 172 | ``` 173 | 174 | ## Utilities 175 | 176 | ### unsafe(value) 177 | 178 | Does a literal interpolation of the provided value, interpreting the provided value as-is. 179 | 180 | It works similarly to the `unsafe` option of the `append` method and requires the same security considerations. 181 | 182 | ```js 183 | const username = 'john' 184 | const userId = 1 185 | 186 | const sql = SQL` 187 | UPDATE users 188 | SET username = '${SQL.unsafe(username)}' 189 | WHERE id = ${userId} 190 | ` 191 | ``` 192 | 193 | ### quoteIdent(value) 194 | 195 | Mimics the native PostgreSQL `quote_ident` and MySQL `quote_identifier` functions. 196 | 197 | In PostgreSQL, it wraps the provided value in double quotes `"` and escapes any double quotes existing in the provided value. 198 | 199 | In MySQL, it wraps the provided value in backticks `` ` `` and escapes any backticks existing in the provided value. 200 | 201 | It's convenient to use when schema, table or field names are dynamic and can't be hardcoded in the SQL query string. 202 | 203 | ```js 204 | const table = 'users' 205 | const username = 'john' 206 | const userId = 1 207 | 208 | const sql = SQL` 209 | UPDATE ${SQL.quoteIdent(table)} 210 | SET username = ${username} 211 | WHERE id = ${userId} 212 | ` 213 | ``` 214 | 215 | ## How it works? 216 | 217 | The SQL template string tag parses query and returns an objects that's understandable by [pg](https://www.npmjs.com/package/pg) library: 218 | 219 | ```js 220 | const username = 'user' 221 | const email = 'user@email.com' 222 | const password = 'Password1' 223 | 224 | const sql = SQL`INSERT INTO users (username, email, password) VALUES (${username}, ${email}, ${password})` // generate SQL query 225 | sql.text // INSERT INTO users (username, email, password) VALUES ($1 , $2 , $3) - for pg 226 | sql.sql // INSERT INTO users (username, email, password) VALUES (? , ? , ?) - for mysql and mysql2 227 | sql.values // ['user, 'user@email.com', 'Password1'] 228 | ``` 229 | 230 | To help with debugging, you can view an approximate representation of the SQL query with values filled in. It may differ from the actual SQL executed by your database, but serves as a handy reference when debugging. The debug output _should not_ be executed as it is not guaranteed safe. You can may also inspect the `SQL` object via `console.log`. 231 | 232 | ```js 233 | sql.debug // INSERT INTO users (username, email, password) VALUES ('user','user@email.com','Password1') 234 | 235 | console.log(sql) // SQL << INSERT INTO users (username, email, password) VALUES ('user','user@email.com','Password1') >> 236 | ``` 237 | 238 | ## Undefined values and nullable fields 239 | 240 | Don't pass undefined values into the sql query string builder. It throws on undefined values as this is a javascript concept and sql does not handle it. 241 | 242 | Sometimes you may expect to not have a value to be provided to the string builder, and this is ok as the coresponding field is nullable. In this or similar cases the recommended way to handle this is to coerce it to a null js value. 243 | 244 | Example: 245 | 246 | ```js 247 | const user = { name: 'foo bar' } 248 | 249 | const sql = SQL`INSERT into users (name, address) VALUES (${user.name},${ 250 | user.address || null 251 | })` 252 | sql.debug // INSERT INTO users (name, address) VALUES ('foo bar',null) 253 | ``` 254 | 255 | ## Example custom utilities 256 | 257 | ### Insert into from a JS object 258 | 259 | The below example functions can be used to generate an INSERT INTO statement from an object, which will convert the object keys to snake case. 260 | 261 | ```js 262 | function insert(table, insertData, { toSnakeCase } = { toSnakeCase: false }) { 263 | const builder = Object.entries(insertData).reduce( 264 | (acc, [column, value]) => { 265 | if (value !== undefined) { 266 | toSnakeCase 267 | ? acc.columns.push(pascalOrCamelToSnake(column)) 268 | : acc.columns.push(column) 269 | acc.values.push(SQL`${value}`) 270 | } 271 | return acc 272 | }, 273 | { columns: [], values: [] } 274 | ) 275 | return SQL`INSERT INTO ${SQL.quoteIdent(table)} (${SQL.unsafe( 276 | builder.columns.join(', ') 277 | )}) VALUES (${SQL.glue(builder.values, ', ')})` 278 | } 279 | 280 | const pascalOrCamelToSnake = str => 281 | str[0].toLowerCase() + 282 | str 283 | .slice(1, str.length) 284 | .replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`) 285 | ``` 286 | 287 | ## Testing, linting, & coverage 288 | 289 | This module can be tested and reported on in a variety of ways... 290 | 291 | ```sh 292 | npm run test # runs tap based unit test suite. 293 | npm run test:security # runs sqlmap security tests. 294 | npm run test:typescript # runs type definition tests. 295 | npm run coverage # generates a coverage report in docs dir. 296 | npm run lint # lints via standardJS. 297 | ``` 298 | 299 | ## Benchmark 300 | 301 | Find more about `@nearform/sql` speed [here](benchmark) 302 | 303 | ## Editor syntax higlighting 304 | To get syntax higlighting, you can use extension/plugin for these editors: 305 | - Visual studio code: [thebearingedge.vscode-sql-lit](https://marketplace.visualstudio.com/items?itemName=thebearingedge.vscode-sql-lit) 306 | 307 | # License 308 | 309 | Copyright NearForm 2021. Licensed under 310 | [Apache 2.0][7] 311 | 312 | [1]: https://img.shields.io/npm/v/@nearform/sql.svg?style=flat-square 313 | [2]: https://npmjs.org/package/@nearform/sql 314 | [3]: https://github.com/nearform/sql/workflows/CI/badge.svg 315 | [4]: https://github.com/nearform/sql/actions?query=workflow%3ACI 316 | [5]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square 317 | [6]: https://github.com/feross/standard 318 | [7]: https://tldrlegal.com/license/apache-license-2.0-(apache-2.0) 319 | 320 | [![banner](https://raw.githubusercontent.com/nearform/.github/refs/heads/master/assets/os-banner-green.svg)](https://www.nearform.com/contact/?utm_source=open-source&utm_medium=banner&utm_campaign=os-project-pages) -------------------------------------------------------------------------------- /SQL.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const util = require('util') 4 | const { test } = require('node:test') 5 | const assert = require('node:assert') 6 | 7 | const SQL = require('./SQL') 8 | const unsafe = SQL.unsafe 9 | const quoteIdent = SQL.quoteIdent 10 | 11 | test('SQL helper - build complex query with append', async t => { 12 | const name = 'Team 5' 13 | const description = 'description' 14 | const teamId = 7 15 | const organizationId = 'WONKA' 16 | 17 | const sql = SQL`UPDATE teams SET name = ${name}, description = ${description} ` 18 | sql.append(SQL`WHERE id = ${teamId} AND org_id = ${organizationId}`) 19 | 20 | assert.equal( 21 | sql.text, 22 | 'UPDATE teams SET name = $1, description = $2 WHERE id = $3 AND org_id = $4' 23 | ) 24 | assert.equal( 25 | sql.sql, 26 | 'UPDATE teams SET name = ?, description = ? WHERE id = ? AND org_id = ?' 27 | ) 28 | assert.equal( 29 | sql.debug, 30 | `UPDATE teams SET name = '${name}', description = '${description}' WHERE id = ${teamId} AND org_id = '${organizationId}'` 31 | ) 32 | assert.deepStrictEqual(sql.values, [name, description, teamId, organizationId]) 33 | }) 34 | 35 | test('SQL helper - multiline', async t => { 36 | const name = 'Team 5' 37 | const description = 'description' 38 | const teamId = 7 39 | const organizationId = 'WONKA' 40 | 41 | const sql = SQL` 42 | UPDATE teams SET name = ${name}, description = ${description} 43 | WHERE id = ${teamId} AND org_id = ${organizationId} 44 | ` 45 | 46 | assert.equal( 47 | sql.text, 48 | 'UPDATE teams SET name = $1, description = $2\nWHERE id = $3 AND org_id = $4' 49 | ) 50 | assert.equal( 51 | sql.sql, 52 | 'UPDATE teams SET name = ?, description = ?\nWHERE id = ? AND org_id = ?' 53 | ) 54 | assert.equal( 55 | sql.debug, 56 | `UPDATE teams SET name = '${name}', description = '${description}'\nWHERE id = ${teamId} AND org_id = '${organizationId}'` 57 | ) 58 | assert.deepStrictEqual(sql.values, [name, description, teamId, organizationId]) 59 | }) 60 | 61 | test('SQL helper - multiline with emtpy lines', async t => { 62 | const name = 'Team 5' 63 | const description = 'description' 64 | const teamId = 7 65 | const organizationId = 'WONKA' 66 | 67 | const sql = SQL` 68 | UPDATE teams SET name = ${name}, description = ${description} 69 | WHERE id = ${teamId} AND org_id = ${organizationId} 70 | 71 | RETURNING id 72 | ` 73 | 74 | assert.equal( 75 | sql.text, 76 | 'UPDATE teams SET name = $1, description = $2\nWHERE id = $3 AND org_id = $4\nRETURNING id' 77 | ) 78 | assert.equal( 79 | sql.sql, 80 | 'UPDATE teams SET name = ?, description = ?\nWHERE id = ? AND org_id = ?\nRETURNING id' 81 | ) 82 | assert.equal( 83 | sql.debug, 84 | `UPDATE teams SET name = '${name}', description = '${description}'\nWHERE id = ${teamId} AND org_id = '${organizationId}'\nRETURNING id` 85 | ) 86 | assert.deepStrictEqual(sql.values, [name, description, teamId, organizationId]) 87 | }) 88 | 89 | test('SQL helper - build complex query with map', async t => { 90 | const objArray = [{ 91 | id: 1, 92 | name: 'name1' 93 | }, 94 | { 95 | id: 2, 96 | name: 'name2' 97 | }, 98 | { 99 | id: 3, 100 | name: 'name3' 101 | }] 102 | 103 | const mapFunction = (objItem) => { 104 | return objItem.id 105 | } 106 | 107 | const values = SQL.map(objArray, mapFunction) 108 | assert.equal(values !== null, true) 109 | const sql = SQL`INSERT INTO users (id) VALUES (${values})` 110 | 111 | assert.equal(sql.text, 'INSERT INTO users (id) VALUES ($1,$2,$3)') 112 | assert.equal(sql.sql, 'INSERT INTO users (id) VALUES (?,?,?)') 113 | assert.equal(sql.debug, 'INSERT INTO users (id) VALUES (1,2,3)') 114 | assert.deepStrictEqual(sql.values, [1, 2, 3]) 115 | }) 116 | 117 | test('SQL helper - build complex query with map - using default mapper function', async t => { 118 | const ids = [1, 2, 3] 119 | 120 | const values = SQL.map(ids) 121 | assert.equal(values !== null, true) 122 | const sql = SQL`INSERT INTO users (id) VALUES (${values})` 123 | 124 | assert.equal(sql.text, 'INSERT INTO users (id) VALUES ($1,$2,$3)') 125 | assert.equal(sql.sql, 'INSERT INTO users (id) VALUES (?,?,?)') 126 | assert.equal(sql.debug, 'INSERT INTO users (id) VALUES (1,2,3)') 127 | assert.deepStrictEqual(sql.values, [1, 2, 3]) 128 | }) 129 | 130 | test('SQL helper - build complex query with map - empty array', async t => { 131 | const objArray = [] 132 | 133 | const mapFunction = (objItem) => { 134 | return objItem.id 135 | } 136 | 137 | const values = SQL.map(objArray, mapFunction) 138 | 139 | assert.equal(values, null) 140 | }) 141 | 142 | test('SQL helper - build complex query with map - bad mapper function', async t => { 143 | const objArray = [] 144 | 145 | const mapFunction = null 146 | 147 | const values = SQL.map(objArray, mapFunction) 148 | 149 | assert.equal(values, null) 150 | }) 151 | 152 | test('SQL helper - build complex query with glue', async t => { 153 | const name = 'Team 5' 154 | const description = 'description' 155 | const teamId = 7 156 | const organizationId = 'WONKA' 157 | 158 | const sql = SQL` UPDATE teams SET ` 159 | 160 | const updates = [] 161 | updates.push(SQL`name = ${name}`) 162 | updates.push(SQL`description = ${description}`) 163 | 164 | sql.append(sql.glue(updates, ' , ')) 165 | sql.append(SQL`WHERE id = ${teamId} AND org_id = ${organizationId}`) 166 | 167 | assert.equal( 168 | sql.text, 169 | 'UPDATE teams SET name = $1 , description = $2 WHERE id = $3 AND org_id = $4' 170 | ) 171 | assert.equal( 172 | sql.sql, 173 | 'UPDATE teams SET name = ? , description = ? WHERE id = ? AND org_id = ?' 174 | ) 175 | assert.equal( 176 | sql.debug, 177 | `UPDATE teams SET name = '${name}' , description = '${description}' WHERE id = ${teamId} AND org_id = '${organizationId}'` 178 | ) 179 | assert.deepStrictEqual(sql.values, [name, description, teamId, organizationId]) 180 | }) 181 | 182 | test('SQL helper - build complex query with glue - regression #13', async t => { 183 | const name = 'Team 5' 184 | const ids = [1, 2, 3].map(id => SQL`${id}`) 185 | 186 | const sql = SQL`UPDATE teams SET name = ${name} ` 187 | sql.append(SQL`WHERE id IN (`) 188 | sql.append(sql.glue(ids, ' , ')) 189 | sql.append(SQL`)`) 190 | 191 | assert.equal(sql.text, 'UPDATE teams SET name = $1 WHERE id IN ($2 , $3 , $4 )') 192 | assert.equal(sql.sql, 'UPDATE teams SET name = ? WHERE id IN (? , ? , ? )') 193 | assert.equal( 194 | sql.debug, 195 | `UPDATE teams SET name = '${name}' WHERE id IN (1 , 2 , 3 )` 196 | ) 197 | assert.deepStrictEqual(sql.values, [name, 1, 2, 3]) 198 | }) 199 | 200 | test('SQL helper - build complex query with glue - regression #17', async t => { 201 | const ids = [1, 2, 3].map(id => SQL`(${id})`) 202 | 203 | const sql = SQL`INSERT INTO users (id) VALUES ` 204 | sql.append(sql.glue(ids, ' , ')) 205 | 206 | assert.equal(sql.text, 'INSERT INTO users (id) VALUES ($1) , ($2) , ($3)') 207 | assert.equal(sql.sql, 'INSERT INTO users (id) VALUES (?) , (?) , (?)') 208 | assert.equal(sql.debug, 'INSERT INTO users (id) VALUES (1) , (2) , (3)') 209 | assert.deepStrictEqual(sql.values, [1, 2, 3]) 210 | }) 211 | 212 | test('SQL helper - build complex query with static glue - regression #17', async t => { 213 | const ids = [1, 2, 3].map(id => SQL`(${id})`) 214 | 215 | const sql = SQL`INSERT INTO users (id) VALUES ` 216 | sql.append(SQL.glue(ids, ' , ')) 217 | 218 | assert.equal(sql.text, 'INSERT INTO users (id) VALUES ($1) , ($2) , ($3)') 219 | assert.equal(sql.sql, 'INSERT INTO users (id) VALUES (?) , (?) , (?)') 220 | assert.equal(sql.debug, 'INSERT INTO users (id) VALUES (1) , (2) , (3)') 221 | assert.deepStrictEqual(sql.values, [1, 2, 3]) 222 | }) 223 | 224 | test('glue works with quoteIdent - regression #77', async t => { 225 | const sql = SQL.glue([SQL`SELECT * FROM ${quoteIdent('tbl')}`]) 226 | 227 | assert.equal(sql.text, 'SELECT * FROM "tbl"') 228 | assert.equal(sql.sql, 'SELECT * FROM `tbl`') 229 | assert.equal(sql.debug, 'SELECT * FROM "tbl"') 230 | assert.deepStrictEqual(sql.values, []) 231 | }) 232 | 233 | test('SQL helper - build complex query with append and glue', async t => { 234 | const updates = [] 235 | const v1 = 'v1' 236 | const v2 = 'v2' 237 | const v3 = 'v3' 238 | const v4 = 'v4' 239 | const v5 = 'v5' 240 | const v6 = 'v6' 241 | const v7 = 'v7' 242 | 243 | const sql = SQL`TEST QUERY glue pieces FROM ` 244 | updates.push(SQL`v1 = ${v1}`) 245 | updates.push(SQL`v2 = ${v2}`) 246 | updates.push(SQL`v3 = ${v3}`) 247 | updates.push(SQL`v4 = ${v4}`) 248 | updates.push(SQL`v5 = ${v5}`) 249 | 250 | sql.append(sql.glue(updates, ' , ')) 251 | sql.append(SQL`WHERE v6 = ${v6} `) 252 | sql.append(SQL`AND v7 = ${v7}`) 253 | 254 | assert.equal( 255 | sql.text, 256 | 'TEST QUERY glue pieces FROM v1 = $1 , v2 = $2 , v3 = $3 , v4 = $4 , v5 = $5 WHERE v6 = $6 AND v7 = $7' 257 | ) 258 | assert.equal( 259 | sql.sql, 260 | 'TEST QUERY glue pieces FROM v1 = ? , v2 = ? , v3 = ? , v4 = ? , v5 = ? WHERE v6 = ? AND v7 = ?' 261 | ) 262 | assert.equal( 263 | sql.debug, 264 | "TEST QUERY glue pieces FROM v1 = 'v1' , v2 = 'v2' , v3 = 'v3' , v4 = 'v4' , v5 = 'v5' WHERE v6 = 'v6' AND v7 = 'v7'" 265 | ) 266 | assert.deepStrictEqual(sql.values, [v1, v2, v3, v4, v5, v6, v7]) 267 | }) 268 | 269 | test('SQL helper - build complex query with append', async t => { 270 | const v1 = 'v1' 271 | const v2 = 'v2' 272 | const v3 = 'v3' 273 | const v4 = 'v4' 274 | const v5 = 'v5' 275 | const v6 = 'v6' 276 | const v7 = 'v7' 277 | 278 | const sql = SQL`TEST QUERY glue pieces FROM ` 279 | sql.append(SQL`v1 = ${v1}, `) 280 | sql.append(SQL`v2 = ${v2}, `) 281 | sql.append(SQL`v3 = ${v3}, `) 282 | sql.append(SQL`v4 = ${v4}, `) 283 | sql.append(SQL`v5 = ${v5} `) 284 | sql.append(SQL`WHERE v6 = ${v6} `) 285 | sql.append(SQL`AND v7 = ${v7}`) 286 | 287 | assert.equal( 288 | sql.text, 289 | 'TEST QUERY glue pieces FROM v1 = $1, v2 = $2, v3 = $3, v4 = $4, v5 = $5 WHERE v6 = $6 AND v7 = $7' 290 | ) 291 | assert.equal( 292 | sql.sql, 293 | 'TEST QUERY glue pieces FROM v1 = ?, v2 = ?, v3 = ?, v4 = ?, v5 = ? WHERE v6 = ? AND v7 = ?' 294 | ) 295 | assert.equal( 296 | sql.debug, 297 | "TEST QUERY glue pieces FROM v1 = 'v1', v2 = 'v2', v3 = 'v3', v4 = 'v4', v5 = 'v5' WHERE v6 = 'v6' AND v7 = 'v7'" 298 | ) 299 | assert.deepStrictEqual(sql.values, [v1, v2, v3, v4, v5, v6, v7]) 300 | }) 301 | 302 | test('SQL helper - build complex query with append passing simple strings and template strings', async t => { 303 | const v1 = 'v1' 304 | const v2 = 'v2' 305 | const v3 = 'v3' 306 | const v4 = 'v4' 307 | const v5 = 'v5' 308 | const v6 = 'v6' 309 | const v7 = 'v7' 310 | 311 | const sql = SQL`TEST QUERY glue pieces FROM ` 312 | sql.append(SQL`v1 = ${v1}, `) 313 | sql.append(SQL`v2 = ${v2}, `) 314 | sql.append(SQL`v3 = ${v3}, `) 315 | sql.append(SQL`v4 = ${v4}, `) 316 | sql.append(SQL`v5 = ${v5}, `) 317 | sql.append(SQL`v6 = v6 `) 318 | sql.append(SQL`WHERE v6 = ${v6} `) 319 | sql.append(SQL`AND v7 = ${v7} `) 320 | sql.append(SQL`AND v8 = v8`) 321 | 322 | assert.equal( 323 | sql.text, 324 | 'TEST QUERY glue pieces FROM v1 = $1, v2 = $2, v3 = $3, v4 = $4, v5 = $5, v6 = v6 WHERE v6 = $6 AND v7 = $7 AND v8 = v8' 325 | ) 326 | assert.equal( 327 | sql.debug, 328 | "TEST QUERY glue pieces FROM v1 = 'v1', v2 = 'v2', v3 = 'v3', v4 = 'v4', v5 = 'v5', v6 = v6 WHERE v6 = 'v6' AND v7 = 'v7' AND v8 = v8" 329 | ) 330 | assert.deepStrictEqual(sql.values, [v1, v2, v3, v4, v5, v6, v7]) 331 | }) 332 | 333 | test('SQL helper - will throw an error if append is called without using SQL', async t => { 334 | const sql = SQL`TEST QUERY glue pieces FROM ` 335 | try { 336 | sql.append('v1 = v1') 337 | t.fail('showld throw an error when passing strings not prefixed with SQL') 338 | } catch (e) { 339 | assert.equal( 340 | e.message, 341 | '"append" accepts only template string prefixed with SQL (SQL`...`)' 342 | ) 343 | } 344 | }) 345 | 346 | test('SQL helper - build string using append with and without unsafe flag', async t => { 347 | const v2 = 'v2' 348 | const longName = 'whateverThisIs' 349 | const sql = SQL`TEST QUERY glue pieces FROM test WHERE test1 == test2` 350 | sql.append(SQL` AND v1 = v1,`) 351 | sql.append(SQL` AND v2 = ${v2}, `) 352 | sql.append(SQL` AND v3 = ${longName}`, { unsafe: true }) 353 | sql.append(SQL` AND v4 = v4`, { unsafe: true }) 354 | 355 | assert.equal( 356 | sql.text, 357 | 'TEST QUERY glue pieces FROM test WHERE test1 == test2 AND v1 = v1, AND v2 = $1, AND v3 = whateverThisIs AND v4 = v4' 358 | ) 359 | assert.equal( 360 | sql.debug, 361 | "TEST QUERY glue pieces FROM test WHERE test1 == test2 AND v1 = v1, AND v2 = 'v2', AND v3 = whateverThisIs AND v4 = v4" 362 | ) 363 | assert.equal(sql.values.length, 1) 364 | assert.ok(sql.values.includes(v2)) 365 | }) 366 | 367 | test('SQL helper - build string using append and only unsafe', async t => { 368 | const v2 = 'v2' 369 | const longName = 'whateverThisIs' 370 | 371 | const sql = SQL`TEST QUERY glue pieces FROM test WHERE test1 == test2` 372 | assert.equal(sql.text, 'TEST QUERY glue pieces FROM test WHERE test1 == test2') 373 | 374 | sql.append(SQL` AND v1 = v1,`, { unsafe: true }) 375 | assert.equal( 376 | sql.text, 377 | 'TEST QUERY glue pieces FROM test WHERE test1 == test2 AND v1 = v1,' 378 | ) 379 | 380 | sql.append(SQL` AND v2 = ${v2} AND v3 = ${longName} AND v4 = 'v4'`, { 381 | unsafe: true 382 | }) 383 | assert.equal( 384 | sql.text, 385 | "TEST QUERY glue pieces FROM test WHERE test1 == test2 AND v1 = v1, AND v2 = v2 AND v3 = whateverThisIs AND v4 = 'v4'" 386 | ) 387 | assert.equal( 388 | sql.debug, 389 | "TEST QUERY glue pieces FROM test WHERE test1 == test2 AND v1 = v1, AND v2 = v2 AND v3 = whateverThisIs AND v4 = 'v4'" 390 | ) 391 | }) 392 | 393 | test('SQL helper - handles js null values as valid `null` sql values', async t => { 394 | const name = null 395 | const id = 123 396 | 397 | const sql = SQL`UPDATE teams SET name = ${name} WHERE id = ${id}` 398 | 399 | assert.equal(sql.text, 'UPDATE teams SET name = $1 WHERE id = $2') 400 | assert.equal(sql.sql, 'UPDATE teams SET name = ? WHERE id = ?') 401 | assert.equal(sql.debug, `UPDATE teams SET name = null WHERE id = ${id}`) 402 | assert.deepStrictEqual(sql.values, [name, id]) 403 | }) 404 | 405 | test('SQL helper - throws when building an sql string with an `undefined` value', async t => { 406 | assert.throws(() => SQL`UPDATE teams SET name = ${undefined}`) 407 | }) 408 | 409 | test('empty append', async t => { 410 | const sql = SQL`UPDATE teams SET name = ${'team'}`.append() 411 | 412 | assert.equal(sql.text, 'UPDATE teams SET name = $1') 413 | assert.equal(sql.sql, 'UPDATE teams SET name = ?') 414 | assert.equal(sql.debug, "UPDATE teams SET name = 'team'") 415 | assert.deepStrictEqual(sql.values, ['team']) 416 | }) 417 | 418 | test('inspect', async t => { 419 | const sql = SQL`UPDATE teams SET name = ${'team'}` 420 | assert.equal(util.inspect(sql), "SQL << UPDATE teams SET name = 'team' >>") 421 | }) 422 | 423 | test('quoteIdent', async t => { 424 | t.test('simple', async t => { 425 | const table = 'teams' 426 | const name = 'name' 427 | const id = 123 428 | 429 | const sql = SQL`UPDATE ${quoteIdent( 430 | table 431 | )} SET name = ${name} WHERE id = ${id}` 432 | 433 | assert.equal(sql.text, 'UPDATE "teams" SET name = $1 WHERE id = $2') 434 | assert.equal(sql.sql, 'UPDATE `teams` SET name = ? WHERE id = ?') 435 | assert.equal(sql.debug, `UPDATE "teams" SET name = 'name' WHERE id = ${id}`) 436 | assert.deepStrictEqual(sql.values, [name, id]) 437 | }) 438 | }) 439 | 440 | test('unsafe', async t => { 441 | const name = 'name' 442 | const id = 123 443 | 444 | const sql = SQL`UPDATE teams SET name = '${unsafe(name)}' WHERE id = ${id}` 445 | 446 | assert.equal(sql.text, "UPDATE teams SET name = 'name' WHERE id = $1") 447 | assert.equal(sql.sql, "UPDATE teams SET name = 'name' WHERE id = ?") 448 | assert.equal(sql.debug, `UPDATE teams SET name = 'name' WHERE id = ${id}`) 449 | assert.deepStrictEqual(sql.values, [id]) 450 | }) 451 | 452 | test('should be able to append query that is using "{ unsafe: true }"', async t => { 453 | const table = 'teams' 454 | const id = 123 455 | 456 | const reusableSql = SQL`SELECT id FROM` 457 | reusableSql.append(SQL` ${table}`, { unsafe: true }) 458 | reusableSql.append(SQL` WHERE id = ${id}`) 459 | 460 | const sql = SQL`SELECT * FROM` 461 | sql.append(SQL` ${table} `, { unsafe: true }) 462 | sql.append(SQL`INNER JOIN (`) 463 | sql.append(reusableSql) 464 | sql.append(SQL`) as t2 ON t2.id = id`) 465 | 466 | assert.equal( 467 | sql.text, 468 | 'SELECT * FROM teams INNER JOIN (SELECT id FROM teams WHERE id = $1) as t2 ON t2.id = id' 469 | ) 470 | assert.equal( 471 | sql.sql, 472 | 'SELECT * FROM teams INNER JOIN (SELECT id FROM teams WHERE id = ?) as t2 ON t2.id = id' 473 | ) 474 | assert.equal( 475 | sql.debug, 476 | `SELECT * FROM teams INNER JOIN (SELECT id FROM teams WHERE id = ${id}) as t2 ON t2.id = id` 477 | ) 478 | assert.deepStrictEqual(sql.values, [id]) 479 | }) 480 | 481 | test('should be able to append query that is using "quoteIdent(...)"', async t => { 482 | const table = 'teams' 483 | const id = 123 484 | 485 | const reusableSql = SQL`SELECT id FROM ${quoteIdent(table)} WHERE id = ${id}` 486 | 487 | const sql = SQL`SELECT * FROM ${quoteIdent(table)} INNER JOIN (` 488 | sql.append(reusableSql) 489 | sql.append(SQL`) as t2 ON t2.id = id`) 490 | 491 | assert.equal( 492 | sql.text, 493 | 'SELECT * FROM "teams" INNER JOIN (SELECT id FROM "teams" WHERE id = $1) as t2 ON t2.id = id' 494 | ) 495 | assert.equal( 496 | sql.sql, 497 | 'SELECT * FROM `teams` INNER JOIN (SELECT id FROM `teams` WHERE id = ?) as t2 ON t2.id = id' 498 | ) 499 | assert.equal( 500 | sql.debug, 501 | `SELECT * FROM "teams" INNER JOIN (SELECT id FROM "teams" WHERE id = ${id}) as t2 ON t2.id = id` 502 | ) 503 | assert.deepStrictEqual(sql.values, [id]) 504 | }) 505 | 506 | test('should be able to append a SqlStatement within a template literal', t => { 507 | const a = SQL`FROM table` 508 | const selectWithLiteralExpression = SQL`SELECT * ${a}` 509 | 510 | assert.equal(selectWithLiteralExpression.text, 'SELECT * FROM table') 511 | assert.equal(selectWithLiteralExpression.sql, 'SELECT * FROM table') 512 | assert.equal(selectWithLiteralExpression.debug, 'SELECT * FROM table') 513 | }) 514 | 515 | test('should be able to use SQL.glue within template literal', t => { 516 | const pre = 'A' 517 | const ids = [1, '2', 'three'] 518 | const idValues = ids.map(id => SQL`${id}`) 519 | const names = ['Bee', 'Cee', 'Dee'] 520 | const nameValues = names.map(name => SQL`${name}`) 521 | const post = 'B' 522 | const sql = SQL`UPDATE my_table SET active = FALSE WHERE pre=${pre} AND id IN (${SQL.glue( 523 | idValues, 524 | ',' 525 | )}) AND name IN (${SQL.glue(nameValues, ',')}) AND post=${post}` 526 | assert.equal( 527 | sql.text, 528 | 'UPDATE my_table SET active = FALSE WHERE pre=$1 AND id IN ($2,$3,$4) AND name IN ($5,$6,$7) AND post=$8' 529 | ) 530 | assert.equal( 531 | sql.sql, 532 | 'UPDATE my_table SET active = FALSE WHERE pre=? AND id IN (?,?,?) AND name IN (?,?,?) AND post=?' 533 | ) 534 | assert.equal( 535 | sql.debug, 536 | "UPDATE my_table SET active = FALSE WHERE pre='A' AND id IN (1,'2','three') AND name IN ('Bee','Cee','Dee') AND post='B'" 537 | ) 538 | assert.deepStrictEqual(sql.values, ['A', 1, '2', 'three', 'Bee', 'Cee', 'Dee', 'B']) 539 | }) 540 | 541 | test('should be able to use nested SQLStatements in template literal', t => { 542 | const a = 'A' 543 | const b = 'B' 544 | const c = 'C' 545 | const d = 'D' 546 | const sql = SQL`UPDATE my_table SET active = FALSE WHERE a=${a} AND ${SQL`b=${b} AND ${SQL`c=${c}`}`} AND d=${d}` 547 | assert.equal( 548 | sql.text, 549 | 'UPDATE my_table SET active = FALSE WHERE a=$1 AND b=$2 AND c=$3 AND d=$4' 550 | ) 551 | assert.equal( 552 | sql.sql, 553 | 'UPDATE my_table SET active = FALSE WHERE a=? AND b=? AND c=? AND d=?' 554 | ) 555 | assert.equal( 556 | sql.debug, 557 | "UPDATE my_table SET active = FALSE WHERE a='A' AND b='B' AND c='C' AND d='D'" 558 | ) 559 | assert.deepStrictEqual(sql.values, ['A', 'B', 'C', 'D']) 560 | }) 561 | 562 | test('should be able to use the result of SQL.glue([SQL``, SQL``], separator) result with multiple values inside the first element of SQL.glue', t => { 563 | const ids = [1, 2, 3] 564 | const name = 'foo' 565 | const inIds = SQL.glue( 566 | ids.map((id) => SQL`${id}`), 567 | ' , ' 568 | ) 569 | const condition = SQL`tsd.id IN (${inIds})` 570 | 571 | const filters = [ 572 | condition, 573 | SQL`tsd.name = ${name}` 574 | ] 575 | 576 | const sql = SQL`SELECT tsd.* FROM data tsd WHERE ${SQL.glue(filters, ' AND ')}` 577 | assert.equal( 578 | sql.text, 579 | 'SELECT tsd.* FROM data tsd WHERE tsd.id IN ($1 , $2 , $3) AND tsd.name = $4' 580 | ) 581 | assert.deepStrictEqual( 582 | sql.values, [1, 2, 3, 'foo'] 583 | ) 584 | assert.equal( 585 | sql.debug, 586 | "SELECT tsd.* FROM data tsd WHERE tsd.id IN (1 , 2 , 3) AND tsd.name = 'foo'" 587 | ) 588 | }) 589 | 590 | test('examples in the readme work as expected', t => { 591 | { 592 | const username = 'user1' 593 | const email = 'user1@email.com' 594 | const userId = 1 595 | 596 | const updates = [] 597 | updates.push(SQL`name = ${username}`) 598 | updates.push(SQL`email = ${email}`) 599 | 600 | const sql = SQL`UPDATE users SET ${SQL.glue( 601 | updates, 602 | ' , ' 603 | )} WHERE id = ${userId}` 604 | assert.equal(sql.text, 'UPDATE users SET name = $1 , email = $2 WHERE id = $3') 605 | } 606 | { 607 | const ids = [1, 2, 3] 608 | const value = 'test' 609 | const sql = SQL` 610 | UPDATE users 611 | SET property = ${value} 612 | WHERE id 613 | IN (${SQL.glue( 614 | ids.map(id => SQL`${id}`), 615 | ' , ' 616 | )}) 617 | ` 618 | assert.equal( 619 | sql.text, 620 | `UPDATE users 621 | SET property = $1 622 | WHERE id 623 | IN ($2 , $3 , $4)` 624 | ) 625 | } 626 | 627 | { 628 | const users = [ 629 | { id: 1, name: 'something' }, 630 | { id: 2, name: 'something-else' }, 631 | { id: 3, name: 'something-other' } 632 | ] 633 | 634 | const sql = SQL`INSERT INTO users (id, name) VALUES 635 | ${SQL.glue( 636 | users.map(user => SQL`(${user.id},${user.name}})`), 637 | ' , ' 638 | )} 639 | ` 640 | assert.equal( 641 | sql.text, 642 | `INSERT INTO users (id, name) VALUES 643 | ($1,$2}) , ($3,$4}) , ($5,$6})` 644 | ) 645 | } 646 | }) 647 | --------------------------------------------------------------------------------