@budibase/backend-core 2.29.28 → 2.29.29

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.
@@ -38,7 +38,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
38
38
  const knex_1 = require("knex");
39
39
  const dbCore = __importStar(require("../db"));
40
40
  const utils_1 = require("./utils");
41
- const sqlStatements_1 = require("./sqlStatements");
42
41
  const sqlTable_1 = __importDefault(require("./sqlTable"));
43
42
  const types_1 = require("@budibase/types");
44
43
  const environment_1 = __importDefault(require("../environment"));
@@ -49,126 +48,6 @@ function getBaseLimit() {
49
48
  : null;
50
49
  return envLimit || 5000;
51
50
  }
52
- // Takes a string like foo and returns a quoted string like [foo] for SQL Server
53
- // and "foo" for Postgres.
54
- function quote(client, str) {
55
- switch (client) {
56
- case types_1.SqlClient.SQL_LITE:
57
- case types_1.SqlClient.ORACLE:
58
- case types_1.SqlClient.POSTGRES:
59
- return `"${str}"`;
60
- case types_1.SqlClient.MS_SQL:
61
- return `[${str}]`;
62
- case types_1.SqlClient.MY_SQL:
63
- return `\`${str}\``;
64
- }
65
- }
66
- // Takes a string like a.b.c and returns a quoted identifier like [a].[b].[c]
67
- // for SQL Server and `a`.`b`.`c` for MySQL.
68
- function quotedIdentifier(client, key) {
69
- return key
70
- .split(".")
71
- .map(part => quote(client, part))
72
- .join(".");
73
- }
74
- function parse(input) {
75
- if (Array.isArray(input)) {
76
- return JSON.stringify(input);
77
- }
78
- if (input == undefined) {
79
- return null;
80
- }
81
- if (typeof input !== "string") {
82
- return input;
83
- }
84
- if ((0, utils_1.isInvalidISODateString)(input)) {
85
- return null;
86
- }
87
- if ((0, utils_1.isValidISODateString)(input)) {
88
- return new Date(input.trim());
89
- }
90
- return input;
91
- }
92
- function parseBody(body) {
93
- for (let [key, value] of Object.entries(body)) {
94
- body[key] = parse(value);
95
- }
96
- return body;
97
- }
98
- function parseFilters(filters) {
99
- if (!filters) {
100
- return {};
101
- }
102
- for (let [key, value] of Object.entries(filters)) {
103
- let parsed;
104
- if (typeof value === "object") {
105
- parsed = parseFilters(value);
106
- }
107
- else {
108
- parsed = parse(value);
109
- }
110
- // @ts-ignore
111
- filters[key] = parsed;
112
- }
113
- return filters;
114
- }
115
- function generateSelectStatement(json, knex) {
116
- const { resource, meta } = json;
117
- const client = knex.client.config.client;
118
- if (!resource || !resource.fields || resource.fields.length === 0) {
119
- return "*";
120
- }
121
- const schema = meta.table.schema;
122
- return resource.fields.map(field => {
123
- var _a;
124
- const parts = field.split(/\./g);
125
- let table = undefined;
126
- let column = undefined;
127
- // Just a column name, e.g.: "column"
128
- if (parts.length === 1) {
129
- column = parts[0];
130
- }
131
- // A table name and a column name, e.g.: "table.column"
132
- if (parts.length === 2) {
133
- table = parts[0];
134
- column = parts[1];
135
- }
136
- // A link doc, e.g.: "table.doc1.fieldName"
137
- if (parts.length > 2) {
138
- table = parts[0];
139
- column = parts.slice(1).join(".");
140
- }
141
- if (!column) {
142
- throw new Error(`Invalid field name: ${field}`);
143
- }
144
- const columnSchema = schema[column];
145
- if (client === types_1.SqlClient.POSTGRES &&
146
- ((_a = columnSchema === null || columnSchema === void 0 ? void 0 : columnSchema.externalType) === null || _a === void 0 ? void 0 : _a.includes("money"))) {
147
- return knex.raw(`${quotedIdentifier(client, [table, column].join("."))}::money::numeric as ${quote(client, field)}`);
148
- }
149
- if (client === types_1.SqlClient.MS_SQL &&
150
- (columnSchema === null || columnSchema === void 0 ? void 0 : columnSchema.type) === types_1.FieldType.DATETIME &&
151
- columnSchema.timeOnly) {
152
- // Time gets returned as timestamp from mssql, not matching the expected
153
- // HH:mm format
154
- return knex.raw(`CONVERT(varchar, ${field}, 108) as "${field}"`);
155
- }
156
- // There's at least two edge cases being handled in the expression below.
157
- // 1. The column name could start/end with a space, and in that case we
158
- // want to preseve that space.
159
- // 2. Almost all column names are specified in the form table.column, except
160
- // in the case of relationships, where it's table.doc1.column. In that
161
- // case, we want to split it into `table`.`doc1.column` for reasons that
162
- // aren't actually clear to me, but `table`.`doc1` breaks things with the
163
- // sample data tests.
164
- if (table) {
165
- return knex.raw(`${quote(client, table)}.${quote(client, column)} as ${quote(client, field)}`);
166
- }
167
- else {
168
- return knex.raw(`${quote(client, field)} as ${quote(client, field)}`);
169
- }
170
- });
171
- }
172
51
  function getTableName(table) {
173
52
  // SQS uses the table ID rather than the table name
174
53
  if ((table === null || table === void 0 ? void 0 : table.sourceType) === types_1.TableSourceType.INTERNAL ||
@@ -196,26 +75,226 @@ function convertBooleans(query) {
196
75
  return query;
197
76
  }
198
77
  class InternalBuilder {
199
- constructor(client) {
78
+ constructor(client, knex, query) {
200
79
  this.client = client;
80
+ this.query = query;
81
+ this.knex = knex;
82
+ this.splitter = new shared_core_1.dataFilters.ColumnSplitter([this.table], {
83
+ aliases: this.query.tableAliases,
84
+ columnPrefix: this.query.meta.columnPrefix,
85
+ });
86
+ }
87
+ get table() {
88
+ return this.query.meta.table;
89
+ }
90
+ getFieldSchema(key) {
91
+ const { column } = this.splitter.run(key);
92
+ return this.table.schema[column];
93
+ }
94
+ // Takes a string like foo and returns a quoted string like [foo] for SQL Server
95
+ // and "foo" for Postgres.
96
+ quote(str) {
97
+ switch (this.client) {
98
+ case types_1.SqlClient.SQL_LITE:
99
+ case types_1.SqlClient.ORACLE:
100
+ case types_1.SqlClient.POSTGRES:
101
+ return `"${str}"`;
102
+ case types_1.SqlClient.MS_SQL:
103
+ return `[${str}]`;
104
+ case types_1.SqlClient.MY_SQL:
105
+ return `\`${str}\``;
106
+ }
107
+ }
108
+ // Takes a string like a.b.c and returns a quoted identifier like [a].[b].[c]
109
+ // for SQL Server and `a`.`b`.`c` for MySQL.
110
+ quotedIdentifier(key) {
111
+ return key
112
+ .split(".")
113
+ .map(part => this.quote(part))
114
+ .join(".");
115
+ }
116
+ generateSelectStatement() {
117
+ const { resource, meta } = this.query;
118
+ if (!resource || !resource.fields || resource.fields.length === 0) {
119
+ return "*";
120
+ }
121
+ const schema = meta.table.schema;
122
+ return resource.fields.map(field => {
123
+ var _a;
124
+ const parts = field.split(/\./g);
125
+ let table = undefined;
126
+ let column = undefined;
127
+ // Just a column name, e.g.: "column"
128
+ if (parts.length === 1) {
129
+ column = parts[0];
130
+ }
131
+ // A table name and a column name, e.g.: "table.column"
132
+ if (parts.length === 2) {
133
+ table = parts[0];
134
+ column = parts[1];
135
+ }
136
+ // A link doc, e.g.: "table.doc1.fieldName"
137
+ if (parts.length > 2) {
138
+ table = parts[0];
139
+ column = parts.slice(1).join(".");
140
+ }
141
+ if (!column) {
142
+ throw new Error(`Invalid field name: ${field}`);
143
+ }
144
+ const columnSchema = schema[column];
145
+ if (this.client === types_1.SqlClient.POSTGRES &&
146
+ ((_a = columnSchema === null || columnSchema === void 0 ? void 0 : columnSchema.externalType) === null || _a === void 0 ? void 0 : _a.includes("money"))) {
147
+ return this.knex.raw(`${this.quotedIdentifier([table, column].join("."))}::money::numeric as ${this.quote(field)}`);
148
+ }
149
+ if (this.client === types_1.SqlClient.MS_SQL &&
150
+ (columnSchema === null || columnSchema === void 0 ? void 0 : columnSchema.type) === types_1.FieldType.DATETIME &&
151
+ columnSchema.timeOnly) {
152
+ // Time gets returned as timestamp from mssql, not matching the expected
153
+ // HH:mm format
154
+ return this.knex.raw(`CONVERT(varchar, ${field}, 108) as "${field}"`);
155
+ }
156
+ // There's at least two edge cases being handled in the expression below.
157
+ // 1. The column name could start/end with a space, and in that case we
158
+ // want to preseve that space.
159
+ // 2. Almost all column names are specified in the form table.column, except
160
+ // in the case of relationships, where it's table.doc1.column. In that
161
+ // case, we want to split it into `table`.`doc1.column` for reasons that
162
+ // aren't actually clear to me, but `table`.`doc1` breaks things with the
163
+ // sample data tests.
164
+ if (table) {
165
+ return this.knex.raw(`${this.quote(table)}.${this.quote(column)} as ${this.quote(field)}`);
166
+ }
167
+ else {
168
+ return this.knex.raw(`${this.quote(field)} as ${this.quote(field)}`);
169
+ }
170
+ });
171
+ }
172
+ // OracleDB can't use character-large-objects (CLOBs) in WHERE clauses,
173
+ // so when we use them we need to wrap them in to_char(). This function
174
+ // converts a field name to the appropriate identifier.
175
+ convertClobs(field) {
176
+ const parts = field.split(".");
177
+ const col = parts.pop();
178
+ const schema = this.table.schema[col];
179
+ let identifier = this.quotedIdentifier(field);
180
+ if (schema.type === types_1.FieldType.STRING ||
181
+ schema.type === types_1.FieldType.LONGFORM ||
182
+ schema.type === types_1.FieldType.BB_REFERENCE_SINGLE ||
183
+ schema.type === types_1.FieldType.BB_REFERENCE ||
184
+ schema.type === types_1.FieldType.OPTIONS ||
185
+ schema.type === types_1.FieldType.BARCODEQR) {
186
+ identifier = `to_char(${identifier})`;
187
+ }
188
+ return identifier;
189
+ }
190
+ parse(input, schema) {
191
+ if (Array.isArray(input)) {
192
+ return JSON.stringify(input);
193
+ }
194
+ if (input == undefined) {
195
+ return null;
196
+ }
197
+ if (this.client === types_1.SqlClient.ORACLE &&
198
+ schema.type === types_1.FieldType.DATETIME &&
199
+ schema.timeOnly) {
200
+ if (input instanceof Date) {
201
+ const hours = input.getHours().toString().padStart(2, "0");
202
+ const minutes = input.getMinutes().toString().padStart(2, "0");
203
+ const seconds = input.getSeconds().toString().padStart(2, "0");
204
+ return `${hours}:${minutes}:${seconds}`;
205
+ }
206
+ if (typeof input === "string") {
207
+ return new Date(`1970-01-01T${input}Z`);
208
+ }
209
+ }
210
+ if (typeof input === "string") {
211
+ if ((0, utils_1.isInvalidISODateString)(input)) {
212
+ return null;
213
+ }
214
+ if ((0, utils_1.isValidISODateString)(input)) {
215
+ return new Date(input.trim());
216
+ }
217
+ }
218
+ return input;
219
+ }
220
+ parseBody(body) {
221
+ for (let [key, value] of Object.entries(body)) {
222
+ const { column } = this.splitter.run(key);
223
+ const schema = this.table.schema[column];
224
+ if (!schema) {
225
+ continue;
226
+ }
227
+ body[key] = this.parse(value, schema);
228
+ }
229
+ return body;
230
+ }
231
+ parseFilters(filters) {
232
+ for (const op of Object.values(types_1.BasicOperator)) {
233
+ const filter = filters[op];
234
+ if (!filter) {
235
+ continue;
236
+ }
237
+ for (const key of Object.keys(filter)) {
238
+ if (Array.isArray(filter[key])) {
239
+ filter[key] = JSON.stringify(filter[key]);
240
+ continue;
241
+ }
242
+ const { column } = this.splitter.run(key);
243
+ const schema = this.table.schema[column];
244
+ if (!schema) {
245
+ continue;
246
+ }
247
+ filter[key] = this.parse(filter[key], schema);
248
+ }
249
+ }
250
+ for (const op of Object.values(types_1.ArrayOperator)) {
251
+ const filter = filters[op];
252
+ if (!filter) {
253
+ continue;
254
+ }
255
+ for (const key of Object.keys(filter)) {
256
+ const { column } = this.splitter.run(key);
257
+ const schema = this.table.schema[column];
258
+ if (!schema) {
259
+ continue;
260
+ }
261
+ filter[key] = filter[key].map(v => this.parse(v, schema));
262
+ }
263
+ }
264
+ for (const op of Object.values(types_1.RangeOperator)) {
265
+ const filter = filters[op];
266
+ if (!filter) {
267
+ continue;
268
+ }
269
+ for (const key of Object.keys(filter)) {
270
+ const { column } = this.splitter.run(key);
271
+ const schema = this.table.schema[column];
272
+ if (!schema) {
273
+ continue;
274
+ }
275
+ const value = filter[key];
276
+ if ("low" in value) {
277
+ value.low = this.parse(value.low, schema);
278
+ }
279
+ if ("high" in value) {
280
+ value.high = this.parse(value.high, schema);
281
+ }
282
+ }
283
+ }
284
+ return filters;
201
285
  }
202
286
  // right now we only do filters on the specific table being queried
203
- addFilters(query, filters, table, opts) {
204
- var _a;
287
+ addFilters(query, filters, opts) {
205
288
  if (!filters) {
206
289
  return query;
207
290
  }
208
- filters = parseFilters(filters);
291
+ filters = this.parseFilters(filters);
292
+ const aliases = this.query.tableAliases;
209
293
  // if all or specified in filters, then everything is an or
210
294
  const allOr = filters.allOr;
211
- const sqlStatements = new sqlStatements_1.SqlStatements(this.client, table, {
212
- allOr,
213
- columnPrefix: opts.columnPrefix,
214
- });
215
- const tableName = this.client === types_1.SqlClient.SQL_LITE ? table._id : table.name;
295
+ const tableName = this.client === types_1.SqlClient.SQL_LITE ? this.table._id : this.table.name;
216
296
  function getTableAlias(name) {
217
- var _a;
218
- const alias = (_a = opts.aliases) === null || _a === void 0 ? void 0 : _a[name];
297
+ const alias = aliases === null || aliases === void 0 ? void 0 : aliases[name];
219
298
  return alias || name;
220
299
  }
221
300
  function iterate(structure, fn, complexKeyFn) {
@@ -230,11 +309,11 @@ class InternalBuilder {
230
309
  const alias = getTableAlias(tableName);
231
310
  complexKeyFn(castedTypeValue.id.map((x) => alias ? `${alias}.${x}` : x), castedTypeValue.values);
232
311
  }
233
- else if (!opts.relationship && !isRelationshipField) {
312
+ else if (!(opts === null || opts === void 0 ? void 0 : opts.relationship) && !isRelationshipField) {
234
313
  const alias = getTableAlias(tableName);
235
314
  fn(alias ? `${alias}.${updatedKey}` : updatedKey, value);
236
315
  }
237
- else if (opts.relationship && isRelationshipField) {
316
+ else if ((opts === null || opts === void 0 ? void 0 : opts.relationship) && isRelationshipField) {
238
317
  const [filterTableName, property] = updatedKey.split(".");
239
318
  const alias = getTableAlias(filterTableName);
240
319
  fn(alias ? `${alias}.${property}` : property, value);
@@ -251,7 +330,9 @@ class InternalBuilder {
251
330
  else {
252
331
  const rawFnc = `${fnc}Raw`;
253
332
  // @ts-ignore
254
- query = query[rawFnc](`LOWER(${quotedIdentifier(this.client, key)}) LIKE ?`, [`%${value.toLowerCase()}%`]);
333
+ query = query[rawFnc](`LOWER(${this.quotedIdentifier(key)}) LIKE ?`, [
334
+ `%${value.toLowerCase()}%`,
335
+ ]);
255
336
  }
256
337
  };
257
338
  const contains = (mode, any = false) => {
@@ -285,6 +366,7 @@ class InternalBuilder {
285
366
  const andOr = mode === (filters === null || filters === void 0 ? void 0 : filters.containsAny) ? " OR " : " AND ";
286
367
  iterate(mode, (key, value) => {
287
368
  let statement = "";
369
+ const identifier = this.quotedIdentifier(key);
288
370
  for (let i in value) {
289
371
  if (typeof value[i] === "string") {
290
372
  value[i] = `%"${value[i].toLowerCase()}"%`;
@@ -292,24 +374,43 @@ class InternalBuilder {
292
374
  else {
293
375
  value[i] = `%${value[i]}%`;
294
376
  }
295
- statement +=
296
- (statement ? andOr : "") +
297
- `COALESCE(LOWER(${quotedIdentifier(this.client, key)}), '') LIKE ?`;
377
+ statement += `${statement ? andOr : ""}COALESCE(LOWER(${identifier}), '') LIKE ?`;
298
378
  }
299
379
  if (statement === "") {
300
380
  return;
301
381
  }
302
- // @ts-ignore
303
- query = query[rawFnc](`${not}(${statement})`, value);
382
+ if (not) {
383
+ query = query[rawFnc](`(NOT (${statement}) OR ${identifier} IS NULL)`, value);
384
+ }
385
+ else {
386
+ query = query[rawFnc](statement, value);
387
+ }
304
388
  });
305
389
  }
306
390
  };
307
391
  if (filters.oneOf) {
308
392
  const fnc = allOr ? "orWhereIn" : "whereIn";
309
393
  iterate(filters.oneOf, (key, array) => {
310
- query = query[fnc](key, Array.isArray(array) ? array : [array]);
394
+ if (this.client === types_1.SqlClient.ORACLE) {
395
+ key = this.convertClobs(key);
396
+ array = Array.isArray(array) ? array : [array];
397
+ const binding = new Array(array.length).fill("?").join(",");
398
+ query = query.whereRaw(`${key} IN (${binding})`, array);
399
+ }
400
+ else {
401
+ query = query[fnc](key, Array.isArray(array) ? array : [array]);
402
+ }
311
403
  }, (key, array) => {
312
- query = query[fnc](key, Array.isArray(array) ? array : [array]);
404
+ if (this.client === types_1.SqlClient.ORACLE) {
405
+ const keyStr = `(${key.map(k => this.convertClobs(k)).join(",")})`;
406
+ const binding = `(${array
407
+ .map((a) => `(${new Array(a.length).fill("?").join(",")})`)
408
+ .join(",")})`;
409
+ query = query.whereRaw(`${keyStr} IN ${binding}`, array.flat());
410
+ }
411
+ else {
412
+ query = query[fnc](key, Array.isArray(array) ? array : [array]);
413
+ }
313
414
  });
314
415
  }
315
416
  if (filters.string) {
@@ -322,7 +423,9 @@ class InternalBuilder {
322
423
  else {
323
424
  const rawFnc = `${fnc}Raw`;
324
425
  // @ts-ignore
325
- query = query[rawFnc](`LOWER(${quotedIdentifier(this.client, key)}) LIKE ?`, [`${value.toLowerCase()}%`]);
426
+ query = query[rawFnc](`LOWER(${this.quotedIdentifier(key)}) LIKE ?`, [
427
+ `${value.toLowerCase()}%`,
428
+ ]);
326
429
  }
327
430
  });
328
431
  }
@@ -343,14 +446,40 @@ class InternalBuilder {
343
446
  value.high = "";
344
447
  }
345
448
  const lowValid = (0, utils_1.isValidFilter)(value.low), highValid = (0, utils_1.isValidFilter)(value.high);
449
+ const schema = this.getFieldSchema(key);
450
+ if (this.client === types_1.SqlClient.ORACLE) {
451
+ // @ts-ignore
452
+ key = this.knex.raw(this.convertClobs(key));
453
+ }
346
454
  if (lowValid && highValid) {
347
- query = sqlStatements.between(query, key, value.low, value.high);
455
+ if ((schema === null || schema === void 0 ? void 0 : schema.type) === types_1.FieldType.BIGINT &&
456
+ this.client === types_1.SqlClient.SQL_LITE) {
457
+ query = query.whereRaw(`CAST(${key} AS INTEGER) BETWEEN CAST(? AS INTEGER) AND CAST(? AS INTEGER)`, [value.low, value.high]);
458
+ }
459
+ else {
460
+ const fnc = allOr ? "orWhereBetween" : "whereBetween";
461
+ query = query[fnc](key, [value.low, value.high]);
462
+ }
348
463
  }
349
464
  else if (lowValid) {
350
- query = sqlStatements.lte(query, key, value.low);
465
+ if ((schema === null || schema === void 0 ? void 0 : schema.type) === types_1.FieldType.BIGINT &&
466
+ this.client === types_1.SqlClient.SQL_LITE) {
467
+ query = query.whereRaw(`CAST(${key} AS INTEGER) >= CAST(? AS INTEGER)`, [value.low]);
468
+ }
469
+ else {
470
+ const fnc = allOr ? "orWhere" : "where";
471
+ query = query[fnc](key, ">=", value.low);
472
+ }
351
473
  }
352
474
  else if (highValid) {
353
- query = sqlStatements.gte(query, key, value.high);
475
+ if ((schema === null || schema === void 0 ? void 0 : schema.type) === types_1.FieldType.BIGINT &&
476
+ this.client === types_1.SqlClient.SQL_LITE) {
477
+ query = query.whereRaw(`CAST(${key} AS INTEGER) <= CAST(? AS INTEGER)`, [value.high]);
478
+ }
479
+ else {
480
+ const fnc = allOr ? "orWhere" : "where";
481
+ query = query[fnc](key, "<=", value.high);
482
+ }
354
483
  }
355
484
  });
356
485
  }
@@ -358,13 +487,14 @@ class InternalBuilder {
358
487
  iterate(filters.equal, (key, value) => {
359
488
  const fnc = allOr ? "orWhereRaw" : "whereRaw";
360
489
  if (this.client === types_1.SqlClient.MS_SQL) {
361
- query = query[fnc](`CASE WHEN ${quotedIdentifier(this.client, key)} = ? THEN 1 ELSE 0 END = 1`, [value]);
490
+ query = query[fnc](`CASE WHEN ${this.quotedIdentifier(key)} = ? THEN 1 ELSE 0 END = 1`, [value]);
362
491
  }
363
492
  else if (this.client === types_1.SqlClient.ORACLE) {
364
- query = query[fnc](`COALESCE(${quotedIdentifier(this.client, key)}, -1) = ?`, [value]);
493
+ const identifier = this.convertClobs(key);
494
+ query = query[fnc](`(${identifier} IS NOT NULL AND ${identifier} = ?)`, [value]);
365
495
  }
366
496
  else {
367
- query = query[fnc](`COALESCE(${quotedIdentifier(this.client, key)} = ?, FALSE)`, [value]);
497
+ query = query[fnc](`COALESCE(${this.quotedIdentifier(key)} = ?, FALSE)`, [value]);
368
498
  }
369
499
  });
370
500
  }
@@ -372,13 +502,14 @@ class InternalBuilder {
372
502
  iterate(filters.notEqual, (key, value) => {
373
503
  const fnc = allOr ? "orWhereRaw" : "whereRaw";
374
504
  if (this.client === types_1.SqlClient.MS_SQL) {
375
- query = query[fnc](`CASE WHEN ${quotedIdentifier(this.client, key)} = ? THEN 1 ELSE 0 END = 0`, [value]);
505
+ query = query[fnc](`CASE WHEN ${this.quotedIdentifier(key)} = ? THEN 1 ELSE 0 END = 0`, [value]);
376
506
  }
377
507
  else if (this.client === types_1.SqlClient.ORACLE) {
378
- query = query[fnc](`COALESCE(${quotedIdentifier(this.client, key)}, -1) != ?`, [value]);
508
+ const identifier = this.convertClobs(key);
509
+ query = query[fnc](`(${identifier} IS NOT NULL AND ${identifier} != ?) OR ${identifier} IS NULL`, [value]);
379
510
  }
380
511
  else {
381
- query = query[fnc](`COALESCE(${quotedIdentifier(this.client, key)} != ?, TRUE)`, [value]);
512
+ query = query[fnc](`COALESCE(${this.quotedIdentifier(key)} != ?, TRUE)`, [value]);
382
513
  }
383
514
  });
384
515
  }
@@ -403,43 +534,50 @@ class InternalBuilder {
403
534
  if (filters.containsAny) {
404
535
  contains(filters.containsAny, true);
405
536
  }
406
- const tableRef = ((_a = opts === null || opts === void 0 ? void 0 : opts.aliases) === null || _a === void 0 ? void 0 : _a[table._id]) || table._id;
537
+ const tableRef = (aliases === null || aliases === void 0 ? void 0 : aliases[this.table._id]) || this.table._id;
407
538
  // when searching internal tables make sure long looking for rows
408
- if (filters.documentType && !(0, utils_1.isExternalTable)(table) && tableRef) {
539
+ if (filters.documentType && !(0, utils_1.isExternalTable)(this.table) && tableRef) {
409
540
  // has to be its own option, must always be AND onto the search
410
541
  query.andWhereLike(`${tableRef}._id`, `${(0, types_1.prefixed)(filters.documentType)}%`);
411
542
  }
412
543
  return query;
413
544
  }
414
- addDistinctCount(query, json) {
415
- const table = json.meta.table;
416
- const primary = table.primary;
417
- const aliases = json.tableAliases;
418
- const aliased = table.name && (aliases === null || aliases === void 0 ? void 0 : aliases[table.name]) ? aliases[table.name] : table.name;
545
+ addDistinctCount(query) {
546
+ const primary = this.table.primary;
547
+ const aliases = this.query.tableAliases;
548
+ const aliased = this.table.name && (aliases === null || aliases === void 0 ? void 0 : aliases[this.table.name])
549
+ ? aliases[this.table.name]
550
+ : this.table.name;
419
551
  if (!primary) {
420
552
  throw new Error("SQL counting requires primary key to be supplied");
421
553
  }
422
554
  return query.countDistinct(`${aliased}.${primary[0]} as total`);
423
555
  }
424
- addSorting(query, json) {
425
- let { sort } = json;
426
- const table = json.meta.table;
427
- const primaryKey = table.primary;
428
- const tableName = getTableName(table);
429
- const aliases = json.tableAliases;
430
- const aliased = tableName && (aliases === null || aliases === void 0 ? void 0 : aliases[tableName]) ? aliases[tableName] : table === null || table === void 0 ? void 0 : table.name;
556
+ addSorting(query) {
557
+ var _a;
558
+ let { sort } = this.query;
559
+ const primaryKey = this.table.primary;
560
+ const tableName = getTableName(this.table);
561
+ const aliases = this.query.tableAliases;
562
+ const aliased = tableName && (aliases === null || aliases === void 0 ? void 0 : aliases[tableName]) ? aliases[tableName] : (_a = this.table) === null || _a === void 0 ? void 0 : _a.name;
431
563
  if (!Array.isArray(primaryKey)) {
432
564
  throw new Error("Sorting requires primary key to be specified for table");
433
565
  }
434
566
  if (sort && Object.keys(sort || {}).length > 0) {
435
567
  for (let [key, value] of Object.entries(sort)) {
436
568
  const direction = value.direction === types_1.SortOrder.ASCENDING ? "asc" : "desc";
437
- let nulls;
438
- if (this.client === types_1.SqlClient.POSTGRES) {
439
- // All other clients already sort this as expected by default, and adding this to the rest of the clients is causing issues
569
+ let nulls = undefined;
570
+ if (this.client === types_1.SqlClient.POSTGRES ||
571
+ this.client === types_1.SqlClient.ORACLE) {
440
572
  nulls = value.direction === types_1.SortOrder.ASCENDING ? "first" : "last";
441
573
  }
442
- query = query.orderBy(`${aliased}.${key}`, direction, nulls);
574
+ let composite = `${aliased}.${key}`;
575
+ if (this.client === types_1.SqlClient.ORACLE) {
576
+ query = query.orderByRaw(`${this.convertClobs(composite)} ${direction} nulls ${nulls}`);
577
+ }
578
+ else {
579
+ query = query.orderBy(composite, direction, nulls);
580
+ }
443
581
  }
444
582
  }
445
583
  // add sorting by the primary key if the result isn't already sorted by it,
@@ -522,22 +660,47 @@ class InternalBuilder {
522
660
  }
523
661
  return query;
524
662
  }
525
- knexWithAlias(knex, endpoint, aliases) {
526
- const tableName = endpoint.entityId;
527
- const tableAlias = aliases === null || aliases === void 0 ? void 0 : aliases[tableName];
528
- return knex(this.tableNameWithSchema(tableName, {
529
- alias: tableAlias,
530
- schema: endpoint.schema,
663
+ qualifiedKnex(opts) {
664
+ var _a;
665
+ let alias = (_a = this.query.tableAliases) === null || _a === void 0 ? void 0 : _a[this.query.endpoint.entityId];
666
+ if ((opts === null || opts === void 0 ? void 0 : opts.alias) === false) {
667
+ alias = undefined;
668
+ }
669
+ else if (typeof (opts === null || opts === void 0 ? void 0 : opts.alias) === "string") {
670
+ alias = opts.alias;
671
+ }
672
+ return this.knex(this.tableNameWithSchema(this.query.endpoint.entityId, {
673
+ alias,
674
+ schema: this.query.endpoint.schema,
531
675
  }));
532
676
  }
533
- create(knex, json, opts) {
534
- const { endpoint, body } = json;
535
- let query = this.knexWithAlias(knex, endpoint);
536
- const parsedBody = parseBody(body);
537
- // make sure no null values in body for creation
538
- for (let [key, value] of Object.entries(parsedBody)) {
539
- if (value == null) {
540
- delete parsedBody[key];
677
+ create(opts) {
678
+ var _a;
679
+ const { body } = this.query;
680
+ let query = this.qualifiedKnex({ alias: false });
681
+ const parsedBody = this.parseBody(body);
682
+ if (this.client === types_1.SqlClient.ORACLE) {
683
+ // Oracle doesn't seem to automatically insert nulls
684
+ // if we don't specify them, so we need to do that here
685
+ for (const [column, schema] of Object.entries(this.query.meta.table.schema)) {
686
+ if (((_a = schema.constraints) === null || _a === void 0 ? void 0 : _a.presence) === true ||
687
+ schema.type === types_1.FieldType.FORMULA ||
688
+ schema.type === types_1.FieldType.AUTO ||
689
+ schema.type === types_1.FieldType.LINK) {
690
+ continue;
691
+ }
692
+ const value = parsedBody[column];
693
+ if (value == null) {
694
+ parsedBody[column] = null;
695
+ }
696
+ }
697
+ }
698
+ else {
699
+ // make sure no null values in body for creation
700
+ for (let [key, value] of Object.entries(parsedBody)) {
701
+ if (value == null) {
702
+ delete parsedBody[key];
703
+ }
541
704
  }
542
705
  }
543
706
  // mysql can't use returning
@@ -548,46 +711,47 @@ class InternalBuilder {
548
711
  return query.insert(parsedBody).returning("*");
549
712
  }
550
713
  }
551
- bulkCreate(knex, json) {
552
- const { endpoint, body } = json;
553
- let query = this.knexWithAlias(knex, endpoint);
714
+ bulkCreate() {
715
+ const { body } = this.query;
716
+ let query = this.qualifiedKnex({ alias: false });
554
717
  if (!Array.isArray(body)) {
555
718
  return query;
556
719
  }
557
- const parsedBody = body.map(row => parseBody(row));
720
+ const parsedBody = body.map(row => this.parseBody(row));
558
721
  return query.insert(parsedBody);
559
722
  }
560
- bulkUpsert(knex, json) {
561
- const { endpoint, body } = json;
562
- let query = this.knexWithAlias(knex, endpoint);
723
+ bulkUpsert() {
724
+ const { body } = this.query;
725
+ let query = this.qualifiedKnex({ alias: false });
563
726
  if (!Array.isArray(body)) {
564
727
  return query;
565
728
  }
566
- const parsedBody = body.map(row => parseBody(row));
729
+ const parsedBody = body.map(row => this.parseBody(row));
567
730
  if (this.client === types_1.SqlClient.POSTGRES ||
568
731
  this.client === types_1.SqlClient.SQL_LITE ||
569
732
  this.client === types_1.SqlClient.MY_SQL) {
570
- const primary = json.meta.table.primary;
733
+ const primary = this.table.primary;
571
734
  if (!primary) {
572
735
  throw new Error("Primary key is required for upsert");
573
736
  }
574
737
  const ret = query.insert(parsedBody).onConflict(primary).merge();
575
738
  return ret;
576
739
  }
577
- else if (this.client === types_1.SqlClient.MS_SQL) {
578
- // No upsert or onConflict support in MSSQL yet, see:
740
+ else if (this.client === types_1.SqlClient.MS_SQL ||
741
+ this.client === types_1.SqlClient.ORACLE) {
742
+ // No upsert or onConflict support in MSSQL/Oracle yet, see:
579
743
  // https://github.com/knex/knex/pull/6050
580
744
  return query.insert(parsedBody);
581
745
  }
582
746
  return query.upsert(parsedBody);
583
747
  }
584
- read(knex, json, opts = {}) {
585
- let { endpoint, filters, paginate, relationships, tableAliases } = json;
748
+ read(opts = {}) {
749
+ let { endpoint, filters, paginate, relationships, tableAliases } = this.query;
586
750
  const { limits } = opts;
587
751
  const counting = endpoint.operation === types_1.Operation.COUNT;
588
752
  const tableName = endpoint.entityId;
589
753
  // start building the query
590
- let query = this.knexWithAlias(knex, endpoint, tableAliases);
754
+ let query = this.qualifiedKnex();
591
755
  // handle pagination
592
756
  let foundOffset = null;
593
757
  let foundLimit = (limits === null || limits === void 0 ? void 0 : limits.query) || (limits === null || limits === void 0 ? void 0 : limits.base);
@@ -617,15 +781,12 @@ class InternalBuilder {
617
781
  }
618
782
  // add sorting to pre-query
619
783
  // no point in sorting when counting
620
- query = this.addSorting(query, json);
784
+ query = this.addSorting(query);
621
785
  }
622
786
  // add filters to the query (where)
623
- query = this.addFilters(query, filters, json.meta.table, {
624
- columnPrefix: json.meta.columnPrefix,
625
- aliases: tableAliases,
626
- });
787
+ query = this.addFilters(query, filters);
627
788
  const alias = (tableAliases === null || tableAliases === void 0 ? void 0 : tableAliases[tableName]) || tableName;
628
- let preQuery = knex({
789
+ let preQuery = this.knex({
629
790
  // the typescript definition for the knex constructor doesn't support this
630
791
  // syntax, but it is the only way to alias a pre-query result as part of
631
792
  // a query - there is an alias dictionary type, but it assumes it can only
@@ -634,11 +795,11 @@ class InternalBuilder {
634
795
  });
635
796
  // if counting, use distinct count, else select
636
797
  preQuery = !counting
637
- ? preQuery.select(generateSelectStatement(json, knex))
638
- : this.addDistinctCount(preQuery, json);
798
+ ? preQuery.select(this.generateSelectStatement())
799
+ : this.addDistinctCount(preQuery);
639
800
  // have to add after as well (this breaks MS-SQL)
640
801
  if (this.client !== types_1.SqlClient.MS_SQL && !counting) {
641
- preQuery = this.addSorting(preQuery, json);
802
+ preQuery = this.addSorting(preQuery);
642
803
  }
643
804
  // handle joins
644
805
  query = this.addRelationships(preQuery, tableName, relationships, endpoint.schema, tableAliases);
@@ -647,20 +808,13 @@ class InternalBuilder {
647
808
  if (limits === null || limits === void 0 ? void 0 : limits.base) {
648
809
  query = query.limit(limits.base);
649
810
  }
650
- return this.addFilters(query, filters, json.meta.table, {
651
- columnPrefix: json.meta.columnPrefix,
652
- relationship: true,
653
- aliases: tableAliases,
654
- });
811
+ return this.addFilters(query, filters, { relationship: true });
655
812
  }
656
- update(knex, json, opts) {
657
- const { endpoint, body, filters, tableAliases } = json;
658
- let query = this.knexWithAlias(knex, endpoint, tableAliases);
659
- const parsedBody = parseBody(body);
660
- query = this.addFilters(query, filters, json.meta.table, {
661
- columnPrefix: json.meta.columnPrefix,
662
- aliases: tableAliases,
663
- });
813
+ update(opts) {
814
+ const { body, filters } = this.query;
815
+ let query = this.qualifiedKnex();
816
+ const parsedBody = this.parseBody(body);
817
+ query = this.addFilters(query, filters);
664
818
  // mysql can't use returning
665
819
  if (opts.disableReturning) {
666
820
  return query.update(parsedBody);
@@ -669,19 +823,16 @@ class InternalBuilder {
669
823
  return query.update(parsedBody).returning("*");
670
824
  }
671
825
  }
672
- delete(knex, json, opts) {
673
- const { endpoint, filters, tableAliases } = json;
674
- let query = this.knexWithAlias(knex, endpoint, tableAliases);
675
- query = this.addFilters(query, filters, json.meta.table, {
676
- columnPrefix: json.meta.columnPrefix,
677
- aliases: tableAliases,
678
- });
826
+ delete(opts) {
827
+ const { filters } = this.query;
828
+ let query = this.qualifiedKnex();
829
+ query = this.addFilters(query, filters);
679
830
  // mysql can't use returning
680
831
  if (opts.disableReturning) {
681
832
  return query.delete();
682
833
  }
683
834
  else {
684
- return query.delete().returning(generateSelectStatement(json, knex));
835
+ return query.delete().returning(this.generateSelectStatement());
685
836
  }
686
837
  }
687
838
  }
@@ -715,18 +866,18 @@ class SqlQueryBuilder extends sqlTable_1.default {
715
866
  const config = {
716
867
  client: sqlClient,
717
868
  };
718
- if (sqlClient === types_1.SqlClient.SQL_LITE) {
869
+ if (sqlClient === types_1.SqlClient.SQL_LITE || sqlClient === types_1.SqlClient.ORACLE) {
719
870
  config.useNullAsDefault = true;
720
871
  }
721
872
  const client = (0, knex_1.knex)(config);
722
873
  let query;
723
- const builder = new InternalBuilder(sqlClient);
874
+ const builder = new InternalBuilder(sqlClient, client, json);
724
875
  switch (this._operation(json)) {
725
876
  case types_1.Operation.CREATE:
726
- query = builder.create(client, json, opts);
877
+ query = builder.create(opts);
727
878
  break;
728
879
  case types_1.Operation.READ:
729
- query = builder.read(client, json, {
880
+ query = builder.read({
730
881
  limits: {
731
882
  query: this.limit,
732
883
  base: getBaseLimit(),
@@ -735,19 +886,19 @@ class SqlQueryBuilder extends sqlTable_1.default {
735
886
  break;
736
887
  case types_1.Operation.COUNT:
737
888
  // read without any limits to count
738
- query = builder.read(client, json);
889
+ query = builder.read();
739
890
  break;
740
891
  case types_1.Operation.UPDATE:
741
- query = builder.update(client, json, opts);
892
+ query = builder.update(opts);
742
893
  break;
743
894
  case types_1.Operation.DELETE:
744
- query = builder.delete(client, json, opts);
895
+ query = builder.delete(opts);
745
896
  break;
746
897
  case types_1.Operation.BULK_CREATE:
747
- query = builder.bulkCreate(client, json);
898
+ query = builder.bulkCreate();
748
899
  break;
749
900
  case types_1.Operation.BULK_UPSERT:
750
- query = builder.bulkUpsert(client, json);
901
+ query = builder.bulkUpsert();
751
902
  break;
752
903
  case types_1.Operation.CREATE_TABLE:
753
904
  case types_1.Operation.UPDATE_TABLE: