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