@oino-ts/types 0.0.16 → 0.0.18
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 +7 -42
- package/dist/cjs/OINOLog.js +12 -1
- package/dist/cjs/OINOParser.js +466 -0
- package/dist/cjs/OINOResult.js +7 -0
- package/dist/cjs/index.js +3 -1
- package/dist/esm/OINOLog.js +12 -1
- package/dist/esm/OINOParser.js +462 -0
- package/dist/esm/OINOResult.js +7 -0
- package/dist/esm/index.js +1 -0
- package/dist/types/OINOParser.d.ts +53 -0
- package/dist/types/OINOResult.d.ts +5 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/OINOLog.ts +9 -1
- package/src/OINOParser.ts +458 -0
- package/src/OINOResult.ts +8 -0
- package/src/index.ts +1 -0
package/README.md
CHANGED
|
@@ -59,20 +59,8 @@
|
|
|
59
59
|
|
|
60
60
|
## RESTfull
|
|
61
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
62
|
|
|
75
|
-
|
|
63
|
+
For example HTTP POST
|
|
76
64
|
```
|
|
77
65
|
Request and response:
|
|
78
66
|
> curl.exe -X POST http://localhost:3001/orderdetails -H "Content-Type: application/json" --data '[{\"OrderID\":11077,\"ProductID\":99,\"UnitPrice\":19,\"Quantity\":1,\"Discount\":0}]'
|
|
@@ -82,31 +70,12 @@
|
|
|
82
70
|
INSERT INTO [OrderDetails] ("OrderID","ProductID","UnitPrice","Quantity","Discount") VALUES (11077,99,19,1,0);
|
|
83
71
|
```
|
|
84
72
|
|
|
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
73
|
|
|
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
74
|
## Universal Serialization
|
|
106
75
|
OINO handles serialization of data to JSON/CSV/etc. and back based on the data model. It knows what columns exist, what is their data type and how to convert each to JSON/CSV and back. This allows also partial data to be sent, i.e. you can send only columns that need updating or even send extra columns and have them ignored.
|
|
107
76
|
|
|
108
77
|
### Features
|
|
109
|
-
- Files can be sent to BLOB fields using BASE64 encoding.
|
|
78
|
+
- Files can be sent to BLOB fields using BASE64 or MIME multipart encoding. Also supports standard HTML form file submission to blob fields and returning them data url images.
|
|
110
79
|
- Datetimes are (optionally) normalized to ISO 8601 format.
|
|
111
80
|
- Extended JSON-encoding
|
|
112
81
|
- Unquoted literal `undefined` can be used to represent non-existent values (leaving property out works too but preserving structure might be easier e.g. when translating data).
|
|
@@ -131,11 +100,11 @@
|
|
|
131
100
|
- Mariadb / Mysql-support through [mariadb](https://www.npmjs.com/package/mariadb)-package
|
|
132
101
|
- Sql Server through [mssql](https://www.npmjs.com/package/mssql)-package
|
|
133
102
|
|
|
134
|
-
##
|
|
103
|
+
## Composite Keys
|
|
135
104
|
To support tables with multipart primary keys OINO generates a composite key `_OINOID_` that is included in the result and can be used as the REST ID. For example in the example above table `OrderDetails` has two primary keys `OrderID` and `ProductID` making the `_OINOID_` of form `11077:99`.
|
|
136
105
|
|
|
137
106
|
## Power Of SQL
|
|
138
|
-
Since OINO is just generating SQL, WHERE-conditions can be defined with [`OINOSqlFilter`](https://pragmatta.github.io/oino-ts/classes/db_src.OINODbSqlFilter.html)
|
|
107
|
+
Since OINO is just generating SQL, WHERE-conditions can be defined with [`OINOSqlFilter`](https://pragmatta.github.io/oino-ts/classes/db_src.OINODbSqlFilter.html), order with [`OINOSqlOrder`](https://pragmatta.github.io/oino-ts/classes/db_src.OINODbSqlOrder.html) and limits/paging with [`OINOSqlLimit`](https://pragmatta.github.io/oino-ts/classes/db_src.OINODbSqlLimit.html) that are passed as HTTP request parameters. No more API development where you make unique API endpoints for each filter that fetch all data with original API and filter in backend code. Every API can be filtered when and as needed without unnessecary data tranfer and utilizing SQL indexing when available.
|
|
139
108
|
|
|
140
109
|
## Swagger Support
|
|
141
110
|
Swagger is great as long as the definitions are updated and with OINO you can automatically get a Swagger definition including a data model schema.
|
|
@@ -152,7 +121,7 @@
|
|
|
152
121
|
## HTMX support
|
|
153
122
|
OINO is [htmx.org](https://htmx.org) friendly, allowing easy translation of [`OINODataRow`](https://pragmatta.github.io/oino-ts/types/db_src.OINODataRow.html) to HTML output using templates (cf. the [htmx sample app](https://github.com/pragmatta/oino-ts/tree/main/samples/htmxApp)).
|
|
154
123
|
|
|
155
|
-
|
|
124
|
+
## Hashids
|
|
156
125
|
Autoinc numeric id's are very pragmatic and fit well with OINO (e.g. using a form without primary key fields to insert new rows with database assigned ids). However it's not always sensible to share information about the sequence. Hashids solve this by masking the original values by encrypting the ids using AES-128 and some randomness. Length of the hashid can be chosen from 12-32 characters where longer ids provide more security. However this should not be considereded a cryptographic solution for keeping ids secret but rather making it infeasible to iterate all ids.
|
|
157
126
|
|
|
158
127
|
|
|
@@ -164,9 +133,6 @@
|
|
|
164
133
|
|
|
165
134
|
### Realistic app
|
|
166
135
|
There needs to be a realistic app built on top of OINO to get a better grasp of the edge cases.
|
|
167
|
-
|
|
168
|
-
### Security review
|
|
169
|
-
Handling of SQL-injection attacks needs a thorough review, what are the relevant attack vectors are for OINO and what protections are still needed.
|
|
170
136
|
|
|
171
137
|
## Roadmap
|
|
172
138
|
Things that need to happen in some order before beta-status are at least following:
|
|
@@ -179,8 +145,8 @@
|
|
|
179
145
|
### Batch updates
|
|
180
146
|
Supporting batch updates similar to batch inserts is slightly bending the RESTfull principles but would still be a useful optional feature.
|
|
181
147
|
|
|
182
|
-
### Aggregation
|
|
183
|
-
Similar to filtering and
|
|
148
|
+
### Aggregation
|
|
149
|
+
Similar to filtering, ordering and limits, aggregation could be implemented as HTTP request parameters telling what column is aggregated or used for ordering or how many results to return.
|
|
184
150
|
|
|
185
151
|
### Streaming
|
|
186
152
|
One core idea is to be efficient in not making unnecessary copies of the data and minimizing garbage collection debt. This can be taken further by implementing streaming, allowing large dataset to be written to HTTP response as SQL result rows are received.
|
|
@@ -222,4 +188,3 @@
|
|
|
222
188
|
|
|
223
189
|
## SQL Scripts
|
|
224
190
|
The SQL scripts for creating the sample Northwind database are based on [Google Code archive](https://code.google.com/archive/p/northwindextended/downloads) and have been further customized to ensure they would have identical data (in the scope of the automated testing).
|
|
225
|
-
|
package/dist/cjs/OINOLog.js
CHANGED
|
@@ -134,7 +134,18 @@ class OINOConsoleLog extends OINOLog {
|
|
|
134
134
|
if (data) {
|
|
135
135
|
log += " " + JSON.stringify(data);
|
|
136
136
|
}
|
|
137
|
-
|
|
137
|
+
if (level == "ERROR") {
|
|
138
|
+
console.error(log);
|
|
139
|
+
}
|
|
140
|
+
else if (level == "WARN") {
|
|
141
|
+
console.warn(log);
|
|
142
|
+
}
|
|
143
|
+
else if (level == "INFO") {
|
|
144
|
+
console.info(log);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
console.log(log);
|
|
148
|
+
}
|
|
138
149
|
}
|
|
139
150
|
}
|
|
140
151
|
exports.OINOConsoleLog = OINOConsoleLog;
|
|
@@ -0,0 +1,466 @@
|
|
|
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.OINOParser = void 0;
|
|
9
|
+
const index_js_1 = require("../../db/src/index.js");
|
|
10
|
+
/**
|
|
11
|
+
* Static factory class for easily creating things based on data
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
class OINOParser {
|
|
15
|
+
/**
|
|
16
|
+
* Create data rows from request body based on the datamodel.
|
|
17
|
+
*
|
|
18
|
+
* @param datamodel datamodel of the api
|
|
19
|
+
* @param data data as a string
|
|
20
|
+
* @param requestParams parameters
|
|
21
|
+
*
|
|
22
|
+
*/
|
|
23
|
+
static createRows(datamodel, data, requestParams) {
|
|
24
|
+
let result = [];
|
|
25
|
+
if (typeof data == "string") {
|
|
26
|
+
result = this.createRowsFromText(datamodel, data, requestParams);
|
|
27
|
+
}
|
|
28
|
+
else if (data instanceof Buffer) {
|
|
29
|
+
result = this.createRowsFromBlob(datamodel, data, requestParams);
|
|
30
|
+
}
|
|
31
|
+
else if (typeof data == "object") {
|
|
32
|
+
result = [this.createRowFromObject(datamodel, data)];
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create data rows from request body based on the datamodel.
|
|
38
|
+
*
|
|
39
|
+
* @param datamodel datamodel of the api
|
|
40
|
+
* @param data data as a string
|
|
41
|
+
* @param requestParams parameters
|
|
42
|
+
*
|
|
43
|
+
*/
|
|
44
|
+
static createRowsFromText(datamodel, data, requestParams) {
|
|
45
|
+
if ((requestParams.requestType == index_js_1.OINOContentType.json) || (requestParams.requestType == undefined)) {
|
|
46
|
+
return this._createRowFromJson(datamodel, data);
|
|
47
|
+
}
|
|
48
|
+
else if (requestParams.requestType == index_js_1.OINOContentType.csv) {
|
|
49
|
+
return this._createRowFromCsv(datamodel, data);
|
|
50
|
+
}
|
|
51
|
+
else if (requestParams.requestType == index_js_1.OINOContentType.formdata) {
|
|
52
|
+
return this._createRowFromFormdata(datamodel, Buffer.from(data, "utf8"), requestParams.multipartBoundary || "");
|
|
53
|
+
}
|
|
54
|
+
else if (requestParams.requestType == index_js_1.OINOContentType.urlencode) {
|
|
55
|
+
return this._createRowFromUrlencoded(datamodel, data);
|
|
56
|
+
}
|
|
57
|
+
else if (requestParams.requestType == index_js_1.OINOContentType.html) {
|
|
58
|
+
index_js_1.OINOLog.error("HTML can't be used as an input content type!", { contentType: index_js_1.OINOContentType.html });
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
index_js_1.OINOLog.error("Unrecognized input content type!", { contentType: requestParams.requestType });
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Create data rows from request body based on the datamodel.
|
|
68
|
+
*
|
|
69
|
+
* @param datamodel datamodel of the api
|
|
70
|
+
* @param data data as an Buffer
|
|
71
|
+
* @param requestParams parameters
|
|
72
|
+
*
|
|
73
|
+
*/
|
|
74
|
+
static createRowsFromBlob(datamodel, data, requestParams) {
|
|
75
|
+
if ((requestParams.requestType == index_js_1.OINOContentType.json) || (requestParams.requestType == undefined)) {
|
|
76
|
+
return this._createRowFromJson(datamodel, data.toString()); // JSON is always a string
|
|
77
|
+
}
|
|
78
|
+
else if (requestParams.requestType == index_js_1.OINOContentType.csv) {
|
|
79
|
+
return this._createRowFromCsv(datamodel, data.toString()); // binary data has to be base64 encoded so it's a string
|
|
80
|
+
}
|
|
81
|
+
else if (requestParams.requestType == index_js_1.OINOContentType.formdata) {
|
|
82
|
+
return this._createRowFromFormdata(datamodel, data, requestParams.multipartBoundary || "");
|
|
83
|
+
}
|
|
84
|
+
else if (requestParams.requestType == index_js_1.OINOContentType.urlencode) {
|
|
85
|
+
return this._createRowFromUrlencoded(datamodel, data.toString()); // data is urlencoded so it's a string
|
|
86
|
+
}
|
|
87
|
+
else if (requestParams.requestType == index_js_1.OINOContentType.html) {
|
|
88
|
+
index_js_1.OINOLog.error("HTML can't be used as an input content type!", { contentType: index_js_1.OINOContentType.html });
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
index_js_1.OINOLog.error("Unrecognized input content type!", { contentType: requestParams.requestType });
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Create one data row from javascript object based on the datamodel.
|
|
98
|
+
* NOTE! Data assumed to be unserialized i.e. of the native type (string, number, boolean, Buffer)
|
|
99
|
+
*
|
|
100
|
+
* @param datamodel datamodel of the api
|
|
101
|
+
* @param data data as javascript object
|
|
102
|
+
*
|
|
103
|
+
*/
|
|
104
|
+
static createRowFromObject(datamodel, data) {
|
|
105
|
+
const fields = datamodel.fields;
|
|
106
|
+
let result = new Array(fields.length);
|
|
107
|
+
for (let i = 0; i < fields.length; i++) {
|
|
108
|
+
result[i] = data[fields[i].name];
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
static _findCsvLineEnd(csvData, start) {
|
|
113
|
+
const n = csvData.length;
|
|
114
|
+
if (start >= n) {
|
|
115
|
+
return start;
|
|
116
|
+
}
|
|
117
|
+
let end = start;
|
|
118
|
+
let quote_open = false;
|
|
119
|
+
while (end < n) {
|
|
120
|
+
if (csvData[end] == "\"") {
|
|
121
|
+
if (!quote_open) {
|
|
122
|
+
quote_open = true;
|
|
123
|
+
}
|
|
124
|
+
else if ((end < n - 1) && (csvData[end + 1] == "\"")) {
|
|
125
|
+
end++;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
quote_open = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else if ((!quote_open) && (csvData[end] == "\r")) {
|
|
132
|
+
return end;
|
|
133
|
+
}
|
|
134
|
+
end++;
|
|
135
|
+
}
|
|
136
|
+
return n;
|
|
137
|
+
}
|
|
138
|
+
static _parseCsvLine(csvLine) {
|
|
139
|
+
let result = [];
|
|
140
|
+
const n = csvLine.length;
|
|
141
|
+
let start = 0;
|
|
142
|
+
let end = 0;
|
|
143
|
+
let quote_open = false;
|
|
144
|
+
let has_quotes = false;
|
|
145
|
+
let has_escaped_quotes = false;
|
|
146
|
+
let found_field = false;
|
|
147
|
+
while (end < n) {
|
|
148
|
+
if (csvLine[end] == "\"") {
|
|
149
|
+
if (!quote_open) {
|
|
150
|
+
quote_open = true;
|
|
151
|
+
}
|
|
152
|
+
else if ((end < n - 1) && (csvLine[end + 1] == "\"")) {
|
|
153
|
+
end++;
|
|
154
|
+
has_escaped_quotes = true;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
has_quotes = true;
|
|
158
|
+
quote_open = false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if ((!quote_open) && ((end == n - 1) || (csvLine[end] == ","))) {
|
|
162
|
+
found_field = true;
|
|
163
|
+
if (end == n - 1) {
|
|
164
|
+
end++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (found_field) {
|
|
168
|
+
// console.log("OINODB_csvParseLine: next field=" + csvLine.substring(start,end) + ", start="+start+", end="+end)
|
|
169
|
+
let field_str;
|
|
170
|
+
if (has_quotes) {
|
|
171
|
+
field_str = csvLine.substring(start + 1, end - 1);
|
|
172
|
+
}
|
|
173
|
+
else if (start == end) {
|
|
174
|
+
field_str = undefined;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
field_str = csvLine.substring(start, end);
|
|
178
|
+
if (field_str == "null") {
|
|
179
|
+
field_str = null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
result.push(field_str);
|
|
183
|
+
has_quotes = false;
|
|
184
|
+
has_escaped_quotes = true;
|
|
185
|
+
found_field = false;
|
|
186
|
+
start = end + 1;
|
|
187
|
+
}
|
|
188
|
+
end++;
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
static _createRowFromCsv(datamodel, data) {
|
|
193
|
+
let result = [];
|
|
194
|
+
const n = data.length;
|
|
195
|
+
let start = 0;
|
|
196
|
+
let end = this._findCsvLineEnd(data, start);
|
|
197
|
+
const header_str = data.substring(start, end);
|
|
198
|
+
const headers = this._parseCsvLine(header_str);
|
|
199
|
+
let field_to_header_mapping = new Array(datamodel.fields.length);
|
|
200
|
+
let headers_found = false;
|
|
201
|
+
for (let i = 0; i < field_to_header_mapping.length; i++) {
|
|
202
|
+
field_to_header_mapping[i] = headers.indexOf(datamodel.fields[i].name);
|
|
203
|
+
headers_found = headers_found || (field_to_header_mapping[i] >= 0);
|
|
204
|
+
}
|
|
205
|
+
// OINOLog.debug("createRowFromCsv", {headers:headers, field_to_header_mapping:field_to_header_mapping})
|
|
206
|
+
if (!headers_found) {
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
start = end + 1;
|
|
210
|
+
end = start;
|
|
211
|
+
while (end < n) {
|
|
212
|
+
while ((start < n) && ((data[start] == "\r") || (data[start] == "\n"))) {
|
|
213
|
+
start++;
|
|
214
|
+
}
|
|
215
|
+
if (start >= n) {
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
end = this._findCsvLineEnd(data, start);
|
|
219
|
+
const row_data = this._parseCsvLine(data.substring(start, end));
|
|
220
|
+
const row = new Array(field_to_header_mapping.length);
|
|
221
|
+
let has_data = false;
|
|
222
|
+
for (let i = 0; i < datamodel.fields.length; i++) {
|
|
223
|
+
const field = datamodel.fields[i];
|
|
224
|
+
let j = field_to_header_mapping[i];
|
|
225
|
+
let value = row_data[j];
|
|
226
|
+
if ((value === undefined) || (value === null)) { // null/undefined-decoding built into the parser
|
|
227
|
+
row[i] = value;
|
|
228
|
+
}
|
|
229
|
+
else if ((j >= 0) && (j < row_data.length)) {
|
|
230
|
+
value = index_js_1.OINOStr.decode(value, index_js_1.OINOContentType.csv);
|
|
231
|
+
if (value && (field.fieldParams.isPrimaryKey || field.fieldParams.isForeignKey) && (field instanceof index_js_1.OINONumberDataField) && (datamodel.api.hashid)) {
|
|
232
|
+
value = datamodel.api.hashid.decode(value);
|
|
233
|
+
}
|
|
234
|
+
row[i] = field.deserializeCell(value);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
row[i] = undefined;
|
|
238
|
+
}
|
|
239
|
+
has_data = has_data || (row[i] !== undefined);
|
|
240
|
+
}
|
|
241
|
+
// console.log("createRowFromCsv: next row=" + row)
|
|
242
|
+
if (has_data) {
|
|
243
|
+
result.push(row);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
index_js_1.OINOLog.warning("createRowFromCsv: empty row skipped");
|
|
247
|
+
}
|
|
248
|
+
start = end;
|
|
249
|
+
end = start;
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
static _createRowFromJsonObj(obj, datamodel) {
|
|
254
|
+
// console.log("createRowFromJsonObj: obj=" + JSON.stringify(obj))
|
|
255
|
+
const fields = datamodel.fields;
|
|
256
|
+
let result = new Array(fields.length);
|
|
257
|
+
let has_data = false;
|
|
258
|
+
// console.log("createRowFromJsonObj: " + result)
|
|
259
|
+
for (let i = 0; i < fields.length; i++) {
|
|
260
|
+
const field = fields[i];
|
|
261
|
+
let value = obj[field.name];
|
|
262
|
+
// console.log("createRowFromJsonObj: key=" + field.name + ", val=" + val)
|
|
263
|
+
if ((value === null) || (value === undefined)) { // must be checed first as null is an object
|
|
264
|
+
result[i] = value;
|
|
265
|
+
}
|
|
266
|
+
else if (Array.isArray(value) || typeof value === "object") {
|
|
267
|
+
result[i] = JSON.stringify(value).replaceAll("\"", "\\\""); // only single level deep objects, rest is handled as JSON-strings
|
|
268
|
+
}
|
|
269
|
+
else if (typeof value === "string") {
|
|
270
|
+
value = index_js_1.OINOStr.decode(value, index_js_1.OINOContentType.json);
|
|
271
|
+
if (value && (field.fieldParams.isPrimaryKey || field.fieldParams.isForeignKey) && (field instanceof index_js_1.OINONumberDataField) && (datamodel.api.hashid)) {
|
|
272
|
+
value = datamodel.api.hashid.decode(value);
|
|
273
|
+
}
|
|
274
|
+
result[i] = field.deserializeCell(value);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
result[i] = value; // value types are passed as-is
|
|
278
|
+
}
|
|
279
|
+
has_data = has_data || (result[i] !== undefined);
|
|
280
|
+
// console.log("createRowFromJsonObj: result["+i+"]=" + result[i])
|
|
281
|
+
}
|
|
282
|
+
// console.log("createRowFromJsonObj: " + result)
|
|
283
|
+
if (has_data) {
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
index_js_1.OINOLog.warning("createRowFromJsonObj: empty row skipped");
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
static _createRowFromJson(datamodel, data) {
|
|
292
|
+
let result = [];
|
|
293
|
+
// console.log("OINORowFactoryJson: data=" + data)
|
|
294
|
+
const obj = JSON.parse(data);
|
|
295
|
+
if (Array.isArray(obj)) {
|
|
296
|
+
obj.forEach(row => {
|
|
297
|
+
const data_row = this._createRowFromJsonObj(row, datamodel);
|
|
298
|
+
if (data_row !== undefined) {
|
|
299
|
+
result.push(data_row);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const data_row = this._createRowFromJsonObj(obj, datamodel);
|
|
305
|
+
if (data_row !== undefined) {
|
|
306
|
+
result.push(data_row);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
static _findMultipartBoundary(formData, multipartBoundary, start) {
|
|
312
|
+
let n = formData.indexOf(multipartBoundary, start);
|
|
313
|
+
if (n >= 0) {
|
|
314
|
+
n += multipartBoundary.length + 2;
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
n = formData.length;
|
|
318
|
+
}
|
|
319
|
+
return n;
|
|
320
|
+
}
|
|
321
|
+
static _parseMultipartLine(data, start) {
|
|
322
|
+
let line_end = data.indexOf('\r\n', start);
|
|
323
|
+
if (line_end >= start) {
|
|
324
|
+
return data.subarray(start, line_end).toString();
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
return '';
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
static _multipartHeaderRegex = /Content-Disposition\: (form-data|file); name=\"([^\"]+)\"(; filename=.*)?/i;
|
|
331
|
+
static _createRowFromFormdata(datamodel, data, multipartBoundary) {
|
|
332
|
+
let result = [];
|
|
333
|
+
try {
|
|
334
|
+
const n = data.length;
|
|
335
|
+
let start = this._findMultipartBoundary(data, multipartBoundary, 0);
|
|
336
|
+
let end = this._findMultipartBoundary(data, multipartBoundary, start);
|
|
337
|
+
// OINOLog.debug("createRowFromFormdata: enter", {start:start, end:end, multipartBoundary:multipartBoundary})
|
|
338
|
+
const row = new Array(datamodel.fields.length);
|
|
339
|
+
let has_data = false;
|
|
340
|
+
while (end < n) {
|
|
341
|
+
// OINOLog.debug("createRowFromFormdata: next block", {start:start, end:end, block:data.substring(start, end)})
|
|
342
|
+
let block_ok = true;
|
|
343
|
+
let l = this._parseMultipartLine(data, start);
|
|
344
|
+
// OINOLog.debug("createRowFromFormdata: next line", {start:start, end:end, line:l})
|
|
345
|
+
start += l.length + 2;
|
|
346
|
+
const header_matches = OINOParser._multipartHeaderRegex.exec(l);
|
|
347
|
+
if (!header_matches) {
|
|
348
|
+
index_js_1.OINOLog.warning("OINODbFactory.createRowFromFormdata: unsupported block skipped!", { header_line: l });
|
|
349
|
+
block_ok = false;
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
const field_name = header_matches[2];
|
|
353
|
+
const is_file = header_matches[3] != null;
|
|
354
|
+
let is_base64 = false;
|
|
355
|
+
const field_index = datamodel.findFieldIndexByName(field_name);
|
|
356
|
+
// OINOLog.debug("createRowFromFormdata: header", {field_name:field_name, field_index:field_index, is_file:is_file, is_base64:is_base64})
|
|
357
|
+
if (field_index < 0) {
|
|
358
|
+
index_js_1.OINOLog.warning("OINODbFactory.createRowFromFormdata: form field not found and skipped!", { field_name: field_name });
|
|
359
|
+
block_ok = false;
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
const field = datamodel.fields[field_index];
|
|
363
|
+
l = this._parseMultipartLine(data, start);
|
|
364
|
+
// OINOLog.debug("createRowFromFormdata: next line", {start:start, end:end, line:l})
|
|
365
|
+
while (block_ok && (l != '')) {
|
|
366
|
+
if (l.startsWith('Content-Type:') && (l.indexOf('multipart/mixed') >= 0)) {
|
|
367
|
+
index_js_1.OINOLog.warning("OINODbFactory.createRowFromFormdata: mixed multipart files not supported and skipped!", { header_line: l });
|
|
368
|
+
block_ok = false;
|
|
369
|
+
}
|
|
370
|
+
else if (l.startsWith('Content-Transfer-Encoding:') && (l.indexOf('BASE64') >= 0)) {
|
|
371
|
+
is_base64 = true;
|
|
372
|
+
}
|
|
373
|
+
start += l.length + 2;
|
|
374
|
+
l = this._parseMultipartLine(data, start);
|
|
375
|
+
// OINOLog.debug("createRowFromFormdata: next line", {start:start, end:end, line:l})
|
|
376
|
+
}
|
|
377
|
+
start += 2;
|
|
378
|
+
if (!block_ok) {
|
|
379
|
+
index_js_1.OINOLog.warning("OINODbFactory.createRowFromFormdata: invalid block skipped", { field_name: field_name });
|
|
380
|
+
}
|
|
381
|
+
else if (start + multipartBoundary.length + 2 >= end) {
|
|
382
|
+
// OINOLog.debug("OINODbFactory.createRowFromFormdata: null value", {field_name:field_name})
|
|
383
|
+
row[field_index] = null;
|
|
384
|
+
}
|
|
385
|
+
else if (is_file) {
|
|
386
|
+
if (is_base64) {
|
|
387
|
+
const value = this._parseMultipartLine(data, start).trim();
|
|
388
|
+
row[field_index] = field.deserializeCell(index_js_1.OINOStr.decode(value, index_js_1.OINOContentType.formdata));
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
const e = this._findMultipartBoundary(data, multipartBoundary, start);
|
|
392
|
+
const value = data.subarray(start, e - 2);
|
|
393
|
+
row[field_index] = value;
|
|
394
|
+
}
|
|
395
|
+
// console.log("OINODbFactory.createRowFromFormdata: file field", {field_name:field_name, value:row[field_index]})
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
let value = index_js_1.OINOStr.decode(this._parseMultipartLine(data, start).trim(), index_js_1.OINOContentType.formdata);
|
|
399
|
+
// OINOLog.debug("OINODbFactory.createRowFromFormdata: parse form field", {field_name:field_name, value:value})
|
|
400
|
+
if (value && (field.fieldParams.isPrimaryKey || field.fieldParams.isForeignKey) && (field instanceof index_js_1.OINONumberDataField) && (datamodel.api.hashid)) {
|
|
401
|
+
value = datamodel.api.hashid.decode(value);
|
|
402
|
+
}
|
|
403
|
+
row[field_index] = field.deserializeCell(value);
|
|
404
|
+
}
|
|
405
|
+
has_data = has_data || (row[field_index] !== undefined);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
start = end;
|
|
409
|
+
end = this._findMultipartBoundary(data, multipartBoundary, start);
|
|
410
|
+
}
|
|
411
|
+
// OINOLog.debug("createRowFromFormdata: next row", {row:row})
|
|
412
|
+
if (has_data) {
|
|
413
|
+
result.push(row);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
index_js_1.OINOLog.warning("createRowFromFormdata: empty row skipped");
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
catch (e) {
|
|
420
|
+
index_js_1.OINOLog.error("createRowFromFormdata: error parsing formdata", { exception: e.message });
|
|
421
|
+
}
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
static _createRowFromUrlencoded(datamodel, data) {
|
|
425
|
+
// OINOLog.debug("createRowFromUrlencoded: enter", {data:data})
|
|
426
|
+
let result = [];
|
|
427
|
+
const row = new Array(datamodel.fields.length);
|
|
428
|
+
let has_data = false;
|
|
429
|
+
const data_parts = data.trim().split('&');
|
|
430
|
+
try {
|
|
431
|
+
for (let i = 0; i < data_parts.length; i++) {
|
|
432
|
+
const param_parts = data_parts[i].split('=');
|
|
433
|
+
// OINOLog.debug("createRowFromUrlencoded: next param", {param_parts:param_parts})
|
|
434
|
+
if (param_parts.length == 2) {
|
|
435
|
+
const key = index_js_1.OINOStr.decodeUrlencode(param_parts[0]) || "";
|
|
436
|
+
const field_index = datamodel.findFieldIndexByName(key);
|
|
437
|
+
if (field_index < 0) {
|
|
438
|
+
index_js_1.OINOLog.info("createRowFromUrlencoded: param field not found", { field: key });
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
const field = datamodel.fields[field_index];
|
|
442
|
+
let value = index_js_1.OINOStr.decode(param_parts[1], index_js_1.OINOContentType.urlencode);
|
|
443
|
+
if (value && (field.fieldParams.isPrimaryKey || field.fieldParams.isForeignKey) && (field instanceof index_js_1.OINONumberDataField) && (datamodel.api.hashid)) {
|
|
444
|
+
value = datamodel.api.hashid.decode(value);
|
|
445
|
+
}
|
|
446
|
+
row[field_index] = field.deserializeCell(value);
|
|
447
|
+
has_data = has_data || (row[field_index] !== undefined);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// const value = requestParams[]
|
|
451
|
+
}
|
|
452
|
+
if (has_data) {
|
|
453
|
+
result.push(row);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
index_js_1.OINOLog.warning("createRowFromUrlencoded: empty row skipped");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch (e) {
|
|
460
|
+
index_js_1.OINOLog.error("createRowFromUrlencoded: error parsing urlencoded data", { exception: e.message });
|
|
461
|
+
}
|
|
462
|
+
// console.log("createRowFromUrlencoded: next row=" + row)
|
|
463
|
+
return result;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
exports.OINOParser = OINOParser;
|
package/dist/cjs/OINOResult.js
CHANGED
|
@@ -148,6 +148,13 @@ class OINOResult {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Print result for logging.
|
|
153
|
+
*
|
|
154
|
+
*/
|
|
155
|
+
printLog() {
|
|
156
|
+
return "OINOResult: statusCode=" + this.statusCode + ", statusMessage=" + this.statusMessage + ", messages=[" + this.messages.join(", ") + "]";
|
|
157
|
+
}
|
|
151
158
|
}
|
|
152
159
|
exports.OINOResult = OINOResult;
|
|
153
160
|
/**
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.OINOContentType = exports.OINO_DEBUG_PREFIX = exports.OINO_INFO_PREFIX = exports.OINO_WARNING_PREFIX = exports.OINO_ERROR_PREFIX = exports.OINOHtmlTemplate = exports.OINOStr = exports.OINOHttpResult = exports.OINOResult = exports.OINOConsoleLog = exports.OINOLogLevel = exports.OINOLog = exports.OINOBenchmark = void 0;
|
|
3
|
+
exports.OINOContentType = exports.OINO_DEBUG_PREFIX = exports.OINO_INFO_PREFIX = exports.OINO_WARNING_PREFIX = exports.OINO_ERROR_PREFIX = exports.OINOParser = exports.OINOHtmlTemplate = exports.OINOStr = exports.OINOHttpResult = exports.OINOResult = exports.OINOConsoleLog = exports.OINOLogLevel = exports.OINOLog = exports.OINOBenchmark = void 0;
|
|
4
4
|
var OINOBenchmark_js_1 = require("./OINOBenchmark.js");
|
|
5
5
|
Object.defineProperty(exports, "OINOBenchmark", { enumerable: true, get: function () { return OINOBenchmark_js_1.OINOBenchmark; } });
|
|
6
6
|
var OINOLog_js_1 = require("./OINOLog.js");
|
|
@@ -14,6 +14,8 @@ var OINOStr_js_1 = require("./OINOStr.js");
|
|
|
14
14
|
Object.defineProperty(exports, "OINOStr", { enumerable: true, get: function () { return OINOStr_js_1.OINOStr; } });
|
|
15
15
|
var OINOHtmlTemplate_js_1 = require("./OINOHtmlTemplate.js");
|
|
16
16
|
Object.defineProperty(exports, "OINOHtmlTemplate", { enumerable: true, get: function () { return OINOHtmlTemplate_js_1.OINOHtmlTemplate; } });
|
|
17
|
+
var OINOParser_1 = require("./OINOParser");
|
|
18
|
+
Object.defineProperty(exports, "OINOParser", { enumerable: true, get: function () { return OINOParser_1.OINOParser; } });
|
|
17
19
|
/** OINO error message prefix */
|
|
18
20
|
exports.OINO_ERROR_PREFIX = "OINO ERROR";
|
|
19
21
|
/** OINO warning message prefix */
|
package/dist/esm/OINOLog.js
CHANGED
|
@@ -130,6 +130,17 @@ export class OINOConsoleLog extends OINOLog {
|
|
|
130
130
|
if (data) {
|
|
131
131
|
log += " " + JSON.stringify(data);
|
|
132
132
|
}
|
|
133
|
-
|
|
133
|
+
if (level == "ERROR") {
|
|
134
|
+
console.error(log);
|
|
135
|
+
}
|
|
136
|
+
else if (level == "WARN") {
|
|
137
|
+
console.warn(log);
|
|
138
|
+
}
|
|
139
|
+
else if (level == "INFO") {
|
|
140
|
+
console.info(log);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
console.log(log);
|
|
144
|
+
}
|
|
134
145
|
}
|
|
135
146
|
}
|