@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.
- package/README.md +222 -0
- package/dist/cjs/OINODb.js +27 -0
- package/dist/cjs/OINODbApi.js +270 -0
- package/dist/cjs/OINODbConfig.js +86 -0
- package/dist/cjs/OINODbDataField.js +354 -0
- package/dist/cjs/OINODbDataModel.js +279 -0
- package/dist/cjs/OINODbDataSet.js +139 -0
- package/dist/cjs/OINODbFactory.js +563 -0
- package/dist/cjs/OINODbModelSet.js +267 -0
- package/dist/cjs/OINODbParams.js +280 -0
- package/dist/cjs/OINODbRequestParams.js +280 -0
- package/dist/cjs/OINODbSwagger.js +201 -0
- package/dist/cjs/index.js +51 -0
- package/dist/esm/OINODb.js +23 -0
- package/dist/esm/OINODbApi.js +265 -0
- package/dist/esm/OINODbConfig.js +82 -0
- package/dist/esm/OINODbDataField.js +345 -0
- package/dist/esm/OINODbDataModel.js +275 -0
- package/dist/esm/OINODbDataSet.js +134 -0
- package/dist/esm/OINODbFactory.js +559 -0
- package/dist/esm/OINODbModelSet.js +263 -0
- package/dist/esm/OINODbRequestParams.js +274 -0
- package/dist/esm/OINODbSwagger.js +197 -0
- package/dist/esm/index.js +17 -0
- package/dist/types/OINODb.d.ts +75 -0
- package/dist/types/OINODbApi.d.ts +57 -0
- package/dist/types/OINODbConfig.d.ts +52 -0
- package/dist/types/OINODbDataField.d.ts +202 -0
- package/dist/types/OINODbDataModel.d.ts +108 -0
- package/dist/types/OINODbDataSet.d.ts +95 -0
- package/dist/types/OINODbFactory.d.ts +99 -0
- package/dist/types/OINODbModelSet.d.ts +50 -0
- package/dist/types/OINODbRequestParams.d.ts +130 -0
- package/dist/types/OINODbSwagger.d.ts +25 -0
- package/dist/types/index.d.ts +103 -0
- package/package.json +35 -0
- package/src/OINODb.ts +98 -0
- package/src/OINODbApi.test.ts +243 -0
- package/src/OINODbApi.ts +270 -0
- package/src/OINODbConfig.ts +92 -0
- package/src/OINODbDataField.ts +372 -0
- package/src/OINODbDataModel.ts +290 -0
- package/src/OINODbDataSet.ts +170 -0
- package/src/OINODbFactory.ts +570 -0
- package/src/OINODbModelSet.ts +286 -0
- package/src/OINODbRequestParams.ts +281 -0
- package/src/OINODbSwagger.ts +209 -0
- 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
|
+
}
|
package/src/OINODbApi.ts
ADDED
|
@@ -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
|
+
}
|