@oino-ts/db 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.
Files changed (48) hide show
  1. package/README.md +222 -0
  2. package/dist/cjs/OINODb.js +27 -0
  3. package/dist/cjs/OINODbApi.js +270 -0
  4. package/dist/cjs/OINODbConfig.js +86 -0
  5. package/dist/cjs/OINODbDataField.js +354 -0
  6. package/dist/cjs/OINODbDataModel.js +279 -0
  7. package/dist/cjs/OINODbDataSet.js +139 -0
  8. package/dist/cjs/OINODbFactory.js +563 -0
  9. package/dist/cjs/OINODbModelSet.js +267 -0
  10. package/dist/cjs/OINODbParams.js +280 -0
  11. package/dist/cjs/OINODbRequestParams.js +280 -0
  12. package/dist/cjs/OINODbSwagger.js +201 -0
  13. package/dist/cjs/index.js +51 -0
  14. package/dist/esm/OINODb.js +23 -0
  15. package/dist/esm/OINODbApi.js +265 -0
  16. package/dist/esm/OINODbConfig.js +82 -0
  17. package/dist/esm/OINODbDataField.js +345 -0
  18. package/dist/esm/OINODbDataModel.js +275 -0
  19. package/dist/esm/OINODbDataSet.js +134 -0
  20. package/dist/esm/OINODbFactory.js +559 -0
  21. package/dist/esm/OINODbModelSet.js +263 -0
  22. package/dist/esm/OINODbRequestParams.js +274 -0
  23. package/dist/esm/OINODbSwagger.js +197 -0
  24. package/dist/esm/index.js +17 -0
  25. package/dist/types/OINODb.d.ts +75 -0
  26. package/dist/types/OINODbApi.d.ts +57 -0
  27. package/dist/types/OINODbConfig.d.ts +52 -0
  28. package/dist/types/OINODbDataField.d.ts +202 -0
  29. package/dist/types/OINODbDataModel.d.ts +108 -0
  30. package/dist/types/OINODbDataSet.d.ts +95 -0
  31. package/dist/types/OINODbFactory.d.ts +99 -0
  32. package/dist/types/OINODbModelSet.d.ts +50 -0
  33. package/dist/types/OINODbRequestParams.d.ts +130 -0
  34. package/dist/types/OINODbSwagger.d.ts +25 -0
  35. package/dist/types/index.d.ts +103 -0
  36. package/package.json +35 -0
  37. package/src/OINODb.ts +98 -0
  38. package/src/OINODbApi.test.ts +243 -0
  39. package/src/OINODbApi.ts +270 -0
  40. package/src/OINODbConfig.ts +92 -0
  41. package/src/OINODbDataField.ts +372 -0
  42. package/src/OINODbDataModel.ts +290 -0
  43. package/src/OINODbDataSet.ts +170 -0
  44. package/src/OINODbFactory.ts +570 -0
  45. package/src/OINODbModelSet.ts +286 -0
  46. package/src/OINODbRequestParams.ts +281 -0
  47. package/src/OINODbSwagger.ts +209 -0
  48. package/src/index.ts +116 -0
