@budibase/backend-core 2.31.7 → 2.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/index.js +380 -155
  2. package/dist/index.js.map +4 -4
  3. package/dist/index.js.meta.json +1 -1
  4. package/dist/package.json +4 -4
  5. package/dist/plugins.js.map +2 -2
  6. package/dist/plugins.js.meta.json +1 -1
  7. package/dist/src/environment.d.ts +1 -0
  8. package/dist/src/environment.js +1 -1
  9. package/dist/src/environment.js.map +1 -1
  10. package/dist/src/events/publishers/ai.d.ts +7 -0
  11. package/dist/src/events/publishers/ai.js +30 -0
  12. package/dist/src/events/publishers/ai.js.map +1 -0
  13. package/dist/src/events/publishers/index.d.ts +1 -0
  14. package/dist/src/events/publishers/index.js +3 -1
  15. package/dist/src/events/publishers/index.js.map +1 -1
  16. package/dist/src/features/index.d.ts +2 -0
  17. package/dist/src/features/index.js +2 -0
  18. package/dist/src/features/index.js.map +1 -1
  19. package/dist/src/sql/sql.js +343 -148
  20. package/dist/src/sql/sql.js.map +1 -1
  21. package/dist/src/sql/utils.d.ts +2 -1
  22. package/dist/src/sql/utils.js +14 -0
  23. package/dist/src/sql/utils.js.map +1 -1
  24. package/dist/tests/core/utilities/structures/licenses.js +10 -0
  25. package/dist/tests/core/utilities/structures/licenses.js.map +1 -1
  26. package/dist/tests/core/utilities/structures/quotas.js +4 -0
  27. package/dist/tests/core/utilities/structures/quotas.js.map +1 -1
  28. package/package.json +4 -4
  29. package/src/environment.ts +1 -0
  30. package/src/events/publishers/ai.ts +21 -0
  31. package/src/events/publishers/index.ts +1 -0
  32. package/src/features/index.ts +2 -0
  33. package/src/sql/sql.ts +474 -211
  34. package/src/sql/utils.ts +29 -1
  35. package/tests/core/utilities/structures/licenses.ts +10 -0
  36. package/tests/core/utilities/structures/quotas.ts +4 -0
@@ -43,12 +43,19 @@ const types_1 = require("@budibase/types");
43
43
  const environment_1 = __importDefault(require("../environment"));
44
44
  const shared_core_1 = require("@budibase/shared-core");
45
45
  const lodash_1 = require("lodash");
46
+ const MAX_SQS_RELATIONSHIP_FIELDS = 63;
46
47
  function getBaseLimit() {
47
48
  const envLimit = environment_1.default.SQL_MAX_ROWS
48
49
  ? parseInt(environment_1.default.SQL_MAX_ROWS)
49
50
  : null;
50
51
  return envLimit || 5000;
51
52
  }
