├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Changelog.md ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json └── test └── example.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ master ] 7 | workflow_dispatch: 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | tests: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | node-version: [20.x, 22.x] 18 | 19 | name: Node.js ${{ matrix.node-version }} 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Set up Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | 29 | - name: Install npm dependencies 30 | run: npm install 31 | 32 | - name: Run tests 33 | run: npm test 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | name: release-please 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: google-github-actions/release-please-action@v3 11 | id: release 12 | with: 13 | release-type: node 14 | package-name: named-placeholders 15 | changelog-path: 'Changelog.md' 16 | 17 | - uses: actions/checkout@v3 18 | if: ${{ steps.release.outputs.release_created }} 19 | 20 | - uses: actions/setup-node@v3 21 | if: ${{ steps.release.outputs.release_created }} 22 | with: 23 | node-version: 16 24 | registry-url: 'https://registry.npmjs.org' 25 | 26 | - run: npm ci 27 | if: ${{ steps.release.outputs.release_created }} 28 | 29 | - run: npm publish 30 | if: ${{ steps.release.outputs.release_created }} 31 | env: 32 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | .DS_Store 10 | 11 | tmp 12 | pids 13 | logs 14 | 15 | node_modules 16 | npm-debug.log 17 | 18 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | 1.1.0 - 21/09/2015 2 | - exception is now thrown if parameters are missing 3 | 4 | 1.0.0 - 14/09/2015 5 | - added double-semicolon placeholders ( see https://github.com/sidorares/node-mysql2/issues/176 ) 6 | - added toNumbered helper ( postgres style $1 parameters ) 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Andrey Sidorov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM](https://nodei.co/npm/named-placeholders.png?downloads=true&stars=true)](https://nodei.co/npm/named-placeholders/) 2 | 3 | [![CI](https://github.com/mysqljs/named-placeholders/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/mysqljs/named-placeholders/actions/workflows/ci.yml) 4 | 5 | # named-placeholders 6 | 7 | compiles "select foo where foo.id = :bar and foo.baz < :baz" into "select foo where foo.id = ? and foo.baz < ?" + ["bar", "baz"] 8 | 9 | ## usage 10 | 11 | ```sh 12 | npm install named-placeholders 13 | ``` 14 | 15 | see [this mysql2 discussion](https://github.com/sidorares/node-mysql2/issues/117) 16 | 17 | ```js 18 | var mysql = require('mysql'); 19 | var toUnnamed = require('named-placeholders')(); 20 | 21 | var q = toUnnamed('select 1+:test', { test: 123}); 22 | mysql.createConnection().query(q[0], q[1]); 23 | ``` 24 | 25 | ## credits 26 | 27 | parser is based on @mscdex code of his excellent [node-mariasql](https://github.com/mscdex/node-mariasql) library 28 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // based on code from Brian White @mscdex mariasql library - https://github.com/mscdex/node-mariasql/blob/master/lib/Client.js#L272-L332 4 | // License: https://github.com/mscdex/node-mariasql/blob/master/LICENSE 5 | 6 | const RE_PARAM = /(?:\?)|(?::(\d+|(?:[a-zA-Z][a-zA-Z0-9_]*)))/g, 7 | DQUOTE = 34, 8 | SQUOTE = 39, 9 | BSLASH = 92; 10 | 11 | function parse(query) { 12 | let ppos = RE_PARAM.exec(query); 13 | let curpos = 0; 14 | let start = 0; 15 | let end; 16 | const parts = []; 17 | let inQuote = false; 18 | let escape = false; 19 | let qchr; 20 | const tokens = []; 21 | let qcnt = 0; 22 | let lastTokenEndPos = 0; 23 | let i; 24 | 25 | if (ppos) { 26 | do { 27 | for (i=curpos,end=ppos.index; i params[n])]; 176 | } 177 | 178 | module.exports = createCompiler; 179 | module.exports.toNumbered = toNumbered; 180 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "named-placeholders", 3 | "version": "1.1.3", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "named-placeholders", 9 | "version": "1.1.3", 10 | "license": "MIT", 11 | "dependencies": { 12 | "lru.min": "^1.1.0" 13 | }, 14 | "engines": { 15 | "node": ">=8.0.0" 16 | } 17 | }, 18 | "node_modules/lru.min": { 19 | "version": "1.1.0", 20 | "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.0.tgz", 21 | "integrity": "sha512-86xXMB6DiuKrTqkE/lRL0drlNh568awttBPJ7D66fzDHpy6NC5r3N+Ly/lKCS2zjmeGyvFDx670z0cD0PVBwGA==", 22 | "license": "MIT", 23 | "engines": { 24 | "bun": ">=1.0.0", 25 | "deno": ">=1.30.0", 26 | "node": ">=8.0.0" 27 | }, 28 | "funding": { 29 | "type": "github", 30 | "url": "https://github.com/sponsors/wellwelwel" 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "named-placeholders", 3 | "version": "1.1.3", 4 | "description": "sql named placeholders to unnamed compiler", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node --test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/sidorares/named-placeholders" 12 | }, 13 | "keywords": [ 14 | "sql", 15 | "pdo", 16 | "named", 17 | "placeholders" 18 | ], 19 | "engines": { 20 | "node": ">=8.0.0" 21 | }, 22 | "author": "Andrey Sidorov ", 23 | "files": [], 24 | "license": "MIT", 25 | "dependencies": { 26 | "lru.min": "^1.1.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { describe, it } = require('node:test'); 4 | const assert = require('node:assert'); 5 | 6 | const compile = require('..')(); 7 | 8 | describe('given input query with named parameters', () => { 9 | it('should build corresponding query with `?` placeholders and fill array of parameters from input object', () => { 10 | let query = 'Select users.json,EXISTS(Select 1 from moderators where moderators.id = :id) as is_moderator from users where users.id = :id and users.status = :status and users.complete_status = :complete_status'; 11 | 12 | const result1 = compile(query, {id: 123, status: 'Yes!', complete_status: 'No!'}); 13 | 14 | assert.deepEqual(result1, [ 'Select users.json,EXISTS(Select 1 from moderators where moderators.id = ?) as is_moderator from users where users.id = ? and users.status = ? and users.complete_status = ?', 15 | [ 123, 123, 'Yes!', 'No!' ] ]); 16 | 17 | // from https://github.com/sidorares/named-placeholders/issues/2 18 | query = 'SELECT * FROM items WHERE id = :id AND deleted = "0000-00-00 00:00:00"'; 19 | const result2 = compile(query, { id: Number(123) }) 20 | assert.deepEqual(result2, [ 'SELECT * FROM items WHERE id = ? AND deleted = "0000-00-00 00:00:00"', 21 | [ 123 ] ]); 22 | 23 | query = 'SELECT * FROM items WHERE deleted = "0000-00-00 00:00:00" AND id = :id'; 24 | const result3 = compile(query, { id: Number(123) }) 25 | assert.deepEqual(result3, [ 'SELECT * FROM items WHERE deleted = "0000-00-00 00:00:00" AND id = ?', 26 | [ 123 ] ]); 27 | }); 28 | 29 | it('should throw error when query contains placeholders but parameters object not passed', () => { 30 | const query = 'test ::p2 test :p1'; 31 | 32 | assert.throws( 33 | () => { 34 | compile(query); 35 | }, 36 | /Named query contains placeholders, but parameters object is undefined/ 37 | ); 38 | }); 39 | 40 | it('should replace ::name style placeholders with `??` placeholders', () => { 41 | let query = 'normal placeholder :p1 and double semicolon ::p2'; 42 | assert.deepEqual(compile(query, {p1: 'test1', p2: 'test2'}), 43 | [ 'normal placeholder ? and double semicolon ??', [ 'test1', 'test2' ] ]); 44 | 45 | query = 'normal placeholder ::p1 and double semicolon :p2'; 46 | assert.deepEqual(compile(query, {p1: 'test1', p2: 'test2'}), 47 | [ 'normal placeholder ?? and double semicolon ?', [ 'test1', 'test2' ] ]); 48 | 49 | query = 'normal placeholder ::p2 and double semicolon :p1'; 50 | assert.deepEqual(compile(query, {p1: 'test1', p2: 'test2'}), 51 | [ 'normal placeholder ?? and double semicolon ?', [ 'test2', 'test1' ] ]); 52 | 53 | query = 'normal placeholder :p1 and double semicolon ::p2 test'; 54 | assert.deepEqual(compile(query, {p1: 'test1', p2: 'test2'}), 55 | [ 'normal placeholder ? and double semicolon ?? test', [ 'test1', 'test2' ] ]); 56 | }); 57 | 58 | it('compiles the query the same twice', () => { 59 | const query = 'SELECT * FROM foo WHERE id = :id'; 60 | const expected = [ 'SELECT * FROM foo WHERE id = ?', [ 123 ] ]; 61 | assert.deepEqual(compile(query, { id: 123 }), expected); 62 | assert.deepEqual(compile(query, { id: 123 }), expected); 63 | }); 64 | }); 65 | 66 | describe('postgres-style toNumbered conversion', () => { 67 | it('basic test', () => { 68 | const toNumbered = require('..').toNumbered; 69 | const query = 'SELECT usr.p_pause_stop_track(:doc_dtl_id, :plan_id, :wc_id, 20, :time_from)'; 70 | assert.deepEqual(toNumbered(query, { 71 | doc_dtl_id: 123, 72 | time_from: 345, 73 | plan_id: 456, 74 | wc_id: 678 75 | }), [ 'SELECT usr.p_pause_stop_track($1, $2, $3, 20, $4)', [ 123, 456, 678, 345 ]]); 76 | }); 77 | }); --------------------------------------------------------------------------------