package/src/OINODb.ts ADDED
@@ -0,0 +1,98 @@
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 { OINODbParams, OINODbApi, OINODataCell, OINODbDataSet } from "./index.js"
8
+
9
+ /**
10
+ * Base class for database abstraction, implementing methods for connecting, making queries and parsing/formatting data
11
+ * between SQL and serialization formats.
12
+ *
13
+ */
14
+ export abstract class OINODb {
15
+
16
+ protected _params:OINODbParams
17
+
18
+ /** Name of the database */
19
+ readonly name:string
20
+
21
+ /**
22
+ * Constructor for `OINODb`.
23
+ * @param params database parameters
24
+ */
25
+ constructor(params:OINODbParams) {
26
+ this._params = params
27
+ this.name = params.database
28
+ }
29
+
30
+ /**
31
+ * Connect to database.
32
+ *
33
+ */
34
+ abstract connect(): Promise<boolean>
35
+
36
+ /**
37
+ * Print a table name using database specific SQL escaping.
38
+ *
39
+ * @param sqlTable name of the table
40
+ *
41
+ */
42
+ abstract printSqlTablename(sqlTable:string): string
43
+
44
+ /**
45
+ * Print a column name with correct SQL escaping.
46
+ *
47
+ * @param sqlColumn name of the column
48
+ *
49
+ */
50
+ abstract printSqlColumnname(sqlColumn:string): string
51
+
52
+ /**
53
+ * Print a single data value from serialization using the context of the native data
54
+ * type with the correct SQL escaping.
55
+ *
56
+ * @param cellValue data from sql results
57
+ * @param sqlType native type name for table column
58
+ *
59
+ */
60
+ abstract printCellAsSqlValue(cellValue:OINODataCell, sqlType: string): string
61
+
62
+ /**
63
+ * Parse a single SQL result value for serialization using the context of the native data
64
+ * type.
65
+ *
66
+ * @param sqlValue data from serialization
67
+ * @param sqlType native type name for table column
68
+ *
69
+ */
70
+ abstract parseSqlValueAsCell(sqlValue:OINODataCell, sqlType: string): OINODataCell
71
+
72
+ /**
73
+ * Execute a select operation.
74
+ *
75
+ * @param sql SQL statement.
76
+ *
77
+ */
78
+ abstract sqlSelect(sql:string): Promise<OINODbDataSet>
79
+
80
+ /**
81
+ * Execute other sql operations.
82
+ *
83
+ * @param sql SQL statement.
84
+ *
85
+ */
86
+ abstract sqlExec(sql:string): Promise<OINODbDataSet>
87
+
88
+ /**
89
+ * Initialize a data model by getting the SQL schema and populating OINODbDataFields of
90
+ * the model.
91
+ *
92
+ * @param api api which data model to initialize.
93
+ *
94
+ */
95
+ abstract initializeApiDatamodel(api:OINODbApi): Promise<void>
96
+ }
97
+
98
+
@@ -0,0 +1,243 @@
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 { expect, test } from "bun:test";
8
+
9
+ import { OINODbApi, OINODbApiParams, OINOContentType, OINODataRow, OINODbDataField, OINOStringDataField, OINODb, OINODbFactory, OINODbParams, OINODbMemoryDataSet, OINODbModelSet, OINOBenchmark, OINOConsoleLog, OINORequestParams, OINODbSqlFilter, OINODbConfig, OINODbSqlOrder, OINOLogLevel, OINOLog } from "./index.js";
10
+
11
+ import { OINODbBunSqlite } from "@oino-ts/db-bunsqlite"
12
+ import { OINODbPostgresql } from "@oino-ts/db-postgresql"
13
+ import { OINODbMariadb } from "@oino-ts/db-mariadb"
14
+
15
+ Math.random()
16
+
17
+ OINOLog.setLogger(new OINOConsoleLog(OINOLogLevel.error))
18
+ OINODbFactory.registerDb("OINODbBunSqlite", OINODbBunSqlite)
19
+ OINODbFactory.registerDb("OINODbPostgresql", OINODbPostgresql)
20
+ OINODbFactory.registerDb("OINODbMariadb", OINODbMariadb)
21
+
22
+ export async function OINOTestApi(dbParams:OINODbParams, apiDataset: OINOTestApiParams) {
23
+ // OINOLog.info("OINOTestApi", {dbParams:dbParams, apiDataset:apiDataset})
24
+ const db:OINODb = await OINODbFactory.createDb( dbParams )
25
+ const api:OINODbApi = await OINODbFactory.createApi(db, apiDataset.apiParams)
26
+
27
+ const post_dataset:OINODbMemoryDataSet = new OINODbMemoryDataSet([apiDataset.postRow])
28
+ const post_modelset:OINODbModelSet = new OINODbModelSet(api.datamodel, post_dataset)
29
+
30
+ const put_dataset:OINODbMemoryDataSet = new OINODbMemoryDataSet([apiDataset.putRow])
31
+ const put_modelset:OINODbModelSet = new OINODbModelSet(api.datamodel, put_dataset)
32
+
33
+ // const new_row_id:string = OINODbConfig.printOINOId(post_modelset.datamodel.getRowPrimarykeyValues(apiDataset.postRow))
34
+ const new_row_id:string = OINODbConfig.printOINOId(post_modelset.datamodel.getRowPrimarykeyValues(apiDataset.postRow, true))
35
+ OINOLog.debug("OINOTestApi", {new_row_id:new_row_id})
36
+
37
+ const empty_params:OINORequestParams = { sqlParams: {}}
38
+
39
+ const target_db:string = "[" + dbParams.type + "]"
40
+ let target_table:string = "[" + apiDataset.apiParams.tableName + "]"
41
+ let target_group:string = "[SCHEMA]"
42
+
43
+ // test("dummy", () => {
44
+ // expect({foo:"h\\i"}).toMatchSnapshot()
45
+ // })
46
+
47
+ test(target_db + target_table + target_group + " public properties", async () => {
48
+ expect(api.datamodel.printFieldPublicPropertiesJson()).toMatchSnapshot()
49
+ })
50
+
51
+ target_group = "[HTTP GET]"
52
+ test(target_db + target_table + target_group + " select *", async () => {
53
+ expect((await api.doRequest("GET", "", "", empty_params)).data?.writeString()).toMatchSnapshot("GET JSON")
54
+ })
55
+
56
+ test(target_db + target_table + target_group + " select *", async () => {
57
+ expect((await api.doRequest("GET", "", "", empty_params)).data?.writeString(OINOContentType.csv)).toMatchSnapshot("GET CSV")
58
+ })
59
+
60
+
61
+ test(target_db + target_table + target_group + " select * with filter", async () => {
62
+ expect((await api.doRequest("GET", "", "", apiDataset.requestParams)).data?.writeString()).toMatchSnapshot("GET JSON FILTER")
63
+ apiDataset.requestParams.sqlParams.filter = undefined // remove filter so it does not affect rest of the tests
64
+ })
65
+
66
+ target_group = "[HTTP POST]"
67
+ const post_body_json:string = post_modelset.writeString(OINOContentType.json)
68
+ OINOLog.info("HTTP POST json", {post_body_json:post_body_json})
69
+ test(target_db + target_table + target_group + " insert with id", async () => {
70
+ expect((await api.doRequest("POST", new_row_id, post_body_json, empty_params))).toMatchSnapshot("POST")
71
+ })
72
+ test(target_db + target_table + target_group + " insert", async () => {
73
+ expect((await api.doRequest("POST", "", post_body_json, empty_params))).toMatchSnapshot("POST")
74
+ expect((await api.doRequest("GET", new_row_id, "", empty_params)).data?.writeString()).toMatchSnapshot("GET JSON")
75
+ expect((await api.doRequest("GET", new_row_id, "", empty_params)).data?.writeString(OINOContentType.csv)).toMatchSnapshot("GET CSV")
76
+ })
77
+ test(target_db + target_table + target_group + " insert no data", async () => {
78
+ expect((await api.doRequest("POST", "", "", empty_params))).toMatchSnapshot("POST")
79
+ })
80
+ test(target_db + target_table + target_group + " insert duplicate", async () => {
81
+ expect((await api.doRequest("POST", "", post_body_json, empty_params))).toMatchSnapshot("POST")
82
+ })
83
+
84
+ target_group = "[HTTP PUT]"
85
+ const put_body_json = put_modelset.writeString(OINOContentType.json)
86
+ OINOLog.info("HTTP PUT JSON", {put_body_json:put_body_json})
87
+ test(target_db + target_table + target_group + " update JSON", async () => {
88
+ apiDataset.requestParams.requestType = OINOContentType.json
89
+ expect((await api.doRequest("PUT", new_row_id, post_body_json, empty_params))).toMatchSnapshot("PUT JSON reset")
90
+ expect((await api.doRequest("PUT", new_row_id, put_body_json, empty_params))).toMatchSnapshot("PUT JSON")
91
+ expect((await api.doRequest("GET", new_row_id, "", empty_params)).data?.writeString()).toMatchSnapshot("GET JSON")
92
+ })
93
+
94
+ put_dataset.first()
95
+ const put_body_csv = put_modelset.writeString(OINOContentType.csv)
96
+ OINOLog.info("HTTP PUT csv", {put_body_csv:put_body_csv})
97
+ test(target_db + target_table + target_group + " update CSV", async () => {
98
+ apiDataset.requestParams.requestType = OINOContentType.csv
99
+ expect((await api.doRequest("PUT", new_row_id, post_body_json, empty_params))).toMatchSnapshot("PUT CSV reset")
100
+ expect((await api.doRequest("PUT", new_row_id, put_body_csv, apiDataset.requestParams))).toMatchSnapshot("PUT CSV")
101
+ expect((await api.doRequest("GET", new_row_id, "", empty_params)).data?.writeString(OINOContentType.csv)).toMatchSnapshot("GET CSV")
102
+ })
103
+
104
+ put_dataset.first()
105
+ let put_body_formdata = put_modelset.writeString(OINOContentType.formdata)
106
+ const multipart_boundary = put_body_formdata.substring(0, put_body_formdata.indexOf('\r'))
107
+ put_body_formdata.replaceAll(multipart_boundary, "---------OINO999999999")
108
+ OINOLog.info("HTTP PUT FORMDATA", {put_body_formdata:put_body_formdata})
109
+ test(target_db + target_table + target_group + " update FORMDATA", async () => {
110
+ apiDataset.requestParams.requestType = OINOContentType.formdata
111
+ apiDataset.requestParams.multipartBoundary = multipart_boundary
112
+ expect((await api.doRequest("PUT", new_row_id, post_body_json, empty_params))).toMatchSnapshot("PUT FORMDATA reset")
113
+ expect((await api.doRequest("PUT", new_row_id, put_body_formdata, apiDataset.requestParams))).toMatchSnapshot("PUT FORMDATA")
114
+ expect((await api.doRequest("GET", new_row_id, "", empty_params)).data?.writeString(OINOContentType.formdata)).toMatchSnapshot("GET FORMDATA")
115
+ apiDataset.requestParams.multipartBoundary = undefined
116
+ })
117
+
118
+ put_dataset.first()
119
+ const put_body_urlencode = put_modelset.writeString(OINOContentType.urlencode)
120
+ OINOLog.info("HTTP PUT URLENCODE", {put_body_urlencode:put_body_urlencode})
121
+ test(target_db + target_table + target_group + " update URLENCODE", async () => {
122
+ apiDataset.requestParams.requestType = OINOContentType.urlencode
123
+ expect((await api.doRequest("PUT", new_row_id, post_body_json, empty_params))).toMatchSnapshot("PUT URLENCODE reset")
124
+ expect((await api.doRequest("PUT", new_row_id, put_body_urlencode, apiDataset.requestParams))).toMatchSnapshot("PUT URLENCODE")
125
+ expect((await api.doRequest("GET", new_row_id, "", empty_params)).data?.writeString(OINOContentType.urlencode)).toMatchSnapshot("GET URLENCODE")
126
+ })
127
+
128
+ test(target_db + target_table + target_group + " update no data", async () => {
129
+ expect((await api.doRequest("PUT", new_row_id, "", empty_params))).toMatchSnapshot("PUT")
130
+ })
131
+
132
+ const primary_keys:OINODbDataField[] = api.datamodel.filterFields((field:OINODbDataField) => { return field.fieldParams.isPrimaryKey })
133
+ if (primary_keys.length != 1) {
134
+ OINOLog.info("HTTP PUT table " + apiDataset.apiParams.tableName + " does not have an individual primary key so 'invalid null' and 'oversized data' tests are skipped")
135
+ } else {
136
+ const id_field:string = primary_keys[0].name
137
+ const notnull_fields:OINODbDataField[] = api.datamodel.filterFields((field:OINODbDataField) => { return (field.fieldParams.isPrimaryKey == false) && (field.fieldParams.isNotNull == true) })
138
+ if (notnull_fields.length > 0) {
139
+ const invalid_null_value = "[{\"" + id_field + "\":\"" + new_row_id + "\",\"" + notnull_fields[0].name + "\":null}]"
140
+ test(target_db + target_table + target_group + " update with invalid null value", async () => {
141
+ expect((await api.doRequest("PUT", new_row_id, invalid_null_value, empty_params))).toMatchSnapshot("PUT")
142
+ })
143
+ }
144
+ const maxsize_fields:OINODbDataField[] = api.datamodel.filterFields((field:OINODbDataField) => { return (field instanceof OINOStringDataField) && (field.fieldParams.isPrimaryKey == false) && (field.maxLength > 0) })
145
+ if (maxsize_fields.length > 0) {
146
+ const oversized_value = "[{\"" + id_field + "\":\"" + new_row_id + "\",\"" + maxsize_fields[0].name + "\":\"" + "".padEnd(maxsize_fields[0].maxLength+1, "z") + "\"}]"
147
+ test(target_db + target_table + target_group + " update with oversized data", async () => {
148
+ expect((await api.doRequest("PUT", new_row_id, oversized_value, empty_params))).toMatchSnapshot("PUT")
149
+ })
150
+ }
151
+ }
152
+
153
+ target_group = "[HTTP DELETE]"
154
+ test(target_db + target_table + target_group + " remove", async () => {
155
+ expect((await api.doRequest("DELETE", new_row_id, "", empty_params))).toMatchSnapshot("DELETE")
156
+ expect((await api.doRequest("GET", new_row_id, "", empty_params)).data?.writeString()).toMatchSnapshot("GET JSON")
157
+ })
158
+ }
159
+
160
+ type OINOTestApiParams = {
161
+ apiParams: OINODbApiParams
162
+ requestParams: OINORequestParams
163
+ postRow: OINODataRow
164
+ putRow: OINODataRow
165
+ }
166
+
167
+ // OINOLog.setLogLevel(OINOLogLevel.debug)
168
+ OINOBenchmark.setEnabled(["doRequest"])
169
+ OINOBenchmark.reset()
170
+
171
+ const dbs:OINODbParams[] = [
172
+ { type: "OINODbBunSqlite", url:"file://../localDb/northwind.sqlite", database: "Northwind" },
173
+ { type: "OINODbPostgresql", url: "localhost", database: "Northwind", port:5432, user: "node", password: Bun.env.OINODB_POSTGRESQL_TOKEN },
174
+ { type: "OINODbMariadb", url: "127.0.0.1", database: "Northwind", port:6543, user: "node", password: Bun.env.OINODB_MARIADB_TOKEN }
175
+ ]
176
+
177
+ const apis:OINOTestApiParams[] = [
178
+ {
179
+ apiParams: { tableName: "Orders" },
180
+ requestParams: {
181
+ sqlParams: { filter: OINODbSqlFilter.parse("(ShipPostalCode)-like(0502%)"), order: new OINODbSqlOrder("ShipPostalCode desc") }
182
+ },
183
+ postRow: [30000,"CACTU",1,new Date("2024-04-05"),new Date("2024-04-06"),new Date("2024-04-07"),2,"184.75","a'b\"c%d_e\tf\rg\nh\\i","Garden House Crowther Way","Cowes","British Isles","PO31 7PJ","UK"],
184
+ putRow: [30000,"CACTU",1,new Date("2023-04-05"),new Date("2023-04-06"),new Date("2023-04-07"),2,"847.51","k'l\"m%n_o\tp\rq\nr\\s","59 rue de l'Abbaye","Cowes2","Western Europe","PO31 8PJ","UK"]
185
+ },
186
+ {
187
+ apiParams: { tableName: "Products" },
188
+ requestParams: {
189
+ sqlParams: { filter: OINODbSqlFilter.parse("(UnitsInStock)-le(5)"), order: new OINODbSqlOrder("UnitsInStock asc,UnitPrice asc") }
190
+ },
191
+ postRow: [99, "Umeshu", 1, 1, "500 ml", 12.99, 2, 0, 20, 0],
192
+ putRow: [99, "Umeshu", 1, 1, undefined, 24.99, 3, 0, 20, 0]
193
+ },
194
+ {
195
+ apiParams: { tableName: "Employees", hashidKey: "12345678901234567890123456789012" },
196
+ requestParams: {
197
+ sqlParams: { filter: OINODbSqlFilter.parse("(TitleOfCourtesy)-eq(Ms.)"), order: new OINODbSqlOrder("LastName") }
198
+ },
199
+ postRow: [99, "LastName", "FirstName", "Title", "TitleOfCourtesy", new Date("2024-04-06"), new Date("2024-04-07"), "Address", "City", "Region", 12345, "EU", "123 456 7890", "9876", Buffer.from("OINO"), "Line1\nLine2", 1, "http://accweb/emmployees/lastnamefirstname.bmp"],
200
+ putRow: [99, "LastName2", "FirstName2", null, "TitleOfCourtesy2", new Date("2023-04-06"), new Date("2023-04-07"), "Address2", "City2", "Region2", 54321, "EU2", "234 567 8901", "8765", Buffer.from("OINO2"), "Line3\nLine4", 1, "http://accweb/emmployees/lastnamefirstname.bmp"],
201
+ },
202
+ {
203
+ apiParams: { tableName: "OrderDetails" },
204
+ requestParams: {
205
+ sqlParams: { filter: OINODbSqlFilter.parse("(Quantity)-gt(100)"), order: new OINODbSqlOrder("Quantity desc") }
206
+ },
207
+ postRow: [10249,77,12.34,56,0],
208
+ putRow: [10249,77,23.45,67,0]
209
+ }
210
+
211
+ ]
212
+ for (let db of dbs) {
213
+ for (let api of apis) {
214
+ await OINOTestApi(db, api)
215
+ }
216
+ }
217
+
218
+ const snapshot_file = Bun.file("./node_modules/@oino-ts/db/src/__snapshots__/OINODbApi.test.ts.snap")
219
+ await Bun.write("./node_modules/@oino-ts/db/src/__snapshots__/OINODbApi.test.ts.snap.js", snapshot_file) // copy snapshots as .js so require works (note! if run with --update-snapshots, it's still the old file)
220
+ const snapshots = require("./__snapshots__/OINODbApi.test.ts.snap.js")
221
+
222
+ const crosscheck_tests:string[] = [
223
+ "[HTTP GET] select *: GET JSON 1",
224
+ "[HTTP POST] insert: GET JSON 1",
225
+ "[HTTP POST] insert: GET CSV 1",
226
+ "[HTTP PUT] update JSON: GET JSON 1",
227
+ "[HTTP PUT] update CSV: GET CSV 1",
228
+ "[HTTP PUT] update FORMDATA: GET FORMDATA 1",
229
+ "[HTTP PUT] update URLENCODE: GET URLENCODE 1"
230
+ ]
231
+
232
+ for (let i=0; i<dbs.length-1; i++) {
233
+ const db1:string = dbs[i].type
234
+ const db2:string = dbs[i+1].type
235
+ for (let api of apis) {
236
+ const table_name = api.apiParams.tableName
237
+ for (let test_name of crosscheck_tests) {
238
+ test("cross check {" + db1 + "} and {" + db2 + "} table {" + table_name + "} snapshots on {" + test_name + "}", () => {
239
+ expect(snapshots["[" + db1 + "][" + table_name + "]" + test_name]).toMatch(snapshots["[" + db2 + "][" + table_name + "]" + test_name])
240
+ })
241
+ }
242
+ }
243
+ }
@@ -0,0 +1,270 @@
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 { OINODbApiParams, OINODb, OINODbDataSet, OINODbDataModel, OINODbDataField, OINOStringDataField, OINO_ERROR_PREFIX, OINO_WARNING_PREFIX, OINO_INFO_PREFIX, OINODataRow, OINODataCell, OINODbModelSet, OINOBenchmark, OINODbFactory, OINORequestParams, OINO_DEBUG_PREFIX, OINOLog, OINOResult } from "./index.js"
8
+ import { OINOHashid } from "@oino-ts/hashid"
9
+
10
+ /**
11
+ * OINO API request result object with returned data and/or http status code/message and
12
+ * error / warning messages.
13
+ *
14
+ */
15
+ export class OINODbApiResult extends OINOResult {
16
+ /** Returned data if any */
17
+ data?: OINODbModelSet;
18
+
19
+ /**
20
+ * Constructor of OINODbApiResult.
21
+ *
22
+ * @param data result data
23
+ *
24
+ */
25
+ constructor (data?:OINODbModelSet) {
26
+ super()
27
+ this.data = data
28
+ }
29
+ }
30
+
31
+ /**
32
+ * API class with method to process HTTP REST requests.
33
+ *
34
+ */
35
+ export class OINODbApi {
36
+ /** API database reference */
37
+ readonly db: OINODb
38
+
39
+ /** API datamodel */
40
+ readonly datamodel: OINODbDataModel
41
+
42
+ /** API parameters */
43
+ readonly params: OINODbApiParams
44
+
45
+ /** API hashid */
46
+ readonly hashid:OINOHashid|null
47
+
48
+ /**
49
+ * Constructor of API object.
50
+ * NOTE! OINODb.initDatamodel must be called if created manually instead of the factory.
51
+ *
52
+ * @param db database for the API
53
+ * @param params parameters for the API
54
+ *
55
+ */
56
+ constructor (db: OINODb, params:OINODbApiParams) {
57
+ // OINOLog.debug("OINODbApi.constructor", {db:db, tableName:tableName, params:params})
58
+ if (!params.tableName) {
59
+ throw new Error(OINO_ERROR_PREFIX + ": OINODbApiParams needs to define a table name!")
60
+ }
61
+ this.db = db
62
+ this.params = params
63
+ this.datamodel = new OINODbDataModel(this)
64
+ if (this.params.hashidKey) {
65
+ this.hashid = new OINOHashid(this.params.hashidKey, this.db.name, this.params.hashidLength, this.params.hashidRandomIds)
66
+ } else {
67
+ this.hashid = null
68
+ }
69
+ }
70
+
71
+ private _validateRowValues(httpResult:OINODbApiResult, row:OINODataRow, requirePrimaryKey:boolean):void {
72
+ let field:OINODbDataField
73
+ for (let i=0; i<this.datamodel.fields.length; i++) {
74
+ field = this.datamodel.fields[i]
75
+ // OINOLog.debug("OINODbApi.validateHttpValues", {field:field})
76
+ const val:OINODataCell = row[i]
77
+ // OINOLog.debug("OINODbApi.validateHttpValues", {val:val})
78
+ if ((val === null) && ((field.fieldParams.isNotNull)||(field.fieldParams.isPrimaryKey))) { // null is a valid SQL value except if it's not allowed
79
+ httpResult.setError(405, "Field '" + field.name + "' is not allowed to be NULL!", "ValidateRowValues")
80
+
81
+ } else if ((val === undefined) && (requirePrimaryKey) && (field.fieldParams.isPrimaryKey) && (!field.fieldParams.isAutoInc)) {
82
+ httpResult.setError(405, "Primary key '" + field.name + "' is not autoinc and missing from the data!", "ValidateRowValues")
83
+
84
+ } else if ((val !== undefined) && (this.params.failOnUpdateOnAutoinc) && (field.fieldParams.isAutoInc)) {
85
+ httpResult.setError(405, "Autoinc field '" + field.name + "' can't be updated!", "ValidateRowValues")
86
+
87
+ } else {
88
+ if ((field instanceof OINOStringDataField) && ((field.maxLength > 0))){
89
+ const str_val = val?.toString() || ""
90
+ // OINOLog.debug("OINODbApi.validateHttpValues", {f:str_field, val:val})
91
+ if (str_val.length > field.maxLength) {
92
+ if (this.params.failOnOversizedValues) {
93
+ httpResult.setError(405, "Field '" + field.name + "' length (" + str_val.length + ") exceeds maximum (" + field.maxLength + ") and can't be set!", "ValidateRowValues")
94
+ } else {
95
+ httpResult.addWarning("Field '" + field.name + "' length (" + str_val.length + ") exceeds maximum (" + field.maxLength + ") and might truncate or fail.", "ValidateRowValues")
96
+ }
97
+ }
98
+ }
99
+
100
+ }
101
+ }
102
+ //logDebug("OINODbApi.validateHttpValues", {result:result})
103
+ }
104
+
105
+ private async _doGet(result:OINODbApiResult, id:string, params:OINORequestParams):Promise<void> {
106
+ OINOBenchmark.start("doGet")
107
+ const sql:string = this.datamodel.printSqlSelect(id, params.sqlParams)
108
+ // OINOLog.debug("OINODbApi.doGet sql", {sql:sql})
109
+ try {
110
+ const sql_res:OINODbDataSet = await this.db.sqlSelect(sql)
111
+ // OINOLog.debug("OINODbApi.doGet sql_res", {sql_res:sql_res})
112
+ if (sql_res.hasErrors()) {
113
+ result.setError(500, sql_res.getFirstError(), "DoGet")
114
+ result.addDebug("OINO GET SQL [" + sql + "]", "DoPut")
115
+ } else {
116
+ result.data = new OINODbModelSet(this.datamodel, sql_res)
117
+ }
118
+ } catch (e:any) {
119
+ result.setError(500, "Unhandled exception in doGet: " + e.message, "DoGet")
120
+ result.addDebug("OINO GET SQL [" + sql + "]", "DoGet")
121
+ }
122
+ OINOBenchmark.end("doGet")
123
+ }
124
+
125
+ private async _doPost(result:OINODbApiResult, rows:OINODataRow[]):Promise<void> {
126
+ OINOBenchmark.start("doPost")
127
+ let sql:string = ""
128
+ try {
129
+ let i:number = 0
130
+ while (i<rows.length) {
131
+ this._validateRowValues(result, rows[i], this.params.failOnInsertWithoutKey||false)
132
+ if (result.success) {
133
+ sql += this.datamodel.printSqlInsert(rows[i])
134
+ }
135
+ result.setOk() // individual rows may fail and will just be messages in response similar to executing multiple sql statements
136
+ i++
137
+ }
138
+ if (sql == "") {
139
+ result.setError(405, "No valid rows for POST!", "DoPost")
140
+ result.addDebug("OINO POST DATA [" + rows.join("|") + "]", "DoPost")
141
+
142
+ } else {
143
+ // OINOLog.debug("OINODbApi.doPost sql", {sql:sql})
144
+ const sql_res:OINODbDataSet = await this.db.sqlExec(sql)
145
+ // OINOLog.debug("OINODbApi.doPost sql_res", {sql_res:sql_res})
146
+ if (sql_res.hasErrors()) {
147
+ result.setError(500, sql_res.getFirstError(), "DoPost")
148
+ result.addDebug("OINO POST MESSAGES [" + sql_res.messages.join('|') + "]", "DoPost")
149
+ result.addDebug("OINO POST SQL [" + sql + "]", "DoPost")
150
+ }
151
+ }
152
+ } catch (e:any) {
153
+ result.setError(500, "Unhandled exception in doPost: " + e.message, "DoPost")
154
+ result.addDebug("OINO POST SQL [" + sql + "]", "DoPost")
155
+ }
156
+ OINOBenchmark.end("doPost")
157
+ }
158
+
159
+ private async _doPut(result:OINODbApiResult, id:string, row:OINODataRow):Promise<void> {
160
+ OINOBenchmark.start("doPut")
161
+ let sql:string = ""
162
+ try {
163
+ this._validateRowValues(result, row, false)
164
+ if (result.success) {
165
+ sql = this.datamodel.printSqlUpdate(id, row)
166
+ // OINOLog.debug("OINODbApi.doPut sql", {sql:sql})
167
+ const sql_res:OINODbDataSet = await this.db.sqlExec(sql)
168
+ // OINOLog.debug("OINODbApi.doPut sql_res", {sql_res:sql_res})
169
+ if (sql_res.hasErrors()) {
170
+ result.setError(500, sql_res.getFirstError(), "DoPut")
171
+ result.addDebug("OINO PUT MESSAGES [" + sql_res.messages.join('|') + "]", "DoPut")
172
+ result.addDebug("OINO PUT SQL [" + sql + "]", "DoPut")
173
+ }
174
+ }
175
+ } catch (e:any) {
176
+ result.setError(500, "Unhandled exception: " + e.message, "DoPut")
177
+ result.addDebug("OINO POST SQL [" + sql + "]", "DoPut")
178
+ }
179
+ OINOBenchmark.end("doPut")
180
+ }
181
+
182
+ private async _doDelete(result:OINODbApiResult, id:string):Promise<void> {
183
+ OINOBenchmark.start("doDelete")
184
+ let sql:string = ""
185
+ try {
186
+ sql = this.datamodel.printSqlDelete(id)
187
+ // OINOLog.debug("OINODbApi.doDelete sql", {sql:sql})
188
+ const sql_res:OINODbDataSet = await this.db.sqlExec(sql)
189
+ // OINOLog.debug("OINODbApi.doDelete sql_res", {sql_res:sql_res})
190
+ if (sql_res.hasErrors()) {
191
+ result.setError(500, sql_res.getFirstError(), "DoDelete")
192
+ result.addDebug("OINO DELETE MESSAGES [" + sql_res.messages.join('|') + "]", "DoDelete")
193
+ result.addDebug("OINO DELETE SQL [" + sql + "]", "DoDelete")
194
+ }
195
+ } catch (e:any) {
196
+ result.setError(500, "Unhandled exception: " + e.message, "DoDelete")
197
+ result.addDebug("OINO DELETE SQL [" + sql + "]", "DoDelete")
198
+ }
199
+ OINOBenchmark.end("doDelete")
200
+ }
201
+
202
+ /**
203
+ * Method for handlind a HTTP REST request with GET, POST, PUT, DELETE corresponding to
204
+ * SQL select, insert, update and delete.
205
+ *
206
+ * @param method HTTP verb (uppercase)
207
+ * @param id URL id of the REST request
208
+ * @param body HTTP body data as string
209
+ * @param params HTTP URL parameters as key-value-pairs
210
+ *
211
+ */
212
+ async doRequest(method:string, id: string, body:string, params:OINORequestParams):Promise<OINODbApiResult> {
213
+ OINOBenchmark.start("doRequest")
214
+ let result:OINODbApiResult = new OINODbApiResult()
215
+ OINOLog.debug("OINODbApi.doRequest enter", {method:method, id:id, body:body, searchParams:params})
216
+ if (method == "GET") {
217
+ await this._doGet(result, id, params)
218
+
219
+ } else if (method == "PUT") {
220
+ const rows:OINODataRow[] = OINODbFactory.createRows(this.datamodel, body, params)
221
+ if (!id) {
222
+ result.setError(400, "HTTP PUT method requires an URL ID for the row that is updated!", "DoRequest")
223
+
224
+ } else if (rows.length != 1) {
225
+ result.setError(400, "HTTP PUT method requires exactly one row in the body data!", "DoRequest")
226
+
227
+ } else {
228
+ try {
229
+ await this._doPut(result, id, rows[0])
230
+
231
+ } catch (e:any) {
232
+ result.setError(500, "Unhandled exception in HTTP PUT doRequest: " + e.message, "DoRequest")
233
+ }
234
+ }
235
+ } else if (method == "POST") {
236
+ const rows:OINODataRow[] = OINODbFactory.createRows(this.datamodel, body, params)
237
+ if (id) {
238
+ result.setError(400, "HTTP POST method must not have an URL ID as it does not target an existing row but creates a new one!", "DoRequest")
239
+
240
+ } else if (rows.length == 0) {
241
+ result.setError(400, "HTTP POST method requires at least one row in the body data!", "DoRequest")
242
+
243
+ } else {
244
+ try {
245
+ OINOLog.debug("OINODbApi.doRequest / POST", {rows:rows})
246
+ await this._doPost(result, rows)
247
+
248
+ } catch (e:any) {
249
+ result.setError(500, "Unhandled exception in HTTP POST doRequest: " + e.message, "DoRequest")
250
+ }
251
+ }
252
+ } else if (method == "DELETE") {
253
+ if (!id) {
254
+ result.setError(400, "HTTP DELETE method requires an id!", "DoRequest")
255
+
256
+ } else {
257
+ try {
258
+ await this._doDelete(result, id)
259
+
260
+ } catch (e:any) {
261
+ result.setError(500, "Unhandled exception in HTTP DELETE doRequest: " + e.message, "DoRequest")
262
+ }
263
+ }
264
+ } else {
265
+ result.setError(405, "Unsupported HTTP method '" + method + "'", "DoRequest")
266
+ }
267
+ OINOBenchmark.end("doRequest")
268
+ return result
269
+ }
270
+ }