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