@oino-ts/db-bunsqlite 0.16.2 → 0.17.1
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.
- package/package.json +36 -36
- package/src/OINODbBunSqlite.ts +347 -347
- package/src/index.ts +1 -1
package/package.json
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@oino-ts/db-bunsqlite",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "OINO TS package for using Bun Sqlite 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
|
-
"sqlite"
|
|
18
|
-
],
|
|
19
|
-
"main": "./dist/cjs/index.js",
|
|
20
|
-
"module": "./dist/esm/index.js",
|
|
21
|
-
"types": "./dist/types/index.d.ts",
|
|
22
|
-
"dependencies": {
|
|
23
|
-
"@oino-ts/db": "^0.
|
|
24
|
-
},
|
|
25
|
-
"devDependencies": {
|
|
26
|
-
"@types/bun": "latest",
|
|
27
|
-
"@types/node": "^20.12.7",
|
|
28
|
-
"typescript": "~5.9.0"
|
|
29
|
-
},
|
|
30
|
-
"files": [
|
|
31
|
-
"src/*.ts",
|
|
32
|
-
"dist/cjs/*.js",
|
|
33
|
-
"dist/esm/*.js",
|
|
34
|
-
"dist/types/*.d.ts"
|
|
35
|
-
]
|
|
36
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@oino-ts/db-bunsqlite",
|
|
3
|
+
"version": "0.17.1",
|
|
4
|
+
"description": "OINO TS package for using Bun Sqlite 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
|
+
"sqlite"
|
|
18
|
+
],
|
|
19
|
+
"main": "./dist/cjs/index.js",
|
|
20
|
+
"module": "./dist/esm/index.js",
|
|
21
|
+
"types": "./dist/types/index.d.ts",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@oino-ts/db": "^0.17.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/bun": "latest",
|
|
27
|
+
"@types/node": "^20.12.7",
|
|
28
|
+
"typescript": "~5.9.0"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src/*.ts",
|
|
32
|
+
"dist/cjs/*.js",
|
|
33
|
+
"dist/esm/*.js",
|
|
34
|
+
"dist/types/*.d.ts"
|
|
35
|
+
]
|
|
36
|
+
}
|
package/src/OINODbBunSqlite.ts
CHANGED
|
@@ -1,347 +1,347 @@
|
|
|
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, OINODbMemoryDataSet, OINODataCell, OINOBenchmark, OINOBlobDataField, OINODatetimeDataField, OINOStr, OINOLog, OINOResult, OINODB_EMPTY_ROWS } from "@oino-ts/db";
|
|
8
|
-
|
|
9
|
-
import { Database as BunSqliteDb } from "bun:sqlite";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Implmentation of OINODbDataSet for BunSqlite.
|
|
13
|
-
*
|
|
14
|
-
*/
|
|
15
|
-
class OINOBunSqliteDataset extends OINODbMemoryDataSet {
|
|
16
|
-
constructor(data: unknown, messages:string[]=[]) {
|
|
17
|
-
super(data, messages)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Implementation of BunSqlite-database.
|
|
23
|
-
*
|
|
24
|
-
*/
|
|
25
|
-
export class OINODbBunSqlite extends OINODb {
|
|
26
|
-
private static _tableDescriptionRegex = /^CREATE TABLE\s*[\"\[]?\w+[\"\]]?\s*\(\s*(.*)\s*\)\s*(WITHOUT ROWID)?$/msi
|
|
27
|
-
private static _tablePrimarykeyRegex = /PRIMARY KEY \(([^\)]+)\)/i
|
|
28
|
-
private static _tableForeignkeyRegex = /FOREIGN KEY \(\[([^\)]+)\]\)/i
|
|
29
|
-
private static _tableFieldTypeRegex = /[\"\[\s]?(\w+)[\"\]\s]\s?(INTEGER|REAL|DOUBLE|NUMERIC|DECIMAL|TEXT|BLOB|VARCHAR|DATETIME|DATE|BOOLEAN)(\s?\((\d+)\s?\,?\s?(\d*)?\))?/i
|
|
30
|
-
|
|
31
|
-
private _db:BunSqliteDb|null
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* OINODbBunSqlite constructor
|
|
35
|
-
* @param params database parameters
|
|
36
|
-
*/
|
|
37
|
-
constructor(params:OINODbParams) {
|
|
38
|
-
super(params)
|
|
39
|
-
this._db = null
|
|
40
|
-
if (!this._params.url.startsWith("file://")) {
|
|
41
|
-
throw new Error(OINO_ERROR_PREFIX + ": OINODbBunSqlite url must be a file://-url!")
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (this._params.type !== "OINODbBunSqlite") {
|
|
45
|
-
throw new Error(OINO_ERROR_PREFIX + ": Not OINODbBunSqlite-type: " + this._params.type)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private _parseDbFieldParams(fieldStr:string): OINODbDataFieldParams {
|
|
50
|
-
const result:OINODbDataFieldParams = {
|
|
51
|
-
isPrimaryKey: fieldStr.indexOf("PRIMARY KEY") >= 0,
|
|
52
|
-
isForeignKey: false,
|
|
53
|
-
isAutoInc: fieldStr.indexOf("AUTOINCREMENT") >= 0,
|
|
54
|
-
isNotNull: fieldStr.indexOf("NOT NULL") >= 0
|
|
55
|
-
}
|
|
56
|
-
return result
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Print a table name using database specific SQL escaping.
|
|
61
|
-
*
|
|
62
|
-
* @param sqlTable name of the table
|
|
63
|
-
*
|
|
64
|
-
*/
|
|
65
|
-
printSqlTablename(sqlTable:string): string {
|
|
66
|
-
return "["+sqlTable+"]"
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Print a column name with correct SQL escaping.
|
|
71
|
-
*
|
|
72
|
-
* @param sqlColumn name of the column
|
|
73
|
-
*
|
|
74
|
-
*/
|
|
75
|
-
printSqlColumnname(sqlColumn:string): string {
|
|
76
|
-
return "\""+sqlColumn+"\""
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Print a single data value from serialization using the context of the native data
|
|
81
|
-
* type with the correct SQL escaping.
|
|
82
|
-
*
|
|
83
|
-
* @param cellValue data from sql results
|
|
84
|
-
* @param sqlType native type name for table column
|
|
85
|
-
*
|
|
86
|
-
*/
|
|
87
|
-
printCellAsSqlValue(cellValue:OINODataCell, sqlType: string): string {
|
|
88
|
-
if (cellValue === null) {
|
|
89
|
-
return "NULL"
|
|
90
|
-
|
|
91
|
-
} else if (cellValue === undefined) {
|
|
92
|
-
return "UNDEFINED"
|
|
93
|
-
|
|
94
|
-
} else if ((sqlType == "INTEGER") || (sqlType == "REAL") || (sqlType == "DOUBLE" || (sqlType == "NUMERIC") || (sqlType == "DECIMAL"))) {
|
|
95
|
-
return cellValue.toString()
|
|
96
|
-
|
|
97
|
-
} else if (sqlType == "BLOB") {
|
|
98
|
-
if (cellValue instanceof Buffer) {
|
|
99
|
-
return "X'" + (cellValue as Buffer).toString("hex") + "'"
|
|
100
|
-
} else if (cellValue instanceof Uint8Array) {
|
|
101
|
-
return "X'" + Buffer.from(cellValue as Uint8Array).toString("hex") + "'"
|
|
102
|
-
} else {
|
|
103
|
-
return "'" + cellValue?.toString() + "'"
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
} else if (((sqlType == "DATETIME") || (sqlType == "DATE")) && (cellValue instanceof Date)) {
|
|
107
|
-
return "\'" + cellValue.toISOString() + "\'"
|
|
108
|
-
|
|
109
|
-
} else {
|
|
110
|
-
return this.printSqlString(cellValue.toString())
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Print a single string value as valid sql literal
|
|
116
|
-
*
|
|
117
|
-
* @param sqlString string value
|
|
118
|
-
*
|
|
119
|
-
*/
|
|
120
|
-
printSqlString(sqlString:string): string {
|
|
121
|
-
return "\"" + sqlString.replaceAll("\"", "\"\"") + "\""
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Parse a single SQL result value for serialization using the context of the native data
|
|
126
|
-
* type.
|
|
127
|
-
*
|
|
128
|
-
* @param sqlValue data from serialization
|
|
129
|
-
* @param sqlType native type name for table column
|
|
130
|
-
*
|
|
131
|
-
*/
|
|
132
|
-
parseSqlValueAsCell(sqlValue:OINODataCell, sqlType: string): OINODataCell {
|
|
133
|
-
if ((sqlValue === null) || (sqlValue == "NULL")) {
|
|
134
|
-
return null
|
|
135
|
-
|
|
136
|
-
} else if (sqlValue === undefined) {
|
|
137
|
-
return undefined
|
|
138
|
-
|
|
139
|
-
} else if (((sqlType == "DATETIME") || (sqlType == "DATE")) && (typeof(sqlValue) == "string") && (sqlValue != "")) {
|
|
140
|
-
return new Date(sqlValue)
|
|
141
|
-
|
|
142
|
-
} else if ((sqlType == "BOOLEAN")) {
|
|
143
|
-
return sqlValue == 1
|
|
144
|
-
|
|
145
|
-
} else if ((sqlType == "BLOB")) {
|
|
146
|
-
if (sqlValue instanceof Uint8Array) {
|
|
147
|
-
return Buffer.from(sqlValue)
|
|
148
|
-
} else {
|
|
149
|
-
return sqlValue
|
|
150
|
-
}
|
|
151
|
-
} else {
|
|
152
|
-
return sqlValue
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Connect to database.
|
|
160
|
-
*
|
|
161
|
-
*/
|
|
162
|
-
async connect(): Promise<OINOResult> {
|
|
163
|
-
OINOBenchmark.startMetric("OINODb", "connect")
|
|
164
|
-
let result:OINOResult = new OINOResult()
|
|
165
|
-
const filepath:string = this._params.url.substring(7)
|
|
166
|
-
try {
|
|
167
|
-
this._db = BunSqliteDb.open(filepath, { create: true, readonly: false, readwrite: true })
|
|
168
|
-
this.isConnected = true
|
|
169
|
-
} catch (e:any) {
|
|
170
|
-
result.setError(500, "Exception connecting to database: " + e.message, "OINODbBunSqlite.connect")
|
|
171
|
-
OINOLog.exception("@oino-ts/db-bunsqlite", "OINODbBunSqlite", "connect", "exception in connect", {message:e.message, stack:e.stack})
|
|
172
|
-
}
|
|
173
|
-
OINOBenchmark.endMetric("OINODb", "connect")
|
|
174
|
-
return result
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Validate connection to database is working.
|
|
179
|
-
*
|
|
180
|
-
*/
|
|
181
|
-
async validate(): Promise<OINOResult> {
|
|
182
|
-
OINOBenchmark.startMetric("OINODb", "validate")
|
|
183
|
-
let result:OINOResult = new OINOResult()
|
|
184
|
-
try {
|
|
185
|
-
const sql = this._getValidateSql(this._params.database)
|
|
186
|
-
const sql_res:OINODbDataSet = await this.sqlSelect(sql)
|
|
187
|
-
if (sql_res.isEmpty()) {
|
|
188
|
-
result.setError(400, "DB returned no rows for select!", "OINODbBunSqlite.validate")
|
|
189
|
-
|
|
190
|
-
} else if (sql_res.getRow().length == 0) {
|
|
191
|
-
result.setError(400, "DB returned no values for database!", "OINODbBunSqlite.validate")
|
|
192
|
-
|
|
193
|
-
} else if (sql_res.getRow()[0] == "0") {
|
|
194
|
-
result.setError(400, "DB returned no schema for database!", "OINODbBunSqlite.validate")
|
|
195
|
-
|
|
196
|
-
} else {
|
|
197
|
-
this.isValidated = true
|
|
198
|
-
}
|
|
199
|
-
} catch (e:any) {
|
|
200
|
-
result.setError(500, OINO_ERROR_PREFIX + " (validate): OINODbBunSqlite.validate exception in _db.query: " + e.message, "OINODbBunSqlite.validate")
|
|
201
|
-
}
|
|
202
|
-
OINOBenchmark.endMetric("OINODb", "validate")
|
|
203
|
-
return result
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Execute a select operation.
|
|
208
|
-
*
|
|
209
|
-
* @param sql SQL statement.
|
|
210
|
-
*
|
|
211
|
-
*/
|
|
212
|
-
async sqlSelect(sql:string): Promise<OINODbDataSet> {
|
|
213
|
-
OINOBenchmark.startMetric("OINODb", "sqlSelect")
|
|
214
|
-
let result:OINODbDataSet
|
|
215
|
-
try {
|
|
216
|
-
result = new OINOBunSqliteDataset(this._db?.query(sql).values(), [])
|
|
217
|
-
|
|
218
|
-
} catch (e:any) {
|
|
219
|
-
result = new OINOBunSqliteDataset(OINODB_EMPTY_ROWS, ["OINODbBunSqlite.sqlSelect exception in _db.query: " + e.message])
|
|
220
|
-
}
|
|
221
|
-
OINOBenchmark.endMetric("OINODb", "sqlSelect")
|
|
222
|
-
return Promise.resolve(result)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Execute other sql operations.
|
|
227
|
-
*
|
|
228
|
-
* @param sql SQL statement.
|
|
229
|
-
*
|
|
230
|
-
*/
|
|
231
|
-
async sqlExec(sql:string): Promise<OINODbDataSet> {
|
|
232
|
-
OINOBenchmark.startMetric("OINODb", "sqlExec")
|
|
233
|
-
let result:OINODbDataSet
|
|
234
|
-
try {
|
|
235
|
-
this._db?.exec(sql)
|
|
236
|
-
result = new OINOBunSqliteDataset(OINODB_EMPTY_ROWS, [])
|
|
237
|
-
|
|
238
|
-
} catch (e:any) {
|
|
239
|
-
result = new OINOBunSqliteDataset(OINODB_EMPTY_ROWS, [OINO_ERROR_PREFIX + "(sqlExec): exception in _db.exec [" + e.message + "]"])
|
|
240
|
-
}
|
|
241
|
-
OINOBenchmark.endMetric("OINODb", "sqlExec")
|
|
242
|
-
return Promise.resolve(result)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
private _getSchemaSql(dbName:string, tableName:string):string {
|
|
246
|
-
const sql = "SELECT sql from sqlite_schema WHERE name='" + tableName + "'"
|
|
247
|
-
return sql
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
private _getValidateSql(dbName:string):string {
|
|
251
|
-
const sql = "SELECT count(*) as COLUMN_COUNT from sqlite_schema"
|
|
252
|
-
return sql
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Initialize a data model by getting the SQL schema and populating OINODbDataFields of
|
|
257
|
-
* the model.
|
|
258
|
-
*
|
|
259
|
-
* @param api api which data model to initialize.
|
|
260
|
-
*
|
|
261
|
-
*/
|
|
262
|
-
async initializeApiDatamodel(api:OINODbApi): Promise<void> {
|
|
263
|
-
const schema_sql:string = this._getSchemaSql(this._params.database, api.params.tableName)
|
|
264
|
-
const res:OINODbDataSet|null = await this.sqlSelect(schema_sql)
|
|
265
|
-
const sql_desc:string = (res?.getRow()[0]) as string
|
|
266
|
-
const excluded_fields:string[] = []
|
|
267
|
-
let table_matches = OINODbBunSqlite._tableDescriptionRegex.exec(sql_desc)
|
|
268
|
-
if (!table_matches || table_matches?.length < 2) {
|
|
269
|
-
throw new Error("Table " + api.params.tableName + " not recognized as a valid Sqlite table!")
|
|
270
|
-
|
|
271
|
-
} else {
|
|
272
|
-
let field_strings:string[] = OINOStr.splitExcludingBrackets(table_matches[1], ',', '(', ')')
|
|
273
|
-
for (let field_str of field_strings) {
|
|
274
|
-
field_str = field_str.trim()
|
|
275
|
-
let field_params = this._parseDbFieldParams(field_str)
|
|
276
|
-
let field_match = OINODbBunSqlite._tableFieldTypeRegex.exec(field_str)
|
|
277
|
-
// console.log("OINODbBunSqlite.initializeApiDatamodel: field_match", field_match)
|
|
278
|
-
if ((!field_match) || (field_match.length < 3)) {
|
|
279
|
-
let primarykey_match = OINODbBunSqlite._tablePrimarykeyRegex.exec(field_str)
|
|
280
|
-
let foreignkey_match = OINODbBunSqlite._tableForeignkeyRegex.exec(field_str)
|
|
281
|
-
if (primarykey_match && primarykey_match.length >= 2) {
|
|
282
|
-
const primary_keys:string[] = primarykey_match[1].replaceAll("\"", "").split(',') // not sure if will have space or not so split by comma and trim later
|
|
283
|
-
for (let i:number=0; i<primary_keys.length; i++) {
|
|
284
|
-
const pk:string = primary_keys[i].trim() //..the trim
|
|
285
|
-
if (excluded_fields.indexOf(pk) >= 0) {
|
|
286
|
-
throw new Error(OINO_ERROR_PREFIX + "Primary key field excluded in API parameters: " + pk)
|
|
287
|
-
}
|
|
288
|
-
for (let j:number=0; j<api.datamodel.fields.length; j++) {
|
|
289
|
-
if (api.datamodel.fields[j].name == pk) {
|
|
290
|
-
api.datamodel.fields[j].fieldParams.isPrimaryKey = true
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
} else if (foreignkey_match && foreignkey_match.length >= 2) {
|
|
296
|
-
const fk:string = foreignkey_match[1].trim()
|
|
297
|
-
for (let j:number=0; j<api.datamodel.fields.length; j++) {
|
|
298
|
-
if (api.datamodel.fields[j].name == fk) {
|
|
299
|
-
api.datamodel.fields[j].fieldParams.isForeignKey = true
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
} else {
|
|
304
|
-
OINOLog.info("@oino-ts/db-bunsqlite", "OINODbBunSqlite", "initializeApiDatamodel", "Unsupported field definition skipped.", { field: field_str })
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
} else {
|
|
308
|
-
// field_str = "NAME TYPE (M, N)" -> 1:NAME, 2:TYPE, 4:M, 5:N
|
|
309
|
-
const field_name:string = field_match[1]
|
|
310
|
-
const sql_type:string = field_match[2]
|
|
311
|
-
const field_length:number = parseInt(field_match[4]) || 0
|
|
312
|
-
if (api.isFieldIncluded(field_name) == false) {
|
|
313
|
-
excluded_fields.push(field_name)
|
|
314
|
-
OINOLog.info("@oino-ts/db-bunsqlite", "OINODbBunSqlite", "initializeApiDatamodel", "Field excluded in API parameters.", {field:field_name})
|
|
315
|
-
|
|
316
|
-
} else {
|
|
317
|
-
if ((sql_type == "INTEGER") || (sql_type == "REAL") || (sql_type == "DOUBLE") || (sql_type == "NUMERIC") || (sql_type == "DECIMAL")) {
|
|
318
|
-
api.datamodel.addField(new OINONumberDataField(this, field_name, sql_type, field_params ))
|
|
319
|
-
|
|
320
|
-
} else if ((sql_type == "BLOB") ) {
|
|
321
|
-
api.datamodel.addField(new OINOBlobDataField(this, field_name, sql_type, field_params, field_length))
|
|
322
|
-
|
|
323
|
-
} else if ((sql_type == "TEXT")) {
|
|
324
|
-
api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, field_length))
|
|
325
|
-
|
|
326
|
-
} else if ((sql_type == "DATETIME") || (sql_type == "DATE")) {
|
|
327
|
-
if (api.params.useDatesAsString) {
|
|
328
|
-
api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
|
|
329
|
-
} else {
|
|
330
|
-
api.datamodel.addField(new OINODatetimeDataField(this, field_name, sql_type, field_params))
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
} else if ((sql_type == "BOOLEAN")) {
|
|
334
|
-
api.datamodel.addField(new OINOBooleanDataField(this, field_name, sql_type, field_params))
|
|
335
|
-
|
|
336
|
-
} else {
|
|
337
|
-
OINOLog.info("@oino-ts/db-bunsqlite", "OINODbBunSqlite", "initializeApiDatamodel", "Unrecognized field type treated as string", {field_name: field_name, sql_type:sql_type, field_length:field_length, field_params:field_params })
|
|
338
|
-
api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
OINOLog.info("@oino-ts/db-bunsqlite", "OINODbBunSqlite", "initializeApiDatamodel", "\n" + api.datamodel.printDebug("\n"))
|
|
344
|
-
return Promise.resolve()
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
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, OINODbMemoryDataSet, OINODataCell, OINOBenchmark, OINOBlobDataField, OINODatetimeDataField, OINOStr, OINOLog, OINOResult, OINODB_EMPTY_ROWS } from "@oino-ts/db";
|
|
8
|
+
|
|
9
|
+
import { Database as BunSqliteDb } from "bun:sqlite";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Implmentation of OINODbDataSet for BunSqlite.
|
|
13
|
+
*
|
|
14
|
+
*/
|
|
15
|
+
class OINOBunSqliteDataset extends OINODbMemoryDataSet {
|
|
16
|
+
constructor(data: unknown, messages:string[]=[]) {
|
|
17
|
+
super(data, messages)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Implementation of BunSqlite-database.
|
|
23
|
+
*
|
|
24
|
+
*/
|
|
25
|
+
export class OINODbBunSqlite extends OINODb {
|
|
26
|
+
private static _tableDescriptionRegex = /^CREATE TABLE\s*[\"\[]?\w+[\"\]]?\s*\(\s*(.*)\s*\)\s*(WITHOUT ROWID)?$/msi
|
|
27
|
+
private static _tablePrimarykeyRegex = /PRIMARY KEY \(([^\)]+)\)/i
|
|
28
|
+
private static _tableForeignkeyRegex = /FOREIGN KEY \(\[([^\)]+)\]\)/i
|
|
29
|
+
private static _tableFieldTypeRegex = /[\"\[\s]?(\w+)[\"\]\s]\s?(INTEGER|REAL|DOUBLE|NUMERIC|DECIMAL|TEXT|BLOB|VARCHAR|DATETIME|DATE|BOOLEAN)(\s?\((\d+)\s?\,?\s?(\d*)?\))?/i
|
|
30
|
+
|
|
31
|
+
private _db:BunSqliteDb|null
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* OINODbBunSqlite constructor
|
|
35
|
+
* @param params database parameters
|
|
36
|
+
*/
|
|
37
|
+
constructor(params:OINODbParams) {
|
|
38
|
+
super(params)
|
|
39
|
+
this._db = null
|
|
40
|
+
if (!this._params.url.startsWith("file://")) {
|
|
41
|
+
throw new Error(OINO_ERROR_PREFIX + ": OINODbBunSqlite url must be a file://-url!")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (this._params.type !== "OINODbBunSqlite") {
|
|
45
|
+
throw new Error(OINO_ERROR_PREFIX + ": Not OINODbBunSqlite-type: " + this._params.type)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private _parseDbFieldParams(fieldStr:string): OINODbDataFieldParams {
|
|
50
|
+
const result:OINODbDataFieldParams = {
|
|
51
|
+
isPrimaryKey: fieldStr.indexOf("PRIMARY KEY") >= 0,
|
|
52
|
+
isForeignKey: false,
|
|
53
|
+
isAutoInc: fieldStr.indexOf("AUTOINCREMENT") >= 0,
|
|
54
|
+
isNotNull: fieldStr.indexOf("NOT NULL") >= 0
|
|
55
|
+
}
|
|
56
|
+
return result
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Print a table name using database specific SQL escaping.
|
|
61
|
+
*
|
|
62
|
+
* @param sqlTable name of the table
|
|
63
|
+
*
|
|
64
|
+
*/
|
|
65
|
+
printSqlTablename(sqlTable:string): string {
|
|
66
|
+
return "["+sqlTable+"]"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Print a column name with correct SQL escaping.
|
|
71
|
+
*
|
|
72
|
+
* @param sqlColumn name of the column
|
|
73
|
+
*
|
|
74
|
+
*/
|
|
75
|
+
printSqlColumnname(sqlColumn:string): string {
|
|
76
|
+
return "\""+sqlColumn+"\""
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Print a single data value from serialization using the context of the native data
|
|
81
|
+
* type with the correct SQL escaping.
|
|
82
|
+
*
|
|
83
|
+
* @param cellValue data from sql results
|
|
84
|
+
* @param sqlType native type name for table column
|
|
85
|
+
*
|
|
86
|
+
*/
|
|
87
|
+
printCellAsSqlValue(cellValue:OINODataCell, sqlType: string): string {
|
|
88
|
+
if (cellValue === null) {
|
|
89
|
+
return "NULL"
|
|
90
|
+
|
|
91
|
+
} else if (cellValue === undefined) {
|
|
92
|
+
return "UNDEFINED"
|
|
93
|
+
|
|
94
|
+
} else if ((sqlType == "INTEGER") || (sqlType == "REAL") || (sqlType == "DOUBLE" || (sqlType == "NUMERIC") || (sqlType == "DECIMAL"))) {
|
|
95
|
+
return cellValue.toString()
|
|
96
|
+
|
|
97
|
+
} else if (sqlType == "BLOB") {
|
|
98
|
+
if (cellValue instanceof Buffer) {
|
|
99
|
+
return "X'" + (cellValue as Buffer).toString("hex") + "'"
|
|
100
|
+
} else if (cellValue instanceof Uint8Array) {
|
|
101
|
+
return "X'" + Buffer.from(cellValue as Uint8Array).toString("hex") + "'"
|
|
102
|
+
} else {
|
|
103
|
+
return "'" + cellValue?.toString() + "'"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
} else if (((sqlType == "DATETIME") || (sqlType == "DATE")) && (cellValue instanceof Date)) {
|
|
107
|
+
return "\'" + cellValue.toISOString() + "\'"
|
|
108
|
+
|
|
109
|
+
} else {
|
|
110
|
+
return this.printSqlString(cellValue.toString())
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Print a single string value as valid sql literal
|
|
116
|
+
*
|
|
117
|
+
* @param sqlString string value
|
|
118
|
+
*
|
|
119
|
+
*/
|
|
120
|
+
printSqlString(sqlString:string): string {
|
|
121
|
+
return "\"" + sqlString.replaceAll("\"", "\"\"") + "\""
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parse a single SQL result value for serialization using the context of the native data
|
|
126
|
+
* type.
|
|
127
|
+
*
|
|
128
|
+
* @param sqlValue data from serialization
|
|
129
|
+
* @param sqlType native type name for table column
|
|
130
|
+
*
|
|
131
|
+
*/
|
|
132
|
+
parseSqlValueAsCell(sqlValue:OINODataCell, sqlType: string): OINODataCell {
|
|
133
|
+
if ((sqlValue === null) || (sqlValue == "NULL")) {
|
|
134
|
+
return null
|
|
135
|
+
|
|
136
|
+
} else if (sqlValue === undefined) {
|
|
137
|
+
return undefined
|
|
138
|
+
|
|
139
|
+
} else if (((sqlType == "DATETIME") || (sqlType == "DATE")) && (typeof(sqlValue) == "string") && (sqlValue != "")) {
|
|
140
|
+
return new Date(sqlValue)
|
|
141
|
+
|
|
142
|
+
} else if ((sqlType == "BOOLEAN")) {
|
|
143
|
+
return sqlValue == 1
|
|
144
|
+
|
|
145
|
+
} else if ((sqlType == "BLOB")) {
|
|
146
|
+
if (sqlValue instanceof Uint8Array) {
|
|
147
|
+
return Buffer.from(sqlValue)
|
|
148
|
+
} else {
|
|
149
|
+
return sqlValue
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
return sqlValue
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Connect to database.
|
|
160
|
+
*
|
|
161
|
+
*/
|
|
162
|
+
async connect(): Promise<OINOResult> {
|
|
163
|
+
OINOBenchmark.startMetric("OINODb", "connect")
|
|
164
|
+
let result:OINOResult = new OINOResult()
|
|
165
|
+
const filepath:string = this._params.url.substring(7)
|
|
166
|
+
try {
|
|
167
|
+
this._db = BunSqliteDb.open(filepath, { create: true, readonly: false, readwrite: true })
|
|
168
|
+
this.isConnected = true
|
|
169
|
+
} catch (e:any) {
|
|
170
|
+
result.setError(500, "Exception connecting to database: " + e.message, "OINODbBunSqlite.connect")
|
|
171
|
+
OINOLog.exception("@oino-ts/db-bunsqlite", "OINODbBunSqlite", "connect", "exception in connect", {message:e.message, stack:e.stack})
|
|
172
|
+
}
|
|
173
|
+
OINOBenchmark.endMetric("OINODb", "connect")
|
|
174
|
+
return result
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Validate connection to database is working.
|
|
179
|
+
*
|
|
180
|
+
*/
|
|
181
|
+
async validate(): Promise<OINOResult> {
|
|
182
|
+
OINOBenchmark.startMetric("OINODb", "validate")
|
|
183
|
+
let result:OINOResult = new OINOResult()
|
|
184
|
+
try {
|
|
185
|
+
const sql = this._getValidateSql(this._params.database)
|
|
186
|
+
const sql_res:OINODbDataSet = await this.sqlSelect(sql)
|
|
187
|
+
if (sql_res.isEmpty()) {
|
|
188
|
+
result.setError(400, "DB returned no rows for select!", "OINODbBunSqlite.validate")
|
|
189
|
+
|
|
190
|
+
} else if (sql_res.getRow().length == 0) {
|
|
191
|
+
result.setError(400, "DB returned no values for database!", "OINODbBunSqlite.validate")
|
|
192
|
+
|
|
193
|
+
} else if (sql_res.getRow()[0] == "0") {
|
|
194
|
+
result.setError(400, "DB returned no schema for database!", "OINODbBunSqlite.validate")
|
|
195
|
+
|
|
196
|
+
} else {
|
|
197
|
+
this.isValidated = true
|
|
198
|
+
}
|
|
199
|
+
} catch (e:any) {
|
|
200
|
+
result.setError(500, OINO_ERROR_PREFIX + " (validate): OINODbBunSqlite.validate exception in _db.query: " + e.message, "OINODbBunSqlite.validate")
|
|
201
|
+
}
|
|
202
|
+
OINOBenchmark.endMetric("OINODb", "validate")
|
|
203
|
+
return result
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Execute a select operation.
|
|
208
|
+
*
|
|
209
|
+
* @param sql SQL statement.
|
|
210
|
+
*
|
|
211
|
+
*/
|
|
212
|
+
async sqlSelect(sql:string): Promise<OINODbDataSet> {
|
|
213
|
+
OINOBenchmark.startMetric("OINODb", "sqlSelect")
|
|
214
|
+
let result:OINODbDataSet
|
|
215
|
+
try {
|
|
216
|
+
result = new OINOBunSqliteDataset(this._db?.query(sql).values(), [])
|
|
217
|
+
|
|
218
|
+
} catch (e:any) {
|
|
219
|
+
result = new OINOBunSqliteDataset(OINODB_EMPTY_ROWS, ["OINODbBunSqlite.sqlSelect exception in _db.query: " + e.message])
|
|
220
|
+
}
|
|
221
|
+
OINOBenchmark.endMetric("OINODb", "sqlSelect")
|
|
222
|
+
return Promise.resolve(result)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Execute other sql operations.
|
|
227
|
+
*
|
|
228
|
+
* @param sql SQL statement.
|
|
229
|
+
*
|
|
230
|
+
*/
|
|
231
|
+
async sqlExec(sql:string): Promise<OINODbDataSet> {
|
|
232
|
+
OINOBenchmark.startMetric("OINODb", "sqlExec")
|
|
233
|
+
let result:OINODbDataSet
|
|
234
|
+
try {
|
|
235
|
+
this._db?.exec(sql)
|
|
236
|
+
result = new OINOBunSqliteDataset(OINODB_EMPTY_ROWS, [])
|
|
237
|
+
|
|
238
|
+
} catch (e:any) {
|
|
239
|
+
result = new OINOBunSqliteDataset(OINODB_EMPTY_ROWS, [OINO_ERROR_PREFIX + "(sqlExec): exception in _db.exec [" + e.message + "]"])
|
|
240
|
+
}
|
|
241
|
+
OINOBenchmark.endMetric("OINODb", "sqlExec")
|
|
242
|
+
return Promise.resolve(result)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private _getSchemaSql(dbName:string, tableName:string):string {
|
|
246
|
+
const sql = "SELECT sql from sqlite_schema WHERE name='" + tableName + "'"
|
|
247
|
+
return sql
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private _getValidateSql(dbName:string):string {
|
|
251
|
+
const sql = "SELECT count(*) as COLUMN_COUNT from sqlite_schema"
|
|
252
|
+
return sql
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Initialize a data model by getting the SQL schema and populating OINODbDataFields of
|
|
257
|
+
* the model.
|
|
258
|
+
*
|
|
259
|
+
* @param api api which data model to initialize.
|
|
260
|
+
*
|
|
261
|
+
*/
|
|
262
|
+
async initializeApiDatamodel(api:OINODbApi): Promise<void> {
|
|
263
|
+
const schema_sql:string = this._getSchemaSql(this._params.database, api.params.tableName)
|
|
264
|
+
const res:OINODbDataSet|null = await this.sqlSelect(schema_sql)
|
|
265
|
+
const sql_desc:string = (res?.getRow()[0]) as string
|
|
266
|
+
const excluded_fields:string[] = []
|
|
267
|
+
let table_matches = OINODbBunSqlite._tableDescriptionRegex.exec(sql_desc)
|
|
268
|
+
if (!table_matches || table_matches?.length < 2) {
|
|
269
|
+
throw new Error("Table " + api.params.tableName + " not recognized as a valid Sqlite table!")
|
|
270
|
+
|
|
271
|
+
} else {
|
|
272
|
+
let field_strings:string[] = OINOStr.splitExcludingBrackets(table_matches[1], ',', '(', ')')
|
|
273
|
+
for (let field_str of field_strings) {
|
|
274
|
+
field_str = field_str.trim()
|
|
275
|
+
let field_params = this._parseDbFieldParams(field_str)
|
|
276
|
+
let field_match = OINODbBunSqlite._tableFieldTypeRegex.exec(field_str)
|
|
277
|
+
// console.log("OINODbBunSqlite.initializeApiDatamodel: field_match", field_match)
|
|
278
|
+
if ((!field_match) || (field_match.length < 3)) {
|
|
279
|
+
let primarykey_match = OINODbBunSqlite._tablePrimarykeyRegex.exec(field_str)
|
|
280
|
+
let foreignkey_match = OINODbBunSqlite._tableForeignkeyRegex.exec(field_str)
|
|
281
|
+
if (primarykey_match && primarykey_match.length >= 2) {
|
|
282
|
+
const primary_keys:string[] = primarykey_match[1].replaceAll("\"", "").split(',') // not sure if will have space or not so split by comma and trim later
|
|
283
|
+
for (let i:number=0; i<primary_keys.length; i++) {
|
|
284
|
+
const pk:string = primary_keys[i].trim() //..the trim
|
|
285
|
+
if (excluded_fields.indexOf(pk) >= 0) {
|
|
286
|
+
throw new Error(OINO_ERROR_PREFIX + "Primary key field excluded in API parameters: " + pk)
|
|
287
|
+
}
|
|
288
|
+
for (let j:number=0; j<api.datamodel.fields.length; j++) {
|
|
289
|
+
if (api.datamodel.fields[j].name == pk) {
|
|
290
|
+
api.datamodel.fields[j].fieldParams.isPrimaryKey = true
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
} else if (foreignkey_match && foreignkey_match.length >= 2) {
|
|
296
|
+
const fk:string = foreignkey_match[1].trim()
|
|
297
|
+
for (let j:number=0; j<api.datamodel.fields.length; j++) {
|
|
298
|
+
if (api.datamodel.fields[j].name == fk) {
|
|
299
|
+
api.datamodel.fields[j].fieldParams.isForeignKey = true
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
} else {
|
|
304
|
+
OINOLog.info("@oino-ts/db-bunsqlite", "OINODbBunSqlite", "initializeApiDatamodel", "Unsupported field definition skipped.", { field: field_str })
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
} else {
|
|
308
|
+
// field_str = "NAME TYPE (M, N)" -> 1:NAME, 2:TYPE, 4:M, 5:N
|
|
309
|
+
const field_name:string = field_match[1]
|
|
310
|
+
const sql_type:string = field_match[2]
|
|
311
|
+
const field_length:number = parseInt(field_match[4]) || 0
|
|
312
|
+
if (api.isFieldIncluded(field_name) == false) {
|
|
313
|
+
excluded_fields.push(field_name)
|
|
314
|
+
OINOLog.info("@oino-ts/db-bunsqlite", "OINODbBunSqlite", "initializeApiDatamodel", "Field excluded in API parameters.", {field:field_name})
|
|
315
|
+
|
|
316
|
+
} else {
|
|
317
|
+
if ((sql_type == "INTEGER") || (sql_type == "REAL") || (sql_type == "DOUBLE") || (sql_type == "NUMERIC") || (sql_type == "DECIMAL")) {
|
|
318
|
+
api.datamodel.addField(new OINONumberDataField(this, field_name, sql_type, field_params ))
|
|
319
|
+
|
|
320
|
+
} else if ((sql_type == "BLOB") ) {
|
|
321
|
+
api.datamodel.addField(new OINOBlobDataField(this, field_name, sql_type, field_params, field_length))
|
|
322
|
+
|
|
323
|
+
} else if ((sql_type == "TEXT")) {
|
|
324
|
+
api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, field_length))
|
|
325
|
+
|
|
326
|
+
} else if ((sql_type == "DATETIME") || (sql_type == "DATE")) {
|
|
327
|
+
if (api.params.useDatesAsString) {
|
|
328
|
+
api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
|
|
329
|
+
} else {
|
|
330
|
+
api.datamodel.addField(new OINODatetimeDataField(this, field_name, sql_type, field_params))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
} else if ((sql_type == "BOOLEAN")) {
|
|
334
|
+
api.datamodel.addField(new OINOBooleanDataField(this, field_name, sql_type, field_params))
|
|
335
|
+
|
|
336
|
+
} else {
|
|
337
|
+
OINOLog.info("@oino-ts/db-bunsqlite", "OINODbBunSqlite", "initializeApiDatamodel", "Unrecognized field type treated as string", {field_name: field_name, sql_type:sql_type, field_length:field_length, field_params:field_params })
|
|
338
|
+
api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
OINOLog.info("@oino-ts/db-bunsqlite", "OINODbBunSqlite", "initializeApiDatamodel", "\n" + api.datamodel.printDebug("\n"))
|
|
344
|
+
return Promise.resolve()
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { OINODbBunSqlite } from "./OINODbBunSqlite.js"
|
|
1
|
+
export { OINODbBunSqlite } from "./OINODbBunSqlite.js"
|