@oino-ts/db 0.13.2 → 0.14.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.
@@ -12,7 +12,7 @@ const index_js_1 = require("./index.js");
12
12
  *
13
13
  */
14
14
  class OINODbDataModel {
15
- _columnLookup;
15
+ _fieldIndexLookup;
16
16
  /** Database refererence of the table */
17
17
  api;
18
18
  /** Field refererences of the API */
@@ -25,7 +25,7 @@ class OINODbDataModel {
25
25
  *
26
26
  */
27
27
  constructor(api) {
28
- this._columnLookup = {};
28
+ this._fieldIndexLookup = {};
29
29
  this.api = api;
30
30
  this.fields = [];
31
31
  }
@@ -116,7 +116,7 @@ class OINODbDataModel {
116
116
  */
117
117
  addField(field) {
118
118
  this.fields.push(field);
119
- this._columnLookup[field.name] = this.fields.length - 1;
119
+ this._fieldIndexLookup[field.name] = this.fields.length - 1;
120
120
  }
121
121
  /**
122
122
  * Find a field of a given name if any.
@@ -125,7 +125,7 @@ class OINODbDataModel {
125
125
  *
126
126
  */
127
127
  findFieldByName(name) {
128
- const i = this._columnLookup[name];
128
+ const i = this._fieldIndexLookup[name];
129
129
  if (i >= 0) {
130
130
  return this.fields[i];
131
131
  }
@@ -140,7 +140,7 @@ class OINODbDataModel {
140
140
  *
141
141
  */
142
142
  findFieldIndexByName(name) {
143
- const i = this._columnLookup[name];
143
+ const i = this._fieldIndexLookup[name];
144
144
  if (i >= 0) {
145
145
  return i;
146
146
  }
@@ -308,13 +308,16 @@ class OINODbModelSet {
308
308
  /**
309
309
  * Export all rows as a record with OINOId as key and object with row cells as values.
310
310
  *
311
+ * @param idFieldName optional field name to use as key instead of OINOId
311
312
  */
312
- async exportAsRecord() {
313
+ async exportAsRecord(idFieldName) {
313
314
  const result = {};
315
+ const row_id_field = idFieldName || index_js_1.OINODbConfig.OINODB_ID_FIELD;
314
316
  while (!this.dataset.isEof()) {
315
317
  const row_data = this.dataset.getRow();
316
318
  const row_export = this._exportRow(row_data);
317
- result[row_export[index_js_1.OINODbConfig.OINODB_ID_FIELD]] = row_export;
319
+ const row_id = row_export[row_id_field];
320
+ result[row_id] = row_export;
318
321
  await this.dataset.next();
319
322
  }
320
323
  return result;
@@ -5,7 +5,7 @@
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.OINODbSqlSelect = exports.OINODbSqlAggregate = exports.OINODbSqlAggregateFunctions = exports.OINODbSqlLimit = exports.OINODbSqlOrder = exports.OINODbSqlFilter = exports.OINODbSqlComparison = exports.OINODbSqlBooleanOperation = void 0;
8
+ exports.OINODbSqlSelect = exports.OINODbSqlAggregate = exports.OINODbSqlAggregateFunctions = exports.OINODbSqlLimit = exports.OINODbSqlOrder = exports.OINODbSqlFilter = exports.OINODbSqlNullCheck = exports.OINODbSqlComparison = exports.OINODbSqlBooleanOperation = void 0;
9
9
  const index_js_1 = require("./index.js");
10
10
  const OINO_FIELD_NAME_CHARS = "\\w\\s\\-\\_\\#\\¤";
11
11
  /**
@@ -27,10 +27,20 @@ var OINODbSqlComparison;
27
27
  OINODbSqlComparison["lt"] = "lt";
28
28
  OINODbSqlComparison["le"] = "le";
29
29
  OINODbSqlComparison["eq"] = "eq";
30
+ OINODbSqlComparison["ne"] = "ne";
30
31
  OINODbSqlComparison["ge"] = "ge";
31
32
  OINODbSqlComparison["gt"] = "gt";
32
33
  OINODbSqlComparison["like"] = "like";
33
34
  })(OINODbSqlComparison || (exports.OINODbSqlComparison = OINODbSqlComparison = {}));
35
+ /**
36
+ * Supported logical conjunctions in filter predicates.
37
+ * @enum
38
+ */
39
+ var OINODbSqlNullCheck;
40
+ (function (OINODbSqlNullCheck) {
41
+ OINODbSqlNullCheck["isnull"] = "isnull";
42
+ OINODbSqlNullCheck["isnotnull"] = "isnotnull";
43
+ })(OINODbSqlNullCheck || (exports.OINODbSqlNullCheck = OINODbSqlNullCheck = {}));
34
44
  /**
35
45
  * Class for recursively parsing of filters and printing them as SQL conditions.
36
46
  * Supports three types of statements
@@ -42,8 +52,9 @@ var OINODbSqlComparison;
42
52
  */
43
53
  class OINODbSqlFilter {
44
54
  static _booleanOperationRegex = /^\s?\-(and|or)\s?$/i;
45
- static _negationRegex = /^-(not|)\((.+)\)$/i;
46
- static _filterComparisonRegex = /^\(([^'"\(\)]+)\)\s?\-(lt|le|eq|ge|gt|like)\s?\(([^'"\(\)]+)\)$/i;
55
+ static _negationRegex = /^-(not)\((.+)\)$/i;
56
+ static _filterComparisonRegex = /^\(([^'"\(\)]+)\)\s?\-(lt|le|eq|ne|ge|gt|like)\s?\(([^'"\(\)]+)\)$/i;
57
+ static _filterNullCheckRegex = /^-(isnull|isnotnull)\((.+)\)$/i;
47
58
  _leftSide;
48
59
  _rightSide;
49
60
  _operator;
@@ -57,6 +68,7 @@ class OINODbSqlFilter {
57
68
  if (!(((operation === null) && (leftSide == "") && (rightSide == "")) ||
58
69
  ((operation !== null) && (Object.values(OINODbSqlComparison).includes(operation)) && (typeof (leftSide) == "string") && (leftSide != "") && (typeof (rightSide) == "string") && (rightSide != "")) ||
59
70
  ((operation == OINODbSqlBooleanOperation.not) && (leftSide == "") && (rightSide instanceof OINODbSqlFilter)) ||
71
+ (((operation == OINODbSqlNullCheck.isnull) || (operation == OINODbSqlNullCheck.isnotnull)) && (typeof (leftSide) == "string") && (rightSide == "")) ||
60
72
  (((operation == OINODbSqlBooleanOperation.and) || (operation == OINODbSqlBooleanOperation.or)) && (leftSide instanceof OINODbSqlFilter) && (rightSide instanceof OINODbSqlFilter)))) {
61
73
  index_js_1.OINOLog.error("@oino-ts/db", "OINODbSqlFilter", "constructor", "Unsupported OINODbSqlFilter format", { leftSide: leftSide, operation: operation, rightSide: rightSide });
62
74
  throw new Error(index_js_1.OINO_ERROR_PREFIX + ": Unsupported OINODbSqlFilter format!");
@@ -72,6 +84,7 @@ class OINODbSqlFilter {
72
84
  * - comparison: (field)-lt|le|eq|ge|gt|like(value)
73
85
  * - negation: -not(filter)
74
86
  * - conjunction/disjunction: (filter)-and|or(filter)
87
+ * - null check: -isnull(field) or -isnotnull(field)
75
88
  *
76
89
  * @param filterString string representation of filter from HTTP-request
77
90
  *
@@ -82,7 +95,7 @@ class OINODbSqlFilter {
82
95
  }
83
96
  else {
84
97
  let match = OINODbSqlFilter._filterComparisonRegex.exec(filterString);
85
- if (match != null) {
98
+ if ((match != null) && (match.length == 4)) {
86
99
  return new OINODbSqlFilter(match[1], match[2].toLowerCase(), match[3]);
87
100
  }
88
101
  else {
@@ -96,8 +109,14 @@ class OINODbSqlFilter {
96
109
  return new OINODbSqlFilter(OINODbSqlFilter.parse(boolean_parts[0]), boolean_parts[1].trim().toLowerCase().substring(1), OINODbSqlFilter.parse(boolean_parts[2]));
97
110
  }
98
111
  else {
99
- index_js_1.OINOLog.error("@oino-ts/db", "OINODbSqlFilter", "constructor", "Invalid filter", { filterString: filterString });
100
- throw new Error(index_js_1.OINO_ERROR_PREFIX + ": Invalid filter '" + filterString + "'"); // invalid filter could be a security risk, stop processing
112
+ let match = OINODbSqlFilter._filterNullCheckRegex.exec(filterString);
113
+ if ((match != null)) {
114
+ return new OINODbSqlFilter(match[2], match[1].toLowerCase(), "");
115
+ }
116
+ else {
117
+ index_js_1.OINOLog.error("@oino-ts/db", "OINODbSqlFilter", "constructor", "Invalid filter", { filterString: filterString });
118
+ throw new Error(index_js_1.OINO_ERROR_PREFIX + ": Invalid filter '" + filterString + "'"); // invalid filter could be a security risk, stop processing
119
+ }
101
120
  }
102
121
  }
103
122
  }
@@ -125,6 +144,50 @@ class OINODbSqlFilter {
125
144
  return undefined;
126
145
  }
127
146
  }
147
+ /**
148
+ * Combine two filters with an AND operation.
149
+ *
150
+ * @param leftSide left side filter
151
+ * @param rightSide right side filter
152
+ *
153
+ */
154
+ static and(leftSide, rightSide) {
155
+ if ((leftSide) && (!leftSide.isEmpty()) && (rightSide) && (!rightSide.isEmpty())) {
156
+ return new OINODbSqlFilter(leftSide, OINODbSqlBooleanOperation.and, rightSide);
157
+ }
158
+ else {
159
+ return undefined;
160
+ }
161
+ }
162
+ /**
163
+ * Combine two filters with an OR operation.
164
+ *
165
+ * @param leftSide left side filter
166
+ * @param rightSide right side filter
167
+ *
168
+ */
169
+ static or(leftSide, rightSide) {
170
+ if ((leftSide) && (!leftSide.isEmpty()) && (rightSide) && (!rightSide.isEmpty())) {
171
+ return new OINODbSqlFilter(leftSide, OINODbSqlBooleanOperation.or, rightSide);
172
+ }
173
+ else {
174
+ return undefined;
175
+ }
176
+ }
177
+ /**
178
+ * Negate a filter with a NOT operation.
179
+ *
180
+ * @param leftSide left side filter
181
+ *
182
+ */
183
+ static not(leftSide) {
184
+ if ((leftSide) && (!leftSide.isEmpty())) {
185
+ return new OINODbSqlFilter(leftSide, OINODbSqlBooleanOperation.not, "");
186
+ }
187
+ else {
188
+ return undefined;
189
+ }
190
+ }
128
191
  _operatorToSql() {
129
192
  switch (this._operator) {
130
193
  case "and": return " AND ";
@@ -133,9 +196,12 @@ class OINODbSqlFilter {
133
196
  case "lt": return " < ";
134
197
  case "le": return " <= ";
135
198
  case "eq": return " = ";
199
+ case "ne": return " != ";
136
200
  case "ge": return " >= ";
137
201
  case "gt": return " > ";
138
202
  case "like": return " LIKE ";
203
+ case "isnull": return " IS NULL";
204
+ case "isnotnull": return " IS NOT NULL";
139
205
  }
140
206
  return " ";
141
207
  }
@@ -173,6 +239,9 @@ class OINODbSqlFilter {
173
239
  if (this._rightSide instanceof OINODbSqlFilter) {
174
240
  result += this._rightSide.toSql(dataModel);
175
241
  }
242
+ else if (this._operator == OINODbSqlNullCheck.isnull || this._operator == OINODbSqlNullCheck.isnotnull) {
243
+ // nothing to do, IS NULL and IS NOT NULL do not have a right side
244
+ }
176
245
  else {
177
246
  const value = field.deserializeCell(this._rightSide);
178
247
  if ((value == null) || (value === "")) {
@@ -9,7 +9,7 @@ import { OINO_ERROR_PREFIX, OINODbConfig, OINONumberDataField, OINODB_UNDEFINED
9
9
  *
10
10
  */
11
11
  export class OINODbDataModel {
12
- _columnLookup;
12
+ _fieldIndexLookup;
13
13
  /** Database refererence of the table */
14
14
  api;
15
15
  /** Field refererences of the API */
@@ -22,7 +22,7 @@ export class OINODbDataModel {
22
22
  *
23
23
  */
24
24
  constructor(api) {
25
- this._columnLookup = {};
25
+ this._fieldIndexLookup = {};
26
26
  this.api = api;
27
27
  this.fields = [];
28
28
  }
@@ -113,7 +113,7 @@ export class OINODbDataModel {
113
113
  */
114
114
  addField(field) {
115
115
  this.fields.push(field);
116
- this._columnLookup[field.name] = this.fields.length - 1;
116
+ this._fieldIndexLookup[field.name] = this.fields.length - 1;
117
117
  }
118
118
  /**
119
119
  * Find a field of a given name if any.
@@ -122,7 +122,7 @@ export class OINODbDataModel {
122
122
  *
123
123
  */
124
124
  findFieldByName(name) {
125
- const i = this._columnLookup[name];
125
+ const i = this._fieldIndexLookup[name];
126
126
  if (i >= 0) {
127
127
  return this.fields[i];
128
128
  }
@@ -137,7 +137,7 @@ export class OINODbDataModel {
137
137
  *
138
138
  */
139
139
  findFieldIndexByName(name) {
140
- const i = this._columnLookup[name];
140
+ const i = this._fieldIndexLookup[name];
141
141
  if (i >= 0) {
142
142
  return i;
143
143
  }
@@ -305,13 +305,16 @@ export class OINODbModelSet {
305
305
  /**
306
306
  * Export all rows as a record with OINOId as key and object with row cells as values.
307
307
  *
308
+ * @param idFieldName optional field name to use as key instead of OINOId
308
309
  */
309
- async exportAsRecord() {
310
+ async exportAsRecord(idFieldName) {
310
311
  const result = {};
312
+ const row_id_field = idFieldName || OINODbConfig.OINODB_ID_FIELD;
311
313
  while (!this.dataset.isEof()) {
312
314
  const row_data = this.dataset.getRow();
313
315
  const row_export = this._exportRow(row_data);
314
- result[row_export[OINODbConfig.OINODB_ID_FIELD]] = row_export;
316
+ const row_id = row_export[row_id_field];
317
+ result[row_id] = row_export;
315
318
  await this.dataset.next();
316
319
  }
317
320
  return result;
@@ -24,10 +24,20 @@ export var OINODbSqlComparison;
24
24
  OINODbSqlComparison["lt"] = "lt";
25
25
  OINODbSqlComparison["le"] = "le";
26
26
  OINODbSqlComparison["eq"] = "eq";
27
+ OINODbSqlComparison["ne"] = "ne";
27
28
  OINODbSqlComparison["ge"] = "ge";
28
29
  OINODbSqlComparison["gt"] = "gt";
29
30
  OINODbSqlComparison["like"] = "like";
30
31
  })(OINODbSqlComparison || (OINODbSqlComparison = {}));
32
+ /**
33
+ * Supported logical conjunctions in filter predicates.
34
+ * @enum
35
+ */
36
+ export var OINODbSqlNullCheck;
37
+ (function (OINODbSqlNullCheck) {
38
+ OINODbSqlNullCheck["isnull"] = "isnull";
39
+ OINODbSqlNullCheck["isnotnull"] = "isnotnull";
40
+ })(OINODbSqlNullCheck || (OINODbSqlNullCheck = {}));
31
41
  /**
32
42
  * Class for recursively parsing of filters and printing them as SQL conditions.
33
43
  * Supports three types of statements
@@ -39,8 +49,9 @@ export var OINODbSqlComparison;
39
49
  */
40
50
  export class OINODbSqlFilter {
41
51
  static _booleanOperationRegex = /^\s?\-(and|or)\s?$/i;
42
- static _negationRegex = /^-(not|)\((.+)\)$/i;
43
- static _filterComparisonRegex = /^\(([^'"\(\)]+)\)\s?\-(lt|le|eq|ge|gt|like)\s?\(([^'"\(\)]+)\)$/i;
52
+ static _negationRegex = /^-(not)\((.+)\)$/i;
53
+ static _filterComparisonRegex = /^\(([^'"\(\)]+)\)\s?\-(lt|le|eq|ne|ge|gt|like)\s?\(([^'"\(\)]+)\)$/i;
54
+ static _filterNullCheckRegex = /^-(isnull|isnotnull)\((.+)\)$/i;
44
55
  _leftSide;
45
56
  _rightSide;
46
57
  _operator;
@@ -54,6 +65,7 @@ export class OINODbSqlFilter {
54
65
  if (!(((operation === null) && (leftSide == "") && (rightSide == "")) ||
55
66
  ((operation !== null) && (Object.values(OINODbSqlComparison).includes(operation)) && (typeof (leftSide) == "string") && (leftSide != "") && (typeof (rightSide) == "string") && (rightSide != "")) ||
56
67
  ((operation == OINODbSqlBooleanOperation.not) && (leftSide == "") && (rightSide instanceof OINODbSqlFilter)) ||
68
+ (((operation == OINODbSqlNullCheck.isnull) || (operation == OINODbSqlNullCheck.isnotnull)) && (typeof (leftSide) == "string") && (rightSide == "")) ||
57
69
  (((operation == OINODbSqlBooleanOperation.and) || (operation == OINODbSqlBooleanOperation.or)) && (leftSide instanceof OINODbSqlFilter) && (rightSide instanceof OINODbSqlFilter)))) {
58
70
  OINOLog.error("@oino-ts/db", "OINODbSqlFilter", "constructor", "Unsupported OINODbSqlFilter format", { leftSide: leftSide, operation: operation, rightSide: rightSide });
59
71
  throw new Error(OINO_ERROR_PREFIX + ": Unsupported OINODbSqlFilter format!");
@@ -69,6 +81,7 @@ export class OINODbSqlFilter {
69
81
  * - comparison: (field)-lt|le|eq|ge|gt|like(value)
70
82
  * - negation: -not(filter)
71
83
  * - conjunction/disjunction: (filter)-and|or(filter)
84
+ * - null check: -isnull(field) or -isnotnull(field)
72
85
  *
73
86
  * @param filterString string representation of filter from HTTP-request
74
87
  *
@@ -79,7 +92,7 @@ export class OINODbSqlFilter {
79
92
  }
80
93
  else {
81
94
  let match = OINODbSqlFilter._filterComparisonRegex.exec(filterString);
82
- if (match != null) {
95
+ if ((match != null) && (match.length == 4)) {
83
96
  return new OINODbSqlFilter(match[1], match[2].toLowerCase(), match[3]);
84
97
  }
85
98
  else {
@@ -93,8 +106,14 @@ export class OINODbSqlFilter {
93
106
  return new OINODbSqlFilter(OINODbSqlFilter.parse(boolean_parts[0]), boolean_parts[1].trim().toLowerCase().substring(1), OINODbSqlFilter.parse(boolean_parts[2]));
94
107
  }
95
108
  else {
96
- OINOLog.error("@oino-ts/db", "OINODbSqlFilter", "constructor", "Invalid filter", { filterString: filterString });
97
- throw new Error(OINO_ERROR_PREFIX + ": Invalid filter '" + filterString + "'"); // invalid filter could be a security risk, stop processing
109
+ let match = OINODbSqlFilter._filterNullCheckRegex.exec(filterString);
110
+ if ((match != null)) {
111
+ return new OINODbSqlFilter(match[2], match[1].toLowerCase(), "");
112
+ }
113
+ else {
114
+ OINOLog.error("@oino-ts/db", "OINODbSqlFilter", "constructor", "Invalid filter", { filterString: filterString });
115
+ throw new Error(OINO_ERROR_PREFIX + ": Invalid filter '" + filterString + "'"); // invalid filter could be a security risk, stop processing
116
+ }
98
117
  }
99
118
  }
100
119
  }
@@ -122,6 +141,50 @@ export class OINODbSqlFilter {
122
141
  return undefined;
123
142
  }
124
143
  }
144
+ /**
145
+ * Combine two filters with an AND operation.
146
+ *
147
+ * @param leftSide left side filter
148
+ * @param rightSide right side filter
149
+ *
150
+ */
151
+ static and(leftSide, rightSide) {
152
+ if ((leftSide) && (!leftSide.isEmpty()) && (rightSide) && (!rightSide.isEmpty())) {
153
+ return new OINODbSqlFilter(leftSide, OINODbSqlBooleanOperation.and, rightSide);
154
+ }
155
+ else {
156
+ return undefined;
157
+ }
158
+ }
159
+ /**
160
+ * Combine two filters with an OR operation.
161
+ *
162
+ * @param leftSide left side filter
163
+ * @param rightSide right side filter
164
+ *
165
+ */
166
+ static or(leftSide, rightSide) {
167
+ if ((leftSide) && (!leftSide.isEmpty()) && (rightSide) && (!rightSide.isEmpty())) {
168
+ return new OINODbSqlFilter(leftSide, OINODbSqlBooleanOperation.or, rightSide);
169
+ }
170
+ else {
171
+ return undefined;
172
+ }
173
+ }
174
+ /**
175
+ * Negate a filter with a NOT operation.
176
+ *
177
+ * @param leftSide left side filter
178
+ *
179
+ */
180
+ static not(leftSide) {
181
+ if ((leftSide) && (!leftSide.isEmpty())) {
182
+ return new OINODbSqlFilter(leftSide, OINODbSqlBooleanOperation.not, "");
183
+ }
184
+ else {
185
+ return undefined;
186
+ }
187
+ }
125
188
  _operatorToSql() {
126
189
  switch (this._operator) {
127
190
  case "and": return " AND ";
@@ -130,9 +193,12 @@ export class OINODbSqlFilter {
130
193
  case "lt": return " < ";
131
194
  case "le": return " <= ";
132
195
  case "eq": return " = ";
196
+ case "ne": return " != ";
133
197
  case "ge": return " >= ";
134
198
  case "gt": return " > ";
135
199
  case "like": return " LIKE ";
200
+ case "isnull": return " IS NULL";
201
+ case "isnotnull": return " IS NOT NULL";
136
202
  }
137
203
  return " ";
138
204
  }
@@ -170,6 +236,9 @@ export class OINODbSqlFilter {
170
236
  if (this._rightSide instanceof OINODbSqlFilter) {
171
237
  result += this._rightSide.toSql(dataModel);
172
238
  }
239
+ else if (this._operator == OINODbSqlNullCheck.isnull || this._operator == OINODbSqlNullCheck.isnotnull) {
240
+ // nothing to do, IS NULL and IS NOT NULL do not have a right side
241
+ }
173
242
  else {
174
243
  const value = field.deserializeCell(this._rightSide);
175
244
  if ((value == null) || (value === "")) {
@@ -1,5 +1,3 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
3
1
  import { OINODbApiParams, OINODb, OINODbDataModel, OINODataRow, OINODbModelSet, OINODbApiRequestParams, OINOHttpResult, OINOHtmlTemplate } from "./index.js";
4
2
  import { OINOResult } from "@oino-ts/common";
5
3
  import { OINOHashid } from "@oino-ts/hashid";
@@ -4,7 +4,7 @@ import { OINODbDataField, OINODbApi, OINODataRow, OINODbDataFieldFilter, OINODbS
4
4
  *
5
5
  */
6
6
  export declare class OINODbDataModel {
7
- private _columnLookup;
7
+ private _fieldIndexLookup;
8
8
  /** Database refererence of the table */
9
9
  readonly api: OINODbApi;
10
10
  /** Field refererences of the API */
@@ -55,6 +55,7 @@ export declare class OINODbModelSet {
55
55
  /**
56
56
  * Export all rows as a record with OINOId as key and object with row cells as values.
57
57
  *
58
+ * @param idFieldName optional field name to use as key instead of OINOId
58
59
  */
59
- exportAsRecord(): Promise<Record<string, any>>;
60
+ exportAsRecord(idFieldName?: string): Promise<Record<string, any>>;
60
61
  }
@@ -1,5 +1,3 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
3
1
  import { OINODbDataModel, OINODataRow, OINODbApiRequestParams } from "./index.js";
4
2
  /**
5
3
  * Static factory class for easily creating things based on data
@@ -16,10 +16,19 @@ export declare enum OINODbSqlComparison {
16
16
  lt = "lt",
17
17
  le = "le",
18
18
  eq = "eq",
19
+ ne = "ne",
19
20
  ge = "ge",
20
21
  gt = "gt",
21
22
  like = "like"
22
23
  }
24
+ /**
25
+ * Supported logical conjunctions in filter predicates.
26
+ * @enum
27
+ */
28
+ export declare enum OINODbSqlNullCheck {
29
+ isnull = "isnull",
30
+ isnotnull = "isnotnull"
31
+ }
23
32
  /**
24
33
  * Class for recursively parsing of filters and printing them as SQL conditions.
25
34
  * Supports three types of statements
@@ -33,6 +42,7 @@ export declare class OINODbSqlFilter {
33
42
  private static _booleanOperationRegex;
34
43
  private static _negationRegex;
35
44
  private static _filterComparisonRegex;
45
+ private static _filterNullCheckRegex;
36
46
  private _leftSide;
37
47
  private _rightSide;
38
48
  private _operator;
@@ -42,7 +52,7 @@ export declare class OINODbSqlFilter {
42
52
  * @param operation operation of the filter, either `OINODbSqlComparison` or `OINODbSqlBooleanOperation`
43
53
  * @param rightSide right side of the filter, either another filter or a value
44
54
  */
45
- constructor(leftSide: OINODbSqlFilter | string, operation: OINODbSqlComparison | OINODbSqlBooleanOperation | null, rightSide: OINODbSqlFilter | string);
55
+ constructor(leftSide: OINODbSqlFilter | string, operation: OINODbSqlComparison | OINODbSqlBooleanOperation | OINODbSqlNullCheck | null, rightSide: OINODbSqlFilter | string);
46
56
  /**
47
57
  * Constructor for `OINODbSqlFilter` as parser of http parameter.
48
58
  *
@@ -50,6 +60,7 @@ export declare class OINODbSqlFilter {
50
60
  * - comparison: (field)-lt|le|eq|ge|gt|like(value)
51
61
  * - negation: -not(filter)
52
62
  * - conjunction/disjunction: (filter)-and|or(filter)
63
+ * - null check: -isnull(field) or -isnotnull(field)
53
64
  *
54
65
  * @param filterString string representation of filter from HTTP-request
55
66
  *
@@ -64,6 +75,29 @@ export declare class OINODbSqlFilter {
64
75
  *
65
76
  */
66
77
  static combine(leftSide: OINODbSqlFilter | undefined, operation: OINODbSqlBooleanOperation, rightSide: OINODbSqlFilter | undefined): OINODbSqlFilter | undefined;
78
+ /**
79
+ * Combine two filters with an AND operation.
80
+ *
81
+ * @param leftSide left side filter
82
+ * @param rightSide right side filter
83
+ *
84
+ */
85
+ static and(leftSide: OINODbSqlFilter, rightSide: OINODbSqlFilter): OINODbSqlFilter | undefined;
86
+ /**
87
+ * Combine two filters with an OR operation.
88
+ *
89
+ * @param leftSide left side filter
90
+ * @param rightSide right side filter
91
+ *
92
+ */
93
+ static or(leftSide: OINODbSqlFilter, rightSide: OINODbSqlFilter): OINODbSqlFilter | undefined;
94
+ /**
95
+ * Negate a filter with a NOT operation.
96
+ *
97
+ * @param leftSide left side filter
98
+ *
99
+ */
100
+ static not(leftSide: OINODbSqlFilter): OINODbSqlFilter | undefined;
67
101
  private _operatorToSql;
68
102
  /**
69
103
  * Does filter contain any valid conditions.
@@ -1,5 +1,3 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
3
1
  import { OINOContentType } from "@oino-ts/common";
4
2
  export { OINOContentType };
5
3
  export { OINO_ERROR_PREFIX, OINO_WARNING_PREFIX, OINO_INFO_PREFIX, OINO_DEBUG_PREFIX, OINOStr, OINOBenchmark, OINOMemoryBenchmark, OINOLog, OINOLogLevel, OINOConsoleLog, OINOResult, OINOHttpResult, OINOHtmlTemplate } from "@oino-ts/common";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oino-ts/db",
3
- "version": "0.13.2",
3
+ "version": "0.14.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,11 +19,11 @@
19
19
  "module": "./dist/esm/index.js",
20
20
  "types": "./dist/types/index.d.ts",
21
21
  "dependencies": {
22
- "@oino-ts/common": "0.13.2",
22
+ "@oino-ts/common": "0.14.0",
23
23
  "oino-ts": "file:.."
24
24
  },
25
25
  "devDependencies": {
26
- "@oino-ts/types": "0.13.2",
26
+ "@oino-ts/types": "0.14.0",
27
27
  "@types/bun": "^1.1.14",
28
28
  "@types/node": "^20.14.10",
29
29
  "typescript": "~5.9.0"
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { expect, test } from "bun:test";
8
8
 
9
- import { OINODbApi, OINODbApiParams, OINOContentType, OINODataRow, OINODbDataField, OINOStringDataField, OINODb, OINODbFactory, OINODbParams, OINODbMemoryDataSet, OINODbModelSet, OINOBenchmark, OINOConsoleLog, OINODbSqlFilter, OINODbConfig, OINODbSqlOrder, OINOLogLevel, OINOLog, OINODbSqlLimit, OINODbApiResult, OINODbSqlComparison, OINONumberDataField, OINODatetimeDataField, OINODbApiRequestParams, OINODbHtmlTemplate, OINODbParser } from "./index.js";
9
+ import { OINODbApi, OINODbApiParams, OINOContentType, OINODataRow, OINODbDataField, OINOStringDataField, OINODb, OINODbFactory, OINODbParams, OINODbMemoryDataSet, OINODbModelSet, OINOBenchmark, OINOConsoleLog, OINODbSqlFilter, OINODbConfig, OINODbSqlOrder, OINOLogLevel, OINOLog, OINODbSqlLimit, OINODbApiResult, OINODbSqlComparison, OINONumberDataField, OINODatetimeDataField, OINODbApiRequestParams, OINODbHtmlTemplate, OINODbParser, OINODbSqlBooleanOperation } from "./index.js";
10
10
 
11
11
  import { OINODbBunSqlite } from "@oino-ts/db-bunsqlite"
12
12
  import { OINODbPostgresql } from "@oino-ts/db-postgresql"
@@ -40,7 +40,13 @@ const API_TESTS:OINOTestParams[] = [
40
40
  name: "API 1",
41
41
  apiParams: { apiName: "Orders", tableName: "Orders" },
42
42
  requestParams: {
43
- sqlParams: { filter: OINODbSqlFilter.parse("(ShipPostalCode)-like(0502%)"), order: OINODbSqlOrder.parse("ShipPostalCode-,Freight+"), limit: OINODbSqlLimit.parse("5 page 2") }
43
+ sqlParams: { filter: OINODbSqlFilter.and(
44
+ OINODbSqlFilter.parse("(ShipPostalCode)-like(0502%)"),
45
+ OINODbSqlFilter.parse("-isnull(ShipRegion)")
46
+ ),
47
+ order: OINODbSqlOrder.parse("ShipPostalCode-,Freight+"),
48
+ limit: OINODbSqlLimit.parse("5 page 2")
49
+ }
44
50
  },
45
51
  postRow: [30000,"CACTU",1,new Date("2024-04-05"),new Date("2024-04-06"),new Date("2024-04-07"),2,"184.75","a'b\"c%d_e\tf\rg\nh\\i","Garden House Crowther Way","Cowes","British Isles","PO31 7PJ","UK"],
46
52
  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"]
@@ -49,7 +55,13 @@ const API_TESTS:OINOTestParams[] = [
49
55
  name: "API 2",
50
56
  apiParams: { apiName: "Products", tableName: "Products", failOnOversizedValues: true },
51
57
  requestParams: {
52
- sqlParams: { filter: OINODbSqlFilter.parse("(UnitsInStock)-le(5)"), order: OINODbSqlOrder.parse("UnitsInStock,UnitPrice"), limit: OINODbSqlLimit.parse("7") }
58
+ sqlParams: { filter: OINODbSqlFilter.and(
59
+ OINODbSqlFilter.parse("(UnitsInStock)-le(5)"),
60
+ OINODbSqlFilter.parse("(UnitsInStock)-ne(4)"),
61
+ ),
62
+ order: OINODbSqlOrder.parse("UnitsInStock,UnitPrice"),
63
+ limit: OINODbSqlLimit.parse("7")
64
+ }
53
65
  },
54
66
  postRow: [99, "Umeshu", 1, 1, "500 ml", 12.99, 2, 0, 20, 0],
55
67
  putRow: [99, "Umeshu", 1, 1, undefined, 24.99, 3, 0, 20, 0]
@@ -11,7 +11,7 @@ import { OINODbDataField, OINODbApi, OINODataRow, OINO_ERROR_PREFIX, OINODbDataF
11
11
  *
12
12
  */
13
13
  export class OINODbDataModel {
14
- private _columnLookup:Record<string, number>;
14
+ private _fieldIndexLookup:Record<string, number>;
15
15
 
16
16
  /** Database refererence of the table */
17
17
  readonly api:OINODbApi
@@ -27,7 +27,7 @@ export class OINODbDataModel {
27
27
  *
28
28
  */
29
29
  constructor(api:OINODbApi) {
30
- this._columnLookup = {}
30
+ this._fieldIndexLookup = {}
31
31
  this.api = api
32
32
  this.fields = []
33
33
  }
@@ -122,7 +122,7 @@ export class OINODbDataModel {
122
122
  */
123
123
  addField(field:OINODbDataField) {
124
124
  this.fields.push(field)
125
- this._columnLookup[field.name] = this.fields.length-1
125
+ this._fieldIndexLookup[field.name] = this.fields.length-1
126
126
  }
127
127
 
128
128
  /**
@@ -132,7 +132,7 @@ export class OINODbDataModel {
132
132
  *
133
133
  */
134
134
  findFieldByName(name:string):OINODbDataField|null {
135
- const i:number = this._columnLookup[name]
135
+ const i:number = this._fieldIndexLookup[name]
136
136
  if (i >= 0) {
137
137
  return this.fields[i]
138
138
  } else {
@@ -147,7 +147,7 @@ export class OINODbDataModel {
147
147
  *
148
148
  */
149
149
  findFieldIndexByName(name:string):number {
150
- const i:number = this._columnLookup[name]
150
+ const i:number = this._fieldIndexLookup[name]
151
151
  if (i >= 0) {
152
152
  return i
153
153
  } else {
@@ -329,15 +329,18 @@ export class OINODbModelSet {
329
329
 
330
330
  /**
331
331
  * Export all rows as a record with OINOId as key and object with row cells as values.
332
- *
332
+ *
333
+ * @param idFieldName optional field name to use as key instead of OINOId
333
334
  */
334
335
 
335
- async exportAsRecord():Promise<Record<string, any>> {
336
+ async exportAsRecord(idFieldName?:string):Promise<Record<string, any>> {
336
337
  const result:Record<string, any> = {}
338
+ const row_id_field = idFieldName || OINODbConfig.OINODB_ID_FIELD
337
339
  while (!this.dataset.isEof()) {
338
340
  const row_data:OINODataRow = this.dataset.getRow()
339
341
  const row_export = this._exportRow(row_data)
340
- result[row_export[OINODbConfig.OINODB_ID_FIELD]] = row_export
342
+ const row_id = row_export[row_id_field]
343
+ result[row_id] = row_export
341
344
  await this.dataset.next()
342
345
  }
343
346
  return result
@@ -18,7 +18,14 @@ export enum OINODbSqlBooleanOperation { and = "and", or = "or", not = "not" }
18
18
  * Supported logical conjunctions in filter predicates.
19
19
  * @enum
20
20
  */
21
- export enum OINODbSqlComparison { lt = "lt", le = "le", eq = "eq", ge = "ge", gt = "gt", like = "like" }
21
+ export enum OINODbSqlComparison { lt = "lt", le = "le", eq = "eq", ne = "ne", ge = "ge", gt = "gt", like = "like" }
22
+
23
+ /**
24
+ * Supported logical conjunctions in filter predicates.
25
+ * @enum
26
+ */
27
+ export enum OINODbSqlNullCheck { isnull = "isnull", isnotnull = "isnotnull" }
28
+
22
29
 
23
30
  /**
24
31
  * Class for recursively parsing of filters and printing them as SQL conditions.
@@ -31,12 +38,13 @@ export enum OINODbSqlComparison { lt = "lt", le = "le", eq = "eq", ge = "ge", gt
31
38
  */
32
39
  export class OINODbSqlFilter {
33
40
  private static _booleanOperationRegex = /^\s?\-(and|or)\s?$/i
34
- private static _negationRegex = /^-(not|)\((.+)\)$/i
35
- private static _filterComparisonRegex = /^\(([^'"\(\)]+)\)\s?\-(lt|le|eq|ge|gt|like)\s?\(([^'"\(\)]+)\)$/i
41
+ private static _negationRegex = /^-(not)\((.+)\)$/i
42
+ private static _filterComparisonRegex = /^\(([^'"\(\)]+)\)\s?\-(lt|le|eq|ne|ge|gt|like)\s?\(([^'"\(\)]+)\)$/i
43
+ private static _filterNullCheckRegex = /^-(isnull|isnotnull)\((.+)\)$/i
36
44
 
37
45
  private _leftSide: OINODbSqlFilter | string
38
46
  private _rightSide: OINODbSqlFilter | string
39
- private _operator:OINODbSqlComparison|OINODbSqlBooleanOperation|null
47
+ private _operator:OINODbSqlComparison|OINODbSqlBooleanOperation|OINODbSqlNullCheck|null
40
48
 
41
49
  /**
42
50
  * Constructor of `OINODbSqlFilter`
@@ -44,11 +52,12 @@ export class OINODbSqlFilter {
44
52
  * @param operation operation of the filter, either `OINODbSqlComparison` or `OINODbSqlBooleanOperation`
45
53
  * @param rightSide right side of the filter, either another filter or a value
46
54
  */
47
- constructor(leftSide:OINODbSqlFilter|string, operation:OINODbSqlComparison|OINODbSqlBooleanOperation|null, rightSide:OINODbSqlFilter|string) {
55
+ constructor(leftSide:OINODbSqlFilter|string, operation:OINODbSqlComparison|OINODbSqlBooleanOperation|OINODbSqlNullCheck|null, rightSide:OINODbSqlFilter|string) {
48
56
  if (!(
49
57
  ((operation === null) && (leftSide == "") && (rightSide == "")) ||
50
58
  ((operation !== null) && (Object.values(OINODbSqlComparison).includes(operation as OINODbSqlComparison)) && (typeof(leftSide) == "string") && (leftSide != "") && (typeof(rightSide) == "string") && (rightSide != "")) ||
51
59
  ((operation == OINODbSqlBooleanOperation.not) && (leftSide == "") && (rightSide instanceof OINODbSqlFilter)) ||
60
+ (((operation == OINODbSqlNullCheck.isnull) || (operation == OINODbSqlNullCheck.isnotnull)) && (typeof(leftSide) == "string") && (rightSide == "")) ||
52
61
  (((operation == OINODbSqlBooleanOperation.and) || (operation == OINODbSqlBooleanOperation.or)) && (leftSide instanceof OINODbSqlFilter) && (rightSide instanceof OINODbSqlFilter))
53
62
  )) {
54
63
  OINOLog.error("@oino-ts/db", "OINODbSqlFilter", "constructor", "Unsupported OINODbSqlFilter format", {leftSide:leftSide, operation:operation, rightSide:rightSide})
@@ -66,6 +75,7 @@ export class OINODbSqlFilter {
66
75
  * - comparison: (field)-lt|le|eq|ge|gt|like(value)
67
76
  * - negation: -not(filter)
68
77
  * - conjunction/disjunction: (filter)-and|or(filter)
78
+ * - null check: -isnull(field) or -isnotnull(field)
69
79
  *
70
80
  * @param filterString string representation of filter from HTTP-request
71
81
  *
@@ -76,20 +86,28 @@ export class OINODbSqlFilter {
76
86
 
77
87
  } else {
78
88
  let match = OINODbSqlFilter._filterComparisonRegex.exec(filterString)
79
- if (match != null) {
89
+ if ((match != null) && (match.length == 4)) {
80
90
  return new OINODbSqlFilter(match[1], match[2].toLowerCase() as OINODbSqlComparison, match[3])
91
+
81
92
  } else {
82
93
  let match = OINODbSqlFilter._negationRegex.exec(filterString)
83
94
  if (match != null) {
84
95
  return new OINODbSqlFilter("", OINODbSqlBooleanOperation.not, OINODbSqlFilter.parse(match[3]))
96
+
85
97
  } else {
86
98
  let boolean_parts = OINOStr.splitByBrackets(filterString, true, false, '(', ')')
87
99
  if (boolean_parts.length == 3 && (boolean_parts[1].match(OINODbSqlFilter._booleanOperationRegex))) {
88
100
  return new OINODbSqlFilter(OINODbSqlFilter.parse(boolean_parts[0]), boolean_parts[1].trim().toLowerCase().substring(1) as OINODbSqlBooleanOperation, OINODbSqlFilter.parse(boolean_parts[2]))
89
-
101
+
90
102
  } else {
91
- OINOLog.error("@oino-ts/db", "OINODbSqlFilter", "constructor", "Invalid filter", {filterString:filterString})
92
- throw new Error(OINO_ERROR_PREFIX + ": Invalid filter '" + filterString + "'") // invalid filter could be a security risk, stop processing
103
+ let match = OINODbSqlFilter._filterNullCheckRegex.exec(filterString)
104
+ if ((match != null)) {
105
+ return new OINODbSqlFilter(match[2], match[1].toLowerCase() as OINODbSqlComparison, "")
106
+
107
+ } else {
108
+ OINOLog.error("@oino-ts/db", "OINODbSqlFilter", "constructor", "Invalid filter", {filterString:filterString})
109
+ throw new Error(OINO_ERROR_PREFIX + ": Invalid filter '" + filterString + "'") // invalid filter could be a security risk, stop processing
110
+ }
93
111
  }
94
112
  }
95
113
  }
@@ -119,6 +137,53 @@ export class OINODbSqlFilter {
119
137
  }
120
138
  }
121
139
 
140
+ /**
141
+ * Combine two filters with an AND operation.
142
+ *
143
+ * @param leftSide left side filter
144
+ * @param rightSide right side filter
145
+ *
146
+ */
147
+ static and(leftSide:OINODbSqlFilter, rightSide:OINODbSqlFilter):OINODbSqlFilter|undefined {
148
+ if ((leftSide) && (!leftSide.isEmpty()) && (rightSide) && (!rightSide.isEmpty())) {
149
+ return new OINODbSqlFilter(leftSide, OINODbSqlBooleanOperation.and, rightSide)
150
+
151
+ } else {
152
+ return undefined
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Combine two filters with an OR operation.
158
+ *
159
+ * @param leftSide left side filter
160
+ * @param rightSide right side filter
161
+ *
162
+ */
163
+ static or(leftSide:OINODbSqlFilter, rightSide:OINODbSqlFilter):OINODbSqlFilter|undefined {
164
+ if ((leftSide) && (!leftSide.isEmpty()) && (rightSide) && (!rightSide.isEmpty())) {
165
+ return new OINODbSqlFilter(leftSide, OINODbSqlBooleanOperation.or, rightSide)
166
+
167
+ } else {
168
+ return undefined
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Negate a filter with a NOT operation.
174
+ *
175
+ * @param leftSide left side filter
176
+ *
177
+ */
178
+ static not(leftSide:OINODbSqlFilter):OINODbSqlFilter|undefined {
179
+ if ((leftSide) && (!leftSide.isEmpty())) {
180
+ return new OINODbSqlFilter(leftSide, OINODbSqlBooleanOperation.not, "")
181
+
182
+ } else {
183
+ return undefined
184
+ }
185
+ }
186
+
122
187
 
123
188
  private _operatorToSql():string {
124
189
  switch (this._operator) {
@@ -128,9 +193,12 @@ export class OINODbSqlFilter {
128
193
  case "lt": return " < "
129
194
  case "le": return " <= "
130
195
  case "eq": return " = "
196
+ case "ne": return " != "
131
197
  case "ge": return " >= "
132
198
  case "gt": return " > "
133
199
  case "like": return " LIKE "
200
+ case "isnull": return " IS NULL"
201
+ case "isnotnull": return " IS NOT NULL"
134
202
  }
135
203
  return " "
136
204
  }
@@ -168,6 +236,8 @@ export class OINODbSqlFilter {
168
236
  result += this._operatorToSql()
169
237
  if (this._rightSide instanceof OINODbSqlFilter) {
170
238
  result += this._rightSide.toSql(dataModel)
239
+ } else if (this._operator == OINODbSqlNullCheck.isnull || this._operator == OINODbSqlNullCheck.isnotnull) {
240
+ // nothing to do, IS NULL and IS NOT NULL do not have a right side
171
241
  } else {
172
242
  const value = field!.deserializeCell(this._rightSide)
173
243
  if ((value == null) || (value === "")) {