@oino-ts/db 0.0.17 → 0.1.0
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 +4 -7
- package/dist/cjs/OINODb.js +132 -1
- package/dist/cjs/OINODbApi.js +15 -12
- package/dist/cjs/OINODbDataField.js +7 -1
- package/dist/cjs/OINODbDataModel.js +1 -1
- package/dist/cjs/OINODbFactory.js +0 -356
- package/dist/cjs/OINODbModelSet.js +1 -1
- package/dist/cjs/OINODbParser.js +466 -0
- package/dist/cjs/OINODbRequestParams.js +24 -18
- package/dist/cjs/OINODbSqlParams.js +336 -0
- package/dist/cjs/index.js +26 -25
- package/dist/esm/OINODb.js +129 -0
- package/dist/esm/OINODbApi.js +15 -12
- package/dist/esm/OINODbDataField.js +7 -1
- package/dist/esm/OINODbDataModel.js +2 -2
- package/dist/esm/OINODbFactory.js +1 -357
- package/dist/esm/OINODbModelSet.js +1 -1
- package/dist/esm/OINODbParser.js +462 -0
- package/dist/esm/OINODbRequestParams.js +24 -18
- package/dist/esm/OINODbSqlParams.js +330 -0
- package/dist/esm/index.js +5 -5
- package/package.json +4 -3
- package/src/OINODb.ts +163 -1
- package/src/OINODbApi.test.ts +182 -44
- package/src/OINODbApi.ts +16 -14
- package/src/OINODbDataField.ts +7 -2
- package/src/OINODbDataModel.ts +1 -1
- package/src/OINODbFactory.ts +1 -358
- package/src/OINODbModelSet.ts +1 -1
- package/src/OINODbParser.ts +458 -0
- package/src/{OINODbRequestParams.ts → OINODbSqlParams.ts} +24 -16
- package/src/index.ts +6 -6
- package/dist/types/OINODb.d.ts +0 -86
- package/dist/types/OINODbApi.d.ts +0 -82
- package/dist/types/OINODbConfig.d.ts +0 -52
- package/dist/types/OINODbDataField.d.ts +0 -202
- package/dist/types/OINODbDataModel.d.ts +0 -108
- package/dist/types/OINODbDataSet.d.ts +0 -95
- package/dist/types/OINODbFactory.d.ts +0 -63
- package/dist/types/OINODbModelSet.d.ts +0 -51
- package/dist/types/OINODbRequestParams.d.ts +0 -146
- package/dist/types/OINODbSwagger.d.ts +0 -25
- package/dist/types/index.d.ts +0 -111
- package/src/OINODbDataSet.ts +0 -167
package/README.md
CHANGED
|
@@ -75,7 +75,7 @@
|
|
|
75
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.
|
|
76
76
|
|
|
77
77
|
### Features
|
|
78
|
-
- Files can be sent to BLOB fields using BASE64 or MIME multipart 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.
|
|
79
79
|
- Datetimes are (optionally) normalized to ISO 8601 format.
|
|
80
80
|
- Extended JSON-encoding
|
|
81
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).
|
|
@@ -100,11 +100,11 @@
|
|
|
100
100
|
- Mariadb / Mysql-support through [mariadb](https://www.npmjs.com/package/mariadb)-package
|
|
101
101
|
- Sql Server through [mssql](https://www.npmjs.com/package/mssql)-package
|
|
102
102
|
|
|
103
|
-
##
|
|
103
|
+
## Composite Keys
|
|
104
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`.
|
|
105
105
|
|
|
106
106
|
## Power Of SQL
|
|
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 [`
|
|
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.
|
|
108
108
|
|
|
109
109
|
## Swagger Support
|
|
110
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.
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
## HTMX support
|
|
122
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)).
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
## Hashids
|
|
125
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.
|
|
126
126
|
|
|
127
127
|
|
|
@@ -133,9 +133,6 @@
|
|
|
133
133
|
|
|
134
134
|
### Realistic app
|
|
135
135
|
There needs to be a realistic app built on top of OINO to get a better grasp of the edge cases.
|
|
136
|
-
|
|
137
|
-
### Security review
|
|
138
|
-
Handling of SQL-injection attacks needs a thorough review, what are the relevant attack vectors are for OINO and what protections are still needed.
|
|
139
136
|
|
|
140
137
|
## Roadmap
|
|
141
138
|
Things that need to happen in some order before beta-status are at least following:
|
package/dist/cjs/OINODb.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.OINODb = void 0;
|
|
8
|
+
exports.OINODbMemoryDataSet = exports.OINODbDataSet = exports.OINODb = void 0;
|
|
9
|
+
const index_js_1 = require("./index.js");
|
|
9
10
|
/**
|
|
10
11
|
* Base class for database abstraction, implementing methods for connecting, making queries and parsing/formatting data
|
|
11
12
|
* between SQL and serialization formats.
|
|
@@ -51,3 +52,133 @@ class OINODb {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
exports.OINODb = OINODb;
|
|
55
|
+
/**
|
|
56
|
+
* Base class for SQL results that can be asynchronously iterated (but
|
|
57
|
+
* not necessarity rewinded). Idea is to handle database specific mechanisms
|
|
58
|
+
* for returning and formatting conventions in the database specific
|
|
59
|
+
* implementation. Data might be in memory or streamed in chunks and
|
|
60
|
+
* `OINODbDataSet` will serve it out consistently.
|
|
61
|
+
*
|
|
62
|
+
*/
|
|
63
|
+
class OINODbDataSet {
|
|
64
|
+
_data;
|
|
65
|
+
/** Error messages */
|
|
66
|
+
messages;
|
|
67
|
+
/**
|
|
68
|
+
* Constructor for `OINODbDataSet`.
|
|
69
|
+
*
|
|
70
|
+
* @param data internal database specific data type (constructor will throw if invalid)
|
|
71
|
+
* @param messages error messages from SQL-query
|
|
72
|
+
*
|
|
73
|
+
*/
|
|
74
|
+
constructor(data, messages = []) {
|
|
75
|
+
this._data = data;
|
|
76
|
+
this.messages = messages;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Checks if the messages contain errors.
|
|
80
|
+
*
|
|
81
|
+
*/
|
|
82
|
+
hasErrors() {
|
|
83
|
+
for (let i = 0; i < this.messages.length; i++) {
|
|
84
|
+
if (this.messages[i].startsWith(index_js_1.OINO_ERROR_PREFIX)) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Checks if the messages contain errors.
|
|
92
|
+
*
|
|
93
|
+
*/
|
|
94
|
+
getFirstError() {
|
|
95
|
+
for (let i = 0; i < this.messages.length; i++) {
|
|
96
|
+
if (this.messages[i].startsWith(index_js_1.OINO_ERROR_PREFIX)) {
|
|
97
|
+
return this.messages[i];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.OINODbDataSet = OINODbDataSet;
|
|
104
|
+
/**
|
|
105
|
+
* Generic in memory implementation of a data set where data is an array of rows. Used
|
|
106
|
+
* by BunSqlite and automated testing. Can be rewinded.
|
|
107
|
+
*
|
|
108
|
+
*/
|
|
109
|
+
class OINODbMemoryDataSet extends OINODbDataSet {
|
|
110
|
+
_rows;
|
|
111
|
+
_currentRow;
|
|
112
|
+
_eof;
|
|
113
|
+
/**
|
|
114
|
+
* Constructor of `OINODbMemoryDataSet`.
|
|
115
|
+
*
|
|
116
|
+
* @param data data as OINODataRow[] (constructor will throw if invalid)
|
|
117
|
+
* @param errors error messages from SQL-query
|
|
118
|
+
*
|
|
119
|
+
*/
|
|
120
|
+
constructor(data, errors = []) {
|
|
121
|
+
super(data, errors);
|
|
122
|
+
if ((data == null) || !(Array.isArray(data))) {
|
|
123
|
+
throw new Error(index_js_1.OINO_ERROR_PREFIX + ": Data needs to be compatible with OINORow[]!"); // TODO: maybe check all rows
|
|
124
|
+
}
|
|
125
|
+
this._rows = data;
|
|
126
|
+
if (this.isEmpty()) {
|
|
127
|
+
this._currentRow = -1;
|
|
128
|
+
this._eof = true;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
this._currentRow = 0;
|
|
132
|
+
this._eof = false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Is data set empty.
|
|
137
|
+
*
|
|
138
|
+
*/
|
|
139
|
+
isEmpty() {
|
|
140
|
+
return (this._rows.length == 0);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Is there no more content, i.e. either dataset is empty or we have moved beyond last line
|
|
144
|
+
*
|
|
145
|
+
*/
|
|
146
|
+
isEof() {
|
|
147
|
+
return (this._eof);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Attempts to moves dataset to the next row, possibly waiting for more data to become available. Returns !isEof().
|
|
151
|
+
*
|
|
152
|
+
*/
|
|
153
|
+
async next() {
|
|
154
|
+
if (this._currentRow < this._rows.length - 1) {
|
|
155
|
+
this._currentRow = this._currentRow + 1;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
this._eof = true;
|
|
159
|
+
}
|
|
160
|
+
return Promise.resolve(!this._eof);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Gets current row of data.
|
|
164
|
+
*
|
|
165
|
+
*/
|
|
166
|
+
getRow() {
|
|
167
|
+
if ((this._currentRow >= 0) && (this._currentRow < this._rows.length)) {
|
|
168
|
+
return this._rows[this._currentRow];
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
return index_js_1.OINODB_EMPTY_ROW;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Rewinds data set to the first row, returns !isEof().
|
|
176
|
+
*
|
|
177
|
+
*/
|
|
178
|
+
first() {
|
|
179
|
+
this._currentRow = 0;
|
|
180
|
+
this._eof = this._rows.length == 0;
|
|
181
|
+
return !this._eof;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
exports.OINODbMemoryDataSet = OINODbMemoryDataSet;
|
package/dist/cjs/OINODbApi.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.OINODbApi = exports.OINODbHtmlTemplate = exports.OINODbApiResult = void 0;
|
|
9
9
|
const index_js_1 = require("./index.js");
|
|
10
|
-
const
|
|
10
|
+
const common_1 = require("@oino-ts/common");
|
|
11
11
|
const hashid_1 = require("@oino-ts/hashid");
|
|
12
12
|
const API_EMPTY_PARAMS = { sqlParams: {} };
|
|
13
13
|
/**
|
|
@@ -15,7 +15,7 @@ const API_EMPTY_PARAMS = { sqlParams: {} };
|
|
|
15
15
|
* error / warning messages.
|
|
16
16
|
*
|
|
17
17
|
*/
|
|
18
|
-
class OINODbApiResult extends
|
|
18
|
+
class OINODbApiResult extends common_1.OINOResult {
|
|
19
19
|
/** DbApi request params */
|
|
20
20
|
params;
|
|
21
21
|
/** Returned data if any */
|
|
@@ -38,7 +38,7 @@ class OINODbApiResult extends types_1.OINOResult {
|
|
|
38
38
|
* @param headers Headers to include in the response
|
|
39
39
|
*
|
|
40
40
|
*/
|
|
41
|
-
async
|
|
41
|
+
async getResponse(headers = {}) {
|
|
42
42
|
let response = null;
|
|
43
43
|
if (this.success && this.data) {
|
|
44
44
|
const body = await this.data.writeString(this.params.responseType);
|
|
@@ -184,9 +184,10 @@ class OINODbApi {
|
|
|
184
184
|
//logDebug("OINODbApi.validateHttpValues", {result:result})
|
|
185
185
|
}
|
|
186
186
|
async _doGet(result, id, params) {
|
|
187
|
-
|
|
188
|
-
// OINOLog.debug("OINODbApi.doGet sql", {sql:sql})
|
|
187
|
+
let sql = "";
|
|
189
188
|
try {
|
|
189
|
+
sql = this.datamodel.printSqlSelect(id, params.sqlParams || {});
|
|
190
|
+
// OINOLog.debug("OINODbApi.doGet sql", {sql:sql})
|
|
190
191
|
const sql_res = await this.db.sqlSelect(sql);
|
|
191
192
|
// OINOLog.debug("OINODbApi.doGet sql_res", {sql_res:sql_res})
|
|
192
193
|
if (sql_res.hasErrors()) {
|
|
@@ -289,14 +290,16 @@ class OINODbApi {
|
|
|
289
290
|
let result = new OINODbApiResult(params);
|
|
290
291
|
let rows = [];
|
|
291
292
|
if ((method == "POST") || (method == "PUT")) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
293
|
+
try {
|
|
294
|
+
if (Array.isArray(body)) {
|
|
295
|
+
rows = body;
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
rows = index_js_1.OINODbParser.createRows(this.datamodel, body, params);
|
|
299
|
+
}
|
|
297
300
|
}
|
|
298
|
-
|
|
299
|
-
|
|
301
|
+
catch (e) {
|
|
302
|
+
result.setError(400, "Invalid data: " + e.message, "DoRequest");
|
|
300
303
|
}
|
|
301
304
|
// OINOLog.debug("OINODbApi.doRequest - OINODataRow rows", {rows:rows})
|
|
302
305
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.OINODatetimeDataField = exports.OINOBlobDataField = exports.OINONumberDataField = exports.OINOBooleanDataField = exports.OINOStringDataField = exports.OINODbDataField = void 0;
|
|
9
|
+
const index_js_1 = require("./index.js");
|
|
9
10
|
/**
|
|
10
11
|
* Base class for a column of data responsible for appropriatelly serializing/deserializing the data.
|
|
11
12
|
*
|
|
@@ -245,7 +246,12 @@ class OINONumberDataField extends OINODbDataField {
|
|
|
245
246
|
return null;
|
|
246
247
|
}
|
|
247
248
|
else {
|
|
248
|
-
|
|
249
|
+
const result = parseFloat(value);
|
|
250
|
+
if (isNaN(result)) {
|
|
251
|
+
index_js_1.OINOLog.error("OINODbSqlFilter.toSql: Invalid value!", { value: value });
|
|
252
|
+
throw new Error(index_js_1.OINO_ERROR_PREFIX + ": OINONumberDataField.deserializeCell - Invalid value '" + value + "'"); // incorrectly formatted data could be a security risk, abort processing
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
249
255
|
}
|
|
250
256
|
}
|
|
251
257
|
}
|
|
@@ -236,7 +236,7 @@ class OINODbDataModel {
|
|
|
236
236
|
where_sql = filter_sql;
|
|
237
237
|
}
|
|
238
238
|
const result = this.api.db.printSqlSelect(this.api.params.tableName, column_names, where_sql, order_sql, limit_sql);
|
|
239
|
-
|
|
239
|
+
// OINOLog.debug("OINODbDataModel.printSqlSelect", {result:result})
|
|
240
240
|
return result;
|
|
241
241
|
}
|
|
242
242
|
/**
|
|
@@ -111,361 +111,5 @@ class OINODbFactory {
|
|
|
111
111
|
// OINOLog.debug("createParamsFromRequest", {params:result})
|
|
112
112
|
return result;
|
|
113
113
|
}
|
|
114
|
-
static _findCsvLineEnd(csvData, start) {
|
|
115
|
-
const n = csvData.length;
|
|
116
|
-
if (start >= n) {
|
|
117
|
-
return start;
|
|
118
|
-
}
|
|
119
|
-
let end = start;
|
|
120
|
-
let quote_open = false;
|
|
121
|
-
while (end < n) {
|
|
122
|
-
if (csvData[end] == "\"") {
|
|
123
|
-
if (!quote_open) {
|
|
124
|
-
quote_open = true;
|
|
125
|
-
}
|
|
126
|
-
else if ((end < n - 1) && (csvData[end + 1] == "\"")) {
|
|
127
|
-
end++;
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
quote_open = false;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
else if ((!quote_open) && (csvData[end] == "\r")) {
|
|
134
|
-
return end;
|
|
135
|
-
}
|
|
136
|
-
end++;
|
|
137
|
-
}
|
|
138
|
-
return n;
|
|
139
|
-
}
|
|
140
|
-
static _parseCsvLine(csvLine) {
|
|
141
|
-
let result = [];
|
|
142
|
-
const n = csvLine.length;
|
|
143
|
-
let start = 0;
|
|
144
|
-
let end = 0;
|
|
145
|
-
let quote_open = false;
|
|
146
|
-
let has_quotes = false;
|
|
147
|
-
let has_escaped_quotes = false;
|
|
148
|
-
let found_field = false;
|
|
149
|
-
while (end < n) {
|
|
150
|
-
if (csvLine[end] == "\"") {
|
|
151
|
-
if (!quote_open) {
|
|
152
|
-
quote_open = true;
|
|
153
|
-
}
|
|
154
|
-
else if ((end < n - 1) && (csvLine[end + 1] == "\"")) {
|
|
155
|
-
end++;
|
|
156
|
-
has_escaped_quotes = true;
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
has_quotes = true;
|
|
160
|
-
quote_open = false;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if ((!quote_open) && ((end == n - 1) || (csvLine[end] == ","))) {
|
|
164
|
-
found_field = true;
|
|
165
|
-
if (end == n - 1) {
|
|
166
|
-
end++;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (found_field) {
|
|
170
|
-
// console.log("OINODB_csvParseLine: next field=" + csvLine.substring(start,end) + ", start="+start+", end="+end)
|
|
171
|
-
let field_str;
|
|
172
|
-
if (has_quotes) {
|
|
173
|
-
field_str = csvLine.substring(start + 1, end - 1);
|
|
174
|
-
}
|
|
175
|
-
else if (start == end) {
|
|
176
|
-
field_str = undefined;
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
field_str = csvLine.substring(start, end);
|
|
180
|
-
if (field_str == "null") {
|
|
181
|
-
field_str = null;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
result.push(field_str);
|
|
185
|
-
has_quotes = false;
|
|
186
|
-
has_escaped_quotes = true;
|
|
187
|
-
found_field = false;
|
|
188
|
-
start = end + 1;
|
|
189
|
-
}
|
|
190
|
-
end++;
|
|
191
|
-
}
|
|
192
|
-
return result;
|
|
193
|
-
}
|
|
194
|
-
static createRowFromCsv(datamodel, data) {
|
|
195
|
-
let result = [];
|
|
196
|
-
const n = data.length;
|
|
197
|
-
let start = 0;
|
|
198
|
-
let end = this._findCsvLineEnd(data, start);
|
|
199
|
-
const header_str = data.substring(start, end);
|
|
200
|
-
const headers = this._parseCsvLine(header_str);
|
|
201
|
-
let field_to_header_mapping = new Array(datamodel.fields.length);
|
|
202
|
-
let headers_found = false;
|
|
203
|
-
for (let i = 0; i < field_to_header_mapping.length; i++) {
|
|
204
|
-
field_to_header_mapping[i] = headers.indexOf(datamodel.fields[i].name);
|
|
205
|
-
headers_found = headers_found || (field_to_header_mapping[i] >= 0);
|
|
206
|
-
}
|
|
207
|
-
// OINOLog.debug("createRowFromCsv", {headers:headers, field_to_header_mapping:field_to_header_mapping})
|
|
208
|
-
if (!headers_found) {
|
|
209
|
-
return result;
|
|
210
|
-
}
|
|
211
|
-
start = end + 1;
|
|
212
|
-
end = start;
|
|
213
|
-
while (end < n) {
|
|
214
|
-
while ((start < n) && ((data[start] == "\r") || (data[start] == "\n"))) {
|
|
215
|
-
start++;
|
|
216
|
-
}
|
|
217
|
-
if (start >= n) {
|
|
218
|
-
return result;
|
|
219
|
-
}
|
|
220
|
-
end = this._findCsvLineEnd(data, start);
|
|
221
|
-
const row_data = this._parseCsvLine(data.substring(start, end));
|
|
222
|
-
const row = new Array(field_to_header_mapping.length);
|
|
223
|
-
for (let i = 0; i < datamodel.fields.length; i++) {
|
|
224
|
-
const field = datamodel.fields[i];
|
|
225
|
-
let j = field_to_header_mapping[i];
|
|
226
|
-
let value = row_data[j];
|
|
227
|
-
if ((value === undefined) || (value === null)) { // null/undefined-decoding built into the parser
|
|
228
|
-
row[i] = value;
|
|
229
|
-
}
|
|
230
|
-
else if ((j >= 0) && (j < row_data.length)) {
|
|
231
|
-
value = index_js_1.OINOStr.decode(value, index_js_1.OINOContentType.csv);
|
|
232
|
-
if (value && (field.fieldParams.isPrimaryKey || field.fieldParams.isForeignKey) && (field instanceof index_js_1.OINONumberDataField) && (datamodel.api.hashid)) {
|
|
233
|
-
value = datamodel.api.hashid.decode(value);
|
|
234
|
-
}
|
|
235
|
-
row[i] = field.deserializeCell(value);
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
row[i] = undefined;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
// console.log("createRowFromCsv: next row=" + row)
|
|
242
|
-
result.push(row);
|
|
243
|
-
start = end;
|
|
244
|
-
end = start;
|
|
245
|
-
}
|
|
246
|
-
return result;
|
|
247
|
-
}
|
|
248
|
-
static _createRowFromJsonObj(obj, datamodel) {
|
|
249
|
-
// console.log("createRowFromJsonObj: obj=" + JSON.stringify(obj))
|
|
250
|
-
const fields = datamodel.fields;
|
|
251
|
-
let result = new Array(fields.length);
|
|
252
|
-
// console.log("createRowFromJsonObj: " + result)
|
|
253
|
-
for (let i = 0; i < fields.length; i++) {
|
|
254
|
-
const field = fields[i];
|
|
255
|
-
let value = obj[field.name];
|
|
256
|
-
// console.log("createRowFromJsonObj: key=" + field.name + ", val=" + val)
|
|
257
|
-
if ((value === null) || (value === undefined)) { // must be checed first as null is an object
|
|
258
|
-
result[i] = value;
|
|
259
|
-
}
|
|
260
|
-
else if (Array.isArray(value) || typeof value === "object") {
|
|
261
|
-
result[i] = JSON.stringify(value).replaceAll("\"", "\\\""); // only single level deep objects, rest is handled as JSON-strings
|
|
262
|
-
}
|
|
263
|
-
else if (typeof value === "string") {
|
|
264
|
-
value = index_js_1.OINOStr.decode(value, index_js_1.OINOContentType.json);
|
|
265
|
-
if (value && (field.fieldParams.isPrimaryKey || field.fieldParams.isForeignKey) && (field instanceof index_js_1.OINONumberDataField) && (datamodel.api.hashid)) {
|
|
266
|
-
value = datamodel.api.hashid.decode(value);
|
|
267
|
-
}
|
|
268
|
-
result[i] = field.deserializeCell(value);
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
result[i] = value; // value types are passed as-is
|
|
272
|
-
}
|
|
273
|
-
// console.log("createRowFromJsonObj: result["+i+"]=" + result[i])
|
|
274
|
-
}
|
|
275
|
-
// console.log("createRowFromJsonObj: " + result)
|
|
276
|
-
return result;
|
|
277
|
-
}
|
|
278
|
-
static _createRowFromJson(datamodel, data) {
|
|
279
|
-
try {
|
|
280
|
-
let result = [];
|
|
281
|
-
// console.log("OINORowFactoryJson: data=" + data)
|
|
282
|
-
const obj = JSON.parse(data);
|
|
283
|
-
if (Array.isArray(obj)) {
|
|
284
|
-
obj.forEach(row => {
|
|
285
|
-
result.push(this._createRowFromJsonObj(row, datamodel));
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
result.push(this._createRowFromJsonObj(obj, datamodel));
|
|
290
|
-
}
|
|
291
|
-
return result;
|
|
292
|
-
}
|
|
293
|
-
catch (e) {
|
|
294
|
-
return [];
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
static _findMultipartBoundary(formData, multipartBoundary, start) {
|
|
298
|
-
let n = formData.indexOf(multipartBoundary, start);
|
|
299
|
-
if (n >= 0) {
|
|
300
|
-
n += multipartBoundary.length + 2;
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
n = formData.length;
|
|
304
|
-
}
|
|
305
|
-
return n;
|
|
306
|
-
}
|
|
307
|
-
static _parseMultipartLine(csvData, start) {
|
|
308
|
-
let line_end = csvData.indexOf('\r\n', start);
|
|
309
|
-
if (line_end >= start) {
|
|
310
|
-
return csvData.substring(start, line_end);
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
return '';
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
static _multipartHeaderRegex = /Content-Disposition\: (form-data|file); name=\"([^\"]+)\"(; filename=.*)?/i;
|
|
317
|
-
static createRowFromFormdata(datamodel, data, multipartBoundary) {
|
|
318
|
-
let result = [];
|
|
319
|
-
const n = data.length;
|
|
320
|
-
let start = this._findMultipartBoundary(data, multipartBoundary, 0);
|
|
321
|
-
let end = this._findMultipartBoundary(data, multipartBoundary, start);
|
|
322
|
-
// OINOLog.debug("createRowFromFormdata: enter", {start:start, end:end, multipartBoundary:multipartBoundary})
|
|
323
|
-
const row = new Array(datamodel.fields.length);
|
|
324
|
-
while (end < n) {
|
|
325
|
-
// OINOLog.debug("createRowFromFormdata: next block", {start:start, end:end, block:data.substring(start, end)})
|
|
326
|
-
let block_ok = true;
|
|
327
|
-
let l = this._parseMultipartLine(data, start);
|
|
328
|
-
// OINOLog.debug("createRowFromFormdata: next line", {start:start, end:end, line:l})
|
|
329
|
-
start += l.length + 2;
|
|
330
|
-
const header_matches = OINODbFactory._multipartHeaderRegex.exec(l);
|
|
331
|
-
if (!header_matches) {
|
|
332
|
-
index_js_1.OINOLog.warning("OINODbFactory.createRowFromFormdata: unsupported block skipped!", { header_line: l });
|
|
333
|
-
block_ok = false;
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
const field_name = header_matches[2];
|
|
337
|
-
const is_file = header_matches[3] != null;
|
|
338
|
-
let is_base64 = false;
|
|
339
|
-
const field_index = datamodel.findFieldIndexByName(field_name);
|
|
340
|
-
// OINOLog.debug("createRowFromFormdata: header", {field_name:field_name, field_index:field_index, is_file:is_file})
|
|
341
|
-
if (field_index < 0) {
|
|
342
|
-
index_js_1.OINOLog.warning("OINODbFactory.createRowFromFormdata: form field not found and skipped!", { field_name: field_name });
|
|
343
|
-
block_ok = false;
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
const field = datamodel.fields[field_index];
|
|
347
|
-
l = this._parseMultipartLine(data, start);
|
|
348
|
-
// OINOLog.debug("createRowFromFormdata: next line", {start:start, end:end, line:l})
|
|
349
|
-
while (block_ok && (l != '')) {
|
|
350
|
-
if (l.startsWith('Content-Type:') && (l.indexOf('multipart/mixed') >= 0)) {
|
|
351
|
-
index_js_1.OINOLog.warning("OINODbFactory.createRowFromFormdata: mixed multipart files not supported and skipped!", { header_line: l });
|
|
352
|
-
block_ok = false;
|
|
353
|
-
}
|
|
354
|
-
else if (l.startsWith('Content-Transfer-Encoding:') && (l.indexOf('BASE64') >= 0)) {
|
|
355
|
-
is_base64 = true;
|
|
356
|
-
}
|
|
357
|
-
start += l.length + 2;
|
|
358
|
-
l = this._parseMultipartLine(data, start);
|
|
359
|
-
// OINOLog.debug("createRowFromFormdata: next line", {start:start, end:end, line:l})
|
|
360
|
-
}
|
|
361
|
-
start += 2;
|
|
362
|
-
if (!block_ok) {
|
|
363
|
-
index_js_1.OINOLog.warning("OINODbFactory.createRowFromFormdata: invalid block skipped", { field_name: field_name });
|
|
364
|
-
}
|
|
365
|
-
else if (start + multipartBoundary.length + 2 >= end) {
|
|
366
|
-
// OINOLog.debug("OINODbFactory.createRowFromFormdata: null value", {field_name:field_name})
|
|
367
|
-
row[field_index] = null;
|
|
368
|
-
}
|
|
369
|
-
else if (is_file) {
|
|
370
|
-
const value = this._parseMultipartLine(data, start).trim();
|
|
371
|
-
if (is_base64) {
|
|
372
|
-
row[field_index] = field.deserializeCell(index_js_1.OINOStr.decode(value, index_js_1.OINOContentType.formdata));
|
|
373
|
-
}
|
|
374
|
-
else {
|
|
375
|
-
row[field_index] = Buffer.from(value, "binary");
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
let value = index_js_1.OINOStr.decode(this._parseMultipartLine(data, start).trim(), index_js_1.OINOContentType.formdata);
|
|
380
|
-
// OINOLog.debug("OINODbFactory.createRowFromFormdata: parse form field", {field_name:field_name, value:value})
|
|
381
|
-
if (value && (field.fieldParams.isPrimaryKey || field.fieldParams.isForeignKey) && (field instanceof index_js_1.OINONumberDataField) && (datamodel.api.hashid)) {
|
|
382
|
-
value = datamodel.api.hashid.decode(value);
|
|
383
|
-
}
|
|
384
|
-
row[field_index] = field.deserializeCell(value);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
start = end;
|
|
389
|
-
end = this._findMultipartBoundary(data, multipartBoundary, start);
|
|
390
|
-
}
|
|
391
|
-
// OINOLog.debug("createRowFromFormdata: next row", {row:row})
|
|
392
|
-
result.push(row);
|
|
393
|
-
return result;
|
|
394
|
-
}
|
|
395
|
-
static createRowFromUrlencoded(datamodel, data) {
|
|
396
|
-
// OINOLog.debug("createRowFromUrlencoded: enter", {data:data})
|
|
397
|
-
let result = [];
|
|
398
|
-
const row = new Array(datamodel.fields.length);
|
|
399
|
-
const data_parts = data.trim().split('&');
|
|
400
|
-
for (let i = 0; i < data_parts.length; i++) {
|
|
401
|
-
const param_parts = data_parts[i].split('=');
|
|
402
|
-
// OINOLog.debug("createRowFromUrlencoded: next param", {param_parts:param_parts})
|
|
403
|
-
if (param_parts.length == 2) {
|
|
404
|
-
const key = index_js_1.OINOStr.decodeUrlencode(param_parts[0]) || "";
|
|
405
|
-
const field_index = datamodel.findFieldIndexByName(key);
|
|
406
|
-
if (field_index < 0) {
|
|
407
|
-
index_js_1.OINOLog.info("createRowFromUrlencoded: param field not found", { field: key });
|
|
408
|
-
}
|
|
409
|
-
else {
|
|
410
|
-
const field = datamodel.fields[field_index];
|
|
411
|
-
let value = index_js_1.OINOStr.decode(param_parts[1], index_js_1.OINOContentType.urlencode);
|
|
412
|
-
if (value && (field.fieldParams.isPrimaryKey || field.fieldParams.isForeignKey) && (field instanceof index_js_1.OINONumberDataField) && (datamodel.api.hashid)) {
|
|
413
|
-
value = datamodel.api.hashid.decode(value);
|
|
414
|
-
}
|
|
415
|
-
row[field_index] = field.deserializeCell(value);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
// const value = requestParams[]
|
|
419
|
-
}
|
|
420
|
-
// console.log("createRowFromUrlencoded: next row=" + row)
|
|
421
|
-
result.push(row);
|
|
422
|
-
return result;
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* Create data rows from request body based on the datamodel.
|
|
426
|
-
*
|
|
427
|
-
* @param datamodel datamodel of the api
|
|
428
|
-
* @param data data as a string
|
|
429
|
-
* @param requestParams parameters
|
|
430
|
-
*
|
|
431
|
-
*/
|
|
432
|
-
static createRows(datamodel, data, requestParams) {
|
|
433
|
-
if ((requestParams.requestType == index_js_1.OINOContentType.json) || (requestParams.requestType == undefined)) {
|
|
434
|
-
return this._createRowFromJson(datamodel, data);
|
|
435
|
-
}
|
|
436
|
-
else if (requestParams.requestType == index_js_1.OINOContentType.csv) {
|
|
437
|
-
return this.createRowFromCsv(datamodel, data);
|
|
438
|
-
}
|
|
439
|
-
else if (requestParams.requestType == index_js_1.OINOContentType.formdata) {
|
|
440
|
-
return this.createRowFromFormdata(datamodel, data, requestParams.multipartBoundary || "");
|
|
441
|
-
}
|
|
442
|
-
else if (requestParams.requestType == index_js_1.OINOContentType.urlencode) {
|
|
443
|
-
return this.createRowFromUrlencoded(datamodel, data);
|
|
444
|
-
}
|
|
445
|
-
else if (requestParams.requestType == index_js_1.OINOContentType.html) {
|
|
446
|
-
index_js_1.OINOLog.error("HTML can't be used as an input content type!", { contentType: index_js_1.OINOContentType.html });
|
|
447
|
-
return [];
|
|
448
|
-
}
|
|
449
|
-
else {
|
|
450
|
-
index_js_1.OINOLog.error("Unrecognized input content type!", { contentType: requestParams.requestType });
|
|
451
|
-
return [];
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
/**
|
|
455
|
-
* Create one data row from javascript object based on the datamodel.
|
|
456
|
-
* NOTE! Data assumed to be unserialized i.e. of the native type (string, number, boolean, Buffer)
|
|
457
|
-
*
|
|
458
|
-
* @param datamodel datamodel of the api
|
|
459
|
-
* @param data data as javascript object
|
|
460
|
-
*
|
|
461
|
-
*/
|
|
462
|
-
static createRowFromObject(datamodel, data) {
|
|
463
|
-
const fields = datamodel.fields;
|
|
464
|
-
let result = new Array(fields.length);
|
|
465
|
-
for (let i = 0; i < fields.length; i++) {
|
|
466
|
-
result[i] = data[fields[i].name];
|
|
467
|
-
}
|
|
468
|
-
return result;
|
|
469
|
-
}
|
|
470
114
|
}
|
|
471
115
|
exports.OINODbFactory = OINODbFactory;
|
|
@@ -56,7 +56,7 @@ class OINODbModelSet {
|
|
|
56
56
|
const f = fields[i];
|
|
57
57
|
let value = f.serializeCell(row[i]);
|
|
58
58
|
if (value === undefined) {
|
|
59
|
-
|
|
59
|
+
// OINOLog.info("OINODbModelSet._writeRowJson: undefined value skipped", {field_name:f.name})
|
|
60
60
|
}
|
|
61
61
|
else if (value === null) {
|
|
62
62
|
json_row += "," + index_js_1.OINOStr.encode(f.name, index_js_1.OINOContentType.json) + ":null";
|