@oino-ts/db-postgresql 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,491 +1,492 @@
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, 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 { Pool, PoolClient, QueryResult } from "pg";
11
-
12
-
13
- /**
14
- * Implmentation of OINODbDataSet for Postgresql.
15
- *
16
- */
17
- class OINOPostgresqlData extends OINODbDataSet {
18
- private _rows:OINODataRow[]
19
-
20
- /**
21
- * OINOPostgresqlData constructor
22
- * @param params database parameters
23
- */
24
- constructor(data: unknown, messages:string[]=[]) {
25
- super(data, messages)
26
-
27
- if ((data != null) && !(Array.isArray(data))) {
28
- throw new Error(OINO_ERROR_PREFIX + ": Invalid Posgresql data type!") // TODO: maybe check all rows
29
- }
30
- this._rows = data as OINODataRow[]
31
- if (this.isEmpty()) {
32
- this._currentRow = -1
33
- this._eof = true
34
- } else {
35
- this._currentRow = 0
36
- this._eof = false
37
- }
38
- }
39
- private _currentRow: number
40
- private _eof: boolean
41
-
42
- /**
43
- * Is data set empty.
44
- *
45
- */
46
- isEmpty():boolean {
47
- return (this._rows.length == 0)
48
- }
49
-
50
- /**
51
- * Is there no more content, i.e. either dataset is empty or we have moved beyond last line
52
- *
53
- */
54
- isEof():boolean {
55
- return (this._eof)
56
- }
57
-
58
- /**
59
- * Attempts to moves dataset to the next row, possibly waiting for more data to become available. Returns !isEof().
60
- *
61
- */
62
- async next():Promise<boolean> {
63
- if (this._currentRow < this._rows.length-1) {
64
- this._currentRow = this._currentRow + 1
65
- } else {
66
- this._eof = true
67
- }
68
- return Promise.resolve(!this._eof)
69
- }
70
-
71
- /**
72
- * Gets current row of data.
73
- *
74
- */
75
- getRow(): OINODataRow {
76
- if ((this._currentRow >=0) && (this._currentRow < this._rows.length)) {
77
- return this._rows[this._currentRow]
78
- } else {
79
- return OINODB_EMPTY_ROW
80
- }
81
- }
82
-
83
- /**
84
- * Gets all rows of data.
85
- *
86
- */
87
- async getAllRows(): Promise<OINODataRow[]> {
88
- return this._rows // at the moment theres no result streaming, so we can just return the rows
89
- }
90
- }
91
-
92
- /**
93
- * Implementation of Postgresql-database.
94
- *
95
- */
96
- export class OINODbPostgresql extends OINODb {
97
-
98
- private _pool:Pool
99
-
100
- /**
101
- * Constructor of `OINODbPostgresql`
102
- * @param params database paraneters
103
- */
104
- constructor(params:OINODbParams) {
105
- super(params)
106
-
107
- if (this._params.type !== "OINODbPostgresql") {
108
- throw new Error(OINO_ERROR_PREFIX + ": Not OINODbPostgresql-type: " + this._params.type)
109
- }
110
- const ssl_enabled:boolean = !(this._params.url == "localhost" || this._params.url == "127.0.0.1")
111
- this._pool = new Pool({ host: this._params.url, database: this._params.database, port: this._params.port, user: this._params.user, password: this._params.password, ssl: ssl_enabled })
112
- delete this._params.password
113
-
114
- this._pool.on("error", (err: any) => {
115
- OINOLog.error("@oino-ts/db-postgresql", "OINODbPostgresql", ".on(error)", "Error-event", {err:err})
116
- })
117
- }
118
-
119
- private _parseFieldLength(fieldLength:OINODataCell):number {
120
- let result:number = parseInt((fieldLength || "0").toString())
121
- if (Number.isNaN(result)) {
122
- result = 0
123
- }
124
- return result
125
- }
126
-
127
- private async _query(sql:string):Promise<OINODbDataSet> {
128
- let connection:PoolClient|null = null
129
- try {
130
- connection = await this._pool.connect()
131
- const query_result = await connection.query({rowMode: "array", text: sql})
132
- let rows:OINODataRow[]
133
- if (Array.isArray(query_result) == true) {
134
- rows = query_result.flatMap((q) => q.rows)
135
- } else if (query_result.rows) {
136
- rows = query_result.rows
137
- } else {
138
- rows = OINODB_EMPTY_ROWS // return empty row if no rows returned
139
- }
140
- return new OINOPostgresqlData(rows, [])
141
- } catch (e:any) {
142
- return new OINOPostgresqlData(OINODB_EMPTY_ROWS, []).setError(500, OINO_ERROR_PREFIX + ": Exception in db query: " + e.message, "OINODbPostgresql._query") as OINOPostgresqlData
143
- } finally {
144
- if (connection) {
145
- connection.release()
146
- }
147
- }
148
- }
149
-
150
- private async _exec(sql:string):Promise<OINODbDataSet> {
151
- let connection:PoolClient|null = null
152
- try {
153
- connection = await this._pool.connect()
154
- const query_result:QueryResult = await connection.query({rowMode: "array", text: sql})
155
- let rows:OINODataRow[]
156
- if (Array.isArray(query_result) == true) {
157
- rows = query_result.flatMap((q) => q.rows)
158
- } else if (query_result.rows) {
159
- rows = query_result.rows
160
- } else {
161
- rows = OINODB_EMPTY_ROWS // return empty row if no rows returned
162
- }
163
- // if (rows.length > 0) { console.log("OINODbPostgresql._exec: rows", rows) }
164
- return new OINOPostgresqlData(rows, [])
165
- } catch (e:any) {
166
- return new OINOPostgresqlData(OINODB_EMPTY_ROWS, []).setError(500, OINO_ERROR_PREFIX + ": Exception in db exec: " + e.message, "OINODbPostgresql._exec") as OINOPostgresqlData
167
- } finally {
168
- if (connection) {
169
- connection.release()
170
- }
171
- }
172
- }
173
-
174
- /**
175
- * Print a table name using database specific SQL escaping.
176
- *
177
- * @param sqlTable name of the table
178
- *
179
- */
180
- printSqlTablename(sqlTable:string): string {
181
- return "\""+sqlTable.toLowerCase()+"\""
182
- }
183
-
184
- /**
185
- * Print a column name with correct SQL escaping.
186
- *
187
- * @param sqlColumn name of the column
188
- *
189
- */
190
- printSqlColumnname(sqlColumn:string): string {
191
- return "\""+sqlColumn+"\""
192
- }
193
-
194
- /**
195
- * Print a single data value from serialization using the context of the native data
196
- * type with the correct SQL escaping.
197
- *
198
- * @param cellValue data from sql results
199
- * @param sqlType native type name for table column
200
- *
201
- */
202
- printCellAsSqlValue(cellValue:OINODataCell, sqlType: string): string {
203
- if (cellValue === null) {
204
- return "NULL"
205
-
206
- } else if (cellValue === undefined) {
207
- return "UNDEFINED"
208
-
209
- } else if ((sqlType == "integer") || (sqlType == "smallint") || (sqlType == "real")) {
210
- return cellValue.toString()
211
-
212
- } else if (sqlType == "bytea") {
213
- if (cellValue instanceof Buffer) {
214
- return "'\\x" + (cellValue as Buffer).toString("hex") + "'"
215
- } else if (cellValue instanceof Uint8Array) {
216
- return "'\\x" + Buffer.from(cellValue as Uint8Array).toString("hex") + "'"
217
- } else {
218
- return "\'" + cellValue?.toString() + "\'"
219
- }
220
-
221
- } else if (sqlType == "boolean") {
222
- if (cellValue == null || cellValue == "" || cellValue.toString().toLowerCase() == "false" || cellValue == "0") {
223
- return "false"
224
- } else {
225
- return "true"
226
- }
227
-
228
- } else if ((sqlType == "date") && (cellValue instanceof Date)) {
229
- return "\'" + cellValue.toISOString() + "\'"
230
-
231
- } else {
232
- return this.printSqlString(cellValue.toString())
233
- }
234
- }
235
-
236
- /**
237
- * Print a single string value as valid sql literal
238
- *
239
- * @param sqlString string value
240
- *
241
- */
242
- printSqlString(sqlString:string): string {
243
- return "\'" + sqlString.replaceAll("'", "''") + "\'"
244
- }
245
-
246
-
247
- /**
248
- * Parse a single SQL result value for serialization using the context of the native data
249
- * type.
250
- *
251
- * @param sqlValue data from serialization
252
- * @param sqlType native type name for table column
253
- *
254
- */
255
- parseSqlValueAsCell(sqlValue:OINODataCell, sqlType: string): OINODataCell {
256
- if ((sqlValue === null) || (sqlValue == "NULL")) {
257
- return null
258
-
259
- } else if (sqlValue === undefined) {
260
- return undefined
261
-
262
- } else if (((sqlType == "date")) && (typeof(sqlValue) == "string") && (sqlValue != "")) {
263
- return new Date(sqlValue)
264
-
265
- } else {
266
- return sqlValue
267
- }
268
-
269
- }
270
-
271
- /**
272
- * Connect to database.
273
- *
274
- */
275
- async connect(): Promise<OINOResult> {
276
- let result:OINOResult = new OINOResult()
277
- if (this.isConnected) {
278
- return result
279
- }
280
- let connection:PoolClient|null = null
281
- try {
282
- // make sure that any items are correctly URL encoded in the connection string
283
- connection = await this._pool.connect()
284
- this.isConnected = true
285
-
286
- } catch (e:any) {
287
- result.setError(500, "Exception connecting to database: " + e.message, "OINODbPostgresql.connect")
288
- OINOLog.exception("@oino-ts/db-postgresql", "OINODbPostgresql", "connect", "exception in connect", {message:e.message, stack:e.stack})
289
- } finally {
290
- if (connection) {
291
- connection.release()
292
- }
293
- }
294
-
295
- return result
296
- }
297
-
298
- /**
299
- * Validate connection to database is working.
300
- *
301
- */
302
- async validate(): Promise<OINOResult> {
303
- OINOBenchmark.startMetric("OINODb", "validate")
304
- let result:OINOResult = new OINOResult()
305
- try {
306
- const sql = this._getValidateSql(this._params.database)
307
- const sql_res:OINODbDataSet = await this._query(sql)
308
- if (sql_res.isEmpty()) {
309
- result.setError(400, "DB returned no rows for select!", "OINODbPostgresql.validate")
310
-
311
- } else if (sql_res.getRow().length == 0) {
312
- result.setError(400, "DB returned no values for database!", "OINODbPostgresql.validate")
313
-
314
- } else if (sql_res.getRow()[0] == "0") {
315
- result.setError(400, "DB returned no schema for database!", "OINODbPostgresql.validate")
316
-
317
- } else {
318
- this.isValidated = true
319
- }
320
- } catch (e:any) {
321
- result.setError(500, "Exception validating connection: " + e.message, "OINODbPostgresql.validate")
322
- OINOLog.exception("@oino-ts/db-postgresql", "OINODbPostgresql", "validate", "exception in validate", {message:e.message, stack:e.stack})
323
- }
324
- OINOBenchmark.endMetric("OINODb", "validate", result.status != 500)
325
- return result
326
- }
327
-
328
- /**
329
- * Disconnect from database.
330
- *
331
- */
332
- async disconnect(): Promise<void> {
333
- if (this.isConnected) {
334
- this._pool.end().catch((e:any) => {
335
- OINOLog.exception("@oino-ts/db-postgresql", "OINODbPostgresql", "disconnect", "exception in pool end", {message:e.message, stack:e.stack})
336
- })
337
- }
338
- this.isConnected = false
339
- this.isValidated = false
340
- }
341
-
342
-
343
- /**
344
- * Execute a select operation.
345
- *
346
- * @param sql SQL statement.
347
- *
348
- */
349
- async sqlSelect(sql:string): Promise<OINODbDataSet> {
350
- if (!this.isValidated) {
351
- throw new Error(OINO_ERROR_PREFIX + ": Database connection not validated!")
352
- }
353
- OINOBenchmark.startMetric("OINODb", "sqlSelect")
354
- let result:OINODbDataSet = await this._query(sql)
355
- OINOBenchmark.endMetric("OINODb", "sqlSelect", result.status != 500)
356
- return result
357
- }
358
-
359
- /**
360
- * Execute other sql operations.
361
- *
362
- * @param sql SQL statement.
363
- *
364
- */
365
- async sqlExec(sql:string): Promise<OINODbDataSet> {
366
- if (!this.isValidated) {
367
- throw new Error(OINO_ERROR_PREFIX + ": Database connection not validated!")
368
- }
369
- OINOBenchmark.startMetric("OINODb", "sqlExec")
370
- let result:OINODbDataSet = await this._exec(sql)
371
- OINOBenchmark.endMetric("OINODb", "sqlExec", result.status != 500)
372
- return result
373
- }
374
-
375
- private _getSchemaSql(dbName:string, tableName:string):string {
376
- const sql =
377
- `SELECT
378
- col.column_name,
379
- col.data_type,
380
- col.character_maximum_length,
381
- col.is_nullable,
382
- con.constraint_type,
383
- col.numeric_precision,
384
- col.numeric_scale,
385
- col.column_default
386
- FROM information_schema.columns col
387
- LEFT JOIN LATERAL
388
- (select kcu.column_name, STRING_AGG(tco.constraint_type,',') as constraint_type
389
- from
390
- information_schema.table_constraints tco,
391
- information_schema.key_column_usage kcu
392
- where
393
- kcu.constraint_name = tco.constraint_name
394
- and kcu.constraint_schema = tco.constraint_schema
395
- and tco.table_catalog = col.table_catalog
396
- and tco.table_name = col.table_name
397
- and (tco.constraint_type = 'PRIMARY KEY' OR tco.constraint_type = 'FOREIGN KEY')
398
- group by kcu.column_name) con on col.column_name = con.column_name
399
- WHERE col.table_catalog = '${dbName}' AND col.table_name = '${tableName}'`
400
- return sql
401
- }
402
-
403
- private _getValidateSql(dbName:string):string {
404
- const sql =
405
- `SELECT
406
- count(col.column_name) AS column_count
407
- FROM information_schema.columns col
408
- LEFT JOIN LATERAL
409
- (select kcu.column_name, STRING_AGG(tco.constraint_type,',') as constraint_type
410
- from
411
- information_schema.table_constraints tco,
412
- information_schema.key_column_usage kcu
413
- where
414
- kcu.constraint_name = tco.constraint_name
415
- and kcu.constraint_schema = tco.constraint_schema
416
- and tco.table_catalog = col.table_catalog
417
- and tco.table_name = col.table_name
418
- and (tco.constraint_type = 'PRIMARY KEY' OR tco.constraint_type = 'FOREIGN KEY')
419
- group by kcu.column_name) con on col.column_name = con.column_name
420
- WHERE col.table_catalog = '${dbName}'`
421
- return sql
422
- }
423
-
424
- /**
425
- * Initialize a data model by getting the SQL schema and populating OINODbDataFields of
426
- * the model.
427
- *
428
- * @param api api which data model to initialize.
429
- *
430
- */
431
- async initializeApiDatamodel(api:OINODbApi): Promise<void> {
432
-
433
- const schema_res:OINODbDataSet = await this._query(this._getSchemaSql(this._params.database, api.params.tableName.toLowerCase()))
434
- while (!schema_res.isEof()) {
435
- const row:OINODataRow = schema_res.getRow()
436
- const field_name:string = row[0]?.toString() || ""
437
- const sql_type:string = row[1]?.toString() || ""
438
- const field_length:number = this._parseFieldLength(row[2])
439
- const constraints = row[4]?.toString() || ""
440
- const numeric_precision:number = this._parseFieldLength(row[5])
441
- const numeric_scale:number = this._parseFieldLength(row[6])
442
- const default_val:string = row[7]?.toString() || ""
443
- const field_params:OINODbDataFieldParams = {
444
- isPrimaryKey: constraints.indexOf('PRIMARY KEY') >= 0 || false,
445
- isForeignKey: constraints.indexOf('FOREIGN KEY') >= 0 || false,
446
- isNotNull: row[3] == "NO",
447
- isAutoInc: default_val.startsWith("nextval(")
448
- }
449
- if (api.isFieldIncluded(field_name) == false) {
450
- OINOLog.info("@oino-ts/db-postgresql", "OINODbPostgresql", "initializeApiDatamodel", "Field excluded in API parameters.", {field:field_name})
451
- if (field_params.isPrimaryKey) {
452
- throw new Error(OINO_ERROR_PREFIX + "Primary key field excluded in API parameters: " + field_name)
453
- }
454
-
455
- } else {
456
- if ((sql_type == "integer") || (sql_type == "smallint") || (sql_type == "real")) {
457
- api.datamodel.addField(new OINONumberDataField(this, field_name, sql_type, field_params ))
458
-
459
- } else if ((sql_type == "date")) {
460
- if (api.params.useDatesAsString) {
461
- api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
462
- } else {
463
- api.datamodel.addField(new OINODatetimeDataField(this, field_name, sql_type, field_params))
464
- }
465
-
466
- } else if ((sql_type == "character") || (sql_type == "character varying") || (sql_type == "varchar") || (sql_type == "text")) {
467
- api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, field_length))
468
-
469
- } else if ((sql_type == "bytea")) {
470
- api.datamodel.addField(new OINOBlobDataField(this, field_name, sql_type, field_params, field_length))
471
-
472
- } else if ((sql_type == "boolean")) {
473
- api.datamodel.addField(new OINOBooleanDataField(this, field_name, sql_type, field_params))
474
-
475
- } else if ((sql_type == "decimal") || (sql_type == "numeric")) {
476
- api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, numeric_precision + numeric_scale + 1))
477
-
478
- } else {
479
- OINOLog.info("@oino-ts/db-postgresql", "OINODbPostgresql", "initializeApiDatamodel", "Unrecognized field type treated as string", {field_name: field_name, sql_type:sql_type, field_length:field_length, field_params:field_params })
480
- api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
481
- }
482
- }
483
- await schema_res.next()
484
- }
485
- OINOLog.info("@oino-ts/db-postgresql", "OINODbPostgresql", "initializeApiDatamodel", "\n" + api.datamodel.printDebug("\n"))
486
- return Promise.resolve()
487
- }
488
- }
489
-
490
-
491
-
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, 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 { Pool, PoolClient, QueryResult } from "pg";
12
+
13
+
14
+ /**
15
+ * Implmentation of OINODataSet for Postgresql.
16
+ *
17
+ */
18
+ class OINOPostgresqlData extends OINODataSet {
19
+ private _rows:OINODataRow[]
20
+
21
+ /**
22
+ * OINOPostgresqlData constructor
23
+ * @param params database parameters
24
+ */
25
+ constructor(data: unknown, messages:string[]=[]) {
26
+ super(data, messages)
27
+
28
+ if ((data != null) && !(Array.isArray(data))) {
29
+ throw new Error(OINO_ERROR_PREFIX + ": Invalid Posgresql data type!") // TODO: maybe check all rows
30
+ }
31
+ this._rows = data as OINODataRow[]
32
+ if (this.isEmpty()) {
33
+ this._currentRow = -1
34
+ this._eof = true
35
+ } else {
36
+ this._currentRow = 0
37
+ this._eof = false
38
+ }
39
+ }
40
+ private _currentRow: number
41
+ private _eof: boolean
42
+
43
+ /**
44
+ * Is data set empty.
45
+ *
46
+ */
47
+ isEmpty():boolean {
48
+ return (this._rows.length == 0)
49
+ }
50
+
51
+ /**
52
+ * Is there no more content, i.e. either dataset is empty or we have moved beyond last line
53
+ *
54
+ */
55
+ isEof():boolean {
56
+ return (this._eof)
57
+ }
58
+
59
+ /**
60
+ * Attempts to moves dataset to the next row, possibly waiting for more data to become available. Returns !isEof().
61
+ *
62
+ */
63
+ async next():Promise<boolean> {
64
+ if (this._currentRow < this._rows.length-1) {
65
+ this._currentRow = this._currentRow + 1
66
+ } else {
67
+ this._eof = true
68
+ }
69
+ return Promise.resolve(!this._eof)
70
+ }
71
+
72
+ /**
73
+ * Gets current row of data.
74
+ *
75
+ */
76
+ getRow(): OINODataRow {
77
+ if ((this._currentRow >=0) && (this._currentRow < this._rows.length)) {
78
+ return this._rows[this._currentRow]
79
+ } else {
80
+ return OINO_EMPTY_ROW
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Gets all rows of data.
86
+ *
87
+ */
88
+ async getAllRows(): Promise<OINODataRow[]> {
89
+ return this._rows // at the moment theres no result streaming, so we can just return the rows
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Implementation of Postgresql-database.
95
+ *
96
+ */
97
+ export class OINODbPostgresql extends OINODb {
98
+
99
+ private _pool:Pool
100
+
101
+ /**
102
+ * Constructor of `OINODbPostgresql`
103
+ * @param params database paraneters
104
+ */
105
+ constructor(params:OINODbParams) {
106
+ super(params)
107
+
108
+ if (this.dbParams.type !== "OINODbPostgresql") {
109
+ throw new Error(OINO_ERROR_PREFIX + ": Not OINODbPostgresql-type: " + this.dbParams.type)
110
+ }
111
+ const ssl_enabled:boolean = !(this.dbParams.url == "localhost" || this.dbParams.url == "127.0.0.1")
112
+ this._pool = new Pool({ host: this.dbParams.url, database: this.dbParams.database, port: this.dbParams.port, user: this.dbParams.user, password: this.dbParams.password, ssl: ssl_enabled })
113
+ delete this.dbParams.password
114
+
115
+ this._pool.on("error", (err: any) => {
116
+ OINOLog.error("@oino-ts/db-postgresql", "OINODbPostgresql", ".on(error)", "Error-event", {err:err})
117
+ })
118
+ }
119
+
120
+ private _parseFieldLength(fieldLength:OINODataCell):number {
121
+ let result:number = parseInt((fieldLength || "0").toString())
122
+ if (Number.isNaN(result)) {
123
+ result = 0
124
+ }
125
+ return result
126
+ }
127
+
128
+ private async _query(sql:string):Promise<OINODataSet> {
129
+ let connection:PoolClient|null = null
130
+ try {
131
+ connection = await this._pool.connect()
132
+ const query_result = await connection.query({rowMode: "array", text: sql})
133
+ let rows:OINODataRow[]
134
+ if (Array.isArray(query_result) == true) {
135
+ rows = query_result.flatMap((q) => q.rows)
136
+ } else if (query_result.rows) {
137
+ rows = query_result.rows
138
+ } else {
139
+ rows = OINO_EMPTY_ROWS // return empty row if no rows returned
140
+ }
141
+ return new OINOPostgresqlData(rows, [])
142
+ } catch (e:any) {
143
+ return new OINOPostgresqlData(OINO_EMPTY_ROWS, []).setError(500, OINO_ERROR_PREFIX + ": Exception in db query: " + e.message, "OINODbPostgresql._query") as OINOPostgresqlData
144
+ } finally {
145
+ if (connection) {
146
+ connection.release()
147
+ }
148
+ }
149
+ }
150
+
151
+ private async _exec(sql:string):Promise<OINODataSet> {
152
+ let connection:PoolClient|null = null
153
+ try {
154
+ connection = await this._pool.connect()
155
+ const query_result:QueryResult = await connection.query({rowMode: "array", text: sql})
156
+ let rows:OINODataRow[]
157
+ if (Array.isArray(query_result) == true) {
158
+ rows = query_result.flatMap((q) => q.rows)
159
+ } else if (query_result.rows) {
160
+ rows = query_result.rows
161
+ } else {
162
+ rows = OINO_EMPTY_ROWS // return empty row if no rows returned
163
+ }
164
+ // if (rows.length > 0) { console.log("OINODbPostgresql._exec: rows", rows) }
165
+ return new OINOPostgresqlData(rows, [])
166
+ } catch (e:any) {
167
+ return new OINOPostgresqlData(OINO_EMPTY_ROWS, []).setError(500, OINO_ERROR_PREFIX + ": Exception in db exec: " + e.message, "OINODbPostgresql._exec") as OINOPostgresqlData
168
+ } finally {
169
+ if (connection) {
170
+ connection.release()
171
+ }
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Print a table name using database specific SQL escaping.
177
+ *
178
+ * @param sqlTable name of the table
179
+ *
180
+ */
181
+ printTableName(sqlTable:string): string {
182
+ return "\""+sqlTable.toLowerCase()+"\""
183
+ }
184
+
185
+ /**
186
+ * Print a column name with correct SQL escaping.
187
+ *
188
+ * @param sqlColumn name of the column
189
+ *
190
+ */
191
+ printColumnName(sqlColumn:string): string {
192
+ return "\""+sqlColumn+"\""
193
+ }
194
+
195
+ /**
196
+ * Print a single data value from serialization using the context of the native data
197
+ * type with the correct SQL escaping.
198
+ *
199
+ * @param cellValue data from sql results
200
+ * @param nativeType native type name for table column
201
+ *
202
+ */
203
+ printCellAsValue(cellValue:OINODataCell, nativeType: string): string {
204
+ if (cellValue === null) {
205
+ return "NULL"
206
+
207
+ } else if (cellValue === undefined) {
208
+ return "UNDEFINED"
209
+
210
+ } else if ((nativeType == "integer") || (nativeType == "smallint") || (nativeType == "real")) {
211
+ return cellValue.toString()
212
+
213
+ } else if (nativeType == "bytea") {
214
+ if (cellValue instanceof Buffer) {
215
+ return "'\\x" + (cellValue as Buffer).toString("hex") + "'"
216
+ } else if (cellValue instanceof Uint8Array) {
217
+ return "'\\x" + Buffer.from(cellValue as Uint8Array).toString("hex") + "'"
218
+ } else {
219
+ return "\'" + cellValue?.toString() + "\'"
220
+ }
221
+
222
+ } else if (nativeType == "boolean") {
223
+ if (cellValue == null || cellValue == "" || cellValue.toString().toLowerCase() == "false" || cellValue == "0") {
224
+ return "false"
225
+ } else {
226
+ return "true"
227
+ }
228
+
229
+ } else if ((nativeType == "date") && (cellValue instanceof Date)) {
230
+ return "\'" + cellValue.toISOString() + "\'"
231
+
232
+ } else {
233
+ return this.printStringValue(cellValue.toString())
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Print a single string value as valid sql literal
239
+ *
240
+ * @param sqlString string value
241
+ *
242
+ */
243
+ printStringValue(sqlString:string): string {
244
+ return "\'" + sqlString.replaceAll("'", "''") + "\'"
245
+ }
246
+
247
+
248
+ /**
249
+ * Parse a single SQL result value for serialization using the context of the native data
250
+ * type.
251
+ *
252
+ * @param sqlValue data from serialization
253
+ * @param nativeType native type name for table column
254
+ *
255
+ */
256
+ parseValueAsCell(sqlValue:OINODataCell, nativeType: string): OINODataCell {
257
+ if ((sqlValue === null) || (sqlValue == "NULL")) {
258
+ return null
259
+
260
+ } else if (sqlValue === undefined) {
261
+ return undefined
262
+
263
+ } else if (((nativeType == "date")) && (typeof(sqlValue) == "string") && (sqlValue != "")) {
264
+ return new Date(sqlValue)
265
+
266
+ } else {
267
+ return sqlValue
268
+ }
269
+
270
+ }
271
+
272
+ /**
273
+ * Connect to database.
274
+ *
275
+ */
276
+ async connect(): Promise<OINOResult> {
277
+ let result:OINOResult = new OINOResult()
278
+ if (this.isConnected) {
279
+ return result
280
+ }
281
+ let connection:PoolClient|null = null
282
+ try {
283
+ // make sure that any items are correctly URL encoded in the connection string
284
+ connection = await this._pool.connect()
285
+ this.isConnected = true
286
+
287
+ } catch (e:any) {
288
+ result.setError(500, "Exception connecting to database: " + e.message, "OINODbPostgresql.connect")
289
+ OINOLog.exception("@oino-ts/db-postgresql", "OINODbPostgresql", "connect", "exception in connect", {message:e.message, stack:e.stack})
290
+ } finally {
291
+ if (connection) {
292
+ connection.release()
293
+ }
294
+ }
295
+
296
+ return result
297
+ }
298
+
299
+ /**
300
+ * Validate connection to database is working.
301
+ *
302
+ */
303
+ async validate(): Promise<OINOResult> {
304
+ OINOBenchmark.startMetric("OINODb", "validate")
305
+ let result:OINOResult = new OINOResult()
306
+ try {
307
+ const sql = this._getValidateSql(this.dbParams.database)
308
+ const sql_res:OINODataSet = await this._query(sql)
309
+ if (sql_res.isEmpty()) {
310
+ result.setError(400, "DB returned no rows for select!", "OINODbPostgresql.validate")
311
+
312
+ } else if (sql_res.getRow().length == 0) {
313
+ result.setError(400, "DB returned no values for database!", "OINODbPostgresql.validate")
314
+
315
+ } else if (sql_res.getRow()[0] == "0") {
316
+ result.setError(400, "DB returned no schema for database!", "OINODbPostgresql.validate")
317
+
318
+ } else {
319
+ this.isValidated = true
320
+ }
321
+ } catch (e:any) {
322
+ result.setError(500, "Exception validating connection: " + e.message, "OINODbPostgresql.validate")
323
+ OINOLog.exception("@oino-ts/db-postgresql", "OINODbPostgresql", "validate", "exception in validate", {message:e.message, stack:e.stack})
324
+ }
325
+ OINOBenchmark.endMetric("OINODb", "validate", result.status != 500)
326
+ return result
327
+ }
328
+
329
+ /**
330
+ * Disconnect from database.
331
+ *
332
+ */
333
+ async disconnect(): Promise<void> {
334
+ if (this.isConnected) {
335
+ this._pool.end().catch((e:any) => {
336
+ OINOLog.exception("@oino-ts/db-postgresql", "OINODbPostgresql", "disconnect", "exception in pool end", {message:e.message, stack:e.stack})
337
+ })
338
+ }
339
+ this.isConnected = false
340
+ this.isValidated = false
341
+ }
342
+
343
+
344
+ /**
345
+ * Execute a select operation.
346
+ *
347
+ * @param sql SQL statement.
348
+ *
349
+ */
350
+ async sqlSelect(sql:string): Promise<OINODataSet> {
351
+ if (!this.isValidated) {
352
+ throw new Error(OINO_ERROR_PREFIX + ": Database connection not validated!")
353
+ }
354
+ OINOBenchmark.startMetric("OINODb", "sqlSelect")
355
+ let result:OINODataSet = await this._query(sql)
356
+ OINOBenchmark.endMetric("OINODb", "sqlSelect", result.status != 500)
357
+ return result
358
+ }
359
+
360
+ /**
361
+ * Execute other sql operations.
362
+ *
363
+ * @param sql SQL statement.
364
+ *
365
+ */
366
+ async sqlExec(sql:string): Promise<OINODataSet> {
367
+ if (!this.isValidated) {
368
+ throw new Error(OINO_ERROR_PREFIX + ": Database connection not validated!")
369
+ }
370
+ OINOBenchmark.startMetric("OINODb", "sqlExec")
371
+ let result:OINODataSet = await this._exec(sql)
372
+ OINOBenchmark.endMetric("OINODb", "sqlExec", result.status != 500)
373
+ return result
374
+ }
375
+
376
+ private _getSchemaSql(dbName:string, tableName:string):string {
377
+ const sql =
378
+ `SELECT
379
+ col.column_name,
380
+ col.data_type,
381
+ col.character_maximum_length,
382
+ col.is_nullable,
383
+ con.constraint_type,
384
+ col.numeric_precision,
385
+ col.numeric_scale,
386
+ col.column_default
387
+ FROM information_schema.columns col
388
+ LEFT JOIN LATERAL
389
+ (select kcu.column_name, STRING_AGG(tco.constraint_type,',') as constraint_type
390
+ from
391
+ information_schema.table_constraints tco,
392
+ information_schema.key_column_usage kcu
393
+ where
394
+ kcu.constraint_name = tco.constraint_name
395
+ and kcu.constraint_schema = tco.constraint_schema
396
+ and tco.table_catalog = col.table_catalog
397
+ and tco.table_name = col.table_name
398
+ and (tco.constraint_type = 'PRIMARY KEY' OR tco.constraint_type = 'FOREIGN KEY')
399
+ group by kcu.column_name) con on col.column_name = con.column_name
400
+ WHERE col.table_catalog = '${dbName}' AND col.table_name = '${tableName}'`
401
+ return sql
402
+ }
403
+
404
+ private _getValidateSql(dbName:string):string {
405
+ const sql =
406
+ `SELECT
407
+ count(col.column_name) AS column_count
408
+ FROM information_schema.columns col
409
+ LEFT JOIN LATERAL
410
+ (select kcu.column_name, STRING_AGG(tco.constraint_type,',') as constraint_type
411
+ from
412
+ information_schema.table_constraints tco,
413
+ information_schema.key_column_usage kcu
414
+ where
415
+ kcu.constraint_name = tco.constraint_name
416
+ and kcu.constraint_schema = tco.constraint_schema
417
+ and tco.table_catalog = col.table_catalog
418
+ and tco.table_name = col.table_name
419
+ and (tco.constraint_type = 'PRIMARY KEY' OR tco.constraint_type = 'FOREIGN KEY')
420
+ group by kcu.column_name) con on col.column_name = con.column_name
421
+ WHERE col.table_catalog = '${dbName}'`
422
+ return sql
423
+ }
424
+
425
+ /**
426
+ * Initialize a data model by getting the SQL schema and populating OINODataFields of
427
+ * the model.
428
+ *
429
+ * @param api api which data model to initialize.
430
+ *
431
+ */
432
+ async initializeApiDatamodel(api:OINODbApi): Promise<void> {
433
+ api.initializeDatamodel(new OINODbDataModel(api))
434
+ const schema_res:OINODataSet = await this._query(this._getSchemaSql(this.dbParams.database, api.params.tableName.toLowerCase()))
435
+ while (!schema_res.isEof()) {
436
+ const row:OINODataRow = schema_res.getRow()
437
+ const field_name:string = row[0]?.toString() || ""
438
+ const sql_type:string = row[1]?.toString() || ""
439
+ const field_length:number = this._parseFieldLength(row[2])
440
+ const constraints = row[4]?.toString() || ""
441
+ const numeric_precision:number = this._parseFieldLength(row[5])
442
+ const numeric_scale:number = this._parseFieldLength(row[6])
443
+ const default_val:string = row[7]?.toString() || ""
444
+ const field_params:OINODataFieldParams = {
445
+ isPrimaryKey: constraints.indexOf('PRIMARY KEY') >= 0 || false,
446
+ isForeignKey: constraints.indexOf('FOREIGN KEY') >= 0 || false,
447
+ isNotNull: row[3] == "NO",
448
+ isAutoInc: default_val.startsWith("nextval(")
449
+ }
450
+ if (api.isFieldIncluded(field_name) == false) {
451
+ OINOLog.info("@oino-ts/db-postgresql", "OINODbPostgresql", "initializeApiDatamodel", "Field excluded in API parameters.", {field:field_name})
452
+ if (field_params.isPrimaryKey) {
453
+ throw new Error(OINO_ERROR_PREFIX + "Primary key field excluded in API parameters: " + field_name)
454
+ }
455
+
456
+ } else {
457
+ if ((sql_type == "integer") || (sql_type == "smallint") || (sql_type == "real")) {
458
+ api.datamodel!.addField(new OINONumberDataField(this, field_name, sql_type, field_params ))
459
+
460
+ } else if ((sql_type == "date")) {
461
+ if (api.params.useDatesAsString) {
462
+ api.datamodel!.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
463
+ } else {
464
+ api.datamodel!.addField(new OINODatetimeDataField(this, field_name, sql_type, field_params))
465
+ }
466
+
467
+ } else if ((sql_type == "character") || (sql_type == "character varying") || (sql_type == "varchar") || (sql_type == "text")) {
468
+ api.datamodel!.addField(new OINOStringDataField(this, field_name, sql_type, field_params, field_length))
469
+
470
+ } else if ((sql_type == "bytea")) {
471
+ api.datamodel!.addField(new OINOBlobDataField(this, field_name, sql_type, field_params, field_length))
472
+
473
+ } else if ((sql_type == "boolean")) {
474
+ api.datamodel!.addField(new OINOBooleanDataField(this, field_name, sql_type, field_params))
475
+
476
+ } else if ((sql_type == "decimal") || (sql_type == "numeric")) {
477
+ api.datamodel!.addField(new OINOStringDataField(this, field_name, sql_type, field_params, numeric_precision + numeric_scale + 1))
478
+
479
+ } else {
480
+ OINOLog.info("@oino-ts/db-postgresql", "OINODbPostgresql", "initializeApiDatamodel", "Unrecognized field type treated as string", {field_name: field_name, sql_type:sql_type, field_length:field_length, field_params:field_params })
481
+ api.datamodel!.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
482
+ }
483
+ }
484
+ await schema_res.next()
485
+ }
486
+ OINOLog.info("@oino-ts/db-postgresql", "OINODbPostgresql", "initializeApiDatamodel", "\n" + api.datamodel!.printDebug("\n"))
487
+ return Promise.resolve()
488
+ }
489
+ }
490
+
491
+
492
+