@oino-ts/db-mssql 0.21.2 → 1.0.0

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.
@@ -1,540 +1,541 @@
1
- /*
2
- * This Source Code Form is subject to the terms of the Mozilla Public
3
- * License, v. 2.0. If a copy of the MPL was not distributed with this
4
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
- */
6
-
7
- import { OINO_ERROR_PREFIX, OINOBenchmark, OINO_INFO_PREFIX, OINOLog, OINOResult } from "@oino-ts/common";
8
- import { OINODb, OINODbParams, OINODbDataSet, OINODbApi, OINOBooleanDataField, OINONumberDataField, OINOStringDataField, OINODbDataFieldParams, OINODataRow, OINODataCell, OINODatetimeDataField, OINOBlobDataField, OINODB_EMPTY_ROW, OINODB_EMPTY_ROWS } from "@oino-ts/db";
9
-
10
- import {ConnectionPool, config} from "mssql";
11
-
12
- /**
13
- * Implmentation of OINODbDataSet for MsSql.
14
- *
15
- */
16
- class OINOMsSqlData extends OINODbDataSet {
17
- private _recordsets:any
18
- private _rows:OINODataRow[] = OINODB_EMPTY_ROWS
19
-
20
- private _currentRecordset: number
21
- private _currentRow: number
22
- private _eof: boolean
23
-
24
- /**
25
- * OINOMsSqlData constructor
26
- * @param params database parameters
27
- */
28
- constructor(data: any, messages:string[]=[]) {
29
- super(data, messages)
30
- if (data == null) {
31
- this.messages.push(OINO_INFO_PREFIX + "SQL result is empty")
32
-
33
- } else if (!(Array.isArray(data)) && (data.length > 0)) {
34
- throw new Error(OINO_ERROR_PREFIX + ": OINOMsSqlData constructor: invalid data!")
35
-
36
- } else {
37
- this._recordsets = data
38
- this._rows = this._recordsets[0]
39
- }
40
- if (this.isEmpty()) {
41
- this._currentRecordset = -1
42
- this._currentRow = -1
43
- this._eof = true
44
- } else {
45
- this._currentRecordset = 0
46
- this._currentRow = 0
47
- this._eof = false
48
- }
49
- }
50
-
51
- /**
52
- * Is data set empty.
53
- *
54
- */
55
- isEmpty():boolean {
56
- return (this._recordsets.length == 0) || (this._rows == undefined) || (this._rows.length == 0)
57
- }
58
-
59
- /**
60
- * Is there no more content, i.e. either dataset is empty or we have moved beyond last line
61
- *
62
- */
63
- isEof():boolean {
64
- return (this._eof)
65
- }
66
-
67
- /**
68
- * Attempts to moves dataset to the next row, possibly waiting for more data to become available. Returns !isEof().
69
- *
70
- */
71
- async next():Promise<boolean> {
72
- if (this._currentRow < this._rows.length-1) {
73
- this._currentRow = this._currentRow + 1
74
-
75
- } else if (this._currentRecordset < this._recordsets.length-1) {
76
- this._currentRecordset = this._currentRecordset + 1
77
- this._rows = this._recordsets[this._currentRecordset]
78
- this._currentRow = 0
79
-
80
- } else {
81
- this._eof = true
82
- }
83
- return Promise.resolve(!this._eof)
84
- }
85
-
86
- /**
87
- * Gets current row of data.
88
- *
89
- */
90
- getRow(): OINODataRow {
91
- if ((this._currentRow >=0) && (this._currentRow < this._rows.length)) {
92
- return this._rows[this._currentRow]
93
- } else {
94
- return OINODB_EMPTY_ROW
95
- }
96
- }
97
-
98
- /**
99
- * Gets all rows of data.
100
- *
101
- */
102
- async getAllRows(): Promise<OINODataRow[]> {
103
- return this._rows // at the moment theres no result streaming, so we can just return the rows
104
- }
105
-
106
- }
107
-
108
- /**
109
- * Implementation of MsSql-database.
110
- *
111
- */
112
- export class OINODbMsSql extends OINODb {
113
-
114
- private _pool:ConnectionPool
115
-
116
- /**
117
- * Constructor of `OINODbMsSql`
118
- * @param params database parameters
119
- */
120
- constructor(params:OINODbParams) {
121
- super(params)
122
-
123
- if (this._params.type !== "OINODbMsSql") {
124
- throw new Error(OINO_ERROR_PREFIX + ": Not OINODbMsSql-type: " + this._params.type)
125
- }
126
- this._pool = new ConnectionPool({
127
- user: this._params.user,
128
- password: this._params.password,
129
- server: this._params.url,
130
- port: this._params.port,
131
- database: this._params.database,
132
- arrayRowMode:true,
133
- options: {
134
- encrypt: true, // Use encryption for Azure SQL Database
135
- rowCollectionOnRequestCompletion:false,
136
- rowCollectionOnDone:false,
137
- trustServerCertificate: true // Change to false for production
138
- }
139
- })
140
- delete this._params.password // do not store password in db object
141
-
142
- this._pool.on("error", (conn:any) => {
143
- OINOLog.error("@oino-ts/db-mssql", "OINODbMsSql", "constructor", "OINODbMsSql error event", conn)
144
- })
145
- }
146
-
147
- private async _query(sql:string):Promise<OINOMsSqlData> {
148
- try {
149
- const request = this._pool.request() // this does not need to be released but the pool will handle it
150
- const sql_res = await request.query(sql)
151
- // console.log("_query: result=", sql_res.recordsets, sql_res.recordsets?.length) // TODO: remove
152
- return new OINOMsSqlData(sql_res.recordsets, [])
153
- } catch (e:any) {
154
- OINOLog.exception("@oino-ts/db-mssql", "OINODbMsSql", "_query", "exception in SQL query", {message:e.message, stack:e.stack, sql:sql})
155
- return new OINOMsSqlData(OINODB_EMPTY_ROWS, []).setError(500, OINO_ERROR_PREFIX + ": Exception in db query: " + e.message, "OINODbMsSql._query") as OINOMsSqlData
156
- }
157
- }
158
-
159
- private async _exec(sql:string):Promise<OINOMsSqlData> {
160
- try {
161
- const request = this._pool.request() // this does not need to be released but the pool will handle it
162
- const sql_res = await request.query(sql)
163
- // console.log("_exec: result=", sql_res.recordsets, sql_res.recordsets?.length) // TODO: remove
164
- return new OINOMsSqlData(sql_res.recordsets, [])
165
- } catch (e:any) {
166
- OINOLog.exception("@oino-ts/db-mssql", "OINODbMsSql", "_exec", "exception in SQL exec", {message:e.message, stack:e.stack, sql:sql})
167
- return new OINOMsSqlData(OINODB_EMPTY_ROWS, []).setError(500, OINO_ERROR_PREFIX + ": Exception in db exec: " + e.message, "OINODbMsSql._exec") as OINOMsSqlData
168
- }
169
- }
170
-
171
- /**
172
- * Print a table name using database specific SQL escaping.
173
- *
174
- * @param sqlTable name of the table
175
- *
176
- */
177
- printSqlTablename(sqlTable:string): string {
178
- return "["+sqlTable+"]"
179
- }
180
-
181
- /**
182
- * Print a column name with correct SQL escaping.
183
- *
184
- * @param sqlColumn name of the column
185
- *
186
- */
187
- printSqlColumnname(sqlColumn:string): string {
188
- return "["+sqlColumn+"]"
189
- }
190
-
191
-
192
- /**
193
- * Print a single data value from serialization using the context of the native data
194
- * type with the correct SQL escaping.
195
- *
196
- * @param cellValue data from sql results
197
- * @param sqlType native type name for table column
198
- *
199
- */
200
- printCellAsSqlValue(cellValue:OINODataCell, sqlType: string): string {
201
- if (cellValue === null) {
202
- return "NULL"
203
-
204
- } else if (cellValue === undefined) {
205
- return "UNDEFINED"
206
-
207
- } else if ((sqlType == "int") || (sqlType == "smallint") || (sqlType == "float")) {
208
- return cellValue.toString()
209
-
210
- } else if ((sqlType == "longblob") || (sqlType == "binary") || (sqlType == "varbinary")) {
211
- if (cellValue instanceof Buffer) {
212
- return "0x" + (cellValue as Buffer).toString("hex") + ""
213
- } else if (cellValue instanceof Uint8Array) {
214
- return "0x" + Buffer.from(cellValue as Uint8Array).toString("hex") + ""
215
- } else {
216
- return "'" + cellValue?.toString() + "'"
217
- }
218
-
219
- } else if (((sqlType == "date") || (sqlType == "datetime") || (sqlType == "datetime2") || (sqlType == "timestamp")) && (cellValue instanceof Date)) {
220
- return "'" + cellValue.toISOString().substring(0, 23) + "'"
221
-
222
- } else {
223
- return this.printSqlString(cellValue.toString())
224
- }
225
- }
226
-
227
- /**
228
- * Print a single string value as valid sql literal
229
- *
230
- * @param sqlString string value
231
- *
232
- */
233
- printSqlString(sqlString:string): string {
234
- return "'" + sqlString.replaceAll("'", "''") + "'"
235
- }
236
-
237
- /**
238
- * Parse a single SQL result value for serialization using the context of the native data
239
- * type.
240
- *
241
- * @param sqlValue data from serialization
242
- * @param sqlType native type name for table column
243
- *
244
- */
245
- parseSqlValueAsCell(sqlValue:OINODataCell, sqlType: string): OINODataCell {
246
- if ((sqlValue === null) || (sqlValue == "NULL")) {
247
- return null
248
-
249
- } else if (sqlValue === undefined) {
250
- return undefined
251
-
252
- } else if (((sqlType == "date") || (sqlType == "datetime") || (sqlType == "datetime2")) && (typeof(sqlValue) == "string") && (sqlValue != "")) {
253
- return new Date(sqlValue)
254
-
255
- } else if (sqlType == "bit") {
256
- return (sqlValue === 1) || (sqlValue === true) // sometimes boolean and sometimes number
257
-
258
- } else {
259
- return sqlValue
260
- }
261
-
262
- }
263
-
264
- /**
265
- * Print SQL select statement with DB specific formatting.
266
- *
267
- * @param tableName - The name of the table to select from.
268
- * @param columnNames - The columns to be selected.
269
- * @param whereCondition - The WHERE clause to filter the results.
270
- * @param orderCondition - The ORDER BY clause to sort the results.
271
- * @param limitCondition - The LIMIT clause to limit the number of results.
272
- * @param groupByCondition - The GROUP BY clause to group the results.
273
- *
274
- */
275
- printSqlSelect(tableName:string, columnNames:string, whereCondition:string, orderCondition:string, limitCondition:string, groupByCondition: string): string {
276
- const limit_parts = limitCondition.split(" OFFSET ")
277
- let result:string = "SELECT "
278
- if ((limitCondition != "") && (limit_parts.length == 1)) {
279
- result += "TOP " + limit_parts[0] + " "
280
- }
281
- result += columnNames + " FROM " + tableName
282
- if (whereCondition != "") {
283
- result += " WHERE " + whereCondition
284
- }
285
- if (groupByCondition != "") {
286
- result += " GROUP BY " + groupByCondition
287
- }
288
- if (orderCondition != "") {
289
- result += " ORDER BY " + orderCondition
290
- }
291
- if ((limitCondition != "") && (limit_parts.length == 2)) {
292
- if (orderCondition == "") {
293
- OINOLog.error("@oino-ts/db-mssql", "OINODbMsSql", "printSqlSelect", "LIMIT without ORDER BY is not supported in MS SQL Server")
294
- throw new Error(OINO_ERROR_PREFIX + ": LIMIT without ORDER BY is not supported in MS SQL Server")
295
- } else {
296
- result += " OFFSET " + limit_parts[1] + " ROWS FETCH NEXT " + limit_parts[0] + " ROWS ONLY"
297
- }
298
- }
299
- result += ";"
300
- OINOLog.debug("@oino-ts/db-mssql", "OINODbMsSql", "printSqlSelect", "Result", {sql:result})
301
- return result;
302
- }
303
-
304
- /**
305
- * Print SQL select statement with DB specific formatting.
306
- *
307
- * @param tableName - The name of the table to select from.
308
- * @param columns - The columns to be selected.
309
- * @param values - The values to be inserted.
310
- * @param returnIdFields - the id fields to return if returnIds is true (if supported by the database)
311
- *
312
- */
313
- printSqlInsert(tableName:string, columns:string, values:string, returnIdFields?:string[]): string {
314
- let result = "INSERT INTO " + tableName + " (" + columns + ")"
315
- if (returnIdFields) {
316
- result += " OUTPUT " + returnIdFields.map(f => "INSERTED."+f ).join(", ")
317
- }
318
- result += " VALUES (" + values + ");"
319
- return result;
320
- }
321
-
322
-
323
- /**
324
- * Connect to database.
325
- *
326
- */
327
- async connect(): Promise<OINOResult> {
328
- let result:OINOResult = new OINOResult()
329
- if (this.isConnected) {
330
- return result
331
- }
332
- try {
333
- // make sure that any items are correctly URL encoded in the connection string
334
- await this._pool.connect()
335
- this.isConnected = true
336
-
337
- } catch (e:any) {
338
- // ... error checks
339
- result.setError(500, "Exception connecting to database: " + e.message, "OINODbMsSql.connect")
340
- OINOLog.exception("@oino-ts/db-mssql", "OINODbMsSql", "connect", "exception in connect", {message:e.message, stack:e.stack})
341
- }
342
- return Promise.resolve(result)
343
- }
344
-
345
- /**
346
- * Validate connection to database is working.
347
- *
348
- */
349
- async validate(): Promise<OINOResult> {
350
- let result:OINOResult = new OINOResult()
351
- if (!this.isConnected) {
352
- result.setError(400, "Database is not connected!", "OINODbMsSql.validate")
353
- return result
354
- }
355
- OINOBenchmark.startMetric("OINODb", "validate")
356
- try {
357
- const sql = this._getValidateSql(this._params.database)
358
- const sql_res:OINODbDataSet = await this._query(sql)
359
- if (sql_res.isEmpty()) {
360
- result.setError(400, "DB returned no rows for select!", "OINODbMsSql.validate")
361
-
362
- } else if (sql_res.getRow().length == 0) {
363
- result.setError(400, "DB returned no values for database!", "OINODbMsSql.validate")
364
-
365
- } else if (sql_res.getRow()[0] == "0") {
366
- result.setError(400, "DB returned no schema for database!", "OINODbMsSql.validate")
367
-
368
- } else {
369
- // connection is working
370
- this.isValidated = true
371
- }
372
- } catch (e:any) {
373
- result.setError(500, "Exception in validating connection: " + e.message, "OINODbMsSql.validate")
374
- OINOLog.exception("@oino-ts/db-mssql", "OINODbMsSql", "validate", "exception in validate", {message:e.message, stack:e.stack})
375
- }
376
- OINOBenchmark.endMetric("OINODb", "validate", result.status != 500)
377
- return result
378
- }
379
-
380
- /**
381
- * Disconnect from database.
382
- *
383
- */
384
- async disconnect(): Promise<void> {
385
- if (this._pool) {
386
- try {
387
- await this._pool.close()
388
-
389
- } catch (e:any) {
390
- OINOLog.exception("@oino-ts/db-mssql", "OINODbMsSql", "disconnect", "exception in disconnect", {message:e.message, stack:e.stack})
391
- }
392
- }
393
- this.isConnected = false
394
- this.isValidated = false
395
- }
396
-
397
- /**
398
- * Execute a select operation.
399
- *
400
- * @param sql SQL statement.
401
- *
402
- */
403
- async sqlSelect(sql:string): Promise<OINODbDataSet> {
404
- if (!this.isValidated) {
405
- throw new Error(OINO_ERROR_PREFIX + ": Database connection not validated!")
406
- }
407
- OINOBenchmark.startMetric("OINODb", "sqlSelect")
408
- let result:OINODbDataSet = await this._query(sql)
409
- OINOBenchmark.endMetric("OINODb", "sqlSelect", result.status != 500)
410
- return result
411
- }
412
-
413
- /**
414
- * Execute other sql operations.
415
- *
416
- * @param sql SQL statement.
417
- *
418
- */
419
- async sqlExec(sql:string): Promise<OINODbDataSet> {
420
- if (!this.isValidated) {
421
- throw new Error(OINO_ERROR_PREFIX + ": Database connection not validated!")
422
- }
423
- OINOBenchmark.startMetric("OINODb", "sqlExec")
424
- let result:OINODbDataSet = await this._exec(sql)
425
- OINOBenchmark.endMetric("OINODb", "sqlExec", result.status != 500)
426
- return result
427
- }
428
-
429
- private _getSchemaSql(dbName:string, tableName:string):string {
430
- const sql =
431
- `SELECT
432
- C.COLUMN_NAME,
433
- C.IS_NULLABLE,
434
- C.DATA_TYPE,
435
- C.CHARACTER_MAXIMUM_LENGTH,
436
- C.NUMERIC_PRECISION,
437
- C.NUMERIC_PRECISION_RADIX,
438
- CONST.CONSTRAINT_TYPES,
439
- COLUMNPROPERTY(OBJECT_ID(C.TABLE_SCHEMA + '.' + C.TABLE_NAME), C.COLUMN_NAME, 'IsIdentity') AS IS_AUTO_INCREMENT,
440
- COLUMNPROPERTY(OBJECT_ID(C.TABLE_SCHEMA + '.' + C.TABLE_NAME), C.COLUMN_NAME, 'IsComputed') AS IS_COMPUTED
441
- FROM
442
- INFORMATION_SCHEMA.COLUMNS as C LEFT JOIN
443
- (
444
- SELECT TC.TABLE_NAME, KU.COLUMN_NAME, STRING_AGG(TC.CONSTRAINT_TYPE, ',') as CONSTRAINT_TYPES
445
- FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS TC
446
- INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KU ON TC.CONSTRAINT_NAME = KU.CONSTRAINT_NAME
447
- GROUP BY TC.TABLE_NAME, KU.COLUMN_NAME
448
- ) as CONST
449
- ON C.TABLE_NAME = CONST.TABLE_NAME AND C.COLUMN_NAME = CONST.COLUMN_NAME
450
- WHERE C.TABLE_CATALOG = '${dbName}' AND C.TABLE_NAME = '${tableName}'
451
- ORDER BY C.ORDINAL_POSITION;`
452
- return sql
453
- }
454
-
455
- private _getValidateSql(dbName:string):string {
456
- const sql =
457
- `SELECT
458
- count(C.COLUMN_NAME) AS COLUMN_COUNT
459
- FROM
460
- INFORMATION_SCHEMA.COLUMNS as C LEFT JOIN
461
- (
462
- SELECT TC.TABLE_NAME, KU.COLUMN_NAME, STRING_AGG(TC.CONSTRAINT_TYPE, ',') as CONSTRAINT_TYPES
463
- FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS TC
464
- INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KU ON TC.CONSTRAINT_NAME = KU.CONSTRAINT_NAME
465
- GROUP BY TC.TABLE_NAME, KU.COLUMN_NAME
466
- ) as CONST
467
- ON C.TABLE_NAME = CONST.TABLE_NAME AND C.COLUMN_NAME = CONST.COLUMN_NAME
468
- WHERE C.TABLE_CATALOG = '${dbName}';`
469
- return sql
470
- }
471
-
472
- /**
473
- * Initialize a data model by getting the SQL schema and populating OINODbDataFields of
474
- * the model.
475
- *
476
- * @param api api which data model to initialize.
477
- *
478
- */
479
- async initializeApiDatamodel(api:OINODbApi): Promise<void> {
480
-
481
- //"SELECT COLUMN_NAME, IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_PRECISION_RADIX
482
- const schema_res:OINODbDataSet = await this.sqlSelect(this._getSchemaSql(this._params.database, api.params.tableName))
483
- while (!schema_res.isEof()) {
484
- const row:OINODataRow = schema_res.getRow()
485
- const field_name:string = row[0]?.toString() || ""
486
- const sql_type:string = row[2] as string || ""
487
- const char_field_length:number = row[3] as number || 0
488
- const numeric_field_length1:number = row[4] as number || 0
489
- const numeric_field_length2:number = row[5] as number || 0
490
- const constraint_types:string = row[6] as string || ""
491
- const field_params:OINODbDataFieldParams = {
492
- isPrimaryKey: constraint_types.indexOf("PRIMARY KEY") >= 0,
493
- isForeignKey: constraint_types.indexOf("FOREIGN KEY") >= 0,
494
- isAutoInc: row[7] == 1,
495
- isNotNull: row[1] == "NO"
496
- }
497
- if (api.isFieldIncluded(field_name) == false) {
498
- OINOLog.info("@oino-ts/db-mssql", "OINODbMsSql", "initializeApiDatamodel", "Field excluded in API parameters.", {field:field_name})
499
- if (field_params.isPrimaryKey) {
500
- throw new Error(OINO_ERROR_PREFIX + "Primary key field excluded in API parameters: " + field_name)
501
- }
502
-
503
- } else {
504
- if ((sql_type == "tinyint") || (sql_type == "smallint") || (sql_type == "int") || (sql_type == "bigint") || (sql_type == "float") || (sql_type == "real")) {
505
- api.datamodel.addField(new OINONumberDataField(this, field_name, sql_type, field_params ))
506
-
507
- } else if ((sql_type == "date") || (sql_type == "datetime") || (sql_type == "datetime2")) {
508
- if (api.params.useDatesAsString) {
509
- api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
510
- } else {
511
- api.datamodel.addField(new OINODatetimeDataField(this, field_name, sql_type, field_params))
512
- }
513
-
514
- } else if ((sql_type == "ntext") || (sql_type == "nchar") || (sql_type == "nvarchar") || (sql_type == "text") || (sql_type == "char") || (sql_type == "varchar")) {
515
- api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, char_field_length))
516
-
517
- } else if ((sql_type == "binary") || (sql_type == "varbinary") || (sql_type == "image")) {
518
- api.datamodel.addField(new OINOBlobDataField(this, field_name, sql_type, field_params, char_field_length))
519
-
520
- } else if ((sql_type == "numeric") || (sql_type == "decimal") || (sql_type == "money")) {
521
- api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, numeric_field_length1 + numeric_field_length2 + 1))
522
-
523
- } else if ((sql_type == "bit")) {
524
- api.datamodel.addField(new OINOBooleanDataField(this, field_name, sql_type, field_params))
525
-
526
- } else {
527
- OINOLog.info("@oino-ts/db-mssql", "OINODbMsSql", "initializeApiDatamodel", "Unrecognized field type treated as string", {field_name: field_name, sql_type:sql_type, char_length: char_field_length, numeric_field_length1:numeric_field_length1, numeric_field_length2:numeric_field_length2, field_params:field_params })
528
- api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
529
- }
530
- }
531
- await schema_res.next()
532
- }
533
- OINOLog.info("@oino-ts/db-mssql", "OINODbMsSql", "initializeApiDatamodel", "\n" + api.datamodel.printDebug("\n"))
534
- return Promise.resolve()
535
- }
536
-
537
- }
538
-
539
-
540
-
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+ import { OINO_ERROR_PREFIX, OINOBenchmark, OINO_INFO_PREFIX, OINOLog, OINOResult, OINODataSet, OINOBooleanDataField, OINONumberDataField, OINOStringDataField, OINODataFieldParams, OINODataRow, OINODataCell, OINODatetimeDataField, OINOBlobDataField, OINO_EMPTY_ROW, OINO_EMPTY_ROWS } from "@oino-ts/common";
8
+
9
+ import { OINODb, OINODbApi, OINODbParams, OINODbDataModel } from "@oino-ts/db";
10
+
11
+ import { ConnectionPool } from "mssql";
12
+
13
+ /**
14
+ * Implmentation of OINODataSet for MsSql.
15
+ *
16
+ */
17
+ class OINOMsSqlData extends OINODataSet {
18
+ private _recordsets:any
19
+ private _rows:OINODataRow[] = OINO_EMPTY_ROWS
20
+
21
+ private _currentRecordset: number
22
+ private _currentRow: number
23
+ private _eof: boolean
24
+
25
+ /**
26
+ * OINOMsSqlData constructor
27
+ * @param params database parameters
28
+ */
29
+ constructor(data: any, messages:string[]=[]) {
30
+ super(data, messages)
31
+ if (data == null) {
32
+ this.messages.push(OINO_INFO_PREFIX + "SQL result is empty")
33
+
34
+ } else if (!(Array.isArray(data)) && (data.length > 0)) {
35
+ throw new Error(OINO_ERROR_PREFIX + ": OINOMsSqlData constructor: invalid data!")
36
+
37
+ } else {
38
+ this._recordsets = data
39
+ this._rows = this._recordsets[0]
40
+ }
41
+ if (this.isEmpty()) {
42
+ this._currentRecordset = -1
43
+ this._currentRow = -1
44
+ this._eof = true
45
+ } else {
46
+ this._currentRecordset = 0
47
+ this._currentRow = 0
48
+ this._eof = false
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Is data set empty.
54
+ *
55
+ */
56
+ isEmpty():boolean {
57
+ return (this._recordsets.length == 0) || (this._rows == undefined) || (this._rows.length == 0)
58
+ }
59
+
60
+ /**
61
+ * Is there no more content, i.e. either dataset is empty or we have moved beyond last line
62
+ *
63
+ */
64
+ isEof():boolean {
65
+ return (this._eof)
66
+ }
67
+
68
+ /**
69
+ * Attempts to moves dataset to the next row, possibly waiting for more data to become available. Returns !isEof().
70
+ *
71
+ */
72
+ async next():Promise<boolean> {
73
+ if (this._currentRow < this._rows.length-1) {
74
+ this._currentRow = this._currentRow + 1
75
+
76
+ } else if (this._currentRecordset < this._recordsets.length-1) {
77
+ this._currentRecordset = this._currentRecordset + 1
78
+ this._rows = this._recordsets[this._currentRecordset]
79
+ this._currentRow = 0
80
+
81
+ } else {
82
+ this._eof = true
83
+ }
84
+ return Promise.resolve(!this._eof)
85
+ }
86
+
87
+ /**
88
+ * Gets current row of data.
89
+ *
90
+ */
91
+ getRow(): OINODataRow {
92
+ if ((this._currentRow >=0) && (this._currentRow < this._rows.length)) {
93
+ return this._rows[this._currentRow]
94
+ } else {
95
+ return OINO_EMPTY_ROW
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Gets all rows of data.
101
+ *
102
+ */
103
+ async getAllRows(): Promise<OINODataRow[]> {
104
+ return this._rows // at the moment theres no result streaming, so we can just return the rows
105
+ }
106
+
107
+ }
108
+
109
+ /**
110
+ * Implementation of MsSql-database.
111
+ *
112
+ */
113
+ export class OINODbMsSql extends OINODb {
114
+
115
+ private _pool:ConnectionPool
116
+
117
+ /**
118
+ * Constructor of `OINODbMsSql`
119
+ * @param params database parameters
120
+ */
121
+ constructor(params:OINODbParams) {
122
+ super(params)
123
+
124
+ if (this.dbParams.type !== "OINODbMsSql") {
125
+ throw new Error(OINO_ERROR_PREFIX + ": Not OINODbMsSql-type: " + this.dbParams.type)
126
+ }
127
+ this._pool = new ConnectionPool({
128
+ user: this.dbParams.user,
129
+ password: this.dbParams.password,
130
+ server: this.dbParams.url,
131
+ port: this.dbParams.port,
132
+ database: this.dbParams.database,
133
+ arrayRowMode:true,
134
+ options: {
135
+ encrypt: true, // Use encryption for Azure SQL Database
136
+ rowCollectionOnRequestCompletion:false,
137
+ rowCollectionOnDone:false,
138
+ trustServerCertificate: true // Change to false for production
139
+ }
140
+ })
141
+ delete this.dbParams.password // do not store password in db object
142
+
143
+ this._pool.on("error", (conn:any) => {
144
+ OINOLog.error("@oino-ts/db-mssql", "OINODbMsSql", "constructor", "OINODbMsSql error event", conn)
145
+ })
146
+ }
147
+
148
+ private async _query(sql:string):Promise<OINOMsSqlData> {
149
+ try {
150
+ const request = this._pool.request() // this does not need to be released but the pool will handle it
151
+ const sql_res = await request.query(sql)
152
+ // console.log("_query: result=", sql_res.recordsets, sql_res.recordsets?.length) // TODO: remove
153
+ return new OINOMsSqlData(sql_res.recordsets, [])
154
+ } catch (e:any) {
155
+ OINOLog.exception("@oino-ts/db-mssql", "OINODbMsSql", "_query", "exception in SQL query", {message:e.message, stack:e.stack, sql:sql})
156
+ return new OINOMsSqlData(OINO_EMPTY_ROWS, []).setError(500, OINO_ERROR_PREFIX + ": Exception in db query: " + e.message, "OINODbMsSql._query") as OINOMsSqlData
157
+ }
158
+ }
159
+
160
+ private async _exec(sql:string):Promise<OINOMsSqlData> {
161
+ try {
162
+ const request = this._pool.request() // this does not need to be released but the pool will handle it
163
+ const sql_res = await request.query(sql)
164
+ // console.log("_exec: result=", sql_res.recordsets, sql_res.recordsets?.length) // TODO: remove
165
+ return new OINOMsSqlData(sql_res.recordsets, [])
166
+ } catch (e:any) {
167
+ OINOLog.exception("@oino-ts/db-mssql", "OINODbMsSql", "_exec", "exception in SQL exec", {message:e.message, stack:e.stack, sql:sql})
168
+ return new OINOMsSqlData(OINO_EMPTY_ROWS, []).setError(500, OINO_ERROR_PREFIX + ": Exception in db exec: " + e.message, "OINODbMsSql._exec") as OINOMsSqlData
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Print a table name using database specific SQL escaping.
174
+ *
175
+ * @param sqlTable name of the table
176
+ *
177
+ */
178
+ printTableName(sqlTable:string): string {
179
+ return "["+sqlTable+"]"
180
+ }
181
+
182
+ /**
183
+ * Print a column name with correct SQL escaping.
184
+ *
185
+ * @param sqlColumn name of the column
186
+ *
187
+ */
188
+ printColumnName(sqlColumn:string): string {
189
+ return "["+sqlColumn+"]"
190
+ }
191
+
192
+
193
+ /**
194
+ * Print a single data value from serialization using the context of the native data
195
+ * type with the correct SQL escaping.
196
+ *
197
+ * @param cellValue data from sql results
198
+ * @param nativeType native type name for table column
199
+ *
200
+ */
201
+ printCellAsValue(cellValue:OINODataCell, nativeType: string): string {
202
+ if (cellValue === null) {
203
+ return "NULL"
204
+
205
+ } else if (cellValue === undefined) {
206
+ return "UNDEFINED"
207
+
208
+ } else if ((nativeType == "int") || (nativeType == "smallint") || (nativeType == "float")) {
209
+ return cellValue.toString()
210
+
211
+ } else if ((nativeType == "longblob") || (nativeType == "binary") || (nativeType == "varbinary")) {
212
+ if (cellValue instanceof Buffer) {
213
+ return "0x" + (cellValue as Buffer).toString("hex") + ""
214
+ } else if (cellValue instanceof Uint8Array) {
215
+ return "0x" + Buffer.from(cellValue as Uint8Array).toString("hex") + ""
216
+ } else {
217
+ return "'" + cellValue?.toString() + "'"
218
+ }
219
+
220
+ } else if (((nativeType == "date") || (nativeType == "datetime") || (nativeType == "datetime2") || (nativeType == "timestamp")) && (cellValue instanceof Date)) {
221
+ return "'" + cellValue.toISOString().substring(0, 23) + "'"
222
+
223
+ } else {
224
+ return this.printStringValue(cellValue.toString())
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Print a single string value as valid sql literal
230
+ *
231
+ * @param sqlString string value
232
+ *
233
+ */
234
+ printStringValue(sqlString:string): string {
235
+ return "'" + sqlString.replaceAll("'", "''") + "'"
236
+ }
237
+
238
+ /**
239
+ * Parse a single SQL result value for serialization using the context of the native data
240
+ * type.
241
+ *
242
+ * @param sqlValue data from serialization
243
+ * @param nativeType native type name for table column
244
+ *
245
+ */
246
+ parseValueAsCell(sqlValue:OINODataCell, nativeType: string): OINODataCell {
247
+ if ((sqlValue === null) || (sqlValue == "NULL")) {
248
+ return null
249
+
250
+ } else if (sqlValue === undefined) {
251
+ return undefined
252
+
253
+ } else if (((nativeType == "date") || (nativeType == "datetime") || (nativeType == "datetime2")) && (typeof(sqlValue) == "string") && (sqlValue != "")) {
254
+ return new Date(sqlValue)
255
+
256
+ } else if (nativeType == "bit") {
257
+ return (sqlValue === 1) || (sqlValue === true) // sometimes boolean and sometimes number
258
+
259
+ } else {
260
+ return sqlValue
261
+ }
262
+
263
+ }
264
+
265
+ /**
266
+ * Print SQL select statement with DB specific formatting.
267
+ *
268
+ * @param tableName - The name of the table to select from.
269
+ * @param columnNames - The columns to be selected.
270
+ * @param whereCondition - The WHERE clause to filter the results.
271
+ * @param orderCondition - The ORDER BY clause to sort the results.
272
+ * @param limitCondition - The LIMIT clause to limit the number of results.
273
+ * @param groupByCondition - The GROUP BY clause to group the results.
274
+ *
275
+ */
276
+ printSqlSelect(tableName:string, columnNames:string, whereCondition:string, orderCondition:string, limitCondition:string, groupByCondition: string): string {
277
+ const limit_parts = limitCondition.split(" OFFSET ")
278
+ let result:string = "SELECT "
279
+ if ((limitCondition != "") && (limit_parts.length == 1)) {
280
+ result += "TOP " + limit_parts[0] + " "
281
+ }
282
+ result += columnNames + " FROM " + tableName
283
+ if (whereCondition != "") {
284
+ result += " WHERE " + whereCondition
285
+ }
286
+ if (groupByCondition != "") {
287
+ result += " GROUP BY " + groupByCondition
288
+ }
289
+ if (orderCondition != "") {
290
+ result += " ORDER BY " + orderCondition
291
+ }
292
+ if ((limitCondition != "") && (limit_parts.length == 2)) {
293
+ if (orderCondition == "") {
294
+ OINOLog.error("@oino-ts/db-mssql", "OINODbMsSql", "printSqlSelect", "LIMIT without ORDER BY is not supported in MS SQL Server")
295
+ throw new Error(OINO_ERROR_PREFIX + ": LIMIT without ORDER BY is not supported in MS SQL Server")
296
+ } else {
297
+ result += " OFFSET " + limit_parts[1] + " ROWS FETCH NEXT " + limit_parts[0] + " ROWS ONLY"
298
+ }
299
+ }
300
+ result += ";"
301
+ OINOLog.debug("@oino-ts/db-mssql", "OINODbMsSql", "printSqlSelect", "Result", {sql:result})
302
+ return result;
303
+ }
304
+
305
+ /**
306
+ * Print SQL select statement with DB specific formatting.
307
+ *
308
+ * @param tableName - The name of the table to select from.
309
+ * @param columns - The columns to be selected.
310
+ * @param values - The values to be inserted.
311
+ * @param returnIdFields - the id fields to return if returnIds is true (if supported by the database)
312
+ *
313
+ */
314
+ printSqlInsert(tableName:string, columns:string, values:string, returnIdFields?:string[]): string {
315
+ let result = "INSERT INTO " + tableName + " (" + columns + ")"
316
+ if (returnIdFields) {
317
+ result += " OUTPUT " + returnIdFields.map(f => "INSERTED."+f ).join(", ")
318
+ }
319
+ result += " VALUES (" + values + ");"
320
+ return result;
321
+ }
322
+
323
+
324
+ /**
325
+ * Connect to database.
326
+ *
327
+ */
328
+ async connect(): Promise<OINOResult> {
329
+ let result:OINOResult = new OINOResult()
330
+ if (this.isConnected) {
331
+ return result
332
+ }
333
+ try {
334
+ // make sure that any items are correctly URL encoded in the connection string
335
+ await this._pool.connect()
336
+ this.isConnected = true
337
+
338
+ } catch (e:any) {
339
+ // ... error checks
340
+ result.setError(500, "Exception connecting to database: " + e.message, "OINODbMsSql.connect")
341
+ OINOLog.exception("@oino-ts/db-mssql", "OINODbMsSql", "connect", "exception in connect", {message:e.message, stack:e.stack})
342
+ }
343
+ return Promise.resolve(result)
344
+ }
345
+
346
+ /**
347
+ * Validate connection to database is working.
348
+ *
349
+ */
350
+ async validate(): Promise<OINOResult> {
351
+ let result:OINOResult = new OINOResult()
352
+ if (!this.isConnected) {
353
+ result.setError(400, "Database is not connected!", "OINODbMsSql.validate")
354
+ return result
355
+ }
356
+ OINOBenchmark.startMetric("OINODb", "validate")
357
+ try {
358
+ const sql = this._getValidateSql(this.dbParams.database)
359
+ const sql_res:OINODataSet = await this._query(sql)
360
+ if (sql_res.isEmpty()) {
361
+ result.setError(400, "DB returned no rows for select!", "OINODbMsSql.validate")
362
+
363
+ } else if (sql_res.getRow().length == 0) {
364
+ result.setError(400, "DB returned no values for database!", "OINODbMsSql.validate")
365
+
366
+ } else if (sql_res.getRow()[0] == "0") {
367
+ result.setError(400, "DB returned no schema for database!", "OINODbMsSql.validate")
368
+
369
+ } else {
370
+ // connection is working
371
+ this.isValidated = true
372
+ }
373
+ } catch (e:any) {
374
+ result.setError(500, "Exception in validating connection: " + e.message, "OINODbMsSql.validate")
375
+ OINOLog.exception("@oino-ts/db-mssql", "OINODbMsSql", "validate", "exception in validate", {message:e.message, stack:e.stack})
376
+ }
377
+ OINOBenchmark.endMetric("OINODb", "validate", result.status != 500)
378
+ return result
379
+ }
380
+
381
+ /**
382
+ * Disconnect from database.
383
+ *
384
+ */
385
+ async disconnect(): Promise<void> {
386
+ if (this._pool) {
387
+ try {
388
+ await this._pool.close()
389
+
390
+ } catch (e:any) {
391
+ OINOLog.exception("@oino-ts/db-mssql", "OINODbMsSql", "disconnect", "exception in disconnect", {message:e.message, stack:e.stack})
392
+ }
393
+ }
394
+ this.isConnected = false
395
+ this.isValidated = false
396
+ }
397
+
398
+ /**
399
+ * Execute a select operation.
400
+ *
401
+ * @param sql SQL statement.
402
+ *
403
+ */
404
+ async sqlSelect(sql:string): Promise<OINODataSet> {
405
+ if (!this.isValidated) {
406
+ throw new Error(OINO_ERROR_PREFIX + ": Database connection not validated!")
407
+ }
408
+ OINOBenchmark.startMetric("OINODb", "sqlSelect")
409
+ let result:OINODataSet = await this._query(sql)
410
+ OINOBenchmark.endMetric("OINODb", "sqlSelect", result.status != 500)
411
+ return result
412
+ }
413
+
414
+ /**
415
+ * Execute other sql operations.
416
+ *
417
+ * @param sql SQL statement.
418
+ *
419
+ */
420
+ async sqlExec(sql:string): Promise<OINODataSet> {
421
+ if (!this.isValidated) {
422
+ throw new Error(OINO_ERROR_PREFIX + ": Database connection not validated!")
423
+ }
424
+ OINOBenchmark.startMetric("OINODb", "sqlExec")
425
+ let result:OINODataSet = await this._exec(sql)
426
+ OINOBenchmark.endMetric("OINODb", "sqlExec", result.status != 500)
427
+ return result
428
+ }
429
+
430
+ private _getSchemaSql(dbName:string, tableName:string):string {
431
+ const sql =
432
+ `SELECT
433
+ C.COLUMN_NAME,
434
+ C.IS_NULLABLE,
435
+ C.DATA_TYPE,
436
+ C.CHARACTER_MAXIMUM_LENGTH,
437
+ C.NUMERIC_PRECISION,
438
+ C.NUMERIC_PRECISION_RADIX,
439
+ CONST.CONSTRAINT_TYPES,
440
+ COLUMNPROPERTY(OBJECT_ID(C.TABLE_SCHEMA + '.' + C.TABLE_NAME), C.COLUMN_NAME, 'IsIdentity') AS IS_AUTO_INCREMENT,
441
+ COLUMNPROPERTY(OBJECT_ID(C.TABLE_SCHEMA + '.' + C.TABLE_NAME), C.COLUMN_NAME, 'IsComputed') AS IS_COMPUTED
442
+ FROM
443
+ INFORMATION_SCHEMA.COLUMNS as C LEFT JOIN
444
+ (
445
+ SELECT TC.TABLE_NAME, KU.COLUMN_NAME, STRING_AGG(TC.CONSTRAINT_TYPE, ',') as CONSTRAINT_TYPES
446
+ FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS TC
447
+ INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KU ON TC.CONSTRAINT_NAME = KU.CONSTRAINT_NAME
448
+ GROUP BY TC.TABLE_NAME, KU.COLUMN_NAME
449
+ ) as CONST
450
+ ON C.TABLE_NAME = CONST.TABLE_NAME AND C.COLUMN_NAME = CONST.COLUMN_NAME
451
+ WHERE C.TABLE_CATALOG = '${dbName}' AND C.TABLE_NAME = '${tableName}'
452
+ ORDER BY C.ORDINAL_POSITION;`
453
+ return sql
454
+ }
455
+
456
+ private _getValidateSql(dbName:string):string {
457
+ const sql =
458
+ `SELECT
459
+ count(C.COLUMN_NAME) AS COLUMN_COUNT
460
+ FROM
461
+ INFORMATION_SCHEMA.COLUMNS as C LEFT JOIN
462
+ (
463
+ SELECT TC.TABLE_NAME, KU.COLUMN_NAME, STRING_AGG(TC.CONSTRAINT_TYPE, ',') as CONSTRAINT_TYPES
464
+ FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS TC
465
+ INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KU ON TC.CONSTRAINT_NAME = KU.CONSTRAINT_NAME
466
+ GROUP BY TC.TABLE_NAME, KU.COLUMN_NAME
467
+ ) as CONST
468
+ ON C.TABLE_NAME = CONST.TABLE_NAME AND C.COLUMN_NAME = CONST.COLUMN_NAME
469
+ WHERE C.TABLE_CATALOG = '${dbName}';`
470
+ return sql
471
+ }
472
+
473
+ /**
474
+ * Initialize a data model by getting the SQL schema and populating OINODataFields of
475
+ * the model.
476
+ *
477
+ * @param api api which data model to initialize.
478
+ *
479
+ */
480
+ async initializeApiDatamodel(api:OINODbApi): Promise<void> {
481
+ api.initializeDatamodel(new OINODbDataModel(api))
482
+ //"SELECT COLUMN_NAME, IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_PRECISION_RADIX
483
+ const schema_res:OINODataSet = await this.sqlSelect(this._getSchemaSql(this.dbParams.database, api.params.tableName))
484
+ while (!schema_res.isEof()) {
485
+ const row:OINODataRow = schema_res.getRow()
486
+ const field_name:string = row[0]?.toString() || ""
487
+ const sql_type:string = row[2] as string || ""
488
+ const char_field_length:number = row[3] as number || 0
489
+ const numeric_field_length1:number = row[4] as number || 0
490
+ const numeric_field_length2:number = row[5] as number || 0
491
+ const constraint_types:string = row[6] as string || ""
492
+ const field_params:OINODataFieldParams = {
493
+ isPrimaryKey: constraint_types.indexOf("PRIMARY KEY") >= 0,
494
+ isForeignKey: constraint_types.indexOf("FOREIGN KEY") >= 0,
495
+ isAutoInc: row[7] == 1,
496
+ isNotNull: row[1] == "NO"
497
+ }
498
+ if (api.isFieldIncluded(field_name) == false) {
499
+ OINOLog.info("@oino-ts/db-mssql", "OINODbMsSql", "initializeApiDatamodel", "Field excluded in API parameters.", {field:field_name})
500
+ if (field_params.isPrimaryKey) {
501
+ throw new Error(OINO_ERROR_PREFIX + "Primary key field excluded in API parameters: " + field_name)
502
+ }
503
+
504
+ } else {
505
+ if ((sql_type == "tinyint") || (sql_type == "smallint") || (sql_type == "int") || (sql_type == "bigint") || (sql_type == "float") || (sql_type == "real")) {
506
+ api.datamodel!.addField(new OINONumberDataField(this, field_name, sql_type, field_params ))
507
+
508
+ } else if ((sql_type == "date") || (sql_type == "datetime") || (sql_type == "datetime2")) {
509
+ if (api.params.useDatesAsString) {
510
+ api.datamodel!.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
511
+ } else {
512
+ api.datamodel!.addField(new OINODatetimeDataField(this, field_name, sql_type, field_params))
513
+ }
514
+
515
+ } else if ((sql_type == "ntext") || (sql_type == "nchar") || (sql_type == "nvarchar") || (sql_type == "text") || (sql_type == "char") || (sql_type == "varchar")) {
516
+ api.datamodel!.addField(new OINOStringDataField(this, field_name, sql_type, field_params, char_field_length))
517
+
518
+ } else if ((sql_type == "binary") || (sql_type == "varbinary") || (sql_type == "image")) {
519
+ api.datamodel!.addField(new OINOBlobDataField(this, field_name, sql_type, field_params, char_field_length))
520
+
521
+ } else if ((sql_type == "numeric") || (sql_type == "decimal") || (sql_type == "money")) {
522
+ api.datamodel!.addField(new OINOStringDataField(this, field_name, sql_type, field_params, numeric_field_length1 + numeric_field_length2 + 1))
523
+
524
+ } else if ((sql_type == "bit")) {
525
+ api.datamodel!.addField(new OINOBooleanDataField(this, field_name, sql_type, field_params))
526
+
527
+ } else {
528
+ OINOLog.info("@oino-ts/db-mssql", "OINODbMsSql", "initializeApiDatamodel", "Unrecognized field type treated as string", {field_name: field_name, sql_type:sql_type, char_length: char_field_length, numeric_field_length1:numeric_field_length1, numeric_field_length2:numeric_field_length2, field_params:field_params })
529
+ api.datamodel!.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
530
+ }
531
+ }
532
+ await schema_res.next()
533
+ }
534
+ OINOLog.info("@oino-ts/db-mssql", "OINODbMsSql", "initializeApiDatamodel", "\n" + api.datamodel!.printDebug("\n"))
535
+ return Promise.resolve()
536
+ }
537
+
538
+ }
539
+
540
+
541
+