@oino-ts/db 0.2.0 → 0.3.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.
@@ -32,9 +32,10 @@ class OINODb {
32
32
  * @param whereCondition - The WHERE clause to filter the results.
33
33
  * @param orderCondition - The ORDER BY clause to sort the results.
34
34
  * @param limitCondition - The LIMIT clause to limit the number of results.
35
+ * @param groupByCondition - The GROUP BY clause to group the results.
35
36
  *
36
37
  */
37
- printSqlSelect(tableName, columnNames, whereCondition, orderCondition, limitCondition) {
38
+ printSqlSelect(tableName, columnNames, whereCondition, orderCondition, limitCondition, groupByCondition) {
38
39
  let result = "SELECT " + columnNames + " FROM " + tableName;
39
40
  // OINOLog.debug("OINODb.printSqlSelect", {tableName:tableName, columnNames:columnNames, whereCondition:whereCondition, orderCondition:orderCondition, limitCondition:limitCondition })
40
41
  if (whereCondition != "") {
@@ -46,6 +47,9 @@ class OINODb {
46
47
  if (limitCondition != "") {
47
48
  result += " LIMIT " + limitCondition;
48
49
  }
50
+ if (groupByCondition != "") {
51
+ result += " GROUP BY " + groupByCondition;
52
+ }
49
53
  result += ";";
50
54
  // OINOLog.debug("OINODb.printSqlSelect", {result:result})
51
55
  return result;
@@ -358,5 +358,18 @@ class OINODbApi {
358
358
  index_js_1.OINOBenchmark.end("OINODbApi", "doRequest", method);
359
359
  return Promise.resolve(result);
360
360
  }
361
+ /**
362
+ * Method to check if a field is included in the API params.
363
+ *
364
+ * @param fieldName name of the field
365
+ *
366
+ */
367
+ isFieldIncluded(fieldName) {
368
+ // OINOLog.debug("OINODbApi.isFieldIncluded", {fieldName:fieldName, included:this.params.includeFields})
369
+ const params = this.params;
370
+ return (((params.excludeFieldPrefix == undefined) || (params.excludeFieldPrefix == "") || fieldName.startsWith(params.excludeFieldPrefix)) &&
371
+ ((params.excludeFields == undefined) || (params.excludeFields.length == 0) || (params.excludeFields.indexOf(fieldName) < 0)) &&
372
+ ((params.includeFields == undefined) || (params.includeFields.length == 0) || (params.includeFields.indexOf(fieldName) >= 0)));
373
+ }
361
374
  }
362
375
  exports.OINODbApi = OINODbApi;
@@ -14,6 +14,8 @@ class OINODbConfig {
14
14
  static OINODB_SQL_ORDER_PARAM = "oinosqlorder";
15
15
  /** Name of the OINODbSqlLimit-parameter in request */
16
16
  static OINODB_SQL_LIMIT_PARAM = "oinosqllimit";
17
+ /** Name of the OINODbSqlAggregate-parameter in request */
18
+ static OINODB_SQL_AGGREGATE_PARAM = "oinosqlaggregate";
17
19
  /**
18
20
  * Set the name of the OINO ID field
19
21
  * @param idField name of the OINO ID field
@@ -248,7 +248,7 @@ class OINONumberDataField extends OINODbDataField {
248
248
  else {
249
249
  const result = parseFloat(value);
250
250
  if (isNaN(result)) {
251
- index_js_1.OINOLog.error("OINODbSqlFilter.toSql: Invalid value!", { value: value });
251
+ index_js_1.OINOLog.error("OINONumberDataField.toSql: Invalid value!", { value: value });
252
252
  throw new Error(index_js_1.OINO_ERROR_PREFIX + ": OINONumberDataField.deserializeCell - Invalid value '" + value + "'"); // incorrectly formatted data could be a security risk, abort processing
253
253
  }
254
254
  return result;
@@ -39,13 +39,10 @@ class OINODbDataModel {
39
39
  }
40
40
  _printSqlColumnNames() {
41
41
  let result = "";
42
- for (let f of this.fields) {
43
- if (result != "") {
44
- result += ",";
45
- }
46
- result += f.printSqlColumnName();
42
+ for (let i = 0; i < this.fields.length; i++) {
43
+ result += this.fields[i].printSqlColumnName() + ",";
47
44
  }
48
- return result;
45
+ return result.substring(0, result.length - 1);
49
46
  }
50
47
  _printSqlInsertColumnsAndValues(row) {
51
48
  let columns = "";
@@ -220,10 +217,17 @@ class OINODbDataModel {
220
217
  *
221
218
  */
222
219
  printSqlSelect(id, params) {
223
- const column_names = this._printSqlColumnNames();
220
+ let column_names = "";
221
+ if (params.aggregate) {
222
+ column_names = params.aggregate.printSqlColumnNames(this);
223
+ }
224
+ else {
225
+ column_names = this._printSqlColumnNames();
226
+ }
224
227
  const order_sql = params.order?.toSql(this) || "";
225
228
  const limit_sql = params.limit?.toSql(this) || "";
226
229
  const filter_sql = params.filter?.toSql(this) || "";
230
+ const aggregate_sql = params.aggregate?.toSql(this) || "";
227
231
  let where_sql = "";
228
232
  // OINOLog.debug("OINODbDataModel.printSqlSelect", {id:id, select_sql:result, filter_sql:filter_sql, order_sql:order_sql})
229
233
  if ((id != null) && (id != "") && (filter_sql != "")) {
@@ -235,7 +239,7 @@ class OINODbDataModel {
235
239
  else if (filter_sql != "") {
236
240
  where_sql = filter_sql;
237
241
  }
238
- const result = this.api.db.printSqlSelect(this.api.params.tableName, column_names, where_sql, order_sql, limit_sql);
242
+ const result = this.api.db.printSqlSelect(this.api.params.tableName, column_names, where_sql, order_sql, limit_sql, aggregate_sql);
239
243
  // OINOLog.debug("OINODbDataModel.printSqlSelect", {result:result})
240
244
  return result;
241
245
  }
@@ -7,6 +7,7 @@
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.OINODbFactory = void 0;
9
9
  const index_js_1 = require("./index.js");
10
+ const OINODbSqlParams_js_1 = require("./OINODbSqlParams.js");
10
11
  /**
11
12
  * Static factory class for easily creating things based on data
12
13
  *
@@ -72,6 +73,10 @@ class OINODbFactory {
72
73
  if (limit) {
73
74
  sql_params.limit = index_js_1.OINODbSqlLimit.parse(limit);
74
75
  }
76
+ const aggregate = url.searchParams.get(index_js_1.OINODbConfig.OINODB_SQL_AGGREGATE_PARAM);
77
+ if (aggregate) {
78
+ sql_params.aggregate = OINODbSqlParams_js_1.OINODbSqlAggregate.parse(aggregate);
79
+ }
75
80
  let result = { sqlParams: sql_params };
76
81
  const content_type = request.headers.get("content-type");
77
82
  if (content_type == index_js_1.OINOContentType.csv) {
@@ -5,8 +5,9 @@
5
5
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.OINODbSqlLimit = exports.OINODbSqlOrder = exports.OINODbSqlFilter = exports.OINODbSqlComparison = exports.OINODbSqlBooleanOperation = void 0;
8
+ exports.OINODbSqlAggregate = exports.OINODbSqlAggregateFunctions = exports.OINODbSqlLimit = exports.OINODbSqlOrder = exports.OINODbSqlFilter = exports.OINODbSqlComparison = exports.OINODbSqlBooleanOperation = void 0;
9
9
  const index_js_1 = require("./index.js");
10
+ const OINO_FIELD_NAME_CHARS = "\\w\\s\\-\\_\\#\\¤";
10
11
  /**
11
12
  * Supported logical conjunctions in filter predicates.
12
13
  * @enum
@@ -65,13 +66,13 @@ class OINODbSqlFilter {
65
66
  this._rightSide = rightSide;
66
67
  }
67
68
  /**
68
- * Constructor for `OINOFilter` as parser of http parameter.
69
+ * Constructor for `OINODbSqlFilter` as parser of http parameter.
69
70
  *
70
71
  * @param filterString string representation of filter from HTTP-request
71
72
  *
72
73
  */
73
74
  static parse(filterString) {
74
- // OINOLog_debug("OINOFilter.constructor", {filterString:filterString})
75
+ // OINOLog_debug("OINODbSqlFilter.constructor", {filterString:filterString})
75
76
  if (!filterString) {
76
77
  return new OINODbSqlFilter("", null, "");
77
78
  }
@@ -87,7 +88,7 @@ class OINODbSqlFilter {
87
88
  }
88
89
  else {
89
90
  let boolean_parts = index_js_1.OINOStr.splitByBrackets(filterString, true, false, '(', ')');
90
- // OINOLog_debug("OINOFilter.constructor", {boolean_parts:boolean_parts})
91
+ // OINOLog_debug("OINODbSqlFilter.constructor", {boolean_parts:boolean_parts})
91
92
  if (boolean_parts.length == 3 && (boolean_parts[1].match(OINODbSqlFilter._booleanOperationRegex))) {
92
93
  return new OINODbSqlFilter(OINODbSqlFilter.parse(boolean_parts[0]), boolean_parts[1].trim().toLowerCase().substring(1), OINODbSqlFilter.parse(boolean_parts[2]));
93
94
  }
@@ -99,7 +100,7 @@ class OINODbSqlFilter {
99
100
  }
100
101
  }
101
102
  /**
102
- * Construct a new `OINOFilter` as combination of (boolean and/or) of two filters.
103
+ * Construct a new `OINODbSqlFilter` as combination of (boolean and/or) of two filters.
103
104
  *
104
105
  * @param leftSide left side to combine
105
106
  * @param operation boolean operation to use in combination
@@ -148,7 +149,7 @@ class OINODbSqlFilter {
148
149
  *
149
150
  */
150
151
  toSql(dataModel) {
151
- // OINOLog.debug("OINOFilter.toSql", {_leftSide:this._leftSide, _operator:this._operator, _rightSide:this._rightSide})
152
+ // OINOLog.debug("OINODbSqlFilter.toSql", {_leftSide:this._leftSide, _operator:this._operator, _rightSide:this._rightSide})
152
153
  if (this.isEmpty()) {
153
154
  return "";
154
155
  }
@@ -177,7 +178,7 @@ class OINODbSqlFilter {
177
178
  }
178
179
  result += field.printCellAsSqlValue(value);
179
180
  }
180
- // OINOLog.debug("OINOFilter.toSql", {result:result})
181
+ // OINOLog.debug("OINODbSqlFilter.toSql", {result:result})
181
182
  return "(" + result + ")";
182
183
  }
183
184
  }
@@ -187,7 +188,7 @@ exports.OINODbSqlFilter = OINODbSqlFilter;
187
188
  *
188
189
  */
189
190
  class OINODbSqlOrder {
190
- static _orderColumnRegex = /^\s*(\w+)\s?(ASC|DESC)?\s*?$/i;
191
+ static _orderColumnRegex = /^\s*(\w+)\s?(ASC|DESC|\+|\-)?\s*?$/i;
191
192
  _columns;
192
193
  _descending;
193
194
  /**
@@ -226,7 +227,8 @@ class OINODbSqlOrder {
226
227
  let match = OINODbSqlOrder._orderColumnRegex.exec(column_strings[i]);
227
228
  if (match != null) {
228
229
  columns.push(match[1]);
229
- directions.push((match[2] || "DESC").toUpperCase() == "DESC");
230
+ const dir = (match[2] || "ASC").toUpperCase();
231
+ directions.push((dir == "DESC") || (dir == "-"));
230
232
  }
231
233
  }
232
234
  return new OINODbSqlOrder(columns, directions);
@@ -334,3 +336,102 @@ class OINODbSqlLimit {
334
336
  }
335
337
  }
336
338
  exports.OINODbSqlLimit = OINODbSqlLimit;
339
+ /**
340
+ * Supported aggregation functions in OINODbSqlAggregate.
341
+ * @enum
342
+ */
343
+ var OINODbSqlAggregateFunctions;
344
+ (function (OINODbSqlAggregateFunctions) {
345
+ OINODbSqlAggregateFunctions["count"] = "count";
346
+ OINODbSqlAggregateFunctions["sum"] = "sum";
347
+ OINODbSqlAggregateFunctions["avg"] = "avg";
348
+ OINODbSqlAggregateFunctions["min"] = "min";
349
+ OINODbSqlAggregateFunctions["max"] = "max";
350
+ })(OINODbSqlAggregateFunctions || (exports.OINODbSqlAggregateFunctions = OINODbSqlAggregateFunctions = {}));
351
+ /**
352
+ * Class for limiting the number of results.
353
+ *
354
+ */
355
+ class OINODbSqlAggregate {
356
+ static _aggregateRegex = new RegExp("^(count|sum|avg|min|max)\\(([" + OINO_FIELD_NAME_CHARS + "]+)\\)$", "mi");
357
+ _functions;
358
+ _fields;
359
+ /**
360
+ * Constructor for `OINODbSqlAggregate`.
361
+ *
362
+ * @param function aggregate function to use
363
+ * @param fields fields to aggregate
364
+ *
365
+ */
366
+ constructor(func, fields) {
367
+ this._functions = func;
368
+ this._fields = fields;
369
+ }
370
+ /**
371
+ * Constructor for `OINODbSqlAggregate` as parser of http parameter.
372
+ *
373
+ * @param aggregatorString string representation of limit from HTTP-request
374
+ *
375
+ */
376
+ static parse(aggregatorString) {
377
+ let funtions = [];
378
+ let fields = [];
379
+ const aggregator_parts = aggregatorString.split(',');
380
+ for (let i = 0; i < aggregator_parts.length; i++) {
381
+ let match = OINODbSqlAggregate._aggregateRegex.exec(aggregator_parts[i]);
382
+ // OINOLog.debug("OINODbSqlAggregate.parse - next aggregator", {aggregator: aggregator_parts[i], match:match})
383
+ if ((match != null) && (match.length == 3)) {
384
+ funtions.push(match[1]);
385
+ fields.push(match[2]);
386
+ }
387
+ }
388
+ return new OINODbSqlAggregate(funtions, fields);
389
+ }
390
+ /**
391
+ * Does filter contain any valid conditions.
392
+ *
393
+ */
394
+ isEmpty() {
395
+ return (this._functions.length <= 0);
396
+ }
397
+ /**
398
+ * Print non-aggregated fields as SQL GROUP BY-condition based on the datamodel of the API.
399
+ *
400
+ * @param dataModel data model (and database) to use for formatting of values
401
+ *
402
+ */
403
+ toSql(dataModel) {
404
+ if (this.isEmpty()) {
405
+ return "";
406
+ }
407
+ let result = "";
408
+ for (let i = 0; i < dataModel.fields.length; i++) {
409
+ if (this._fields.includes(dataModel.fields[i].name) == false) {
410
+ result += dataModel.fields[i].printSqlColumnName() + ",";
411
+ }
412
+ }
413
+ index_js_1.OINOLog.debug("OINODbSqlAggregate.toSql", { result: result });
414
+ return result.substring(0, result.length - 1);
415
+ }
416
+ /**
417
+ * Print non-aggregated fields as SQL GROUP BY-condition based on the datamodel of the API.
418
+ *
419
+ * @param dataModel data model (and database) to use for formatting of values
420
+ *
421
+ */
422
+ printSqlColumnNames(dataModel) {
423
+ let result = "";
424
+ for (let i = 0; i < dataModel.fields.length; i++) {
425
+ const aggregate_index = this._fields.indexOf(dataModel.fields[i].name);
426
+ if (aggregate_index >= 0) {
427
+ result += this._functions[aggregate_index] + "(" + dataModel.fields[i].printSqlColumnName() + "),";
428
+ }
429
+ else {
430
+ result += dataModel.fields[i].printSqlColumnName() + ",";
431
+ }
432
+ }
433
+ index_js_1.OINOLog.debug("OINODbSqlAggregate.printSqlColumnNames", { result: result });
434
+ return result.substring(0, result.length - 1);
435
+ }
436
+ }
437
+ exports.OINODbSqlAggregate = OINODbSqlAggregate;
@@ -29,9 +29,10 @@ export class OINODb {
29
29
  * @param whereCondition - The WHERE clause to filter the results.
30
30
  * @param orderCondition - The ORDER BY clause to sort the results.
31
31
  * @param limitCondition - The LIMIT clause to limit the number of results.
32
+ * @param groupByCondition - The GROUP BY clause to group the results.
32
33
  *
33
34
  */
34
- printSqlSelect(tableName, columnNames, whereCondition, orderCondition, limitCondition) {
35
+ printSqlSelect(tableName, columnNames, whereCondition, orderCondition, limitCondition, groupByCondition) {
35
36
  let result = "SELECT " + columnNames + " FROM " + tableName;
36
37
  // OINOLog.debug("OINODb.printSqlSelect", {tableName:tableName, columnNames:columnNames, whereCondition:whereCondition, orderCondition:orderCondition, limitCondition:limitCondition })
37
38
  if (whereCondition != "") {
@@ -43,6 +44,9 @@ export class OINODb {
43
44
  if (limitCondition != "") {
44
45
  result += " LIMIT " + limitCondition;
45
46
  }
47
+ if (groupByCondition != "") {
48
+ result += " GROUP BY " + groupByCondition;
49
+ }
46
50
  result += ";";
47
51
  // OINOLog.debug("OINODb.printSqlSelect", {result:result})
48
52
  return result;
@@ -353,4 +353,17 @@ export class OINODbApi {
353
353
  OINOBenchmark.end("OINODbApi", "doRequest", method);
354
354
  return Promise.resolve(result);
355
355
  }
356
+ /**
357
+ * Method to check if a field is included in the API params.
358
+ *
359
+ * @param fieldName name of the field
360
+ *
361
+ */
362
+ isFieldIncluded(fieldName) {
363
+ // OINOLog.debug("OINODbApi.isFieldIncluded", {fieldName:fieldName, included:this.params.includeFields})
364
+ const params = this.params;
365
+ return (((params.excludeFieldPrefix == undefined) || (params.excludeFieldPrefix == "") || fieldName.startsWith(params.excludeFieldPrefix)) &&
366
+ ((params.excludeFields == undefined) || (params.excludeFields.length == 0) || (params.excludeFields.indexOf(fieldName) < 0)) &&
367
+ ((params.includeFields == undefined) || (params.includeFields.length == 0) || (params.includeFields.indexOf(fieldName) >= 0)));
368
+ }
356
369
  }
@@ -11,6 +11,8 @@ export class OINODbConfig {
11
11
  static OINODB_SQL_ORDER_PARAM = "oinosqlorder";
12
12
  /** Name of the OINODbSqlLimit-parameter in request */
13
13
  static OINODB_SQL_LIMIT_PARAM = "oinosqllimit";
14
+ /** Name of the OINODbSqlAggregate-parameter in request */
15
+ static OINODB_SQL_AGGREGATE_PARAM = "oinosqlaggregate";
14
16
  /**
15
17
  * Set the name of the OINO ID field
16
18
  * @param idField name of the OINO ID field
@@ -242,7 +242,7 @@ export class OINONumberDataField extends OINODbDataField {
242
242
  else {
243
243
  const result = parseFloat(value);
244
244
  if (isNaN(result)) {
245
- OINOLog.error("OINODbSqlFilter.toSql: Invalid value!", { value: value });
245
+ OINOLog.error("OINONumberDataField.toSql: Invalid value!", { value: value });
246
246
  throw new Error(OINO_ERROR_PREFIX + ": OINONumberDataField.deserializeCell - Invalid value '" + value + "'"); // incorrectly formatted data could be a security risk, abort processing
247
247
  }
248
248
  return result;
@@ -36,13 +36,10 @@ export class OINODbDataModel {
36
36
  }
37
37
  _printSqlColumnNames() {
38
38
  let result = "";
39
- for (let f of this.fields) {
40
- if (result != "") {
41
- result += ",";
42
- }
43
- result += f.printSqlColumnName();
39
+ for (let i = 0; i < this.fields.length; i++) {
40
+ result += this.fields[i].printSqlColumnName() + ",";
44
41
  }
45
- return result;
42
+ return result.substring(0, result.length - 1);
46
43
  }
47
44
  _printSqlInsertColumnsAndValues(row) {
48
45
  let columns = "";
@@ -217,10 +214,17 @@ export class OINODbDataModel {
217
214
  *
218
215
  */
219
216
  printSqlSelect(id, params) {
220
- const column_names = this._printSqlColumnNames();
217
+ let column_names = "";
218
+ if (params.aggregate) {
219
+ column_names = params.aggregate.printSqlColumnNames(this);
220
+ }
221
+ else {
222
+ column_names = this._printSqlColumnNames();
223
+ }
221
224
  const order_sql = params.order?.toSql(this) || "";
222
225
  const limit_sql = params.limit?.toSql(this) || "";
223
226
  const filter_sql = params.filter?.toSql(this) || "";
227
+ const aggregate_sql = params.aggregate?.toSql(this) || "";
224
228
  let where_sql = "";
225
229
  // OINOLog.debug("OINODbDataModel.printSqlSelect", {id:id, select_sql:result, filter_sql:filter_sql, order_sql:order_sql})
226
230
  if ((id != null) && (id != "") && (filter_sql != "")) {
@@ -232,7 +236,7 @@ export class OINODbDataModel {
232
236
  else if (filter_sql != "") {
233
237
  where_sql = filter_sql;
234
238
  }
235
- const result = this.api.db.printSqlSelect(this.api.params.tableName, column_names, where_sql, order_sql, limit_sql);
239
+ const result = this.api.db.printSqlSelect(this.api.params.tableName, column_names, where_sql, order_sql, limit_sql, aggregate_sql);
236
240
  // OINOLog.debug("OINODbDataModel.printSqlSelect", {result:result})
237
241
  return result;
238
242
  }
@@ -4,6 +4,7 @@
4
4
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
5
  */
6
6
  import { OINODbApi, OINOContentType, OINODbSqlFilter, OINODbConfig, OINODbSqlOrder, OINODbSqlLimit } from "./index.js";
7
+ import { OINODbSqlAggregate } from "./OINODbSqlParams.js";
7
8
  /**
8
9
  * Static factory class for easily creating things based on data
9
10
  *
@@ -69,6 +70,10 @@ export class OINODbFactory {
69
70
  if (limit) {
70
71
  sql_params.limit = OINODbSqlLimit.parse(limit);
71
72
  }
73
+ const aggregate = url.searchParams.get(OINODbConfig.OINODB_SQL_AGGREGATE_PARAM);
74
+ if (aggregate) {
75
+ sql_params.aggregate = OINODbSqlAggregate.parse(aggregate);
76
+ }
72
77
  let result = { sqlParams: sql_params };
73
78
  const content_type = request.headers.get("content-type");
74
79
  if (content_type == OINOContentType.csv) {
@@ -4,6 +4,7 @@
4
4
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
5
  */
6
6
  import { OINOStr, OINO_ERROR_PREFIX, OINOLog } from "./index.js";
7
+ const OINO_FIELD_NAME_CHARS = "\\w\\s\\-\\_\\#\\¤";
7
8
  /**
8
9
  * Supported logical conjunctions in filter predicates.
9
10
  * @enum
@@ -62,13 +63,13 @@ export class OINODbSqlFilter {
62
63
  this._rightSide = rightSide;
63
64
  }
64
65
  /**
65
- * Constructor for `OINOFilter` as parser of http parameter.
66
+ * Constructor for `OINODbSqlFilter` as parser of http parameter.
66
67
  *
67
68
  * @param filterString string representation of filter from HTTP-request
68
69
  *
69
70
  */
70
71
  static parse(filterString) {
71
- // OINOLog_debug("OINOFilter.constructor", {filterString:filterString})
72
+ // OINOLog_debug("OINODbSqlFilter.constructor", {filterString:filterString})
72
73
  if (!filterString) {
73
74
  return new OINODbSqlFilter("", null, "");
74
75
  }
@@ -84,7 +85,7 @@ export class OINODbSqlFilter {
84
85
  }
85
86
  else {
86
87
  let boolean_parts = OINOStr.splitByBrackets(filterString, true, false, '(', ')');
87
- // OINOLog_debug("OINOFilter.constructor", {boolean_parts:boolean_parts})
88
+ // OINOLog_debug("OINODbSqlFilter.constructor", {boolean_parts:boolean_parts})
88
89
  if (boolean_parts.length == 3 && (boolean_parts[1].match(OINODbSqlFilter._booleanOperationRegex))) {
89
90
  return new OINODbSqlFilter(OINODbSqlFilter.parse(boolean_parts[0]), boolean_parts[1].trim().toLowerCase().substring(1), OINODbSqlFilter.parse(boolean_parts[2]));
90
91
  }
@@ -96,7 +97,7 @@ export class OINODbSqlFilter {
96
97
  }
97
98
  }
98
99
  /**
99
- * Construct a new `OINOFilter` as combination of (boolean and/or) of two filters.
100
+ * Construct a new `OINODbSqlFilter` as combination of (boolean and/or) of two filters.
100
101
  *
101
102
  * @param leftSide left side to combine
102
103
  * @param operation boolean operation to use in combination
@@ -145,7 +146,7 @@ export class OINODbSqlFilter {
145
146
  *
146
147
  */
147
148
  toSql(dataModel) {
148
- // OINOLog.debug("OINOFilter.toSql", {_leftSide:this._leftSide, _operator:this._operator, _rightSide:this._rightSide})
149
+ // OINOLog.debug("OINODbSqlFilter.toSql", {_leftSide:this._leftSide, _operator:this._operator, _rightSide:this._rightSide})
149
150
  if (this.isEmpty()) {
150
151
  return "";
151
152
  }
@@ -174,7 +175,7 @@ export class OINODbSqlFilter {
174
175
  }
175
176
  result += field.printCellAsSqlValue(value);
176
177
  }
177
- // OINOLog.debug("OINOFilter.toSql", {result:result})
178
+ // OINOLog.debug("OINODbSqlFilter.toSql", {result:result})
178
179
  return "(" + result + ")";
179
180
  }
180
181
  }
@@ -183,7 +184,7 @@ export class OINODbSqlFilter {
183
184
  *
184
185
  */
185
186
  export class OINODbSqlOrder {
186
- static _orderColumnRegex = /^\s*(\w+)\s?(ASC|DESC)?\s*?$/i;
187
+ static _orderColumnRegex = /^\s*(\w+)\s?(ASC|DESC|\+|\-)?\s*?$/i;
187
188
  _columns;
188
189
  _descending;
189
190
  /**
@@ -222,7 +223,8 @@ export class OINODbSqlOrder {
222
223
  let match = OINODbSqlOrder._orderColumnRegex.exec(column_strings[i]);
223
224
  if (match != null) {
224
225
  columns.push(match[1]);
225
- directions.push((match[2] || "DESC").toUpperCase() == "DESC");
226
+ const dir = (match[2] || "ASC").toUpperCase();
227
+ directions.push((dir == "DESC") || (dir == "-"));
226
228
  }
227
229
  }
228
230
  return new OINODbSqlOrder(columns, directions);
@@ -328,3 +330,101 @@ export class OINODbSqlLimit {
328
330
  return result;
329
331
  }
330
332
  }
333
+ /**
334
+ * Supported aggregation functions in OINODbSqlAggregate.
335
+ * @enum
336
+ */
337
+ export var OINODbSqlAggregateFunctions;
338
+ (function (OINODbSqlAggregateFunctions) {
339
+ OINODbSqlAggregateFunctions["count"] = "count";
340
+ OINODbSqlAggregateFunctions["sum"] = "sum";
341
+ OINODbSqlAggregateFunctions["avg"] = "avg";
342
+ OINODbSqlAggregateFunctions["min"] = "min";
343
+ OINODbSqlAggregateFunctions["max"] = "max";
344
+ })(OINODbSqlAggregateFunctions || (OINODbSqlAggregateFunctions = {}));
345
+ /**
346
+ * Class for limiting the number of results.
347
+ *
348
+ */
349
+ export class OINODbSqlAggregate {
350
+ static _aggregateRegex = new RegExp("^(count|sum|avg|min|max)\\(([" + OINO_FIELD_NAME_CHARS + "]+)\\)$", "mi");
351
+ _functions;
352
+ _fields;
353
+ /**
354
+ * Constructor for `OINODbSqlAggregate`.
355
+ *
356
+ * @param function aggregate function to use
357
+ * @param fields fields to aggregate
358
+ *
359
+ */
360
+ constructor(func, fields) {
361
+ this._functions = func;
362
+ this._fields = fields;
363
+ }
364
+ /**
365
+ * Constructor for `OINODbSqlAggregate` as parser of http parameter.
366
+ *
367
+ * @param aggregatorString string representation of limit from HTTP-request
368
+ *
369
+ */
370
+ static parse(aggregatorString) {
371
+ let funtions = [];
372
+ let fields = [];
373
+ const aggregator_parts = aggregatorString.split(',');
374
+ for (let i = 0; i < aggregator_parts.length; i++) {
375
+ let match = OINODbSqlAggregate._aggregateRegex.exec(aggregator_parts[i]);
376
+ // OINOLog.debug("OINODbSqlAggregate.parse - next aggregator", {aggregator: aggregator_parts[i], match:match})
377
+ if ((match != null) && (match.length == 3)) {
378
+ funtions.push(match[1]);
379
+ fields.push(match[2]);
380
+ }
381
+ }
382
+ return new OINODbSqlAggregate(funtions, fields);
383
+ }
384
+ /**
385
+ * Does filter contain any valid conditions.
386
+ *
387
+ */
388
+ isEmpty() {
389
+ return (this._functions.length <= 0);
390
+ }
391
+ /**
392
+ * Print non-aggregated fields as SQL GROUP BY-condition based on the datamodel of the API.
393
+ *
394
+ * @param dataModel data model (and database) to use for formatting of values
395
+ *
396
+ */
397
+ toSql(dataModel) {
398
+ if (this.isEmpty()) {
399
+ return "";
400
+ }
401
+ let result = "";
402
+ for (let i = 0; i < dataModel.fields.length; i++) {
403
+ if (this._fields.includes(dataModel.fields[i].name) == false) {
404
+ result += dataModel.fields[i].printSqlColumnName() + ",";
405
+ }
406
+ }
407
+ OINOLog.debug("OINODbSqlAggregate.toSql", { result: result });
408
+ return result.substring(0, result.length - 1);
409
+ }
410
+ /**
411
+ * Print non-aggregated fields as SQL GROUP BY-condition based on the datamodel of the API.
412
+ *
413
+ * @param dataModel data model (and database) to use for formatting of values
414
+ *
415
+ */
416
+ printSqlColumnNames(dataModel) {
417
+ let result = "";
418
+ for (let i = 0; i < dataModel.fields.length; i++) {
419
+ const aggregate_index = this._fields.indexOf(dataModel.fields[i].name);
420
+ if (aggregate_index >= 0) {
421
+ result += this._functions[aggregate_index] + "(" + dataModel.fields[i].printSqlColumnName() + "),";
422
+ }
423
+ else {
424
+ result += dataModel.fields[i].printSqlColumnName() + ",";
425
+ }
426
+ }
427
+ OINOLog.debug("OINODbSqlAggregate.printSqlColumnNames", { result: result });
428
+ return result.substring(0, result.length - 1);
429
+ }
430
+ }
@@ -80,9 +80,10 @@ export declare abstract class OINODb {
80
80
  * @param whereCondition - The WHERE clause to filter the results.
81
81
  * @param orderCondition - The ORDER BY clause to sort the results.
82
82
  * @param limitCondition - The LIMIT clause to limit the number of results.
83
+ * @param groupByCondition - The GROUP BY clause to group the results.
83
84
  *
84
85
  */
85
- printSqlSelect(tableName: string, columnNames: string, whereCondition: string, orderCondition: string, limitCondition: string): string;
86
+ printSqlSelect(tableName: string, columnNames: string, whereCondition: string, orderCondition: string, limitCondition: string, groupByCondition: string): string;
86
87
  }
87
88
  /**
88
89
  * Base class for SQL results that can be asynchronously iterated (but
@@ -79,4 +79,11 @@ export declare class OINODbApi {
79
79
  *
80
80
  */
81
81
  doRequest(method: string, id: string, body: string | OINODataRow[] | Buffer | any, params?: OINODbApiRequestParams): Promise<OINODbApiResult>;
82
+ /**
83
+ * Method to check if a field is included in the API params.
84
+ *
85
+ * @param fieldName name of the field
86
+ *
87
+ */
88
+ isFieldIncluded(fieldName: string): boolean;
82
89
  }
@@ -11,6 +11,8 @@ export declare class OINODbConfig {
11
11
  static OINODB_SQL_ORDER_PARAM: string;
12
12
  /** Name of the OINODbSqlLimit-parameter in request */
13
13
  static OINODB_SQL_LIMIT_PARAM: string;
14
+ /** Name of the OINODbSqlAggregate-parameter in request */
15
+ static OINODB_SQL_AGGREGATE_PARAM: string;
14
16
  /**
15
17
  * Set the name of the OINO ID field
16
18
  * @param idField name of the OINO ID field
@@ -44,14 +44,14 @@ export declare class OINODbSqlFilter {
44
44
  */
45
45
  constructor(leftSide: OINODbSqlFilter | string, operation: OINODbSqlComparison | OINODbSqlBooleanOperation | null, rightSide: OINODbSqlFilter | string);
46
46
  /**
47
- * Constructor for `OINOFilter` as parser of http parameter.
47
+ * Constructor for `OINODbSqlFilter` as parser of http parameter.
48
48
  *
49
49
  * @param filterString string representation of filter from HTTP-request
50
50
  *
51
51
  */
52
52
  static parse(filterString: string): OINODbSqlFilter;
53
53
  /**
54
- * Construct a new `OINOFilter` as combination of (boolean and/or) of two filters.
54
+ * Construct a new `OINODbSqlFilter` as combination of (boolean and/or) of two filters.
55
55
  *
56
56
  * @param leftSide left side to combine
57
57
  * @param operation boolean operation to use in combination
@@ -145,3 +145,57 @@ export declare class OINODbSqlLimit {
145
145
  */
146
146
  toSql(dataModel: OINODbDataModel): string;
147
147
  }
148
+ /**
149
+ * Supported aggregation functions in OINODbSqlAggregate.
150
+ * @enum
151
+ */
152
+ export declare enum OINODbSqlAggregateFunctions {
153
+ count = "count",
154
+ sum = "sum",
155
+ avg = "avg",
156
+ min = "min",
157
+ max = "max"
158
+ }
159
+ /**
160
+ * Class for limiting the number of results.
161
+ *
162
+ */
163
+ export declare class OINODbSqlAggregate {
164
+ private static _aggregateRegex;
165
+ private _functions;
166
+ private _fields;
167
+ /**
168
+ * Constructor for `OINODbSqlAggregate`.
169
+ *
170
+ * @param function aggregate function to use
171
+ * @param fields fields to aggregate
172
+ *
173
+ */
174
+ constructor(func: OINODbSqlAggregateFunctions[], fields: string[]);
175
+ /**
176
+ * Constructor for `OINODbSqlAggregate` as parser of http parameter.
177
+ *
178
+ * @param aggregatorString string representation of limit from HTTP-request
179
+ *
180
+ */
181
+ static parse(aggregatorString: string): OINODbSqlAggregate;
182
+ /**
183
+ * Does filter contain any valid conditions.
184
+ *
185
+ */
186
+ isEmpty(): boolean;
187
+ /**
188
+ * Print non-aggregated fields as SQL GROUP BY-condition based on the datamodel of the API.
189
+ *
190
+ * @param dataModel data model (and database) to use for formatting of values
191
+ *
192
+ */
193
+ toSql(dataModel: OINODbDataModel): string;
194
+ /**
195
+ * Print non-aggregated fields as SQL GROUP BY-condition based on the datamodel of the API.
196
+ *
197
+ * @param dataModel data model (and database) to use for formatting of values
198
+ *
199
+ */
200
+ printSqlColumnNames(dataModel: OINODbDataModel): string;
201
+ }
@@ -3,7 +3,7 @@ export { OINOContentType };
3
3
  export { OINO_ERROR_PREFIX, OINO_WARNING_PREFIX, OINO_INFO_PREFIX, OINO_DEBUG_PREFIX, OINOStr, OINOBenchmark, OINOLog, OINOLogLevel, OINOConsoleLog, OINOResult, OINOHttpResult, OINOHtmlTemplate } from "@oino-ts/common";
4
4
  import { OINODb } from "./OINODb.js";
5
5
  import { OINODbDataField } from "./OINODbDataField.js";
6
- import { OINODbSqlFilter, OINODbSqlLimit, OINODbSqlOrder } from "./OINODbSqlParams.js";
6
+ import { OINODbSqlAggregate, OINODbSqlFilter, OINODbSqlLimit, OINODbSqlOrder } from "./OINODbSqlParams.js";
7
7
  export { OINODbApiResult, OINODbHtmlTemplate, OINODbApi } from "./OINODbApi.js";
8
8
  export { OINODbDataModel } from "./OINODbDataModel.js";
9
9
  export { OINODbModelSet } from "./OINODbModelSet.js";
@@ -16,6 +16,8 @@ export { OINODbSwagger } from "./OINODbSwagger.js";
16
16
  export { OINODbParser } from "./OINODbParser.js";
17
17
  /** API parameters */
18
18
  export type OINODbApiParams = {
19
+ /** Name of the api */
20
+ apiName: string;
19
21
  /** Name of the database table */
20
22
  tableName: string;
21
23
  /** Reject values that exceed field max length (behaviour on such is platform dependent) */
@@ -26,9 +28,11 @@ export type OINODbApiParams = {
26
28
  failOnInsertWithoutKey?: boolean;
27
29
  /** Treat date type fields as just strings and use the native formatting instead of the ISO 8601 format */
28
30
  useDatesAsString?: Boolean;
31
+ /** Include given fields from the API and exclude rest (if defined) */
32
+ includeFields?: string[];
29
33
  /** Exclude all fields with this prefix from the API */
30
34
  excludeFieldPrefix?: string;
31
- /** Exclude given fields from the API */
35
+ /** Exclude given fields from the API and include rest (if defined) */
32
36
  excludeFields?: string[];
33
37
  /** Enable hashids for numeric primarykeys by adding a 32 char key */
34
38
  hashidKey?: string;
@@ -83,6 +87,8 @@ export type OINODbSqlParams = {
83
87
  order?: OINODbSqlOrder;
84
88
  /** SQL result limit condition */
85
89
  limit?: OINODbSqlLimit;
90
+ /** SQL aggregation functions */
91
+ aggregate?: OINODbSqlAggregate;
86
92
  };
87
93
  /** Request options */
88
94
  export type OINODbApiRequestParams = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oino-ts/db",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "OINO TS library package for publishing an SQL database tables as a REST API.",
5
5
  "author": "Matias Kiviniemi (pragmatta)",
6
6
  "license": "MPL-2.0",
@@ -19,12 +19,12 @@
19
19
  "module": "./dist/esm/index.js",
20
20
  "types": "./dist/types/index.d.ts",
21
21
  "dependencies": {
22
- "@oino-ts/common": "0.2.0"
22
+ "@oino-ts/common": "0.3.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^20.14.10",
26
26
  "@types/bun": "^1.1.14",
27
- "@oino-ts/types": "0.2.0",
27
+ "@oino-ts/types": "0.3.0",
28
28
  "typedoc": "^0.25.13"
29
29
  },
30
30
  "files": [
package/src/OINODb.ts CHANGED
@@ -102,9 +102,10 @@ export abstract class OINODb {
102
102
  * @param whereCondition - The WHERE clause to filter the results.
103
103
  * @param orderCondition - The ORDER BY clause to sort the results.
104
104
  * @param limitCondition - The LIMIT clause to limit the number of results.
105
+ * @param groupByCondition - The GROUP BY clause to group the results.
105
106
  *
106
107
  */
107
- printSqlSelect(tableName:string, columnNames:string, whereCondition:string, orderCondition:string, limitCondition:string): string {
108
+ printSqlSelect(tableName:string, columnNames:string, whereCondition:string, orderCondition:string, limitCondition:string, groupByCondition: string): string {
108
109
  let result:string = "SELECT " + columnNames + " FROM " + tableName;
109
110
  // OINOLog.debug("OINODb.printSqlSelect", {tableName:tableName, columnNames:columnNames, whereCondition:whereCondition, orderCondition:orderCondition, limitCondition:limitCondition })
110
111
  if (whereCondition != "") {
@@ -116,6 +117,9 @@ export abstract class OINODb {
116
117
  if (limitCondition != "") {
117
118
  result += " LIMIT " + limitCondition
118
119
  }
120
+ if (groupByCondition != "") {
121
+ result += " GROUP BY " + groupByCondition
122
+ }
119
123
  result += ";"
120
124
  // OINOLog.debug("OINODb.printSqlSelect", {result:result})
121
125
  return result;
@@ -12,6 +12,7 @@ import { OINODbBunSqlite } from "@oino-ts/db-bunsqlite"
12
12
  import { OINODbPostgresql } from "@oino-ts/db-postgresql"
13
13
  import { OINODbMariadb } from "@oino-ts/db-mariadb"
14
14
  import { OINODbMsSql } from "@oino-ts/db-mssql"
15
+ import { OINODbSqlAggregate } from "./OINODbSqlParams.js";
15
16
 
16
17
  const OINODB_POSTGRESQL_TOKEN = process.env.OINODB_POSTGRESQL_TOKEN || console.error("OINODB_POSTGRESQL_TOKEN not set") || ""
17
18
  const OINODB_MARIADB_TOKEN = process.env.OINODB_MARIADB_TOKEN || console.error("OINODB_MARIADB_TOKEN not set") || ""
@@ -36,8 +37,8 @@ const dbs:OINODbParams[] = [
36
37
 
37
38
  const api_tests:OINOTestParams[] = [
38
39
  {
39
- name: "API",
40
- apiParams: { tableName: "Orders" },
40
+ name: "API 1",
41
+ apiParams: { apiName: "Orders", tableName: "Orders" },
41
42
  requestParams: {
42
43
  sqlParams: { filter: OINODbSqlFilter.parse("(ShipPostalCode)-like(0502%)"), order: OINODbSqlOrder.parse("ShipPostalCode-,Freight+"), limit: OINODbSqlLimit.parse("5 page 2") }
43
44
  },
@@ -45,8 +46,8 @@ const api_tests:OINOTestParams[] = [
45
46
  putRow: [30000,"CACTU",1,new Date("2023-04-05"),new Date("2023-04-06"),new Date("2023-04-07"),2,"847.51","k'l\"m%n_o\tp\rq\nr\\s","59 rue de l'Abbaye","Cowes2","Western Europe","PO31 8PJ","UK"]
46
47
  },
47
48
  {
48
- name: "API",
49
- apiParams: { tableName: "Products", failOnOversizedValues: true },
49
+ name: "API 2",
50
+ apiParams: { apiName: "Products", tableName: "Products", failOnOversizedValues: true },
50
51
  requestParams: {
51
52
  sqlParams: { filter: OINODbSqlFilter.parse("(UnitsInStock)-le(5)"), order: OINODbSqlOrder.parse("UnitsInStock,UnitPrice"), limit: OINODbSqlLimit.parse("7") }
52
53
  },
@@ -54,8 +55,8 @@ const api_tests:OINOTestParams[] = [
54
55
  putRow: [99, "Umeshu", 1, 1, undefined, 24.99, 3, 0, 20, 0]
55
56
  },
56
57
  {
57
- name: "API",
58
- apiParams: { tableName: "Employees", hashidKey: "12345678901234567890123456789012", hashidStaticIds:true },
58
+ name: "API 3",
59
+ apiParams: { apiName: "Employees", tableName: "Employees", hashidKey: "12345678901234567890123456789012", hashidStaticIds:true },
59
60
  requestParams: {
60
61
  sqlParams: { filter: OINODbSqlFilter.parse("(TitleOfCourtesy)-eq(Ms.)"), order: OINODbSqlOrder.parse("LastName asc"), limit: OINODbSqlLimit.parse("5") }
61
62
  },
@@ -63,20 +64,21 @@ const api_tests:OINOTestParams[] = [
63
64
  putRow: [99, "LastName2", "FirstName2", null, "TitleOfCourtesy2", new Date("2023-04-06"), new Date("2023-04-07"), "Address2", "City2", "Region2", 54321, "EU2", "234 567 8901", "8765", Buffer.from("0506070809", "hex"), "Line3\nLine4", 1, "http://accweb/emmployees/lastnamefirstname.bmp"],
64
65
  },
65
66
  {
66
- name: "API",
67
- apiParams: { tableName: "OrderDetails" },
67
+ name: "API 4",
68
+ apiParams: { apiName: "OrderDetails", tableName: "OrderDetails" },
68
69
  requestParams: {
69
- sqlParams: { filter: OINODbSqlFilter.parse("(Quantity)-gt(100)"), order: OINODbSqlOrder.parse("Quantity desc,UnitPrice asc"), limit: OINODbSqlLimit.parse("5 page 2") }
70
+ sqlParams: { aggregate: OINODbSqlAggregate.parse("count(ProductID),avg(UnitPrice),sum(Quantity),max(Discount)") }
70
71
  },
71
72
  postRow: [10249,77,12.34,56,0],
72
73
  putRow: [10249,77,23.45,67,0]
73
74
  }
75
+
74
76
  ]
75
77
 
76
78
  const owasp_tests:OINOTestParams[] = [
77
79
  {
78
80
  name: "OWASP 1",
79
- apiParams: { tableName: "Products", failOnOversizedValues: true },
81
+ apiParams: { apiName: "Products", tableName: "Products", failOnOversizedValues: true },
80
82
  requestParams: {
81
83
  sqlParams: { filter: OINODbSqlFilter.parse("(1)-eq(1)") }
82
84
  },
@@ -85,7 +87,7 @@ const owasp_tests:OINOTestParams[] = [
85
87
  },
86
88
  {
87
89
  name: "OWASP 2",
88
- apiParams: { tableName: "Products", failOnOversizedValues: true },
90
+ apiParams: { apiName: "Products", tableName: "Products", failOnOversizedValues: true },
89
91
  requestParams: {
90
92
  sqlParams: { order: OINODbSqlOrder.parse("1 asc") }
91
93
  },
@@ -94,7 +96,7 @@ const owasp_tests:OINOTestParams[] = [
94
96
  },
95
97
  {
96
98
  name: "OWASP 3",
97
- apiParams: { tableName: "Products", failOnOversizedValues: true },
99
+ apiParams: { apiName: "Products", tableName: "Products", failOnOversizedValues: true },
98
100
  requestParams: {
99
101
  sqlParams: { filter: OINODbSqlFilter.parse("(ProductID)-eq(FOO)") }
100
102
  },
package/src/OINODbApi.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { OINODbApiParams, OINODb, OINODbDataSet, OINODbDataModel, OINODbDataField, OINOStringDataField, OINO_ERROR_PREFIX, OINODataRow, OINODataCell, OINODbModelSet, OINOBenchmark, OINODbApiRequestParams, OINODbConfig, OINOHttpResult, OINOHtmlTemplate, OINONumberDataField, OINODbParser } from "./index.js"
8
- import { OINOResult } from "@oino-ts/common";
8
+ import { OINOLog, OINOResult } from "@oino-ts/common";
9
9
  import { OINOHashid } from "@oino-ts/hashid"
10
10
 
11
11
  const API_EMPTY_PARAMS:OINODbApiRequestParams = { sqlParams: {} }
@@ -364,4 +364,22 @@ export class OINODbApi {
364
364
  OINOBenchmark.end("OINODbApi", "doRequest", method)
365
365
  return Promise.resolve(result)
366
366
  }
367
+
368
+ /**
369
+ * Method to check if a field is included in the API params.
370
+ *
371
+ * @param fieldName name of the field
372
+ *
373
+ */
374
+
375
+ public isFieldIncluded(fieldName:string):boolean {
376
+ // OINOLog.debug("OINODbApi.isFieldIncluded", {fieldName:fieldName, included:this.params.includeFields})
377
+ const params = this.params
378
+ return (
379
+ ((params.excludeFieldPrefix == undefined) || (params.excludeFieldPrefix == "") || fieldName.startsWith(params.excludeFieldPrefix)) &&
380
+ ((params.excludeFields == undefined) || (params.excludeFields.length == 0) || (params.excludeFields.indexOf(fieldName) < 0)) &&
381
+ ((params.includeFields == undefined) || (params.includeFields.length == 0) || (params.includeFields.indexOf(fieldName) >= 0))
382
+ )
383
+ }
384
+
367
385
  }
@@ -16,6 +16,9 @@ export class OINODbConfig {
16
16
  /** Name of the OINODbSqlLimit-parameter in request */
17
17
  static OINODB_SQL_LIMIT_PARAM:string = "oinosqllimit"
18
18
 
19
+ /** Name of the OINODbSqlAggregate-parameter in request */
20
+ static OINODB_SQL_AGGREGATE_PARAM:string = "oinosqlaggregate"
21
+
19
22
  /**
20
23
  * Set the name of the OINO ID field
21
24
  * @param idField name of the OINO ID field
@@ -258,7 +258,7 @@ export class OINONumberDataField extends OINODbDataField {
258
258
  } else {
259
259
  const result:number = parseFloat(value)
260
260
  if (isNaN(result)) {
261
- OINOLog.error("OINODbSqlFilter.toSql: Invalid value!", {value:value})
261
+ OINOLog.error("OINONumberDataField.toSql: Invalid value!", {value:value})
262
262
  throw new Error(OINO_ERROR_PREFIX + ": OINONumberDataField.deserializeCell - Invalid value '" + value + "'") // incorrectly formatted data could be a security risk, abort processing
263
263
  }
264
264
  return result
@@ -43,13 +43,10 @@ export class OINODbDataModel {
43
43
 
44
44
  private _printSqlColumnNames(): string {
45
45
  let result: string = "";
46
- for (let f of this.fields) {
47
- if (result != "") {
48
- result += ",";
49
- }
50
- result += f.printSqlColumnName();
46
+ for (let i=0; i < this.fields.length; i++) {
47
+ result += this.fields[i].printSqlColumnName()+","
51
48
  }
52
- return result;
49
+ return result.substring(0, result.length-1)
53
50
  }
54
51
 
55
52
  private _printSqlInsertColumnsAndValues(row: OINODataRow): string {
@@ -231,10 +228,17 @@ export class OINODbDataModel {
231
228
  *
232
229
  */
233
230
  printSqlSelect(id: string, params:OINODbSqlParams): string {
234
- const column_names = this._printSqlColumnNames()
231
+ let column_names = ""
232
+ if (params.aggregate) {
233
+ column_names = params.aggregate.printSqlColumnNames(this)
234
+ } else {
235
+ column_names = this._printSqlColumnNames()
236
+ }
235
237
  const order_sql = params.order?.toSql(this) || ""
236
238
  const limit_sql = params.limit?.toSql(this) || ""
237
239
  const filter_sql = params.filter?.toSql(this) || ""
240
+ const aggregate_sql = params.aggregate?.toSql(this) || ""
241
+
238
242
  let where_sql = ""
239
243
  // OINOLog.debug("OINODbDataModel.printSqlSelect", {id:id, select_sql:result, filter_sql:filter_sql, order_sql:order_sql})
240
244
  if ((id != null) && (id != "") && (filter_sql != "")) {
@@ -244,7 +248,7 @@ export class OINODbDataModel {
244
248
  } else if (filter_sql != "") {
245
249
  where_sql = filter_sql
246
250
  }
247
- const result = this.api.db.printSqlSelect(this.api.params.tableName, column_names, where_sql, order_sql, limit_sql)
251
+ const result = this.api.db.printSqlSelect(this.api.params.tableName, column_names, where_sql, order_sql, limit_sql, aggregate_sql)
248
252
  // OINOLog.debug("OINODbDataModel.printSqlSelect", {result:result})
249
253
  return result;
250
254
  }
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { OINODbApi, OINODbApiParams, OINODbParams, OINOContentType, OINODb, OINODbConstructor, OINODbApiRequestParams, OINODbSqlFilter, OINODbConfig, OINODbSqlOrder, OINODbSqlLimit, OINODbSqlParams } from "./index.js"
8
+ import { OINODbSqlAggregate } from "./OINODbSqlParams.js"
8
9
 
9
10
  /**
10
11
  * Static factory class for easily creating things based on data
@@ -75,6 +76,10 @@ export class OINODbFactory {
75
76
  if (limit) {
76
77
  sql_params.limit = OINODbSqlLimit.parse(limit)
77
78
  }
79
+ const aggregate = url.searchParams.get(OINODbConfig.OINODB_SQL_AGGREGATE_PARAM)
80
+ if (aggregate) {
81
+ sql_params.aggregate = OINODbSqlAggregate.parse(aggregate)
82
+ }
78
83
 
79
84
  let result:OINODbApiRequestParams = { sqlParams: sql_params }
80
85
 
@@ -6,6 +6,8 @@
6
6
 
7
7
  import { OINOStr, OINODbDataField, OINODbDataModel, OINO_ERROR_PREFIX, OINOLog } from "./index.js"
8
8
 
9
+ const OINO_FIELD_NAME_CHARS:string = "\\w\\s\\-\\_\\#\\¤"
10
+
9
11
  /**
10
12
  * Supported logical conjunctions in filter predicates.
11
13
  * @enum
@@ -58,13 +60,13 @@ export class OINODbSqlFilter {
58
60
  }
59
61
 
60
62
  /**
61
- * Constructor for `OINOFilter` as parser of http parameter.
63
+ * Constructor for `OINODbSqlFilter` as parser of http parameter.
62
64
  *
63
65
  * @param filterString string representation of filter from HTTP-request
64
66
  *
65
67
  */
66
68
  static parse(filterString: string):OINODbSqlFilter {
67
- // OINOLog_debug("OINOFilter.constructor", {filterString:filterString})
69
+ // OINOLog_debug("OINODbSqlFilter.constructor", {filterString:filterString})
68
70
  if (!filterString) {
69
71
  return new OINODbSqlFilter("", null, "")
70
72
 
@@ -78,7 +80,7 @@ export class OINODbSqlFilter {
78
80
  return new OINODbSqlFilter("", OINODbSqlBooleanOperation.not, OINODbSqlFilter.parse(match[3]))
79
81
  } else {
80
82
  let boolean_parts = OINOStr.splitByBrackets(filterString, true, false, '(', ')')
81
- // OINOLog_debug("OINOFilter.constructor", {boolean_parts:boolean_parts})
83
+ // OINOLog_debug("OINODbSqlFilter.constructor", {boolean_parts:boolean_parts})
82
84
  if (boolean_parts.length == 3 && (boolean_parts[1].match(OINODbSqlFilter._booleanOperationRegex))) {
83
85
  return new OINODbSqlFilter(OINODbSqlFilter.parse(boolean_parts[0]), boolean_parts[1].trim().toLowerCase().substring(1) as OINODbSqlBooleanOperation, OINODbSqlFilter.parse(boolean_parts[2]))
84
86
 
@@ -91,7 +93,7 @@ export class OINODbSqlFilter {
91
93
  }
92
94
 
93
95
  /**
94
- * Construct a new `OINOFilter` as combination of (boolean and/or) of two filters.
96
+ * Construct a new `OINODbSqlFilter` as combination of (boolean and/or) of two filters.
95
97
  *
96
98
  * @param leftSide left side to combine
97
99
  * @param operation boolean operation to use in combination
@@ -144,7 +146,7 @@ export class OINODbSqlFilter {
144
146
  *
145
147
  */
146
148
  toSql(dataModel:OINODbDataModel):string {
147
- // OINOLog.debug("OINOFilter.toSql", {_leftSide:this._leftSide, _operator:this._operator, _rightSide:this._rightSide})
149
+ // OINOLog.debug("OINODbSqlFilter.toSql", {_leftSide:this._leftSide, _operator:this._operator, _rightSide:this._rightSide})
148
150
  if (this.isEmpty()) {
149
151
  return ""
150
152
  }
@@ -171,7 +173,7 @@ export class OINODbSqlFilter {
171
173
  }
172
174
  result += field!.printCellAsSqlValue(value)
173
175
  }
174
- // OINOLog.debug("OINOFilter.toSql", {result:result})
176
+ // OINOLog.debug("OINODbSqlFilter.toSql", {result:result})
175
177
  return "(" + result + ")"
176
178
  }
177
179
  }
@@ -335,4 +337,101 @@ export class OINODbSqlLimit {
335
337
  }
336
338
  }
337
339
 
340
+ /**
341
+ * Supported aggregation functions in OINODbSqlAggregate.
342
+ * @enum
343
+ */
344
+ export enum OINODbSqlAggregateFunctions { count = "count", sum = "sum", avg = "avg", min = "min", max = "max" }
345
+
346
+ /**
347
+ * Class for limiting the number of results.
348
+ *
349
+ */
350
+ export class OINODbSqlAggregate {
351
+ private static _aggregateRegex:RegExp = new RegExp("^(count|sum|avg|min|max)\\(([" + OINO_FIELD_NAME_CHARS + "]+)\\)$", "mi")
352
+
353
+ private _functions: OINODbSqlAggregateFunctions[]
354
+ private _fields: string[]
355
+
356
+ /**
357
+ * Constructor for `OINODbSqlAggregate`.
358
+ *
359
+ * @param function aggregate function to use
360
+ * @param fields fields to aggregate
361
+ *
362
+ */
363
+ constructor(func: OINODbSqlAggregateFunctions[], fields: string[]) {
364
+ this._functions = func
365
+ this._fields = fields
366
+ }
367
+ /**
368
+ * Constructor for `OINODbSqlAggregate` as parser of http parameter.
369
+ *
370
+ * @param aggregatorString string representation of limit from HTTP-request
371
+ *
372
+ */
373
+ static parse(aggregatorString: string):OINODbSqlAggregate {
374
+ let funtions:OINODbSqlAggregateFunctions[] = []
375
+ let fields:string[] = []
376
+ const aggregator_parts = aggregatorString.split(',')
377
+ for (let i=0; i<aggregator_parts.length; i++) {
378
+ let match = OINODbSqlAggregate._aggregateRegex.exec(aggregator_parts[i])
379
+ // OINOLog.debug("OINODbSqlAggregate.parse - next aggregator", {aggregator: aggregator_parts[i], match:match})
380
+ if ((match != null) && (match.length == 3)) {
381
+ funtions.push(match[1] as OINODbSqlAggregateFunctions)
382
+ fields.push(match[2])
383
+ }
384
+ }
385
+ return new OINODbSqlAggregate(funtions, fields)
386
+ }
387
+
388
+ /**
389
+ * Does filter contain any valid conditions.
390
+ *
391
+ */
392
+ isEmpty():boolean {
393
+ return (this._functions.length <= 0)
394
+ }
395
+
396
+ /**
397
+ * Print non-aggregated fields as SQL GROUP BY-condition based on the datamodel of the API.
398
+ *
399
+ * @param dataModel data model (and database) to use for formatting of values
400
+ *
401
+ */
402
+ toSql(dataModel:OINODbDataModel):string {
403
+ if (this.isEmpty()) {
404
+ return ""
405
+ }
406
+ let result:string = ""
407
+ for (let i=0; i<dataModel.fields.length; i++) {
408
+ if (this._fields.includes(dataModel.fields[i].name) == false) {
409
+ result += dataModel.fields[i].printSqlColumnName() + ","
410
+ }
411
+ }
412
+ OINOLog.debug("OINODbSqlAggregate.toSql", {result:result})
413
+ return result.substring(0, result.length-1)
414
+ }
415
+
416
+ /**
417
+ * Print non-aggregated fields as SQL GROUP BY-condition based on the datamodel of the API.
418
+ *
419
+ * @param dataModel data model (and database) to use for formatting of values
420
+ *
421
+ */
422
+ printSqlColumnNames(dataModel:OINODbDataModel):string {
423
+ let result:string = ""
424
+ for (let i=0; i<dataModel.fields.length; i++) {
425
+ const aggregate_index = this._fields.indexOf(dataModel.fields[i].name)
426
+ if (aggregate_index >= 0) {
427
+ result += this._functions[aggregate_index] + "(" + dataModel.fields[i].printSqlColumnName() + "),"
428
+ } else {
429
+ result += dataModel.fields[i].printSqlColumnName() + ","
430
+ }
431
+ }
432
+ OINOLog.debug("OINODbSqlAggregate.printSqlColumnNames", {result:result})
433
+ return result.substring(0, result.length-1)
434
+ }
435
+ }
436
+
338
437
 
package/src/index.ts CHANGED
@@ -5,7 +5,7 @@ export { OINO_ERROR_PREFIX, OINO_WARNING_PREFIX, OINO_INFO_PREFIX, OINO_DEBUG_PR
5
5
 
6
6
  import { OINODb } from "./OINODb.js"
7
7
  import { OINODbDataField } from "./OINODbDataField.js"
8
- import { OINODbSqlFilter, OINODbSqlLimit, OINODbSqlOrder } from "./OINODbSqlParams.js"
8
+ import { OINODbSqlAggregate, OINODbSqlFilter, OINODbSqlLimit, OINODbSqlOrder } from "./OINODbSqlParams.js"
9
9
 
10
10
  export { OINODbApiResult, OINODbHtmlTemplate, OINODbApi } from "./OINODbApi.js"
11
11
  export { OINODbDataModel } from "./OINODbDataModel.js"
@@ -20,6 +20,8 @@ export { OINODbParser } from "./OINODbParser.js"
20
20
 
21
21
  /** API parameters */
22
22
  export type OINODbApiParams = {
23
+ /** Name of the api */
24
+ apiName: string
23
25
  /** Name of the database table */
24
26
  tableName: string
25
27
  /** Reject values that exceed field max length (behaviour on such is platform dependent) */
@@ -30,9 +32,11 @@ export type OINODbApiParams = {
30
32
  failOnInsertWithoutKey?: boolean
31
33
  /** Treat date type fields as just strings and use the native formatting instead of the ISO 8601 format */
32
34
  useDatesAsString?: Boolean
35
+ /** Include given fields from the API and exclude rest (if defined) */
36
+ includeFields?:string[],
33
37
  /** Exclude all fields with this prefix from the API */
34
38
  excludeFieldPrefix?:string
35
- /** Exclude given fields from the API */
39
+ /** Exclude given fields from the API and include rest (if defined) */
36
40
  excludeFields?:string[],
37
41
  /** Enable hashids for numeric primarykeys by adding a 32 char key */
38
42
  hashidKey?:string,
@@ -92,6 +96,8 @@ export type OINODbSqlParams = {
92
96
  order?:OINODbSqlOrder
93
97
  /** SQL result limit condition */
94
98
  limit?:OINODbSqlLimit
99
+ /** SQL aggregation functions */
100
+ aggregate?:OINODbSqlAggregate
95
101
  }
96
102
 
97
103
  /** Request options */