Skip to content

Commit b3981f1

Browse files
author
Andrea Giammarchi
committed
Improved append(...) and nested statements
This MR addresses all concerns and optimizations reised in felixfbecker#30 and felixfbecker#44 * SQLStatement can be used as value * raw names can be passed via `'${'table_' + name}'` with `'` or `"` transform * `.append(...)` doesn't need a space at the beginning, it's handled automatically
1 parent 7be1d2a commit b3981f1

File tree

2 files changed

+85
-11
lines changed

2 files changed

+85
-11
lines changed

index.js

+56-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
'use strict'
22

3+
const lsp = str => (/^[\s\n\r]/.test(str) ? str : ' ' + str)
4+
const push = (str, val, statement, spaced) => {
5+
const { strings } = statement
6+
str[str.length - 1] += spaced ? lsp(strings[0]) : strings[0]
7+
str.push(...strings.slice(1))
8+
val.push(...(statement.values || statement.bind))
9+
}
10+
311
class SQLStatement {
412
/**
513
* @param {string[]} strings
@@ -25,20 +33,19 @@ class SQLStatement {
2533
* @returns {this}
2634
*/
2735
append(statement) {
36+
const { strings } = this
2837
if (statement instanceof SQLStatement) {
29-
this.strings[this.strings.length - 1] += statement.strings[0]
30-
this.strings.push.apply(this.strings, statement.strings.slice(1))
31-
const list = this.values || this.bind
32-
list.push.apply(list, statement.values)
38+
push(strings, this.values || this.bind, statement, true)
3339
} else {
34-
this.strings[this.strings.length - 1] += statement
40+
strings[strings.length - 1] += lsp(statement)
3541
}
3642
return this
3743
}
3844

3945
/**
4046
* Use a prepared statement with Sequelize.
41-
* Makes `query` return a query with `$n` syntax instead of `?` and switches the `values` key name to `bind`
47+
* Makes `query` return a query with `$n` syntax instead of `?`
48+
* and switches the `values` key name to `bind`
4249
* @param {boolean} [value=true] value If omitted, defaults to `true`
4350
* @returns this
4451
*/
@@ -74,16 +81,54 @@ Object.defineProperty(SQLStatement.prototype, 'sql', {
7481
},
7582
})
7683

84+
class SQLQuote {
85+
constructor(chr) {
86+
this.char = chr
87+
this.escape = new RegExp(chr, 'g')
88+
}
89+
}
90+
7791
/**
7892
* @param {string[]} strings
7993
* @param {...any} values
8094
* @returns {SQLStatement}
8195
*/
82-
function SQL(strings) {
83-
return new SQLStatement(strings.slice(0), Array.from(arguments).slice(1))
96+
function SQL(tpl, ...val) {
97+
const strings = [tpl[0]]
98+
const values = []
99+
for (let { length } = tpl, prev = tpl[0], j = 0, i = 1; i < length; i++) {
100+
const current = tpl[i]
101+
const value = val[i - 1]
102+
if (/^('|")/.test(current) && RegExp.$1 === prev.slice(-1)) {
103+
if (this instanceof SQLQuote) {
104+
strings[j] = [
105+
strings[j].slice(0, -1),
106+
String(value).replace(this.escape, '\\$&'),
107+
current.slice(1)
108+
].join(this.char)
109+
} else {
110+
throw new Error(`unable to escape ${value}`)
111+
}
112+
} else {
113+
if (value instanceof SQLStatement) {
114+
push(strings, values, value, false)
115+
j = strings.length - 1
116+
strings[j] += current
117+
} else {
118+
values.push(value)
119+
j = strings.push(current) - 1
120+
}
121+
prev = strings[j]
122+
}
123+
}
124+
return new SQLStatement(strings, values)
84125
}
85126

127+
SQL.withQuote = quote => {
128+
const sqlQuote = new SQLQuote(quote)
129+
return (...args) => SQL.apply(sqlQuote, args)
130+
}
131+
SQL.SQL = SQL
132+
SQL.default = SQL
133+
SQL.SQLStatement = SQLStatement
86134
module.exports = SQL
87-
module.exports.SQL = SQL
88-
module.exports.default = SQL
89-
module.exports.SQLStatement = SQLStatement

test/unit.js

+29
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,33 @@ describe('SQL', () => {
118118
assert.deepStrictEqual(statement.values, [123])
119119
})
120120
})
121+
122+
describe('Auto Escape', () => {
123+
it('should escape the table name', () => {
124+
const sql = SQL.withQuote('`');
125+
assert.strictEqual(sql`SELECT * FROM '${`table_${123}`}'`.sql, 'SELECT * FROM `table_123`')
126+
})
127+
it('should have no extra values', () => {
128+
const sql = SQL.withQuote('`');
129+
assert.strictEqual(sql`SELECT * FROM '${`table_${123}`}'`.values.length, 0)
130+
})
131+
it('should throw if no escape', () => {
132+
try {
133+
SQL`SELECT * FROM '${`table_${123}`}'`
134+
assert.strictEqual(true, false)
135+
} catch (e) {
136+
assert.strictEqual(true, true)
137+
}
138+
})
139+
})
140+
141+
describe('Nested SQLStatement', () => {
142+
it('should pollute the initial statement', () => {
143+
const name = 'no-shenanigans'
144+
const age = Math.random()
145+
const sql = SQL`SELECT * FROM table WHERE ${SQL`name = ${name}`} AND age = ${age}`
146+
assert.strictEqual(sql.sql, 'SELECT * FROM table WHERE name = ? AND age = ?')
147+
assert.deepStrictEqual(sql.values, [name, age])
148+
})
149+
})
121150
})

0 commit comments

Comments
 (0)