mssql 9.1.2 → 9.1.4
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/README.md +112 -9
- package/lib/base/prepared-statement.js +18 -10
- package/lib/base/request.js +5 -5
- package/lib/datatypes.js +26 -12
- package/lib/msnodesqlv8/connection-pool.js +9 -2
- package/lib/msnodesqlv8/request.js +91 -24
- package/lib/table.js +3 -3
- package/lib/tedious/request.js +128 -34
- package/lib/utils.js +88 -0
- package/package.json +6 -6
- package/CHANGELOG.txt +0 -689
package/lib/tedious/request.js
CHANGED
|
@@ -4,7 +4,7 @@ const tds = require('tedious')
|
|
|
4
4
|
const debug = require('debug')('mssql:tedi')
|
|
5
5
|
const BaseRequest = require('../base/request')
|
|
6
6
|
const RequestError = require('../error/request-error')
|
|
7
|
-
const { IDS, objectHasProperty } = require('../utils')
|
|
7
|
+
const { IDS, objectHasProperty, assertSafeColumnName, assertSafeTypeName, assertSafeTypeSize, assertSafeProcedureName } = require('../utils')
|
|
8
8
|
const { TYPES, DECLARATIONS, declare, cast } = require('../datatypes')
|
|
9
9
|
const Table = require('../table')
|
|
10
10
|
const { PARSERS: UDT } = require('../udt')
|
|
@@ -219,6 +219,15 @@ class Request extends BaseRequest {
|
|
|
219
219
|
|
|
220
220
|
try {
|
|
221
221
|
table._makeBulk()
|
|
222
|
+
// Checked before a connection is borrowed, so that a rejected name cannot leave
|
|
223
|
+
// the pool short of one.
|
|
224
|
+
table.columns.forEach(col => assertSafeColumnName(col.name))
|
|
225
|
+
// tedious writes these into `insert bulk ... WITH (ORDER (<key> <direction>))`
|
|
226
|
+
// UNQUOTED — unlike a column name, which it emits as `[name]`. So the quoted-identifier
|
|
227
|
+
// rule does not apply here: what matters is that the key cannot end the clause. It is
|
|
228
|
+
// checked as an identifier, bare or quoted, which is what that position accepts. The
|
|
229
|
+
// checked keys are rebuilt into the options actually handed to the driver, so the value
|
|
230
|
+
// that was validated is the one emitted even if the caller's object changes after.
|
|
222
231
|
} catch (e) {
|
|
223
232
|
return callback(new RequestError(e, 'EREQUEST'))
|
|
224
233
|
}
|
|
@@ -332,15 +341,38 @@ class Request extends BaseRequest {
|
|
|
332
341
|
}
|
|
333
342
|
}
|
|
334
343
|
|
|
335
|
-
|
|
344
|
+
// A connection is held from here on, so anything that can throw has to hand the
|
|
345
|
+
// error back through `done`, which releases it. That includes newBulkLoad itself:
|
|
346
|
+
// tedious validates the bulk options there and throws for a bad `order` direction,
|
|
347
|
+
// which would otherwise strand the borrowed connection.
|
|
348
|
+
let bulk
|
|
349
|
+
let statement
|
|
350
|
+
try {
|
|
351
|
+
let bulkOptions = options
|
|
352
|
+
if (options && options.order) {
|
|
353
|
+
bulkOptions = Object.assign({}, options, {
|
|
354
|
+
order: Object.fromEntries(Object.entries(options.order)
|
|
355
|
+
.map(([key, direction]) => [assertSafeTypeName(key), direction]))
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
bulk = connection.newBulkLoad(table.path, bulkOptions, done)
|
|
359
|
+
for (const col of table.columns) {
|
|
360
|
+
// tedious emits this name as a quoted identifier of its own when it builds
|
|
361
|
+
// the `insert bulk` statement, so it is checked here as well as in
|
|
362
|
+
// Table#declare, in case the name changed since it was checked above.
|
|
363
|
+
bulk.addColumn(assertSafeColumnName(col.name), getTediousType(col.type), { nullable: col.nullable, length: assertSafeTypeSize(col.length), scale: assertSafeTypeSize(col.scale), precision: assertSafeTypeSize(col.precision) })
|
|
364
|
+
}
|
|
336
365
|
|
|
337
|
-
|
|
338
|
-
|
|
366
|
+
if (table.create) {
|
|
367
|
+
const objectid = table.temporary ? `tempdb..[${table.name}]` : table.path
|
|
368
|
+
statement = `if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`
|
|
369
|
+
}
|
|
370
|
+
} catch (e) {
|
|
371
|
+
return done(e)
|
|
339
372
|
}
|
|
340
373
|
|
|
341
374
|
if (table.create) {
|
|
342
|
-
const
|
|
343
|
-
const req = new tds.Request(`if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`, err => {
|
|
375
|
+
const req = new tds.Request(statement, err => {
|
|
344
376
|
if (err) return done(err)
|
|
345
377
|
|
|
346
378
|
connection.execBulkLoad(bulk, table.rows)
|
|
@@ -439,6 +471,26 @@ class Request extends BaseRequest {
|
|
|
439
471
|
connection.on('errorMessage', errorHandlers.errorMessage = handleError.bind(null, false, connection))
|
|
440
472
|
connection.on('error', errorHandlers.error = handleError.bind(null, true, connection))
|
|
441
473
|
|
|
474
|
+
// Give up before the request is sent: the listeners attached just above have to
|
|
475
|
+
// come off the connection before it goes back to the pool, or they accumulate on
|
|
476
|
+
// it and fire for whoever borrows it next.
|
|
477
|
+
const abandon = (err) => {
|
|
478
|
+
delete this._cancel
|
|
479
|
+
|
|
480
|
+
if (hasReturned) {
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
for (const event in errorHandlers) {
|
|
485
|
+
connection.removeListener(event, errorHandlers[event])
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
this.parent.release(connection)
|
|
489
|
+
hasReturned = true
|
|
490
|
+
|
|
491
|
+
return callback(err)
|
|
492
|
+
}
|
|
493
|
+
|
|
442
494
|
debug('request(%d): query', IDS.get(this), command)
|
|
443
495
|
|
|
444
496
|
const req = new tds.Request(command, err => {
|
|
@@ -647,20 +699,23 @@ class Request extends BaseRequest {
|
|
|
647
699
|
param.value = getTediousType(param.type).validate(param.value, this.parent.collation)
|
|
648
700
|
} catch (e) {
|
|
649
701
|
e.message = `Validation failed for parameter '${name}'. ${e.message}`
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
this.parent.release(connection)
|
|
653
|
-
return callback(err)
|
|
702
|
+
return abandon(new RequestError(e, 'EPARAM'))
|
|
654
703
|
}
|
|
655
704
|
}
|
|
656
705
|
|
|
657
706
|
const declarations = []
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
707
|
+
try {
|
|
708
|
+
for (const name in this.parameters) {
|
|
709
|
+
if (!objectHasProperty(this.parameters, name)) {
|
|
710
|
+
continue
|
|
711
|
+
}
|
|
712
|
+
const param = this.parameters[name]
|
|
713
|
+
// a connection is held here, so a rejected declaration has to be reported
|
|
714
|
+
// through the path that releases it
|
|
715
|
+
declarations.push(`@${name} ${declare(param.type, param)}`)
|
|
661
716
|
}
|
|
662
|
-
|
|
663
|
-
|
|
717
|
+
} catch (e) {
|
|
718
|
+
return abandon(new RequestError(e, 'EREQUEST'))
|
|
664
719
|
}
|
|
665
720
|
|
|
666
721
|
const assigns = []
|
|
@@ -688,16 +743,22 @@ class Request extends BaseRequest {
|
|
|
688
743
|
req.sqlTextOrProcedure = `declare ${declarations.join(', ')};select ${assigns.join(', ')};${req.sqlTextOrProcedure};${batchHasOutput ? (`select 1 as [___return___], ${selects.join(', ')}`) : ''}`
|
|
689
744
|
}
|
|
690
745
|
} else {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
746
|
+
try {
|
|
747
|
+
for (const name in this.parameters) {
|
|
748
|
+
if (!objectHasProperty(this.parameters, name)) {
|
|
749
|
+
continue
|
|
750
|
+
}
|
|
751
|
+
const param = this.parameters[name]
|
|
752
|
+
// tedious builds the sp_executesql declaration from these, so a rejected size
|
|
753
|
+
// has to be reported through the path that releases the connection
|
|
754
|
+
if (param.io === 1) {
|
|
755
|
+
req.addParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: assertSafeTypeSize(param.length), scale: assertSafeTypeSize(param.scale), precision: assertSafeTypeSize(param.precision) })
|
|
756
|
+
} else {
|
|
757
|
+
req.addOutputParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: assertSafeTypeSize(param.length), scale: assertSafeTypeSize(param.scale), precision: assertSafeTypeSize(param.precision) })
|
|
758
|
+
}
|
|
700
759
|
}
|
|
760
|
+
} catch (e) {
|
|
761
|
+
return abandon(new RequestError(e, 'EREQUEST'))
|
|
701
762
|
}
|
|
702
763
|
}
|
|
703
764
|
|
|
@@ -793,6 +854,35 @@ class Request extends BaseRequest {
|
|
|
793
854
|
connection.on('errorMessage', errorHandlers.errorMessage = handleError.bind(null, false, connection))
|
|
794
855
|
connection.on('error', errorHandlers.error = handleError.bind(null, true, connection))
|
|
795
856
|
|
|
857
|
+
// A connection is held from here on. handleError builds a fresh Error and would
|
|
858
|
+
// relabel a rejected identifier as EREQUEST, so anything that can throw here is
|
|
859
|
+
// handed back through this instead, which keeps the code and releases the connection.
|
|
860
|
+
const abandon = (err) => {
|
|
861
|
+
delete this._cancel
|
|
862
|
+
|
|
863
|
+
if (hasReturned) {
|
|
864
|
+
return
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
for (const event in errorHandlers) {
|
|
868
|
+
connection.removeListener(event, errorHandlers[event])
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
this.parent.release(connection)
|
|
872
|
+
hasReturned = true
|
|
873
|
+
|
|
874
|
+
return callback(err)
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
let procedureName
|
|
878
|
+
try {
|
|
879
|
+
procedureName = assertSafeProcedureName(procedure)
|
|
880
|
+
} catch (e) {
|
|
881
|
+
// wrapped like every other rejection here, so the caller sees the same error class
|
|
882
|
+
// whichever driver is in use; MSSQLError keeps the EINJECT code through the wrap
|
|
883
|
+
return abandon(new RequestError(e, 'EREQUEST'))
|
|
884
|
+
}
|
|
885
|
+
|
|
796
886
|
if (debug.enabled) {
|
|
797
887
|
// log stored procedure executions and provided parameters
|
|
798
888
|
const params = Object.keys(this.parameters).map(k => this.parameters[k])
|
|
@@ -805,7 +895,7 @@ class Request extends BaseRequest {
|
|
|
805
895
|
debug('request(%d): execute %s %O', IDS.get(this), procedure, logParams)
|
|
806
896
|
}
|
|
807
897
|
|
|
808
|
-
const req = new tds.Request(
|
|
898
|
+
const req = new tds.Request(procedureName, err => {
|
|
809
899
|
// to make sure we handle no-sql errors as well
|
|
810
900
|
if (err && (!errors.length || (errors.length && err.message !== errors[errors.length - 1].message))) {
|
|
811
901
|
err = new RequestError(err, 'EREQUEST')
|
|
@@ -967,16 +1057,20 @@ class Request extends BaseRequest {
|
|
|
967
1057
|
output[parameterName] = value
|
|
968
1058
|
})
|
|
969
1059
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1060
|
+
try {
|
|
1061
|
+
for (const name in this.parameters) {
|
|
1062
|
+
if (!objectHasProperty(this.parameters, name)) {
|
|
1063
|
+
continue
|
|
1064
|
+
}
|
|
1065
|
+
const param = this.parameters[name]
|
|
1066
|
+
if (param.io === 1) {
|
|
1067
|
+
req.addParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: assertSafeTypeSize(param.length), scale: assertSafeTypeSize(param.scale), precision: assertSafeTypeSize(param.precision) })
|
|
1068
|
+
} else {
|
|
1069
|
+
req.addOutputParameter(param.name, getTediousType(param.type), parameterCorrection(param.value), { length: assertSafeTypeSize(param.length), scale: assertSafeTypeSize(param.scale), precision: assertSafeTypeSize(param.precision) })
|
|
1070
|
+
}
|
|
979
1071
|
}
|
|
1072
|
+
} catch (e) {
|
|
1073
|
+
return abandon(new RequestError(e, 'EREQUEST'))
|
|
980
1074
|
}
|
|
981
1075
|
|
|
982
1076
|
connection.callProcedure(req)
|
package/lib/utils.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const MSSQLError = require('./error/mssql-error')
|
|
2
|
+
|
|
1
3
|
const IDS = new WeakMap()
|
|
2
4
|
const INCREMENT = {
|
|
3
5
|
Connection: 1,
|
|
@@ -7,8 +9,94 @@ const INCREMENT = {
|
|
|
7
9
|
PreparedStatement: 1
|
|
8
10
|
}
|
|
9
11
|
|
|
12
|
+
// Parameter names are emitted into generated T-SQL as `@<name>`. The `@` occupies the
|
|
13
|
+
// identifier's first position, so every character of the name sits in a subsequent
|
|
14
|
+
// position, where T-SQL also permits digits and `$`. The set is deliberately wider than
|
|
15
|
+
// the names the server will accept: it excludes the characters that could end the
|
|
16
|
+
// identifier and let the rest of the name be read as SQL, and nothing else, so a name
|
|
17
|
+
// the server dislikes still fails there rather than becoming an error here. The
|
|
18
|
+
// exception is the invisible format characters, which are rejected outright.
|
|
19
|
+
// Only the two zero-width joiners are taken from the format characters; the rest of that
|
|
20
|
+
// category includes ZERO WIDTH SPACE, which the server does treat as a separator.
|
|
21
|
+
const PARAM_NAME = /^(?:[\p{L}\p{N}\p{M}\p{Pc}@#$・・]|\u200C|\u200D)*$/u
|
|
22
|
+
// A table-valued parameter's type name is emitted into the parameter declaration, and a
|
|
23
|
+
// stored procedure's name is emitted into an `exec`, so both are checked the way the server
|
|
24
|
+
// reads a qualified name: one or more parts separated by `.`, each either bare, holding only
|
|
25
|
+
// the characters an identifier is made of, or quoted in brackets or double quotes, where
|
|
26
|
+
// anything goes until the closing mark. Nothing that gets through can end the statement it
|
|
27
|
+
// sits in and have the rest read as SQL.
|
|
28
|
+
const QUALIFIED_NAME_PART = '(?:\\[(?:[^\\]]|\\]\\])*\\]|"(?:[^"]|"")*"|[\\p{L}\\p{N}\\p{M}\\p{Pc}@#$]+)'
|
|
29
|
+
// An intermediate part may be omitted — `db..proc` and `server.db..proc` are ordinary
|
|
30
|
+
// T-SQL — but the first and last parts must be present, so a bare `.`, a leading `.proc`
|
|
31
|
+
// and a trailing `db..` are still refused. An empty part cannot end the identifier.
|
|
32
|
+
const QUALIFIED_NAME = new RegExp(`^(?:${QUALIFIED_NAME_PART}(?:\\.${QUALIFIED_NAME_PART}?)*\\.)?${QUALIFIED_NAME_PART}$`, 'u')
|
|
33
|
+
|
|
34
|
+
// A length, precision or scale is emitted into the same declaration. `max` is the only
|
|
35
|
+
// word the declaration takes there; everything else the server wants is a number.
|
|
36
|
+
const TYPE_SIZE = /^\w*$/
|
|
37
|
+
|
|
38
|
+
// A column name is emitted as a quoted identifier, `[name]`, by this module and again by
|
|
39
|
+
// the driver's own bulk load. Only `]` can terminate that quoting early, and doubling it
|
|
40
|
+
// is the documented way to include one literally.
|
|
41
|
+
const escapesQuotedIdentifier = (name) => String(name).replace(/]]/g, '').includes(']')
|
|
42
|
+
|
|
10
43
|
module.exports = {
|
|
11
44
|
objectHasProperty: (object, property) => Object.prototype.hasOwnProperty.call(object, property),
|
|
45
|
+
isValidParameterName: (name) => {
|
|
46
|
+
// Only primitives are accepted: an object could return a different value from a second
|
|
47
|
+
// toString() call, passing validation here and emitting something else into the SQL.
|
|
48
|
+
const type = typeof name
|
|
49
|
+
return (type === 'string' || type === 'number') && PARAM_NAME.test(String(name))
|
|
50
|
+
},
|
|
51
|
+
assertSafeTypeName: (name) => {
|
|
52
|
+
// A type name is optional: the tedious driver takes it from the Table passed as the
|
|
53
|
+
// value, and only uses this one when it is given.
|
|
54
|
+
if (name == null) {
|
|
55
|
+
return name
|
|
56
|
+
}
|
|
57
|
+
// Read once, and return what was read. Whatever is emitted is what was checked, so a
|
|
58
|
+
// value that reports a different name the second time cannot reach the SQL.
|
|
59
|
+
const type = String(name).trim()
|
|
60
|
+
if (!QUALIFIED_NAME.test(type)) {
|
|
61
|
+
throw new MSSQLError(`Invalid type name '${type}'. Type names may only contain identifier characters.`, 'EINJECT')
|
|
62
|
+
}
|
|
63
|
+
return type
|
|
64
|
+
},
|
|
65
|
+
assertSafeProcedureName: (name) => {
|
|
66
|
+
// The msnodesqlv8 driver builds `exec @___return___ = <name>` as SQL text, so the name is
|
|
67
|
+
// checked before it is emitted. The tedious driver sends it as a bound RPC and cannot be
|
|
68
|
+
// injected through it, but it is checked there too so that a name is accepted or rejected
|
|
69
|
+
// the same way whichever driver is in use.
|
|
70
|
+
// Read once, and return what was read, for the same reason as the type name.
|
|
71
|
+
const procedure = String(name).trim()
|
|
72
|
+
if (!QUALIFIED_NAME.test(procedure)) {
|
|
73
|
+
throw new MSSQLError(`Invalid procedure name '${procedure}'. Procedure names may only contain identifier characters.`, 'EINJECT')
|
|
74
|
+
}
|
|
75
|
+
return procedure
|
|
76
|
+
},
|
|
77
|
+
assertSafeTypeSize: (value) => {
|
|
78
|
+
if (value == null || typeof value === 'number') {
|
|
79
|
+
return value
|
|
80
|
+
}
|
|
81
|
+
// Read once and return what was read, for the same reason as the type name. Padding is
|
|
82
|
+
// trimmed rather than refused, and a word the server will reject still reaches it.
|
|
83
|
+
const size = String(value).trim()
|
|
84
|
+
if (!TYPE_SIZE.test(size)) {
|
|
85
|
+
throw new MSSQLError(`Invalid type size '${size}'. A length, precision or scale must be a number.`, 'EINJECT')
|
|
86
|
+
}
|
|
87
|
+
return size
|
|
88
|
+
},
|
|
89
|
+
assertSafeColumnName: (name) => {
|
|
90
|
+
const type = typeof name
|
|
91
|
+
if (type !== 'string' && type !== 'number') {
|
|
92
|
+
throw new MSSQLError(`Invalid column name '${String(name)}'. Column names must be a string or a number.`, 'EINJECT')
|
|
93
|
+
}
|
|
94
|
+
const column = String(name)
|
|
95
|
+
if (escapesQuotedIdentifier(column)) {
|
|
96
|
+
throw new MSSQLError(`Invalid column name '${column}'. A ']' in a column name must be written as ']]'.`, 'EINJECT')
|
|
97
|
+
}
|
|
98
|
+
return column
|
|
99
|
+
},
|
|
12
100
|
INCREMENT,
|
|
13
101
|
IDS: {
|
|
14
102
|
get: IDS.get.bind(IDS),
|
package/package.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"azure",
|
|
22
22
|
"node-mssql"
|
|
23
23
|
],
|
|
24
|
-
"version": "9.1.
|
|
24
|
+
"version": "9.1.4",
|
|
25
25
|
"main": "index.js",
|
|
26
26
|
"repository": "github:tediousjs/node-mssql",
|
|
27
27
|
"license": "MIT",
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@commitlint/cli": "^17.6.6",
|
|
38
38
|
"@commitlint/config-conventional": "^17.6.6",
|
|
39
|
-
"@semantic-release/commit-analyzer": "^
|
|
40
|
-
"@semantic-release/github": "^
|
|
41
|
-
"@semantic-release/npm": "^
|
|
42
|
-
"@semantic-release/release-notes-generator": "^
|
|
39
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
40
|
+
"@semantic-release/github": "^12.0.6",
|
|
41
|
+
"@semantic-release/npm": "^13.1.5",
|
|
42
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
43
43
|
"mocha": "^10.0.0",
|
|
44
|
-
"semantic-release": "^
|
|
44
|
+
"semantic-release": "^25.0.3",
|
|
45
45
|
"standard": "^17.0.0"
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|