Document client.escapeIdentifier and client.escapeLiteral (#2954)

* Document client.escapeIdentifier and client.escapeLiteral

Per #1978 it seems that these client APIs are undocumented. Added documentation for these functions along with some examples and relevant links.

* Fix typos in new docs

* Migrate escapeIdentifier and escapeLiteral from Client to PG

These are standalone utility functions, they do not need a client instance to function.

Changes made:
- Refactored escapeIdentifer and escapeLiteral from client class to functions in utils
- Update PG to export  escapeIdentifier and escapeLiteral
- Migrated tests for Client.escapeIdentifier and Client.escapeLiteral to tests for utils
- Updated documentation, added a "utilities" page where these helpers are discussed

**note** this is a breaking change. Users who used these functions (previously undocumented) on instances of Client, or via Client.prototype.

* Export escapeIdentifier and escapeLiteral from PG

These are standalone utility functions, they should not depend on a client instance.

Changes made:
- Refactored escapeIdentifer and escapeLiteral from client class to functions in utils
- Re-exported functions on client for backwards compatibility
- Update PG to export  escapeIdentifier and escapeLiteral
- Updated tests to validate the newly exported functions from both entry points
- Updated documentation, added a "utilities" page where these helpers are discussed

* Ensure escape functions work via Client.prototype

Updated changes such that escapeIdentifier and escapeLiteral are usable via the client prototype
Updated tests to check for both entry points in client
This commit is contained in:
Conner 2023-05-02 08:55:59 -04:00 committed by GitHub
parent d63c761be8
commit 249182ea9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 150 additions and 26 deletions

View File

@ -3,5 +3,6 @@
"pool": "pg.Pool",
"result": "pg.Result",
"types": "pg.Types",
"cursor": "Cursor"
"cursor": "Cursor",
"utilities": "Utilities"
}

View File

