@oino-ts/db 0.17.1 → 0.17.2

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/src/OINODbApi.ts CHANGED
@@ -1,603 +1,612 @@
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 { OINODbApiParams, OINODb, OINODbDataSet, OINODbDataModel, OINODbDataField, OINOStringDataField, OINO_ERROR_PREFIX, OINODataRow, OINODataCell, OINODbModelSet, OINOBenchmark, OINODbConfig, OINOHttpResult, OINOHtmlTemplate, OINONumberDataField, OINODbParser, OINODatetimeDataField, OINODbSqlParams, OINODbSqlAggregate, OINODbSqlSelect, OINODbSqlFilter, OINODbSqlOrder, OINODbSqlLimit } from "./index.js"
8
- import { OINOLog, OINOResult, OINOHttpRequest, OINOHttpRequestInit } from "@oino-ts/common";
9
- import { OINOHashid } from "@oino-ts/hashid"
10
-
11
- export interface OINODbApiRequestInit extends OINOHttpRequestInit {
12
- rowId?: string
13
- data?: string|OINODataRow[]|Buffer|Uint8Array|object|null
14
- sqlParams?: OINODbSqlParams
15
- filter?: OINODbSqlFilter
16
- order?: OINODbSqlOrder
17
- limit?: OINODbSqlLimit
18
- aggregate?: OINODbSqlAggregate
19
- select?: OINODbSqlSelect
20
- }
21
-
22
- export class OINODbApiRequest extends OINOHttpRequest {
23
- readonly rowId:string
24
- readonly data:string|OINODataRow[]|Buffer|Uint8Array|object|null
25
- readonly sqlParams:OINODbSqlParams
26
-
27
- constructor (init: OINODbApiRequestInit) {
28
- super(init)
29
- this.rowId = init?.rowId || ""
30
- this.data = init?.data || null
31
- this.sqlParams = init?.sqlParams || {}
32
-
33
- if (init?.filter) {
34
- this.sqlParams.filter = init.filter
35
- }
36
- if (!this.sqlParams.filter) {
37
- const filter_param = this.url?.searchParams.get(OINODbConfig.OINODB_SQL_FILTER_PARAM)
38
- if (filter_param) {
39
- this.sqlParams.filter = OINODbSqlFilter.parse(filter_param)
40
- }
41
- }
42
- if (init?.order) {
43
- this.sqlParams.order = init.order
44
- }
45
- if (!this.sqlParams.order) {
46
- const order_param = this.url?.searchParams.get(OINODbConfig.OINODB_SQL_ORDER_PARAM)
47
- if (order_param) {
48
- this.sqlParams.order = OINODbSqlOrder.parse(order_param)
49
- }
50
- }
51
- if (init?.limit) {
52
- this.sqlParams.limit = init.limit
53
- }
54
- if (!this.sqlParams.limit) {
55
- const limit_param = this.url?.searchParams.get(OINODbConfig.OINODB_SQL_LIMIT_PARAM)
56
- if (limit_param) {
57
- this.sqlParams.limit = OINODbSqlLimit.parse(limit_param)
58
- }
59
- }
60
- if (init?.aggregate) {
61
- this.sqlParams.aggregate = init.aggregate
62
- }
63
- if (!this.sqlParams.aggregate) {
64
- const aggregate_param = this.url?.searchParams.get(OINODbConfig.OINODB_SQL_AGGREGATE_PARAM)
65
- if (aggregate_param) {
66
- this.sqlParams.aggregate = OINODbSqlAggregate.parse(aggregate_param)
67
- }
68
- }
69
- if (init?.select) {
70
- this.sqlParams.select = init.select
71
- }
72
- if (!this.sqlParams.select) {
73
- const select_param = this.url?.searchParams.get(OINODbConfig.OINODB_SQL_SELECT_PARAM)
74
- if (select_param) {
75
- this.sqlParams.select = OINODbSqlSelect.parse(select_param)
76
- }
77
- }
78
- }
79
- }
80
-
81
- /**
82
- * OINO API request result object with returned data and/or http status code/message and
83
- * error / warning messages.
84
- *
85
- */
86
- export class OINODbApiResult extends OINOResult {
87
- /** DbApi request params */
88
- request: OINODbApiRequest
89
-
90
- /** Returned data if any */
91
- data?: OINODbModelSet;
92
-
93
- /**
94
- * Constructor of OINODbApiResult.
95
- *
96
- * @param request DbApi request parameters
97
- * @param data result data
98
- *
99
- */
100
- constructor (request:OINODbApiRequest, data?:OINODbModelSet) {
101
- super()
102
- this.request = request
103
- this.data = data
104
- }
105
-
106
- /**
107
- * Creates a HTTP Response from API results.
108
- *
109
- * @param headers Headers to include in the response
110
- *
111
- */
112
- async writeApiResponse(headers:Record<string, string> = {}):Promise<Response> {
113
- let response:Response|null = null
114
- if (this.success && this.data) {
115
- const body = await this.data.writeString(this.request.responseType)
116
- response = new Response(body, {status:this.status, statusText: this.statusText, headers: headers })
117
- } else {
118
- response = new Response(JSON.stringify(this, null, 3), {status:this.status, statusText: this.statusText, headers: headers })
119
- }
120
- for (let i=0; i<this.messages.length; i++) {
121
- response.headers.set('X-OINO-MESSAGE-' + i, this.messages[i])
122
- }
123
- return Promise.resolve(response)
124
- }
125
- }
126
-
127
- /**
128
- * Specialized HTML template that can render ´OINODbApiResult´.
129
- *
130
- */
131
- export class OINODbHtmlTemplate extends OINOHtmlTemplate {
132
- /** Locale validation regex */
133
- static LOCALE_REGEX:RegExp = /^(\w\w)(\-\w\w)?$/
134
- /** Locale formatter */
135
- protected _locale:Intl.DateTimeFormat|null
136
- protected _numberDecimals:number = -1
137
-
138
- /**
139
- * Constructor of OINODbHtmlTemplate.
140
- *
141
- * @param template HTML template string
142
- * @param numberDecimals Number of decimals to use for numbers, -1 for no formatting
143
- * @param dateLocaleStr Datetime format string, either "iso" for ISO8601 or "default" for system default or valid locale string
144
- * @param dateLocaleStyle Datetime format style, either "short/medium/long/full" or Intl.DateTimeFormat options
145
- *
146
- */
147
- constructor (template:string, numberDecimals:number=-1, dateLocaleStr:string="", dateLocaleStyle:string|any="") {
148
- super(template)
149
- let locale_opts:any
150
- if ((dateLocaleStyle == null) || (dateLocaleStyle == "")) {
151
- locale_opts = { dateStyle: "medium", timeStyle: "medium" }
152
- } else if (typeof dateLocaleStyle == "string") {
153
- locale_opts = { dateStyle: dateLocaleStyle, timeStyle: dateLocaleStyle }
154
- } else {
155
- locale_opts = dateLocaleStyle
156
- }
157
- this._locale = null
158
- this._numberDecimals = numberDecimals
159
-
160
- if ((dateLocaleStr != null) && (dateLocaleStr != "") && OINODbHtmlTemplate.LOCALE_REGEX.test(dateLocaleStr)) {
161
- try {
162
- this._locale = new Intl.DateTimeFormat(dateLocaleStr, locale_opts)
163
- } catch (e:any) {}
164
- }
165
- }
166
-
167
- /**
168
- * Creates HTML Response from API modelset.
169
- *
170
- * @param modelset OINO API dataset
171
- * @param overrideValues values to override in the data
172
- *
173
- */
174
- async renderFromDbData(modelset:OINODbModelSet, overrideValues?:any):Promise<OINOHttpResult> {
175
- OINOBenchmark.startMetric("OINOHtmlTemplate", "renderFromDbData")
176
- let html:string = ""
177
- const dataset:OINODbDataSet|undefined = modelset.dataset
178
- const datamodel:OINODbDataModel = modelset.datamodel
179
- const api:OINODbApi = modelset.datamodel.api
180
- const modified_index = datamodel.findFieldIndexByName(api.params.cacheModifiedField || "")
181
- let last_modified:number = this.modified
182
-
183
- while (!dataset.isEof()) {
184
- const row:OINODataRow = dataset.getRow()
185
- if (modified_index >= 0) {
186
- last_modified = Math.max(last_modified, new Date(row[modified_index] as Date).getTime())
187
- }
188
- let row_id_seed:string = datamodel.getRowPrimarykeyValues(row).join(' ')
189
- let primary_key_values:string[] = []
190
- this.clearVariables()
191
- this.setVariableFromValue(OINODbConfig.OINODB_ID_FIELD, "")
192
- for (let i=0; i<datamodel.fields.length; i++) {
193
- const f:OINODbDataField = datamodel.fields[i]
194
- let value:string|null|undefined
195
- if ((f instanceof OINODatetimeDataField) && (this._locale != null)) {
196
- value = f.serializeCellWithLocale(row[i], this._locale)
197
-
198
- } else if ((f instanceof OINONumberDataField) && (this._numberDecimals >= 0) && (typeof row[i] === "number")) {
199
- // console.debug("renderFromDbData number decimals", { field: f.name, value: row[i], type: typeof row[i] });
200
- value = (row[i]! as number).toFixed(this._numberDecimals)
201
-
202
- } else {
203
- value = f.serializeCell(row[i])
204
- }
205
- if (f.fieldParams.isPrimaryKey || f.fieldParams.isForeignKey) {
206
- if (value && (f instanceof OINONumberDataField) && (datamodel.api.hashid)) {
207
- value = datamodel.api.hashid.encode(value, f.name + " " + row_id_seed)
208
- }
209
- if (f.fieldParams.isPrimaryKey) {
210
- primary_key_values.push(value || "")
211
- }
212
- }
213
- this.setVariableFromValue(f.name, value || "")
214
- }
215
- this.setVariableFromProperties(overrideValues)
216
- this.setVariableFromValue(OINODbConfig.OINODB_ID_FIELD, OINODbConfig.printOINOId(primary_key_values))
217
- html += this._renderHtml() + "\r\n"
218
- await dataset.next()
219
- }
220
- this.modified = last_modified
221
- const result:OINOHttpResult = this._createHttpResult(html)
222
- OINOBenchmark.endMetric("OINOHtmlTemplate", "renderFromDbData")
223
- return result
224
- }
225
-
226
- }
227
-
228
-
229
- /**
230
- * API class with method to process HTTP REST requests.
231
- *
232
- */
233
- export class OINODbApi {
234
- /** Enable debug output on errors */
235
- private _debugOnError:boolean = false
236
-
237
- /** API database reference */
238
- readonly db: OINODb
239
-
240
- /** API datamodel */
241
- readonly datamodel: OINODbDataModel
242
-
243
- /** API parameters */
244
- readonly params: OINODbApiParams
245
-
246
- /** API hashid */
247
- readonly hashid:OINOHashid|null
248
-
249
- /**
250
- * Constructor of API object.
251
- * NOTE! OINODb.initDatamodel must be called if created manually instead of the factory.
252
- *
253
- * @param db database for the API
254
- * @param params parameters for the API
255
- *
256
- */
257
- constructor (db: OINODb, params:OINODbApiParams) {
258
- if (!params.tableName) {
259
- throw new Error(OINO_ERROR_PREFIX + ": OINODbApiParams needs to define a table name!")
260
- }
261
- this.db = db
262
- this.params = params
263
- this.datamodel = new OINODbDataModel(this)
264
- if (this.params.hashidKey) {
265
- this.hashid = new OINOHashid(this.params.hashidKey, this.db.name, this.params.hashidLength, this.params.hashidStaticIds)
266
- } else {
267
- this.hashid = null
268
- }
269
- }
270
-
271
- private _validateRow(result:OINODbApiResult, row:OINODataRow, requirePrimaryKey:boolean):void {
272
- let field:OINODbDataField
273
- for (let i=0; i<this.datamodel.fields.length; i++) {
274
- field = this.datamodel.fields[i]
275
- const val:OINODataCell = row[i]
276
- if ((val === null) && ((field.fieldParams.isNotNull)||(field.fieldParams.isPrimaryKey))) { // null is a valid SQL value except if it's not allowed
277
- result.setError(405, "Field '" + field.name + "' is not allowed to be NULL!", "ValidateRowValues")
278
-
279
- } else if ((val === undefined) && (requirePrimaryKey) && (field.fieldParams.isPrimaryKey) && (!field.fieldParams.isAutoInc)) {
280
- result.setError(405, "Primary key '" + field.name + "' is not autoinc and missing from the data!", "ValidateRowValues")
281
-
282
- } else if ((val !== undefined) && (this.params.failOnUpdateOnAutoinc) && (field.fieldParams.isAutoInc)) {
283
- result.setError(405, "Autoinc field '" + field.name + "' can't be updated!", "ValidateRowValues")
284
-
285
- } else {
286
- if ((field instanceof OINOStringDataField) && ((field.maxLength > 0))){
287
- const str_val = val?.toString() || ""
288
- if (str_val.length > field.maxLength) {
289
- if (this.params.failOnOversizedValues) {
290
- result.setError(405, "Field '" + field.name + "' length (" + str_val.length + ") exceeds maximum (" + field.maxLength + ") and can't be set!", "ValidateRowValues")
291
- } else {
292
- result.addWarning("Field '" + field.name + "' length (" + str_val.length + ") exceeds maximum (" + field.maxLength + ") and might truncate or fail.", "ValidateRowValues")
293
- }
294
- }
295
- }
296
-
297
- }
298
- }
299
- //logDebug("OINODbApi.validateHttpValues", {result:result})
300
- }
301
-
302
- private _parseData(httpResult:OINODbApiResult, request:OINODbApiRequest):OINODataRow[] {
303
- let rows:OINODataRow[] = []
304
- try {
305
- if (Array.isArray(request.data)) {
306
- rows = request.data as OINODataRow[]
307
- } else if (request.data != null) {
308
- rows = OINODbParser.createRows(this.datamodel, request.data, request)
309
- }
310
-
311
- } catch (e:any) {
312
- httpResult.setError(400, "Invalid data: " + e.message, "DoRequest")
313
- }
314
- return rows
315
- }
316
-
317
- private async _doGet(result:OINODbApiResult, rowId:string, request:OINODbApiRequest):Promise<void> {
318
- let sql:string = ""
319
- try {
320
- sql = this.datamodel.printSqlSelect(rowId, request.sqlParams || {})
321
- OINOLog.debug("@oino-ts/db", "OINODbApi", "_doGet", "Print SQL", {sql:sql})
322
- const sql_res:OINODbDataSet = await this.db.sqlSelect(sql)
323
- if (sql_res.hasErrors()) {
324
- result.setError(500, sql_res.getFirstError(), "DoGet")
325
- if (this._debugOnError) {
326
- result.addDebug("OINO GET SQL [" + sql + "]", "DoPut")
327
- }
328
- } else {
329
- result.data = new OINODbModelSet(this.datamodel, sql_res, request.sqlParams)
330
- }
331
- } catch (e:any) {
332
- result.setError(500, "Unhandled exception in doGet: " + e.message, "DoGet")
333
- OINOLog.exception("@oino-ts/db", "OINODbApi", "_doGet", "exception in get request", {message:e.message, stack:e.stack})
334
- if (this._debugOnError) {
335
- result.addDebug("OINO GET SQL [" + sql + "]", "DoGet")
336
- }
337
- }
338
- }
339
-
340
- private async _doPost(result:OINODbApiResult, rows:OINODataRow[]):Promise<void> {
341
- let sql:string = ""
342
- try {
343
- for (let i=0; i<rows.length; i++) {
344
- this._validateRow(result, rows[i], this.params.failOnInsertWithoutKey||false)
345
- if (result.success) {
346
- sql += this.datamodel.printSqlInsert(rows[i])
347
-
348
- } else if (this.params.failOnAnyInvalidRows == false) {
349
- result.setOk() // individual rows may fail and will just be messages in response similar to executing multiple sql statements
350
- }
351
- }
352
- if ((sql == "") && result.success) {
353
- result.setError(405, "No valid rows for POST!", "DoPost")
354
-
355
- } else if (result.success) {
356
- OINOLog.debug("@oino-ts/db", "OINODbApi", "_doPost", "Print SQL", {sql:sql})
357
- const sql_res:OINODbDataSet = await this.db.sqlExec(sql)
358
- if (sql_res.hasErrors()) {
359
- result.setError(500, sql_res.getFirstError(), "DoPost")
360
- if (this._debugOnError) {
361
- result.addDebug("OINO POST MESSAGES [" + sql_res.messages.join('|') + "]", "DoPost")
362
- result.addDebug("OINO POST SQL [" + sql + "]", "DoPost")
363
- }
364
- }
365
- }
366
- } catch (e:any) {
367
- result.setError(500, "Unhandled exception in doPost: " + e.message, "DoPost")
368
- OINOLog.exception("@oino-ts/db", "OINODbApi", "_doPost", "exception in post request", {message:e.message, stack:e.stack})
369
- if (this._debugOnError) {
370
- result.addDebug("OINO POST SQL [" + sql + "]", "DoPost")
371
- }
372
- }
373
- }
374
-
375
- private async _doPut(result:OINODbApiResult, id:string|null, rows:OINODataRow[]):Promise<void> {
376
- let sql:string = ""
377
- try {
378
- // this._validateRowValues(result, row, false)
379
- for (let i=0; i<rows.length; i++) {
380
- const row_id = id || OINODbConfig.printOINOId(this.datamodel.getRowPrimarykeyValues(rows[i], this.hashid != null))
381
- this._validateRow(result, rows[i], this.params.failOnInsertWithoutKey||false)
382
- if (result.success) {
383
- sql += this.datamodel.printSqlUpdate(row_id, rows[i])
384
-
385
- } else if (this.params.failOnAnyInvalidRows == false) {
386
- result.setOk() // individual rows may fail and will just be messages in response similar to executing multiple sql statements
387
- }
388
- }
389
- if ((sql == "") && result.success) {
390
- result.setError(405, "No valid rows for PUT!", "DoPut") // only set error if there are multiple rows and no valid sql was created
391
-
392
- } else if (result.success) {
393
- OINOLog.debug("@oino-ts/db", "OINODbApi", "_doPut", "Print SQL", {sql:sql})
394
- const sql_res:OINODbDataSet = await this.db.sqlExec(sql)
395
- if (sql_res.hasErrors()) {
396
- result.setError(500, sql_res.getFirstError(), "DoPut")
397
- if (this._debugOnError) {
398
- result.addDebug("OINO PUT MESSAGES [" + sql_res.messages.join('|') + "]", "DoPut")
399
- result.addDebug("OINO PUT SQL [" + sql + "]", "DoPut")
400
- }
401
- }
402
- }
403
- } catch (e:any) {
404
- result.setError(500, "Unhandled exception: " + e.message, "DoPut")
405
- OINOLog.exception("@oino-ts/db", "OINODbApi", "_doPut", "exception in put request", {message:e.message, stack:e.stack})
406
- if (this._debugOnError) {
407
- result.addDebug("OINO POST SQL [" + sql + "]", "DoPut")
408
- }
409
- }
410
- }
411
-
412
- private async _doDelete(result:OINODbApiResult, id:string|null, rows:OINODataRow[]|null):Promise<void> {
413
- let sql:string = ""
414
- try {
415
- if (rows != null) {
416
- for (let i=0; i<rows.length; i++) {
417
- const row_id = OINODbConfig.printOINOId(this.datamodel.getRowPrimarykeyValues(rows[i], this.hashid != null))
418
- if (row_id) {
419
- sql += this.datamodel.printSqlDelete(row_id)
420
- } else if (this.params.failOnAnyInvalidRows == false) {
421
- result.setOk() // individual rows may fail and will just be messages in response similar to executing multiple sql statements
422
- }
423
- }
424
- } else if (id) {
425
- sql = this.datamodel.printSqlDelete(id)
426
- }
427
- if ((sql == "") && result.success) {
428
- result.setError(405, "No valid rows for DELETE!", "DoDelete") // only set error if there are multiple rows and no valid sql was created
429
-
430
- } else if (result.success) {
431
-
432
- OINOLog.debug("@oino-ts/db", "OINODbApi", "_doDelete", "Print SQL", {sql:sql})
433
- const sql_res:OINODbDataSet = await this.db.sqlExec(sql)
434
- if (sql_res.hasErrors()) {
435
- result.setError(500, sql_res.getFirstError(), "DoDelete")
436
- if (this._debugOnError) {
437
- result.addDebug("OINO DELETE MESSAGES [" + sql_res.messages.join('|') + "]", "DoDelete")
438
- result.addDebug("OINO DELETE SQL [" + sql + "]", "DoDelete")
439
- }
440
- }
441
- }
442
- } catch (e:any) {
443
- result.setError(500, "Unhandled exception: " + e.message, "DoDelete")
444
- OINOLog.exception("@oino-ts/db", "OINODbApi", "_doDelete", "exception in delete request", {message:e.message, stack:e.stack})
445
- if (this._debugOnError) {
446
- result.addDebug("OINO DELETE SQL [" + sql + "]", "DoDelete")
447
- }
448
- }
449
- }
450
-
451
- /**
452
- * Enable or disable debug output on errors.
453
- *
454
- * @param debugOnError true to enable debug output on errors, false to disable
455
- */
456
- setDebugOnError(debugOnError:boolean) {
457
- this._debugOnError = debugOnError
458
- }
459
-
460
- /**
461
- * Method for handling a HTTP REST request with GET, POST, PUT, DELETE corresponding to
462
- * SQL select, insert, update and delete.
463
- *
464
- * @param method HTTP method of the REST request
465
- * @param rowId URL id of the REST request
466
- * @param data HTTP body data as either serialized string or unserialized JS object or OINODataRow-array or Buffer/Uint8Array binary data
467
- * @param sqlParams SQL parameters for the REST request
468
- *
469
- */
470
- async doRequest(method:string, rowId:string, data:string|OINODataRow[]|Buffer|Uint8Array|object|null, sqlParams:OINODbSqlParams):Promise<OINODbApiResult> {
471
- return this.runRequest(new OINODbApiRequest({ method: method, rowId: rowId, data: data, sqlParams: sqlParams }))
472
- }
473
- /**
474
- * Method for handling a HTTP REST request with GET, POST, PUT, DELETE corresponding to
475
- * SQL select, insert, update and delete.
476
- *
477
- * @param request OINO DB API request
478
- *
479
- */
480
- async runRequest(request:OINODbApiRequest):Promise<OINODbApiResult> {
481
- OINOBenchmark.startMetric("OINODbApi", "doRequest." + request.method)
482
- OINOLog.debug("@oino-ts/db", "OINODbApi", "doRequest", "Request", {method:request.method, id:request.rowId, data:request.data})
483
- let result:OINODbApiResult = new OINODbApiResult(request)
484
- let rows:OINODataRow[] = []
485
- if ((request.method == "POST") || (request.method == "PUT")) {
486
- rows = this._parseData(result, request)
487
- }
488
- if (request.method == "GET") {
489
- await this._doGet(result, request.rowId, request)
490
-
491
- } else if (request.method == "PUT") {
492
- if (!request.rowId) {
493
- result.setError(400, "HTTP PUT method requires an URL ID for the row that is updated!", "DoRequest")
494
-
495
- } else if (rows.length != 1) {
496
- result.setError(400, "HTTP PUT method requires exactly one row in the body data!", "DoRequest")
497
-
498
- } else {
499
- try {
500
- await this._doPut(result, request.rowId, rows)
501
-
502
- } catch (e:any) {
503
- result.setError(500, "Unhandled exception in HTTP PUT doRequest: " + e.message, "DoRequest")
504
- }
505
- }
506
- } else if (request.method == "POST") {
507
- if (request.rowId) {
508
- result.setError(400, "HTTP POST method must not have an URL ID as it does not target an existing row but creates a new one!", "DoRequest")
509
-
510
- } else if (rows.length == 0) {
511
- result.setError(400, "HTTP POST method requires at least one row in the body data!", "DoRequest")
512
-
513
- } else {
514
- try {
515
- await this._doPost(result, rows)
516
-
517
- } catch (e:any) {
518
- result.setError(500, "Unhandled exception in HTTP POST doRequest: " + e.message, "DoRequest")
519
- }
520
- }
521
- } else if (request.method == "DELETE") {
522
- if (!request.rowId) {
523
- result.setError(400, "HTTP DELETE method requires an id!", "DoRequest")
524
-
525
- } else {
526
- try {
527
- await this._doDelete(result, request.rowId, null)
528
-
529
- } catch (e:any) {
530
- result.setError(500, "Unhandled exception in HTTP DELETE doRequest: " + e.message, "DoRequest")
531
- }
532
- }
533
- } else {
534
- result.setError(405, "Unsupported HTTP method '" + request.method + "' for REST request", "DoRequest")
535
- }
536
- OINOBenchmark.endMetric("OINODbApi", "doRequest." + request.method)
537
- return Promise.resolve(result)
538
- }
539
-
540
- /**
541
- * Method for handling a HTTP REST request with batch update using PUT or DELETE methods.
542
- *
543
- * @param method HTTP method of the REST request
544
- * @param rowId URL id of the REST request
545
- * @param data HTTP body data as either serialized string or unserialized JS object or OINODataRow-array or Buffer/Uint8Array binary data
546
- *
547
- */
548
- async doBatchUpdate(method:string, rowId:string, data:string|OINODataRow[]|Buffer|Uint8Array|object|null, sqlParams?: OINODbSqlParams):Promise<OINODbApiResult> {
549
- return this.runRequest(new OINODbApiRequest({ method: method, rowId: rowId, data: data, sqlParams: sqlParams }))
550
- }
551
- /**
552
- * Method for handling a HTTP REST request with batch update using PUT or DELETE methods.
553
- *
554
- * @param request HTTP URL parameters as key-value-pairs
555
- *
556
- */
557
- async runBatchUpdate(request:OINODbApiRequest):Promise<OINODbApiResult> {
558
- OINOLog.debug("@oino-ts/db", "OINODbApi", "doBatchUpdate", "Request", {request:request, data:request.data})
559
- let result:OINODbApiResult = new OINODbApiResult(request)
560
- if ((request.method != "PUT") && (request.method != "DELETE")) {
561
- result.setError(500, "Batch update only supports PUT and DELETE methods!", "DoBatchUpdate")
562
- return Promise.resolve(result)
563
- }
564
- OINOBenchmark.startMetric("OINODbApi", "doBatchUpdate." + request.method)
565
- const rows:OINODataRow[] = [] = this._parseData(result, request)
566
- if (request.method == "PUT") {
567
-
568
- try {
569
- await this._doPut(result, null, rows)
570
-
571
- } catch (e:any) {
572
- result.setError(500, "Unhandled exception in HTTP PUT doRequest: " + e.message, "DoBatchUpdate")
573
- }
574
-
575
- } else if (request.method == "DELETE") {
576
- try {
577
- await this._doDelete(result, null, rows)
578
-
579
- } catch (e:any) {
580
- result.setError(500, "Unhandled exception in HTTP DELETE doRequest: " + e.message, "DoBatchUpdate")
581
- }
582
- }
583
- OINOBenchmark.endMetric("OINODbApi", "doBatchUpdate." + request.method)
584
- return Promise.resolve(result)
585
- }
586
-
587
- /**
588
- * Method to check if a field is included in the API params.
589
- *
590
- * @param fieldName name of the field
591
- *
592
- */
593
-
594
- public isFieldIncluded(fieldName:string):boolean {
595
- const params = this.params
596
- return (
597
- ((params.excludeFieldPrefix == undefined) || (params.excludeFieldPrefix == "") || (fieldName.startsWith(params.excludeFieldPrefix) == false)) &&
598
- ((params.excludeFields == undefined) || (params.excludeFields.length == 0) || (params.excludeFields.indexOf(fieldName) < 0)) &&
599
- ((params.includeFields == undefined) || (params.includeFields.length == 0) || (params.includeFields.indexOf(fieldName) >= 0))
600
- )
601
- }
602
-
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 { OINODbApiParams, OINODb, OINODbDataSet, OINODbDataModel, OINODbDataField, OINOStringDataField, OINO_ERROR_PREFIX, OINODataRow, OINODataCell, OINODbModelSet, OINOBenchmark, OINODbConfig, OINOHttpResult, OINOHtmlTemplate, OINONumberDataField, OINODbParser, OINODatetimeDataField, OINODbSqlParams, OINODbSqlAggregate, OINODbSqlSelect, OINODbSqlFilter, OINODbSqlOrder, OINODbSqlLimit } from "./index.js"
8
+ import { OINOLog, OINOResult, OINOHttpRequest, OINOHttpRequestInit } from "@oino-ts/common";
9
+ import { OINOHashid } from "@oino-ts/hashid"
10
+
11
+ export interface OINODbApiRequestInit extends OINOHttpRequestInit {
12
+ rowId?: string
13
+ data?: string|OINODataRow[]|Buffer|Uint8Array|object|null
14
+ sqlParams?: OINODbSqlParams
15
+ filter?: OINODbSqlFilter
16
+ order?: OINODbSqlOrder
17
+ limit?: OINODbSqlLimit
18
+ aggregate?: OINODbSqlAggregate
19
+ select?: OINODbSqlSelect
20
+ }
21
+
22
+ export class OINODbApiRequest extends OINOHttpRequest {
23
+ readonly rowId:string
24
+ readonly data:string|OINODataRow[]|Buffer|Uint8Array|object|null
25
+ readonly sqlParams:OINODbSqlParams
26
+
27
+ constructor (init: OINODbApiRequestInit) {
28
+ super(init)
29
+ this.rowId = init?.rowId || ""
30
+ this.data = init?.data || null
31
+ this.sqlParams = init?.sqlParams || {}
32
+
33
+ if (init?.filter) {
34
+ this.sqlParams.filter = init.filter
35
+ }
36
+ if (!this.sqlParams.filter) {
37
+ const filter_param = this.url?.searchParams.get(OINODbConfig.OINODB_SQL_FILTER_PARAM)
38
+ if (filter_param) {
39
+ this.sqlParams.filter = OINODbSqlFilter.parse(filter_param)
40
+ }
41
+ }
42
+ if (init?.order) {
43
+ this.sqlParams.order = init.order
44
+ }
45
+ if (!this.sqlParams.order) {
46
+ const order_param = this.url?.searchParams.get(OINODbConfig.OINODB_SQL_ORDER_PARAM)
47
+ if (order_param) {
48
+ this.sqlParams.order = OINODbSqlOrder.parse(order_param)
49
+ }
50
+ }
51
+ if (init?.limit) {
52
+ this.sqlParams.limit = init.limit
53
+ }
54
+ if (!this.sqlParams.limit) {
55
+ const limit_param = this.url?.searchParams.get(OINODbConfig.OINODB_SQL_LIMIT_PARAM)
56
+ if (limit_param) {
57
+ this.sqlParams.limit = OINODbSqlLimit.parse(limit_param)
58
+ }
59
+ }
60
+ if (init?.aggregate) {
61
+ this.sqlParams.aggregate = init.aggregate
62
+ }
63
+ if (!this.sqlParams.aggregate) {
64
+ const aggregate_param = this.url?.searchParams.get(OINODbConfig.OINODB_SQL_AGGREGATE_PARAM)
65
+ if (aggregate_param) {
66
+ this.sqlParams.aggregate = OINODbSqlAggregate.parse(aggregate_param)
67
+ }
68
+ }
69
+ if (init?.select) {
70
+ this.sqlParams.select = init.select
71
+ }
72
+ if (!this.sqlParams.select) {
73
+ const select_param = this.url?.searchParams.get(OINODbConfig.OINODB_SQL_SELECT_PARAM)
74
+ if (select_param) {
75
+ this.sqlParams.select = OINODbSqlSelect.parse(select_param)
76
+ }
77
+ }
78
+ }
79
+ static async fromRequest(request: Request): Promise<OINODbApiRequest> {
80
+ const body = await request.arrayBuffer()
81
+ return new OINODbApiRequest({
82
+ url: new URL(request.url),
83
+ method: request.method,
84
+ headers: Object.fromEntries(request.headers as any),
85
+ data: Buffer.from(body),
86
+ })
87
+ }
88
+ }
89
+
90
+ /**
91
+ * OINO API request result object with returned data and/or http status code/message and
92
+ * error / warning messages.
93
+ *
94
+ */
95
+ export class OINODbApiResult extends OINOResult {
96
+ /** DbApi request params */
97
+ request: OINODbApiRequest
98
+
99
+ /** Returned data if any */
100
+ data?: OINODbModelSet;
101
+
102
+ /**
103
+ * Constructor of OINODbApiResult.
104
+ *
105
+ * @param request DbApi request parameters
106
+ * @param data result data
107
+ *
108
+ */
109
+ constructor (request:OINODbApiRequest, data?:OINODbModelSet) {
110
+ super()
111
+ this.request = request
112
+ this.data = data
113
+ }
114
+
115
+ /**
116
+ * Creates a HTTP Response from API results.
117
+ *
118
+ * @param headers Headers to include in the response
119
+ *
120
+ */
121
+ async writeApiResponse(headers:Record<string, string> = {}):Promise<Response> {
122
+ let response:Response|null = null
123
+ if (this.success && this.data) {
124
+ const body = await this.data.writeString(this.request.responseType)
125
+ response = new Response(body, {status:this.status, statusText: this.statusText, headers: headers })
126
+ } else {
127
+ response = new Response(JSON.stringify(this, null, 3), {status:this.status, statusText: this.statusText, headers: headers })
128
+ }
129
+ for (let i=0; i<this.messages.length; i++) {
130
+ response.headers.set('X-OINO-MESSAGE-' + i, this.messages[i])
131
+ }
132
+ return Promise.resolve(response)
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Specialized HTML template that can render ´OINODbApiResult´.
138
+ *
139
+ */
140
+ export class OINODbHtmlTemplate extends OINOHtmlTemplate {
141
+ /** Locale validation regex */
142
+ static LOCALE_REGEX:RegExp = /^(\w\w)(\-\w\w)?$/
143
+ /** Locale formatter */
144
+ protected _locale:Intl.DateTimeFormat|null
145
+ protected _numberDecimals:number = -1
146
+
147
+ /**
148
+ * Constructor of OINODbHtmlTemplate.
149
+ *
150
+ * @param template HTML template string
151
+ * @param numberDecimals Number of decimals to use for numbers, -1 for no formatting
152
+ * @param dateLocaleStr Datetime format string, either "iso" for ISO8601 or "default" for system default or valid locale string
153
+ * @param dateLocaleStyle Datetime format style, either "short/medium/long/full" or Intl.DateTimeFormat options
154
+ *
155
+ */
156
+ constructor (template:string, numberDecimals:number=-1, dateLocaleStr:string="", dateLocaleStyle:string|any="") {
157
+ super(template)
158
+ let locale_opts:any
159
+ if ((dateLocaleStyle == null) || (dateLocaleStyle == "")) {
160
+ locale_opts = { dateStyle: "medium", timeStyle: "medium" }
161
+ } else if (typeof dateLocaleStyle == "string") {
162
+ locale_opts = { dateStyle: dateLocaleStyle, timeStyle: dateLocaleStyle }
163
+ } else {
164
+ locale_opts = dateLocaleStyle
165
+ }
166
+ this._locale = null
167
+ this._numberDecimals = numberDecimals
168
+
169
+ if ((dateLocaleStr != null) && (dateLocaleStr != "") && OINODbHtmlTemplate.LOCALE_REGEX.test(dateLocaleStr)) {
170
+ try {
171
+ this._locale = new Intl.DateTimeFormat(dateLocaleStr, locale_opts)
172
+ } catch (e:any) {}
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Creates HTML Response from API modelset.
178
+ *
179
+ * @param modelset OINO API dataset
180
+ * @param overrideValues values to override in the data
181
+ *
182
+ */
183
+ async renderFromDbData(modelset:OINODbModelSet, overrideValues?:any):Promise<OINOHttpResult> {
184
+ OINOBenchmark.startMetric("OINOHtmlTemplate", "renderFromDbData")
185
+ let html:string = ""
186
+ const dataset:OINODbDataSet|undefined = modelset.dataset
187
+ const datamodel:OINODbDataModel = modelset.datamodel
188
+ const api:OINODbApi = modelset.datamodel.api
189
+ const modified_index = datamodel.findFieldIndexByName(api.params.cacheModifiedField || "")
190
+ let last_modified:number = this.modified
191
+
192
+ while (!dataset.isEof()) {
193
+ const row:OINODataRow = dataset.getRow()
194
+ if (modified_index >= 0) {
195
+ last_modified = Math.max(last_modified, new Date(row[modified_index] as Date).getTime())
196
+ }
197
+ let row_id_seed:string = datamodel.getRowPrimarykeyValues(row).join(' ')
198
+ let primary_key_values:string[] = []
199
+ this.clearVariables()
200
+ this.setVariableFromValue(OINODbConfig.OINODB_ID_FIELD, "")
201
+ for (let i=0; i<datamodel.fields.length; i++) {
202
+ const f:OINODbDataField = datamodel.fields[i]
203
+ let value:string|null|undefined
204
+ if ((f instanceof OINODatetimeDataField) && (this._locale != null)) {
205
+ value = f.serializeCellWithLocale(row[i], this._locale)
206
+
207
+ } else if ((f instanceof OINONumberDataField) && (this._numberDecimals >= 0) && (typeof row[i] === "number")) {
208
+ // console.debug("renderFromDbData number decimals", { field: f.name, value: row[i], type: typeof row[i] });
209
+ value = (row[i]! as number).toFixed(this._numberDecimals)
210
+
211
+ } else {
212
+ value = f.serializeCell(row[i])
213
+ }
214
+ if (f.fieldParams.isPrimaryKey || f.fieldParams.isForeignKey) {
215
+ if (value && (f instanceof OINONumberDataField) && (datamodel.api.hashid)) {
216
+ value = datamodel.api.hashid.encode(value, f.name + " " + row_id_seed)
217
+ }
218
+ if (f.fieldParams.isPrimaryKey) {
219
+ primary_key_values.push(value || "")
220
+ }
221
+ }
222
+ this.setVariableFromValue(f.name, value || "")
223
+ }
224
+ this.setVariableFromProperties(overrideValues)
225
+ this.setVariableFromValue(OINODbConfig.OINODB_ID_FIELD, OINODbConfig.printOINOId(primary_key_values))
226
+ html += this._renderHtml() + "\r\n"
227
+ await dataset.next()
228
+ }
229
+ this.modified = last_modified
230
+ const result:OINOHttpResult = this._createHttpResult(html)
231
+ OINOBenchmark.endMetric("OINOHtmlTemplate", "renderFromDbData")
232
+ return result
233
+ }
234
+
235
+ }
236
+
237
+
238
+ /**
239
+ * API class with method to process HTTP REST requests.
240
+ *
241
+ */
242
+ export class OINODbApi {
243
+ /** Enable debug output on errors */
244
+ private _debugOnError:boolean = false
245
+
246
+ /** API database reference */
247
+ readonly db: OINODb
248
+
249
+ /** API datamodel */
250
+ readonly datamodel: OINODbDataModel
251
+
252
+ /** API parameters */
253
+ readonly params: OINODbApiParams
254
+
255
+ /** API hashid */
256
+ readonly hashid:OINOHashid|null
257
+
258
+ /**
259
+ * Constructor of API object.
260
+ * NOTE! OINODb.initDatamodel must be called if created manually instead of the factory.
261
+ *
262
+ * @param db database for the API
263
+ * @param params parameters for the API
264
+ *
265
+ */
266
+ constructor (db: OINODb, params:OINODbApiParams) {
267
+ if (!params.tableName) {
268
+ throw new Error(OINO_ERROR_PREFIX + ": OINODbApiParams needs to define a table name!")
269
+ }
270
+ this.db = db
271
+ this.params = params
272
+ this.datamodel = new OINODbDataModel(this)
273
+ if (this.params.hashidKey) {
274
+ this.hashid = new OINOHashid(this.params.hashidKey, this.db.name, this.params.hashidLength, this.params.hashidStaticIds)
275
+ } else {
276
+ this.hashid = null
277
+ }
278
+ }
279
+
280
+ private _validateRow(result:OINODbApiResult, row:OINODataRow, requirePrimaryKey:boolean):void {
281
+ let field:OINODbDataField
282
+ for (let i=0; i<this.datamodel.fields.length; i++) {
283
+ field = this.datamodel.fields[i]
284
+ const val:OINODataCell = row[i]
285
+ if ((val === null) && ((field.fieldParams.isNotNull)||(field.fieldParams.isPrimaryKey))) { // null is a valid SQL value except if it's not allowed
286
+ result.setError(405, "Field '" + field.name + "' is not allowed to be NULL!", "ValidateRowValues")
287
+
288
+ } else if ((val === undefined) && (requirePrimaryKey) && (field.fieldParams.isPrimaryKey) && (!field.fieldParams.isAutoInc)) {
289
+ result.setError(405, "Primary key '" + field.name + "' is not autoinc and missing from the data!", "ValidateRowValues")
290
+
291
+ } else if ((val !== undefined) && (this.params.failOnUpdateOnAutoinc) && (field.fieldParams.isAutoInc)) {
292
+ result.setError(405, "Autoinc field '" + field.name + "' can't be updated!", "ValidateRowValues")
293
+
294
+ } else {
295
+ if ((field instanceof OINOStringDataField) && ((field.maxLength > 0))){
296
+ const str_val = val?.toString() || ""
297
+ if (str_val.length > field.maxLength) {
298
+ if (this.params.failOnOversizedValues) {
299
+ result.setError(405, "Field '" + field.name + "' length (" + str_val.length + ") exceeds maximum (" + field.maxLength + ") and can't be set!", "ValidateRowValues")
300
+ } else {
301
+ result.addWarning("Field '" + field.name + "' length (" + str_val.length + ") exceeds maximum (" + field.maxLength + ") and might truncate or fail.", "ValidateRowValues")
302
+ }
303
+ }
304
+ }
305
+
306
+ }
307
+ }
308
+ //logDebug("OINODbApi.validateHttpValues", {result:result})
309
+ }
310
+
311
+ private _parseData(httpResult:OINODbApiResult, request:OINODbApiRequest):OINODataRow[] {
312
+ let rows:OINODataRow[] = []
313
+ try {
314
+ if (Array.isArray(request.data)) {
315
+ rows = request.data as OINODataRow[]
316
+ } else if (request.data != null) {
317
+ rows = OINODbParser.createRows(this.datamodel, request.data, request)
318
+ }
319
+
320
+ } catch (e:any) {
321
+ httpResult.setError(400, "Invalid data: " + e.message, "DoRequest")
322
+ }
323
+ return rows
324
+ }
325
+
326
+ private async _doGet(result:OINODbApiResult, rowId:string, request:OINODbApiRequest):Promise<void> {
327
+ let sql:string = ""
328
+ try {
329
+ sql = this.datamodel.printSqlSelect(rowId, request.sqlParams || {})
330
+ OINOLog.debug("@oino-ts/db", "OINODbApi", "_doGet", "Print SQL", {sql:sql})
331
+ const sql_res:OINODbDataSet = await this.db.sqlSelect(sql)
332
+ if (sql_res.hasErrors()) {
333
+ result.setError(500, sql_res.getFirstError(), "DoGet")
334
+ if (this._debugOnError) {
335
+ result.addDebug("OINO GET SQL [" + sql + "]", "DoPut")
336
+ }
337
+ } else {
338
+ result.data = new OINODbModelSet(this.datamodel, sql_res, request.sqlParams)
339
+ }
340
+ } catch (e:any) {
341
+ result.setError(500, "Unhandled exception in doGet: " + e.message, "DoGet")
342
+ OINOLog.exception("@oino-ts/db", "OINODbApi", "_doGet", "exception in get request", {message:e.message, stack:e.stack})
343
+ if (this._debugOnError) {
344
+ result.addDebug("OINO GET SQL [" + sql + "]", "DoGet")
345
+ }
346
+ }
347
+ }
348
+
349
+ private async _doPost(result:OINODbApiResult, rows:OINODataRow[]):Promise<void> {
350
+ let sql:string = ""
351
+ try {
352
+ for (let i=0; i<rows.length; i++) {
353
+ this._validateRow(result, rows[i], this.params.failOnInsertWithoutKey||false)
354
+ if (result.success) {
355
+ sql += this.datamodel.printSqlInsert(rows[i])
356
+
357
+ } else if (this.params.failOnAnyInvalidRows == false) {
358
+ result.setOk() // individual rows may fail and will just be messages in response similar to executing multiple sql statements
359
+ }
360
+ }
361
+ if ((sql == "") && result.success) {
362
+ result.setError(405, "No valid rows for POST!", "DoPost")
363
+
364
+ } else if (result.success) {
365
+ OINOLog.debug("@oino-ts/db", "OINODbApi", "_doPost", "Print SQL", {sql:sql})
366
+ const sql_res:OINODbDataSet = await this.db.sqlExec(sql)
367
+ if (sql_res.hasErrors()) {
368
+ result.setError(500, sql_res.getFirstError(), "DoPost")
369
+ if (this._debugOnError) {
370
+ result.addDebug("OINO POST MESSAGES [" + sql_res.messages.join('|') + "]", "DoPost")
371
+ result.addDebug("OINO POST SQL [" + sql + "]", "DoPost")
372
+ }
373
+ }
374
+ }
375
+ } catch (e:any) {
376
+ result.setError(500, "Unhandled exception in doPost: " + e.message, "DoPost")
377
+ OINOLog.exception("@oino-ts/db", "OINODbApi", "_doPost", "exception in post request", {message:e.message, stack:e.stack})
378
+ if (this._debugOnError) {
379
+ result.addDebug("OINO POST SQL [" + sql + "]", "DoPost")
380
+ }
381
+ }
382
+ }
383
+
384
+ private async _doPut(result:OINODbApiResult, id:string|null, rows:OINODataRow[]):Promise<void> {
385
+ let sql:string = ""
386
+ try {
387
+ // this._validateRowValues(result, row, false)
388
+ for (let i=0; i<rows.length; i++) {
389
+ const row_id = id || OINODbConfig.printOINOId(this.datamodel.getRowPrimarykeyValues(rows[i], this.hashid != null))
390
+ this._validateRow(result, rows[i], this.params.failOnInsertWithoutKey||false)
391
+ if (result.success) {
392
+ sql += this.datamodel.printSqlUpdate(row_id, rows[i])
393
+
394
+ } else if (this.params.failOnAnyInvalidRows == false) {
395
+ result.setOk() // individual rows may fail and will just be messages in response similar to executing multiple sql statements
396
+ }
397
+ }
398
+ if ((sql == "") && result.success) {
399
+ result.setError(405, "No valid rows for PUT!", "DoPut") // only set error if there are multiple rows and no valid sql was created
400
+
401
+ } else if (result.success) {
402
+ OINOLog.debug("@oino-ts/db", "OINODbApi", "_doPut", "Print SQL", {sql:sql})
403
+ const sql_res:OINODbDataSet = await this.db.sqlExec(sql)
404
+ if (sql_res.hasErrors()) {
405
+ result.setError(500, sql_res.getFirstError(), "DoPut")
406
+ if (this._debugOnError) {
407
+ result.addDebug("OINO PUT MESSAGES [" + sql_res.messages.join('|') + "]", "DoPut")
408
+ result.addDebug("OINO PUT SQL [" + sql + "]", "DoPut")
409
+ }
410
+ }
411
+ }
412
+ } catch (e:any) {
413
+ result.setError(500, "Unhandled exception: " + e.message, "DoPut")
414
+ OINOLog.exception("@oino-ts/db", "OINODbApi", "_doPut", "exception in put request", {message:e.message, stack:e.stack})
415
+ if (this._debugOnError) {
416
+ result.addDebug("OINO POST SQL [" + sql + "]", "DoPut")
417
+ }
418
+ }
419
+ }
420
+
421
+ private async _doDelete(result:OINODbApiResult, id:string|null, rows:OINODataRow[]|null):Promise<void> {
422
+ let sql:string = ""
423
+ try {
424
+ if (rows != null) {
425
+ for (let i=0; i<rows.length; i++) {
426
+ const row_id = OINODbConfig.printOINOId(this.datamodel.getRowPrimarykeyValues(rows[i], this.hashid != null))
427
+ if (row_id) {
428
+ sql += this.datamodel.printSqlDelete(row_id)
429
+ } else if (this.params.failOnAnyInvalidRows == false) {
430
+ result.setOk() // individual rows may fail and will just be messages in response similar to executing multiple sql statements
431
+ }
432
+ }
433
+ } else if (id) {
434
+ sql = this.datamodel.printSqlDelete(id)
435
+ }
436
+ if ((sql == "") && result.success) {
437
+ result.setError(405, "No valid rows for DELETE!", "DoDelete") // only set error if there are multiple rows and no valid sql was created
438
+
439
+ } else if (result.success) {
440
+
441
+ OINOLog.debug("@oino-ts/db", "OINODbApi", "_doDelete", "Print SQL", {sql:sql})
442
+ const sql_res:OINODbDataSet = await this.db.sqlExec(sql)
443
+ if (sql_res.hasErrors()) {
444
+ result.setError(500, sql_res.getFirstError(), "DoDelete")
445
+ if (this._debugOnError) {
446
+ result.addDebug("OINO DELETE MESSAGES [" + sql_res.messages.join('|') + "]", "DoDelete")
447
+ result.addDebug("OINO DELETE SQL [" + sql + "]", "DoDelete")
448
+ }
449
+ }
450
+ }
451
+ } catch (e:any) {
452
+ result.setError(500, "Unhandled exception: " + e.message, "DoDelete")
453
+ OINOLog.exception("@oino-ts/db", "OINODbApi", "_doDelete", "exception in delete request", {message:e.message, stack:e.stack})
454
+ if (this._debugOnError) {
455
+ result.addDebug("OINO DELETE SQL [" + sql + "]", "DoDelete")
456
+ }
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Enable or disable debug output on errors.
462
+ *
463
+ * @param debugOnError true to enable debug output on errors, false to disable
464
+ */
465
+ setDebugOnError(debugOnError:boolean) {
466
+ this._debugOnError = debugOnError
467
+ }
468
+
469
+ /**
470
+ * Method for handling a HTTP REST request with GET, POST, PUT, DELETE corresponding to
471
+ * SQL select, insert, update and delete.
472
+ *
473
+ * @param method HTTP method of the REST request
474
+ * @param rowId URL id of the REST request
475
+ * @param data HTTP body data as either serialized string or unserialized JS object or OINODataRow-array or Buffer/Uint8Array binary data
476
+ * @param sqlParams SQL parameters for the REST request
477
+ *
478
+ */
479
+ async doRequest(method:string, rowId:string, data:string|OINODataRow[]|Buffer|Uint8Array|object|null, sqlParams:OINODbSqlParams):Promise<OINODbApiResult> {
480
+ return this.runRequest(new OINODbApiRequest({ method: method, rowId: rowId, data: data, sqlParams: sqlParams }))
481
+ }
482
+ /**
483
+ * Method for handling a HTTP REST request with GET, POST, PUT, DELETE corresponding to
484
+ * SQL select, insert, update and delete.
485
+ *
486
+ * @param request OINO DB API request
487
+ *
488
+ */
489
+ async runRequest(request:OINODbApiRequest):Promise<OINODbApiResult> {
490
+ OINOBenchmark.startMetric("OINODbApi", "doRequest." + request.method)
491
+ OINOLog.debug("@oino-ts/db", "OINODbApi", "doRequest", "Request", {method:request.method, id:request.rowId, data:request.data})
492
+ let result:OINODbApiResult = new OINODbApiResult(request)
493
+ let rows:OINODataRow[] = []
494
+ if ((request.method == "POST") || (request.method == "PUT")) {
495
+ rows = this._parseData(result, request)
496
+ }
497
+ if (request.method == "GET") {
498
+ await this._doGet(result, request.rowId, request)
499
+
500
+ } else if (request.method == "PUT") {
501
+ if (!request.rowId) {
502
+ result.setError(400, "HTTP PUT method requires an URL ID for the row that is updated!", "DoRequest")
503
+
504
+ } else if (rows.length != 1) {
505
+ result.setError(400, "HTTP PUT method requires exactly one row in the body data!", "DoRequest")
506
+
507
+ } else {
508
+ try {
509
+ await this._doPut(result, request.rowId, rows)
510
+
511
+ } catch (e:any) {
512
+ result.setError(500, "Unhandled exception in HTTP PUT doRequest: " + e.message, "DoRequest")
513
+ }
514
+ }
515
+ } else if (request.method == "POST") {
516
+ if (request.rowId) {
517
+ result.setError(400, "HTTP POST method must not have an URL ID as it does not target an existing row but creates a new one!", "DoRequest")
518
+
519
+ } else if (rows.length == 0) {
520
+ result.setError(400, "HTTP POST method requires at least one row in the body data!", "DoRequest")
521
+
522
+ } else {
523
+ try {
524
+ await this._doPost(result, rows)
525
+
526
+ } catch (e:any) {
527
+ result.setError(500, "Unhandled exception in HTTP POST doRequest: " + e.message, "DoRequest")
528
+ }
529
+ }
530
+ } else if (request.method == "DELETE") {
531
+ if (!request.rowId) {
532
+ result.setError(400, "HTTP DELETE method requires an id!", "DoRequest")
533
+
534
+ } else {
535
+ try {
536
+ await this._doDelete(result, request.rowId, null)
537
+
538
+ } catch (e:any) {
539
+ result.setError(500, "Unhandled exception in HTTP DELETE doRequest: " + e.message, "DoRequest")
540
+ }
541
+ }
542
+ } else {
543
+ result.setError(405, "Unsupported HTTP method '" + request.method + "' for REST request", "DoRequest")
544
+ }
545
+ OINOBenchmark.endMetric("OINODbApi", "doRequest." + request.method)
546
+ return Promise.resolve(result)
547
+ }
548
+
549
+ /**
550
+ * Method for handling a HTTP REST request with batch update using PUT or DELETE methods.
551
+ *
552
+ * @param method HTTP method of the REST request
553
+ * @param rowId URL id of the REST request
554
+ * @param data HTTP body data as either serialized string or unserialized JS object or OINODataRow-array or Buffer/Uint8Array binary data
555
+ *
556
+ */
557
+ async doBatchUpdate(method:string, rowId:string, data:string|OINODataRow[]|Buffer|Uint8Array|object|null, sqlParams?: OINODbSqlParams):Promise<OINODbApiResult> {
558
+ return this.runRequest(new OINODbApiRequest({ method: method, rowId: rowId, data: data, sqlParams: sqlParams }))
559
+ }
560
+ /**
561
+ * Method for handling a HTTP REST request with batch update using PUT or DELETE methods.
562
+ *
563
+ * @param request HTTP URL parameters as key-value-pairs
564
+ *
565
+ */
566
+ async runBatchUpdate(request:OINODbApiRequest):Promise<OINODbApiResult> {
567
+ OINOLog.debug("@oino-ts/db", "OINODbApi", "doBatchUpdate", "Request", {request:request, data:request.data})
568
+ let result:OINODbApiResult = new OINODbApiResult(request)
569
+ if ((request.method != "PUT") && (request.method != "DELETE")) {
570
+ result.setError(500, "Batch update only supports PUT and DELETE methods!", "DoBatchUpdate")
571
+ return Promise.resolve(result)
572
+ }
573
+ OINOBenchmark.startMetric("OINODbApi", "doBatchUpdate." + request.method)
574
+ const rows:OINODataRow[] = [] = this._parseData(result, request)
575
+ if (request.method == "PUT") {
576
+
577
+ try {
578
+ await this._doPut(result, null, rows)
579
+
580
+ } catch (e:any) {
581
+ result.setError(500, "Unhandled exception in HTTP PUT doRequest: " + e.message, "DoBatchUpdate")
582
+ }
583
+
584
+ } else if (request.method == "DELETE") {
585
+ try {
586
+ await this._doDelete(result, null, rows)
587
+
588
+ } catch (e:any) {
589
+ result.setError(500, "Unhandled exception in HTTP DELETE doRequest: " + e.message, "DoBatchUpdate")
590
+ }
591
+ }
592
+ OINOBenchmark.endMetric("OINODbApi", "doBatchUpdate." + request.method)
593
+ return Promise.resolve(result)
594
+ }
595
+
596
+ /**
597
+ * Method to check if a field is included in the API params.
598
+ *
599
+ * @param fieldName name of the field
600
+ *
601
+ */
602
+
603
+ public isFieldIncluded(fieldName:string):boolean {
604
+ const params = this.params
605
+ return (
606
+ ((params.excludeFieldPrefix == undefined) || (params.excludeFieldPrefix == "") || (fieldName.startsWith(params.excludeFieldPrefix) == false)) &&
607
+ ((params.excludeFields == undefined) || (params.excludeFields.length == 0) || (params.excludeFields.indexOf(fieldName) < 0)) &&
608
+ ((params.includeFields == undefined) || (params.includeFields.length == 0) || (params.includeFields.indexOf(fieldName) >= 0))
609
+ )
610
+ }
611
+
603
612
  }