@oino-ts/db-mariadb 0.3.3 → 0.4.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.
@@ -222,9 +222,18 @@ class OINODbMariadb extends db_1.OINODb {
222
222
  }
223
223
  }
224
224
  else {
225
- return "\"" + cellValue?.toString().replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\r", "\\r").replaceAll("\n", "\\n").replaceAll("\t", "\\t") + "\"";
225
+ return this.printSqlString(cellValue?.toString());
226
226
  }
227
227
  }
228
+ /**
229
+ * Print a single string value as valid sql literal
230
+ *
231
+ * @param sqlString string value
232
+ *
233
+ */
234
+ printSqlString(sqlString) {
235
+ return "\"" + sqlString.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\r", "\\r").replaceAll("\n", "\\n").replaceAll("\t", "\\t") + "\"";
236
+ }
228
237
  /**
229
238
  * Parse a single SQL result value for serialization using the context of the native data
230
239
  * type.
@@ -314,17 +323,17 @@ class OINODbMariadb extends db_1.OINODb {
314
323
  return result;
315
324
  }
316
325
  _getSchemaSql(dbName, tableName) {
317
- const sql = `SELECT
318
- c.COLUMN_NAME,
319
- c.COLUMN_TYPE,
320
- c.IS_NULLABLE,
321
- c.COLUMN_KEY,
322
- c.COLUMN_DEFAULT,
323
- c.EXTRA,
324
- KCU.CONSTRAINT_NAME AS ForeignKeyName
325
- FROM information_schema.COLUMNS C
326
- LEFT JOIN information_schema.KEY_COLUMN_USAGE KCU ON KCU.TABLE_SCHEMA = C.TABLE_SCHEMA AND KCU.TABLE_NAME = C.TABLE_NAME AND C.COLUMN_NAME = KCU.COLUMN_NAME and KCU.REFERENCED_TABLE_NAME IS NOT NULL
327
- WHERE C.TABLE_SCHEMA = '${dbName}' AND C.TABLE_NAME = '${tableName}'
326
+ const sql = `SELECT
327
+ c.COLUMN_NAME,
328
+ c.COLUMN_TYPE,
329
+ c.IS_NULLABLE,
330
+ c.COLUMN_KEY,
331
+ c.COLUMN_DEFAULT,
332
+ c.EXTRA,
333
+ KCU.CONSTRAINT_NAME AS ForeignKeyName
334
+ FROM information_schema.COLUMNS C
335
+ LEFT JOIN information_schema.KEY_COLUMN_USAGE KCU ON KCU.TABLE_SCHEMA = C.TABLE_SCHEMA AND KCU.TABLE_NAME = C.TABLE_NAME AND C.COLUMN_NAME = KCU.COLUMN_NAME and KCU.REFERENCED_TABLE_NAME IS NOT NULL
336
+ WHERE C.TABLE_SCHEMA = '${dbName}' AND C.TABLE_NAME = '${tableName}'
328
337
  ORDER BY C.ORDINAL_POSITION;`;
329
338
  return sql;
330
339
  }
@@ -219,9 +219,18 @@ export class OINODbMariadb extends OINODb {
219
219
  }
220
220
  }
221
221
  else {
222
- return "\"" + cellValue?.toString().replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\r", "\\r").replaceAll("\n", "\\n").replaceAll("\t", "\\t") + "\"";
222
+ return this.printSqlString(cellValue?.toString());
223
223
  }
224
224
  }
225
+ /**
226
+ * Print a single string value as valid sql literal
227
+ *
228
+ * @param sqlString string value
229
+ *
230
+ */
231
+ printSqlString(sqlString) {
232
+ return "\"" + sqlString.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\r", "\\r").replaceAll("\n", "\\n").replaceAll("\t", "\\t") + "\"";
233
+ }
225
234
  /**
226
235
  * Parse a single SQL result value for serialization using the context of the native data
227
236
  * type.
@@ -311,17 +320,17 @@ export class OINODbMariadb extends OINODb {
311
320
  return result;
312
321
  }
313
322
  _getSchemaSql(dbName, tableName) {
314
- const sql = `SELECT
315
- c.COLUMN_NAME,
316
- c.COLUMN_TYPE,
317
- c.IS_NULLABLE,
318
- c.COLUMN_KEY,
319
- c.COLUMN_DEFAULT,
320
- c.EXTRA,
321
- KCU.CONSTRAINT_NAME AS ForeignKeyName
322
- FROM information_schema.COLUMNS C
323
- LEFT JOIN information_schema.KEY_COLUMN_USAGE KCU ON KCU.TABLE_SCHEMA = C.TABLE_SCHEMA AND KCU.TABLE_NAME = C.TABLE_NAME AND C.COLUMN_NAME = KCU.COLUMN_NAME and KCU.REFERENCED_TABLE_NAME IS NOT NULL
324
- WHERE C.TABLE_SCHEMA = '${dbName}' AND C.TABLE_NAME = '${tableName}'
323
+ const sql = `SELECT
324
+ c.COLUMN_NAME,
325
+ c.COLUMN_TYPE,
326
+ c.IS_NULLABLE,
327
+ c.COLUMN_KEY,
328
+ c.COLUMN_DEFAULT,
329
+ c.EXTRA,
330
+ KCU.CONSTRAINT_NAME AS ForeignKeyName
331
+ FROM information_schema.COLUMNS C
332
+ LEFT JOIN information_schema.KEY_COLUMN_USAGE KCU ON KCU.TABLE_SCHEMA = C.TABLE_SCHEMA AND KCU.TABLE_NAME = C.TABLE_NAME AND C.COLUMN_NAME = KCU.COLUMN_NAME and KCU.REFERENCED_TABLE_NAME IS NOT NULL
333
+ WHERE C.TABLE_SCHEMA = '${dbName}' AND C.TABLE_NAME = '${tableName}'
325
334
  ORDER BY C.ORDINAL_POSITION;`;
326
335
  return sql;
327
336
  }
@@ -38,6 +38,13 @@ export declare class OINODbMariadb extends OINODb {
38
38
  *
39
39
  */
40
40
  printCellAsSqlValue(cellValue: OINODataCell, sqlType: string): string;