@ -0,0 +1,30 @@
---
title: Utilities
---
import { Alert } from '/components/alert.tsx'
## Utility Functions
### pg.escapeIdentifier
Escapes a string as a [SQL identifier](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS).
```js
const { escapeIdentifier } = require('pg')
const escapedIdentifier = escapeIdentifier('FooIdentifier')
console.log(escapedIdentifier) // '"FooIdentifier"'
```
### pg.escapeLiteral
<Alert>
**Note**: Instead of manually escaping SQL literals, it is recommended to use parameterized queries. Refer to [parameterized queries](/features/queries#parameterized-query) and the [client.query](/apis/client#clientquery) API for more information.
</Alert>
Escapes a string as a [SQL literal](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS).
```js
const { escapeLiteral } = require('pg')
const escapedLiteral = escapeLiteral("hello 'world'")
console.log(escapedLiteral) // "'hello ''world'''"
```

View File

@ -456,35 +456,15 @@ class Client extends EventEmitter {
return this._types.getTypeParser(oid, format)
}
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
// escapeIdentifier and escapeLiteral moved to utility functions & exported
// on PG
// re-exported here for backwards compatibility
escapeIdentifier(str) {
return '"' + str.replace(/"/g, '""') + '"'
return utils.escapeIdentifier(str)
}
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
escapeLiteral(str) {
var hasBackslash = false
var escaped = "'"
for (var i = 0; i < str.length; i++) {
var c = str[i]
if (c === "'") {
escaped += c + c
} else if (c === '\\') {
escaped += c + c
hasBackslash = true
} else {
escaped += c
}
}
escaped += "'"
if (hasBackslash === true) {
escaped = ' E' + escaped
}
return escaped
return utils.escapeLiteral(str)
}
_pulseQueryQueue() {

View File

@ -5,6 +5,7 @@ var defaults = require('./defaults')
var Connection = require('./connection')
var Pool = require('pg-pool')
const { DatabaseError } = require('pg-protocol')
const { escapeIdentifier, escapeLiteral } = require('./utils')
const poolFactory = (Client) => {
return class BoundPool extends Pool {
@ -23,6 +24,8 @@ var PG = function (clientConstructor) {
this.Connection = Connection
this.types = require('pg-types')
this.DatabaseError = DatabaseError
this.escapeIdentifier = escapeIdentifier
this.escapeLiteral = escapeLiteral
}
if (typeof process.env.NODE_PG_FORCE_NATIVE !== 'undefined') {

View File

@ -175,6 +175,38 @@ const postgresMd5PasswordHash = function (user, password, salt) {
return 'md5' + outer
}
// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c
const escapeIdentifier = function (str) {
return '"' + str.replace(/"/g, '""') + '"'
}
const escapeLiteral = function (str) {
var hasBackslash = false
var escaped = "'"
for (var i = 0; i < str.length; i++) {
var c = str[i]
if (c === "'") {
escaped += c + c
} else if (c === '\\') {
escaped += c + c
hasBackslash = true
} else {
escaped += c
}
}
escaped += "'"
if (hasBackslash === true) {
escaped = ' E' + escaped
}
return escaped
}
module.exports = {
prepareValue: function prepareValueWrapper(value) {
// this ensures that extra arguments do not get passed into prepareValue
@ -184,4 +216,6 @@ module.exports = {
normalizeQueryConfig,
postgresMd5PasswordHash,
md5,
escapeIdentifier,
escapeLiteral
}

View File

@ -1,5 +1,6 @@
'use strict'
var helper = require('./test-helper')
var utils = require('../../../lib/utils')
function createClient(callback) {
var client = new Client(helper.config)
@ -14,6 +15,17 @@ var testLit = function (testName, input, expected) {
var actual = client.escapeLiteral(input)
assert.equal(expected, actual)
})
test('Client.prototype.' + testName, function () {
var actual = Client.prototype.escapeLiteral(input)
assert.equal(expected, actual)
})
test('utils.' + testName, function () {
var actual = utils.escapeLiteral(input)
assert.equal(expected, actual)
})
}
var testIdent = function (testName, input, expected) {
@ -22,6 +34,17 @@ var testIdent = function (testName, input, expected) {
var actual = client.escapeIdentifier(input)
assert.equal(expected, actual)
})
test('Client.prototype.' + testName, function () {
var actual = Client.prototype.escapeIdentifier(input)
assert.equal(expected, actual)
})
test('utils.' + testName, function () {
var actual = utils.escapeIdentifier(input)
assert.equal(expected, actual)
})
}
testLit('escapeLiteral: no special characters', 'hello world', "'hello world'")

View File

@ -239,3 +239,56 @@ test('prepareValue: can safely be used to map an array of values including those
var out = values.map(utils.prepareValue)
assert.deepEqual(out, [1, 'test', 'zomgcustom!'])
})
var testEscapeLiteral = function (testName, input, expected) {
test(testName, function () {
var actual = utils.escapeLiteral(input)
assert.equal(expected, actual)
})
}
testEscapeLiteral('escapeLiteral: no special characters', 'hello world', "'hello world'")
testEscapeLiteral('escapeLiteral: contains double quotes only', 'hello " world', "'hello \" world'")
testEscapeLiteral('escapeLiteral: contains single quotes only', "hello ' world", "'hello '' world'")
testEscapeLiteral('escapeLiteral: contains backslashes only', 'hello \\ world', " E'hello \\\\ world'")
testEscapeLiteral('escapeLiteral: contains single quotes and double quotes', 'hello \' " world', "'hello '' \" world'")
testEscapeLiteral('escapeLiteral: contains double quotes and backslashes', 'hello \\ " world', " E'hello \\\\ \" world'")
testEscapeLiteral('escapeLiteral: contains single quotes and backslashes', "hello \\ ' world", " E'hello \\\\ '' world'")
testEscapeLiteral(
'escapeLiteral: contains single quotes, double quotes, and backslashes',
'hello \\ \' " world',
" E'hello \\\\ '' \" world'"
)
var testEscapeIdentifier = function (testName, input, expected) {
test(testName, function () {
var actual = utils.escapeIdentifier(input)
assert.equal(expected, actual)
})
}
testEscapeIdentifier('escapeIdentifier: no special characters', 'hello world', '"hello world"')
testEscapeIdentifier('escapeIdentifier: contains double quotes only', 'hello " world', '"hello "" world"')
testEscapeIdentifier('escapeIdentifier: contains single quotes only', "hello ' world", '"hello \' world"')
testEscapeIdentifier('escapeIdentifier: contains backslashes only', 'hello \\ world', '"hello \\ world"')
testEscapeIdentifier('escapeIdentifier: contains single quotes and double quotes', 'hello \' " world', '"hello \' "" world"')
testEscapeIdentifier('escapeIdentifier: contains double quotes and backslashes', 'hello \\ " world', '"hello \\ "" world"')
testEscapeIdentifier('escapeIdentifier: contains single quotes and backslashes', "hello \\ ' world", '"hello \\ \' world"')
testEscapeIdentifier(
'escapeIdentifier: contains single quotes, double quotes, and backslashes',
'hello \\ \' " world',
'"hello \\ \' "" world"'
)