mssql-cr-layer 3.0.7 → 3.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +42 -11
- package/src/scan-markers.js +111 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@ const mssql = require('mssql')
|
|
|
2
2
|
const assert = require('assert')
|
|
3
3
|
const every = require('lodash.every')
|
|
4
4
|
const debug = require('debug')('layer:mssql')
|
|
5
|
+
const scanMarkers = require('./scan-markers')
|
|
5
6
|
|
|
6
7
|
const connectionParams = new WeakMap() // Hidden connection parameters
|
|
7
8
|
|
|
@@ -237,19 +238,23 @@ MssqlCrLayer.prototype.query = function(statement, params, options) {
|
|
|
237
238
|
|
|
238
239
|
const convertParams = function() {
|
|
239
240
|
if (Array.isArray(params)) {
|
|
240
|
-
const
|
|
241
|
+
const markers = scanMarkers(statement)
|
|
241
242
|
assert(
|
|
242
|
-
|
|
243
|
+
markers.length <= Object.keys(params).length,
|
|
243
244
|
'There are more ' + 'parameters in statement than in object params'
|
|
244
245
|
)
|
|
245
246
|
const paramsObj = {}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
247
|
+
// rewrite by position: replacing by token would hit the first
|
|
248
|
+
// occurrence anywhere in the statement, comments and literals included
|
|
249
|
+
let converted = ''
|
|
250
|
+
let last = 0
|
|
251
|
+
markers.forEach(function(marker) {
|
|
252
|
+
const key = marker.name
|
|
253
|
+
paramsObj['p' + key] = params[Number(key) - 1]
|
|
254
|
+
converted += statement.slice(last, marker.index) + '@p' + key
|
|
255
|
+
last = marker.index + marker.length
|
|
256
|
+
})
|
|
257
|
+
statement = converted + statement.slice(last)
|
|
253
258
|
params = paramsObj
|
|
254
259
|
}
|
|
255
260
|
}
|
|
@@ -310,12 +315,38 @@ MssqlCrLayer.prototype.close = function() {
|
|
|
310
315
|
}
|
|
311
316
|
|
|
312
317
|
/**
|
|
313
|
-
* Wrap the identifier within the appropriate delimiters
|
|
318
|
+
* Wrap the identifier within the appropriate delimiters, escaping the
|
|
319
|
+
* closing delimiter by doubling it - the standard SQL escape - so an
|
|
320
|
+
* identifier holding a delimiter cannot close the quoted context and
|
|
321
|
+
* append arbitrary SQL. A NUL character cannot be escaped by any
|
|
322
|
+
* delimiter style, so it is rejected, as is a non string identifier
|
|
314
323
|
* @param identifier {string}
|
|
315
324
|
* @returns identifier {string}
|
|
316
325
|
*/
|
|
317
326
|
MssqlCrLayer.prototype.wrap = function(identifier) {
|
|
318
|
-
|
|
327
|
+
if (typeof identifier !== 'string') {
|
|
328
|
+
throw new Error(
|
|
329
|
+
'Invalid identifier: a string is expected, got ' + typeof identifier
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
if (identifier.indexOf('\0') !== -1) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
'Invalid identifier: the NUL character is not allowed'
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
// delimiters is a prototype property a consumer can override. Read it once
|
|
338
|
+
// and check it here, at the point of use: anything but a two character
|
|
339
|
+
// string leaves close undefined or hijackable via Symbol.split, and the
|
|
340
|
+
// escape below silently becomes a no op
|
|
341
|
+
const delimiters = this.delimiters
|
|
342
|
+
if (typeof delimiters !== 'string' || delimiters.length !== 2) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
'Invalid delimiters: a two character string is expected'
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
const open = delimiters[0]
|
|
348
|
+
const close = delimiters[1]
|
|
349
|
+
return open + identifier.split(close).join(close + close) + close
|
|
319
350
|
}
|
|
320
351
|
|
|
321
352
|
function getType(value, param) {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the parameter markers of a T-SQL statement that are in code context.
|
|
3
|
+
*
|
|
4
|
+
* A regex over the whole statement cannot tell a marker from a marker
|
|
5
|
+
* lookalike sitting inside a string literal, a quoted identifier or a
|
|
6
|
+
* comment - 'Total R$100 a vista' reads as the marker $100 - so the
|
|
7
|
+
* statement is walked once, tracking the context, and only the markers
|
|
8
|
+
* found in code are reported.
|
|
9
|
+
*
|
|
10
|
+
* The marker grammar reproduces the old /(\$\w*\b)/g exactly: the sigil
|
|
11
|
+
* followed by one or more word characters, maximal munch. A sigil not
|
|
12
|
+
* followed by a word character is not a marker, so '%R$%' stays inert.
|
|
13
|
+
*
|
|
14
|
+
* There is no dollar quoting in T-SQL and no backslash escape in its
|
|
15
|
+
* string literals - a quote is escaped by doubling it, and so is the
|
|
16
|
+
* closing bracket of a bracketed identifier.
|
|
17
|
+
*
|
|
18
|
+
* @param statement {string}
|
|
19
|
+
* @returns {Array} [{index, length, name}, ...] in the order they appear,
|
|
20
|
+
* index being the offset of the sigil and name the token without it
|
|
21
|
+
*/
|
|
22
|
+
module.exports = function scanMarkers(statement) {
|
|
23
|
+
const markers = []
|
|
24
|
+
const length = statement.length
|
|
25
|
+
let i = 0
|
|
26
|
+
while (i < length) {
|
|
27
|
+
const char = statement[i]
|
|
28
|
+
if (char === '\'') {
|
|
29
|
+
i = skipQuoted(statement, i, '\'')
|
|
30
|
+
} else if (char === '"') {
|
|
31
|
+
i = skipQuoted(statement, i, '"')
|
|
32
|
+
} else if (char === '[') {
|
|
33
|
+
i = skipQuoted(statement, i, ']')
|
|
34
|
+
} else if (char === '-' && statement[i + 1] === '-') {
|
|
35
|
+
const end = statement.indexOf('\n', i + 2)
|
|
36
|
+
i = end === -1 ? length : end + 1
|
|
37
|
+
} else if (char === '/' && statement[i + 1] === '*') {
|
|
38
|
+
i = skipBlockComment(statement, i)
|
|
39
|
+
} else if (char === '$') {
|
|
40
|
+
let end = i + 1
|
|
41
|
+
while (end < length && isWordChar(statement[end])) {
|
|
42
|
+
end++
|
|
43
|
+
}
|
|
44
|
+
if (end > i + 1) {
|
|
45
|
+
markers.push({
|
|
46
|
+
index: i,
|
|
47
|
+
length: end - i,
|
|
48
|
+
name: statement.slice(i + 1, end)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
i = end
|
|
52
|
+
} else {
|
|
53
|
+
i++
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return markers
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isWordChar(char) {
|
|
60
|
+
return (
|
|
61
|
+
(char >= 'a' && char <= 'z') ||
|
|
62
|
+
(char >= 'A' && char <= 'Z') ||
|
|
63
|
+
(char >= '0' && char <= '9') ||
|
|
64
|
+
char === '_'
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Skip the quoted run opened at start, whose closing delimiter is close and
|
|
70
|
+
* is escaped by doubling it. Unterminated runs to the end of the statement
|
|
71
|
+
* @returns the offset of the first character after the run
|
|
72
|
+
*/
|
|
73
|
+
function skipQuoted(statement, start, close) {
|
|
74
|
+
let i = start + 1
|
|
75
|
+
while (i < statement.length) {
|
|
76
|
+
if (statement[i] === close) {
|
|
77
|
+
if (statement[i + 1] === close) {
|
|
78
|
+
i += 2
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
return i + 1
|
|
82
|
+
}
|
|
83
|
+
i++
|
|
84
|
+
}
|
|
85
|
+
return statement.length
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Skip the block comment opened at start. T-SQL nests block comments, so the
|
|
90
|
+
* depth is tracked and the comment ends only when it gets back to zero
|
|
91
|
+
* @returns the offset of the first character after the comment
|
|
92
|
+
*/
|
|
93
|
+
function skipBlockComment(statement, start) {
|
|
94
|
+
let depth = 1
|
|
95
|
+
let i = start + 2
|
|
96
|
+
while (i < statement.length) {
|
|
97
|
+
if (statement[i] === '/' && statement[i + 1] === '*') {
|
|
98
|
+
depth++
|
|
99
|
+
i += 2
|
|
100
|
+
} else if (statement[i] === '*' && statement[i + 1] === '/') {
|
|
101
|
+
depth--
|
|
102
|
+
i += 2
|
|
103
|
+
if (depth === 0) {
|
|
104
|
+
return i
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
i++
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return statement.length
|
|
111
|
+
}
|