@budibase/backend-core 2.33.0 → 2.33.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/sql/sql.ts CHANGED
@@ -23,12 +23,14 @@ import {
23
23
  InternalSearchFilterOperator,
24
24
  JsonFieldMetadata,
25
25
  JsonTypes,
26
+ LogicalOperator,
26
27
  Operation,
27
28
  prefixed,
28
29
  QueryJson,
29
30
  QueryOptions,
30
31
  RangeOperator,
31
32
  RelationshipsJson,
33
+ SearchFilterKey,
32
34
  SearchFilters,
33
35
  SortOrder,
34
36
  SqlClient,
@@ -96,6 +98,22 @@ function isSqs(table: Table): boolean {
96
98
  )
97
99
  }
98
100
 
101
+ const allowEmptyRelationships: Record<SearchFilterKey, boolean> = {
102
+ [BasicOperator.EQUAL]: false,
103
+ [BasicOperator.NOT_EQUAL]: true,
104
+ [BasicOperator.EMPTY]: false,
105
+ [BasicOperator.NOT_EMPTY]: true,
106
+ [BasicOperator.FUZZY]: false,
107
+ [BasicOperator.STRING]: false,
108
+ [RangeOperator.RANGE]: false,
109
+ [ArrayOperator.CONTAINS]: false,
110
+ [ArrayOperator.NOT_CONTAINS]: true,
111
+ [ArrayOperator.CONTAINS_ANY]: false,
112
+ [ArrayOperator.ONE_OF]: false,
113
+ [LogicalOperator.AND]: false,
114
+ [LogicalOperator.OR]: false,
115
+ }
116
+
99
117
  class InternalBuilder {
100
118
  private readonly client: SqlClient
101
119
  private readonly query: QueryJson
@@ -405,31 +423,48 @@ class InternalBuilder {
405
423
 
406
424
  addRelationshipForFilter(
407
425
  query: Knex.QueryBuilder,
426
+ allowEmptyRelationships: boolean,
408
427
  filterKey: string,
409
- whereCb: (query: Knex.QueryBuilder) => Knex.QueryBuilder
428
+ whereCb: (filterKey: string, query: Knex.QueryBuilder) => Knex.QueryBuilder
410
429
  ): Knex.QueryBuilder {
411
430
  const mainKnex = this.knex
412
431
  const { relationships, endpoint, tableAliases: aliases } = this.query
413
432
  const tableName = endpoint.entityId
414
433
  const fromAlias = aliases?.[tableName] || tableName
415
- const matches = (possibleTable: string) =>
416
- filterKey.startsWith(`${possibleTable}`)
434
+ const matches = (value: string) =>
435
+ filterKey.match(new RegExp(`^${value}\\.`))
417
436
  if (!relationships) {
418
437
  return query
419
438
  }
420
439
  for (const relationship of relationships) {
421
440
  const relatedTableName = relationship.tableName
422
441
  const toAlias = aliases?.[relatedTableName] || relatedTableName
442
+
443
+ const matchesTableName = matches(relatedTableName) || matches(toAlias)
444
+ const matchesRelationName = matches(relationship.column)
445
+
423
446
  // this is the relationship which is being filtered
424
447
  if (
425
- (matches(relatedTableName) || matches(toAlias)) &&
448
+ (matchesTableName || matchesRelationName) &&
426
449
  relationship.to &&
427
450
  relationship.tableName
428
451
  ) {
429
- let subQuery = mainKnex
452
+ const joinTable = mainKnex
430
453
  .select(mainKnex.raw(1))
431
454
  .from({ [toAlias]: relatedTableName })
455
+ let subQuery = joinTable.clone()
432
456
  const manyToMany = validateManyToMany(relationship)
457
+ let updatedKey
458
+
459
+ if (!matchesTableName) {
460
+ updatedKey = filterKey.replace(
461
+ new RegExp(`^${relationship.column}.`),
462
+ `${aliases![relationship.tableName]}.`
463
+ )
464
+ } else {
465
+ updatedKey = filterKey
466
+ }
467
+
433
468
  if (manyToMany) {
434
469
  const throughAlias =
435
470
  aliases?.[manyToMany.through] || relationship.through
@@ -440,7 +475,6 @@ class InternalBuilder {
440
475
  subQuery = subQuery
441
476
  // add a join through the junction table
442
477
  .innerJoin(throughTable, function () {
443
- // @ts-ignore
444
478
  this.on(
445
479
  `${toAlias}.${manyToMany.toPrimary}`,
446
480
  "=",
@@ -460,18 +494,38 @@ class InternalBuilder {
460
494
  if (this.client === SqlClient.SQL_LITE) {
461
495
  subQuery = this.addJoinFieldCheck(subQuery, manyToMany)
462
496
  }
497
+
498
+ query = query.where(q => {
499
+ q.whereExists(whereCb(updatedKey, subQuery))
500
+ if (allowEmptyRelationships) {
501
+ q.orWhereNotExists(
502
+ joinTable.clone().innerJoin(throughTable, function () {
503
+ this.on(
504
+ `${fromAlias}.${manyToMany.fromPrimary}`,
505
+ "=",
506
+ `${throughAlias}.${manyToMany.from}`
507
+ )
508
+ })
509
+ )
510
+ }
511
+ })
463
512
  } else {
513
+ const toKey = `${toAlias}.${relationship.to}`
514
+ const foreignKey = `${fromAlias}.${relationship.from}`
464
515
  // "join" to the main table, making sure the ID matches that of the main
465
516
  subQuery = subQuery.where(
466
- `${toAlias}.${relationship.to}`,
517
+ toKey,
467
518
  "=",
468
- mainKnex.raw(
469
- this.quotedIdentifier(`${fromAlias}.${relationship.from}`)
470
- )
519
+ mainKnex.raw(this.quotedIdentifier(foreignKey))
471
520
  )
521
+
522
+ query = query.where(q => {
523
+ q.whereExists(whereCb(updatedKey, subQuery.clone()))
524
+ if (allowEmptyRelationships) {
525
+ q.orWhereNotExists(subQuery)
526
+ }
527
+ })
472
528
  }
473
- query = query.whereExists(whereCb(subQuery))
474
- break
475
529
  }
476
530
  }
477
531
  return query
@@ -502,6 +556,7 @@ class InternalBuilder {
502
556
  }
503
557
  function iterate(
504
558
  structure: AnySearchFilter,
559
+ operation: SearchFilterKey,
505
560
  fn: (
506
561
  query: Knex.QueryBuilder,
507
562
  key: string,
@@ -558,9 +613,14 @@ class InternalBuilder {
558
613
  if (allOr) {
559
614
  query = query.or
560
615
  }
561
- query = builder.addRelationshipForFilter(query, updatedKey, q => {
562
- return handleRelationship(q, updatedKey, value)
563
- })
616
+ query = builder.addRelationshipForFilter(
617
+ query,
618
+ allowEmptyRelationships[operation],
619
+ updatedKey,
620
+ (updatedKey, q) => {
621
+ return handleRelationship(q, updatedKey, value)
622
+ }
623
+ )
564
624
  }
565
625
  }
566
626
  }
@@ -592,7 +652,7 @@ class InternalBuilder {
592
652
  return `[${value.join(",")}]`
593
653
  }
594
654
  if (this.client === SqlClient.POSTGRES) {
595
- iterate(mode, (q, key, value) => {
655
+ iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
596
656
  const wrap = any ? "" : "'"
597
657
  const op = any ? "\\?| array" : "@>"
598
658
  const fieldNames = key.split(/\./g)
@@ -610,7 +670,7 @@ class InternalBuilder {
610
670
  this.client === SqlClient.MARIADB
611
671
  ) {
612
672
  const jsonFnc = any ? "JSON_OVERLAPS" : "JSON_CONTAINS"
613
- iterate(mode, (q, key, value) => {
673
+ iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
614
674
  return q[rawFnc](
615
675
  `${not}COALESCE(${jsonFnc}(${key}, '${stringifyArray(
616
676
  value
@@ -619,7 +679,7 @@ class InternalBuilder {
619
679
  })
620
680
  } else {
621
681
  const andOr = mode === filters?.containsAny ? " OR " : " AND "
622
- iterate(mode, (q, key, value) => {
682
+ iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
623
683
  let statement = ""
624
684
  const identifier = this.quotedIdentifier(key)
625
685
  for (let i in value) {
@@ -673,6 +733,7 @@ class InternalBuilder {
673
733
  const fnc = allOr ? "orWhereIn" : "whereIn"
674
734
  iterate(
675
735
  filters.oneOf,
736
+ ArrayOperator.ONE_OF,
676
737
  (q, key: string, array) => {
677
738
  if (this.client === SqlClient.ORACLE) {
678
739
  key = this.convertClobs(key)
@@ -697,7 +758,7 @@ class InternalBuilder {
697
758
  )
698
759
  }
699
760
  if (filters.string) {
700
- iterate(filters.string, (q, key, value) => {
761
+ iterate(filters.string, BasicOperator.STRING, (q, key, value) => {
701
762
  const fnc = allOr ? "orWhere" : "where"
702
763
  // postgres supports ilike, nothing else does
703
764
  if (this.client === SqlClient.POSTGRES) {
@@ -712,10 +773,10 @@ class InternalBuilder {
712
773
  })
713
774
  }
714
775
  if (filters.fuzzy) {
715
- iterate(filters.fuzzy, like)
776
+ iterate(filters.fuzzy, BasicOperator.FUZZY, like)
716
777
  }
717
778
  if (filters.range) {
718
- iterate(filters.range, (q, key, value) => {
779
+ iterate(filters.range, RangeOperator.RANGE, (q, key, value) => {
719
780
  const isEmptyObject = (val: any) => {
720
781
  return (
721
782
  val &&
@@ -781,7 +842,7 @@ class InternalBuilder {
781
842
  })
782
843
  }
783
844
  if (filters.equal) {
784
- iterate(filters.equal, (q, key, value) => {
845
+ iterate(filters.equal, BasicOperator.EQUAL, (q, key, value) => {
785
846
  const fnc = allOr ? "orWhereRaw" : "whereRaw"
786
847
  if (this.client === SqlClient.MS_SQL) {
787
848
  return q[fnc](
@@ -801,7 +862,7 @@ class InternalBuilder {
801
862
  })
802
863
  }
803
864
  if (filters.notEqual) {
804
- iterate(filters.notEqual, (q, key, value) => {
865
+ iterate(filters.notEqual, BasicOperator.NOT_EQUAL, (q, key, value) => {
805
866
  const fnc = allOr ? "orWhereRaw" : "whereRaw"
806
867
  if (this.client === SqlClient.MS_SQL) {
807
868
  return q[fnc](
@@ -822,13 +883,13 @@ class InternalBuilder {
822
883
  })
823
884
  }
824
885
  if (filters.empty) {
825
- iterate(filters.empty, (q, key) => {
886
+ iterate(filters.empty, BasicOperator.EMPTY, (q, key) => {
826
887
  const fnc = allOr ? "orWhereNull" : "whereNull"
827
888
  return q[fnc](key)
828
889
  })
829
890
  }
830
891
  if (filters.notEmpty) {
831
- iterate(filters.notEmpty, (q, key) => {
892
+ iterate(filters.notEmpty, BasicOperator.NOT_EMPTY, (q, key) => {
832
893
  const fnc = allOr ? "orWhereNotNull" : "whereNotNull"
833
894
  return q[fnc](key)
834
895
  })
@@ -1224,12 +1285,10 @@ class InternalBuilder {
1224
1285
  })
1225
1286
  : undefined
1226
1287
  if (!throughTable) {
1227
- // @ts-ignore
1228
1288
  query = query.leftJoin(toTableWithSchema, function () {
1229
1289
  for (let relationship of columns) {
1230
1290
  const from = relationship.from,
1231
1291
  to = relationship.to
1232
- // @ts-ignore
1233
1292
  this.orOn(`${fromAlias}.${from}`, "=", `${toAlias}.${to}`)
1234
1293
  }
1235
1294
  })
@@ -1240,7 +1299,6 @@ class InternalBuilder {
1240
1299
  for (let relationship of columns) {
1241
1300
  const fromPrimary = relationship.fromPrimary
1242
1301
  const from = relationship.from
1243
- // @ts-ignore
1244
1302
  this.orOn(
1245
1303
  `${fromAlias}.${fromPrimary}`,
1246
1304
  "=",
@@ -1252,7 +1310,6 @@ class InternalBuilder {
1252
1310
  for (let relationship of columns) {
1253
1311
  const toPrimary = relationship.toPrimary
1254
1312
  const to = relationship.to
1255
- // @ts-ignore
1256
1313
  this.orOn(`${toAlias}.${toPrimary}`, `${throughAlias}.${to}`)
1257
1314
  }
1258
1315
  })