Skip to content

Commit 14abdc3

Browse files
authored
chore: safe quoteIdent (#50)
1 parent de4e3ba commit 14abdc3

File tree

11 files changed

+145
-71
lines changed

11 files changed

+145
-71
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,11 @@ const sql = SQL`
153153

154154
### quoteIdent(value)
155155

156-
Does a literal interpolation of the provided value, interpreting the provided value as-is and additionally wrapping it in double quotes. It uses `unsafe` internally.
156+
Mimics the native PostgreSQL `quote_ident` and MySQL `quote_identifier` functions.
157+
158+
In PostgreSQL, it wraps the provided value in double quotes `"` and escapes any double quotes existing in the provided value.
159+
160+
In MySQL, it wraps the provided value in backticks `` ` `` and escapes any backticks existing in the provided value.
157161

158162
It's convenient to use when schema, table or field names are dynamic and can't be hardcoded in the SQL query string.
159163

SQL.js

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
'use strict'
22
const inspect = Symbol.for('nodejs.util.inspect.custom')
3-
const unsafe = Symbol('unsafe')
3+
const wrapped = Symbol('wrapped')
4+
5+
const quoteIdentifier = require('./quoteIdentifier')
46

57
class SqlStatement {
68
constructor (strings, values) {
@@ -52,8 +54,8 @@ class SqlStatement {
5254
const valueIndex = i - 1 + valueOffset
5355
const valueContainer = values[valueIndex]
5456

55-
if (valueContainer && valueContainer[unsafe]) {
56-
text += `${valueContainer.value}${this.strings[i]}`
57+
if (valueContainer && valueContainer[wrapped]) {
58+
text += `${valueContainer.transform(type)}${this.strings[i]}`
5759
values.splice(valueIndex, 1)
5860
valueOffset--
5961
} else {
@@ -75,8 +77,8 @@ class SqlStatement {
7577
for (let i = 1; i < this.strings.length; i++) {
7678
let data = this._values[i - 1]
7779
let quote = "'"
78-
if (data && data[unsafe]) {
79-
data = data.value
80+
if (data && data[wrapped]) {
81+
data = data.transform()
8082
quote = ''
8183
}
8284
typeof data === 'string' ? (text += quote + data + quote) : (text += data)
@@ -99,7 +101,7 @@ class SqlStatement {
99101
}
100102

101103
get values () {
102-
return this._values.filter(v => !v || !v[unsafe])
104+
return this._values.filter(v => !v || !v[wrapped])
103105
}
104106

105107
append (statement, options) {
@@ -148,7 +150,14 @@ module.exports = SQL
148150
module.exports.SQL = SQL
149151
module.exports.default = SQL
150152
module.exports.unsafe = value => ({
151-
value,
152-
[unsafe]: true
153+
transform () {
154+
return value
155+
},
156+
[wrapped]: true
157+
})
158+
module.exports.quoteIdent = value => ({
159+
transform (type) {
160+
return quoteIdentifier(value, type)
161+
},
162+
[wrapped]: true
153163
})
154-
module.exports.quoteIdent = value => module.exports.unsafe(`"${value}"`)

SQL.test.js

Lines changed: 34 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const SQL = require('./SQL')
77
const unsafe = SQL.unsafe
88
const quoteIdent = SQL.quoteIdent
99

10-
test('SQL helper - build complex query with append', t => {
10+
test('SQL helper - build complex query with append', async t => {
1111
const name = 'Team 5'
1212
const description = 'description'
1313
const teamId = 7
@@ -29,10 +29,9 @@ test('SQL helper - build complex query with append', t => {
2929
`UPDATE teams SET name = '${name}', description = '${description}' WHERE id = ${teamId} AND org_id = '${organizationId}'`
3030
)
3131
t.same(sql.values, [name, description, teamId, organizationId])
32-
t.end()
3332
})
3433

35-
test('SQL helper - multiline', t => {
34+
test('SQL helper - multiline', async t => {
3635
const name = 'Team 5'
3736
const description = 'description'
3837
const teamId = 7
@@ -56,11 +55,9 @@ test('SQL helper - multiline', t => {
5655
`UPDATE teams SET name = '${name}', description = '${description}'\nWHERE id = ${teamId} AND org_id = '${organizationId}'`
5756
)
5857
t.same(sql.values, [name, description, teamId, organizationId])
59-
60-
t.end()
6158
})
6259

63-
test('SQL helper - multiline with emtpy lines', t => {
60+
test('SQL helper - multiline with emtpy lines', async t => {
6461
const name = 'Team 5'
6562
const description = 'description'
6663
const teamId = 7
@@ -86,10 +83,9 @@ test('SQL helper - multiline with emtpy lines', t => {
8683
`UPDATE teams SET name = '${name}', description = '${description}'\nWHERE id = ${teamId} AND org_id = '${organizationId}'\nRETURNING id`
8784
)
8885
t.same(sql.values, [name, description, teamId, organizationId])
89-
t.end()
9086
})
9187

92-
test('SQL helper - build complex query with glue', t => {
88+
test('SQL helper - build complex query with glue', async t => {
9389
const name = 'Team 5'
9490
const description = 'description'
9591
const teamId = 7
@@ -117,10 +113,9 @@ test('SQL helper - build complex query with glue', t => {
117113
`UPDATE teams SET name = '${name}' , description = '${description}' WHERE id = ${teamId} AND org_id = '${organizationId}'`
118114
)
119115
t.same(sql.values, [name, description, teamId, organizationId])
120-
t.end()
121116
})
122117

123-
test('SQL helper - build complex query with glue - regression #13', t => {
118+
test('SQL helper - build complex query with glue - regression #13', async t => {
124119
const name = 'Team 5'
125120
const ids = [1, 2, 3].map(id => SQL`${id}`)
126121

@@ -136,10 +131,9 @@ test('SQL helper - build complex query with glue - regression #13', t => {
136131
`UPDATE teams SET name = '${name}' WHERE id IN (1 , 2 , 3 )`
137132
)
138133
t.same(sql.values, [name, 1, 2, 3])
139-
t.end()
140134
})
141135

142-
test('SQL helper - build complex query with glue - regression #17', t => {
136+
test('SQL helper - build complex query with glue - regression #17', async t => {
143137
const ids = [1, 2, 3].map(id => SQL`(${id})`)
144138

145139
const sql = SQL`INSERT INTO users (id) VALUES `
@@ -149,10 +143,9 @@ test('SQL helper - build complex query with glue - regression #17', t => {
149143
t.equal(sql.sql, 'INSERT INTO users (id) VALUES (?) , (?) , (?)')
150144
t.equal(sql.debug, 'INSERT INTO users (id) VALUES (1) , (2) , (3)')
151145
t.same(sql.values, [1, 2, 3])
152-
t.end()
153146
})
154147

155-
test('SQL helper - build complex query with static glue - regression #17', t => {
148+
test('SQL helper - build complex query with static glue - regression #17', async t => {
156149
const ids = [1, 2, 3].map(id => SQL`(${id})`)
157150

158151
const sql = SQL`INSERT INTO users (id) VALUES `
@@ -162,10 +155,9 @@ test('SQL helper - build complex query with static glue - regression #17', t =>
162155
t.equal(sql.sql, 'INSERT INTO users (id) VALUES (?) , (?) , (?)')
163156
t.equal(sql.debug, 'INSERT INTO users (id) VALUES (1) , (2) , (3)')
164157
t.same(sql.values, [1, 2, 3])
165-
t.end()
166158
})
167159

168-
test('SQL helper - build complex query with append and glue', t => {
160+
test('SQL helper - build complex query with append and glue', async t => {
169161
const updates = []
170162
const v1 = 'v1'
171163
const v2 = 'v2'
@@ -199,10 +191,9 @@ test('SQL helper - build complex query with append and glue', t => {
199191
"TEST QUERY glue pieces FROM v1 = 'v1' , v2 = 'v2' , v3 = 'v3' , v4 = 'v4' , v5 = 'v5' WHERE v6 = 'v6' AND v7 = 'v7'"
200192
)
201193
t.same(sql.values, [v1, v2, v3, v4, v5, v6, v7])
202-
t.end()
203194
})
204195

205-
test('SQL helper - build complex query with append', t => {
196+
test('SQL helper - build complex query with append', async t => {
206197
const v1 = 'v1'
207198
const v2 = 'v2'
208199
const v3 = 'v3'
@@ -233,10 +224,9 @@ test('SQL helper - build complex query with append', t => {
233224
"TEST QUERY glue pieces FROM v1 = 'v1', v2 = 'v2', v3 = 'v3', v4 = 'v4', v5 = 'v5' WHERE v6 = 'v6' AND v7 = 'v7'"
234225
)
235226
t.same(sql.values, [v1, v2, v3, v4, v5, v6, v7])
236-
t.end()
237227
})
238228

239-
test('SQL helper - build complex query with append passing simple strings and template strings', t => {
229+
test('SQL helper - build complex query with append passing simple strings and template strings', async t => {
240230
const v1 = 'v1'
241231
const v2 = 'v2'
242232
const v3 = 'v3'
@@ -265,10 +255,9 @@ test('SQL helper - build complex query with append passing simple strings and te
265255
"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"
266256
)
267257
t.same(sql.values, [v1, v2, v3, v4, v5, v6, v7])
268-
t.end()
269258
})
270259

271-
test('SQL helper - will throw an error if append is called without using SQL', t => {
260+
test('SQL helper - will throw an error if append is called without using SQL', async t => {
272261
const sql = SQL`TEST QUERY glue pieces FROM `
273262
try {
274263
sql.append('v1 = v1')
@@ -279,10 +268,9 @@ test('SQL helper - will throw an error if append is called without using SQL', t
279268
'"append" accepts only template string prefixed with SQL (SQL`...`)'
280269
)
281270
}
282-
t.end()
283271
})
284272

285-
test('SQL helper - build string using append with and without unsafe flag', t => {
273+
test('SQL helper - build string using append with and without unsafe flag', async t => {
286274
const v2 = 'v2'
287275
const longName = 'whateverThisIs'
288276
const sql = SQL`TEST QUERY glue pieces FROM test WHERE test1 == test2`
@@ -301,10 +289,9 @@ test('SQL helper - build string using append with and without unsafe flag', t =>
301289
)
302290
t.equal(sql.values.length, 1)
303291
t.ok(sql.values.includes(v2))
304-
t.end()
305292
})
306293

307-
test('SQL helper - build string using append and only unsafe', t => {
294+
test('SQL helper - build string using append and only unsafe', async t => {
308295
const v2 = 'v2'
309296
const longName = 'whateverThisIs'
310297

@@ -328,11 +315,9 @@ test('SQL helper - build string using append and only unsafe', t => {
328315
sql.debug,
329316
"TEST QUERY glue pieces FROM test WHERE test1 == test2 AND v1 = v1, AND v2 = v2 AND v3 = whateverThisIs AND v4 = 'v4'"
330317
)
331-
332-
t.end()
333318
})
334319

335-
test('SQL helper - handles js null values as valid `null` sql values', t => {
320+
test('SQL helper - handles js null values as valid `null` sql values', async t => {
336321
const name = null
337322
const id = 123
338323

@@ -342,49 +327,44 @@ test('SQL helper - handles js null values as valid `null` sql values', t => {
342327
t.equal(sql.sql, 'UPDATE teams SET name = ? WHERE id = ?')
343328
t.equal(sql.debug, `UPDATE teams SET name = null WHERE id = ${id}`)
344329
t.same(sql.values, [name, id])
345-
t.end()
346330
})
347331

348-
test('SQL helper - throws when building an sql string with an `undefined` value', t => {
332+
test('SQL helper - throws when building an sql string with an `undefined` value', async t => {
349333
t.throws(() => SQL`UPDATE teams SET name = ${undefined}`)
350-
t.end()
351334
})
352335

353-
test('empty append', t => {
336+
test('empty append', async t => {
354337
const sql = SQL`UPDATE teams SET name = ${'team'}`.append()
355338

356339
t.equal(sql.text, 'UPDATE teams SET name = $1')
357340
t.equal(sql.sql, 'UPDATE teams SET name = ?')
358341
t.equal(sql.debug, "UPDATE teams SET name = 'team'")
359342
t.same(sql.values, ['team'])
360-
361-
t.end()
362343
})
363344

364-
test('inspect', t => {
345+
test('inspect', async t => {
365346
const sql = SQL`UPDATE teams SET name = ${'team'}`
366-
367347
t.equal(util.inspect(sql), "SQL << UPDATE teams SET name = 'team' >>")
368-
t.end()
369348
})
370349

371-
test('quoteIdent', t => {
372-
const table = 'teams'
373-
const name = 'name'
374-
const id = 123
350+
test('quoteIdent', async t => {
351+
t.test('simple', async t => {
352+
const table = 'teams'
353+
const name = 'name'
354+
const id = 123
375355

376-
const sql = SQL`UPDATE ${quoteIdent(
377-
table
378-
)} SET name = ${name} WHERE id = ${id}`
356+
const sql = SQL`UPDATE ${quoteIdent(
357+
table
358+
)} SET name = ${name} WHERE id = ${id}`
379359

380-
t.equal(sql.text, 'UPDATE "teams" SET name = $1 WHERE id = $2')
381-
t.equal(sql.sql, 'UPDATE "teams" SET name = ? WHERE id = ?')
382-
t.equal(sql.debug, `UPDATE "teams" SET name = 'name' WHERE id = ${id}`)
383-
t.same(sql.values, [name, id])
384-
t.end()
360+
t.equal(sql.text, 'UPDATE "teams" SET name = $1 WHERE id = $2')
361+
t.equal(sql.sql, 'UPDATE `teams` SET name = ? WHERE id = ?')
362+
t.equal(sql.debug, `UPDATE "teams" SET name = 'name' WHERE id = ${id}`)
363+
t.same(sql.values, [name, id])
364+
})
385365
})
386366

387-
test('unsafe', t => {
367+
test('unsafe', async t => {
388368
const name = 'name'
389369
const id = 123
390370

@@ -394,10 +374,9 @@ test('unsafe', t => {
394374
t.equal(sql.sql, "UPDATE teams SET name = 'name' WHERE id = ?")
395375
t.equal(sql.debug, `UPDATE teams SET name = 'name' WHERE id = ${id}`)
396376
t.same(sql.values, [id])
397-
t.end()
398377
})
399378

400-
test('should be able to append query that is using "{ unsafe: true }"', t => {
379+
test('should be able to append query that is using "{ unsafe: true }"', async t => {
401380
const table = 'teams'
402381
const id = 123
403382

@@ -424,11 +403,9 @@ test('should be able to append query that is using "{ unsafe: true }"', t => {
424403
`SELECT * FROM teams INNER JOIN (SELECT id FROM teams WHERE id = ${id}) as t2 ON t2.id = id`
425404
)
426405
t.same(sql.values, [id])
427-
428-
t.end()
429406
})
430407

431-
test('should be able to append query that is using "quoteIdent(...)"', t => {
408+
test('should be able to append query that is using "quoteIdent(...)"', async t => {
432409
const table = 'teams'
433410
const id = 123
434411

@@ -444,12 +421,11 @@ test('should be able to append query that is using "quoteIdent(...)"', t => {
444421
)
445422
t.equal(
446423
sql.sql,
447-
'SELECT * FROM "teams" INNER JOIN (SELECT id FROM "teams" WHERE id = ?) as t2 ON t2.id = id'
424+
'SELECT * FROM `teams` INNER JOIN (SELECT id FROM `teams` WHERE id = ?) as t2 ON t2.id = id'
448425
)
449426
t.equal(
450427
sql.debug,
451428
`SELECT * FROM "teams" INNER JOIN (SELECT id FROM "teams" WHERE id = ${id}) as t2 ON t2.id = id`
452429
)
453430
t.same(sql.values, [id])
454-
t.end()
455431
})

docker-compose.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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

quoteIdentifier.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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

0 commit comments

Comments
 (0)