mssql 11.0.0 → 11.0.2
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 +183 -28
- package/lib/base/prepared-statement.js +18 -10
- package/lib/base/request.js +5 -5
- package/lib/datatypes.js +26 -12
- package/lib/msnodesqlv8/request.js +91 -24
- package/lib/shared.js +7 -1
- 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/README.md
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
# node-mssql
|
|
2
2
|
|
|
3
|
-
Microsoft SQL Server client for Node.js
|
|
3
|
+
**Microsoft SQL Server client for Node.js**
|
|
4
4
|
|
|
5
|
-
[![NPM Version][npm-image]][npm-url] [![NPM Downloads][downloads-image]][downloads-url] [![
|
|
5
|
+
[![NPM Version][npm-image]][npm-url] [![NPM Downloads][downloads-image]][downloads-url] [](https://gitter.im/patriksimek/node-mssql?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
|
6
6
|
|
|
7
7
|
Supported TDS drivers:
|
|
8
|
+
|
|
8
9
|
- [Tedious][tedious-url] (pure JavaScript - Windows/macOS/Linux, default)
|
|
9
|
-
- [Microsoft / Contributors Node V8 Driver for Node.js for SQL Server
|
|
10
|
+
- [MSNodeSQLv8][msnodesqlv8-url] (Microsoft / Contributors Node V8 Driver for Node.js for SQL Server, v2 native - Windows or Linux/macOS 64 bits only)
|
|
10
11
|
|
|
11
12
|
## Installation
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
### Tedious driver (default)
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
npm install mssql
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### MSNodeSQLv8 driver (optional)
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
npm install mssql msnodesqlv8
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## SQL Server prerequisites
|
|
27
|
+
|
|
28
|
+
This package requires TCP/IP to connect to SQL Server, and you may need to enable this in your installation.
|
|
14
29
|
|
|
15
30
|
## Short Example: Use Connect String
|
|
16
31
|
|
|
@@ -68,6 +83,32 @@ const sqlConfig = {
|
|
|
68
83
|
})()
|
|
69
84
|
```
|
|
70
85
|
|
|
86
|
+
## Windows Authentication Example Using MSNodeSQLv8
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
const sql = require('mssql/msnodesqlv8');
|
|
90
|
+
|
|
91
|
+
const config = {
|
|
92
|
+
server: "MyServer",
|
|
93
|
+
database: "MyDatabase",
|
|
94
|
+
options: {
|
|
95
|
+
trustedConnection: true, // Set to true if using Windows Authentication
|
|
96
|
+
trustServerCertificate: true, // Set to true if using self-signed certificates
|
|
97
|
+
},
|
|
98
|
+
driver: "msnodesqlv8", // Required if using Windows Authentication
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
(async () => {
|
|
102
|
+
try {
|
|
103
|
+
await sql.connect(config);
|
|
104
|
+
const result = await sql.query`select TOP 10 * from MyTable`;
|
|
105
|
+
console.dir(result);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error(err);
|
|
108
|
+
}
|
|
109
|
+
})();
|
|
110
|
+
```
|
|
111
|
+
|
|
71
112
|
## Documentation
|
|
72
113
|
|
|
73
114
|
### Examples
|
|
@@ -87,7 +128,7 @@ const sqlConfig = {
|
|
|
87
128
|
### Drivers
|
|
88
129
|
|
|
89
130
|
* [Tedious](#tedious)
|
|
90
|
-
* [
|
|
131
|
+
* [MSNodeSQLv8](#msnodesqlv8)
|
|
91
132
|
|
|
92
133
|
### Connections
|
|
93
134
|
|
|
@@ -109,6 +150,13 @@ const sqlConfig = {
|
|
|
109
150
|
* [bulk](#bulk-table-options-callback)
|
|
110
151
|
* [cancel](#cancel)
|
|
111
152
|
|
|
153
|
+
### Identifier rules
|
|
154
|
+
|
|
155
|
+
* [Parameter names](#parameter-names)
|
|
156
|
+
* [Column names](#column-names)
|
|
157
|
+
* [Type names and sizes](#type-names-and-sizes)
|
|
158
|
+
* [Procedure names](#procedure-names)
|
|
159
|
+
|
|
112
160
|
### Transactions
|
|
113
161
|
|
|
114
162
|
* [Transaction](#transaction)
|
|
@@ -174,16 +222,16 @@ const sql = require('mssql')
|
|
|
174
222
|
let result1 = await pool.request()
|
|
175
223
|
.input('input_parameter', sql.Int, value)
|
|
176
224
|
.query('select * from mytable where id = @input_parameter')
|
|
177
|
-
|
|
225
|
+
|
|
178
226
|
console.dir(result1)
|
|
179
|
-
|
|
227
|
+
|
|
180
228
|
// Stored procedure
|
|
181
|
-
|
|
229
|
+
|
|
182
230
|
let result2 = await pool.request()
|
|
183
231
|
.input('input_parameter', sql.Int, value)
|
|
184
232
|
.output('output_parameter', sql.VarChar(50))
|
|
185
233
|
.execute('procedure_name')
|
|
186
|
-
|
|
234
|
+
|
|
187
235
|
console.dir(result2)
|
|
188
236
|
} catch (err) {
|
|
189
237
|
// ... error checks
|
|
@@ -208,7 +256,7 @@ sql.on('error', err => {
|
|
|
208
256
|
|
|
209
257
|
sql.connect(config).then(pool => {
|
|
210
258
|
// Query
|
|
211
|
-
|
|
259
|
+
|
|
212
260
|
return pool.request()
|
|
213
261
|
.input('input_parameter', sql.Int, value)
|
|
214
262
|
.query('select * from mytable where id = @input_parameter')
|
|
@@ -229,9 +277,9 @@ sql.on('error', err => {
|
|
|
229
277
|
})
|
|
230
278
|
|
|
231
279
|
sql.connect(config).then(pool => {
|
|
232
|
-
|
|
280
|
+
|
|
233
281
|
// Stored procedure
|
|
234
|
-
|
|
282
|
+
|
|
235
283
|
return pool.request()
|
|
236
284
|
.input('input_parameter', sql.Int, value)
|
|
237
285
|
.output('output_parameter', sql.VarChar(50))
|
|
@@ -263,7 +311,7 @@ sql.on('error', err => {
|
|
|
263
311
|
})
|
|
264
312
|
```
|
|
265
313
|
|
|
266
|
-
All values are automatically sanitized against sql injection.
|
|
314
|
+
All values are automatically sanitized against sql injection.
|
|
267
315
|
This is because it is rendered as prepared statement, and thus all limitations imposed in MS SQL on parameters apply.
|
|
268
316
|
e.g. Column names cannot be passed/set in statements using variables.
|
|
269
317
|
|
|
@@ -580,7 +628,7 @@ In addition to configuration object there is an option to pass config as a conne
|
|
|
580
628
|
```
|
|
581
629
|
Server=localhost,1433;Database=database;User Id=username;Password=password;Encrypt=true
|
|
582
630
|
```
|
|
583
|
-
###### Standard configuration using
|
|
631
|
+
###### Standard configuration using MSNodeSQLv8 driver
|
|
584
632
|
```
|
|
585
633
|
Driver=msnodesqlv8;Server=(local)\INSTANCE;Database=database;UID=DOMAIN\username;PWD=password;Encrypt=true
|
|
586
634
|
```
|
|
@@ -648,12 +696,21 @@ On top of the extra options, an `authentication` property can be added to the po
|
|
|
648
696
|
- **authentication** - An object with authentication settings, according to the [Tedious Documentation](https://tediousjs.github.io/tedious/api-connection.html). Passing this object will override `user`, `password`, `domain` settings.
|
|
649
697
|
- **authentication.type** - Type of the authentication method, valid types are `default`, `ntlm`, `azure-active-directory-password`, `azure-active-directory-access-token`, `azure-active-directory-msi-vm`, or `azure-active-directory-msi-app-service`
|
|
650
698
|
- **authentication.options** - Options of the authentication required by the `tedious` driver, depends on `authentication.type`. For more details, check [Tedious Authentication Interfaces](https://github.com/tediousjs/tedious/blob/v11.1.1/src/connection.ts#L200-L318)
|
|
699
|
+
- `tedious` does not support Windows Authentication/Trusted Connection, however the `msnodesqlv8` driver does.
|
|
651
700
|
|
|
652
701
|
More information about Tedious specific options: http://tediousjs.github.io/tedious/api-connection.html
|
|
653
702
|
|
|
654
|
-
|
|
703
|
+
___
|
|
704
|
+
|
|
705
|
+
### MSNodeSQLv8
|
|
655
706
|
|
|
656
|
-
|
|
707
|
+
Alternative driver, requires Node.js v10+ or newer; Windows (32 or 64-bit) or Linux/macOS (64-bit only). It's not part of the default package so it must be [installed](#msnodesqlv8-driver) in addition. Supports [Windows/Trusted Connection authentication](#windows-authentication-example-using-msnodesqlv8).
|
|
708
|
+
|
|
709
|
+
**To use this driver you must use this `require` statement:**
|
|
710
|
+
|
|
711
|
+
```javascript
|
|
712
|
+
const sql = require('mssql/msnodesqlv8')
|
|
713
|
+
```
|
|
657
714
|
|
|
658
715
|
Note: If you use import into your lib to prepare your request (`const { VarChar } = require('mssql')`) you also need to upgrade all your types import into your code (`const { VarChar } = require('mssql/msnodesqlv8')`) or a `connection.on is not a function` error will be thrown.
|
|
659
716
|
|
|
@@ -678,6 +735,8 @@ Driver={SQL Server Native Client 11.0};Server={#{server}\\#{instance}};Database=
|
|
|
678
735
|
|
|
679
736
|
Please note that the connection string with this driver is not the same than tedious and use yes/no instead of true/false. You can see more on the [ODBC](https://docs.microsoft.com/fr-fr/dotnet/api/system.data.odbc.odbcconnection.connectionstring?view=dotnet-plat-ext-5.0) documentation.
|
|
680
737
|
|
|
738
|
+
___
|
|
739
|
+
|
|
681
740
|
## Connections
|
|
682
741
|
|
|
683
742
|
Internally, each `ConnectionPool` instance is a separate pool of TDS connections. Once you create a new `Request`/`Transaction`/`Prepared Statement`, a new TDS connection is acquired from the pool and reserved for desired action. Once the action is complete, connection is released back to the pool. Connection health check is built-in so once the dead connection is discovered, it is immediately replaced with a new one.
|
|
@@ -786,6 +845,7 @@ request.execute('procedure_name', (err, result) => {
|
|
|
786
845
|
|
|
787
846
|
__Errors__
|
|
788
847
|
- EREQUEST (`RequestError`) - *Message from SQL Server*
|
|
848
|
+
- EINJECT (`RequestError`) - Procedure name or parameter type is not a valid identifier. See [Identifiers](#identifiers).
|
|
789
849
|
- ECANCEL (`RequestError`) - Cancelled.
|
|
790
850
|
- ETIMEOUT (`RequestError`) - Request timeout.
|
|
791
851
|
- ENOCONN (`RequestError`) - No connection is specified for that request.
|
|
@@ -838,7 +898,7 @@ sql.map.register(Number, sql.BigInt)
|
|
|
838
898
|
|
|
839
899
|
__Errors__ (synchronous)
|
|
840
900
|
- EARGS (`RequestError`) - Invalid number of arguments.
|
|
841
|
-
- EINJECT (`RequestError`) -
|
|
901
|
+
- EINJECT (`RequestError`) - Parameter name is not a valid identifier. See [Identifiers](#identifiers).
|
|
842
902
|
|
|
843
903
|
---------------------------------------
|
|
844
904
|
|
|
@@ -863,7 +923,7 @@ request.output('output_parameter', sql.VarChar(50), 'abc')
|
|
|
863
923
|
|
|
864
924
|
__Errors__ (synchronous)
|
|
865
925
|
- EARGS (`RequestError`) - Invalid number of arguments.
|
|
866
|
-
- EINJECT (`RequestError`) -
|
|
926
|
+
- EINJECT (`RequestError`) - Parameter name is not a valid identifier. See [Identifiers](#identifiers).
|
|
867
927
|
|
|
868
928
|
---------------------------------------
|
|
869
929
|
|
|
@@ -947,6 +1007,7 @@ __Errors__
|
|
|
947
1007
|
- ECONNCLOSED (`ConnectionError`) - Connection is closed.
|
|
948
1008
|
- ENOTBEGUN (`TransactionError`) - Transaction has not begun.
|
|
949
1009
|
- EABORT (`TransactionError`) - Transaction was aborted (by user or because of an error).
|
|
1010
|
+
- EINJECT (`RequestError`) - Parameter type is not a valid identifier. See [Identifiers](#identifiers).
|
|
950
1011
|
|
|
951
1012
|
```javascript
|
|
952
1013
|
const request = new sql.Request()
|
|
@@ -992,6 +1053,7 @@ __Errors__
|
|
|
992
1053
|
- ECONNCLOSED (`ConnectionError`) - Connection is closed.
|
|
993
1054
|
- ENOTBEGUN (`TransactionError`) - Transaction has not begun.
|
|
994
1055
|
- EABORT (`TransactionError`) - Transaction was aborted (by user or because of an error).
|
|
1056
|
+
- EINJECT (`RequestError`) - Parameter type is not a valid identifier. See [Identifiers](#identifiers).
|
|
995
1057
|
|
|
996
1058
|
You can enable multiple recordsets in queries with the `request.multiple = true` command.
|
|
997
1059
|
|
|
@@ -1038,6 +1100,7 @@ __Errors__
|
|
|
1038
1100
|
- ECONNCLOSED (`ConnectionError`) - Connection is closed.
|
|
1039
1101
|
- ENOTBEGUN (`TransactionError`) - Transaction has not begun.
|
|
1040
1102
|
- EABORT (`TransactionError`) - Transaction was aborted (by user or because of an error).
|
|
1103
|
+
- EINJECT (`RequestError`) - Column name or parameter type is not a valid identifier. See [Identifiers](#identifiers).
|
|
1041
1104
|
|
|
1042
1105
|
---------------------------------------
|
|
1043
1106
|
|
|
@@ -1268,7 +1331,7 @@ ps.input('input_parameter', sql.VarChar(50))
|
|
|
1268
1331
|
|
|
1269
1332
|
__Errors__ (synchronous)
|
|
1270
1333
|
- EARGS (`PreparedStatementError`) - Invalid number of arguments.
|
|
1271
|
-
- EINJECT (`PreparedStatementError`) -
|
|
1334
|
+
- EINJECT (`PreparedStatementError`) - Parameter name is not a valid identifier. See [Identifiers](#identifiers).
|
|
1272
1335
|
|
|
1273
1336
|
---------------------------------------
|
|
1274
1337
|
|
|
@@ -1290,7 +1353,7 @@ ps.output('output_parameter', sql.VarChar(50))
|
|
|
1290
1353
|
|
|
1291
1354
|
__Errors__ (synchronous)
|
|
1292
1355
|
- EARGS (`PreparedStatementError`) - Invalid number of arguments.
|
|
1293
|
-
- EINJECT (`PreparedStatementError`) -
|
|
1356
|
+
- EINJECT (`PreparedStatementError`) - Parameter name is not a valid identifier. See [Identifiers](#identifiers).
|
|
1294
1357
|
|
|
1295
1358
|
---------------------------------------
|
|
1296
1359
|
|
|
@@ -1341,7 +1404,7 @@ ps.prepare('select @param as value', err => {
|
|
|
1341
1404
|
|
|
1342
1405
|
console.log(result.recordset[0].value) // return 12345
|
|
1343
1406
|
console.log(result.rowsAffected) // Returns number of affected rows in case of INSERT, UPDATE or DELETE statement.
|
|
1344
|
-
|
|
1407
|
+
|
|
1345
1408
|
ps.unprepare(err => {
|
|
1346
1409
|
// ... error checks
|
|
1347
1410
|
})
|
|
@@ -1374,9 +1437,9 @@ ps.prepare('select @param as value', err => {
|
|
|
1374
1437
|
|
|
1375
1438
|
request.on('done', result => {
|
|
1376
1439
|
// Always emitted as the last one
|
|
1377
|
-
|
|
1440
|
+
|
|
1378
1441
|
console.log(result.rowsAffected) // Returns number of affected rows in case of INSERT, UPDATE or DELETE statement.
|
|
1379
|
-
|
|
1442
|
+
|
|
1380
1443
|
ps.unprepare(err => {
|
|
1381
1444
|
// ... error checks
|
|
1382
1445
|
})
|
|
@@ -1682,7 +1745,7 @@ More information about JSON support can be found in [official documentation](htt
|
|
|
1682
1745
|
|
|
1683
1746
|
## Handling Duplicate Column Names
|
|
1684
1747
|
|
|
1685
|
-
If your queries contain output columns with identical names, the default behaviour of `mssql` will only return column metadata for the last column with that name. You will also not always be able to re-assemble the order of output columns requested.
|
|
1748
|
+
If your queries contain output columns with identical names, the default behaviour of `mssql` will only return column metadata for the last column with that name. You will also not always be able to re-assemble the order of output columns requested.
|
|
1686
1749
|
|
|
1687
1750
|
Default behaviour:
|
|
1688
1751
|
```javascript
|
|
@@ -1863,6 +1926,100 @@ Results in:
|
|
|
1863
1926
|
]
|
|
1864
1927
|
```
|
|
1865
1928
|
|
|
1929
|
+
## Identifiers
|
|
1930
|
+
|
|
1931
|
+
Values are sent to the server as parameters and may hold anything. A few things
|
|
1932
|
+
are not values, and cannot be: the name of a parameter, the name of a column in a
|
|
1933
|
+
bulk load, the type a parameter is declared as, and the name of a stored procedure.
|
|
1934
|
+
Those are *identifiers*, and
|
|
1935
|
+
no SQL database can bind them, so this library builds them into the statement it
|
|
1936
|
+
sends. It checks them, and rejects anything that could end the identifier and have
|
|
1937
|
+
the rest of the value read as SQL.
|
|
1938
|
+
|
|
1939
|
+
That check is not a substitute for your own. If any of these comes from somewhere
|
|
1940
|
+
you do not control, map it to a value you expect first:
|
|
1941
|
+
|
|
1942
|
+
```javascript
|
|
1943
|
+
const columns = { name: 'name', created: 'created_at' } // what you allow
|
|
1944
|
+
const column = columns[req.query.sortBy] // what they asked for
|
|
1945
|
+
if (!column) throw new Error('unknown column')
|
|
1946
|
+
|
|
1947
|
+
request.input(column, sql.NVarChar, value)
|
|
1948
|
+
```
|
|
1949
|
+
|
|
1950
|
+
### Parameter names
|
|
1951
|
+
|
|
1952
|
+
`request.input()`, `request.output()` and the same methods on a prepared statement
|
|
1953
|
+
take a name without the leading `@`. It may hold letters, digits, marks and
|
|
1954
|
+
connector punctuation from any script, along with `@`, `#`, `$` and the two
|
|
1955
|
+
zero-width joiners. A name that is not a string or a number is rejected, as is one
|
|
1956
|
+
holding an invisible format character, because one of them is a separator the
|
|
1957
|
+
server treats as ending the identifier.
|
|
1958
|
+
|
|
1959
|
+
Anything else raises `EINJECT`. Note that a name this library accepts may still be
|
|
1960
|
+
one the server rejects; the check is deliberately wider than the server's own rules
|
|
1961
|
+
so that it only refuses what could change the shape of the statement.
|
|
1962
|
+
|
|
1963
|
+
### Column names
|
|
1964
|
+
|
|
1965
|
+
`table.columns.add()` names a column in a bulk load, and the name is emitted as a
|
|
1966
|
+
quoted identifier. A `]` ends that quoting, so a name holding one must double it,
|
|
1967
|
+
which is how T-SQL escapes it:
|
|
1968
|
+
|
|
1969
|
+
```javascript
|
|
1970
|
+
table.columns.add('a]]b', sql.Int) // the column named a]b
|
|
1971
|
+
```
|
|
1972
|
+
|
|
1973
|
+
A single `]` raises `EINJECT`. Column names are not otherwise restricted: spaces,
|
|
1974
|
+
punctuation and reserved words are all fine, because the quoting handles them.
|
|
1975
|
+
|
|
1976
|
+
The keys of `options.order` are also identifiers, but the driver writes them into the bulk
|
|
1977
|
+
statement's `ORDER (...)` clause *unquoted* rather than as `[name]`. They follow the type
|
|
1978
|
+
name rule instead: bare, or quoted with brackets or double quotes.
|
|
1979
|
+
|
|
1980
|
+
```javascript
|
|
1981
|
+
request.bulk(table, { order: { created_at: 'ASC' } })
|
|
1982
|
+
request.bulk(table, { order: { '[Created At]': 'DESC' } }) // quote a spaced name yourself
|
|
1983
|
+
```
|
|
1984
|
+
|
|
1985
|
+
### Type names and sizes
|
|
1986
|
+
|
|
1987
|
+
`sql.TVP()` takes the name of a table type, optionally qualified with a schema and
|
|
1988
|
+
optionally quoted with brackets or double quotes. A size — the length, precision or
|
|
1989
|
+
scale of a type such as `sql.VarChar()` — must be a number, or the word `max` the
|
|
1990
|
+
declaration accepts in its place. Padding is trimmed.
|
|
1991
|
+
|
|
1992
|
+
```javascript
|
|
1993
|
+
request.input('rows', sql.TVP('dbo.[My Type]'), table)
|
|
1994
|
+
request.input('name', sql.VarChar(50), value)
|
|
1995
|
+
request.input('note', sql.VarChar('max'), value)
|
|
1996
|
+
```
|
|
1997
|
+
|
|
1998
|
+
A size is checked wherever it is built into SQL, which includes the declaration a
|
|
1999
|
+
bulk load sends and the parameter list a query sends, not only `sql.TVP()`.
|
|
2000
|
+
|
|
2001
|
+
### Procedure names
|
|
2002
|
+
|
|
2003
|
+
`request.execute()` takes the name of a stored procedure, optionally qualified with
|
|
2004
|
+
a server, database and schema and optionally quoted with brackets or double quotes.
|
|
2005
|
+
An omitted part may be left empty, as T-SQL allows. The `msnodesqlv8` driver builds
|
|
2006
|
+
this into the statement it sends; the `tedious` driver sends it as a bound remote
|
|
2007
|
+
procedure call and could not be injected through it. Both check it, so a name is
|
|
2008
|
+
accepted or rejected the same way whichever driver you use.
|
|
2009
|
+
|
|
2010
|
+
```javascript
|
|
2011
|
+
request.execute('dbo.[My Procedure]')
|
|
2012
|
+
request.execute('master..sp_who') // an omitted schema is fine
|
|
2013
|
+
```
|
|
2014
|
+
|
|
2015
|
+
The first and last parts must be present, so `.proc` and `db..` are rejected.
|
|
2016
|
+
|
|
2017
|
+
Anything else raises `EINJECT`. Numbered procedures — the deprecated
|
|
2018
|
+
`procedure;1` form — are rejected, because the `;` cannot be told apart from the
|
|
2019
|
+
end of a statement.
|
|
2020
|
+
|
|
2021
|
+
---------------------------------------
|
|
2022
|
+
|
|
1866
2023
|
## Errors
|
|
1867
2024
|
|
|
1868
2025
|
There are 4 types of errors you can handle:
|
|
@@ -1899,10 +2056,10 @@ Name | Code | Message
|
|
|
1899
2056
|
`RequestError` | ECANCEL | Cancelled.
|
|
1900
2057
|
`RequestError` | ETIMEOUT | Request timeout.
|
|
1901
2058
|
`RequestError` | EARGS | Invalid number of arguments.
|
|
1902
|
-
`RequestError` | EINJECT |
|
|
2059
|
+
`RequestError` | EINJECT | Parameter name, column name or type is not a valid identifier.
|
|
1903
2060
|
`RequestError` | ENOCONN | No connection is specified for that request.
|
|
1904
2061
|
`PreparedStatementError` | EARGS | Invalid number of arguments.
|
|
1905
|
-
`PreparedStatementError` | EINJECT |
|
|
2062
|
+
`PreparedStatementError` | EINJECT | Parameter name is not a valid identifier.
|
|
1906
2063
|
`PreparedStatementError` | EALREADYPREPARED | Statement is already prepared.
|
|
1907
2064
|
`PreparedStatementError` | ENOTPREPARED | Statement is not prepared.
|
|
1908
2065
|
|
|
@@ -2133,8 +2290,6 @@ to create new connections or not
|
|
|
2133
2290
|
[downloads-url]: https://www.npmjs.com/package/mssql
|
|
2134
2291
|
[david-image]: https://img.shields.io/david/tediousjs/node-mssql.svg?style=flat-square
|
|
2135
2292
|
[david-url]: https://david-dm.org/tediousjs/node-mssql
|
|
2136
|
-
[appveyor-image]: https://ci.appveyor.com/api/projects/status/e5gq1a0ujwams9t7/branch/master?svg=true
|
|
2137
|
-
[appveyor-url]: https://ci.appveyor.com/project/tediousjs/node-mssql
|
|
2138
2293
|
|
|
2139
2294
|
[tedious-url]: https://www.npmjs.com/package/tedious
|
|
2140
2295
|
[msnodesqlv8-url]: https://www.npmjs.com/package/msnodesqlv8
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const debug = require('debug')('mssql:base')
|
|
4
4
|
const { EventEmitter } = require('node:events')
|
|
5
|
-
const { IDS, objectHasProperty } = require('../utils')
|
|
5
|
+
const { IDS, objectHasProperty, isValidParameterName } = require('../utils')
|
|
6
6
|
const globalConnection = require('../global-connection')
|
|
7
7
|
const { TransactionError, PreparedStatementError } = require('../error')
|
|
8
8
|
const shared = require('../shared')
|
|
@@ -91,8 +91,8 @@ class PreparedStatement extends EventEmitter {
|
|
|
91
91
|
*/
|
|
92
92
|
|
|
93
93
|
input (name, type) {
|
|
94
|
-
if (
|
|
95
|
-
throw new PreparedStatementError(`
|
|
94
|
+
if (!isValidParameterName(name)) {
|
|
95
|
+
throw new PreparedStatementError(`Invalid parameter name '${name}'. Parameter names may only contain identifier characters`, 'EINJECT')
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
if (arguments.length < 2) {
|
|
@@ -144,8 +144,8 @@ class PreparedStatement extends EventEmitter {
|
|
|
144
144
|
*/
|
|
145
145
|
|
|
146
146
|
output (name, type) {
|
|
147
|
-
if (
|
|
148
|
-
throw new PreparedStatementError(`
|
|
147
|
+
if (!isValidParameterName(name)) {
|
|
148
|
+
throw new PreparedStatementError(`Invalid parameter name '${name}'. Parameter names may only contain identifier characters`, 'EINJECT')
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
if (arguments.length < 2) {
|
|
@@ -235,17 +235,25 @@ class PreparedStatement extends EventEmitter {
|
|
|
235
235
|
const req = new shared.driver.Request(this)
|
|
236
236
|
req.stream = false
|
|
237
237
|
req.output('handle', TYPES.Int)
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
let params
|
|
239
|
+
try {
|
|
240
|
+
params = []
|
|
240
241
|
for (const name in this.parameters) {
|
|
241
242
|
if (!objectHasProperty(this.parameters, name)) {
|
|
242
243
|
continue
|
|
243
244
|
}
|
|
244
245
|
const param = this.parameters[name]
|
|
245
|
-
|
|
246
|
+
// a connection is held here, so a rejected declaration has to be reported
|
|
247
|
+
// through the path that releases it
|
|
248
|
+
params.push(`@${name} ${declare(param.type, param)}${param.io === 2 ? ' output' : ''}`)
|
|
246
249
|
}
|
|
247
|
-
|
|
248
|
-
|
|
250
|
+
} catch (e) {
|
|
251
|
+
this.parent.release(this._acquiredConnection)
|
|
252
|
+
this._acquiredConnection = null
|
|
253
|
+
this._acquiredConfig = null
|
|
254
|
+
return callback(new PreparedStatementError(e, 'EREQUEST'))
|
|
255
|
+
}
|
|
256
|
+
req.input('params', TYPES.NVarChar, params.join(','))
|
|
249
257
|
req.input('stmt', TYPES.NVarChar, this.statement)
|
|
250
258
|
req.execute('sp_prepare', (err, result) => {
|
|
251
259
|
if (err) {
|
package/lib/base/request.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const debug = require('debug')('mssql:base')
|
|
4
4
|
const { EventEmitter } = require('node:events')
|
|
5
5
|
const { Readable } = require('node:stream')
|
|
6
|
-
const { IDS, objectHasProperty } = require('../utils')
|
|
6
|
+
const { IDS, objectHasProperty, isValidParameterName } = require('../utils')
|
|
7
7
|
const globalConnection = require('../global-connection')
|
|
8
8
|
const { RequestError, ConnectionError } = require('../error')
|
|
9
9
|
const { TYPES } = require('../datatypes')
|
|
@@ -107,8 +107,8 @@ class Request extends EventEmitter {
|
|
|
107
107
|
*/
|
|
108
108
|
|
|
109
109
|
input (name, type, value) {
|
|
110
|
-
if (
|
|
111
|
-
throw new RequestError(`
|
|
110
|
+
if (!isValidParameterName(name)) {
|
|
111
|
+
throw new RequestError(`Invalid parameter name '${name}'. Parameter names may only contain identifier characters`, 'EINJECT')
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
if (arguments.length < 2) {
|
|
@@ -170,8 +170,8 @@ class Request extends EventEmitter {
|
|
|
170
170
|
output (name, type, value) {
|
|
171
171
|
if (!type) { type = TYPES.NVarChar }
|
|
172
172
|
|
|
173
|
-
if (
|
|
174
|
-
throw new RequestError(`
|
|
173
|
+
if (!isValidParameterName(name)) {
|
|
174
|
+
throw new RequestError(`Invalid parameter name '${name}'. Parameter names may only contain identifier characters`, 'EINJECT')
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
if ((type === TYPES.Text) || (type === TYPES.NText) || (type === TYPES.Image)) {
|
package/lib/datatypes.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use strict'
|
|
2
|
-
const objectHasProperty = require('./utils')
|
|
2
|
+
const { objectHasProperty, assertSafeTypeName, assertSafeTypeSize } = require('./utils')
|
|
3
3
|
const inspect = Symbol.for('nodejs.util.inspect.custom')
|
|
4
4
|
|
|
5
5
|
const TYPES = {
|
|
@@ -132,19 +132,33 @@ for (const key in TYPES) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
module.exports.declare = (type, options) => {
|
|
135
|
+
// sizes and the type name reach the generated declaration directly, so they are checked
|
|
136
|
+
// here rather than trusted from whatever built the parameter. Each is read only in the
|
|
137
|
+
// branch that emits it, so a type that takes no size still needs no options.
|
|
135
138
|
switch (type) {
|
|
136
|
-
case TYPES.VarChar: case TYPES.VarBinary:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
case TYPES.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
case TYPES.
|
|
145
|
-
|
|
139
|
+
case TYPES.VarChar: case TYPES.VarBinary: {
|
|
140
|
+
const length = assertSafeTypeSize(options.length)
|
|
141
|
+
return `${type.declaration} (${length > 8000 ? 'MAX' : (length == null ? 'MAX' : length)})`
|
|
142
|
+
}
|
|
143
|
+
case TYPES.NVarChar: {
|
|
144
|
+
const length = assertSafeTypeSize(options.length)
|
|
145
|
+
return `${type.declaration} (${length > 4000 ? 'MAX' : (length == null ? 'MAX' : length)})`
|
|
146
|
+
}
|
|
147
|
+
case TYPES.Char: case TYPES.NChar: case TYPES.Binary: {
|
|
148
|
+
const length = assertSafeTypeSize(options.length)
|
|
149
|
+
return `${type.declaration} (${length == null ? 1 : length})`
|
|
150
|
+
}
|
|
151
|
+
case TYPES.Decimal: case TYPES.Numeric: {
|
|
152
|
+
const precision = assertSafeTypeSize(options.precision)
|
|
153
|
+
const scale = assertSafeTypeSize(options.scale)
|
|
154
|
+
return `${type.declaration} (${precision == null ? 18 : precision}, ${scale == null ? 0 : scale})`
|
|
155
|
+
}
|
|
156
|
+
case TYPES.Time: case TYPES.DateTime2: case TYPES.DateTimeOffset: {
|
|
157
|
+
const scale = assertSafeTypeSize(options.scale)
|
|
158
|
+
return `${type.declaration} (${scale == null ? 7 : scale})`
|
|
159
|
+
}
|
|
146
160
|
case TYPES.TVP:
|
|
147
|
-
return `${options.tvpType} readonly`
|
|
161
|
+
return `${assertSafeTypeName(options.tvpType)} readonly`
|
|
148
162
|
default:
|
|
149
163
|
return type.declaration
|
|
150
164
|
}
|
|
@@ -4,7 +4,7 @@ const msnodesql = require('msnodesqlv8')
|
|
|
4
4
|
const debug = require('debug')('mssql:msv8')
|
|
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 } = require('../datatypes')
|
|
9
9
|
const { PARSERS: UDT } = require('../udt')
|
|
10
10
|
const Table = require('../table')
|
|
@@ -154,16 +154,32 @@ class Request extends BaseRequest {
|
|
|
154
154
|
|
|
155
155
|
try {
|
|
156
156
|
table._makeBulk()
|
|
157
|
+
// Checked before a connection is borrowed, so that a rejected name cannot leave
|
|
158
|
+
// the pool short of one.
|
|
159
|
+
table.columns.forEach(col => {
|
|
160
|
+
assertSafeColumnName(col.name)
|
|
161
|
+
// This driver hands the column definition to its table manager rather than writing
|
|
162
|
+
// it into SQL, so a size never reaches a statement here. It is checked all the same,
|
|
163
|
+
// so that a column either works on both drivers or on neither.
|
|
164
|
+
assertSafeTypeSize(col.length)
|
|
165
|
+
assertSafeTypeSize(col.precision)
|
|
166
|
+
assertSafeTypeSize(col.scale)
|
|
167
|
+
})
|
|
168
|
+
// checked here too, so an ordering key is accepted or rejected the same way on
|
|
169
|
+
// either driver even though this one does not build the statement itself
|
|
170
|
+
if (options && options.order) {
|
|
171
|
+
Object.keys(options.order).forEach(key => assertSafeTypeName(key))
|
|
172
|
+
}
|
|
157
173
|
} catch (e) {
|
|
158
174
|
return callback(new RequestError(e, 'EREQUEST'))
|
|
159
175
|
}
|
|
160
176
|
|
|
161
177
|
if (!table.name) {
|
|
162
|
-
setImmediate(callback, new RequestError('Table name must be specified for bulk insert.', 'ENAME'))
|
|
178
|
+
return setImmediate(callback, new RequestError('Table name must be specified for bulk insert.', 'ENAME'))
|
|
163
179
|
}
|
|
164
180
|
|
|
165
181
|
if (table.name.charAt(0) === '@') {
|
|
166
|
-
setImmediate(callback, new RequestError("You can't use table variables for bulk insert.", 'ENAME'))
|
|
182
|
+
return setImmediate(callback, new RequestError("You can't use table variables for bulk insert.", 'ENAME'))
|
|
167
183
|
}
|
|
168
184
|
|
|
169
185
|
this.parent.acquire(this, (err, connection) => {
|
|
@@ -189,8 +205,11 @@ class Request extends BaseRequest {
|
|
|
189
205
|
connection.hasError = true
|
|
190
206
|
}
|
|
191
207
|
|
|
208
|
+
// a rejected identifier keeps its own code; anything else is a driver
|
|
209
|
+
// error, whose numeric code must not become the public one
|
|
210
|
+
const rejectedIdentifier = err.code === 'EINJECT'
|
|
192
211
|
err = new RequestError(err)
|
|
193
|
-
err.code = 'EREQUEST'
|
|
212
|
+
err.code = rejectedIdentifier ? 'EINJECT' : 'EREQUEST'
|
|
194
213
|
}
|
|
195
214
|
|
|
196
215
|
this.parent.release(connection)
|
|
@@ -210,14 +229,29 @@ class Request extends BaseRequest {
|
|
|
210
229
|
}
|
|
211
230
|
|
|
212
231
|
const rows = []
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
232
|
+
try {
|
|
233
|
+
for (const col of table.columns) {
|
|
234
|
+
// re-checked here for the same reason as the name below, in case the
|
|
235
|
+
// definition changed since it was checked above
|
|
236
|
+
assertSafeTypeSize(col.length)
|
|
237
|
+
assertSafeTypeSize(col.precision)
|
|
238
|
+
assertSafeTypeSize(col.scale)
|
|
218
239
|
}
|
|
219
240
|
|
|
220
|
-
|
|
241
|
+
for (const row of Array.from(table.rows)) {
|
|
242
|
+
const item = {}
|
|
243
|
+
for (let index = 0; index < table.columns.length; index++) {
|
|
244
|
+
const col = table.columns[index]
|
|
245
|
+
// this driver hands the name to its table manager rather than writing
|
|
246
|
+
// it into SQL, but it is checked here all the same: a name either
|
|
247
|
+
// works on both drivers or on neither
|
|
248
|
+
item[assertSafeColumnName(col.name)] = row[index]
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
rows.push(item)
|
|
252
|
+
}
|
|
253
|
+
} catch (e) {
|
|
254
|
+
return done(e)
|
|
221
255
|
}
|
|
222
256
|
|
|
223
257
|
mgr.insertRows(rows, done)
|
|
@@ -232,7 +266,16 @@ class Request extends BaseRequest {
|
|
|
232
266
|
objectid = table.path
|
|
233
267
|
}
|
|
234
268
|
|
|
235
|
-
|
|
269
|
+
// A connection is held from here on, so a column name that changed since it
|
|
270
|
+
// was checked above must be reported through `done`, which releases it.
|
|
271
|
+
let statement
|
|
272
|
+
try {
|
|
273
|
+
statement = `if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`
|
|
274
|
+
} catch (e) {
|
|
275
|
+
return done(e)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return connection.queryRaw(statement, function (err) {
|
|
236
279
|
if (err) { return done(err) }
|
|
237
280
|
go()
|
|
238
281
|
})
|
|
@@ -314,12 +357,18 @@ class Request extends BaseRequest {
|
|
|
314
357
|
|
|
315
358
|
if (!this._nested) {
|
|
316
359
|
const input = []
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
360
|
+
try {
|
|
361
|
+
for (const name in this.parameters) {
|
|
362
|
+
if (!objectHasProperty(this.parameters, name)) {
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
365
|
+
const param = this.parameters[name]
|
|
366
|
+
// this callback is dispatched on its own tick, so a rejected declaration has
|
|
367
|
+
// to be handed back rather than thrown
|
|
368
|
+
input.push(`@${param.name} ${declare(param.type, param)}`)
|
|
320
369
|
}
|
|
321
|
-
|
|
322
|
-
|
|
370
|
+
} catch (e) {
|
|
371
|
+
return callback(new RequestError(e, 'EREQUEST'))
|
|
323
372
|
}
|
|
324
373
|
|
|
325
374
|
const sets = []
|
|
@@ -634,14 +683,32 @@ class Request extends BaseRequest {
|
|
|
634
683
|
if (err) return callback(err)
|
|
635
684
|
|
|
636
685
|
const params = []
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
686
|
+
let procedureName
|
|
687
|
+
try {
|
|
688
|
+
// this is built into `exec @___return___ = <name>` below and run as SQL text, so it is
|
|
689
|
+
// checked before it is emitted
|
|
690
|
+
procedureName = assertSafeProcedureName(procedure)
|
|
691
|
+
for (const name in this.parameters) {
|
|
692
|
+
if (!objectHasProperty(this.parameters, name)) {
|
|
693
|
+
continue
|
|
694
|
+
}
|
|
695
|
+
const param = this.parameters[name]
|
|
696
|
+
if (param.io === 2) {
|
|
697
|
+
// this callback is dispatched on its own tick, so a rejected declaration has
|
|
698
|
+
// to be handed back rather than thrown
|
|
699
|
+
params.push(`@${param.name} ${declare(param.type, param)}`)
|
|
700
|
+
} else {
|
|
701
|
+
// An input parameter is bound rather than written into the command, so only an
|
|
702
|
+
// output parameter's size is declared here and nothing would otherwise check an
|
|
703
|
+
// input's. It is checked so that a size is accepted or rejected the same way
|
|
704
|
+
// whichever driver is in use.
|
|
705
|
+
assertSafeTypeSize(param.length)
|
|
706
|
+
assertSafeTypeSize(param.precision)
|
|
707
|
+
assertSafeTypeSize(param.scale)
|
|
708
|
+
}
|
|
644
709
|
}
|
|
710
|
+
} catch (e) {
|
|
711
|
+
return callback(new RequestError(e, 'EREQUEST'))
|
|
645
712
|
}
|
|
646
713
|
|
|
647
714
|
// set output params w/ values
|
|
@@ -657,7 +724,7 @@ class Request extends BaseRequest {
|
|
|
657
724
|
}
|
|
658
725
|
|
|
659
726
|
let cmd = `declare ${['@___return___ int'].concat(params).join(', ')};${sets.join(';')};`
|
|
660
|
-
cmd += `exec @___return___ = ${
|
|
727
|
+
cmd += `exec @___return___ = ${procedureName} `
|
|
661
728
|
|
|
662
729
|
const spp = []
|
|
663
730
|
for (const name in this.parameters) {
|
package/lib/shared.js
CHANGED
|
@@ -57,7 +57,6 @@ const getTypeByValue = function (value) {
|
|
|
57
57
|
return TYPES.NVarChar
|
|
58
58
|
|
|
59
59
|
case 'number':
|
|
60
|
-
case 'bigint':
|
|
61
60
|
if (value % 1 === 0) {
|
|
62
61
|
if (value < -2147483648 || value > 2147483647) {
|
|
63
62
|
return TYPES.BigInt
|
|
@@ -68,6 +67,13 @@ const getTypeByValue = function (value) {
|
|
|
68
67
|
return TYPES.Float
|
|
69
68
|
}
|
|
70
69
|
|
|
70
|
+
case 'bigint':
|
|
71
|
+
if (value < -2147483648n || value > 2147483647n) {
|
|
72
|
+
return TYPES.BigInt
|
|
73
|
+
} else {
|
|
74
|
+
return TYPES.Int
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
case 'boolean':
|
|
72
78
|
for (const item of Array.from(map)) {
|
|
73
79
|
if (item.js === Boolean) {
|
package/lib/table.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const TYPES = require('./datatypes').TYPES
|
|
4
4
|
const declareType = require('./datatypes').declare
|
|
5
|
-
const objectHasProperty = require('./utils')
|
|
5
|
+
const { objectHasProperty, assertSafeColumnName } = require('./utils')
|
|
6
6
|
|
|
7
7
|
const MAX = 65535 // (1 << 16) - 1
|
|
8
8
|
const JSON_COLUMN_ID = 'JSON_F52E2B61-18A1-11d1-B105-00805F49916B'
|
|
@@ -99,9 +99,9 @@ Table.prototype._makeBulk = function _makeBulk () {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
Table.prototype.declare = function declare () {
|
|
102
|
-
const pkey = this.columns.filter(col => col.primary === true).map(col => `[${col.name}]`)
|
|
102
|
+
const pkey = this.columns.filter(col => col.primary === true).map(col => `[${assertSafeColumnName(col.name)}]`)
|
|
103
103
|
const cols = this.columns.map(col => {
|
|
104
|
-
const def = [`[${col.name}] ${declareType(col.type, col)}`]
|
|
104
|
+
const def = [`[${assertSafeColumnName(col.name)}] ${declareType(col.type, col)}`]
|
|
105
105
|
|
|
106
106
|
if (col.nullable === true) {
|
|
107
107
|
def.push('null')
|
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": "11.0.
|
|
24
|
+
"version": "11.0.2",
|
|
25
25
|
"main": "index.js",
|
|
26
26
|
"type": "commonjs",
|
|
27
27
|
"repository": "github:tediousjs/node-mssql",
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@commitlint/cli": "^19.3.0",
|
|
39
39
|
"@commitlint/config-conventional": "^19.2.2",
|
|
40
|
-
"@semantic-release/commit-analyzer": "^
|
|
41
|
-
"@semantic-release/github": "^
|
|
42
|
-
"@semantic-release/npm": "^
|
|
43
|
-
"@semantic-release/release-notes-generator": "^
|
|
40
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
41
|
+
"@semantic-release/github": "^12.0.6",
|
|
42
|
+
"@semantic-release/npm": "^13.1.5",
|
|
43
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
44
44
|
"mocha": "^10.0.0",
|
|
45
|
-
"semantic-release": "^
|
|
45
|
+
"semantic-release": "^25.0.3",
|
|
46
46
|
"standard": "^17.0.0"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|