@oino-ts/db-mariadb 0.0.11

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.
@@ -0,0 +1,380 @@
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 } from "@oino-ts/db";
8
+
9
+ import mariadb from "mariadb";
10
+
11
+ /**
12
+ * Implmentation of OINODbDataSet for MariaDb.
13
+ *
14
+ */
15
+ class OINOMariadbData extends OINODbDataSet {
16
+ private _rows:OINODataRow[] = OINODB_EMPTY_ROWS
17
+
18
+ /**
19
+ * OINOMariadbData constructor
20
+ * @param params database parameters
21
+ */
22
+ constructor(data: any, messages:string[]=[]) {
23
+ super(data, messages)
24
+
25
+ if (data == null) {
26
+ this.messages.push(OINO_INFO_PREFIX + "SQL result is empty")
27
+
28
+ } else if (Array.isArray(data)) {
29
+ this._rows = data as OINODataRow[]
30
+
31
+ }
32
+ // OINOLog.debug("OINOMariadbData.constructor", {_rows:this._rows})
33
+ if (this.isEmpty()) {
34
+ this._currentRow = -1
35
+ this._eof = true
36
+ } else {
37
+ this._currentRow = 0
38
+ this._eof = false
39
+ }
40
+ }
41
+ private _currentRow: number
42
+ private _eof: boolean
43
+
44
+ isEmpty():boolean {
45
+ return (this._rows.length == 0)
46
+ }
47
+
48
+ // EOF means "there is no more content", i.e. either dataset is empty or we have moved beyond last line
49
+ isEof():boolean {
50
+ return (this._eof)
51
+ }
52
+
53
+ next():boolean {
54
+ // OINOLog.debug("OINODbDataSet.next", {currentRow:this._currentRow, length:this.sqlResult.data.length})
55
+ if (this._currentRow < this._rows.length-1) {
56
+ this._currentRow = this._currentRow + 1
57
+ } else {
58
+ this._eof = true
59
+ }
60
+ return !this._eof
61
+ }
62
+
63
+ getRow(): OINODataRow {
64
+ if ((this._currentRow >=0) && (this._currentRow < this._rows.length)) {
65
+ return this._rows[this._currentRow]
66
+ } else {
67
+ return OINODB_EMPTY_ROW
68
+ }
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Implementation of MariaDb/MySql-database.
74
+ *
75
+ */
76
+ export class OINODbMariadb extends OINODb {
77
+
78
+ private static _fieldLengthRegex = /([^\(\)]+)(\s?\((\d+)\s?\,?\s?(\d*)?\))?/i
79
+ private static _exceptionMessageRegex = /\(([^\)]*)\) (.*)\nsql\:(.*)?/i
80
+ private static _tableSchemaSql:string = `SHOW COLUMNS from `
81
+
82
+ private _pool:mariadb.Pool
83
+
84
+ /**
85
+ * Constructor of `OINODbMariadb`
86
+ * @param params database parameters
87
+ */
88
+ constructor(params:OINODbParams) {
89
+ super(params)
90
+
91
+ // OINOLog.debug("OINODbMariadb.constructor", {params:params})
92
+ if (this._params.type !== "OINODbMariadb") {
93
+ throw new Error(OINO_ERROR_PREFIX + ": Not OINODbMariadb-type: " + this._params.type)
94
+ }
95
+ this._pool = mariadb.createPool({ host: params.url, database: params.database, port: params.port, user: params.user, password: params.password, acquireTimeout: 2000, debug:false, rowsAsArray: true })
96
+
97
+ // this._pool.on("acquire", (conn: mariadb.Connection) => {
98
+ // OINOLog.info("OINODbMariadb acquire", {conn:conn})
99
+ // })
100
+ // this._pool.on("connection", (conn: mariadb.Connection) => {
101
+ // OINOLog.info("OINODbMariadb connection", {conn:conn})
102
+ // })
103
+ // this._pool.on("release", (conn: mariadb.Connection) => {
104
+ // OINOLog.info("OINODbMariadb release", {conn:conn})
105
+ // })
106
+ // this._pool.on("enqueue", () => {
107
+ // OINOLog.info("OINODbMariadb enqueue", {})
108
+ // })
109
+ }
110
+
111
+ private _parseFieldLength(fieldLengthStr:string):number {
112
+ let result:number = parseInt(fieldLengthStr)
113
+ if (Number.isNaN(result)) {
114
+ result = 0
115
+ }
116
+ return result
117
+ }
118
+
119
+ private async _query(sql:string):Promise<OINODataRow[]> {
120
+ // OINOLog.debug("OINODbMariadb._query", {sql:sql})
121
+ let connection:mariadb.Connection|null = null
122
+ try {
123
+ connection = await this._pool.getConnection();
124
+ const result = await connection.query(sql);
125
+ // console.log("OINODbMariadb._query rows="+result)
126
+ return Promise.resolve(result)
127
+
128
+ } catch (err) {
129
+ // console.log("OINODbMariadb._query err=" + err);
130
+ throw err;
131
+ } finally {
132
+ if (connection) {
133
+ await connection.end()
134
+ }
135
+ }
136
+ // OINOLog.debug("OINODbMariadb._query", {result:query_result})
137
+ }
138
+
139
+ private async _exec(sql:string):Promise<any> {
140
+ // OINOLog.debug("OINODbMariadb._exec", {sql:sql})
141
+ let connection:mariadb.Connection|null = null
142
+ try {
143
+ connection = await this._pool.getConnection();
144
+ const result = await connection.query(sql);
145
+ // console.log(result);
146
+ return Promise.resolve(result)
147
+
148
+ } catch (err) {
149
+ const msg_parts = (err as Error).message.match(OINODbMariadb._exceptionMessageRegex) || []
150
+ // OINOLog.debug("OINODbMariadb._exec exception", {connection: msg_parts[1], message:msg_parts[2], sql:msg_parts[3]}) // print connection info just to log so tests don't break on runtime output
151
+ throw new Error(msg_parts[2]);
152
+ } finally {
153
+ if (connection) {
154
+ await connection.end()
155
+ }
156
+ }
157
+ // OINOLog.debug("OINODbMariadb._query", {result:query_result})
158
+ }
159
+
160
+ /**
161
+ * Print a table name using database specific SQL escaping.
162
+ *
163
+ * @param sqlTable name of the table
164
+ *
165
+ */
166
+ printSqlTablename(sqlTable:string): string {
167
+ return "`"+sqlTable+"`"
168
+ }
169
+
170
+ /**
171
+ * Print a column name with correct SQL escaping.
172
+ *
173
+ * @param sqlColumn name of the column
174
+ *
175
+ */
176
+ printSqlColumnname(sqlColumn:string): string {
177
+ return "`"+sqlColumn+"`"
178
+ }
179
+
180
+
181
+ /**
182
+ * Print a single data value from serialization using the context of the native data
183
+ * type with the correct SQL escaping.
184
+ *
185
+ * @param cellValue data from sql results
186
+ * @param sqlType native type name for table column
187
+ *
188
+ */
189
+ printCellAsSqlValue(cellValue:OINODataCell, sqlType: string): string {
190
+ // OINOLog.debug("OINODbMariadb.printCellAsSqlValue", {cellValue:cellValue, sqlType:sqlType})
191
+ if (cellValue === null) {
192
+ return "NULL"
193
+
194
+ } else if (cellValue === undefined) {
195
+ return "UNDEFINED"
196
+
197
+ } else if ((sqlType == "int") || (sqlType == "smallint") || (sqlType == "float")) {
198
+ return cellValue.toString()
199
+
200
+ } else if ((sqlType == "longblob") || (sqlType == "binary") || (sqlType == "varbinary")) {
201
+ return "\"" + cellValue + "\""
202
+
203
+ } else if (((sqlType == "date") || (sqlType == "datetime") || (sqlType == "timestamp")) && (cellValue instanceof Date)) {
204
+ return "\"" + cellValue.toISOString().replace('T', ' ').substring(0, 23) + "\""
205
+
206
+ } else if ((sqlType == "bit")) {
207
+ if ((cellValue === false) || (cellValue == null) || (cellValue == "") || (cellValue.toString().toLowerCase() == "false") || (cellValue == "0")) {
208
+ return "b'0'"
209
+ } else if ((cellValue === true) || (cellValue.toString().toLowerCase() == "true")) {
210
+ return "b'1'"
211
+ } else {
212
+ return "b'" + cellValue.toString() + "'" // rest is assumed to be a valid bitstring
213
+ }
214
+
215
+ } else {
216
+ return "\"" + cellValue?.toString().replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\r", "\\r").replaceAll("\n", "\\n").replaceAll("\t", "\\t") + "\""
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Parse a single SQL result value for serialization using the context of the native data
222
+ * type.
223
+ *
224
+ * @param sqlValue data from serialization
225
+ * @param sqlType native type name for table column
226
+ *
227
+ */
228
+ parseSqlValueAsCell(sqlValue:OINODataCell, sqlType: string): OINODataCell {
229
+ // OINOLog.debug("OINODbMariadb.parseSqlValueAsCell", {sqlValue:sqlValue, sqlType:sqlType})
230
+ if ((sqlValue === null) || (sqlValue == "NULL")) {
231
+ return null
232
+
233
+ } else if (sqlValue === undefined) {
234
+ return undefined
235
+
236
+ } else if (((sqlType == "date")) && (typeof(sqlValue) == "string")) {
237
+ return new Date(sqlValue)
238
+
239
+ } else if ((sqlType == "bit") && (sqlValue instanceof Buffer)) { // mariadb returns a buffer for bit-fields
240
+ const buf:Buffer = sqlValue as Buffer
241
+ let result:string = ""
242
+ for (let i=0; i<buf.length; i++) {
243
+ result += buf[i].toString(2).padStart(8, '0')
244
+ }
245
+ return result
246
+
247
+ } else {
248
+ return sqlValue
249
+ }
250
+
251
+ }
252
+
253
+ /**
254
+ * Connect to database.
255
+ *
256
+ */
257
+ async connect(): Promise<boolean> {
258
+ try {
259
+ // make sure that any items are correctly URL encoded in the connection string
260
+ // OINOLog.debug("OINODbMariadb.connect")
261
+ await this._pool.on
262
+ // await this._client.connect()
263
+ return Promise.resolve(true)
264
+ } catch (err) {
265
+ // ... error checks
266
+ throw new Error(OINO_ERROR_PREFIX + ": Error connecting to Postgresql server: " + err)
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Execute a select operation.
272
+ *
273
+ * @param sql SQL statement.
274
+ *
275
+ */
276
+ async sqlSelect(sql:string): Promise<OINODbDataSet> {
277
+ OINOBenchmark.start("sqlSelect")
278
+ let result:OINODbDataSet
279
+ try {
280
+ const sql_res:OINODataRow[] = await this._query(sql)
281
+ // OINOLog.debug("OINODbMariadb.sqlSelect", {sql_res:sql_res})
282
+ result = new OINOMariadbData(sql_res, [])
283
+
284
+ } catch (e:any) {
285
+ result = new OINOMariadbData([[]], [OINO_ERROR_PREFIX + " (sqlSelect): OINODbMariadb.sqlSelect exception in _db.query: " + e.message])
286
+ }
287
+ OINOBenchmark.end("sqlSelect")
288
+ return result
289
+ }
290
+
291
+ /**
292
+ * Execute other sql operations.
293
+ *
294
+ * @param sql SQL statement.
295
+ *
296
+ */
297
+ async sqlExec(sql:string): Promise<OINODbDataSet> {
298
+ OINOBenchmark.start("sqlExec")
299
+ let result:OINODbDataSet
300
+ try {
301
+ const sql_res:OINODataRow[] = await this._exec(sql)
302
+ // OINOLog.debug("OINODbMariadb.sqlExec", {sql_res:sql_res})
303
+ result = new OINOMariadbData(sql_res, [])
304
+
305
+ } catch (e:any) {
306
+ result = new OINOMariadbData([[]], [OINO_ERROR_PREFIX + " (sqlExec): exception in _db.exec [" + e.message + "]"])
307
+ }
308
+ OINOBenchmark.end("sqlExec")
309
+ return result
310
+ }
311
+
312
+ /**
313
+ * Initialize a data model by getting the SQL schema and populating OINODbDataFields of
314
+ * the model.
315
+ *
316
+ * @param api api which data model to initialize.
317
+ *
318
+ */
319
+ async initializeApiDatamodel(api:OINODbApi): Promise<void> {
320
+
321
+ const res:OINODbDataSet = await this.sqlSelect(OINODbMariadb._tableSchemaSql + this._params.database + "." + api.params.tableName + ";")
322
+ while (!res.isEof()) {
323
+ const row:OINODataRow = res.getRow()
324
+ // OINOLog.debug("OINODbMariadb.initializeApiDatamodel", { description:row })
325
+ const field_name:string = row[0]?.toString() || ""
326
+ const field_matches = OINODbMariadb._fieldLengthRegex.exec(row[1]?.toString() || "") || []
327
+ // OINOLog.debug("OINODbMariadb.initializeApiDatamodel", { field_matches:field_matches })
328
+ const sql_type:string = field_matches[1] || ""
329
+ const field_length1:number = this._parseFieldLength(field_matches[3] || "0")
330
+ const field_length2:number = this._parseFieldLength(field_matches[4] || "0")
331
+ const extra:string = row[5]?.toString() || ""
332
+ const field_params:OINODbDataFieldParams = {
333
+ isPrimaryKey: row[3] == "PRI",
334
+ isAutoInc: extra.indexOf('auto_increment') >= 0,
335
+ isNotNull: row[2] == "NO"
336
+ }
337
+ if (((api.params.excludeFieldPrefix) && field_name.startsWith(api.params.excludeFieldPrefix)) || ((api.params.excludeFields) && (api.params.excludeFields.indexOf(field_name) < 0))) {
338
+ OINOLog.info("OINODbMariadb.initializeApiDatamodel: field excluded in API parameters.", {field:field_name})
339
+ } else {
340
+ // OINOLog.debug("OINODbMariadb.initializeApiDatamodel: next field ", {field_name: field_name, sql_type:sql_type, field_length1:field_length1, field_length2:field_length2, field_params:field_params })
341
+ if ((sql_type == "int") || (sql_type == "smallint") || (sql_type == "float") || (sql_type == "double")) {
342
+ api.datamodel.addField(new OINONumberDataField(this, field_name, sql_type, field_params ))
343
+
344
+ } else if ((sql_type == "date") || (sql_type == "datetime") || (sql_type == "timestamp")) {
345
+ if (api.params.useDatesAsString) {
346
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
347
+ } else {
348
+ api.datamodel.addField(new OINODatetimeDataField(this, field_name, sql_type, field_params))
349
+ }
350
+
351
+ } else if ((sql_type == "char") || (sql_type == "varchar") || (sql_type == "tinytext") || (sql_type == "tinytext") || (sql_type == "mediumtext") || (sql_type == "longtext")) {
352
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, field_length1))
353
+
354
+ } else if ((sql_type == "longblob") || (sql_type == "binary") || (sql_type == "varbinary")) {
355
+ api.datamodel.addField(new OINOBlobDataField(this, field_name, sql_type, field_params, field_length1))
356
+
357
+ } else if ((sql_type == "decimal")) {
358
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, field_length1 + field_length2 + 1))
359
+
360
+ } else if ((sql_type == "bit")) {
361
+ if (field_length1 == 1) {
362
+ api.datamodel.addField(new OINOBooleanDataField(this, field_name, sql_type, field_params))
363
+ } else {
364
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, field_length1*8))
365
+ }
366
+
367
+ } else {
368
+ OINOLog.info("OINODbMariadb.initializeApiDatamodel: unrecognized field type treated as string", {field_name: field_name, sql_type:sql_type, field_length1:field_length1, field_length2:field_length2, field_params:field_params })
369
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
370
+ }
371
+ }
372
+ res.next()
373
+ }
374
+ OINOLog.debug("OINODbMariadb.initializeDatasetModel:\n" + api.datamodel.printDebug("\n"))
375
+ return Promise.resolve()
376
+ }
377
+ }
378
+
379
+
380
+
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { OINODbMariadb } from "./OINODbMariadb.js"