@oino-ts/db-postgresql 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 ADDED
@@ -0,0 +1,222 @@
1
+ # OINO TS
2
+ OINO Is Not an ORM but it's trying to solve a similar problem for API development. Instead of mirroring your DB schema in code that needs manual updates, OINO will get the data schema from DBMS using SQL in real time. Every time your app starts, it has an updated data model which enables automatic (de)serialize SQL results to JSON/CSV and back. OINO works on the level below routing where you pass the method, URL ID, body and request parameters to the API-object. OINO will parse and validate the data against the data model and generate proper SQL for your DB. Because OINO knows how data is serialized (e.g. JSON), what column it belongs to (e.g. floating point number) and what the target database is, it knows how to parse, format and escape the value as valid SQL.
3
+
4
+ ```
5
+ const result:OINOApiResult = await api_orderdetails.doRequest("GET", id, body, params)
6
+ return new Response(result.modelset.writeString(OINOContentType.json))
7
+ ```
8
+
9
+
10
+ # GETTING STARTED
11
+
12
+ ### Setup
13
+ Install the `@oino-ts/core` npm package and necessary database packages and import them in your code.
14
+ ```
15
+ bun install @oino-ts/core
16
+ bun install @oino-ts/bunsqlite
17
+ ```
18
+
19
+ ```
20
+ import { OINODb, OINOApi, OINOFactory } from "@oino-ts/core";
21
+ import { OINODbBunSqlite } from "@oino-ts/bunsqlite"
22
+ ```
23
+
24
+ ### Register database and logger
25
+ Register your database implementation and logger (see [`OINOConsoleLog`](https://pragmatta.github.io/oino-ts/classes/core_src.OINOConsoleLog.html) how to implement your own)
26
+
27
+ ```
28
+ OINOLog.setLogger(new OINOConsoleLog())
29
+ OINOFactory.registerDb("OINODbBunSqlite", OINODbBunSqlite)
30
+ ```
31
+
32
+ ### Create a database
33
+ Creating a database connection [`OINODb`](https://pragmatta.github.io/oino-ts/classes/core_src_OINODb.OINODb.html) is done by passing [`OINODbParams`](https://pragmatta.github.io/oino-ts/types/core_src.OINODbParams.html) to the factory method. For [`OINODbBunSqlite`](https://pragmatta.github.io/oino-ts/classes/bunsqlite_OINODbBunSqlite.OINODbBunSqlite.html) that means a file url for the database file, for others network host, port, credentials etc.
34
+ ```
35
+ const db:OINODb = await OINOFactory.createDb( { type: "OINODbBunSqlite", url: "file://../localDb/northwind.sqlite" } )
36
+ ```
37
+
38
+ ### Create an API
39
+ From a database you can create an [`OINOApi`](https://pragmatta.github.io/oino-ts/classes/core_src_OINOApi.OINOApi.html) by passing [`OINOApiParams`](https://pragmatta.github.io/oino-ts/types/core_src.OINOApiParams.html) with table name and preferences to the factory method.
40
+ ```
41
+ const api_employees:OINOApi = await OINOFactory.createApi(db, { tableName: "Employees", excludeFields:["BirthDate"] })
42
+ ```
43
+
44
+ ### Pass HTTP requests to API
45
+ When you receive a HTTP request, just pass the method, URL ID, body and params to the correct API, which will parse and validate input and return results.
46
+
47
+ ```
48
+ const result:OINOApiResult = await api_orderdetails.doRequest("GET", id, body, params)
49
+ ```
50
+
51
+ ### Write results back to HTTP Response
52
+ The results for a GET request will contain [`OINOModelSet`](https://pragmatta.github.io/oino-ts/classes/core_src_OINOModelSet.OINOModelSet.html) data that can be written out as JSON or CSV as needed. For other requests result is just success or error with messages.
53
+ ```
54
+ return new Response(result.modelset.writeString(OINOContentType.json))
55
+ ```
56
+
57
+
58
+ # FEATURES
59
+
60
+ ## RESTfull
61
+ OINO maps HTTP methods GET/POST/PUT/DELETE to SQL operations SELECT/INSERT/UPDATE/DELETE. The GET/POST requests can be made without URL ID to get all rows or insert new ones and others target a single row using URL ID.
62
+
63
+ ### HTTP GET
64
+ ```
65
+ Request and response:
66
+ > curl.exe -X GET http://localhost:3001/orderdetails/11077:77
67
+ [
68
+ {"_OINOID_":"11077:77","OrderID":11077,"ProductID":77,"UnitPrice":13,"Quantity":2,"Discount":0}
69
+ ]
70
+
71
+ SQL:
72
+ SELECT "OrderID","ProductID","UnitPrice","Quantity","Discount" FROM [OrderDetails] WHERE ("OrderID"=11077 AND "ProductID"=77);
73
+ ```
74
+
75
+ ### HTTP POST
76
+ ```
77
+ Request and response:
78
+ > curl.exe -X POST http://localhost:3001/orderdetails -H "Content-Type: application/json" --data '[{\"OrderID\":11077,\"ProductID\":99,\"UnitPrice\":19,\"Quantity\":1,\"Discount\":0}]'
79
+ {"success":true,"statusCode":200,"statusMessage":"OK","messages":[]}
80
+
81
+ SQL:
82
+ INSERT INTO [OrderDetails] ("OrderID","ProductID","UnitPrice","Quantity","Discount") VALUES (11077,99,19,1,0);
83
+ ```
84
+
85
+ ### HTTP PUT
86
+ ```
87
+ Request and response:
88
+ > curl.exe -X PUT http://localhost:3001/orderdetails/11077:99 -H "Content-Type: application/json" --data '[{\"UnitPrice\":20}]'
89
+ {"success":true,"statusCode":200,"statusMessage":"OK","messages":[]}
90
+
91
+ SQL:
92
+ UPDATE [OrderDetails] SET "UnitPrice"=20 WHERE ("OrderID"=11077 AND "ProductID"=99);
93
+ ```
94
+
95
+ ### HTTP DELETE
96
+ ```
97
+ Request and response:
98
+ > curl.exe -X DELETE http://localhost:3001/orderdetails/11077:99
99
+ {"success":true,"statusCode":200,"statusMessage":"OK","messages":[]}
100
+
101
+ SQL:
102
+ DELETE FROM [OrderDetails] WHERE ("OrderID"=11077 AND "ProductID"=99);
103
+ ```
104
+
105
+ ## Universal Serialization
106
+ OINO handles serialization of data to JSON/CSV/etc. and back based on the data model. It knows what columns exist, what is their data type and how to convert each to JSON/CSV and back. This allows also partial data to be sent, i.e. you can send only columns that need updating or even send extra columns and have them ignored.
107
+
108
+ ### Features
109
+ - Files can be sent to BLOB fields using BASE64 encoding.
110
+ - Datetimes are (optionally) normalized to ISO 8601 format.
111
+ - Extended JSON-encoding
112
+ - Unquoted literal `undefined` can be used to represent non-existent values (leaving property out works too but preserving structure might be easier e.g. when translating data).
113
+ - CSV
114
+ - Comma-separated, doublequotes.
115
+ - Unquoted literal `null` represents null values.
116
+ - Unquoted empty string represents undefined values.
117
+ - Form data
118
+ - Multipart-mixed and binary files not supported.
119
+ - Non-existent value line (i.e. nothing after the empty line) treated as a null value.
120
+ - Url-encoded
121
+ - No null values, missing properties treated as undefined.
122
+ - Multiple lines could be used to post multiple rows.
123
+
124
+
125
+ ## Database Abstraction
126
+ OINO functions as a database abstraction, providing a consistent interface for working with different databases. It abstracts out different conventions in connecting, making queries and formatting data.
127
+
128
+ Currently supported databases:
129
+ - Bun Sqlite through Bun native implementation
130
+ - Postgresql through [pg](https://www.npmjs.com/package/pg)-package
131
+ - Mariadb / Mysql-support through [mariadb](https://www.npmjs.com/package/mariadb)-package
132
+
133
+ ## Complex Keys
134
+ To support tables with multipart primary keys OINO generates a composite key `_OINOID_` that is included in the result and can be used as the REST ID. For example in the example above table `OrderDetails` has two primary keys `OrderID` and `ProductID` making the `_OINOID_` of form `11077:99`.
135
+
136
+ ## Power Of SQL
137
+ Since OINO controls the SQL, WHERE-conditions can be defined with [`OINOSqlFilter`](https://pragmatta.github.io/oino-ts/classes/core_src.OINOSqlFilter.html) and order with [`OINOSqlOrder`](https://pragmatta.github.io/oino-ts/classes/core_src.OINOSqlOrder.html) that are passed as HTTP request parameters. No more API development where you make unique API endpoints for each filter that fetch all data with original API and filter in backend code. Every API can be filtered when and as needed without unnessecary data tranfer and utilizing SQL indexing when available.
138
+
139
+ ## Swagger Support
140
+ Swagger is great as long as the definitions are updated and with OINO you can automatically get a Swagger definition including a data model schema.
141
+ ```
142
+ if (url.pathname == "/swagger.json") {
143
+ return new Response(JSON.stringify(OINOSwagger.getApiDefinition(api_array)))
144
+ }
145
+ ```
146
+ ![Swagger definition with a data model schema](img/readme-swagger.png)
147
+
148
+ ## Node support
149
+ OINO is developped Typescript first but compiles to standard CommonJS and the NPM packages should work on either ESM / CommonJS. Checkout sample apps `readmeApp` (ESM) and `nodeApp` (CommonJS).
150
+
151
+ ## HTMX support
152
+ OINO is [htmx.org](https://htmx.org) friendly, allowing easy translation of [`OINODataRow`](https://pragmatta.github.io/oino-ts/types/core_src.OINODataRow.html) to HTML output using templates (cf. the [htmx sample app](https://github.com/pragmatta/oino-ts/tree/main/samples/htmxApp)).
153
+
154
+ ### Hashids
155
+ Autoinc numeric id's are very pragmatic and fit well with OINO (e.g. using a form without primary key fields to insert new rows with database assigned ids). However it's not always sensible to share information about the sequence. Hashids solve this by masking the original values by encrypting the ids using AES-128 and some randomness. Length of the hashid can be chosen from 12-32 characters where longer ids provide more security. However this should not be considereded a cryptographic solution for keeping ids secret but rather making it infeasible to iterate all ids.
156
+
157
+
158
+ # STATUS
159
+ OINO is currently a hobby project which should and should considered in alpha status. That also means compatibility breaking changes can be made without prior notice when architectual issues are discovered.
160
+
161
+ ## Beta
162
+ For a beta status following milestones are planned:
163
+
164
+ ### Realistic app
165
+ There needs to be a realistic app built on top of OINO to get a better grasp of the edge cases.
166
+
167
+ ### Security review
168
+ Handling of SQL-injection attacks needs a thorough review, what are the relevant attack vectors are for OINO and what protections are still needed.
169
+
170
+ ## Roadmap
171
+ Things that need to happen in some order before beta-status are at least following:
172
+
173
+ ### Support for views
174
+ Simple cases of views would work already in some databases but edge cases might get complicated. For example
175
+ - How to handle a view which does not have a complete private key?
176
+ - What edge cases exist in updating views?
177
+
178
+ ### Batch updates
179
+ Supporting batch updates similar to batch inserts is slightly bending the RESTfull principles but would still be a useful optional feature.
180
+
181
+ ### Aggregation and limits
182
+ Similar to filtering and ordering, aggregation and limits can be implemented as HTTP request parameters telling what column is aggregated or used for ordering or how many results to return.
183
+
184
+ ### Streaming
185
+ One core idea is to be efficient in not making unnecessary copies of the data and minimizing garbage collection debt. This can be taken further by implementing streaming, allowing large dataset to be written to HTTP response as SQL result rows are received.
186
+
187
+ ### SQL generation callbacks
188
+ It would be useful to allow developer to validate / override SQL generation to cover cases OINO does not support or even workaround issues.
189
+
190
+ ### Transactions
191
+ Even though the basic case for OINO is executing SQL operations on individual rows, having an option to use SQL transactions could make sense at least for batch operations.
192
+
193
+
194
+ # HELP
195
+
196
+ ## Bug reports
197
+ Fixing bugs is a priority and getting good quality bug reports helps. It's recommended to use the sample Northwind database included with project to replicate issues or make an SQL script export of the relevant table.
198
+
199
+ ## Feedback
200
+ Understanding and prioritizing the use cases for OINO is also important and feedback about how you'd use OINO is interesting. Feel free to raise issues and feature requests in Github, but understand that short term most of the effort goes towards reaching the beta stage.
201
+
202
+ ## Typescript / Javascript architecture
203
+ Typescript building with different targets and module-systemts and a ton of configuration is a complex domain and something I have little experience un so help in fixing problems and how thing ought to be done is appreciated.
204
+
205
+ # LINKS
206
+ - [Github repository](https://github.com/pragmatta/oino-ts)
207
+ - [NPM repository](https://www.npmjs.com/org/oino-ts)
208
+
209
+
210
+ # ACKNOWLEDGEMENTS
211
+
212
+ ## Libraries
213
+ OINO uses the following open source libraries and npm packages and I would like to thank everyone for their contributions:
214
+ - Postgresql support by [node-postgres package](https://www.npmjs.com/package/pg)
215
+ - Mariadb / Mysql-support by [mariadb package](https://www.npmjs.com/package/mariadb)
216
+
217
+ ## Bun
218
+ OINO has been developed using the Bun runtime, not because of the speed improvements but for the first class Typescript support and integrated developper experience. Kudos on the bun team for making Typescript work more exiting again.
219
+
220
+ ## SQL Scripts
221
+ The SQL scripts for creating the sample Northwind database are based on [Google Code archive](https://code.google.com/archive/p/northwindextended/downloads) and have been further customized to ensure they would have identical data (in the scope of the automated testing).
222
+
@@ -0,0 +1,330 @@
1
+ "use strict";
2
+ /*
3
+ * This Source Code Form is subject to the terms of the Mozilla Public
4
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
5
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.OINODbPostgresql = void 0;
9
+ const db_1 = require("@oino-ts/db");
10
+ const pg_1 = require("pg");
11
+ const EMPTY_ROW = [];
12
+ /**
13
+ * Implmentation of OINODbDataSet for Postgresql.
14
+ *
15
+ */
16
+ class OINOPostgresqlData extends db_1.OINODbDataSet {
17
+ _rows;
18
+ /**
19
+ * OINOPostgresqlData constructor
20
+ * @param params database parameters
21
+ */
22
+ constructor(data, messages = []) {
23
+ super(data, messages);
24
+ if ((data != null) && !(Array.isArray(data))) {
25
+ throw new Error(db_1.OINO_ERROR_PREFIX + ": Invalid Posgresql data type!"); // TODO: maybe check all rows
26
+ }
27
+ this._rows = data;
28
+ if (this.isEmpty()) {
29
+ this._currentRow = -1;
30
+ this._eof = true;
31
+ }
32
+ else {
33
+ this._currentRow = 0;
34
+ this._eof = false;
35
+ }
36
+ }
37
+ _currentRow;
38
+ _eof;
39
+ isEmpty() {
40
+ return (this._rows.length == 0);
41
+ }
42
+ // EOF means "there is no more content", i.e. either dataset is empty or we have moved beyond last line
43
+ isEof() {
44
+ return (this._eof);
45
+ }
46
+ next() {
47
+ // OINOLog.debug("OINODbDataSet.next", {currentRow:this._currentRow, length:this.sqlResult.data.length})
48
+ if (this._currentRow < this._rows.length - 1) {
49
+ this._currentRow = this._currentRow + 1;
50
+ }
51
+ else {
52
+ this._eof = true;
53
+ }
54
+ return !this._eof;
55
+ }
56
+ getRow() {
57
+ if ((this._currentRow >= 0) && (this._currentRow < this._rows.length)) {
58
+ return this._rows[this._currentRow];
59
+ }
60
+ else {
61
+ return EMPTY_ROW;
62
+ }
63
+ }
64
+ }
65
+ /**
66
+ * Implementation of Postgresql-database.
67
+ *
68
+ */
69
+ class OINODbPostgresql extends db_1.OINODb {
70
+ static table_schema_sql = `SELECT
71
+ col.column_name,
72
+ col.data_type,
73
+ col.character_maximum_length,
74
+ col.is_nullable,
75
+ pk.primary_key,
76
+ col.numeric_precision,
77
+ col.numeric_scale,
78
+ col.column_default
79
+ FROM information_schema.columns col
80
+ LEFT JOIN LATERAL
81
+ (select kcu.column_name, 'YES' as primary_key
82
+ from
83
+ information_schema.table_constraints tco,
84
+ information_schema.key_column_usage kcu
85
+ where
86
+ kcu.constraint_name = tco.constraint_name
87
+ and kcu.constraint_schema = tco.constraint_schema
88
+ and tco.table_name = col.table_name
89
+ and tco.constraint_type = 'PRIMARY KEY'
90
+ ) pk on col.column_name = pk.column_name
91
+ WHERE table_name = `;
92
+ // private _client:Client
93
+ _pool;
94
+ /**
95
+ * Constructor of `OINODbPostgresql`
96
+ * @param params database paraneters
97
+ */
98
+ constructor(params) {
99
+ super(params);
100
+ // OINOLog.debug("OINODbPostgresql.constructor", {params:params})
101
+ if (this._params.type !== "OINODbPostgresql") {
102
+ throw new Error(db_1.OINO_ERROR_PREFIX + ": Not OINODbPostgresql-type: " + this._params.type);
103
+ }
104
+ this._pool = new pg_1.Pool({ host: params.url, database: params.database, port: params.port, user: params.user, password: params.password });
105
+ this._pool.on("error", (err) => {
106
+ db_1.OINOLog.error("OINODbPostgresql error", { err: err });
107
+ });
108
+ this._pool.on("connect", (message) => {
109
+ // OINOLog.info("OINODbPostgresql connect")
110
+ });
111
+ this._pool.on("release", (message) => {
112
+ // OINOLog.info("OINODbPostgresql notice")
113
+ });
114
+ this._pool.on("acquire", () => {
115
+ // OINOLog.info("OINODbPostgresql end")
116
+ });
117
+ }
118
+ _parseFieldLength(fieldLength) {
119
+ let result = parseInt((fieldLength || "0").toString());
120
+ if (Number.isNaN(result)) {
121
+ result = 0;
122
+ }
123
+ return result;
124
+ }
125
+ async _query(sql) {
126
+ // OINOLog.debug("OINODbPostgresql._query", {sql:sql})
127
+ const query_result = await this._pool.query({ rowMode: "array", text: sql });
128
+ // OINOLog.debug("OINODbPostgresql._query", {result:query_result})
129
+ return Promise.resolve(query_result.rows);
130
+ }
131
+ async _exec(sql) {
132
+ // OINOLog.debug("OINODbPostgresql._query", {sql:sql})
133
+ const query_result = await this._pool.query({ rowMode: "array", text: sql });
134
+ // OINOLog.debug("OINODbPostgresql._query", {result:query_result})
135
+ return Promise.resolve(query_result.rows);
136
+ }
137
+ /**
138
+ * Print a table name using database specific SQL escaping.
139
+ *
140
+ * @param sqlTable name of the table
141
+ *
142
+ */
143
+ printSqlTablename(sqlTable) {
144
+ return "\"" + sqlTable.toLowerCase() + "\"";
145
+ }
146
+ /**
147
+ * Print a column name with correct SQL escaping.
148
+ *
149
+ * @param sqlColumn name of the column
150
+ *
151
+ */
152
+ printSqlColumnname(sqlColumn) {
153
+ return "\"" + sqlColumn + "\"";
154
+ }
155
+ /**
156
+ * Print a single data value from serialization using the context of the native data
157
+ * type with the correct SQL escaping.
158
+ *
159
+ * @param cellValue data from sql results
160
+ * @param sqlType native type name for table column
161
+ *
162
+ */
163
+ printCellAsSqlValue(cellValue, sqlType) {
164
+ if (cellValue === null) {
165
+ return "NULL";
166
+ }
167
+ else if (cellValue === undefined) {
168
+ return "UNDEFINED";
169
+ }
170
+ else if ((sqlType == "integer") || (sqlType == "smallint") || (sqlType == "real")) {
171
+ return cellValue.toString();
172
+ }
173
+ else if (sqlType == "bytea") {
174
+ return "\'" + cellValue + "\'";
175
+ }
176
+ else if (sqlType == "boolean") {
177
+ if (cellValue == null || cellValue == "" || cellValue.toString().toLowerCase() == "false" || cellValue == "0") {
178
+ return "false";
179
+ }
180
+ else {
181
+ return "true";
182
+ }
183
+ }
184
+ else if ((sqlType == "date") && (cellValue instanceof Date)) {
185
+ return "\'" + cellValue.toISOString() + "\'";
186
+ }
187
+ else {
188
+ return "\'" + cellValue?.toString().replaceAll("'", "''") + "\'";
189
+ }
190
+ }
191
+ /**
192
+ * Parse a single SQL result value for serialization using the context of the native data
193
+ * type.
194
+ *
195
+ * @param sqlValue data from serialization
196
+ * @param sqlType native type name for table column
197
+ *
198
+ */
199
+ parseSqlValueAsCell(sqlValue, sqlType) {
200
+ if ((sqlValue === null) || (sqlValue == "NULL")) {
201
+ return null;
202
+ }
203
+ else if ((sqlValue === undefined)) {
204
+ return undefined;
205
+ }
206
+ else if (((sqlType == "date")) && (typeof (sqlValue) == "string")) {
207
+ return new Date(sqlValue);
208
+ }
209
+ else {
210
+ return sqlValue;
211
+ }
212
+ }
213
+ /**
214
+ * Connect to database.
215
+ *
216
+ */
217
+ async connect() {
218
+ try {
219
+ // make sure that any items are correctly URL encoded in the connection string
220
+ // OINOLog.debug("OINODbPostgresql.connect")
221
+ // await this._pool.connect()
222
+ // await this._client.connect()
223
+ return Promise.resolve(true);
224
+ }
225
+ catch (err) {
226
+ // ... error checks
227
+ throw new Error(db_1.OINO_ERROR_PREFIX + ": Error connecting to Postgresql server: " + err);
228
+ }
229
+ }
230
+ /**
231
+ * Execute a select operation.
232
+ *
233
+ * @param sql SQL statement.
234
+ *
235
+ */
236
+ async sqlSelect(sql) {
237
+ db_1.OINOBenchmark.start("sqlSelect");
238
+ let result;
239
+ try {
240
+ const rows = await this._query(sql);
241
+ // OINOLog.debug("OINODbPostgresql.sqlSelect", {rows:rows})
242
+ result = new OINOPostgresqlData(rows, []);
243
+ }
244
+ catch (e) {
245
+ result = new OINOPostgresqlData([[]], [db_1.OINO_ERROR_PREFIX + " (sqlSelect): exception in _db.query [" + e.message + "]"]);
246
+ }
247
+ db_1.OINOBenchmark.end("sqlSelect");
248
+ return result;
249
+ }
250
+ /**
251
+ * Execute other sql operations.
252
+ *
253
+ * @param sql SQL statement.
254
+ *
255
+ */
256
+ async sqlExec(sql) {
257
+ db_1.OINOBenchmark.start("sqlExec");
258
+ let result;
259
+ try {
260
+ const rows = await this._exec(sql);
261
+ // OINOLog.debug("OINODbPostgresql.sqlExec", {rows:rows})
262
+ result = new OINOPostgresqlData(rows, []);
263
+ }
264
+ catch (e) {
265
+ result = new OINOPostgresqlData([[]], [db_1.OINO_ERROR_PREFIX + " (sqlExec): exception in _db.exec [" + e.message + "]"]);
266
+ }
267
+ db_1.OINOBenchmark.end("sqlExec");
268
+ return result;
269
+ }
270
+ /**
271
+ * Initialize a data model by getting the SQL schema and populating OINODbDataFields of
272
+ * the model.
273
+ *
274
+ * @param api api which data model to initialize.
275
+ *
276
+ */
277
+ async initializeApiDatamodel(api) {
278
+ const res = await this.sqlSelect(OINODbPostgresql.table_schema_sql + "'" + api.params.tableName.toLowerCase() + "';");
279
+ // OINOLog.debug("OINODbPostgresql.initializeApiDatamodel: table description ", {res: res })
280
+ while (!res.isEof()) {
281
+ const row = res.getRow();
282
+ // OINOLog.debug("OINODbPostgresql.initializeApiDatamodel: next row ", {row: row })
283
+ const field_name = row[0]?.toString() || "";
284
+ const sql_type = row[1]?.toString() || "";
285
+ const field_length = this._parseFieldLength(row[2]);
286
+ const numeric_precision = this._parseFieldLength(row[5]);
287
+ const numeric_scale = this._parseFieldLength(row[6]);
288
+ const default_val = row[7]?.toString() || "";
289
+ const field_params = {
290
+ isPrimaryKey: row[4] == "YES",
291
+ isNotNull: row[3] == "NO",
292
+ isAutoInc: default_val.startsWith("nextval(")
293
+ };
294
+ if ((!api.params.excludeFieldPrefix || !field_name.startsWith(api.params.excludeFieldPrefix)) && (!api.params.excludeFields || (api.params.excludeFields.indexOf(field_name) < 0))) {
295
+ // OINOLog.debug("OINODbPostgresql.initializeApiDatamodel: next field ", {field_name: field_name, sql_type:sql_type, field_length:field_length, field_params:field_params })
296
+ if ((sql_type == "integer") || (sql_type == "smallint") || (sql_type == "real")) {
297
+ api.datamodel.addField(new db_1.OINONumberDataField(this, field_name, sql_type, field_params));
298
+ }
299
+ else if ((sql_type == "date")) {
300
+ if (api.params.useDatesAsString) {
301
+ api.datamodel.addField(new db_1.OINOStringDataField(this, field_name, sql_type, field_params, 0));
302
+ }
303
+ else {
304
+ api.datamodel.addField(new db_1.OINODatetimeDataField(this, field_name, sql_type, field_params));
305
+ }
306
+ }
307
+ else if ((sql_type == "character") || (sql_type == "character varying") || (sql_type == "varchar") || (sql_type == "text")) {
308
+ api.datamodel.addField(new db_1.OINOStringDataField(this, field_name, sql_type, field_params, field_length));
309
+ }
310
+ else if ((sql_type == "bytea")) {
311
+ api.datamodel.addField(new db_1.OINOBlobDataField(this, field_name, sql_type, field_params, field_length));
312
+ }
313
+ else if ((sql_type == "boolean")) {
314
+ api.datamodel.addField(new db_1.OINOBooleanDataField(this, field_name, sql_type, field_params));
315
+ }
316
+ else if ((sql_type == "decimal") || (sql_type == "numeric")) {
317
+ api.datamodel.addField(new db_1.OINOStringDataField(this, field_name, sql_type, field_params, numeric_precision + numeric_scale + 1));
318
+ }
319
+ else {
320
+ db_1.OINOLog.info("OINODbPostgresql.initializeApiDatamodel: unrecognized field type treated as string", { field_name: field_name, sql_type: sql_type, field_length: field_length, field_params: field_params });
321
+ api.datamodel.addField(new db_1.OINOStringDataField(this, field_name, sql_type, field_params, 0));
322
+ }
323
+ }
324
+ res.next();
325
+ }
326
+ db_1.OINOLog.debug("OINODbPostgresql.initializeDatasetModel:\n" + api.datamodel.printDebug("\n"));
327
+ return Promise.resolve();
328
+ }
329
+ }
330
+ exports.OINODbPostgresql = OINODbPostgresql;
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OINODbPostgresql = void 0;
4
+ var OINODbPostgresql_js_1 = require("./OINODbPostgresql.js");
5
+ Object.defineProperty(exports, "OINODbPostgresql", { enumerable: true, get: function () { return OINODbPostgresql_js_1.OINODbPostgresql; } });
@@ -0,0 +1,326 @@
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
+ import { OINODb, OINODbDataSet, OINOBooleanDataField, OINONumberDataField, OINOStringDataField, OINO_ERROR_PREFIX, OINOBenchmark, OINODatetimeDataField, OINOBlobDataField, OINOLog } from "@oino-ts/db";
7
+ import { Pool } from "pg";
8
+ const EMPTY_ROW = [];
9
+ /**
10
+ * Implmentation of OINODbDataSet for Postgresql.
11
+ *
12
+ */
13
+ class OINOPostgresqlData extends OINODbDataSet {
14
+ _rows;
15
+ /**
16
+ * OINOPostgresqlData constructor
17
+ * @param params database parameters
18
+ */
19
+ constructor(data, messages = []) {
20
+ super(data, messages);
21
+ if ((data != null) && !(Array.isArray(data))) {
22
+ throw new Error(OINO_ERROR_PREFIX + ": Invalid Posgresql data type!"); // TODO: maybe check all rows
23
+ }
24
+ this._rows = data;
25
+ if (this.isEmpty()) {
26
+ this._currentRow = -1;
27
+ this._eof = true;
28
+ }
29
+ else {
30
+ this._currentRow = 0;
31
+ this._eof = false;
32
+ }
33
+ }
34
+ _currentRow;
35
+ _eof;
36
+ isEmpty() {
37
+ return (this._rows.length == 0);
38
+ }
39
+ // EOF means "there is no more content", i.e. either dataset is empty or we have moved beyond last line
40
+ isEof() {
41
+ return (this._eof);
42
+ }
43
+ next() {
44
+ // OINOLog.debug("OINODbDataSet.next", {currentRow:this._currentRow, length:this.sqlResult.data.length})
45
+ if (this._currentRow < this._rows.length - 1) {
46
+ this._currentRow = this._currentRow + 1;
47
+ }
48
+ else {
49
+ this._eof = true;
50
+ }
51
+ return !this._eof;
52
+ }
53
+ getRow() {
54
+ if ((this._currentRow >= 0) && (this._currentRow < this._rows.length)) {
55
+ return this._rows[this._currentRow];
56
+ }
57
+ else {
58
+ return EMPTY_ROW;
59
+ }
60
+ }
61
+ }
62
+ /**
63
+ * Implementation of Postgresql-database.
64
+ *
65
+ */
66
+ export class OINODbPostgresql extends OINODb {
67
+ static table_schema_sql = `SELECT
68
+ col.column_name,
69
+ col.data_type,
70
+ col.character_maximum_length,
71
+ col.is_nullable,
72
+ pk.primary_key,
73
+ col.numeric_precision,
74
+ col.numeric_scale,
75
+ col.column_default
76
+ FROM information_schema.columns col
77
+ LEFT JOIN LATERAL
78
+ (select kcu.column_name, 'YES' as primary_key
79
+ from
80
+ information_schema.table_constraints tco,
81
+ information_schema.key_column_usage kcu
82
+ where
83
+ kcu.constraint_name = tco.constraint_name
84
+ and kcu.constraint_schema = tco.constraint_schema
85
+ and tco.table_name = col.table_name
86
+ and tco.constraint_type = 'PRIMARY KEY'
87
+ ) pk on col.column_name = pk.column_name
88
+ WHERE table_name = `;
89
+ // private _client:Client
90
+ _pool;
91
+ /**
92
+ * Constructor of `OINODbPostgresql`
93
+ * @param params database paraneters
94
+ */
95
+ constructor(params) {
96
+ super(params);
97
+ // OINOLog.debug("OINODbPostgresql.constructor", {params:params})
98
+ if (this._params.type !== "OINODbPostgresql") {
99
+ throw new Error(OINO_ERROR_PREFIX + ": Not OINODbPostgresql-type: " + this._params.type);
100
+ }
101
+ this._pool = new Pool({ host: params.url, database: params.database, port: params.port, user: params.user, password: params.password });
102
+ this._pool.on("error", (err) => {
103
+ OINOLog.error("OINODbPostgresql error", { err: err });
104
+ });
105
+ this._pool.on("connect", (message) => {
106
+ // OINOLog.info("OINODbPostgresql connect")
107
+ });
108
+ this._pool.on("release", (message) => {
109
+ // OINOLog.info("OINODbPostgresql notice")
110
+ });
111
+ this._pool.on("acquire", () => {
112
+ // OINOLog.info("OINODbPostgresql end")
113
+ });
114
+ }
115
+ _parseFieldLength(fieldLength) {
116
+ let result = parseInt((fieldLength || "0").toString());
117
+ if (Number.isNaN(result)) {
118
+ result = 0;
119
+ }
120
+ return result;
121
+ }
122
+ async _query(sql) {
123
+ // OINOLog.debug("OINODbPostgresql._query", {sql:sql})
124
+ const query_result = await this._pool.query({ rowMode: "array", text: sql });
125
+ // OINOLog.debug("OINODbPostgresql._query", {result:query_result})
126
+ return Promise.resolve(query_result.rows);
127
+ }
128
+ async _exec(sql) {
129
+ // OINOLog.debug("OINODbPostgresql._query", {sql:sql})
130
+ const query_result = await this._pool.query({ rowMode: "array", text: sql });
131
+ // OINOLog.debug("OINODbPostgresql._query", {result:query_result})
132
+ return Promise.resolve(query_result.rows);
133
+ }
134
+ /**
135
+ * Print a table name using database specific SQL escaping.
136
+ *
137
+ * @param sqlTable name of the table
138
+ *
139
+ */
140
+ printSqlTablename(sqlTable) {
141
+ return "\"" + sqlTable.toLowerCase() + "\"";
142
+ }
143
+ /**
144
+ * Print a column name with correct SQL escaping.
145
+ *
146
+ * @param sqlColumn name of the column
147
+ *
148
+ */
149
+ printSqlColumnname(sqlColumn) {
150
+ return "\"" + sqlColumn + "\"";
151
+ }
152
+ /**
153
+ * Print a single data value from serialization using the context of the native data
154
+ * type with the correct SQL escaping.
155
+ *
156
+ * @param cellValue data from sql results
157
+ * @param sqlType native type name for table column
158
+ *
159
+ */
160
+ printCellAsSqlValue(cellValue, sqlType) {
161
+ if (cellValue === null) {
162
+ return "NULL";
163
+ }
164
+ else if (cellValue === undefined) {
165
+ return "UNDEFINED";
166
+ }
167
+ else if ((sqlType == "integer") || (sqlType == "smallint") || (sqlType == "real")) {
168
+ return cellValue.toString();
169
+ }
170
+ else if (sqlType == "bytea") {
171
+ return "\'" + cellValue + "\'";
172
+ }
173
+ else if (sqlType == "boolean") {
174
+ if (cellValue == null || cellValue == "" || cellValue.toString().toLowerCase() == "false" || cellValue == "0") {
175
+ return "false";
176
+ }
177
+ else {
178
+ return "true";
179
+ }
180
+ }
181
+ else if ((sqlType == "date") && (cellValue instanceof Date)) {
182
+ return "\'" + cellValue.toISOString() + "\'";
183
+ }
184
+ else {
185
+ return "\'" + cellValue?.toString().replaceAll("'", "''") + "\'";
186
+ }
187
+ }
188
+ /**
189
+ * Parse a single SQL result value for serialization using the context of the native data
190
+ * type.
191
+ *
192
+ * @param sqlValue data from serialization
193
+ * @param sqlType native type name for table column
194
+ *
195
+ */
196
+ parseSqlValueAsCell(sqlValue, sqlType) {
197
+ if ((sqlValue === null) || (sqlValue == "NULL")) {
198
+ return null;
199
+ }
200
+ else if ((sqlValue === undefined)) {
201
+ return undefined;
202
+ }
203
+ else if (((sqlType == "date")) && (typeof (sqlValue) == "string")) {
204
+ return new Date(sqlValue);
205
+ }
206
+ else {
207
+ return sqlValue;
208
+ }
209
+ }
210
+ /**
211
+ * Connect to database.
212
+ *
213
+ */
214
+ async connect() {
215
+ try {
216
+ // make sure that any items are correctly URL encoded in the connection string
217
+ // OINOLog.debug("OINODbPostgresql.connect")
218
+ // await this._pool.connect()
219
+ // await this._client.connect()
220
+ return Promise.resolve(true);
221
+ }
222
+ catch (err) {
223
+ // ... error checks
224
+ throw new Error(OINO_ERROR_PREFIX + ": Error connecting to Postgresql server: " + err);
225
+ }
226
+ }
227
+ /**
228
+ * Execute a select operation.
229
+ *
230
+ * @param sql SQL statement.
231
+ *
232
+ */
233
+ async sqlSelect(sql) {
234
+ OINOBenchmark.start("sqlSelect");
235
+ let result;
236
+ try {
237
+ const rows = await this._query(sql);
238
+ // OINOLog.debug("OINODbPostgresql.sqlSelect", {rows:rows})
239
+ result = new OINOPostgresqlData(rows, []);
240
+ }
241
+ catch (e) {
242
+ result = new OINOPostgresqlData([[]], [OINO_ERROR_PREFIX + " (sqlSelect): exception in _db.query [" + e.message + "]"]);
243
+ }
244
+ OINOBenchmark.end("sqlSelect");
245
+ return result;
246
+ }
247
+ /**
248
+ * Execute other sql operations.
249
+ *
250
+ * @param sql SQL statement.
251
+ *
252
+ */
253
+ async sqlExec(sql) {
254
+ OINOBenchmark.start("sqlExec");
255
+ let result;
256
+ try {
257
+ const rows = await this._exec(sql);
258
+ // OINOLog.debug("OINODbPostgresql.sqlExec", {rows:rows})
259
+ result = new OINOPostgresqlData(rows, []);
260
+ }
261
+ catch (e) {
262
+ result = new OINOPostgresqlData([[]], [OINO_ERROR_PREFIX + " (sqlExec): exception in _db.exec [" + e.message + "]"]);
263
+ }
264
+ OINOBenchmark.end("sqlExec");
265
+ return result;
266
+ }
267
+ /**
268
+ * Initialize a data model by getting the SQL schema and populating OINODbDataFields of
269
+ * the model.
270
+ *
271
+ * @param api api which data model to initialize.
272
+ *
273
+ */
274
+ async initializeApiDatamodel(api) {
275
+ const res = await this.sqlSelect(OINODbPostgresql.table_schema_sql + "'" + api.params.tableName.toLowerCase() + "';");
276
+ // OINOLog.debug("OINODbPostgresql.initializeApiDatamodel: table description ", {res: res })
277
+ while (!res.isEof()) {
278
+ const row = res.getRow();
279
+ // OINOLog.debug("OINODbPostgresql.initializeApiDatamodel: next row ", {row: row })
280
+ const field_name = row[0]?.toString() || "";
281
+ const sql_type = row[1]?.toString() || "";
282
+ const field_length = this._parseFieldLength(row[2]);
283
+ const numeric_precision = this._parseFieldLength(row[5]);
284
+ const numeric_scale = this._parseFieldLength(row[6]);
285
+ const default_val = row[7]?.toString() || "";
286
+ const field_params = {
287
+ isPrimaryKey: row[4] == "YES",
288
+ isNotNull: row[3] == "NO",
289
+ isAutoInc: default_val.startsWith("nextval(")
290
+ };
291
+ if ((!api.params.excludeFieldPrefix || !field_name.startsWith(api.params.excludeFieldPrefix)) && (!api.params.excludeFields || (api.params.excludeFields.indexOf(field_name) < 0))) {
292
+ // OINOLog.debug("OINODbPostgresql.initializeApiDatamodel: next field ", {field_name: field_name, sql_type:sql_type, field_length:field_length, field_params:field_params })
293
+ if ((sql_type == "integer") || (sql_type == "smallint") || (sql_type == "real")) {
294
+ api.datamodel.addField(new OINONumberDataField(this, field_name, sql_type, field_params));
295
+ }
296
+ else if ((sql_type == "date")) {
297
+ if (api.params.useDatesAsString) {
298
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0));
299
+ }
300
+ else {
301
+ api.datamodel.addField(new OINODatetimeDataField(this, field_name, sql_type, field_params));
302
+ }
303
+ }
304
+ else if ((sql_type == "character") || (sql_type == "character varying") || (sql_type == "varchar") || (sql_type == "text")) {
305
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, field_length));
306
+ }
307
+ else if ((sql_type == "bytea")) {
308
+ api.datamodel.addField(new OINOBlobDataField(this, field_name, sql_type, field_params, field_length));
309
+ }
310
+ else if ((sql_type == "boolean")) {
311
+ api.datamodel.addField(new OINOBooleanDataField(this, field_name, sql_type, field_params));
312
+ }
313
+ else if ((sql_type == "decimal") || (sql_type == "numeric")) {
314
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, numeric_precision + numeric_scale + 1));
315
+ }
316
+ else {
317
+ OINOLog.info("OINODbPostgresql.initializeApiDatamodel: unrecognized field type treated as string", { field_name: field_name, sql_type: sql_type, field_length: field_length, field_params: field_params });
318
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0));
319
+ }
320
+ }
321
+ res.next();
322
+ }
323
+ OINOLog.debug("OINODbPostgresql.initializeDatasetModel:\n" + api.datamodel.printDebug("\n"));
324
+ return Promise.resolve();
325
+ }
326
+ }
@@ -0,0 +1 @@
1
+ export { OINODbPostgresql } from "./OINODbPostgresql.js";
@@ -0,0 +1,76 @@
1
+ import { OINODb, OINODbParams, OINODbDataSet, OINODbApi, OINODataCell } from "@oino-ts/db";
2
+ /**
3
+ * Implementation of Postgresql-database.
4
+ *
5
+ */
6
+ export declare class OINODbPostgresql extends OINODb {
7
+ private static table_schema_sql;
8
+ private _pool;
9
+ /**
10
+ * Constructor of `OINODbPostgresql`
11
+ * @param params database paraneters
12
+ */
13
+ constructor(params: OINODbParams);
14
+ private _parseFieldLength;
15
+ private _query;
16
+ private _exec;
17
+ /**
18
+ * Print a table name using database specific SQL escaping.
19
+ *
20
+ * @param sqlTable name of the table
21
+ *
22
+ */
23
+ printSqlTablename(sqlTable: string): string;
24
+ /**
25
+ * Print a column name with correct SQL escaping.
26
+ *
27
+ * @param sqlColumn name of the column
28
+ *
29
+ */
30
+ printSqlColumnname(sqlColumn: string): string;
31
+ /**
32
+ * Print a single data value from serialization using the context of the native data
33
+ * type with the correct SQL escaping.
34
+ *
35
+ * @param cellValue data from sql results
36
+ * @param sqlType native type name for table column
37
+ *
38
+ */
39
+ printCellAsSqlValue(cellValue: OINODataCell, sqlType: string): string;
40
+ /**
41
+ * Parse a single SQL result value for serialization using the context of the native data
42
+ * type.
43
+ *
44
+ * @param sqlValue data from serialization
45
+ * @param sqlType native type name for table column
46
+ *
47
+ */
48
+ parseSqlValueAsCell(sqlValue: OINODataCell, sqlType: string): OINODataCell;
49
+ /**
50
+ * Connect to database.
51
+ *
52
+ */
53
+ connect(): Promise<boolean>;
54
+ /**
55
+ * Execute a select operation.
56
+ *
57
+ * @param sql SQL statement.
58
+ *
59
+ */
60
+ sqlSelect(sql: string): Promise<OINODbDataSet>;
61
+ /**
62
+ * Execute other sql operations.
63
+ *
64
+ * @param sql SQL statement.
65
+ *
66
+ */
67
+ sqlExec(sql: string): Promise<OINODbDataSet>;
68
+ /**
69
+ * Initialize a data model by getting the SQL schema and populating OINODbDataFields of
70
+ * the model.
71
+ *
72
+ * @param api api which data model to initialize.
73
+ *
74
+ */
75
+ initializeApiDatamodel(api: OINODbApi): Promise<void>;
76
+ }
@@ -0,0 +1 @@
1
+ export { OINODbPostgresql } from "./OINODbPostgresql.js";
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@oino-ts/db-postgresql",
3
+ "version": "0.0.11",
4
+ "description": "OINO TS package for using Postgresql 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
+ "postgresql"
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.0.11",
24
+ "pg": "^8.11.3"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^20.12.7",
28
+ "@types/pg": "^8.11.5",
29
+ "@types/bun": "latest"
30
+ },
31
+ "files": [
32
+ "src/*.ts",
33
+ "dist/cjs/*.js",
34
+ "dist/esm/*.js",
35
+ "dist/types/*.d.ts"
36
+ ]
37
+ }
@@ -0,0 +1,351 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+ import { OINODb, OINODbParams, OINODbDataSet, OINODbApi, OINOBooleanDataField, OINONumberDataField, OINOStringDataField, OINODbDataFieldParams, OINO_ERROR_PREFIX, OINODataRow, OINODataCell, OINOBenchmark, OINODatetimeDataField, OINOBlobDataField, OINOLog } from "@oino-ts/db";
8
+
9
+ import { Pool, QueryResult } from "pg";
10
+
11
+ const EMPTY_ROW:string[] = []
12
+
13
+ /**
14
+ * Implmentation of OINODbDataSet for Postgresql.
15
+ *
16
+ */
17
+ class OINOPostgresqlData extends OINODbDataSet {
18
+ private _rows:OINODataRow[]
19
+
20
+ /**
21
+ * OINOPostgresqlData constructor
22
+ * @param params database parameters
23
+ */
24
+ constructor(data: unknown, messages:string[]=[]) {
25
+ super(data, messages)
26
+
27
+ if ((data != null) && !(Array.isArray(data))) {
28
+ throw new Error(OINO_ERROR_PREFIX + ": Invalid Posgresql data type!") // TODO: maybe check all rows
29
+ }
30
+ this._rows = data as OINODataRow[]
31
+ if (this.isEmpty()) {
32
+ this._currentRow = -1
33
+ this._eof = true
34
+ } else {
35
+ this._currentRow = 0
36
+ this._eof = false
37
+ }
38
+ }
39
+ private _currentRow: number
40
+ private _eof: boolean
41
+ isEmpty():boolean {
42
+ return (this._rows.length == 0)
43
+ }
44
+
45
+ // EOF means "there is no more content", i.e. either dataset is empty or we have moved beyond last line
46
+ isEof():boolean {
47
+ return (this._eof)
48
+ }
49
+
50
+ next():boolean {
51
+ // OINOLog.debug("OINODbDataSet.next", {currentRow:this._currentRow, length:this.sqlResult.data.length})
52
+ if (this._currentRow < this._rows.length-1) {
53
+ this._currentRow = this._currentRow + 1
54
+ } else {
55
+ this._eof = true
56
+ }
57
+ return !this._eof
58
+ }
59
+
60
+ getRow(): OINODataRow {
61
+ if ((this._currentRow >=0) && (this._currentRow < this._rows.length)) {
62
+ return this._rows[this._currentRow]
63
+ } else {
64
+ return EMPTY_ROW
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Implementation of Postgresql-database.
71
+ *
72
+ */
73
+ export class OINODbPostgresql extends OINODb {
74
+
75
+ private static table_schema_sql:string =
76
+ `SELECT
77
+ col.column_name,
78
+ col.data_type,
79
+ col.character_maximum_length,
80
+ col.is_nullable,
81
+ pk.primary_key,
82
+ col.numeric_precision,
83
+ col.numeric_scale,
84
+ col.column_default
85
+ FROM information_schema.columns col
86
+ LEFT JOIN LATERAL
87
+ (select kcu.column_name, 'YES' as primary_key
88
+ from
89
+ information_schema.table_constraints tco,
90
+ information_schema.key_column_usage kcu
91
+ where
92
+ kcu.constraint_name = tco.constraint_name
93
+ and kcu.constraint_schema = tco.constraint_schema
94
+ and tco.table_name = col.table_name
95
+ and tco.constraint_type = 'PRIMARY KEY'
96
+ ) pk on col.column_name = pk.column_name
97
+ WHERE table_name = `
98
+
99
+ // private _client:Client
100
+ private _pool:Pool
101
+
102
+ /**
103
+ * Constructor of `OINODbPostgresql`
104
+ * @param params database paraneters
105
+ */
106
+ constructor(params:OINODbParams) {
107
+ super(params)
108
+
109
+ // OINOLog.debug("OINODbPostgresql.constructor", {params:params})
110
+ if (this._params.type !== "OINODbPostgresql") {
111
+ throw new Error(OINO_ERROR_PREFIX + ": Not OINODbPostgresql-type: " + this._params.type)
112
+ }
113
+ this._pool = new Pool({ host: params.url, database: params.database, port: params.port, user: params.user, password: params.password })
114
+ this._pool.on("error", (err: any) => {
115
+ OINOLog.error("OINODbPostgresql error", {err:err})
116
+ })
117
+ this._pool.on("connect", (message: any) => {
118
+ // OINOLog.info("OINODbPostgresql connect")
119
+ })
120
+ this._pool.on("release", (message: any) => {
121
+ // OINOLog.info("OINODbPostgresql notice")
122
+ })
123
+ this._pool.on("acquire", () => {
124
+ // OINOLog.info("OINODbPostgresql end")
125
+ })
126
+ }
127
+
128
+ private _parseFieldLength(fieldLength:OINODataCell):number {
129
+ let result:number = parseInt((fieldLength || "0").toString())
130
+ if (Number.isNaN(result)) {
131
+ result = 0
132
+ }
133
+ return result
134
+ }
135
+
136
+ private async _query(sql:string):Promise<OINODataRow[]> {
137
+ // OINOLog.debug("OINODbPostgresql._query", {sql:sql})
138
+ const query_result:QueryResult = await this._pool.query({rowMode: "array", text: sql})
139
+ // OINOLog.debug("OINODbPostgresql._query", {result:query_result})
140
+ return Promise.resolve(query_result.rows)
141
+ }
142
+
143
+ private async _exec(sql:string):Promise<OINODataRow[]> {
144
+ // OINOLog.debug("OINODbPostgresql._query", {sql:sql})
145
+ const query_result:QueryResult = await this._pool.query({rowMode: "array", text: sql})
146
+ // OINOLog.debug("OINODbPostgresql._query", {result:query_result})
147
+ return Promise.resolve(query_result.rows)
148
+ }
149
+
150
+ /**
151
+ * Print a table name using database specific SQL escaping.
152
+ *
153
+ * @param sqlTable name of the table
154
+ *
155
+ */
156
+ printSqlTablename(sqlTable:string): string {
157
+ return "\""+sqlTable.toLowerCase()+"\""
158
+ }
159
+
160
+ /**
161
+ * Print a column name with correct SQL escaping.
162
+ *
163
+ * @param sqlColumn name of the column
164
+ *
165
+ */
166
+ printSqlColumnname(sqlColumn:string): string {
167
+ return "\""+sqlColumn+"\""
168
+ }
169
+
170
+ /**
171
+ * Print a single data value from serialization using the context of the native data
172
+ * type with the correct SQL escaping.
173
+ *
174
+ * @param cellValue data from sql results
175
+ * @param sqlType native type name for table column
176
+ *
177
+ */
178
+ printCellAsSqlValue(cellValue:OINODataCell, sqlType: string): string {
179
+ if (cellValue === null) {
180
+ return "NULL"
181
+
182
+ } else if (cellValue === undefined) {
183
+ return "UNDEFINED"
184
+
185
+ } else if ((sqlType == "integer") || (sqlType == "smallint") || (sqlType == "real")) {
186
+ return cellValue.toString()
187
+
188
+ } else if (sqlType == "bytea") {
189
+ return "\'" + cellValue + "\'"
190
+
191
+ } else if (sqlType == "boolean") {
192
+ if (cellValue == null || cellValue == "" || cellValue.toString().toLowerCase() == "false" || cellValue == "0") {
193
+ return "false"
194
+ } else {
195
+ return "true"
196
+ }
197
+
198
+ } else if ((sqlType == "date") && (cellValue instanceof Date)) {
199
+ return "\'" + cellValue.toISOString() + "\'"
200
+
201
+ } else {
202
+ return "\'" + cellValue?.toString().replaceAll("'", "''") + "\'"
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Parse a single SQL result value for serialization using the context of the native data
208
+ * type.
209
+ *
210
+ * @param sqlValue data from serialization
211
+ * @param sqlType native type name for table column
212
+ *
213
+ */
214
+ parseSqlValueAsCell(sqlValue:OINODataCell, sqlType: string): OINODataCell {
215
+ if ((sqlValue === null) || (sqlValue == "NULL")) {
216
+ return null
217
+
218
+ } else if ((sqlValue === undefined)) {
219
+ return undefined
220
+
221
+ } else if (((sqlType == "date")) && (typeof(sqlValue) == "string")) {
222
+ return new Date(sqlValue)
223
+
224
+ } else {
225
+ return sqlValue
226
+ }
227
+
228
+ }
229
+
230
+ /**
231
+ * Connect to database.
232
+ *
233
+ */
234
+ async connect(): Promise<boolean> {
235
+ try {
236
+ // make sure that any items are correctly URL encoded in the connection string
237
+ // OINOLog.debug("OINODbPostgresql.connect")
238
+ // await this._pool.connect()
239
+ // await this._client.connect()
240
+ return Promise.resolve(true)
241
+ } catch (err) {
242
+ // ... error checks
243
+ throw new Error(OINO_ERROR_PREFIX + ": Error connecting to Postgresql server: " + err)
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Execute a select operation.
249
+ *
250
+ * @param sql SQL statement.
251
+ *
252
+ */
253
+ async sqlSelect(sql:string): Promise<OINODbDataSet> {
254
+ OINOBenchmark.start("sqlSelect")
255
+ let result:OINODbDataSet
256
+ try {
257
+ const rows:OINODataRow[] = await this._query(sql)
258
+ // OINOLog.debug("OINODbPostgresql.sqlSelect", {rows:rows})
259
+ result = new OINOPostgresqlData(rows, [])
260
+
261
+ } catch (e:any) {
262
+ result = new OINOPostgresqlData([[]], [OINO_ERROR_PREFIX + " (sqlSelect): exception in _db.query [" + e.message + "]"])
263
+ }
264
+ OINOBenchmark.end("sqlSelect")
265
+ return result
266
+ }
267
+
268
+ /**
269
+ * Execute other sql operations.
270
+ *
271
+ * @param sql SQL statement.
272
+ *
273
+ */
274
+ async sqlExec(sql:string): Promise<OINODbDataSet> {
275
+ OINOBenchmark.start("sqlExec")
276
+ let result:OINODbDataSet
277
+ try {
278
+ const rows:OINODataRow[] = await this._exec(sql)
279
+ // OINOLog.debug("OINODbPostgresql.sqlExec", {rows:rows})
280
+ result = new OINOPostgresqlData(rows, [])
281
+
282
+ } catch (e:any) {
283
+ result = new OINOPostgresqlData([[]], [OINO_ERROR_PREFIX + " (sqlExec): exception in _db.exec [" + e.message + "]"])
284
+ }
285
+ OINOBenchmark.end("sqlExec")
286
+ return result
287
+ }
288
+
289
+ /**
290
+ * Initialize a data model by getting the SQL schema and populating OINODbDataFields of
291
+ * the model.
292
+ *
293
+ * @param api api which data model to initialize.
294
+ *
295
+ */
296
+ async initializeApiDatamodel(api:OINODbApi): Promise<void> {
297
+
298
+ const res:OINODbDataSet = await this.sqlSelect(OINODbPostgresql.table_schema_sql + "'" + api.params.tableName.toLowerCase() + "';")
299
+ // OINOLog.debug("OINODbPostgresql.initializeApiDatamodel: table description ", {res: res })
300
+ while (!res.isEof()) {
301
+ const row:OINODataRow = res.getRow()
302
+ // OINOLog.debug("OINODbPostgresql.initializeApiDatamodel: next row ", {row: row })
303
+ const field_name:string = row[0]?.toString() || ""
304
+ const sql_type:string = row[1]?.toString() || ""
305
+ const field_length:number = this._parseFieldLength(row[2])
306
+ const numeric_precision:number = this._parseFieldLength(row[5])
307
+ const numeric_scale:number = this._parseFieldLength(row[6])
308
+ const default_val:string = row[7]?.toString() || ""
309
+ const field_params:OINODbDataFieldParams = {
310
+ isPrimaryKey: row[4] == "YES",
311
+ isNotNull: row[3] == "NO",
312
+ isAutoInc: default_val.startsWith("nextval(")
313
+ }
314
+ if ((!api.params.excludeFieldPrefix || !field_name.startsWith(api.params.excludeFieldPrefix)) && (!api.params.excludeFields || (api.params.excludeFields.indexOf(field_name) < 0))) {
315
+ // OINOLog.debug("OINODbPostgresql.initializeApiDatamodel: next field ", {field_name: field_name, sql_type:sql_type, field_length:field_length, field_params:field_params })
316
+ if ((sql_type == "integer") || (sql_type == "smallint") || (sql_type == "real")) {
317
+ api.datamodel.addField(new OINONumberDataField(this, field_name, sql_type, field_params ))
318
+
319
+ } else if ((sql_type == "date")) {
320
+ if (api.params.useDatesAsString) {
321
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
322
+ } else {
323
+ api.datamodel.addField(new OINODatetimeDataField(this, field_name, sql_type, field_params))
324
+ }
325
+
326
+ } else if ((sql_type == "character") || (sql_type == "character varying") || (sql_type == "varchar") || (sql_type == "text")) {
327
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, field_length))
328
+
329
+ } else if ((sql_type == "bytea")) {
330
+ api.datamodel.addField(new OINOBlobDataField(this, field_name, sql_type, field_params, field_length))
331
+
332
+ } else if ((sql_type == "boolean")) {
333
+ api.datamodel.addField(new OINOBooleanDataField(this, field_name, sql_type, field_params))
334
+
335
+ } else if ((sql_type == "decimal") || (sql_type == "numeric")) {
336
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, numeric_precision + numeric_scale + 1))
337
+
338
+ } else {
339
+ OINOLog.info("OINODbPostgresql.initializeApiDatamodel: unrecognized field type treated as string", {field_name: field_name, sql_type:sql_type, field_length:field_length, field_params:field_params })
340
+ api.datamodel.addField(new OINOStringDataField(this, field_name, sql_type, field_params, 0))
341
+ }
342
+ }
343
+ res.next()
344
+ }
345
+ OINOLog.debug("OINODbPostgresql.initializeDatasetModel:\n" + api.datamodel.printDebug("\n"))
346
+ return Promise.resolve()
347
+ }
348
+ }
349
+
350
+
351
+
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { OINODbPostgresql } from "./OINODbPostgresql.js"