@oino-ts/db 0.3.2 → 0.3.4

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.
@@ -1,458 +1,458 @@
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 { OINOContentType, OINODbDataModel, OINODbDataField, OINODataRow, OINODbApiRequestParams, OINOStr, OINONumberDataField, OINOLog } from "./index.js"
8
-
9
- /**
10
- * Static factory class for easily creating things based on data
11
- *
12
- */
13
- export class OINODbParser {
14
- /**
15
- * Create data rows from request body based on the datamodel.
16
- *
17
- * @param datamodel datamodel of the api
18
- * @param data data as a string or Buffer or object
19
- * @param requestParams parameters
20
- *
21
- */
22
- static createRows(datamodel:OINODbDataModel, data:string|Buffer|object, requestParams:OINODbApiRequestParams ):OINODataRow[] {
23
- let result:OINODataRow[] = []
24
- if (typeof data == "string") {
25
- result = this.createRowsFromText(datamodel, data, requestParams)
26
-
27
- } else if (data instanceof Buffer) {
28
- result = this.createRowsFromBlob(datamodel, data, requestParams)
29
-
30
- } else if (typeof data == "object") {
31
- result = [this.createRowFromObject(datamodel, data)]
32
- }
33
- return result
34
- }
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:OINODbDataModel, data:string, requestParams:OINODbApiRequestParams ):OINODataRow[] {
45
- if ((requestParams.requestType == OINOContentType.json) || (requestParams.requestType == undefined)) {
46
- return this._createRowFromJson(datamodel, data)
47
-
48
- } else if (requestParams.requestType == OINOContentType.csv) {
49
- return this._createRowFromCsv(datamodel, data)
50
-
51
- } else if (requestParams.requestType == OINOContentType.formdata) {
52
- return this._createRowFromFormdata(datamodel, Buffer.from(data, "utf8"), requestParams.multipartBoundary || "")
53
-
54
- } else if (requestParams.requestType == OINOContentType.urlencode) {
55
- return this._createRowFromUrlencoded(datamodel, data)
56
-
57
- } else if (requestParams.requestType == OINOContentType.html) {
58
- OINOLog.error("HTML can't be used as an input content type!", {contentType:OINOContentType.html})
59
- return []
60
- } else {
61
- OINOLog.error("Unrecognized input content type!", {contentType:requestParams.requestType})
62
- return []
63
- }
64
- }
65
- /**
66
- * Create data rows from request body based on the datamodel.
67
- *
68
- * @param datamodel datamodel of the api
69
- * @param data data as an Buffer
70
- * @param requestParams parameters
71
- *
72
- */
73
- static createRowsFromBlob(datamodel:OINODbDataModel, data:Buffer, requestParams:OINODbApiRequestParams ):OINODataRow[] {
74
- if ((requestParams.requestType == OINOContentType.json) || (requestParams.requestType == undefined)) {
75
- return this._createRowFromJson(datamodel, data.toString()) // JSON is always a string
76
-
77
- } else if (requestParams.requestType == OINOContentType.csv) {
78
- return this._createRowFromCsv(datamodel, data.toString()) // binary data has to be base64 encoded so it's a string
79
-
80
- } else if (requestParams.requestType == OINOContentType.formdata) {
81
- return this._createRowFromFormdata(datamodel, data, requestParams.multipartBoundary || "")
82
-
83
- } else if (requestParams.requestType == OINOContentType.urlencode) {
84
- return this._createRowFromUrlencoded(datamodel, data.toString()) // data is urlencoded so it's a string
85
-
86
- } else if (requestParams.requestType == OINOContentType.html) {
87
- OINOLog.error("HTML can't be used as an input content type!", {contentType:OINOContentType.html})
88
- return []
89
- } else {
90
- OINOLog.error("Unrecognized input content type!", {contentType:requestParams.requestType})
91
- return []
92
- }
93
- }
94
-
95
- /**
96
- * Create one data row from javascript object based on the datamodel.
97
- * NOTE! Data assumed to be unserialized i.e. of the native type (string, number, boolean, Buffer)
98
- *
99
- * @param datamodel datamodel of the api
100
- * @param data data as javascript object
101
- *
102
- */
103
- static createRowFromObject(datamodel:OINODbDataModel, data:any):OINODataRow {
104
- const fields:OINODbDataField[] = datamodel.fields
105
- let result:OINODataRow = new Array(fields.length)
106
- for (let i=0; i < fields.length; i++) {
107
- result[i] = data[fields[i].name]
108
- }
109
- return result
110
- }
111
-
112
- private static _findCsvLineEnd(csvData:string, start:number):number {
113
- const n:number = csvData.length
114
- if (start >= n) {
115
- return start
116
- }
117
- let end:number = start
118
- let quote_open:boolean = false
119
- while (end<n) {
120
- if (csvData[end] == "\"") {
121
- if (!quote_open) {
122
- quote_open = true
123
- } else if ((end < n-1) && (csvData[end+1] == "\"")) {
124
- end++
125
- } else {
126
- quote_open = false
127
- }
128
- } else if ((!quote_open) && (csvData[end] == "\r")) {
129
- return end
130
- }
131
- end++
132
- }
133
- return n
134
- }
135
-
136
- private static _parseCsvLine(csvLine:string):(string|null|undefined)[] {
137
- let result:(string|null|undefined)[] = []
138
- const n:number = csvLine.length
139
- let start:number = 0
140
- let end:number = 0
141
- let quote_open:boolean = false
142
- let has_quotes:boolean = false
143
- let has_escaped_quotes = false
144
- let found_field = false
145
- while (end<n) {
146
- if (csvLine[end] == "\"") {
147
- if (!quote_open) {
148
- quote_open = true
149
- } else if ((end < n-1) && (csvLine[end+1] == "\"")) {
150
- end++
151
- has_escaped_quotes = true
152
- } else {
153
- has_quotes = true
154
- quote_open = false
155
- }
156
- }
157
- if ((!quote_open) && ((end == n-1) || (csvLine[end] == ","))) {
158
- found_field = true
159
- if (end == n-1) {
160
- end++
161
- }
162
- }
163
- if (found_field) {
164
- // console.log("OINODB_csvParseLine: next field=" + csvLine.substring(start,end) + ", start="+start+", end="+end)
165
- let field_str:string|undefined|null
166
- if (has_quotes) {
167
- field_str = csvLine.substring(start+1,end-1)
168
- } else if (start == end) {
169
- field_str = undefined
170
- } else {
171
- field_str = csvLine.substring(start,end)
172
- if (field_str == "null") {
173
- field_str = null
174
- }
175
- }
176
- result.push(field_str)
177
- has_quotes = false
178
- has_escaped_quotes = true
179
- found_field = false
180
- start = end+1
181
- }
182
- end++
183
- }
184
- return result
185
- }
186
-
187
- private static _createRowFromCsv(datamodel:OINODbDataModel, data:string):OINODataRow[] {
188
- let result:OINODataRow[] = []
189
- const n = data.length
190
- let start:number = 0
191
- let end:number = this._findCsvLineEnd(data, start)
192
- const header_str = data.substring(start, end)
193
- const headers:(string|null|undefined)[] = this._parseCsvLine(header_str)
194
- let field_to_header_mapping:number[] = new Array(datamodel.fields.length)
195
- let headers_found:boolean = false
196
- for (let i=0; i<field_to_header_mapping.length; i++) {
197
- field_to_header_mapping[i] = headers.indexOf(datamodel.fields[i].name)
198
- headers_found = headers_found || (field_to_header_mapping[i] >= 0)
199
- }
200
- // OINOLog.debug("createRowFromCsv", {headers:headers, field_to_header_mapping:field_to_header_mapping})
201
- if (!headers_found) {
202
- return result
203
- }
204
- start = end + 1
205
- end = start
206
- while (end < n) {
207
- while ((start < n) && ((data[start] == "\r") || (data[start] == "\n"))) {
208
- start++
209
- }
210
- if (start >= n) {
211
- return result
212
- }
213
- end = this._findCsvLineEnd(data, start)
214
- const row_data:(string|null|undefined)[] = this._parseCsvLine(data.substring(start, end))
215
- const row:OINODataRow = new Array(field_to_header_mapping.length)
216
- let has_data:boolean = false
217
- for (let i=0; i<datamodel.fields.length; i++) {
218
- const field:OINODbDataField = datamodel.fields[i]
219
- let j:number = field_to_header_mapping[i]
220
- let value:string|null|undefined = row_data[j]
221
- if ((value === undefined) || (value === null)) { // null/undefined-decoding built into the parser
222
- row[i] = value
223
-
224
- } else if ((j >= 0) && (j < row_data.length)) {
225
- value = OINOStr.decode(value, OINOContentType.csv)
226
- if (value && (field.fieldParams.isPrimaryKey || field.fieldParams.isForeignKey) && (field instanceof OINONumberDataField) && (datamodel.api.hashid)) {
227
- value = datamodel.api.hashid.decode(value)
228
- }
229
- row[i] = field.deserializeCell(value)
230
-
231
- } else {
232
- row[i] = undefined
233
- }
234
- has_data = has_data || (row[i] !== undefined)
235
- }
236
- // console.log("createRowFromCsv: next row=" + row)
237
- if (has_data) {
238
- result.push(row)
239
- } else {
240
- OINOLog.warning("createRowFromCsv: empty row skipped")
241
- }
242
- start = end
243
- end = start
244
- }
245
-
246
- return result
247
- }
248
-
249
- private static _createRowFromJsonObj(obj:any, datamodel:OINODbDataModel):OINODataRow|undefined {
250
- // console.log("createRowFromJsonObj: obj=" + JSON.stringify(obj))
251
- const fields:OINODbDataField[] = datamodel.fields
252
- let result:OINODataRow = new Array(fields.length)
253
- let has_data:boolean = false
254
- // console.log("createRowFromJsonObj: " + result)
255
- for (let i=0; i < fields.length; i++) {
256
- const field = fields[i]
257
- let value:any = obj[field.name]
258
- // console.log("createRowFromJsonObj: key=" + field.name + ", val=" + val)
259
- if ((value === null) || (value === undefined)) { // must be checed first as null is an object
260
- result[i] = value
261
-
262
- } else if (Array.isArray(value) || typeof value === "object") {
263
- result[i] = JSON.stringify(value).replaceAll("\"","\\\"") // only single level deep objects, rest is handled as JSON-strings
264
-
265
- } else if (typeof value === "string") {
266
- value = OINOStr.decode(value, OINOContentType.json)
267
- if (value && (field.fieldParams.isPrimaryKey || field.fieldParams.isForeignKey) && (field instanceof OINONumberDataField) && (datamodel.api.hashid)) {
268
- value = datamodel.api.hashid.decode(value)
269
- }
270
- result[i] = field.deserializeCell(value)
271
-
272
- } else {
273
- result[i] = value // value types are passed as-is
274
- }
275
- has_data = has_data || (result[i] !== undefined)
276
- // console.log("createRowFromJsonObj: result["+i+"]=" + result[i])
277
- }
278
- // console.log("createRowFromJsonObj: " + result)
279
- if (has_data) {
280
- return result
281
- } else {
282
- OINOLog.warning("createRowFromJsonObj: empty row skipped")
283
- return undefined
284
- }
285
- }
286
-
287
- private static _createRowFromJson(datamodel:OINODbDataModel, data:string):OINODataRow[] {
288
- let result:OINODataRow[] = []
289
- // console.log("OINORowFactoryJson: data=" + data)
290
- const obj:object = JSON.parse(data)
291
- if (Array.isArray(obj)) {
292
- obj.forEach(row => {
293
- const data_row = this._createRowFromJsonObj(row, datamodel)
294
- if (data_row !== undefined) {
295
- result.push(data_row)
296
- }
297
- })
298
-
299
- } else {
300
- const data_row = this._createRowFromJsonObj(obj, datamodel)
301
- if (data_row !== undefined) {
302
- result.push(data_row)
303
- }
304
- }
305
- return result
306
- }
307
-
308
- private static _findMultipartBoundary(formData:Buffer, multipartBoundary:string, start:number):number {
309
- let n:number = formData.indexOf(multipartBoundary, start)
310
- if (n >= 0) {
311
- n += multipartBoundary.length + 2
312
- } else {
313
- n = formData.length
314
- }
315
- return n
316
- }
317
-
318
- private static _parseMultipartLine(data:Buffer, start:number):string {
319
- let line_end:number = data.indexOf('\r\n', start)
320
- if (line_end >= start) {
321
- return data.subarray(start, line_end).toString()
322
- } else {
323
- return ''
324
- }
325
- }
326
-
327
- private static _multipartHeaderRegex:RegExp = /Content-Disposition\: (form-data|file); name=\"([^\"]+)\"(; filename=.*)?/i
328
-
329
- private static _createRowFromFormdata(datamodel:OINODbDataModel, data:Buffer, multipartBoundary:string):OINODataRow[] {
330
- let result:OINODataRow[] = []
331
- try {
332
- const n = data.length
333
- let start:number = this._findMultipartBoundary(data, multipartBoundary, 0)
334
- let end:number = this._findMultipartBoundary(data, multipartBoundary, start)
335
- // OINOLog.debug("createRowFromFormdata: enter", {start:start, end:end, multipartBoundary:multipartBoundary})
336
- const row:OINODataRow = new Array(datamodel.fields.length)
337
- let has_data:boolean = false
338
- while (end < n) {
339
- // OINOLog.debug("createRowFromFormdata: next block", {start:start, end:end, block:data.substring(start, end)})
340
- let block_ok:boolean = true
341
- let l:string = this._parseMultipartLine(data, start)
342
- // OINOLog.debug("createRowFromFormdata: next line", {start:start, end:end, line:l})
343
- start += l.length+2
344
- const header_matches = OINODbParser._multipartHeaderRegex.exec(l)
345
- if (!header_matches) {
346
- OINOLog.warning("OINODbFactory.createRowFromFormdata: unsupported block skipped!", {header_line:l})
347
- block_ok = false
348
-
349
- } else {
350
- const field_name = header_matches[2]
351
- const is_file = header_matches[3] != null
352
- let is_base64:boolean = false
353
- const field_index:number = datamodel.findFieldIndexByName(field_name)
354
- // OINOLog.debug("createRowFromFormdata: header", {field_name:field_name, field_index:field_index, is_file:is_file, is_base64:is_base64})
355
- if (field_index < 0) {
356
- OINOLog.warning("OINODbFactory.createRowFromFormdata: form field not found and skipped!", {field_name:field_name})
357
- block_ok = false
358
-
359
- } else {
360
- const field:OINODbDataField = datamodel.fields[field_index]
361
- l = this._parseMultipartLine(data, start)
362
- // OINOLog.debug("createRowFromFormdata: next line", {start:start, end:end, line:l})
363
- while (block_ok && (l != '')) {
364
- if (l.startsWith('Content-Type:') && (l.indexOf('multipart/mixed')>=0)) {
365
- OINOLog.warning("OINODbFactory.createRowFromFormdata: mixed multipart files not supported and skipped!", {header_line:l})
366
- block_ok = false
367
- } else if (l.startsWith('Content-Transfer-Encoding:') && (l.indexOf('BASE64')>=0)) {
368
- is_base64 = true
369
- }
370
- start += l.length+2
371
- l = this._parseMultipartLine(data, start)
372
- // OINOLog.debug("createRowFromFormdata: next line", {start:start, end:end, line:l})
373
- }
374
- start += 2
375
- if (!block_ok) {
376
- OINOLog.warning("OINODbFactory.createRowFromFormdata: invalid block skipped", {field_name:field_name})
377
- } else if (start + multipartBoundary.length + 2 >= end) {
378
- // OINOLog.debug("OINODbFactory.createRowFromFormdata: null value", {field_name:field_name})
379
- row[field_index] = null
380
-
381
- } else if (is_file) {
382
- if (is_base64) {
383
- const value = this._parseMultipartLine(data, start).trim()
384
- row[field_index] = field.deserializeCell(OINOStr.decode(value, OINOContentType.formdata))
385
- } else {
386
- const e = this._findMultipartBoundary(data, multipartBoundary, start)
387
- const value = data.subarray(start, e-2)
388
- row[field_index] = value
389
- }
390
- // console.log("OINODbFactory.createRowFromFormdata: file field", {field_name:field_name, value:row[field_index]})
391
- } else {
392
- let value:string = OINOStr.decode(this._parseMultipartLine(data, start).trim(), OINOContentType.formdata)
393
- // OINOLog.debug("OINODbFactory.createRowFromFormdata: parse form field", {field_name:field_name, value:value})
394
- if (value && (field.fieldParams.isPrimaryKey || field.fieldParams.isForeignKey) && (field instanceof OINONumberDataField) && (datamodel.api.hashid)) {
395
- value = datamodel.api.hashid.decode(value)
396
- }
397
- row[field_index] = field.deserializeCell(value)
398
- }
399
- has_data = has_data || (row[field_index] !== undefined)
400
- }
401
- }
402
- start = end
403
- end = this._findMultipartBoundary(data, multipartBoundary, start)
404
- }
405
- // OINOLog.debug("createRowFromFormdata: next row", {row:row})
406
- if (has_data) {
407
- result.push(row)
408
- } else {
409
- OINOLog.warning("createRowFromFormdata: empty row skipped")
410
- }
411
- } catch (e:any) {
412
- OINOLog.error("createRowFromFormdata: error parsing formdata", {exception:e.message})
413
- }
414
- return result
415
- }
416
- private static _createRowFromUrlencoded(datamodel:OINODbDataModel, data:string):OINODataRow[] {
417
- // OINOLog.debug("createRowFromUrlencoded: enter", {data:data})
418
- let result:OINODataRow[] = []
419
- const row:OINODataRow = new Array(datamodel.fields.length)
420
- let has_data:boolean = false
421
- const data_parts:string[] = data.trim().split('&')
422
- try {
423
- for (let i=0; i<data_parts.length; i++) {
424
- const param_parts = data_parts[i].split('=')
425
- // OINOLog.debug("createRowFromUrlencoded: next param", {param_parts:param_parts})
426
- if (param_parts.length == 2) {
427
- const key=OINOStr.decodeUrlencode(param_parts[0]) || ""
428
- const field_index:number = datamodel.findFieldIndexByName(key)
429
- if (field_index < 0) {
430
- OINOLog.info("createRowFromUrlencoded: param field not found", {field:key})
431
-
432
- } else {
433
- const field:OINODbDataField = datamodel.fields[field_index]
434
- let value:string=OINOStr.decode(param_parts[1], OINOContentType.urlencode)
435
- if (value && (field.fieldParams.isPrimaryKey || field.fieldParams.isForeignKey) && (field instanceof OINONumberDataField) && (datamodel.api.hashid)) {
436
- value = datamodel.api.hashid.decode(value)
437
- }
438
- row[field_index] = field.deserializeCell(value)
439
- has_data = has_data || (row[field_index] !== undefined)
440
- }
441
- }
442
-
443
- // const value = requestParams[]
444
-
445
- }
446
- if (has_data) {
447
- result.push(row)
448
- } else {
449
- OINOLog.warning("createRowFromUrlencoded: empty row skipped")
450
- }
451
- } catch (e:any) {
452
- OINOLog.error("createRowFromUrlencoded: error parsing urlencoded data", {exception:e.message})
453
- }
454
- // console.log("createRowFromUrlencoded: next row=" + row)
455
- return result
456
- }
457
-
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 { OINOContentType, OINODbDataModel, OINODbDataField, OINODataRow, OINODbApiRequestParams, OINOStr, OINONumberDataField, OINOLog } from "./index.js"
8
+
9
+ /**
10
+ * Static factory class for easily creating things based on data
11
+ *
12
+ */
13
+ export class OINODbParser {
14
+ /**
15
+ * Create data rows from request body based on the datamodel.
16
+ *
17
+ * @param datamodel datamodel of the api
18
+ * @param data data as a string or Buffer or object
19
+ * @param requestParams parameters
20
+ *
21
+ */
22
+ static createRows(datamodel:OINODbDataModel, data:string|Buffer|object, requestParams:OINODbApiRequestParams ):OINODataRow[] {
23
+ let result:OINODataRow[] = []
24
+ if (typeof data == "string") {
25
+ result = this.createRowsFromText(datamodel, data, requestParams)
26
+
27
+ } else if (data instanceof Buffer) {
28
+ result = this.createRowsFromBlob(datamodel, data, requestParams)
29
+
30
+ } else if (typeof data == "object") {
31
+ result = [this.createRowFromObject(datamodel, data)]
32
+ }
33
+ return result
34
+ }
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:OINODbDataModel, data:string, requestParams:OINODbApiRequestParams ):OINODataRow[] {
45
+ if ((requestParams.requestType == OINOContentType.json) || (requestParams.requestType == undefined)) {
46
+ return this._createRowFromJson(datamodel, data)
47
+
48
+ } else if (requestParams.requestType == OINOContentType.csv) {
49
+ return this._createRowFromCsv(datamodel, data)
50
+
51
+ } else if (requestParams.requestType == OINOContentType.formdata) {
52
+ return this._createRowFromFormdata(datamodel, Buffer.from(data, "utf8"), requestParams.multipartBoundary || "")
53
+
54
+ } else if (requestParams.requestType == OINOContentType.urlencode) {
55
+ return this._createRowFromUrlencoded(datamodel, data)
56
+
57
+ } else if (requestParams.requestType == OINOContentType.html) {
58
+ OINOLog.error("HTML can't be used as an input content type!", {contentType:OINOContentType.html})
59
+ return []
60
+ } else {
61
+ OINOLog.error("Unrecognized input content type!", {contentType:requestParams.requestType})
62
+ return []
63
+ }
64
+ }
65
+ /**
66
+ * Create data rows from request body based on the datamodel.
67
+ *
68
+ * @param datamodel datamodel of the api
69
+ * @param data data as an Buffer
70
+ * @param requestParams parameters
71
+ *
72
+ */
73
+ static createRowsFromBlob(datamodel:OINODbDataModel, data:Buffer, requestParams:OINODbApiRequestParams ):OINODataRow[] {
74
+ if ((requestParams.requestType == OINOContentType.json) || (requestParams.requestType == undefined)) {
75
+ return this._createRowFromJson(datamodel, data.toString()) // JSON is always a string
76
+
77
+ } else if (requestParams.requestType == OINOContentType.csv) {
78
+ return this._createRowFromCsv(datamodel, data.toString()) // binary data has to be base64 encoded so it's a string
79
+
80
+ } else if (requestParams.requestType == OINOContentType.formdata) {
81
+ return this._createRowFromFormdata(datamodel, data, requestParams.multipartBoundary || "")
82
+
83
+ } else if (requestParams.requestType == OINOContentType.urlencode) {
84
+ return this._createRowFromUrlencoded(datamodel, data.toString()) // data is urlencoded so it's a string
85
+
86
+ } else if (requestParams.requestType == OINOContentType.html) {
87
+ OINOLog.error("HTML can't be used as an input content type!", {contentType:OINOContentType.html})
88
+ return []
89
+ } else {
90
+ OINOLog.error("Unrecognized input content type!", {contentType:requestParams.requestType})
91
+ return []
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Create one data row from javascript object based on the datamodel.
97
+ * NOTE! Data assumed to be unserialized i.e. of the native type (string, number, boolean, Buffer)
98
+ *
99
+ * @param datamodel datamodel of the api
100
+ * @param data data as javascript object
101
+ *
102
+ */
103
+ static createRowFromObject(datamodel:OINODbDataModel, data:any):OINODataRow {
104
+ const fields:OINODbDataField[] = datamodel.fields
105
+ let result:OINODataRow = new Array(fields.length)
106
+ for (let i=0; i < fields.length; i++) {
107
+ result[i] = data[fields[i].name]
108
+ }
109
+ return result
110
+ }
111
+
112
+ private static _findCsvLineEnd(csvData:string, start:number):number {
113
+ const n:number = csvData.length
114
+ if (start >= n) {
115
+ return start
116
+ }
117
+ let end:number = start
118
+ let quote_open:boolean = false
119
+ while (end<n) {
120
+ if (csvData[end] == "\"") {
121
+ if (!quote_open) {
122
+ quote_open = true
123
+ } else if ((end < n-1) && (csvData[end+1] == "\"")) {
124
+ end++
125
+ } else {
126
+ quote_open = false
127
+ }
128
+ } else if ((!quote_open) && (csvData[end] == "\r")) {
129
+ return end
130
+ }
131
+ end++
132
+ }
133
+ return n
134
+ }
135
+
136
+ private static _parseCsvLine(csvLine:string):(string|null|undefined)[] {
137
+ let result:(string|null|undefined)[] = []
138
+ const n:number = csvLine.length
139
+ let start:number = 0
140
+ let end:number = 0
141
+ let quote_open:boolean = false
142
+ let has_quotes:boolean = false
143
+ let has_escaped_quotes = false
144
+ let found_field = false
145
+ while (end<n) {
146
+ if (csvLine[end] == "\"") {
147
+ if (!quote_open) {
148
+ quote_open = true
149
+ } else if ((end < n-1) && (csvLine[end+1] == "\"")) {
150
+ end++
151
+ has_escaped_quotes = true
152
+ } else {
153
+ has_quotes = true
154
+ quote_open = false
155
+ }
156
+ }
157
+ if ((!quote_open) && ((end == n-1) || (csvLine[end] == ","))) {
158
+ found_field = true
159
+ if (end == n-1) {
160
+ end++
161
+ }
162
+ }
163
+ if (found_field) {
164
+ // console.log("OINODB_csvParseLine: next field=" + csvLine.substring(start,end) + ", start="+start+", end="+end)
165
+ let field_str:string|undefined|null
166
+ if (has_quotes) {
167
+ field_str = csvLine.substring(start+1,end-1)
168
+ } else if (start == end) {
169
+ field_str = undefined
170
+ } else {
171
+ field_str = csvLine.substring(start,end)
172
+ if (field_str == "null") {
173
+ field_str = null
174
+ }
175
+ }
176
+ result.push(field_str)
177
+ has_quotes = false
178
+ has_escaped_quotes = true
179
+ found_field = false
180
+ start = end+1
181
+ }
182
+ end++
183
+ }
184
+ return result
185
+ }
186
+
187
+ private static _createRowFromCsv(datamodel:OINODbDataModel, data:string):OINODataRow[] {
188
+ let result:OINODataRow[] = []
189
+ const n = data.length
190
+ let start:number = 0
191
+ let end:number = this._findCsvLineEnd(data, start)
192
+ const header_str = data.substring(start, end)
193
+ const headers:(string|null|undefined)[] = this._parseCsvLine(header_str)
194
+ let field_to_header_mapping:number[] = new Array(datamodel.fields.length)
195
+ let headers_found:boolean = false
196
+ for (let i=0; i<field_to_header_mapping.length; i++) {
197
+ field_to_header_mapping[i] = headers.indexOf(datamodel.fields[i].name)
198
+ headers_found = headers_found || (field_to_header_mapping[i] >= 0)
199
+ }
200
+ // OINOLog.debug("createRowFromCsv", {headers:headers, field_to_header_mapping:field_to_header_mapping})
201
+ if (!headers_found) {
202
+ return result
203
+ }
204
+ start = end + 1
205
+ end = start
206
+ while (end < n) {
207
+ while ((start < n) && ((data[start] == "\r") || (data[start] == "\n"))) {
208
+ start++
209
+ }
210
+ if (start >= n) {
211
+ return result
212
+ }
213
+ end = this._findCsvLineEnd(data, start)
214
+ const row_data:(string|null|undefined)[] = this._parseCsvLine(data.substring(start, end))
215
+ const row:OINODataRow = new Array(field_to_header_mapping.length)
216
+ let has_data:boolean = false
217
+ for (let i=0; i<datamodel.fields.length; i++) {
218
+ const field:OINODbDataField = datamodel.fields[i]
219
+ let j:number = field_to_header_mapping[i]
220
+ let value:string|null|undefined = row_data[j]
221
+ if ((value === undefined) || (value === null)) { // null/undefined-decoding built into the parser
222
+ row[i] = value
223
+
224
+ } else if ((j >= 0) && (j < row_data.length)) {
225
+ value = OINOStr.decode(value, OINOContentType.csv)
226
+ if (value && (field.fieldParams.isPrimaryKey || field.fieldParams.isForeignKey) && (field instanceof OINONumberDataField) && (datamodel.api.hashid)) {
227
+ value = datamodel.api.hashid.decode(value)
228
+ }
229
+ row[i] = field.deserializeCell(value)
230
+
231
+ } else {
232
+ row[i] = undefined
233
+ }
234
+ has_data = has_data || (row[i] !== undefined)
235
+ }
236
+ // console.log("createRowFromCsv: next row=" + row)
237
+ if (has_data) {
238
+ result.push(row)
239
+ } else {
240
+ OINOLog.warning("createRowFromCsv: empty row skipped")
241
+ }
242
+ start = end
243
+ end = start
244
+ }
245
+
246
+ return result
247
+ }
248
+
249
+ private static _createRowFromJsonObj(obj:any, datamodel:OINODbDataModel):OINODataRow|undefined {
250
+ // console.log("createRowFromJsonObj: obj=" + JSON.stringify(obj))
251
+ const fields:OINODbDataField[] = datamodel.fields
252
+ let result:OINODataRow = new Array(fields.length)
253
+ let has_data:boolean = false
254
+ // console.log("createRowFromJsonObj: " + result)
255
+ for (let i=0; i < fields.length; i++) {
256
+ const field = fields[i]
257
+ let value:any = obj[field.name]
258
+ // console.log("createRowFromJsonObj: key=" + field.name + ", val=" + val)
259
+ if ((value === null) || (value === undefined)) { // must be checed first as null is an object
260
+ result[i] = value
261
+
262
+ } else if (Array.isArray(value) || typeof value === "object") {
263
+ result[i] = JSON.stringify(value).replaceAll("\"","\\\"") // only single level deep objects, rest is handled as JSON-strings
264
+
265
+ } else if (typeof value === "string") {
266
+ value = OINOStr.decode(value, OINOContentType.json)
267
+ if (value && (field.fieldParams.isPrimaryKey || field.fieldParams.isForeignKey) && (field instanceof OINONumberDataField) && (datamodel.api.hashid)) {
268
+ value = datamodel.api.hashid.decode(value)
269
+ }
270
+ result[i] = field.deserializeCell(value)
271
+
272
+ } else {
273
+ result[i] = value // value types are passed as-is
274
+ }
275
+ has_data = has_data || (result[i] !== undefined)
276
+ // console.log("createRowFromJsonObj: result["+i+"]=" + result[i])
277
+ }
278
+ // console.log("createRowFromJsonObj: " + result)
279
+ if (has_data) {
280
+ return result
281
+ } else {
282
+ OINOLog.warning("createRowFromJsonObj: empty row skipped")
283
+ return undefined
284
+ }
285
+ }
286
+
287
+ private static _createRowFromJson(datamodel:OINODbDataModel, data:string):OINODataRow[] {
288
+ let result:OINODataRow[] = []
289
+ // console.log("OINORowFactoryJson: data=" + data)
290
+ const obj:object = JSON.parse(data)
291
+ if (Array.isArray(obj)) {
292
+ obj.forEach(row => {
293
+ const data_row = this._createRowFromJsonObj(row, datamodel)
294
+ if (data_row !== undefined) {
295
+ result.push(data_row)
296
+ }
297
+ })
298
+
299
+ } else {
300
+ const data_row = this._createRowFromJsonObj(obj, datamodel)
301
+ if (data_row !== undefined) {
302
+ result.push(data_row)
303
+ }
304
+ }
305
+ return result
306
+ }
307
+
308
+ private static _findMultipartBoundary(formData:Buffer, multipartBoundary:string, start:number):number {
309
+ let n:number = formData.indexOf(multipartBoundary, start)
310
+ if (n >= 0) {
311
+ n += multipartBoundary.length + 2
312
+ } else {
313
+ n = formData.length
314
+ }
315
+ return n
316
+ }
317
+
318
+ private static _parseMultipartLine(data:Buffer, start:number):string {
319
+ let line_end:number = data.indexOf('\r\n', start)
320
+ if (line_end >= start) {
321
+ return data.subarray(start, line_end).toString()
322
+ } else {
323
+ return ''
324
+ }
325
+ }
326
+
327
+ private static _multipartHeaderRegex:RegExp = /Content-Disposition\: (form-data|file); name=\"([^\"]+)\"(; filename=.*)?/i
328
+
329
+ private static _createRowFromFormdata(datamodel:OINODbDataModel, data:Buffer, multipartBoundary:string):OINODataRow[] {
330
+ let result:OINODataRow[] = []
331
+ try {
332
+ const n = data.length
333
+ let start:number = this._findMultipartBoundary(data, multipartBoundary, 0)
334
+ let end:number = this._findMultipartBoundary(data, multipartBoundary, start)
335
+ // OINOLog.debug("createRowFromFormdata: enter", {start:start, end:end, multipartBoundary:multipartBoundary})
336
+ const row:OINODataRow = new Array(datamodel.fields.length)
337
+ let has_data:boolean = false
338
+ while (end < n) {
339
+ // OINOLog.debug("createRowFromFormdata: next block", {start:start, end:end, block:data.substring(start, end)})
340
+ let block_ok:boolean = true
341
+ let l:string = this._parseMultipartLine(data, start)
342
+ // OINOLog.debug("createRowFromFormdata: next line", {start:start, end:end, line:l})
343
+ start += l.length+2
344
+ const header_matches = OINODbParser._multipartHeaderRegex.exec(l)
345
+ if (!header_matches) {
346
+ OINOLog.warning("OINODbFactory.createRowFromFormdata: unsupported block skipped!", {header_line:l})
347
+ block_ok = false
348
+
349
+ } else {
350
+ const field_name = header_matches[2]
351
+ const is_file = header_matches[3] != null
352
+ let is_base64:boolean = false
353
+ const field_index:number = datamodel.findFieldIndexByName(field_name)
354
+ // OINOLog.debug("createRowFromFormdata: header", {field_name:field_name, field_index:field_index, is_file:is_file, is_base64:is_base64})
355
+ if (field_index < 0) {
356
+ OINOLog.warning("OINODbFactory.createRowFromFormdata: form field not found and skipped!", {field_name:field_name})
357
+ block_ok = false
358
+
359
+ } else {
360
+ const field:OINODbDataField = datamodel.fields[field_index]
361
+ l = this._parseMultipartLine(data, start)
362
+ // OINOLog.debug("createRowFromFormdata: next line", {start:start, end:end, line:l})
363
+ while (block_ok && (l != '')) {
364
+ if (l.startsWith('Content-Type:') && (l.indexOf('multipart/mixed')>=0)) {
365
+ OINOLog.warning("OINODbFactory.createRowFromFormdata: mixed multipart files not supported and skipped!", {header_line:l})
366
+ block_ok = false
367
+ } else if (l.startsWith('Content-Transfer-Encoding:') && (l.indexOf('BASE64')>=0)) {
368
+ is_base64 = true
369
+ }
370
+ start += l.length+2
371
+ l = this._parseMultipartLine(data, start)
372
+ // OINOLog.debug("createRowFromFormdata: next line", {start:start, end:end, line:l})
373
+ }
374
+ start += 2
375
+ if (!block_ok) {
376
+ OINOLog.warning("OINODbFactory.createRowFromFormdata: invalid block skipped", {field_name:field_name})
377
+ } else if (start + multipartBoundary.length + 2 >= end) {
378
+ // OINOLog.debug("OINODbFactory.createRowFromFormdata: null value", {field_name:field_name})
379
+ row[field_index] = null
380
+
381
+ } else if (is_file) {
382
+ if (is_base64) {
383
+ const value = this._parseMultipartLine(data, start).trim()
384
+ row[field_index] = field.deserializeCell(OINOStr.decode(value, OINOContentType.formdata))
385
+ } else {
386
+ const e = this._findMultipartBoundary(data, multipartBoundary, start)
387
+ const value = data.subarray(start, e-2)
388
+ row[field_index] = value
389
+ }
390
+ // console.log("OINODbFactory.createRowFromFormdata: file field", {field_name:field_name, value:row[field_index]})
391
+ } else {
392
+ let value:string = OINOStr.decode(this._parseMultipartLine(data, start).trim(), OINOContentType.formdata)
393
+ // OINOLog.debug("OINODbFactory.createRowFromFormdata: parse form field", {field_name:field_name, value:value})
394
+ if (value && (field.fieldParams.isPrimaryKey || field.fieldParams.isForeignKey) && (field instanceof OINONumberDataField) && (datamodel.api.hashid)) {
395
+ value = datamodel.api.hashid.decode(value)
396
+ }
397
+ row[field_index] = field.deserializeCell(value)
398
+ }
399
+ has_data = has_data || (row[field_index] !== undefined)
400
+ }
401
+ }
402
+ start = end
403
+ end = this._findMultipartBoundary(data, multipartBoundary, start)
404
+ }
405
+ // OINOLog.debug("createRowFromFormdata: next row", {row:row})
406
+ if (has_data) {
407
+ result.push(row)
408
+ } else {
409
+ OINOLog.warning("createRowFromFormdata: empty row skipped")
410
+ }
411
+ } catch (e:any) {
412
+ OINOLog.error("createRowFromFormdata: error parsing formdata", {exception:e.message})
413
+ }
414
+ return result
415
+ }
416
+ private static _createRowFromUrlencoded(datamodel:OINODbDataModel, data:string):OINODataRow[] {
417
+ // OINOLog.debug("createRowFromUrlencoded: enter", {data:data})
418
+ let result:OINODataRow[] = []
419
+ const row:OINODataRow = new Array(datamodel.fields.length)
420
+ let has_data:boolean = false
421
+ const data_parts:string[] = data.trim().split('&')
422
+ try {
423
+ for (let i=0; i<data_parts.length; i++) {
424
+ const param_parts = data_parts[i].split('=')
425
+ // OINOLog.debug("createRowFromUrlencoded: next param", {param_parts:param_parts})
426
+ if (param_parts.length == 2) {
427
+ const key=OINOStr.decodeUrlencode(param_parts[0]) || ""
428
+ const field_index:number = datamodel.findFieldIndexByName(key)
429
+ if (field_index < 0) {
430
+ OINOLog.info("createRowFromUrlencoded: param field not found", {field:key})
431
+
432
+ } else {
433
+ const field:OINODbDataField = datamodel.fields[field_index]
434
+ let value:string=OINOStr.decode(param_parts[1], OINOContentType.urlencode)
435
+ if (value && (field.fieldParams.isPrimaryKey || field.fieldParams.isForeignKey) && (field instanceof OINONumberDataField) && (datamodel.api.hashid)) {
436
+ value = datamodel.api.hashid.decode(value)
437
+ }
438
+ row[field_index] = field.deserializeCell(value)
439
+ has_data = has_data || (row[field_index] !== undefined)
440
+ }
441
+ }
442
+
443
+ // const value = requestParams[]
444
+
445
+ }
446
+ if (has_data) {
447
+ result.push(row)
448
+ } else {
449
+ OINOLog.warning("createRowFromUrlencoded: empty row skipped")
450
+ }
451
+ } catch (e:any) {
452
+ OINOLog.error("createRowFromUrlencoded: error parsing urlencoded data", {exception:e.message})
453
+ }
454
+ // console.log("createRowFromUrlencoded: next row=" + row)
455
+ return result
456
+ }
457
+
458
458
  }