41
+ /**
42
+ * Print a single string value as valid sql literal
43
+ *
44
+ * @param sqlString string value
45
+ *
46
+ */
47
+ printSqlString(sqlString: string): string;
41
48
  /**
42
49
  * Parse a single SQL result value for serialization using the context of the native data
43
50
  * type.
package/package.json CHANGED
@@ -1,37 +1,37 @@
1
- {
2
- "name": "@oino-ts/db-mariadb",
3
- "version": "0.3.3",
4
- "description": "OINO TS package for using Mariadb databases.",
5
- "author": "Matias Kiviniemi (pragmatta)",
6
- "license": "MPL-2.0",
7
- "repository": {
8
- "type": "git",
9
- "url": "https://github.com/pragmatta/oino-ts.git"
10
- },
11
- "keywords": [
12
- "sql",
13
- "database",
14
- "rest-api",
15
- "typescript",
16
- "library",
17
- "mariadb",
18
- "mysql"
19
- ],
20
- "main": "./dist/cjs/index.js",
21
- "module": "./dist/esm/index.js",
22
- "types": "./dist/types/index.d.ts",
23
- "dependencies": {
24
- "@oino-ts/db": "^0.3.3",
25
- "mariadb": "^3.2.3"
26
- },
27
- "devDependencies": {
28
- "@types/node": "^20.12.7",
29
- "@types/bun": "^1.1.14"
30
- },
31
- "files": [
32
- "src/*.ts",
33
- "dist/cjs/*.js",
34
- "dist/esm/*.js",
35
- "dist/types/*.d.ts"
36
- ]
37
- }
1
+ {
2
+ "name": "@oino-ts/db-mariadb",
3
+ "version": "0.4.0",
4
+ "description": "OINO TS package for using Mariadb databases.",
5
+ "author": "Matias Kiviniemi (pragmatta)",
6
+ "license": "MPL-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/pragmatta/oino-ts.git"
10
+ },
11
+ "keywords": [
12
+ "sql",
13
+ "database",
14
+ "rest-api",
15
+ "typescript",
16
+ "library",
17
+ "mariadb",
18
+ "mysql"
19
+ ],
20
+ "main": "./dist/cjs/index.js",
21
+ "module": "./dist/esm/index.js",
22
+ "types": "./dist/types/index.d.ts",
23
+ "dependencies": {
24
+ "@oino-ts/db": "^0.4.0",
25
+ "mariadb": "^3.2.3"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^20.12.7",
29
+ "@types/bun": "^1.1.14"
30
+ },
31
+ "files": [
32
+ "src/*.ts",
33
+ "dist/cjs/*.js",
34
+ "dist/esm/*.js",
35
+ "dist/types/*.d.ts"
36
+ ]
37
+ }
@@ -1,422 +1,433 @@
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
- /**
45
- * Is data set empty.
46
- *
47
- */
48
- isEmpty():boolean {
49
- return (this._rows.length == 0)
50
- }
51
-
52
- /**
53
- * Is there no more content, i.e. either dataset is empty or we have moved beyond last line
54
- *
55
- */
56
- isEof():boolean {
57
- return (this._eof)
58
- }
59
-
60
- /**
61
- * Attempts to moves dataset to the next row, possibly waiting for more data to become available. Returns !isEof().
62
- *
63
- */
64
- async next():Promise<boolean> {
65
- // OINOLog.debug("OINODbDataSet.next", {currentRow:this._currentRow, length:this.sqlResult.data.length})
66
- if (this._currentRow < this._rows.length-1) {
67
- this._currentRow = this._currentRow + 1
68
- } else {
69
- this._eof = true
70
- }
71
- return Promise.resolve(!this._eof)
72
- }
73
-
74
- /**
75
- * Gets current row of data.
76
- *
77
- */
78
- getRow(): OINODataRow {
79
- if ((this._currentRow >=0) && (this._currentRow < this._rows.length)) {
80
- return this._rows[this._currentRow]
81
- } else {
82
- return OINODB_EMPTY_ROW
83
- }
84
- }
85
- }
86
-
87
- /**
88
- * Implementation of MariaDb/MySql-database.
89
- *
90
- */
91
- export class OINODbMariadb extends OINODb {
92
-
93
- private static _fieldLengthRegex = /([^\(\)]+)(\s?\((\d+)\s?\,?\s?(\d*)?\))?/i
94
- private static _exceptionMessageRegex = /\(([^\)]*)\) (.*)\nsql\:(.*)?/i
95
-
96
- private _pool:mariadb.Pool
97
-
98
- /**
99
- * Constructor of `OINODbMariadb`
100
- * @param params database parameters
101
- */
102
- constructor(params:OINODbParams) {
103
- super(params)
104
-
105
- // OINOLog.debug("OINODbMariadb.constructor", {params:params})
106
- if (this._params.type !== "OINODbMariadb") {
107
- throw new Error(OINO_ERROR_PREFIX + ": Not OINODbMariadb-type: " + this._params.type)
108
- }
109
- 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 })
110
-
111
- // this._pool.on("acquire", (conn: mariadb.Connection) => {
112
- // OINOLog.info("OINODbMariadb acquire", {conn:conn})
113
- // })
114
- // this._pool.on("connection", (conn: mariadb.Connection) => {
115
- // OINOLog.info("OINODbMariadb connection", {conn:conn})
116
- // })
117
- // this._pool.on("release", (conn: mariadb.Connection) => {
118
- // OINOLog.info("OINODbMariadb release", {conn:conn})
119
- // })
120
- // this._pool.on("enqueue", () => {
121
- // OINOLog.info("OINODbMariadb enqueue", {})
122
- // })
123
- }
124
-
125
- private _parseFieldLength(fieldLengthStr:string):number {
126
- let result:number = parseInt(fieldLengthStr)
127
- if (Number.isNaN(result)) {
128
- result = 0
129
- }
130
- return result
131
- }
132
-
133
- private async _query(sql:string):Promise<OINODataRow[]> {
134
- // OINOLog.debug("OINODbMariadb._query", {sql:sql})
135
- let connection:mariadb.Connection|null = null
136
- try {
137
- connection = await this._pool.getConnection();
138
- const result = await connection.query(sql);
139
- // console.log("OINODbMariadb._query rows="+result)
140
- return Promise.resolve(result)
141
-
142
- } catch (err) {
143
- // console.log("OINODbMariadb._query err=" + err);
144
- throw err;
145
- } finally {
146
- if (connection) {
147
- await connection.end()
148
- }
149
- }
150
- // OINOLog.debug("OINODbMariadb._query", {result:query_result})
151
- }
152
-
153
- private async _exec(sql:string):Promise<any> {
154
- // OINOLog.debug("OINODbMariadb._exec", {sql:sql})
155
- let connection:mariadb.Connection|null = null
156
- try {
157
- connection = await this._pool.getConnection();
158
- const result = await connection.query(sql);
159
- // console.log(result);
160
- return Promise.resolve(result)
161
-
162
- } catch (err) {
163
- const msg_parts = (err as Error).message.match(OINODbMariadb._exceptionMessageRegex) || []
164
- // 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
165
- throw new Error(msg_parts[2]);
166
- } finally {
167
- if (connection) {
168
- await connection.end()
169
- }
170
- }
171
- // OINOLog.debug("OINODbMariadb._query", {result:query_result})
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+"`"
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
- /**
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 sqlType native type name for table column
201
- *
202
- */
203
- printCellAsSqlValue(cellValue:OINODataCell, sqlType: string): string {
204
- // OINOLog.debug("OINODbMariadb.printCellAsSqlValue", {cellValue:cellValue, sqlType:sqlType})
205
- if (cellValue === null) {
206
- return "NULL"
207
-
208
- } else if (cellValue === undefined) {
209
- return "UNDEFINED"
210
-
211
- } else if ((sqlType == "int") || (sqlType == "smallint") || (sqlType == "float")) {
212
- return cellValue.toString()
213
-
214
- } else if ((sqlType == "longblob") || (sqlType == "binary") || (sqlType == "varbinary")) {
215
- if (cellValue instanceof Buffer) {
216
- return "x'" + (cellValue as Buffer).toString("hex") + "'"
217
- } else if (cellValue instanceof Uint8Array) {
218
- return "x'" + Buffer.from(cellValue as Uint8Array).toString("hex") + "'"
219
- } else {
220
- return "\"" + cellValue?.toString() + "\""
221
- }
222
-
223
- } else if (((sqlType == "date") || (sqlType == "datetime") || (sqlType == "timestamp")) && (cellValue instanceof Date)) {
224
- return "\"" + cellValue.toISOString().replace('T', ' ').substring(0, 23) + "\""
225
-
226
- } else if ((sqlType == "bit")) {
227
- if ((cellValue === false) || (cellValue == null) || (cellValue == "") || (cellValue.toString().toLowerCase() == "false") || (cellValue == "0")) {
228
- return "b'0'"
229
- } else if ((cellValue === true) || (cellValue.toString().toLowerCase() == "true")) {
230
- return "b'1'"
231
- } else {
232
- return "b'" + cellValue.toString() + "'" // rest is assumed to be a valid bitstring
233
- }
234
-
235
- } else {
236
- return "\"" + cellValue?.toString().replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\r", "\\r").replaceAll("\n", "\\n").replaceAll("\t", "\\t") + "\""
237
- }
238
- }
239
-
240
- /**
241
- * Parse a single SQL result value for serialization using the context of the native data
242
- * type.
243
- *
244
- * @param sqlValue data from serialization
245
- * @param sqlType native type name for table column
246
- *
247
- */
248
- parseSqlValueAsCell(sqlValue:OINODataCell, sqlType: string): OINODataCell {
249
- // OINOLog.debug("OINODbMariadb.parseSqlValueAsCell", {sqlValue:sqlValue, sqlType:sqlType})
250
- if ((sqlValue === null) || (sqlValue == "NULL")) {
251
- return null
252
-
253
- } else if (sqlValue === undefined) {
254
- return undefined
255
-
256
- } else if (((sqlType == "date")) && (typeof(sqlValue) == "string")) {
257
- return new Date(sqlValue)
258
-
259
- } else if ((sqlType == "bit") && (sqlValue instanceof Buffer)) { // mariadb returns a buffer for bit-fields
260
- const buf:Buffer = sqlValue as Buffer
261
- let result:string = ""
262
- for (let i=0; i<buf.length; i++) {
263
- result += buf[i].toString(2).padStart(8, '0')
264
- }
265
- return result
266
-
267
- } else {
268
- return sqlValue
269
- }
270
-
271
- }
272
-
273
- /**
274
- * Connect to database.
275
- *
276
- */
277
- async connect(): Promise<boolean> {
278
- try {
279
- // make sure that any items are correctly URL encoded in the connection string
280
- // OINOLog.debug("OINODbMariadb.connect")
281
- await this._pool.on
282
- // await this._client.connect()
283
- return Promise.resolve(true)
284
- } catch (err) {
285
- // ... error checks
286
- throw new Error(OINO_ERROR_PREFIX + ": Error connecting to OINODbMariadb server: " + err)
287
- }
288
- }
289
-
290
- /**
291
- * Execute a select operation.
292
- *
293
- * @param sql SQL statement.
294
- *
295
- */
296
- async sqlSelect(sql:string): Promise<OINODbDataSet> {
297
- OINOBenchmark.start("OINODb", "sqlSelect")
298
- let result:OINODbDataSet
299
- try {
300
- const sql_res:OINODataRow[] = await this._query(sql)
301
- // OINOLog.debug("OINODbMariadb.sqlSelect", {sql_res:sql_res})
302
- result = new OINOMariadbData(sql_res, [])
303
-
304
- } catch (e:any) {
305
- result = new OINOMariadbData([[]], [OINO_ERROR_PREFIX + " (sqlSelect): OINODbMariadb.sqlSelect exception in _db.query: " + e.message])
306
- }
307
- OINOBenchmark.end("OINODb", "sqlSelect")
308
- return result
309
- }
310
-
311
- /**
312
- * Execute other sql operations.
313
- *
314
- * @param sql SQL statement.
315
- *
316
- */
317
- async sqlExec(sql:string): Promise<OINODbDataSet> {
318
- OINOBenchmark.start("OINODb", "sqlExec")
319
- let result:OINODbDataSet
320
- try {
321
- const sql_res:OINODataRow[] = await this._exec(sql)
322
- // OINOLog.debug("OINODbMariadb.sqlExec", {sql_res:sql_res})
323
- result = new OINOMariadbData(sql_res, [])
324
-
325
- } catch (e:any) {
326
- result = new OINOMariadbData([[]], [OINO_ERROR_PREFIX + " (sqlExec): exception in _db.exec [" + e.message + "]"])
327
- }
328
- OINOBenchmark.end("OINODb", "sqlExec")
329
- return result
330
- }
331
-
332
- private _getSchemaSql(dbName:string, tableName:string):string {
333
- const sql =
334
- `SELECT
335
- c.COLUMN_NAME,
336
- c.COLUMN_TYPE,
337
- c.IS_NULLABLE,
338
- c.COLUMN_KEY,
339
- c.COLUMN_DEFAULT,
340
- c.EXTRA,
341
- KCU.CONSTRAINT_NAME AS ForeignKeyName
342
- FROM information_schema.COLUMNS C
343
- LEFT JOIN information_schema.KEY_COLUMN_USAGE KCU ON KCU.TABLE_SCHEMA = C.TABLE_SCHEMA AND KCU.TABLE_NAME = C.TABLE_NAME AND C.COLUMN_NAME = KCU.COLUMN_NAME and KCU.REFERENCED_TABLE_NAME IS NOT NULL
344
- WHERE C.TABLE_SCHEMA = '${dbName}' AND C.TABLE_NAME = '${tableName}'
345
- ORDER BY C.ORDINAL_POSITION;`
346
- return sql
347
- }
348
-
349
- /**
350
- * Initialize a data model by getting the SQL schema and populating OINODbDataFields of
351
- * the model.
352
- *
353
- * @param api api which data model to initialize.
354
- *
355
- */
356
- async initializeApiDatamodel(api:OINODbApi): Promise<void> {
357
-
358
- const res:OINODbDataSet = await this.sqlSelect(this._getSchemaSql(this._params.database, api.params.tableName))
359
- while (!res.isEof()) {
360
- const row:OINODataRow = res.getRow()
361
- // OINOLog.debug("OINODbMariadb.initializeApiDatamodel", { description:row })
362
- const field_name:string = row[0]?.toString() || ""
363
- const field_matches = OINODbMariadb._fieldLengthRegex.exec(row[1]?.toString() || "") || []
364
- // OINOLog.debug("OINODbMariadb.initializeApiDatamodel", { field_matches:field_matches })
365
- const sql_type:string = field_matches[1] || ""
366
- const field_length1:number = this._parseFieldLength(field_matches[3] || "0")
367
- const field_length2:number = this._parseFieldLength(field_matches[4] || "0")
368
- const extra:string = row[5]?.toString() || ""
369
- const field_params:OINODbDataFieldParams = {
370
- isPrimaryKey: row[3] == "PRI",
371
- isForeignKey: row[6] != null,
372
- isAutoInc: extra.indexOf('auto_increment') >= 0,
373
- isNotNull: row[2] == "NO"
374
- }
375
- if (api.isFieldIncluded(field_name)==false) {
376
- OINOLog.info("OINODbMariadb.initializeApiDatamodel: field excluded in API parameters.", {field:field_name})
377
- if (field_params.isPrimaryKey) {
378
- throw new Error(OINO_ERROR_PREFIX + "Primary key field excluded in API parameters: " + field_name)
379
- }
380
-
381
- } else {
382
- // 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 })
383
- if ((sql_type == "int") || (sql_type == "smallint") || (sql_type == "float") || (sql_type == "double")) {
384
- api.datamodel.addField(new OINONumberDataField(this, field_name, sql_type, field_params ))
385
-
386
- } else if ((sql_type == "date") || (sql_type == "datetime") || (sql_type == "timestamp")) {
387
- if (api.params.useDatesAsString) {
388
- api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
389
- } else {
390
- api.datamodel.addField(new OINODatetimeDataField(this, field_name, sql_type, field_params))
391
- }
392
-
393
- } else if ((sql_type == "char") || (sql_type == "varchar") || (sql_type == "tinytext") || (sql_type == "tinytext") || (sql_type == "mediumtext") || (sql_type == "longtext")) {
394
- api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, field_length1))
395
-
396
- } else if ((sql_type == "longblob") || (sql_type == "binary") || (sql_type == "varbinary")) {
397
- api.datamodel.addField(new OINOBlobDataField(this, field_name, sql_type, field_params, field_length1))
398
-
399
- } else if ((sql_type == "decimal")) {
400
- api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, field_length1 + field_length2 + 1))
401
-
402
- } else if ((sql_type == "bit")) {
403
- if (field_length1 == 1) {
404
- api.datamodel.addField(new OINOBooleanDataField(this, field_name, sql_type, field_params))
405
- } else {
406
- api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, field_length1*8))
407
- }
408
-
409
- } else {
410
- 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 })
411
- api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
412
- }
413
- }
414
- await res.next()
415
- }
416
- OINOLog.debug("OINODbMariadb.initializeDatasetModel:\n" + api.datamodel.printDebug("\n"))
417
- return Promise.resolve()
418
- }
419
- }
420
-
421
-
422
-
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
+ /**
45
+ * Is data set empty.
46
+ *
47
+ */
48
+ isEmpty():boolean {
49
+ return (this._rows.length == 0)
50
+ }
51
+
52
+ /**
53
+ * Is there no more content, i.e. either dataset is empty or we have moved beyond last line
54
+ *
55
+ */
56
+ isEof():boolean {
57
+ return (this._eof)
58
+ }
59
+
60
+ /**
61
+ * Attempts to moves dataset to the next row, possibly waiting for more data to become available. Returns !isEof().
62
+ *
63
+ */
64
+ async next():Promise<boolean> {
65
+ // OINOLog.debug("OINODbDataSet.next", {currentRow:this._currentRow, length:this.sqlResult.data.length})
66
+ if (this._currentRow < this._rows.length-1) {
67
+ this._currentRow = this._currentRow + 1
68
+ } else {
69
+ this._eof = true
70
+ }
71
+ return Promise.resolve(!this._eof)
72
+ }
73
+
74
+ /**
75
+ * Gets current row of data.
76
+ *
77
+ */
78
+ getRow(): OINODataRow {
79
+ if ((this._currentRow >=0) && (this._currentRow < this._rows.length)) {
80
+ return this._rows[this._currentRow]
81
+ } else {
82
+ return OINODB_EMPTY_ROW
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Implementation of MariaDb/MySql-database.
89
+ *
90
+ */
91
+ export class OINODbMariadb extends OINODb {
92
+
93
+ private static _fieldLengthRegex = /([^\(\)]+)(\s?\((\d+)\s?\,?\s?(\d*)?\))?/i
94
+ private static _exceptionMessageRegex = /\(([^\)]*)\) (.*)\nsql\:(.*)?/i
95
+
96
+ private _pool:mariadb.Pool
97
+
98
+ /**
99
+ * Constructor of `OINODbMariadb`
100
+ * @param params database parameters
101
+ */
102
+ constructor(params:OINODbParams) {
103
+ super(params)
104
+
105
+ // OINOLog.debug("OINODbMariadb.constructor", {params:params})
106
+ if (this._params.type !== "OINODbMariadb") {
107
+ throw new Error(OINO_ERROR_PREFIX + ": Not OINODbMariadb-type: " + this._params.type)
108
+ }
109
+ 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 })
110
+
111
+ // this._pool.on("acquire", (conn: mariadb.Connection) => {
112
+ // OINOLog.info("OINODbMariadb acquire", {conn:conn})
113
+ // })
114
+ // this._pool.on("connection", (conn: mariadb.Connection) => {
115
+ // OINOLog.info("OINODbMariadb connection", {conn:conn})
116
+ // })
117
+ // this._pool.on("release", (conn: mariadb.Connection) => {
118
+ // OINOLog.info("OINODbMariadb release", {conn:conn})
119
+ // })
120
+ // this._pool.on("enqueue", () => {
121
+ // OINOLog.info("OINODbMariadb enqueue", {})
122
+ // })
123
+ }
124
+
125
+ private _parseFieldLength(fieldLengthStr:string):number {
126
+ let result:number = parseInt(fieldLengthStr)
127
+ if (Number.isNaN(result)) {
128
+ result = 0
129
+ }
130
+ return result
131
+ }
132
+
133
+ private async _query(sql:string):Promise<OINODataRow[]> {
134
+ // OINOLog.debug("OINODbMariadb._query", {sql:sql})
135
+ let connection:mariadb.Connection|null = null
136
+ try {
137
+ connection = await this._pool.getConnection();
138
+ const result = await connection.query(sql);
139
+ // console.log("OINODbMariadb._query rows="+result)
140
+ return Promise.resolve(result)
141
+
142
+ } catch (err) {
143
+ // console.log("OINODbMariadb._query err=" + err);
144
+ throw err;
145
+ } finally {
146
+ if (connection) {
147
+ await connection.end()
148
+ }
149
+ }
150
+ // OINOLog.debug("OINODbMariadb._query", {result:query_result})
151
+ }
152
+
153
+ private async _exec(sql:string):Promise<any> {
154
+ // OINOLog.debug("OINODbMariadb._exec", {sql:sql})
155
+ let connection:mariadb.Connection|null = null
156
+ try {
157
+ connection = await this._pool.getConnection();
158
+ const result = await connection.query(sql);
159
+ // console.log(result);
160
+ return Promise.resolve(result)
161
+
162
+ } catch (err) {
163
+ const msg_parts = (err as Error).message.match(OINODbMariadb._exceptionMessageRegex) || []
164
+ // 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
165
+ throw new Error(msg_parts[2]);
166
+ } finally {
167
+ if (connection) {
168
+ await connection.end()
169
+ }
170
+ }
171
+ // OINOLog.debug("OINODbMariadb._query", {result:query_result})
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+"`"
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
+ /**
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 sqlType native type name for table column
201
+ *
202
+ */
203
+ printCellAsSqlValue(cellValue:OINODataCell, sqlType: string): string {
204
+ // OINOLog.debug("OINODbMariadb.printCellAsSqlValue", {cellValue:cellValue, sqlType:sqlType})
205
+ if (cellValue === null) {
206
+ return "NULL"
207
+
208
+ } else if (cellValue === undefined) {
209
+ return "UNDEFINED"
210
+
211
+ } else if ((sqlType == "int") || (sqlType == "smallint") || (sqlType == "float")) {
212
+ return cellValue.toString()
213
+
214
+ } else if ((sqlType == "longblob") || (sqlType == "binary") || (sqlType == "varbinary")) {
215
+ if (cellValue instanceof Buffer) {
216
+ return "x'" + (cellValue as Buffer).toString("hex") + "'"
217
+ } else if (cellValue instanceof Uint8Array) {
218
+ return "x'" + Buffer.from(cellValue as Uint8Array).toString("hex") + "'"
219
+ } else {
220
+ return "\"" + cellValue?.toString() + "\""
221
+ }
222
+
223
+ } else if (((sqlType == "date") || (sqlType == "datetime") || (sqlType == "timestamp")) && (cellValue instanceof Date)) {
224
+ return "\"" + cellValue.toISOString().replace('T', ' ').substring(0, 23) + "\""
225
+
226
+ } else if ((sqlType == "bit")) {
227
+ if ((cellValue === false) || (cellValue == null) || (cellValue == "") || (cellValue.toString().toLowerCase() == "false") || (cellValue == "0")) {
228
+ return "b'0'"
229
+ } else if ((cellValue === true) || (cellValue.toString().toLowerCase() == "true")) {
230
+ return "b'1'"
231
+ } else {
232
+ return "b'" + cellValue.toString() + "'" // rest is assumed to be a valid bitstring
233
+ }
234
+
235
+ } else {
236
+ return this.printSqlString(cellValue?.toString())
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Print a single string value as valid sql literal
242
+ *
243
+ * @param sqlString string value
244
+ *
245
+ */
246
+ printSqlString(sqlString:string): string {
247
+ return "\"" + sqlString.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\r", "\\r").replaceAll("\n", "\\n").replaceAll("\t", "\\t") + "\""
248
+ }
249
+
250
+
251
+ /**
252
+ * Parse a single SQL result value for serialization using the context of the native data
253
+ * type.
254
+ *
255
+ * @param sqlValue data from serialization
256
+ * @param sqlType native type name for table column
257
+ *
258
+ */
259
+ parseSqlValueAsCell(sqlValue:OINODataCell, sqlType: string): OINODataCell {
260
+ // OINOLog.debug("OINODbMariadb.parseSqlValueAsCell", {sqlValue:sqlValue, sqlType:sqlType})
261
+ if ((sqlValue === null) || (sqlValue == "NULL")) {
262
+ return null
263
+
264
+ } else if (sqlValue === undefined) {
265
+ return undefined
266
+
267
+ } else if (((sqlType == "date")) && (typeof(sqlValue) == "string")) {
268
+ return new Date(sqlValue)
269
+
270
+ } else if ((sqlType == "bit") && (sqlValue instanceof Buffer)) { // mariadb returns a buffer for bit-fields
271
+ const buf:Buffer = sqlValue as Buffer
272
+ let result:string = ""
273
+ for (let i=0; i<buf.length; i++) {
274
+ result += buf[i].toString(2).padStart(8, '0')
275
+ }
276
+ return result
277
+
278
+ } else {
279
+ return sqlValue
280
+ }
281
+
282
+ }
283
+
284
+ /**
285
+ * Connect to database.
286
+ *
287
+ */
288
+ async connect(): Promise<boolean> {
289
+ try {
290
+ // make sure that any items are correctly URL encoded in the connection string
291
+ // OINOLog.debug("OINODbMariadb.connect")
292
+ await this._pool.on
293
+ // await this._client.connect()
294
+ return Promise.resolve(true)
295
+ } catch (err) {
296
+ // ... error checks
297
+ throw new Error(OINO_ERROR_PREFIX + ": Error connecting to OINODbMariadb server: " + err)
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Execute a select operation.
303
+ *
304
+ * @param sql SQL statement.
305
+ *
306
+ */
307
+ async sqlSelect(sql:string): Promise<OINODbDataSet> {
308
+ OINOBenchmark.start("OINODb", "sqlSelect")
309
+ let result:OINODbDataSet
310
+ try {
311
+ const sql_res:OINODataRow[] = await this._query(sql)
312
+ // OINOLog.debug("OINODbMariadb.sqlSelect", {sql_res:sql_res})
313
+ result = new OINOMariadbData(sql_res, [])
314
+
315
+ } catch (e:any) {
316
+ result = new OINOMariadbData([[]], [OINO_ERROR_PREFIX + " (sqlSelect): OINODbMariadb.sqlSelect exception in _db.query: " + e.message])
317
+ }
318
+ OINOBenchmark.end("OINODb", "sqlSelect")
319
+ return result
320
+ }
321
+
322
+ /**
323
+ * Execute other sql operations.
324
+ *
325
+ * @param sql SQL statement.
326
+ *
327
+ */
328
+ async sqlExec(sql:string): Promise<OINODbDataSet> {
329
+ OINOBenchmark.start("OINODb", "sqlExec")
330
+ let result:OINODbDataSet
331
+ try {
332
+ const sql_res:OINODataRow[] = await this._exec(sql)
333
+ // OINOLog.debug("OINODbMariadb.sqlExec", {sql_res:sql_res})
334
+ result = new OINOMariadbData(sql_res, [])
335
+
336
+ } catch (e:any) {
337
+ result = new OINOMariadbData([[]], [OINO_ERROR_PREFIX + " (sqlExec): exception in _db.exec [" + e.message + "]"])
338
+ }
339
+ OINOBenchmark.end("OINODb", "sqlExec")
340
+ return result
341
+ }
342
+
343
+ private _getSchemaSql(dbName:string, tableName:string):string {
344
+ const sql =
345
+ `SELECT
346
+ c.COLUMN_NAME,
347
+ c.COLUMN_TYPE,
348
+ c.IS_NULLABLE,
349
+ c.COLUMN_KEY,
350
+ c.COLUMN_DEFAULT,
351
+ c.EXTRA,
352
+ KCU.CONSTRAINT_NAME AS ForeignKeyName
353
+ FROM information_schema.COLUMNS C
354
+ LEFT JOIN information_schema.KEY_COLUMN_USAGE KCU ON KCU.TABLE_SCHEMA = C.TABLE_SCHEMA AND KCU.TABLE_NAME = C.TABLE_NAME AND C.COLUMN_NAME = KCU.COLUMN_NAME and KCU.REFERENCED_TABLE_NAME IS NOT NULL
355
+ WHERE C.TABLE_SCHEMA = '${dbName}' AND C.TABLE_NAME = '${tableName}'
356
+ ORDER BY C.ORDINAL_POSITION;`
357
+ return sql
358
+ }
359
+
360
+ /**
361
+ * Initialize a data model by getting the SQL schema and populating OINODbDataFields of
362
+ * the model.
363
+ *
364
+ * @param api api which data model to initialize.
365
+ *
366
+ */
367
+ async initializeApiDatamodel(api:OINODbApi): Promise<void> {
368
+
369
+ const res:OINODbDataSet = await this.sqlSelect(this._getSchemaSql(this._params.database, api.params.tableName))
370
+ while (!res.isEof()) {
371
+ const row:OINODataRow = res.getRow()
372
+ // OINOLog.debug("OINODbMariadb.initializeApiDatamodel", { description:row })
373
+ const field_name:string = row[0]?.toString() || ""
374
+ const field_matches = OINODbMariadb._fieldLengthRegex.exec(row[1]?.toString() || "") || []
375
+ // OINOLog.debug("OINODbMariadb.initializeApiDatamodel", { field_matches:field_matches })
376
+ const sql_type:string = field_matches[1] || ""
377
+ const field_length1:number = this._parseFieldLength(field_matches[3] || "0")
378
+ const field_length2:number = this._parseFieldLength(field_matches[4] || "0")
379
+ const extra:string = row[5]?.toString() || ""
380
+ const field_params:OINODbDataFieldParams = {
381
+ isPrimaryKey: row[3] == "PRI",
382
+ isForeignKey: row[6] != null,
383
+ isAutoInc: extra.indexOf('auto_increment') >= 0,
384
+ isNotNull: row[2] == "NO"
385
+ }
386
+ if (api.isFieldIncluded(field_name)==false) {
387
+ OINOLog.info("OINODbMariadb.initializeApiDatamodel: field excluded in API parameters.", {field:field_name})
388
+ if (field_params.isPrimaryKey) {
389
+ throw new Error(OINO_ERROR_PREFIX + "Primary key field excluded in API parameters: " + field_name)
390
+ }
391
+
392
+ } else {
393
+ // 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 })
394
+ if ((sql_type == "int") || (sql_type == "smallint") || (sql_type == "float") || (sql_type == "double")) {
395
+ api.datamodel.addField(new OINONumberDataField(this, field_name, sql_type, field_params ))
396
+
397
+ } else if ((sql_type == "date") || (sql_type == "datetime") || (sql_type == "timestamp")) {
398
+ if (api.params.useDatesAsString) {
399
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
400
+ } else {
401
+ api.datamodel.addField(new OINODatetimeDataField(this, field_name, sql_type, field_params))
402
+ }
403
+
404
+ } else if ((sql_type == "char") || (sql_type == "varchar") || (sql_type == "tinytext") || (sql_type == "tinytext") || (sql_type == "mediumtext") || (sql_type == "longtext")) {
405
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, field_length1))
406
+
407
+ } else if ((sql_type == "longblob") || (sql_type == "binary") || (sql_type == "varbinary")) {
408
+ api.datamodel.addField(new OINOBlobDataField(this, field_name, sql_type, field_params, field_length1))
409
+
410
+ } else if ((sql_type == "decimal")) {
411
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, field_length1 + field_length2 + 1))
412
+
413
+ } else if ((sql_type == "bit")) {
414
+ if (field_length1 == 1) {
415
+ api.datamodel.addField(new OINOBooleanDataField(this, field_name, sql_type, field_params))
416
+ } else {
417
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, field_length1*8))
418
+ }
419
+
420
+ } else {
421
+ 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 })
422
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
423
+ }
424
+ }
425
+ await res.next()
426
+ }
427
+ OINOLog.debug("OINODbMariadb.initializeDatasetModel:\n" + api.datamodel.printDebug("\n"))
428
+ return Promise.resolve()
429
+ }
430
+ }
431
+
432
+
433
+
package/src/index.ts CHANGED
@@ -1 +1 @@
1
- export { OINODbMariadb } from "./OINODbMariadb.js"
1
+ export { OINODbMariadb } from "./OINODbMariadb.js"
package/README.md DELETED
@@ -1,190 +0,0 @@
1
- # OINO TS
2
- OINO Is Not an ORM but it's trying to solve a similar problem for API development. Instead of mirroring your DB schema in code that needs manual updates, OINO will get the data schema from DBMS using SQL in real time. Every time your app starts, it has an updated data model which enables automatic (de)serialize SQL results to JSON/CSV and back. OINO works on the level below routing where you pass the method, URL ID, body and request parameters to the API-object. OINO will parse and validate the data against the data model and generate proper SQL for your DB. Because OINO knows how data is serialized (e.g. JSON), what column it belongs to (e.g. floating point number) and what the target database is, it knows how to parse, format and escape the value as valid SQL.
3
-
4
- ```
5
- const result:OINOApiResult = await api_orderdetails.doRequest("GET", id, body, params)
6
- return new Response(result.modelset.writeString(OINOContentType.json))
7
- ```
8
-
9
-
10
- # GETTING STARTED
11
-
12
- ### Setup
13
- Install the `@oino-ts/db` npm package and necessary database packages and import them in your code.
14
- ```
15
- bun install @oino-ts/db
16
- bun install @oino-ts/db-bunsqlite
17
- ```
18
-
19
- ```
20
- import { OINODb, OINOApi, OINOFactory } from "@oino-ts/db";
21
- import { OINODbBunSqlite } from "@oino-ts/db-bunsqlite"
22
- ```
23
-
24
- ### Register database and logger
25
- Register your database implementation and logger (see [`OINOConsoleLog`](https://pragmatta.github.io/oino-ts/classes/types_src.OINOConsoleLog.html) how to implement your own)
26
-
27
- ```
28
- OINOLog.setLogger(new OINOConsoleLog())
29
- OINOFactory.registerDb("OINODbBunSqlite", OINODbBunSqlite)
30
- ```
31
-
32
- ### Create a database
33
- Creating a database connection [`OINODb`](https://pragmatta.github.io/oino-ts/classes/db_src.OINODb.html) is done by passing [`OINODbParams`](https://pragmatta.github.io/oino-ts/types/db_src.OINODbParams.html) to the factory method. For [`OINODbBunSqlite`](https://pragmatta.github.io/oino-ts/classes/db_bunsqlite_src.OINODbBunSqlite.html) that means a file url for the database file, for others network host, port, credentials etc.
34
- ```
35
- const db:OINODb = await OINOFactory.createDb( { type: "OINODbBunSqlite", url: "file://../localDb/northwind.sqlite" } )
36
- ```
37
-
38
- ### Create an API
39
- From a database you can create an [`OINOApi`](https://pragmatta.github.io/oino-ts/classes/db_src.OINODbApi.html) by passing [`OINOApiParams`](https://pragmatta.github.io/oino-ts/types/db_src.OINODbApiParams.html) with table name and preferences to the factory method.
40
- ```
41
- const api_employees:OINOApi = await OINOFactory.createApi(db, { tableName: "Employees", excludeFields:["BirthDate"] })
42
- ```
43
-
44
- ### Pass HTTP requests to API
45
- When you receive a HTTP request, just pass the method, URL ID, body and params to the correct API, which will parse and validate input and return results.
46
-
47
- ```
48
- const result:OINOApiResult = await api_orderdetails.doRequest("GET", id, body, params)
49
- ```
50
-
51
- ### Write results back to HTTP Response
52
- The results for a GET request will contain [`OINOModelSet`](https://pragmatta.github.io/oino-ts/classes/db_src.OINODbModelSet.html) data that can be written out as JSON or CSV as needed. For other requests result is just success or error with messages.
53
- ```
54
- return new Response(result.data.writeString(OINOContentType.json))
55
- ```
56
-
57
-
58
- # FEATURES
59
-
60
- ## RESTfull
61
- OINO maps HTTP methods GET/POST/PUT/DELETE to SQL operations SELECT/INSERT/UPDATE/DELETE. The GET/POST requests can be made without URL ID to get all rows or insert new ones and others target a single row using URL ID.
62
-
63
- For example HTTP POST
64
- ```
65
- Request and response:
66
- > curl.exe -X POST http://localhost:3001/orderdetails -H "Content-Type: application/json" --data '[{\"OrderID\":11077,\"ProductID\":99,\"UnitPrice\":19,\"Quantity\":1,\"Discount\":0}]'
67
- {"success":true,"statusCode":200,"statusMessage":"OK","messages":[]}
68
-
69
- SQL:
70
- INSERT INTO [OrderDetails] ("OrderID","ProductID","UnitPrice","Quantity","Discount") VALUES (11077,99,19,1,0);
71
- ```
72
-
73
-
74
- ## Universal Serialization
75
- OINO handles serialization of data to JSON/CSV/etc. and back based on the data model. It knows what columns exist, what is their data type and how to convert each to JSON/CSV and back. This allows also partial data to be sent, i.e. you can send only columns that need updating or even send extra columns and have them ignored.
76
-
77
- ### Features
78
- - Files can be sent to BLOB fields using BASE64 or MIME multipart encoding. Also supports standard HTML form file submission to blob fields and returning them data url images.
79
- - Datetimes are (optionally) normalized to ISO 8601 format.
80
- - Extended JSON-encoding
81
- - Unquoted literal `undefined` can be used to represent non-existent values (leaving property out works too but preserving structure might be easier e.g. when translating data).
82
- - CSV
83
- - Comma-separated, doublequotes.
84
- - Unquoted literal `null` represents null values.
85
- - Unquoted empty string represents undefined values.
86
- - Form data
87
- - Multipart-mixed and binary files not supported.
88
- - Non-existent value line (i.e. nothing after the empty line) treated as a null value.
89
- - Url-encoded
90
- - No null values, missing properties treated as undefined.
91
- - Multiple lines could be used to post multiple rows.
92
-
93
-
94
- ## Database Abstraction
95
- OINO functions as a database abstraction, providing a consistent interface for working with different databases. It abstracts out different conventions in connecting, making queries and formatting data.
96
-
97
- Currently supported databases:
98
- - Bun Sqlite through Bun native implementation
99
- - Postgresql through [pg](https://www.npmjs.com/package/pg)-package
100
- - Mariadb / Mysql-support through [mariadb](https://www.npmjs.com/package/mariadb)-package
101
- - Sql Server through [mssql](https://www.npmjs.com/package/mssql)-package
102
-
103
- ## Composite Keys
104
- To support tables with multipart primary keys OINO generates a composite key `_OINOID_` that is included in the result and can be used as the REST ID. For example in the example above table `OrderDetails` has two primary keys `OrderID` and `ProductID` making the `_OINOID_` of form `11077:99`.
105
-
106
- ## Power Of SQL
107
- Since OINO is just generating SQL, WHERE-conditions can be defined with [`OINOSqlFilter`](https://pragmatta.github.io/oino-ts/classes/db_src.OINODbSqlFilter.html), order with [`OINOSqlOrder`](https://pragmatta.github.io/oino-ts/classes/db_src.OINODbSqlOrder.html) and limits/paging with [`OINOSqlLimit`](https://pragmatta.github.io/oino-ts/classes/db_src.OINODbSqlLimit.html) that are passed as HTTP request parameters. No more API development where you make unique API endpoints for each filter that fetch all data with original API and filter in backend code. Every API can be filtered when and as needed without unnessecary data tranfer and utilizing SQL indexing when available.
108
-
109
- ## Swagger Support
110
- Swagger is great as long as the definitions are updated and with OINO you can automatically get a Swagger definition including a data model schema.
111
- ```
112
- if (url.pathname == "/swagger.json") {
113
- return new Response(JSON.stringify(OINOSwagger.getApiDefinition(api_array)))
114
- }
115
- ```
116
- ![Swagger definition with a data model schema](img/readme-swagger.png)
117
-
118
- ## Node support
119
- OINO is developped Typescript first but compiles to standard CommonJS and the NPM packages should work on either ESM / CommonJS. Checkout sample apps `readmeApp` (ESM) and `nodeApp` (CommonJS).
120
-
121
- ## HTMX support
122
- OINO is [htmx.org](https://htmx.org) friendly, allowing easy translation of [`OINODataRow`](https://pragmatta.github.io/oino-ts/types/db_src.OINODataRow.html) to HTML output using templates (cf. the [htmx sample app](https://github.com/pragmatta/oino-ts/tree/main/samples/htmxApp)).
123
-
124
- ## Hashids
125
- Autoinc numeric id's are very pragmatic and fit well with OINO (e.g. using a form without primary key fields to insert new rows with database assigned ids). However it's not always sensible to share information about the sequence. Hashids solve this by masking the original values by encrypting the ids using AES-128 and some randomness. Length of the hashid can be chosen from 12-32 characters where longer ids provide more security. However this should not be considereded a cryptographic solution for keeping ids secret but rather making it infeasible to iterate all ids.
126
-
127
-
128
- # STATUS
129
- OINO is currently a hobby project which should and should considered in alpha status. That also means compatibility breaking changes can be made without prior notice when architectual issues are discovered.
130
-
131
- ## Beta
132
- For a beta status following milestones are planned:
133
-
134
- ### Realistic app
135
- There needs to be a realistic app built on top of OINO to get a better grasp of the edge cases.
136
-
137
- ## Roadmap
138
- Things that need to happen in some order before beta-status are at least following:
139
-
140
- ### Support for views
141
- Simple cases of views would work already in some databases but edge cases might get complicated. For example
142
- - How to handle a view which does not have a complete private key?
143
- - What edge cases exist in updating views?
144
-
145
- ### Batch updates
146
- Supporting batch updates similar to batch inserts is slightly bending the RESTfull principles but would still be a useful optional feature.
147
-
148
- ### Aggregation
149
- Similar to filtering, ordering and limits, aggregation could be implemented as HTTP request parameters telling what column is aggregated or used for ordering or how many results to return.
150
-
151
- ### Streaming
152
- One core idea is to be efficient in not making unnecessary copies of the data and minimizing garbage collection debt. This can be taken further by implementing streaming, allowing large dataset to be written to HTTP response as SQL result rows are received.
153
-
154
- ### SQL generation callbacks
155
- It would be useful to allow developer to validate / override SQL generation to cover cases OINO does not support or even workaround issues.
156
-
157
- ### Transactions
158
- Even though the basic case for OINO is executing SQL operations on individual rows, having an option to use SQL transactions could make sense at least for batch operations.
159
-
160
-
161
- # HELP
162
-
163
- ## Bug reports
164
- Fixing bugs is a priority and getting good quality bug reports helps. It's recommended to use the sample Northwind database included with project to replicate issues or make an SQL script export of the relevant table.
165
-
166
- ## Feedback
167
- Understanding and prioritizing the use cases for OINO is also important and feedback about how you'd use OINO is interesting. Feel free to raise issues and feature requests in Github, but understand that short term most of the effort goes towards reaching the beta stage.
168
-
169
- ## Typescript / Javascript architecture
170
- Typescript building with different targets and module-systemts and a ton of configuration is a complex domain and something I have little experience un so help in fixing problems and how thing ought to be done is appreciated.
171
-
172
- # LINKS
173
- - [Github repository](https://github.com/pragmatta/oino-ts)
174
- - [NPM repository](https://www.npmjs.com/org/oino-ts)
175
-
176
-
177
- # ACKNOWLEDGEMENTS
178
-
179
- ## Libraries
180
- OINO uses the following open source libraries and npm packages and I would like to thank everyone for their contributions:
181
- - Postgresql [node-postgres package](https://www.npmjs.com/package/pg)
182
- - Mariadb / Mysql [mariadb package](https://www.npmjs.com/package/mariadb)
183
- - Sql Server [mssql package](https://www.npmjs.com/package/mssql)
184
- - Custom base encoding [base-x package](https://www.npmjs.com/package/base-x)
185
-
186
- ## Bun
187
- OINO has been developed using the Bun runtime, not because of the speed improvements but for the first class Typescript support and integrated developper experience. Kudos on the bun team for making Typescript work more exiting again.
188
-
189
- ## SQL Scripts
190
- The SQL scripts for creating the sample Northwind database are based on [Google Code archive](https://code.google.com/archive/p/northwindextended/downloads) and have been further customized to ensure they would have identical data (in the scope of the automated testing).