53
+ function getRelationshipLimit() {
54
+ const envLimit = environment_1.default.SQL_MAX_RELATED_ROWS
55
+ ? parseInt(environment_1.default.SQL_MAX_RELATED_ROWS)
56
+ : null;
57
+ return envLimit || 500;
58
+ }
52
59
  function getTableName(table) {
53
60
  // SQS uses the table ID rather than the table name
54
61
  if ((table === null || table === void 0 ? void 0 : table.sourceType) === types_1.TableSourceType.INTERNAL ||
@@ -77,6 +84,19 @@ function convertBooleans(query) {
77
84
  }
78
85
  class InternalBuilder {
79
86
  constructor(client, knex, query) {
87
+ // states the various situations in which we need a full mapped select statement
88
+ this.SPECIAL_SELECT_CASES = {
89
+ POSTGRES_MONEY: (field) => {
90
+ var _a;
91
+ return (this.client === types_1.SqlClient.POSTGRES &&
92
+ ((_a = field === null || field === void 0 ? void 0 : field.externalType) === null || _a === void 0 ? void 0 : _a.includes("money")));
93
+ },
94
+ MSSQL_DATES: (field) => {
95
+ return (this.client === types_1.SqlClient.MS_SQL &&
96
+ (field === null || field === void 0 ? void 0 : field.type) === types_1.FieldType.DATETIME &&
97
+ field.timeOnly);
98
+ },
99
+ };
80
100
  this.client = client;
81
101
  this.query = query;
82
102
  this.knex = knex;
@@ -114,60 +134,58 @@ class InternalBuilder {
114
134
  .map(part => this.quote(part))
115
135
  .join(".");
116
136
  }
137
+ isFullSelectStatementRequired() {
138
+ const { meta } = this.query;
139
+ for (let column of Object.values(meta.table.schema)) {
140
+ if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(column)) {
141
+ return true;
142
+ }
143
+ else if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(column)) {
144
+ return true;
145
+ }
146
+ }
147
+ return false;
148
+ }
117
149
  generateSelectStatement() {
118
- const { resource, meta } = this.query;
150
+ const { meta, endpoint, resource, tableAliases } = this.query;
119
151
  if (!resource || !resource.fields || resource.fields.length === 0) {
120
152
  return "*";
121
153
  }
154
+ const alias = (tableAliases === null || tableAliases === void 0 ? void 0 : tableAliases[endpoint.entityId])
155
+ ? tableAliases === null || tableAliases === void 0 ? void 0 : tableAliases[endpoint.entityId]
156
+ : endpoint.entityId;
122
157
  const schema = meta.table.schema;
123
- return resource.fields.map(field => {
124
- var _a;
158
+ if (!this.isFullSelectStatementRequired()) {
159
+ return [this.knex.raw(`${this.quote(alias)}.*`)];
160
+ }
161
+ // get just the fields for this table
162
+ return resource.fields
163
+ .map(field => {
125
164
  const parts = field.split(/\./g);
126
165
  let table = undefined;
127
- let column = undefined;
166
+ let column = parts[0];
128
167
  // Just a column name, e.g.: "column"
129
- if (parts.length === 1) {
130
- column = parts[0];
131
- }
132
- // A table name and a column name, e.g.: "table.column"
133
- if (parts.length === 2) {
134
- table = parts[0];
135
- column = parts[1];
136
- }
137
- // A link doc, e.g.: "table.doc1.fieldName"
138
- if (parts.length > 2) {
168
+ if (parts.length > 1) {
139
169
  table = parts[0];
140
170
  column = parts.slice(1).join(".");
141
171
  }
142
- if (!column) {
143
- throw new Error(`Invalid field name: ${field}`);
144
- }
172
+ return { table, column, field };
173
+ })
174
+ .filter(({ table }) => !table || table === alias)
175
+ .map(({ table, column, field }) => {
145
176
  const columnSchema = schema[column];
146
- if (this.client === types_1.SqlClient.POSTGRES &&
147
- ((_a = columnSchema === null || columnSchema === void 0 ? void 0 : columnSchema.externalType) === null || _a === void 0 ? void 0 : _a.includes("money"))) {
177
+ if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(columnSchema)) {
148
178
  return this.knex.raw(`${this.quotedIdentifier([table, column].join("."))}::money::numeric as ${this.quote(field)}`);
149
179
  }
150
- if (this.client === types_1.SqlClient.MS_SQL &&
151
- (columnSchema === null || columnSchema === void 0 ? void 0 : columnSchema.type) === types_1.FieldType.DATETIME &&
152
- columnSchema.timeOnly) {
180
+ if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(columnSchema)) {
153
181
  // Time gets returned as timestamp from mssql, not matching the expected
154
182
  // HH:mm format
155
183
  return this.knex.raw(`CONVERT(varchar, ${field}, 108) as "${field}"`);
156
184
  }
157
- // There's at least two edge cases being handled in the expression below.
158
- // 1. The column name could start/end with a space, and in that case we
159
- // want to preseve that space.
160
- // 2. Almost all column names are specified in the form table.column, except
161
- // in the case of relationships, where it's table.doc1.column. In that
162
- // case, we want to split it into `table`.`doc1.column` for reasons that
163
- // aren't actually clear to me, but `table`.`doc1` breaks things with the
164
- // sample data tests.
165
- if (table) {
166
- return this.knex.raw(`${this.quote(table)}.${this.quote(column)} as ${this.quote(field)}`);
167
- }
168
- else {
169
- return this.knex.raw(`${this.quote(field)} as ${this.quote(field)}`);
170
- }
185
+ const quoted = table
186
+ ? `${this.quote(table)}.${this.quote(column)}`
187
+ : this.quote(field);
188
+ return this.knex.raw(quoted);
171
189
  });
172
190
  }
173
191
  // OracleDB can't use character-large-objects (CLOBs) in WHERE clauses,
@@ -285,54 +303,118 @@ class InternalBuilder {
285
303
  }
286
304
  return filters;
287
305
  }
306
+ addJoinFieldCheck(query, relationship) {
307
+ var _a;
308
+ const document = ((_a = relationship.from) === null || _a === void 0 ? void 0 : _a.split(".")[0]) || "";
309
+ return query.andWhere(`${document}.fieldName`, "=", relationship.column);
310
+ }
311
+ addRelationshipForFilter(query, filterKey, whereCb) {
312
+ const mainKnex = this.knex;
313
+ const { relationships, endpoint, tableAliases: aliases } = this.query;
314
+ const tableName = endpoint.entityId;
315
+ const fromAlias = (aliases === null || aliases === void 0 ? void 0 : aliases[tableName]) || tableName;
316
+ const matches = (possibleTable) => filterKey.startsWith(`${possibleTable}`);
317
+ if (!relationships) {
318
+ return query;
319
+ }
320
+ for (const relationship of relationships) {
321
+ const relatedTableName = relationship.tableName;
322
+ const toAlias = (aliases === null || aliases === void 0 ? void 0 : aliases[relatedTableName]) || relatedTableName;
323
+ // this is the relationship which is being filtered
324
+ if ((matches(relatedTableName) || matches(toAlias)) &&
325
+ relationship.to &&
326
+ relationship.tableName) {
327
+ let subQuery = mainKnex
328
+ .select(mainKnex.raw(1))
329
+ .from({ [toAlias]: relatedTableName });
330
+ const manyToMany = (0, utils_1.validateManyToMany)(relationship);
331
+ if (manyToMany) {
332
+ const throughAlias = (aliases === null || aliases === void 0 ? void 0 : aliases[manyToMany.through]) || relationship.through;
333
+ let throughTable = this.tableNameWithSchema(manyToMany.through, {
334
+ alias: throughAlias,
335
+ schema: endpoint.schema,
336
+ });
337
+ subQuery = subQuery
338
+ // add a join through the junction table
339
+ .innerJoin(throughTable, function () {
340
+ // @ts-ignore
341
+ this.on(`${toAlias}.${manyToMany.toPrimary}`, "=", `${throughAlias}.${manyToMany.to}`);
342
+ })
343
+ // check the document in the junction table points to the main table
344
+ .where(`${throughAlias}.${manyToMany.from}`, "=", mainKnex.raw(this.quotedIdentifier(`${fromAlias}.${manyToMany.fromPrimary}`)));
345
+ // in SQS the same junction table is used for different many-to-many relationships between the
346
+ // two same tables, this is needed to avoid rows ending up in all columns
347
+ if (this.client === types_1.SqlClient.SQL_LITE) {
348
+ subQuery = this.addJoinFieldCheck(subQuery, manyToMany);
349
+ }
350
+ }
351
+ else {
352
+ // "join" to the main table, making sure the ID matches that of the main
353
+ subQuery = subQuery.where(`${toAlias}.${relationship.to}`, "=", mainKnex.raw(this.quotedIdentifier(`${fromAlias}.${relationship.from}`)));
354
+ }
355
+ query = query.whereExists(whereCb(subQuery));
356
+ break;
357
+ }
358
+ }
359
+ return query;
360
+ }
288
361
  // right now we only do filters on the specific table being queried
289
362
  addFilters(query, filters, opts) {
290
363
  if (!filters) {
291
364
  return query;
292
365
  }
366
+ const builder = this;
293
367
  filters = this.parseFilters(Object.assign({}, filters));
294
368
  const aliases = this.query.tableAliases;
295
369
  // if all or specified in filters, then everything is an or
296
370
  const allOr = filters.allOr;
297
- const tableName = this.client === types_1.SqlClient.SQL_LITE ? this.table._id : this.table.name;
371
+ const isSqlite = this.client === types_1.SqlClient.SQL_LITE;
372
+ const tableName = isSqlite ? this.table._id : this.table.name;
298
373
  function getTableAlias(name) {
299
374
  const alias = aliases === null || aliases === void 0 ? void 0 : aliases[name];
300
375
  return alias || name;
301
376
  }
302
377
  function iterate(structure, fn, complexKeyFn) {
378
+ const handleRelationship = (q, key, value) => {
379
+ const [filterTableName, ...otherProperties] = key.split(".");
380
+ const property = otherProperties.join(".");
381
+ const alias = getTableAlias(filterTableName);
382
+ return fn(q, alias ? `${alias}.${property}` : property, value);
383
+ };
303
384
  for (const key in structure) {
304
385
  const value = structure[key];
305
386
  const updatedKey = dbCore.removeKeyNumbering(key);
306
387
  const isRelationshipField = updatedKey.includes(".");
388
+ const shouldProcessRelationship = (opts === null || opts === void 0 ? void 0 : opts.relationship) && isRelationshipField;
307
389
  let castedTypeValue;
308
390
  if (key === types_1.InternalSearchFilterOperator.COMPLEX_ID_OPERATOR &&
309
391
  (castedTypeValue = structure[key]) &&
310
392
  complexKeyFn) {
311
393
  const alias = getTableAlias(tableName);
312
- complexKeyFn(castedTypeValue.id.map((x) => alias ? `${alias}.${x}` : x), castedTypeValue.values);
394
+ query = complexKeyFn(query, castedTypeValue.id.map((x) => alias ? `${alias}.${x}` : x), castedTypeValue.values);
313
395
  }
314
396
  else if (!isRelationshipField) {
315
397
  const alias = getTableAlias(tableName);
316
- fn(alias ? `${alias}.${updatedKey}` : updatedKey, value);
398
+ query = fn(query, alias ? `${alias}.${updatedKey}` : updatedKey, value);
317
399
  }
318
- if ((opts === null || opts === void 0 ? void 0 : opts.relationship) && isRelationshipField) {
319
- const [filterTableName, property] = updatedKey.split(".");
320
- const alias = getTableAlias(filterTableName);
321
- fn(alias ? `${alias}.${property}` : property, value);
400
+ else if (shouldProcessRelationship) {
401
+ query = builder.addRelationshipForFilter(query, updatedKey, q => {
402
+ return handleRelationship(q, updatedKey, value);
403
+ });
322
404
  }
323
405
  }
324
406
  }
325
- const like = (key, value) => {
407
+ const like = (q, key, value) => {
326
408
  const fuzzyOr = filters === null || filters === void 0 ? void 0 : filters.fuzzyOr;
327
409
  const fnc = fuzzyOr || allOr ? "orWhere" : "where";
328
410
  // postgres supports ilike, nothing else does
329
411
  if (this.client === types_1.SqlClient.POSTGRES) {
330
- query = query[fnc](key, "ilike", `%${value}%`);
412
+ return q[fnc](key, "ilike", `%${value}%`);
331
413
  }
332
414
  else {
333
415
  const rawFnc = `${fnc}Raw`;
334
416
  // @ts-ignore
335
- query = query[rawFnc](`LOWER(${this.quotedIdentifier(key)}) LIKE ?`, [
417
+ return q[rawFnc](`LOWER(${this.quotedIdentifier(key)}) LIKE ?`, [
336
418
  `%${value.toLowerCase()}%`,
337
419
  ]);
338
420
  }
@@ -349,24 +431,24 @@ class InternalBuilder {
349
431
  return `[${value.join(",")}]`;
350
432
  }
351
433
  if (this.client === types_1.SqlClient.POSTGRES) {
352
- iterate(mode, (key, value) => {
434
+ iterate(mode, (q, key, value) => {
353
435
  const wrap = any ? "" : "'";
354
436
  const op = any ? "\\?| array" : "@>";
355
437
  const fieldNames = key.split(/\./g);
356
438
  const table = fieldNames[0];
357
439
  const col = fieldNames[1];
358
- query = query[rawFnc](`${not}COALESCE("${table}"."${col}"::jsonb ${op} ${wrap}${stringifyArray(value, any ? "'" : '"')}${wrap}, FALSE)`);
440
+ return q[rawFnc](`${not}COALESCE("${table}"."${col}"::jsonb ${op} ${wrap}${stringifyArray(value, any ? "'" : '"')}${wrap}, FALSE)`);
359
441
  });
360
442
  }
361
443
  else if (this.client === types_1.SqlClient.MY_SQL) {
362
444
  const jsonFnc = any ? "JSON_OVERLAPS" : "JSON_CONTAINS";
363
- iterate(mode, (key, value) => {
364
- query = query[rawFnc](`${not}COALESCE(${jsonFnc}(${key}, '${stringifyArray(value)}'), FALSE)`);
445
+ iterate(mode, (q, key, value) => {
446
+ return q[rawFnc](`${not}COALESCE(${jsonFnc}(${key}, '${stringifyArray(value)}'), FALSE)`);
365
447
  });
366
448
  }
367
449
  else {
368
450
  const andOr = mode === (filters === null || filters === void 0 ? void 0 : filters.containsAny) ? " OR " : " AND ";
369
- iterate(mode, (key, value) => {
451
+ iterate(mode, (q, key, value) => {
370
452
  let statement = "";
371
453
  const identifier = this.quotedIdentifier(key);
372
454
  for (let i in value) {
@@ -379,13 +461,13 @@ class InternalBuilder {
379
461
  statement += `${statement ? andOr : ""}COALESCE(LOWER(${identifier}), '') LIKE ?`;
380
462
  }
381
463
  if (statement === "") {
382
- return;
464
+ return q;
383
465
  }
384
466
  if (not) {
385
- query = query[rawFnc](`(NOT (${statement}) OR ${identifier} IS NULL)`, value);
467
+ return q[rawFnc](`(NOT (${statement}) OR ${identifier} IS NULL)`, value);
386
468
  }
387
469
  else {
388
- query = query[rawFnc](statement, value);
470
+ return q[rawFnc](statement, value);
389
471
  }
390
472
  });
391
473
  }
@@ -408,40 +490,40 @@ class InternalBuilder {
408
490
  }
409
491
  if (filters.oneOf) {
410
492
  const fnc = allOr ? "orWhereIn" : "whereIn";
411
- iterate(filters.oneOf, (key, array) => {
493
+ iterate(filters.oneOf, (q, key, array) => {
412
494
  if (this.client === types_1.SqlClient.ORACLE) {
413
495
  key = this.convertClobs(key);
414
496
  array = Array.isArray(array) ? array : [array];
415
497
  const binding = new Array(array.length).fill("?").join(",");
416
- query = query.whereRaw(`${key} IN (${binding})`, array);
498
+ return q.whereRaw(`${key} IN (${binding})`, array);
417
499
  }
418
500
  else {
419
- query = query[fnc](key, Array.isArray(array) ? array : [array]);
501
+ return q[fnc](key, Array.isArray(array) ? array : [array]);
420
502
  }
421
- }, (key, array) => {
503
+ }, (q, key, array) => {
422
504
  if (this.client === types_1.SqlClient.ORACLE) {
423
505
  const keyStr = `(${key.map(k => this.convertClobs(k)).join(",")})`;
424
506
  const binding = `(${array
425
507
  .map((a) => `(${new Array(a.length).fill("?").join(",")})`)
426
508
  .join(",")})`;
427
- query = query.whereRaw(`${keyStr} IN ${binding}`, array.flat());
509
+ return q.whereRaw(`${keyStr} IN ${binding}`, array.flat());
428
510
  }
429
511
  else {
430
- query = query[fnc](key, Array.isArray(array) ? array : [array]);
512
+ return q[fnc](key, Array.isArray(array) ? array : [array]);
431
513
  }
432
514
  });
433
515
  }
434
516
  if (filters.string) {
435
- iterate(filters.string, (key, value) => {
517
+ iterate(filters.string, (q, key, value) => {
436
518
  const fnc = allOr ? "orWhere" : "where";
437
519
  // postgres supports ilike, nothing else does
438
520
  if (this.client === types_1.SqlClient.POSTGRES) {
439
- query = query[fnc](key, "ilike", `${value}%`);
521
+ return q[fnc](key, "ilike", `${value}%`);
440
522
  }
441
523
  else {
442
524
  const rawFnc = `${fnc}Raw`;
443
525
  // @ts-ignore
444
- query = query[rawFnc](`LOWER(${this.quotedIdentifier(key)}) LIKE ?`, [
526
+ return q[rawFnc](`LOWER(${this.quotedIdentifier(key)}) LIKE ?`, [
445
527
  `${value.toLowerCase()}%`,
446
528
  ]);
447
529
  }
@@ -451,7 +533,7 @@ class InternalBuilder {
451
533
  iterate(filters.fuzzy, like);
452
534
  }
453
535
  if (filters.range) {
454
- iterate(filters.range, (key, value) => {
536
+ iterate(filters.range, (q, key, value) => {
455
537
  const isEmptyObject = (val) => {
456
538
  return (val &&
457
539
  Object.keys(val).length === 0 &&
@@ -472,75 +554,86 @@ class InternalBuilder {
472
554
  if (lowValid && highValid) {
473
555
  if ((schema === null || schema === void 0 ? void 0 : schema.type) === types_1.FieldType.BIGINT &&
474
556
  this.client === types_1.SqlClient.SQL_LITE) {
475
- query = query.whereRaw(`CAST(${key} AS INTEGER) BETWEEN CAST(? AS INTEGER) AND CAST(? AS INTEGER)`, [value.low, value.high]);
557
+ return q.whereRaw(`CAST(${key} AS INTEGER) BETWEEN CAST(? AS INTEGER) AND CAST(? AS INTEGER)`, [value.low, value.high]);
476
558
  }
477
559
  else {
478
560
  const fnc = allOr ? "orWhereBetween" : "whereBetween";
479
- query = query[fnc](key, [value.low, value.high]);
561
+ return q[fnc](key, [value.low, value.high]);
480
562
  }
481
563
  }
482
564
  else if (lowValid) {
483
565
  if ((schema === null || schema === void 0 ? void 0 : schema.type) === types_1.FieldType.BIGINT &&
484
566
  this.client === types_1.SqlClient.SQL_LITE) {
485
- query = query.whereRaw(`CAST(${key} AS INTEGER) >= CAST(? AS INTEGER)`, [value.low]);
567
+ return q.whereRaw(`CAST(${key} AS INTEGER) >= CAST(? AS INTEGER)`, [
568
+ value.low,
569
+ ]);
486
570
  }
487
571
  else {
488
572
  const fnc = allOr ? "orWhere" : "where";
489
- query = query[fnc](key, ">=", value.low);
573
+ return q[fnc](key, ">=", value.low);
490
574
  }
491
575
  }
492
576
  else if (highValid) {
493
577
  if ((schema === null || schema === void 0 ? void 0 : schema.type) === types_1.FieldType.BIGINT &&
494
578
  this.client === types_1.SqlClient.SQL_LITE) {
495
- query = query.whereRaw(`CAST(${key} AS INTEGER) <= CAST(? AS INTEGER)`, [value.high]);
579
+ return q.whereRaw(`CAST(${key} AS INTEGER) <= CAST(? AS INTEGER)`, [
580
+ value.high,
581
+ ]);
496
582
  }
497
583
  else {
498
584
  const fnc = allOr ? "orWhere" : "where";
499
- query = query[fnc](key, "<=", value.high);
585
+ return q[fnc](key, "<=", value.high);
500
586
  }
501
587
  }
588
+ return q;
502
589
  });
503
590
  }
504
591
  if (filters.equal) {
505
- iterate(filters.equal, (key, value) => {
592
+ iterate(filters.equal, (q, key, value) => {
506
593
  const fnc = allOr ? "orWhereRaw" : "whereRaw";
507
594
  if (this.client === types_1.SqlClient.MS_SQL) {
508
- query = query[fnc](`CASE WHEN ${this.quotedIdentifier(key)} = ? THEN 1 ELSE 0 END = 1`, [value]);
595
+ return q[fnc](`CASE WHEN ${this.quotedIdentifier(key)} = ? THEN 1 ELSE 0 END = 1`, [value]);
509
596
  }
510
597
  else if (this.client === types_1.SqlClient.ORACLE) {
511
598
  const identifier = this.convertClobs(key);
512
- query = query[fnc](`(${identifier} IS NOT NULL AND ${identifier} = ?)`, [value]);
599
+ return q[fnc](`(${identifier} IS NOT NULL AND ${identifier} = ?)`, [
600
+ value,
601
+ ]);
513
602
  }
514
603
  else {
515
- query = query[fnc](`COALESCE(${this.quotedIdentifier(key)} = ?, FALSE)`, [value]);
604
+ return q[fnc](`COALESCE(${this.quotedIdentifier(key)} = ?, FALSE)`, [
605
+ value,
606
+ ]);
516
607
  }
517
608
  });
518
609
  }
519
610
  if (filters.notEqual) {
520
- iterate(filters.notEqual, (key, value) => {
611
+ iterate(filters.notEqual, (q, key, value) => {
521
612
  const fnc = allOr ? "orWhereRaw" : "whereRaw";
522
613
  if (this.client === types_1.SqlClient.MS_SQL) {
523
- query = query[fnc](`CASE WHEN ${this.quotedIdentifier(key)} = ? THEN 1 ELSE 0 END = 0`, [value]);
614
+ return q[fnc](`CASE WHEN ${this.quotedIdentifier(key)} = ? THEN 1 ELSE 0 END = 0`, [value]);
524
615
  }
525
616
  else if (this.client === types_1.SqlClient.ORACLE) {
526
617
  const identifier = this.convertClobs(key);
527
- query = query[fnc](`(${identifier} IS NOT NULL AND ${identifier} != ?) OR ${identifier} IS NULL`, [value]);
618
+ return q[fnc](`(${identifier} IS NOT NULL AND ${identifier} != ?) OR ${identifier} IS NULL`, [value]);
528
619
  }
529
620
  else {
530
- query = query[fnc](`COALESCE(${this.quotedIdentifier(key)} != ?, TRUE)`, [value]);
621
+ return q[fnc](`COALESCE(${this.quotedIdentifier(key)} != ?, TRUE)`, [
622
+ value,
623
+ ]);
531
624
  }
532
625
  });
533
626
  }
534
627
  if (filters.empty) {
535
- iterate(filters.empty, key => {
628
+ iterate(filters.empty, (q, key) => {
536
629
  const fnc = allOr ? "orWhereNull" : "whereNull";
537
- query = query[fnc](key);
630
+ return q[fnc](key);
538
631
  });
539
632
  }
540
633
  if (filters.notEmpty) {
541
- iterate(filters.notEmpty, key => {
634
+ iterate(filters.notEmpty, (q, key) => {
542
635
  const fnc = allOr ? "orWhereNotNull" : "whereNotNull";
543
- query = query[fnc](key);
636
+ return q[fnc](key);
544
637
  });
545
638
  }
546
639
  if (filters.contains) {
@@ -614,10 +707,162 @@ class InternalBuilder {
614
707
  }
615
708
  return withSchema;
616
709
  }
617
- addRelationships(query, fromTable, relationships, schema, aliases) {
618
- if (!relationships) {
619
- return query;
710
+ addJsonRelationships(query, fromTable, relationships) {
711
+ const sqlClient = this.client;
712
+ const knex = this.knex;
713
+ const { resource, tableAliases: aliases, endpoint } = this.query;
714
+ const fields = (resource === null || resource === void 0 ? void 0 : resource.fields) || [];
715
+ const jsonField = (field) => {
716
+ const parts = field.split(".");
717
+ let tableField, unaliased;
718
+ if (parts.length > 1) {
719
+ const alias = parts.shift();
720
+ unaliased = parts.join(".");
721
+ tableField = `${this.quote(alias)}.${this.quote(unaliased)}`;
722
+ }
723
+ else {
724
+ unaliased = parts.join(".");
725
+ tableField = this.quote(unaliased);
726
+ }
727
+ let separator = ",";
728
+ switch (sqlClient) {
729
+ case types_1.SqlClient.ORACLE:
730
+ separator = " VALUE ";
731
+ break;
732
+ case types_1.SqlClient.MS_SQL:
733
+ separator = ":";
734
+ }
735
+ return `'${unaliased}'${separator}${tableField}`;
736
+ };
737
+ for (let relationship of relationships) {
738
+ const { tableName: toTable, through: throughTable, to: toKey, from: fromKey, fromPrimary, toPrimary, } = relationship;
739
+ // skip invalid relationships
740
+ if (!toTable || !fromTable) {
741
+ continue;
742
+ }
743
+ const toAlias = (aliases === null || aliases === void 0 ? void 0 : aliases[toTable]) || toTable, fromAlias = (aliases === null || aliases === void 0 ? void 0 : aliases[fromTable]) || fromTable;
744
+ let toTableWithSchema = this.tableNameWithSchema(toTable, {
745
+ alias: toAlias,
746
+ schema: endpoint.schema,
747
+ });
748
+ let relationshipFields = fields.filter(field => field.split(".")[0] === toAlias);
749
+ if (this.client === types_1.SqlClient.SQL_LITE) {
750
+ relationshipFields = relationshipFields.slice(0, MAX_SQS_RELATIONSHIP_FIELDS);
751
+ }
752
+ const fieldList = relationshipFields
753
+ .map(field => jsonField(field))
754
+ .join(",");
755
+ // SQL Server uses TOP - which performs a little differently to the normal LIMIT syntax
756
+ // it reduces the result set rather than limiting how much data it filters over
757
+ const primaryKey = `${toAlias}.${toPrimary || toKey}`;
758
+ let subQuery = knex
759
+ .from(toTableWithSchema)
760
+ .limit(getRelationshipLimit())
761
+ // add sorting to get consistent order
762
+ .orderBy(primaryKey);
763
+ // many-to-many relationship with junction table
764
+ if (throughTable && toPrimary && fromPrimary) {
765
+ const throughAlias = (aliases === null || aliases === void 0 ? void 0 : aliases[throughTable]) || throughTable;
766
+ let throughTableWithSchema = this.tableNameWithSchema(throughTable, {
767
+ alias: throughAlias,
768
+ schema: endpoint.schema,
769
+ });
770
+ subQuery = subQuery
771
+ .join(throughTableWithSchema, function () {
772
+ this.on(`${toAlias}.${toPrimary}`, "=", `${throughAlias}.${toKey}`);
773
+ })
774
+ .where(`${throughAlias}.${fromKey}`, "=", knex.raw(this.quotedIdentifier(`${fromAlias}.${fromPrimary}`)));
775
+ }
776
+ // one-to-many relationship with foreign key
777
+ else {
778
+ subQuery = subQuery.where(`${toAlias}.${toKey}`, "=", knex.raw(this.quotedIdentifier(`${fromAlias}.${fromKey}`)));
779
+ }
780
+ const standardWrap = (select) => {
781
+ subQuery = subQuery.select(`${toAlias}.*`);
782
+ // @ts-ignore - the from alias syntax isn't in Knex typing
783
+ return knex.select(knex.raw(select)).from({
784
+ [toAlias]: subQuery,
785
+ });
786
+ };
787
+ let wrapperQuery;
788
+ switch (sqlClient) {
789
+ case types_1.SqlClient.SQL_LITE:
790
+ // need to check the junction table document is to the right column, this is just for SQS
791
+ subQuery = this.addJoinFieldCheck(subQuery, relationship);
792
+ wrapperQuery = standardWrap(`json_group_array(json_object(${fieldList}))`);
793
+ break;
794
+ case types_1.SqlClient.POSTGRES:
795
+ wrapperQuery = standardWrap(`json_agg(json_build_object(${fieldList}))`);
796
+ break;
797
+ case types_1.SqlClient.MY_SQL:
798
+ wrapperQuery = subQuery.select(knex.raw(`json_arrayagg(json_object(${fieldList}))`));
799
+ break;
800
+ case types_1.SqlClient.ORACLE:
801
+ wrapperQuery = standardWrap(`json_arrayagg(json_object(${fieldList}))`);
802
+ break;
803
+ case types_1.SqlClient.MS_SQL:
804
+ wrapperQuery = knex.raw(`(SELECT ${this.quote(toAlias)} = (${knex
805
+ .select(`${fromAlias}.*`)
806
+ // @ts-ignore - from alias syntax not TS supported
807
+ .from({
808
+ [fromAlias]: subQuery.select(`${toAlias}.*`),
809
+ })} FOR JSON PATH))`);
810
+ break;
811
+ default:
812
+ throw new Error(`JSON relationships not implement for ${sqlClient}`);
813
+ }
814
+ query = query.select({ [relationship.column]: wrapperQuery });
620
815
  }
816
+ return query;
817
+ }
818
+ addJoin(query, tables, columns) {
819
+ const { tableAliases: aliases, endpoint } = this.query;
820
+ const schema = endpoint.schema;
821
+ const toTable = tables.to, fromTable = tables.from, throughTable = tables.through;
822
+ const toAlias = (aliases === null || aliases === void 0 ? void 0 : aliases[toTable]) || toTable, throughAlias = (throughTable && (aliases === null || aliases === void 0 ? void 0 : aliases[throughTable])) || throughTable, fromAlias = (aliases === null || aliases === void 0 ? void 0 : aliases[fromTable]) || fromTable;
823
+ let toTableWithSchema = this.tableNameWithSchema(toTable, {
824
+ alias: toAlias,
825
+ schema,
826
+ });
827
+ let throughTableWithSchema = throughTable
828
+ ? this.tableNameWithSchema(throughTable, {
829
+ alias: throughAlias,
830
+ schema,
831
+ })
832
+ : undefined;
833
+ if (!throughTable) {
834
+ // @ts-ignore
835
+ query = query.leftJoin(toTableWithSchema, function () {
836
+ for (let relationship of columns) {
837
+ const from = relationship.from, to = relationship.to;
838
+ // @ts-ignore
839
+ this.orOn(`${fromAlias}.${from}`, "=", `${toAlias}.${to}`);
840
+ }
841
+ });
842
+ }
843
+ else {
844
+ query = query
845
+ // @ts-ignore
846
+ .leftJoin(throughTableWithSchema, function () {
847
+ for (let relationship of columns) {
848
+ const fromPrimary = relationship.fromPrimary;
849
+ const from = relationship.from;
850
+ // @ts-ignore
851
+ this.orOn(`${fromAlias}.${fromPrimary}`, "=", `${throughAlias}.${from}`);
852
+ }
853
+ })
854
+ .leftJoin(toTableWithSchema, function () {
855
+ for (let relationship of columns) {
856
+ const toPrimary = relationship.toPrimary;
857
+ const to = relationship.to;
858
+ // @ts-ignore
859
+ this.orOn(`${toAlias}.${toPrimary}`, `${throughAlias}.${to}`);
860
+ }
861
+ });
862
+ }
863
+ return query;
864
+ }
865
+ addRelationships(query, fromTable, relationships) {
621
866
  const tableSets = {};
622
867
  // aggregate into table sets (all the same to tables)
623
868
  for (let relationship of relationships) {
@@ -638,45 +883,11 @@ class InternalBuilder {
638
883
  }
639
884
  for (let [key, relationships] of Object.entries(tableSets)) {
640
885
  const { toTable, throughTable } = JSON.parse(key);
641
- const toAlias = (aliases === null || aliases === void 0 ? void 0 : aliases[toTable]) || toTable, throughAlias = (aliases === null || aliases === void 0 ? void 0 : aliases[throughTable]) || throughTable, fromAlias = (aliases === null || aliases === void 0 ? void 0 : aliases[fromTable]) || fromTable;
642
- let toTableWithSchema = this.tableNameWithSchema(toTable, {
643
- alias: toAlias,
644
- schema,
645
- });
646
- let throughTableWithSchema = this.tableNameWithSchema(throughTable, {
647
- alias: throughAlias,
648
- schema,
649
- });
650
- if (!throughTable) {
651
- // @ts-ignore
652
- query = query.leftJoin(toTableWithSchema, function () {
653
- for (let relationship of relationships) {
654
- const from = relationship.from, to = relationship.to;
655
- // @ts-ignore
656
- this.orOn(`${fromAlias}.${from}`, "=", `${toAlias}.${to}`);
657
- }
658
- });
659
- }
660
- else {
661
- query = query
662
- // @ts-ignore
663
- .leftJoin(throughTableWithSchema, function () {
664
- for (let relationship of relationships) {
665
- const fromPrimary = relationship.fromPrimary;
666
- const from = relationship.from;
667
- // @ts-ignore
668
- this.orOn(`${fromAlias}.${fromPrimary}`, "=", `${throughAlias}.${from}`);
669
- }
670
- })
671
- .leftJoin(toTableWithSchema, function () {
672
- for (let relationship of relationships) {
673
- const toPrimary = relationship.toPrimary;
674
- const to = relationship.to;
675
- // @ts-ignore
676
- this.orOn(`${toAlias}.${toPrimary}`, `${throughAlias}.${to}`);
677
- }
678
- });
679
- }
886
+ query = this.addJoin(query, {
887
+ from: fromTable,
888
+ to: toTable,
889
+ through: throughTable,
890
+ }, relationships);
680
891
  }
681
892
  return query;
682
893
  }
@@ -766,7 +977,7 @@ class InternalBuilder {
766
977
  return query.upsert(parsedBody);
767
978
  }
768
979
  read(opts = {}) {
769
- let { endpoint, filters, paginate, relationships, tableAliases } = this.query;
980
+ let { endpoint, filters, paginate, relationships } = this.query;
770
981
  const { limits } = opts;
771
982
  const counting = endpoint.operation === types_1.Operation.COUNT;
772
983
  const tableName = endpoint.entityId;
@@ -799,34 +1010,18 @@ class InternalBuilder {
799
1010
  if (foundOffset != null) {
800
1011
  query = query.offset(foundOffset);
801
1012
  }
802
- // add sorting to pre-query
803
- // no point in sorting when counting
804
- query = this.addSorting(query);
805
1013
  }
806
- // add filters to the query (where)
807
- query = this.addFilters(query, filters);
808
- const alias = (tableAliases === null || tableAliases === void 0 ? void 0 : tableAliases[tableName]) || tableName;
809
- let preQuery = this.knex({
810
- // the typescript definition for the knex constructor doesn't support this
811
- // syntax, but it is the only way to alias a pre-query result as part of
812
- // a query - there is an alias dictionary type, but it assumes it can only
813
- // be a table name, not a pre-query
814
- [alias]: query,
815
- });
816
1014
  // if counting, use distinct count, else select
817
- preQuery = !counting
818
- ? preQuery.select(this.generateSelectStatement())
819
- : this.addDistinctCount(preQuery);
1015
+ query = !counting
1016
+ ? query.select(this.generateSelectStatement())
1017
+ : this.addDistinctCount(query);
820
1018
  // have to add after as well (this breaks MS-SQL)
821
- if (this.client !== types_1.SqlClient.MS_SQL && !counting) {
822
- preQuery = this.addSorting(preQuery);
1019
+ if (!counting) {
1020
+ query = this.addSorting(query);
823
1021
  }
824
1022
  // handle joins
825
- query = this.addRelationships(preQuery, tableName, relationships, endpoint.schema, tableAliases);
826
- // add a base limit over the whole query
827
- // if counting we can't set this limit
828
- if (limits === null || limits === void 0 ? void 0 : limits.base) {
829
- query = query.limit(limits.base);
1023
+ if (relationships) {
1024
+ query = this.addJsonRelationships(query, tableName, relationships);
830
1025
  }
831
1026
  return this.addFilters(query, filters, { relationship: true });
832
1027
  }