├── .cz.json ├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ ├── codecov.yml │ ├── danger.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .huskyrc.js ├── .markdownlint.json ├── .npmrc ├── .nvmrc ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── dangerfile.ts ├── jest.config.js ├── lint-staged.config.js ├── package-lock.json ├── package.json ├── release.config.js ├── renovate.json ├── src ├── index.ts ├── pg-format.test.ts ├── pg-format.ts └── reserved.ts ├── tsconfig.build.json └── tsconfig.json /.cz.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "./node_modules/@scaleleap/utils/commitizen" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@scaleleap/utils/eslint') 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.json linguist-language=JSON-with-Comments -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | 3 | on: 4 | - push 5 | 6 | jobs: 7 | release: 8 | name: Upload coverage to Codecov 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | 14 | - name: Install 15 | uses: bahmutov/npm-install@v1 16 | 17 | - name: test 18 | run: npm test -- --coverage 19 | 20 | # - name: Upload coverage to Codecov 21 | # uses: codecov/codecov-action@v1 22 | # with: 23 | # token: ${{ secrets.CODECOV_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/workflows/danger.yml: -------------------------------------------------------------------------------- 1 | name: Danger 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | - reopened 9 | 10 | jobs: 11 | danger: 12 | name: Danger 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | 20 | - name: Install 21 | uses: bahmutov/npm-install@v1 22 | 23 | - name: Danger 24 | uses: danger/danger-js@10.6.4 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | env: 4 | CI: "true" 5 | 6 | on: 7 | push: 8 | branches: 9 | # https://semantic-release.gitbook.io/semantic-release/usage/configuration#branches 10 | - master 11 | - next 12 | - next-major 13 | - beta 14 | - alpha 15 | # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#patterns-to-match-branches-and-tags 16 | - '[0-9]+.x' # N.x 17 | - '[0-9]+.x.x' # N.x.x 18 | - '[0-9]+.[0-9]+.x' # N.N.x 19 | 20 | jobs: 21 | npm-publish: 22 | name: npm publish 23 | 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v2 29 | 30 | - name: Install 31 | uses: bahmutov/npm-install@v1 32 | 33 | - name: Build 34 | run: npm run build 35 | 36 | - name: Semantic Release 37 | if: success() 38 | env: 39 | GIT_AUTHOR_NAME: Scale Bot 40 | GIT_AUTHOR_EMAIL: scale-bot@scaleleap.com 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 45 | run: npm run semantic-release 46 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | env: 4 | CI: "true" 5 | 6 | on: 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | - reopened 12 | 13 | jobs: 14 | test: 15 | name: Test ${{ matrix.node }} and ${{ matrix.os }} 16 | 17 | runs-on: ${{ matrix.os }} 18 | 19 | strategy: 20 | matrix: 21 | node: 22 | - 12 23 | - 14 24 | - 16 25 | os: 26 | - ubuntu-latest 27 | - macOS-latest 28 | - windows-latest 29 | 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v2 33 | 34 | - name: Setup Node 35 | uses: actions/setup-node@v2 36 | with: 37 | node-version: ${{ matrix.node }} 38 | 39 | - name: Install 40 | uses: bahmutov/npm-install@v1 41 | 42 | - name: Lint 43 | run: npm run lint 44 | 45 | - name: Test 46 | run: npm t 47 | 48 | - name: Build 49 | run: npm run build 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | # Edit at https://www.gitignore.io/?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # dotenv environment variables file 71 | .env 72 | .env.test 73 | 74 | # parcel-bundler cache (https://parceljs.org/) 75 | .cache 76 | 77 | # next.js build output 78 | .next 79 | 80 | # nuxt.js build output 81 | .nuxt 82 | 83 | # rollup.js default build output 84 | dist/ 85 | 86 | # Uncomment the public line if your project uses Gatsby 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 89 | # public 90 | 91 | # Storybook build outputs 92 | .out 93 | .storybook-out 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # Temporary folders 108 | tmp/ 109 | temp/ 110 | 111 | # End of https://www.gitignore.io/api/node 112 | 113 | # TypeScript build output 114 | lib/ 115 | -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@scaleleap/utils/husky')(__dirname) 2 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/markdownlint/schema/markdownlint-config-schema.json", 3 | "extends": "./node_modules/@scaleleap/utils/markdownlint/markdownlint.json" 4 | } -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save=true 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14.17.0 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // Debugging Reference: https://code.visualstudio.com/docs/editor/debugging 2 | // Variables Reference: https://code.visualstudio.com/docs/editor/variables-reference 3 | { 4 | "version": "0.2.0", 5 | "configurations": [ 6 | { 7 | "name": "Debug Test", 8 | "type": "node", 9 | "request": "launch", 10 | "args": [ 11 | "--runInBand", 12 | "--no-cache" 13 | ], 14 | "env": { 15 | "CI": "true" 16 | }, 17 | "cwd": "${workspaceFolder}", 18 | "console": "integratedTerminal", 19 | "internalConsoleOptions": "neverOpen", 20 | "disableOptimisticBPs": true, 21 | "program": "${workspaceFolder}/node_modules/.bin/jest", 22 | "skipFiles": [ 23 | // black boxes vendor code from the debugger 24 | "${workspaceRoot}/node_modules/**/*.js", 25 | "${workspaceRoot}/lib/**/*.js", 26 | "async_hooks.js", 27 | "inspector_async_hook.js", 28 | // https://github.com/nodejs/node/issues/15464#issuecomment-332724821 29 | "/**", 30 | ] 31 | }, 32 | { 33 | "type": "node", 34 | "request": "launch", 35 | "name": "Debug Program", 36 | "console": "integratedTerminal", 37 | "runtimeArgs": [ 38 | "-r", 39 | "ts-node/register" 40 | ], 41 | "args": [ 42 | "${workspaceFolder}/src" 43 | ], 44 | "skipFiles": [ 45 | // black boxes vendor code from the debugger 46 | "${workspaceRoot}/node_modules/**/*.js", 47 | "${workspaceRoot}/lib/**/*.js", 48 | "async_hooks.js", 49 | "inspector_async_hook.js", 50 | // https://github.com/nodejs/node/issues/15464#issuecomment-332724821 51 | "/**", 52 | ] 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | ".cz.json": true, 4 | ".eslintcache": true, 5 | ".eslintrc.js": true, 6 | ".gitattributes": true, 7 | ".github": true, 8 | ".gitignore": true, 9 | ".huskyrc.js": true, 10 | ".markdownlint.json": true, 11 | ".npmrc": true, 12 | ".nvmrc": true, 13 | ".prettierrc.js": true, 14 | ".vscode": true, 15 | "commitlint.config.js": true, 16 | "jest.config.js": true, 17 | "lib": true, 18 | "LICENSE": true, 19 | "lint-staged.config.js": true, 20 | "node_modules": true, 21 | "package-lock.json": true, 22 | "release.config.js": true, 23 | "renovate.json": true, 24 | "tsconfig.build.json": true, 25 | "tsconfig.json": true 26 | }, 27 | "search.exclude": { 28 | "lib/**": true, 29 | "node_modules/**": true, 30 | ".eslintcache": true, 31 | }, 32 | "files.watcherExclude": { 33 | "lib/**": true, 34 | "node_modules/**": true, 35 | ".eslintcache": true, 36 | }, 37 | "typescript.tsdk": "node_modules/typescript/lib", 38 | "typescript.suggest.autoImports": true, 39 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # 1.0.0 (2021-05-23) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * npm audit ([b55ec2a](https://github.com/ScaleLeap/pg-format/commit/b55ec2aaf953d8b4f8a9a08cf967a3cf5603d337)) 12 | 13 | 14 | ### Features 15 | 16 | * adds package code ([e98de27](https://github.com/ScaleLeap/pg-format/commit/e98de27ed1b6f4a7c1f1b02d105f149004afc34a)) 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Scale Leap 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 | # 📦 @scaleleap/pg-format 2 | 3 | [![NPM](https://img.shields.io/npm/v/@scaleleap/pg-format)](https://npm.im/@scaleleap/pg-format) 4 | [![License](https://img.shields.io/npm/l/@scaleleap/pg-format)](./LICENSE) 5 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/ScaleLeap/pg-format/Release)](https://github.com/ScaleLeap/pg-format/actions) 6 | [![Codecov](https://img.shields.io/codecov/c/github/scaleleap/pg-format)](https://codecov.io/gh/ScaleLeap/pg-format) 7 | [![Snyk](https://img.shields.io/snyk/vulnerabilities/github/scaleleap/pg-format)](https://snyk.io/test/github/scaleleap/pg-format) 8 | [![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) 9 | 10 | --- 11 | 12 | A fully typed TypeScript and Node.js implementation of 13 | [PostgreSQL format()](http://www.postgresql.org/docs/9.3/static/functions-string.html#FUNCTIONS-STRING-FORMAT) 14 | to safely create dynamic SQL queries. SQL identifiers and literals are escaped to help prevent SQL 15 | injection. 16 | 17 | The behavior is equivalent to 18 | [PostgreSQL format()](http://www.postgresql.org/docs/9.3/static/functions-string.html#FUNCTIONS-STRING-FORMAT). 19 | This package also supports Node buffers, arrays, and objects which is explained [below](#arrobject). 20 | 21 | This package is a derivative of prior art. See Authors or Acknowledgments section below for details. 22 | 23 | --- 24 | 25 | This package does one, two and three. 26 | 27 | ## Download & Installation 28 | 29 | ```sh 30 | npm i -s @scaleleap/pg-format 31 | ``` 32 | 33 | ## Example 34 | 35 | ```ts 36 | import { format } from '@scaleleap/pg-format' 37 | const sql = format('SELECT * FROM %I WHERE my_col = %L %s', 'my_table', 34, 'LIMIT 10') 38 | console.log(sql); // SELECT * FROM my_table WHERE my_col = 34 LIMIT 10 39 | ``` 40 | 41 | ## API 42 | 43 | ### format(fmt, ...) 44 | 45 | Returns a formatted string based on ```fmt``` which has a style similar to the C function ```sprintf()```. 46 | 47 | * ```%%``` outputs a literal ```%``` character. 48 | * ```%I``` outputs an escaped SQL identifier. 49 | * ```%L``` outputs an escaped SQL literal. 50 | * ```%s``` outputs a simple string. 51 | 52 | #### Argument position 53 | 54 | You can define where an argument is positioned using ```n$``` where ```n``` is the argument index 55 | starting at 1. 56 | 57 | ```ts 58 | import { format } from '@scaleleap/pg-format' 59 | const sql = format('SELECT %1$L, %1$L, %L', 34, 'test') 60 | console.log(sql); // SELECT 34, 34, 'test' 61 | ``` 62 | 63 | ### format.config(cfg) 64 | 65 | Changes the global configuration. You can change which letters are used to denote identifiers, 66 | literals, and strings in the formatted string. This is useful when the formatted string contains a 67 | PL/pgSQL function which calls [PostgreSQL format()](http://www.postgresql.org/docs/9.3/static/functions-string.html#FUNCTIONS-STRING-FORMAT) 68 | itself. 69 | 70 | ```ts 71 | import { config } from '@scaleleap/pg-format' 72 | config({ 73 | pattern: { 74 | ident: 'V', 75 | literal: 'C', 76 | string: 't' 77 | } 78 | }) 79 | config() // reset to default 80 | ``` 81 | 82 | ### format.ident(input) 83 | 84 | Returns the input as an escaped SQL identifier string. `undefined`, ```null```, and objects will 85 | throw an error. 86 | 87 | ### format.literal(input) 88 | 89 | Returns the input as an escaped SQL literal string. ```undefined``` and ```null``` will return 90 | ```'NULL'```; 91 | 92 | ### format.string(input) 93 | 94 | Returns the input as a simple string. ```undefined``` and ```null``` will return an empty string. 95 | If an array element is ```undefined``` or ```null```, it will be removed from the output string. 96 | 97 | ### format.withArray(fmt, array) 98 | 99 | Same as ```format(fmt, ...)``` except parameters are provided in an array rather than as function 100 | arguments. This is useful when dynamically creating a SQL query and the number of parameters is 101 | unknown or variable. 102 | 103 | ## Node Buffers 104 | 105 | Node buffers can be used for literals (```%L```) and strings (```%s```), and will be converted to 106 | [PostgreSQL bytea hex format](http://www.postgresql.org/docs/9.3/static/datatype-binary.html). 107 | 108 | ## Arrays and Objects 109 | 110 | For arrays, each element is escaped when appropriate and concatenated to a comma-delimited string. 111 | Nested arrays are turned into grouped lists (for bulk inserts), e.g. `[['a', 'b'], ['c', 'd']]` 112 | turns into `('a', 'b'), ('c', 'd')`. Nested array expansion can be used for literals (```%L```) and 113 | strings (```%s```), but not identifiers (```%I```). 114 | 115 | For objects, ```JSON.stringify()``` is called and the resulting string is escaped if appropriate. 116 | Objects can be used for literals (```%L```) and strings (```%s```), but not identifiers (```%I```). 117 | See the example below. 118 | 119 | ```ts 120 | import { format } from '@scaleleap/pg-format' 121 | 122 | const myArray = [ 1, 2, 3 ] 123 | const myObject = { a: 1, b: 2 } 124 | const myNestedArray = [['a', 1], ['b', 2]] 125 | 126 | let sql = format('SELECT * FROM t WHERE c1 IN (%L) AND c2 = %L', myArray, myObject) 127 | console.log(sql) // SELECT * FROM t WHERE c1 IN (1,2,3) AND c2 = '{"a":1,"b":2}' 128 | 129 | sql = format('INSERT INTO t (name, age) VALUES %L', myNestedArray) 130 | console.log(sql) // INSERT INTO t (name, age) VALUES ('a', 1), ('b', 2) 131 | ``` 132 | 133 | ## Contributing 134 | 135 | This repository uses [Conventional Commit](https://www.conventionalcommits.org/) style commit messages. 136 | 137 | ## Authors or Acknowledgments 138 | 139 | * [TJ Holowaychuk](https://github.com/tj) for the original 140 | [pg-escape](https://github.com/segmentio/pg-escape) 141 | * [Datalanche, Inc](https://github.com/datalanche/node-pg-format) for 142 | [pg-format](https://github.com/datalanche/node-pg-format) 143 | * [Clint Phillips](https://github.com/cphillips/node-pg-format) for 144 | [node-pg-format](https://github.com/cphillips/node-pg-format), a TypeScript port of `pg-format` 145 | package. I borrowed most of the TypeScript code from `node-pg-format`. 146 | * [Roman Filippov](https://github.com/moltar) and 147 | [Scale Leap](https://www.scaleleap.com) for this package. 148 | 149 | ## License 150 | 151 | This project is licensed under the MIT License. 152 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@scaleleap/utils/commitlint') 2 | -------------------------------------------------------------------------------- /dangerfile.ts: -------------------------------------------------------------------------------- 1 | import { danger, warn } from 'danger' 2 | 3 | const packageChanged = danger.git.modified_files.includes('package.json') 4 | const lockfileChanged = danger.git.modified_files.includes('package-lock.json') 5 | 6 | if (packageChanged && !lockfileChanged) { 7 | const message = 'Changes were made to `package.json`, but not to `package-lock.json`.' 8 | warn(message) 9 | } 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | } 5 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@scaleleap/utils/lint-staged') 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@scaleleap/pg-format", 3 | "version": "1.0.0", 4 | "description": "A fully typed TypeScript and Node.js implementation of PostgreSQL format() to safely create dynamic SQL queries. SQL identifiers and literals are escaped to help prevent SQL injection.", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Roman Filippov", 8 | "email": "roman@scaleleap.com", 9 | "url": "https://www.scaleleap.com/" 10 | }, 11 | "homepage": "https://github.com/ScaleLeap/pg-format", 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:ScaleLeap/pg-format.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/ScaleLeap/pg-format/issues" 18 | }, 19 | "main": "lib/index.js", 20 | "files": [ 21 | "/lib" 22 | ], 23 | "scripts": { 24 | "prebuild": "npm run clean", 25 | "build": "tsc --build tsconfig.build.json", 26 | "clean": "rimraf lib/*", 27 | "dev": "ts-node-dev --respawn --transpileOnly src", 28 | "lint": "eslint --ext ts,js src/ test/", 29 | "lint:fix": "npm run lint -- --fix", 30 | "semantic-release": "npx @scaleleap/semantic-release-config", 31 | "start": "ts-node --transpile-only --pretty src", 32 | "test": "jest", 33 | "test:watch": "jest --watchAll" 34 | }, 35 | "types": "lib/index.d.ts", 36 | "devDependencies": { 37 | "@scaleleap/utils": "1.9.34", 38 | "@types/jest": "26.0.23", 39 | "@types/node": "13.13.51", 40 | "danger": "10.6.4", 41 | "jest": "26.6.3", 42 | "rimraf": "3.0.2", 43 | "ts-jest": "26.5.5", 44 | "tsconfigs": "4.0.2", 45 | "typescript": "4.2.4" 46 | }, 47 | "keywords": [ 48 | "escape", 49 | "format", 50 | "pg", 51 | "pg-escape", 52 | "pg-format", 53 | "postgres", 54 | "postgresql", 55 | "query", 56 | "sql injection" 57 | ], 58 | "publishConfig": { 59 | "access": "public" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@scaleleap/utils/semantic-release') 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "@scaleleap" 5 | ] 6 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pg-format' 2 | -------------------------------------------------------------------------------- /src/pg-format.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable radar/no-duplicate-string, eslint-comments/disable-enable-pair */ 2 | 3 | // 4 | // Original source from https://github.com/segmentio/pg-escape 5 | // 6 | import { format, ident, literal, string, withArray } from './pg-format' 7 | 8 | const NULL = JSON.parse('null') 9 | 10 | const testDate = new Date(Date.UTC(2012, 11, 14, 13, 6, 43, 152)) 11 | const testArray = [ 12 | 'abc', 13 | 1, 14 | true, 15 | NULL, 16 | testDate, 17 | Number.NEGATIVE_INFINITY, 18 | Number.POSITIVE_INFINITY, 19 | Number.NaN, 20 | 1n, 21 | ] 22 | const testIdentArray = [ 23 | 'abc', 24 | 'AbC', 25 | 1, 26 | true, 27 | testDate, 28 | Number.NEGATIVE_INFINITY, 29 | Number.POSITIVE_INFINITY, 30 | Number.NaN, 31 | 1n, 32 | ] 33 | const testObject = { a: 1, b: 2 } 34 | const testNestedArray = [ 35 | [1, 2], 36 | [3, 4], 37 | [5, 6], 38 | ] 39 | 40 | describe('format(fmt, ...)', () => { 41 | describe('%s', () => { 42 | it('should format as a simple string', () => { 43 | expect.assertions(2) 44 | 45 | expect(format('some %s here', 'thing')).toStrictEqual('some thing here') 46 | expect(format('some %s thing %s', 'long', 'here')).toStrictEqual('some long thing here') 47 | }) 48 | 49 | it('should format array of array as simple string', () => { 50 | expect.assertions(1) 51 | 52 | expect(format('many %s %s', 'things', testNestedArray)).toStrictEqual( 53 | 'many things (1, 2), (3, 4), (5, 6)', 54 | ) 55 | }) 56 | 57 | it('should format string using position field', () => { 58 | expect.assertions(6) 59 | 60 | expect(format('some %1$s', 'thing')).toStrictEqual('some thing') 61 | expect(format('some %1$s %1$s', 'thing')).toStrictEqual('some thing thing') 62 | expect(format('some %1$s %s', 'thing', 'again')).toStrictEqual('some thing again') 63 | expect(format('some %1$s %2$s', 'thing', 'again')).toStrictEqual('some thing again') 64 | expect(format('some %1$s %2$s %1$s', 'thing', 'again')).toStrictEqual( 65 | 'some thing again thing', 66 | ) 67 | expect(format('some %1$s %2$s %s %1$s', 'thing', 'again', 'some')).toStrictEqual( 68 | 'some thing again some thing', 69 | ) 70 | }) 71 | 72 | it('should not format string using position 0', () => { 73 | expect.assertions(1) 74 | 75 | expect(() => format('some %0$s', 'thing')).toThrow(Error) 76 | }) 77 | 78 | it('should not format string using position field with too few arguments', () => { 79 | expect.assertions(1) 80 | 81 | expect(() => format('some %2$s', 'thing')).toThrow(Error) 82 | }) 83 | }) 84 | 85 | describe('%%', () => { 86 | it('should format as %', () => { 87 | expect.assertions(1) 88 | 89 | expect(format('some %%', 'thing')).toStrictEqual('some %') 90 | }) 91 | 92 | it('should not eat args', () => { 93 | expect.assertions(1) 94 | 95 | expect(format('just %% a %s', 'test')).toStrictEqual('just % a test') 96 | }) 97 | 98 | it('should not format % using position field', () => { 99 | expect.assertions(1) 100 | 101 | expect(format('%1$%', 'thing')).toStrictEqual('%1$%') 102 | }) 103 | }) 104 | 105 | describe('%I', () => { 106 | it('should format as an identifier', () => { 107 | expect.assertions(1) 108 | 109 | expect(format('some %I', 'foo/bar/baz')).toStrictEqual('some "foo/bar/baz"') 110 | }) 111 | 112 | it('should not format array of array as an identifier', () => { 113 | expect.assertions(1) 114 | 115 | expect(() => format('many %I %I', 'foo/bar/baz', testNestedArray)).toThrow(Error) 116 | }) 117 | 118 | it('should format identifier using position field', () => { 119 | expect.assertions(6) 120 | 121 | expect(format('some %1$I', 'thing')).toStrictEqual('some thing') 122 | expect(format('some %1$I %1$I', 'thing')).toStrictEqual('some thing thing') 123 | expect(format('some %1$I %I', 'thing', 'again')).toStrictEqual('some thing again') 124 | expect(format('some %1$I %2$I', 'thing', 'again')).toStrictEqual('some thing again') 125 | expect(format('some %1$I %2$I %1$I', 'thing', 'again')).toStrictEqual( 126 | 'some thing again thing', 127 | ) 128 | expect(format('some %1$I %2$I %I %1$I', 'thing', 'again', 'huh')).toStrictEqual( 129 | 'some thing again huh thing', 130 | ) 131 | }) 132 | 133 | it('should not format identifier using position 0', () => { 134 | expect.assertions(1) 135 | 136 | expect(() => format('some %0$I', 'thing')).toThrow(Error) 137 | }) 138 | 139 | it('should not format identifier using position field with too few arguments', () => { 140 | expect.assertions(1) 141 | 142 | expect(() => format('some %2$I', 'thing')).toThrow(Error) 143 | }) 144 | }) 145 | 146 | describe('%L', () => { 147 | it('should format as a literal', () => { 148 | expect.assertions(1) 149 | 150 | expect(format('%L', "Tobi's")).toStrictEqual("'Tobi''s'") 151 | }) 152 | 153 | it('should format array of array as a literal', () => { 154 | expect.assertions(1) 155 | 156 | expect(format('%L', testNestedArray)).toStrictEqual('(1, 2), (3, 4), (5, 6)') 157 | }) 158 | 159 | it('should format literal using position field', () => { 160 | expect.assertions(6) 161 | 162 | expect(format('some %1$L', 'thing')).toStrictEqual("some 'thing'") 163 | expect(format('some %1$L %1$L', 'thing')).toStrictEqual("some 'thing' 'thing'") 164 | expect(format('some %1$L %L', 'thing', 'again')).toStrictEqual("some 'thing' 'again'") 165 | expect(format('some %1$L %2$L', 'thing', 'again')).toStrictEqual("some 'thing' 'again'") 166 | expect(format('some %1$L %2$L %1$L', 'thing', 'again')).toStrictEqual( 167 | "some 'thing' 'again' 'thing'", 168 | ) 169 | expect(format('some %1$L %2$L %L %1$L', 'thing', 'again', 'some')).toStrictEqual( 170 | "some 'thing' 'again' 'some' 'thing'", 171 | ) 172 | }) 173 | 174 | it('should not format literal using position 0', () => { 175 | expect.assertions(1) 176 | 177 | expect(() => format('some %0$L', 'thing')).toThrow(Error) 178 | }) 179 | 180 | it('should not format literal using position field with too few arguments', () => { 181 | expect.assertions(1) 182 | 183 | expect(() => format('some %2$L', 'thing')).toThrow(Error) 184 | }) 185 | }) 186 | }) 187 | 188 | describe('withArray (fmt, args)', () => { 189 | describe('%s', () => { 190 | it('should format as a simple string', () => { 191 | expect.assertions(2) 192 | 193 | expect(withArray('some %s here', ['thing'])).toStrictEqual('some thing here') 194 | expect(withArray('some %s thing %s', ['long', 'here'])).toStrictEqual('some long thing here') 195 | }) 196 | 197 | it('should format array of array as simple string', () => { 198 | expect.assertions(1) 199 | 200 | expect(withArray('many %s %s', ['things', testNestedArray])).toStrictEqual( 201 | 'many things (1, 2), (3, 4), (5, 6)', 202 | ) 203 | }) 204 | }) 205 | 206 | describe('%%', () => { 207 | it('should format as %', () => { 208 | expect.assertions(1) 209 | 210 | expect(withArray('some %%', ['thing'])).toStrictEqual('some %') 211 | }) 212 | 213 | it('should not eat args', () => { 214 | expect.assertions(2) 215 | 216 | expect(withArray('just %% a %s', ['test'])).toStrictEqual('just % a test') 217 | expect(withArray('just %% a %s %s %s', ['test', 'again', 'and again'])).toStrictEqual( 218 | 'just % a test again and again', 219 | ) 220 | }) 221 | }) 222 | 223 | describe('%I', () => { 224 | it('should format as an identifier', () => { 225 | expect.assertions(2) 226 | 227 | expect(withArray('some %I', ['foo/bar/baz'])).toStrictEqual('some "foo/bar/baz"') 228 | expect(withArray('some %I and %I', ['foo/bar/baz', '#hey'])).toStrictEqual( 229 | 'some "foo/bar/baz" and "#hey"', 230 | ) 231 | }) 232 | 233 | it('should not format array of array as an identifier', () => { 234 | expect.assertions(1) 235 | 236 | expect(() => withArray('many %I %I', ['foo/bar/baz', testNestedArray])).toThrow(Error) 237 | }) 238 | }) 239 | 240 | describe('%L', () => { 241 | it('should format as a literal', () => { 242 | expect.assertions(2) 243 | 244 | expect(withArray('%L', ["Tobi's"])).toStrictEqual("'Tobi''s'") 245 | expect(withArray('%L %L', ["Tobi's", 'birthday'])).toStrictEqual("'Tobi''s' 'birthday'") 246 | }) 247 | 248 | it('should format array of array as a literal', () => { 249 | expect.assertions(1) 250 | 251 | expect(withArray('%L', [testNestedArray])).toStrictEqual('(1, 2), (3, 4), (5, 6)') 252 | }) 253 | }) 254 | }) 255 | 256 | describe('string(val)', () => { 257 | it('should coerce to a string', () => { 258 | expect.assertions(14) 259 | 260 | expect(string()).toStrictEqual('') 261 | expect(string(NULL)).toStrictEqual('') 262 | expect(string(true)).toStrictEqual('t') 263 | expect(string(false)).toStrictEqual('f') 264 | expect(string(0)).toStrictEqual('0') 265 | expect(string(15)).toStrictEqual('15') 266 | expect(string(-15)).toStrictEqual('-15') 267 | expect(string(45.13)).toStrictEqual('45.13') 268 | expect(string(-45.13)).toStrictEqual('-45.13') 269 | expect(string('something')).toStrictEqual('something') 270 | expect(string(testArray)).toStrictEqual( 271 | 'abc,1,t,2012-12-14 13:06:43.152+00,-Infinity,Infinity,NaN,1', 272 | ) 273 | expect(string(testNestedArray)).toStrictEqual('(1, 2), (3, 4), (5, 6)') 274 | expect(string(testDate)).toStrictEqual('2012-12-14 13:06:43.152+00') 275 | expect(string(testObject)).toStrictEqual('{"a":1,"b":2}') 276 | }) 277 | }) 278 | 279 | describe('ident(val)', () => { 280 | it('should quote when necessary', () => { 281 | expect.assertions(5) 282 | 283 | expect(ident('foo')).toStrictEqual('foo') 284 | expect(ident('_foo')).toStrictEqual('_foo') 285 | expect(ident('_foo_bar$baz')).toStrictEqual('_foo_bar$baz') 286 | expect(ident('test.some.stuff')).toStrictEqual('"test.some.stuff"') 287 | expect(ident('test."some".stuff')).toStrictEqual('"test.""some"".stuff"') 288 | }) 289 | 290 | it('should quote reserved words', () => { 291 | expect.assertions(3) 292 | 293 | expect(ident('desc')).toStrictEqual('"desc"') 294 | expect(ident('join')).toStrictEqual('"join"') 295 | expect(ident('cross')).toStrictEqual('"cross"') 296 | }) 297 | 298 | it('should quote', () => { 299 | expect.assertions(10) 300 | 301 | expect(ident(true)).toStrictEqual('"t"') 302 | expect(ident(false)).toStrictEqual('"f"') 303 | expect(ident(0)).toStrictEqual('"0"') 304 | expect(ident(15)).toStrictEqual('"15"') 305 | expect(ident(-15)).toStrictEqual('"-15"') 306 | expect(ident(45.13)).toStrictEqual('"45.13"') 307 | expect(ident(-45.13)).toStrictEqual('"-45.13"') 308 | expect(ident(testIdentArray)).toStrictEqual( 309 | 'abc,"AbC","1","t","2012-12-14 13:06:43.152+00","-Infinity","Infinity","NaN","1"', 310 | ) 311 | expect(() => ident(testNestedArray)).toThrow(Error) 312 | expect(ident(testDate)).toStrictEqual('"2012-12-14 13:06:43.152+00"') 313 | }) 314 | 315 | it('should throw when undefined', () => { 316 | expect.assertions(1) 317 | 318 | expect(() => ident()).toThrow(/SQL identifier cannot be null or undefined/) 319 | }) 320 | 321 | it('should throw when null', () => { 322 | expect.assertions(1) 323 | 324 | expect(() => ident(NULL)).toThrow(/SQL identifier cannot be null or undefined/) 325 | }) 326 | 327 | it('should throw when object', () => { 328 | expect.assertions(1) 329 | 330 | expect(() => ident({})).toThrow(/SQL identifier cannot be an object/) 331 | }) 332 | }) 333 | 334 | describe('literal(val)', () => { 335 | it('should return NULL for null', () => { 336 | expect.assertions(2) 337 | 338 | expect(literal(NULL)).toStrictEqual('NULL') 339 | expect(literal()).toStrictEqual('NULL') 340 | }) 341 | 342 | it('should quote', () => { 343 | expect.assertions(12) 344 | 345 | expect(literal(true)).toStrictEqual("'t'") 346 | expect(literal(false)).toStrictEqual("'f'") 347 | expect(literal(0)).toStrictEqual('0') 348 | expect(literal(15)).toStrictEqual('15') 349 | expect(literal(-15)).toStrictEqual('-15') 350 | expect(literal(45.13)).toStrictEqual('45.13') 351 | expect(literal(-45.13)).toStrictEqual('-45.13') 352 | expect(literal('hello world')).toStrictEqual("'hello world'") 353 | expect(literal(testArray)).toStrictEqual( 354 | "'abc',1,'t',NULL,'2012-12-14 13:06:43.152+00','-Infinity','Infinity','NaN',1", 355 | ) 356 | expect(literal(testNestedArray)).toStrictEqual('(1, 2), (3, 4), (5, 6)') 357 | expect(literal(testDate)).toStrictEqual("'2012-12-14 13:06:43.152+00'") 358 | expect(literal(testObject)).toStrictEqual('\'{"a":1,"b":2}\'::jsonb') 359 | }) 360 | 361 | it('should format quotes', () => { 362 | expect.assertions(1) 363 | 364 | expect(literal("O'Reilly")).toStrictEqual("'O''Reilly'") 365 | }) 366 | 367 | it('should format backslashes', () => { 368 | expect.assertions(1) 369 | 370 | expect(literal('\\whoop\\')).toStrictEqual("E'\\\\whoop\\\\'") 371 | }) 372 | }) 373 | -------------------------------------------------------------------------------- /src/pg-format.ts: -------------------------------------------------------------------------------- 1 | import { POSTGRESQL_RESERVED_WORDS } from './reserved' 2 | 3 | interface PgFormatConfigPattern { 4 | ident: string 5 | literal: string 6 | string: string 7 | } 8 | 9 | export interface PgFormatConfig { 10 | pattern: PgFormatConfigPattern 11 | } 12 | 13 | const FMT_PATTERN_CONFIG: PgFormatConfigPattern = { 14 | ident: 'I', 15 | literal: 'L', 16 | string: 's', 17 | } 18 | 19 | // convert to Postgres default ISO 8601 format 20 | function formatDate(date: string): string { 21 | return date.replace('T', ' ').replace('Z', '+00') 22 | } 23 | 24 | function isReserved(value: string): boolean { 25 | if (POSTGRESQL_RESERVED_WORDS.has(value.toUpperCase())) { 26 | return true 27 | } 28 | return false 29 | } 30 | 31 | function arrayToList(useSpace: boolean, array: unknown[], formatter: (value: unknown) => string) { 32 | let sql = '' 33 | 34 | sql += useSpace ? ' (' : '(' 35 | for (const [index, element] of array.entries()) { 36 | sql += (index === 0 ? '' : ', ') + formatter(element) 37 | } 38 | sql += ')' 39 | 40 | return sql 41 | } 42 | 43 | // Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c 44 | // eslint-disable-next-line radar/cognitive-complexity 45 | export function ident(value?: unknown): string { 46 | if (value === undefined || value === null) { 47 | throw new Error('SQL identifier cannot be null or undefined') 48 | } else if (value === false) { 49 | return '"f"' 50 | } else if (value === true) { 51 | return '"t"' 52 | } else if (value instanceof Date) { 53 | return `"${formatDate(value.toISOString())}"` 54 | } else if (value instanceof Buffer) { 55 | throw new TypeError('SQL identifier cannot be a buffer') 56 | } else if (Array.isArray(value)) { 57 | const temporary: string[] = [] 58 | for (const element of value) { 59 | if (Array.isArray(element) === true) { 60 | throw new TypeError( 61 | 'Nested array to grouped list conversion is not supported for SQL identifier', 62 | ) 63 | } else { 64 | temporary.push(ident(element)) 65 | } 66 | } 67 | return temporary.toString() 68 | } else if (value === Object(value)) { 69 | throw new Error('SQL identifier cannot be an object') 70 | } 71 | 72 | const tident = String(value).slice(0) // create copy 73 | 74 | // do not quote a valid, unquoted identifier 75 | if (/^[_a-z][\d$_a-z]*$/.test(tident) === true && isReserved(tident) === false) { 76 | return tident 77 | } 78 | 79 | let quoted = '"' 80 | 81 | for (const c of tident) { 82 | quoted += c === '"' ? c + c : c 83 | } 84 | 85 | quoted += '"' 86 | 87 | return quoted 88 | } 89 | 90 | // Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c 91 | // eslint-disable-next-line radar/cognitive-complexity 92 | export function literal(value?: unknown): string { 93 | let tliteral = '' 94 | let explicitCast: string | undefined 95 | 96 | if (value === undefined || value === null) { 97 | return 'NULL' 98 | } 99 | if (typeof value === 'bigint') { 100 | return BigInt(value).toString() 101 | } 102 | if (value === Number.POSITIVE_INFINITY) { 103 | return "'Infinity'" 104 | } 105 | if (value === Number.NEGATIVE_INFINITY) { 106 | return "'-Infinity'" 107 | } 108 | if (Number.isNaN(value)) { 109 | return "'NaN'" 110 | } 111 | if (typeof value === 'number') { 112 | // Test must be AFTER other special case number tests 113 | return Number(value).toString() 114 | } 115 | if (value === false) { 116 | return "'f'" 117 | } 118 | if (value === true) { 119 | return "'t'" 120 | } 121 | if (value instanceof Date) { 122 | return `'${formatDate(value.toISOString())}'` 123 | } 124 | if (value instanceof Buffer) { 125 | return `E'\\\\x${value.toString('hex')}'` 126 | } 127 | if (Array.isArray(value)) { 128 | const temporary: string[] = [] 129 | for (const [index, element] of value.entries()) { 130 | if (Array.isArray(element) === true) { 131 | temporary.push(arrayToList(index !== 0, element, literal)) 132 | } else { 133 | temporary.push(literal(element)) 134 | } 135 | } 136 | return temporary.toString() 137 | } 138 | if (value === Object(value)) { 139 | explicitCast = 'jsonb' 140 | tliteral = JSON.stringify(value) 141 | } else { 142 | tliteral = String(value).slice(0) // create copy 143 | } 144 | 145 | let hasBackslash = false 146 | let quoted = "'" 147 | 148 | for (const c of tliteral) { 149 | if (c === "'") { 150 | quoted += c + c 151 | } else if (c === '\\') { 152 | quoted += c + c 153 | hasBackslash = true 154 | } else { 155 | quoted += c 156 | } 157 | } 158 | 159 | quoted += "'" 160 | 161 | if (hasBackslash === true) { 162 | quoted = `E${quoted}` 163 | } 164 | 165 | if (explicitCast) { 166 | quoted += `::${explicitCast}` 167 | } 168 | 169 | return quoted 170 | } 171 | 172 | // eslint-disable-next-line radar/cognitive-complexity 173 | export function string(value?: unknown): string { 174 | if (value === undefined || value === null) { 175 | return '' 176 | } 177 | if (value === false) { 178 | return 'f' 179 | } 180 | if (value === true) { 181 | return 't' 182 | } 183 | if (value instanceof Date) { 184 | return formatDate(value.toISOString()) 185 | } 186 | if (value instanceof Buffer) { 187 | return `\\x${value.toString('hex')}` 188 | } 189 | if (Array.isArray(value)) { 190 | const temporary: string[] = [] 191 | for (const [index, element] of value.entries()) { 192 | if (element !== null && element !== undefined) { 193 | if (Array.isArray(element) === true) { 194 | temporary.push(arrayToList(index !== 0, element, string)) 195 | } else { 196 | temporary.push(string(element)) 197 | } 198 | } 199 | } 200 | return temporary.toString() 201 | } 202 | if (value === Object(value)) { 203 | return JSON.stringify(value) 204 | } 205 | 206 | return String(value).toString().slice(0) // return copy 207 | } 208 | 209 | export function config(cfg: PgFormatConfig): void { 210 | // default 211 | FMT_PATTERN_CONFIG.ident = 'I' 212 | FMT_PATTERN_CONFIG.literal = 'L' 213 | FMT_PATTERN_CONFIG.string = 's' 214 | 215 | if (cfg && cfg.pattern) { 216 | if (cfg.pattern.ident) { 217 | FMT_PATTERN_CONFIG.ident = cfg.pattern.ident 218 | } 219 | if (cfg.pattern.literal) { 220 | FMT_PATTERN_CONFIG.literal = cfg.pattern.literal 221 | } 222 | if (cfg.pattern.string) { 223 | FMT_PATTERN_CONFIG.string = cfg.pattern.string 224 | } 225 | } 226 | } 227 | 228 | export function withArray(fmt: string, parameters: unknown[]): string { 229 | let index = 0 230 | 231 | let reText = '%(%|(\\d+\\$)?[' 232 | reText += FMT_PATTERN_CONFIG.ident 233 | reText += FMT_PATTERN_CONFIG.literal 234 | reText += FMT_PATTERN_CONFIG.string 235 | reText += '])' 236 | const re = new RegExp(reText, 'g') 237 | 238 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 239 | // @ts-ignore 240 | return fmt.replace(re, (_, type: string): string => { 241 | if (type === '%') { 242 | return '%' 243 | } 244 | 245 | let position = index 246 | const tokens = type.split('$') 247 | 248 | if (tokens.length > 1) { 249 | position = Number.parseInt(tokens[0], 10) - 1 250 | // eslint-disable-next-line no-param-reassign, prefer-destructuring 251 | type = tokens[1] 252 | } 253 | 254 | if (position < 0) { 255 | throw new Error('specified argument 0 but arguments start at 1') 256 | } else if (position > parameters.length - 1) { 257 | throw new Error('too few arguments') 258 | } 259 | 260 | index = position + 1 261 | 262 | if (type === FMT_PATTERN_CONFIG.ident) { 263 | return ident(parameters[position]) 264 | } 265 | if (type === FMT_PATTERN_CONFIG.literal) { 266 | return literal(parameters[position]) 267 | } 268 | if (type === FMT_PATTERN_CONFIG.string) { 269 | return string(parameters[position]) 270 | } 271 | }) 272 | } 273 | 274 | export function format(fmt: string, ...arguments_: unknown[]): string { 275 | return withArray(fmt, arguments_) 276 | } 277 | -------------------------------------------------------------------------------- /src/reserved.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PostgreSQL reserved words 3 | // see: https://www.postgresql.org/docs/current/sql-keywords-appendix.html 4 | // 5 | 6 | export const POSTGRESQL_RESERVED_WORDS = new Set([ 7 | 'AES128', 8 | 'AES256', 9 | 'ALL', 10 | 'ALLOWOVERWRITE', 11 | 'ANALYSE', 12 | 'ANALYZE', 13 | 'AND', 14 | 'ANY', 15 | 'ARRAY', 16 | 'AS', 17 | 'ASC', 18 | 'ASYMMETRIC', 19 | 'AUTHORIZATION', 20 | 'BACKUP', 21 | 'BETWEEN', 22 | 'BINARY', 23 | 'BLANKSASNULL', 24 | 'BOTH', 25 | 'BYTEDICT', 26 | 'CASE', 27 | 'CAST', 28 | 'CHECK', 29 | 'COLLATE', 30 | 'COLUMN', 31 | 'CONSTRAINT', 32 | 'CREATE', 33 | 'CREDENTIALS', 34 | 'CROSS', 35 | 'CURRENT_CATALOG', 36 | 'CURRENT_DATE', 37 | 'CURRENT_ROLE', 38 | 'CURRENT_TIME', 39 | 'CURRENT_TIMESTAMP', 40 | 'CURRENT_USER', 41 | 'CURRENT_USER_ID', 42 | 'DEFAULT', 43 | 'DEFERRABLE', 44 | 'DEFLATE', 45 | 'DEFRAG', 46 | 'DELTA', 47 | 'DELTA32K', 48 | 'DESC', 49 | 'DISABLE', 50 | 'DISTINCT', 51 | 'DO', 52 | 'ELSE', 53 | 'EMPTYASNULL', 54 | 'ENABLE', 55 | 'ENCODE', 56 | 'ENCRYPT', 57 | 'ENCRYPTION', 58 | 'END', 59 | 'EXCEPT', 60 | 'EXPLICIT', 61 | 'FALSE', 62 | 'FETCH', 63 | 'FOR', 64 | 'FOREIGN', 65 | 'FREEZE', 66 | 'FROM', 67 | 'FULL', 68 | 'GLOBALDICT256', 69 | 'GLOBALDICT64K', 70 | 'GRANT', 71 | 'GROUP', 72 | 'GZIP', 73 | 'HAVING', 74 | 'IDENTITY', 75 | 'IGNORE', 76 | 'ILIKE', 77 | 'IN', 78 | 'INITIALLY', 79 | 'INNER', 80 | 'INTERSECT', 81 | 'INTO', 82 | 'IS', 83 | 'ISNULL', 84 | 'JOIN', 85 | 'LATERAL', 86 | 'LEADING', 87 | 'LEFT', 88 | 'LIKE', 89 | 'LIMIT', 90 | 'LOCALTIME', 91 | 'LOCALTIMESTAMP', 92 | 'LUN', 93 | 'LUNS', 94 | 'LZO', 95 | 'LZOP', 96 | 'MINUS', 97 | 'MOSTLY13', 98 | 'MOSTLY32', 99 | 'MOSTLY8', 100 | 'NATURAL', 101 | 'NEW', 102 | 'NOT', 103 | 'NOTNULL', 104 | 'NULL', 105 | 'NULLS', 106 | 'OFF', 107 | 'OFFLINE', 108 | 'OFFSET', 109 | 'OLD', 110 | 'ON', 111 | 'ONLY', 112 | 'OPEN', 113 | 'OR', 114 | 'ORDER', 115 | 'OUTER', 116 | 'OVERLAPS', 117 | 'PARALLEL', 118 | 'PARTITION', 119 | 'PERCENT', 120 | 'PLACING', 121 | 'PRIMARY', 122 | 'RAW', 123 | 'READRATIO', 124 | 'RECOVER', 125 | 'REFERENCES', 126 | 'REJECTLOG', 127 | 'RESORT', 128 | 'RESTORE', 129 | 'RETURNING', 130 | 'RIGHT', 131 | 'SELECT', 132 | 'SESSION_USER', 133 | 'SIMILAR', 134 | 'SOME', 135 | 'SYMMETRIC', 136 | 'SYSDATE', 137 | 'SYSTEM', 138 | 'TABLE', 139 | 'TAG', 140 | 'TDES', 141 | 'TEXT255', 142 | 'TEXT32K', 143 | 'THEN', 144 | 'TO', 145 | 'TOP', 146 | 'TRAILING', 147 | 'TRUE', 148 | 'TRUNCATECOLUMNS', 149 | 'UNION', 150 | 'UNIQUE', 151 | 'USER', 152 | 'USING', 153 | 'VARIADIC', 154 | 'VERBOSE', 155 | 'WALLET', 156 | 'WHEN', 157 | 'WHERE', 158 | 'WINDOW', 159 | 'WITH', 160 | 'WITHOUT', 161 | ]) 162 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "exclude": [ 7 | "src/**/*.test.ts", 8 | "**/__test__/**", 9 | "test", 10 | "dangerfile.ts" 11 | ] 12 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfigs/nodejs-module", 3 | "compilerOptions": { 4 | "lib": [ 5 | "ES2020" 6 | ], 7 | "target": "ES2020" 8 | }, 9 | "include": [ 10 | "src/**/*.ts", 11 | "test/**/*.ts" 12 | ] 13 | } --------------------------------------------------------------------------------