@budibase/backend-core 2.33.1 → 2.33.3

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
@@ -13,6 +13,7 @@ import SqlTableQueryBuilder from "./sqlTable"
13
13
  import {
14
14
  Aggregation,
15
15
  AnySearchFilter,
16
+ ArrayFilter,
16
17
  ArrayOperator,
17
18
  BasicOperator,
18
19
  BBReferenceFieldMetadata,
@@ -23,12 +24,14 @@ import {
23
24
  InternalSearchFilterOperator,
24
25
  JsonFieldMetadata,
25
26
  JsonTypes,
27
+ LogicalOperator,
26
28
  Operation,
27
29
  prefixed,
28
30
  QueryJson,
29
31
  QueryOptions,
30
32
  RangeOperator,
31
33
  RelationshipsJson,
34
+ SearchFilterKey,
32
35
  SearchFilters,
33
36
  SortOrder,
34
37
  SqlClient,
@@ -96,6 +99,39 @@ function isSqs(table: Table): boolean {
96
99
  )
97
100
  }
98
101
 
102
+ function escapeQuotes(value: string, quoteChar = '"'): string {
103
+ return value.replace(new RegExp(quoteChar, "g"), `${quoteChar}${quoteChar}`)
104
+ }
105
+
106
+ function wrap(value: string, quoteChar = '"'): string {
107
+ return `${quoteChar}${escapeQuotes(value, quoteChar)}${quoteChar}`
108
+ }
109
+
110
+ function stringifyArray(value: any[], quoteStyle = '"'): string {
111
+ for (let i in value) {
112
+ if (typeof value[i] === "string") {
113
+ value[i] = wrap(value[i], quoteStyle)
114
+ }
115
+ }
116
+ return `[${value.join(",")}]`
117
+ }
118
+
119
+ const allowEmptyRelationships: Record<SearchFilterKey, boolean> = {
120
+ [BasicOperator.EQUAL]: false,
121
+ [BasicOperator.NOT_EQUAL]: true,
122
+ [BasicOperator.EMPTY]: false,
123
+ [BasicOperator.NOT_EMPTY]: true,
124
+ [BasicOperator.FUZZY]: false,
125
+ [BasicOperator.STRING]: false,
126
+ [RangeOperator.RANGE]: false,
127
+ [ArrayOperator.CONTAINS]: false,
128
+ [ArrayOperator.NOT_CONTAINS]: true,
129
+ [ArrayOperator.CONTAINS_ANY]: false,
130
+ [ArrayOperator.ONE_OF]: false,
131
+ [LogicalOperator.AND]: false,
132
+ [LogicalOperator.OR]: false,
133
+ }
134
+
99
135
  class InternalBuilder {
100
136
  private readonly client: SqlClient
101
137
  private readonly query: QueryJson
@@ -134,30 +170,30 @@ class InternalBuilder {
134
170
  return this.query.meta.table
135
171
  }
136
172
 
173
+ get knexClient(): Knex.Client {
174
+ return this.knex.client as Knex.Client
175
+ }
176
+
137
177
  getFieldSchema(key: string): FieldSchema | undefined {
138
178
  const { column } = this.splitter.run(key)
139
179
  return this.table.schema[column]
140
180
  }
141
181
 
182
+ private supportsILike(): boolean {
183
+ return !(
184
+ this.client === SqlClient.ORACLE || this.client === SqlClient.SQL_LITE
185
+ )
186
+ }
187
+
142
188
  private quoteChars(): [string, string] {
143
- switch (this.client) {
144
- case SqlClient.ORACLE:
145
- case SqlClient.POSTGRES:
146
- return ['"', '"']
147
- case SqlClient.MS_SQL:
148
- return ["[", "]"]
149
- case SqlClient.MARIADB:
150
- case SqlClient.MY_SQL:
151
- case SqlClient.SQL_LITE:
152
- return ["`", "`"]
153
- }
189
+ const wrapped = this.knexClient.wrapIdentifier("foo", {})
190
+ return [wrapped[0], wrapped[wrapped.length - 1]]
154
191
  }
155
192
 
156
- // Takes a string like foo and returns a quoted string like [foo] for SQL Server
157
- // and "foo" for Postgres.
193
+ // Takes a string like foo and returns a quoted string like [foo] for SQL
194
+ // Server and "foo" for Postgres.
158
195
  private quote(str: string): string {
159
- const [start, end] = this.quoteChars()
160
- return `${start}${str}${end}`
196
+ return this.knexClient.wrapIdentifier(str, {})
161
197
  }
162
198
 
163
199
  private isQuoted(key: string): boolean {
@@ -175,6 +211,30 @@ class InternalBuilder {
175
211
  return key.map(part => this.quote(part)).join(".")
176
212
  }
177
213
 
214
+ private quotedValue(value: string): string {
215
+ const formatter = this.knexClient.formatter(this.knexClient.queryBuilder())
216
+ return formatter.wrap(value, false)
217
+ }
218
+
219
+ private rawQuotedValue(value: string): Knex.Raw {
220
+ return this.knex.raw(this.quotedValue(value))
221
+ }
222
+
223
+ // Unfortuantely we cannot rely on knex's identifier escaping because it trims
224
+ // the identifier string before escaping it, which breaks cases for us where
225
+ // columns that start or end with a space aren't referenced correctly anymore.
226
+ //
227
+ // So whenever you're using an identifier binding in knex, e.g. knex.raw("??
228
+ // as ?", ["foo", "bar"]), you need to make sure you call this:
229
+ //
230
+ // knex.raw("?? as ?", [this.quotedIdentifier("foo"), "bar"])
231
+ //
232
+ // Issue we filed against knex about this:
233
+ // https://github.com/knex/knex/issues/6143
234
+ private rawQuotedIdentifier(key: string): Knex.Raw {
235
+ return this.knex.raw(this.quotedIdentifier(key))
236
+ }
237
+
178
238
  // Turns an identifier like a.b.c or `a`.`b`.`c` into ["a", "b", "c"]
179
239
  private splitIdentifier(key: string): string[] {
180
240
  const [start, end] = this.quoteChars()
@@ -218,7 +278,7 @@ class InternalBuilder {
218
278
  const alias = this.getTableName(endpoint.entityId)
219
279
  const schema = meta.table.schema
220
280
  if (!this.isFullSelectStatementRequired()) {
221
- return [this.knex.raw(`${this.quote(alias)}.*`)]
281
+ return [this.knex.raw("??", [`${alias}.*`])]
222
282
  }
223
283
  // get just the fields for this table
224
284
  return resource.fields
@@ -240,30 +300,40 @@ class InternalBuilder {
240
300
  const columnSchema = schema[column]
241
301
 
242
302
  if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(columnSchema)) {
243
- return this.knex.raw(
244
- `${this.quotedIdentifier(
245
- [table, column].join(".")
246
- )}::money::numeric as ${this.quote(field)}`
247
- )
303
+ // TODO: figure out how to express this safely without string
304
+ // interpolation.
305
+ return this.knex.raw(`??::money::numeric as "${field}"`, [
306
+ this.rawQuotedIdentifier([table, column].join(".")),
307
+ field,
308
+ ])
248
309
  }
249
310
 
250
311
  if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(columnSchema)) {
251
312
  // Time gets returned as timestamp from mssql, not matching the expected
252
313
  // HH:mm format
253
- return this.knex.raw(`CONVERT(varchar, ${field}, 108) as "${field}"`)
314
+
315
+ // TODO: figure out how to express this safely without string
316
+ // interpolation.
317
+ return this.knex.raw(`CONVERT(varchar, ??, 108) as "${field}"`, [
318
+ this.rawQuotedIdentifier(field),
319
+ ])
254
320
  }
255
321
 
256
- const quoted = table
257
- ? `${this.quote(table)}.${this.quote(column)}`
258
- : this.quote(field)
259
- return this.knex.raw(quoted)
322
+ if (table) {
323
+ return this.rawQuotedIdentifier(`${table}.${column}`)
324
+ } else {
325
+ return this.rawQuotedIdentifier(field)
326
+ }
260
327
  })
261
328
  }
262
329
 
263
330
  // OracleDB can't use character-large-objects (CLOBs) in WHERE clauses,
264
331
  // so when we use them we need to wrap them in to_char(). This function
265
332
  // converts a field name to the appropriate identifier.
266
- private convertClobs(field: string, opts?: { forSelect?: boolean }): string {
333
+ private convertClobs(
334
+ field: string,
335
+ opts?: { forSelect?: boolean }
336
+ ): Knex.Raw {
267
337
  if (this.client !== SqlClient.ORACLE) {
268
338
  throw new Error(
269
339
  "you've called convertClobs on a DB that's not Oracle, this is a mistake"
@@ -272,7 +342,7 @@ class InternalBuilder {
272
342
  const parts = this.splitIdentifier(field)
273
343
  const col = parts.pop()!
274
344
  const schema = this.table.schema[col]
275
- let identifier = this.quotedIdentifier(field)
345
+ let identifier = this.rawQuotedIdentifier(field)
276
346
 
277
347
  if (
278
348
  schema.type === FieldType.STRING ||
@@ -283,9 +353,12 @@ class InternalBuilder {
283
353
  schema.type === FieldType.BARCODEQR
284
354
  ) {
285
355
  if (opts?.forSelect) {
286
- identifier = `to_char(${identifier}) as ${this.quotedIdentifier(col)}`
356
+ identifier = this.knex.raw("to_char(??) as ??", [
357
+ identifier,
358
+ this.rawQuotedIdentifier(col),
359
+ ])
287
360
  } else {
288
- identifier = `to_char(${identifier})`
361
+ identifier = this.knex.raw("to_char(??)", [identifier])
289
362
  }
290
363
  }
291
364
  return identifier
@@ -405,31 +478,47 @@ class InternalBuilder {
405
478
 
406
479
  addRelationshipForFilter(
407
480
  query: Knex.QueryBuilder,
481
+ allowEmptyRelationships: boolean,
408
482
  filterKey: string,
409
- whereCb: (query: Knex.QueryBuilder) => Knex.QueryBuilder
483
+ whereCb: (filterKey: string, query: Knex.QueryBuilder) => Knex.QueryBuilder
410
484
  ): Knex.QueryBuilder {
411
- const mainKnex = this.knex
412
485
  const { relationships, endpoint, tableAliases: aliases } = this.query
413
486
  const tableName = endpoint.entityId
414
487
  const fromAlias = aliases?.[tableName] || tableName
415
- const matches = (possibleTable: string) =>
416
- filterKey.startsWith(`${possibleTable}`)
488
+ const matches = (value: string) =>
489
+ filterKey.match(new RegExp(`^${value}\\.`))
417
490
  if (!relationships) {
418
491
  return query
419
492
  }
420
493
  for (const relationship of relationships) {
421
494
  const relatedTableName = relationship.tableName
422
495
  const toAlias = aliases?.[relatedTableName] || relatedTableName
496
+
497
+ const matchesTableName = matches(relatedTableName) || matches(toAlias)
498
+ const matchesRelationName = matches(relationship.column)
499
+
423
500
  // this is the relationship which is being filtered
424
501
  if (
425
- (matches(relatedTableName) || matches(toAlias)) &&
502
+ (matchesTableName || matchesRelationName) &&
426
503
  relationship.to &&
427
504
  relationship.tableName
428
505
  ) {
429
- let subQuery = mainKnex
430
- .select(mainKnex.raw(1))
506
+ const joinTable = this.knex
507
+ .select(this.knex.raw(1))
431
508
  .from({ [toAlias]: relatedTableName })
509
+ let subQuery = joinTable.clone()
432
510
  const manyToMany = validateManyToMany(relationship)
511
+ let updatedKey
512
+
513
+ if (!matchesTableName) {
514
+ updatedKey = filterKey.replace(
515
+ new RegExp(`^${relationship.column}.`),
516
+ `${aliases![relationship.tableName]}.`
517
+ )
518
+ } else {
519
+ updatedKey = filterKey
520
+ }
521
+
433
522
  if (manyToMany) {
434
523
  const throughAlias =
435
524
  aliases?.[manyToMany.through] || relationship.through
@@ -440,7 +529,6 @@ class InternalBuilder {
440
529
  subQuery = subQuery
441
530
  // add a join through the junction table
442
531
  .innerJoin(throughTable, function () {
443
- // @ts-ignore
444
532
  this.on(
445
533
  `${toAlias}.${manyToMany.toPrimary}`,
446
534
  "=",
@@ -451,27 +539,45 @@ class InternalBuilder {
451
539
  .where(
452
540
  `${throughAlias}.${manyToMany.from}`,
453
541
  "=",
454
- mainKnex.raw(
455
- this.quotedIdentifier(`${fromAlias}.${manyToMany.fromPrimary}`)
456
- )
542
+ this.rawQuotedIdentifier(`${fromAlias}.${manyToMany.fromPrimary}`)
457
543
  )
458
544
  // in SQS the same junction table is used for different many-to-many relationships between the
459
545
  // two same tables, this is needed to avoid rows ending up in all columns
460
546
  if (this.client === SqlClient.SQL_LITE) {
461
547
  subQuery = this.addJoinFieldCheck(subQuery, manyToMany)
462
548
  }
549
+
550
+ query = query.where(q => {
551
+ q.whereExists(whereCb(updatedKey, subQuery))
552
+ if (allowEmptyRelationships) {
553
+ q.orWhereNotExists(
554
+ joinTable.clone().innerJoin(throughTable, function () {
555
+ this.on(
556
+ `${fromAlias}.${manyToMany.fromPrimary}`,
557
+ "=",
558
+ `${throughAlias}.${manyToMany.from}`
559
+ )
560
+ })
561
+ )
562
+ }
563
+ })
463
564
  } else {
565
+ const toKey = `${toAlias}.${relationship.to}`
566
+ const foreignKey = `${fromAlias}.${relationship.from}`
464
567
  // "join" to the main table, making sure the ID matches that of the main
465
568
  subQuery = subQuery.where(
466
- `${toAlias}.${relationship.to}`,
569
+ toKey,
467
570
  "=",
468
- mainKnex.raw(
469
- this.quotedIdentifier(`${fromAlias}.${relationship.from}`)
470
- )
571
+ this.rawQuotedIdentifier(foreignKey)
471
572
  )
573
+
574
+ query = query.where(q => {
575
+ q.whereExists(whereCb(updatedKey, subQuery.clone()))
576
+ if (allowEmptyRelationships) {
577
+ q.orWhereNotExists(subQuery)
578
+ }
579
+ })
472
580
  }
473
- query = query.whereExists(whereCb(subQuery))
474
- break
475
581
  }
476
582
  }
477
583
  return query
@@ -492,7 +598,7 @@ class InternalBuilder {
492
598
  filters = this.parseFilters({ ...filters })
493
599
  const aliases = this.query.tableAliases
494
600
  // if all or specified in filters, then everything is an or
495
- const allOr = filters.allOr
601
+ const shouldOr = filters.allOr
496
602
  const isSqlite = this.client === SqlClient.SQL_LITE
497
603
  const tableName = isSqlite ? this.table._id! : this.table.name
498
604
 
@@ -502,6 +608,7 @@ class InternalBuilder {
502
608
  }
503
609
  function iterate(
504
610
  structure: AnySearchFilter,
611
+ operation: SearchFilterKey,
505
612
  fn: (
506
613
  query: Knex.QueryBuilder,
507
614
  key: string,
@@ -555,96 +662,118 @@ class InternalBuilder {
555
662
  value
556
663
  )
557
664
  } else if (shouldProcessRelationship) {
558
- if (allOr) {
665
+ if (shouldOr) {
559
666
  query = query.or
560
667
  }
561
- query = builder.addRelationshipForFilter(query, updatedKey, q => {
562
- return handleRelationship(q, updatedKey, value)
563
- })
668
+ query = builder.addRelationshipForFilter(
669
+ query,
670
+ allowEmptyRelationships[operation],
671
+ updatedKey,
672
+ (updatedKey, q) => {
673
+ return handleRelationship(q, updatedKey, value)
674
+ }
675
+ )
564
676
  }
565
677
  }
566
678
  }
567
679
 
568
680
  const like = (q: Knex.QueryBuilder, key: string, value: any) => {
569
- const fuzzyOr = filters?.fuzzyOr
570
- const fnc = fuzzyOr || allOr ? "orWhere" : "where"
571
- // postgres supports ilike, nothing else does
572
- if (this.client === SqlClient.POSTGRES) {
573
- return q[fnc](key, "ilike", `%${value}%`)
574
- } else {
575
- const rawFnc = `${fnc}Raw`
576
- // @ts-ignore
577
- return q[rawFnc](`LOWER(${this.quotedIdentifier(key)}) LIKE ?`, [
681
+ if (filters?.fuzzyOr || shouldOr) {
682
+ q = q.or
683
+ }
684
+ if (
685
+ this.client === SqlClient.ORACLE ||
686
+ this.client === SqlClient.SQL_LITE
687
+ ) {
688
+ return q.whereRaw(`LOWER(??) LIKE ?`, [
689
+ this.rawQuotedIdentifier(key),
578
690
  `%${value.toLowerCase()}%`,
579
691
  ])
580
692
  }
693
+ return q.whereILike(
694
+ // @ts-expect-error knex types are wrong, raw is fine here
695
+ this.rawQuotedIdentifier(key),
696
+ this.knex.raw("?", [`%${value}%`])
697
+ )
581
698
  }
582
699
 
583
- const contains = (mode: AnySearchFilter, any: boolean = false) => {
584
- const rawFnc = allOr ? "orWhereRaw" : "whereRaw"
585
- const not = mode === filters?.notContains ? "NOT " : ""
586
- function stringifyArray(value: Array<any>, quoteStyle = '"'): string {
587
- for (let i in value) {
588
- if (typeof value[i] === "string") {
589
- value[i] = `${quoteStyle}${value[i]}${quoteStyle}`
590
- }
700
+ const contains = (mode: ArrayFilter, any = false) => {
701
+ function addModifiers<T extends {}, Q>(q: Knex.QueryBuilder<T, Q>) {
702
+ if (shouldOr || mode === filters?.containsAny) {
703
+ q = q.or
591
704
  }
592
- return `[${value.join(",")}]`
705
+ if (mode === filters?.notContains) {
706
+ q = q.not
707
+ }
708
+ return q
593
709
  }
710
+
594
711
  if (this.client === SqlClient.POSTGRES) {
595
- iterate(mode, (q, key, value) => {
596
- const wrap = any ? "" : "'"
597
- const op = any ? "\\?| array" : "@>"
598
- const fieldNames = key.split(/\./g)
599
- const table = fieldNames[0]
600
- const col = fieldNames[1]
601
- return q[rawFnc](
602
- `${not}COALESCE("${table}"."${col}"::jsonb ${op} ${wrap}${stringifyArray(
603
- value,
604
- any ? "'" : '"'
605
- )}${wrap}, FALSE)`
606
- )
712
+ iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
713
+ q = addModifiers(q)
714
+ if (any) {
715
+ return q.whereRaw(`COALESCE(??::jsonb \\?| array??, FALSE)`, [
716
+ this.rawQuotedIdentifier(key),
717
+ this.knex.raw(stringifyArray(value, "'")),
718
+ ])
719
+ } else {
720
+ return q.whereRaw(`COALESCE(??::jsonb @> '??', FALSE)`, [
721
+ this.rawQuotedIdentifier(key),
722
+ this.knex.raw(stringifyArray(value)),
723
+ ])
724
+ }
607
725
  })
608
726
  } else if (
609
727
  this.client === SqlClient.MY_SQL ||
610
728
  this.client === SqlClient.MARIADB
611
729
  ) {
612
- const jsonFnc = any ? "JSON_OVERLAPS" : "JSON_CONTAINS"
613
- iterate(mode, (q, key, value) => {
614
- return q[rawFnc](
615
- `${not}COALESCE(${jsonFnc}(${key}, '${stringifyArray(
616
- value
617
- )}'), FALSE)`
618
- )
730
+ iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
731
+ return addModifiers(q).whereRaw(`COALESCE(?(??, ?), FALSE)`, [
732
+ this.knex.raw(any ? "JSON_OVERLAPS" : "JSON_CONTAINS"),
733
+ this.rawQuotedIdentifier(key),
734
+ this.knex.raw(wrap(stringifyArray(value))),
735
+ ])
619
736
  })
620
737
  } else {
621
- const andOr = mode === filters?.containsAny ? " OR " : " AND "
622
- iterate(mode, (q, key, value) => {
623
- let statement = ""
624
- const identifier = this.quotedIdentifier(key)
625
- for (let i in value) {
626
- if (typeof value[i] === "string") {
627
- value[i] = `%"${value[i].toLowerCase()}"%`
628
- } else {
629
- value[i] = `%${value[i]}%`
630
- }
631
- statement += `${
632
- statement ? andOr : ""
633
- }COALESCE(LOWER(${identifier}), '') LIKE ?`
634
- }
635
-
636
- if (statement === "") {
738
+ iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
739
+ if (value.length === 0) {
637
740
  return q
638
741
  }
639
742
 
640
- if (not) {
641
- return q[rawFnc](
642
- `(NOT (${statement}) OR ${identifier} IS NULL)`,
643
- value
644
- )
645
- } else {
646
- return q[rawFnc](statement, value)
647
- }
743
+ q = q.where(subQuery => {
744
+ if (mode === filters?.notContains) {
745
+ subQuery = subQuery.not
746
+ }
747
+
748
+ subQuery = subQuery.where(subSubQuery => {
749
+ for (const elem of value) {
750
+ if (mode === filters?.containsAny) {
751
+ subSubQuery = subSubQuery.or
752
+ } else {
753
+ subSubQuery = subSubQuery.and
754
+ }
755
+
756
+ const lower =
757
+ typeof elem === "string" ? `"${elem.toLowerCase()}"` : elem
758
+
759
+ subSubQuery = subSubQuery.whereLike(
760
+ // @ts-expect-error knex types are wrong, raw is fine here
761
+ this.knex.raw(`COALESCE(LOWER(??), '')`, [
762
+ this.rawQuotedIdentifier(key),
763
+ ]),
764
+ `%${lower}%`
765
+ )
766
+ }
767
+ })
768
+ if (mode === filters?.notContains) {
769
+ subQuery = subQuery.or.whereNull(
770
+ // @ts-expect-error knex types are wrong, raw is fine here
771
+ this.rawQuotedIdentifier(key)
772
+ )
773
+ }
774
+ return subQuery
775
+ })
776
+ return q
648
777
  })
649
778
  }
650
779
  }
@@ -670,52 +799,54 @@ class InternalBuilder {
670
799
  }
671
800
 
672
801
  if (filters.oneOf) {
673
- const fnc = allOr ? "orWhereIn" : "whereIn"
674
802
  iterate(
675
803
  filters.oneOf,
804
+ ArrayOperator.ONE_OF,
676
805
  (q, key: string, array) => {
806
+ if (shouldOr) {
807
+ q = q.or
808
+ }
677
809
  if (this.client === SqlClient.ORACLE) {
810
+ // @ts-ignore
678
811
  key = this.convertClobs(key)
679
- array = Array.isArray(array) ? array : [array]
680
- const binding = new Array(array.length).fill("?").join(",")
681
- return q.whereRaw(`${key} IN (${binding})`, array)
682
- } else {
683
- return q[fnc](key, Array.isArray(array) ? array : [array])
684
812
  }
813
+ return q.whereIn(key, Array.isArray(array) ? array : [array])
685
814
  },
686
815
  (q, key: string[], array) => {
816
+ if (shouldOr) {
817
+ q = q.or
818
+ }
687
819
  if (this.client === SqlClient.ORACLE) {
688
- const keyStr = `(${key.map(k => this.convertClobs(k)).join(",")})`
689
- const binding = `(${array
690
- .map((a: any) => `(${new Array(a.length).fill("?").join(",")})`)
691
- .join(",")})`
692
- return q.whereRaw(`${keyStr} IN ${binding}`, array.flat())
693
- } else {
694
- return q[fnc](key, Array.isArray(array) ? array : [array])
820
+ // @ts-ignore
821
+ key = key.map(k => this.convertClobs(k))
695
822
  }
823
+ return q.whereIn(key, Array.isArray(array) ? array : [array])
696
824
  }
697
825
  )
698
826
  }
699
827
  if (filters.string) {
700
- iterate(filters.string, (q, key, value) => {
701
- const fnc = allOr ? "orWhere" : "where"
702
- // postgres supports ilike, nothing else does
703
- if (this.client === SqlClient.POSTGRES) {
704
- return q[fnc](key, "ilike", `${value}%`)
705
- } else {
706
- const rawFnc = `${fnc}Raw`
707
- // @ts-ignore
708
- return q[rawFnc](`LOWER(${this.quotedIdentifier(key)}) LIKE ?`, [
828
+ iterate(filters.string, BasicOperator.STRING, (q, key, value) => {
829
+ if (shouldOr) {
830
+ q = q.or
831
+ }
832
+ if (
833
+ this.client === SqlClient.ORACLE ||
834
+ this.client === SqlClient.SQL_LITE
835
+ ) {
836
+ return q.whereRaw(`LOWER(??) LIKE ?`, [
837
+ this.rawQuotedIdentifier(key),
709
838
  `${value.toLowerCase()}%`,
710
839
  ])
840
+ } else {
841
+ return q.whereILike(key, `${value}%`)
711
842
  }
712
843
  })
713
844
  }
714
845
  if (filters.fuzzy) {
715
- iterate(filters.fuzzy, like)
846
+ iterate(filters.fuzzy, BasicOperator.FUZZY, like)
716
847
  }
717
848
  if (filters.range) {
718
- iterate(filters.range, (q, key, value) => {
849
+ iterate(filters.range, RangeOperator.RANGE, (q, key, value) => {
719
850
  const isEmptyObject = (val: any) => {
720
851
  return (
721
852
  val &&
@@ -734,103 +865,109 @@ class InternalBuilder {
734
865
 
735
866
  const schema = this.getFieldSchema(key)
736
867
 
868
+ let rawKey: string | Knex.Raw = key
869
+ let high = value.high
870
+ let low = value.low
871
+
737
872
  if (this.client === SqlClient.ORACLE) {
738
- // @ts-ignore
739
- key = this.knex.raw(this.convertClobs(key))
873
+ rawKey = this.convertClobs(key)
874
+ } else if (
875
+ this.client === SqlClient.SQL_LITE &&
876
+ schema?.type === FieldType.BIGINT
877
+ ) {
878
+ rawKey = this.knex.raw("CAST(?? AS INTEGER)", [
879
+ this.rawQuotedIdentifier(key),
880
+ ])
881
+ high = this.knex.raw("CAST(? AS INTEGER)", [value.high])
882
+ low = this.knex.raw("CAST(? AS INTEGER)", [value.low])
883
+ }
884
+
885
+ if (shouldOr) {
886
+ q = q.or
740
887
  }
741
888
 
742
889
  if (lowValid && highValid) {
743
- if (
744
- schema?.type === FieldType.BIGINT &&
745
- this.client === SqlClient.SQL_LITE
746
- ) {
747
- return q.whereRaw(
748
- `CAST(${key} AS INTEGER) BETWEEN CAST(? AS INTEGER) AND CAST(? AS INTEGER)`,
749
- [value.low, value.high]
750
- )
751
- } else {
752
- const fnc = allOr ? "orWhereBetween" : "whereBetween"
753
- return q[fnc](key, [value.low, value.high])
754
- }
890
+ // @ts-expect-error knex types are wrong, raw is fine here
891
+ return q.whereBetween(rawKey, [low, high])
755
892
  } else if (lowValid) {
756
- if (
757
- schema?.type === FieldType.BIGINT &&
758
- this.client === SqlClient.SQL_LITE
759
- ) {
760
- return q.whereRaw(`CAST(${key} AS INTEGER) >= CAST(? AS INTEGER)`, [
761
- value.low,
762
- ])
763
- } else {
764
- const fnc = allOr ? "orWhere" : "where"
765
- return q[fnc](key, ">=", value.low)
766
- }
893
+ // @ts-expect-error knex types are wrong, raw is fine here
894
+ return q.where(rawKey, ">=", low)
767
895
  } else if (highValid) {
768
- if (
769
- schema?.type === FieldType.BIGINT &&
770
- this.client === SqlClient.SQL_LITE
771
- ) {
772
- return q.whereRaw(`CAST(${key} AS INTEGER) <= CAST(? AS INTEGER)`, [
773
- value.high,
774
- ])
775
- } else {
776
- const fnc = allOr ? "orWhere" : "where"
777
- return q[fnc](key, "<=", value.high)
778
- }
896
+ // @ts-expect-error knex types are wrong, raw is fine here
897
+ return q.where(rawKey, "<=", high)
779
898
  }
780
899
  return q
781
900
  })
782
901
  }
783
902
  if (filters.equal) {
784
- iterate(filters.equal, (q, key, value) => {
785
- const fnc = allOr ? "orWhereRaw" : "whereRaw"
903
+ iterate(filters.equal, BasicOperator.EQUAL, (q, key, value) => {
904
+ if (shouldOr) {
905
+ q = q.or
906
+ }
786
907
  if (this.client === SqlClient.MS_SQL) {
787
- return q[fnc](
788
- `CASE WHEN ${this.quotedIdentifier(key)} = ? THEN 1 ELSE 0 END = 1`,
789
- [value]
790
- )
791
- } else if (this.client === SqlClient.ORACLE) {
792
- const identifier = this.convertClobs(key)
793
- return q[fnc](`(${identifier} IS NOT NULL AND ${identifier} = ?)`, [
908
+ return q.whereRaw(`CASE WHEN ?? = ? THEN 1 ELSE 0 END = 1`, [
909
+ this.rawQuotedIdentifier(key),
794
910
  value,
795
911
  ])
912
+ } else if (this.client === SqlClient.ORACLE) {
913
+ const identifier = this.convertClobs(key)
914
+ return q.where(subq =>
915
+ // @ts-expect-error knex types are wrong, raw is fine here
916
+ subq.whereNotNull(identifier).andWhere(identifier, value)
917
+ )
796
918
  } else {
797
- return q[fnc](`COALESCE(${this.quotedIdentifier(key)} = ?, FALSE)`, [
919
+ return q.whereRaw(`COALESCE(?? = ?, FALSE)`, [
920
+ this.rawQuotedIdentifier(key),
798
921
  value,
799
922
  ])
800
923
  }
801
924
  })
802
925
  }
803
926
  if (filters.notEqual) {
804
- iterate(filters.notEqual, (q, key, value) => {
805
- const fnc = allOr ? "orWhereRaw" : "whereRaw"
927
+ iterate(filters.notEqual, BasicOperator.NOT_EQUAL, (q, key, value) => {
928
+ if (shouldOr) {
929
+ q = q.or
930
+ }
806
931
  if (this.client === SqlClient.MS_SQL) {
807
- return q[fnc](
808
- `CASE WHEN ${this.quotedIdentifier(key)} = ? THEN 1 ELSE 0 END = 0`,
809
- [value]
810
- )
932
+ return q.whereRaw(`CASE WHEN ?? = ? THEN 1 ELSE 0 END = 0`, [
933
+ this.rawQuotedIdentifier(key),
934
+ value,
935
+ ])
811
936
  } else if (this.client === SqlClient.ORACLE) {
812
937
  const identifier = this.convertClobs(key)
813
- return q[fnc](
814
- `(${identifier} IS NOT NULL AND ${identifier} != ?) OR ${identifier} IS NULL`,
815
- [value]
938
+ return (
939
+ q
940
+ .where(subq =>
941
+ subq.not
942
+ // @ts-expect-error knex types are wrong, raw is fine here
943
+ .whereNull(identifier)
944
+ .and.where(identifier, "!=", value)
945
+ )
946
+ // @ts-expect-error knex types are wrong, raw is fine here
947
+ .or.whereNull(identifier)
816
948
  )
817
949
  } else {
818
- return q[fnc](`COALESCE(${this.quotedIdentifier(key)} != ?, TRUE)`, [
950
+ return q.whereRaw(`COALESCE(?? != ?, TRUE)`, [
951
+ this.rawQuotedIdentifier(key),
819
952
  value,
820
953
  ])
821
954
  }
822
955
  })
823
956
  }
824
957
  if (filters.empty) {
825
- iterate(filters.empty, (q, key) => {
826
- const fnc = allOr ? "orWhereNull" : "whereNull"
827
- return q[fnc](key)
958
+ iterate(filters.empty, BasicOperator.EMPTY, (q, key) => {
959
+ if (shouldOr) {
960
+ q = q.or
961
+ }
962
+ return q.whereNull(key)
828
963
  })
829
964
  }
830
965
  if (filters.notEmpty) {
831
- iterate(filters.notEmpty, (q, key) => {
832
- const fnc = allOr ? "orWhereNotNull" : "whereNotNull"
833
- return q[fnc](key)
966
+ iterate(filters.notEmpty, BasicOperator.NOT_EMPTY, (q, key) => {
967
+ if (shouldOr) {
968
+ q = q.or
969
+ }
970
+ return q.whereNotNull(key)
834
971
  })
835
972
  }
836
973
  if (filters.contains) {
@@ -915,9 +1052,7 @@ class InternalBuilder {
915
1052
  const selectFields = qualifiedFields.map(field =>
916
1053
  this.convertClobs(field, { forSelect: true })
917
1054
  )
918
- query = query
919
- .groupByRaw(groupByFields.join(", "))
920
- .select(this.knex.raw(selectFields.join(", ")))
1055
+ query = query.groupBy(groupByFields).select(selectFields)
921
1056
  } else {
922
1057
  query = query.groupBy(qualifiedFields).select(qualifiedFields)
923
1058
  }
@@ -929,11 +1064,10 @@ class InternalBuilder {
929
1064
  if (this.client === SqlClient.ORACLE) {
930
1065
  const field = this.convertClobs(`${tableName}.${aggregation.field}`)
931
1066
  query = query.select(
932
- this.knex.raw(
933
- `COUNT(DISTINCT ${field}) as ${this.quotedIdentifier(
934
- aggregation.name
935
- )}`
936
- )
1067
+ this.knex.raw(`COUNT(DISTINCT ??) as ??`, [
1068
+ field,
1069
+ aggregation.name,
1070
+ ])
937
1071
  )
938
1072
  } else {
939
1073
  query = query.countDistinct(
@@ -998,9 +1132,11 @@ class InternalBuilder {
998
1132
  } else {
999
1133
  let composite = `${aliased}.${key}`
1000
1134
  if (this.client === SqlClient.ORACLE) {
1001
- query = query.orderByRaw(
1002
- `${this.convertClobs(composite)} ${direction} nulls ${nulls}`
1003
- )
1135
+ query = query.orderByRaw(`?? ?? nulls ??`, [
1136
+ this.convertClobs(composite),
1137
+ this.knex.raw(direction),
1138
+ this.knex.raw(nulls as string),
1139
+ ])
1004
1140
  } else {
1005
1141
  query = query.orderBy(composite, direction, nulls)
1006
1142
  }
@@ -1030,17 +1166,22 @@ class InternalBuilder {
1030
1166
 
1031
1167
  private buildJsonField(field: string): string {
1032
1168
  const parts = field.split(".")
1033
- let tableField: string, unaliased: string
1169
+ let unaliased: string
1170
+
1171
+ let tableField: string
1034
1172
  if (parts.length > 1) {
1035
1173
  const alias = parts.shift()!
1036
1174
  unaliased = parts.join(".")
1037
- tableField = `${this.quote(alias)}.${this.quote(unaliased)}`
1175
+ tableField = `${alias}.${unaliased}`
1038
1176
  } else {
1039
1177
  unaliased = parts.join(".")
1040
- tableField = this.quote(unaliased)
1178
+ tableField = unaliased
1041
1179
  }
1180
+
1042
1181
  const separator = this.client === SqlClient.ORACLE ? " VALUE " : ","
1043
- return `'${unaliased}'${separator}${tableField}`
1182
+ return this.knex
1183
+ .raw(`?${separator}??`, [unaliased, this.rawQuotedIdentifier(tableField)])
1184
+ .toString()
1044
1185
  }
1045
1186
 
1046
1187
  maxFunctionParameters() {
@@ -1136,13 +1277,13 @@ class InternalBuilder {
1136
1277
  subQuery = subQuery.where(
1137
1278
  correlatedTo,
1138
1279
  "=",
1139
- knex.raw(this.quotedIdentifier(correlatedFrom))
1280
+ this.rawQuotedIdentifier(correlatedFrom)
1140
1281
  )
1141
1282
 
1142
- const standardWrap = (select: string): Knex.QueryBuilder => {
1283
+ const standardWrap = (select: Knex.Raw): Knex.QueryBuilder => {
1143
1284
  subQuery = subQuery.select(`${toAlias}.*`).limit(getRelationshipLimit())
1144
1285
  // @ts-ignore - the from alias syntax isn't in Knex typing
1145
- return knex.select(knex.raw(select)).from({
1286
+ return knex.select(select).from({
1146
1287
  [toAlias]: subQuery,
1147
1288
  })
1148
1289
  }
@@ -1152,12 +1293,12 @@ class InternalBuilder {
1152
1293
  // need to check the junction table document is to the right column, this is just for SQS
1153
1294
  subQuery = this.addJoinFieldCheck(subQuery, relationship)
1154
1295
  wrapperQuery = standardWrap(
1155
- `json_group_array(json_object(${fieldList}))`
1296
+ this.knex.raw(`json_group_array(json_object(${fieldList}))`)
1156
1297
  )
1157
1298
  break
1158
1299
  case SqlClient.POSTGRES:
1159
1300
  wrapperQuery = standardWrap(
1160
- `json_agg(json_build_object(${fieldList}))`
1301
+ this.knex.raw(`json_agg(json_build_object(${fieldList}))`)
1161
1302
  )
1162
1303
  break
1163
1304
  case SqlClient.MARIADB:
@@ -1171,21 +1312,25 @@ class InternalBuilder {
1171
1312
  case SqlClient.MY_SQL:
1172
1313
  case SqlClient.ORACLE:
1173
1314
  wrapperQuery = standardWrap(
1174
- `json_arrayagg(json_object(${fieldList}))`
1315
+ this.knex.raw(`json_arrayagg(json_object(${fieldList}))`)
1175
1316
  )
1176
1317
  break
1177
- case SqlClient.MS_SQL:
1318
+ case SqlClient.MS_SQL: {
1319
+ const comparatorQuery = knex
1320
+ .select(`${fromAlias}.*`)
1321
+ // @ts-ignore - from alias syntax not TS supported
1322
+ .from({
1323
+ [fromAlias]: subQuery
1324
+ .select(`${toAlias}.*`)
1325
+ .limit(getRelationshipLimit()),
1326
+ })
1327
+
1178
1328
  wrapperQuery = knex.raw(
1179
- `(SELECT ${this.quote(toAlias)} = (${knex
1180
- .select(`${fromAlias}.*`)
1181
- // @ts-ignore - from alias syntax not TS supported
1182
- .from({
1183
- [fromAlias]: subQuery
1184
- .select(`${toAlias}.*`)
1185
- .limit(getRelationshipLimit()),
1186
- })} FOR JSON PATH))`
1329
+ `(SELECT ?? = (${comparatorQuery} FOR JSON PATH))`,
1330
+ [this.rawQuotedIdentifier(toAlias)]
1187
1331
  )
1188
1332
  break
1333
+ }
1189
1334
  default:
1190
1335
  throw new Error(`JSON relationships not implement for ${sqlClient}`)
1191
1336
  }
@@ -1224,12 +1369,10 @@ class InternalBuilder {
1224
1369
  })
1225
1370
  : undefined
1226
1371
  if (!throughTable) {
1227
- // @ts-ignore
1228
1372
  query = query.leftJoin(toTableWithSchema, function () {
1229
1373
  for (let relationship of columns) {
1230
1374
  const from = relationship.from,
1231
1375
  to = relationship.to
1232
- // @ts-ignore
1233
1376
  this.orOn(`${fromAlias}.${from}`, "=", `${toAlias}.${to}`)
1234
1377
  }
1235
1378
  })
@@ -1240,7 +1383,6 @@ class InternalBuilder {
1240
1383
  for (let relationship of columns) {
1241
1384
  const fromPrimary = relationship.fromPrimary
1242
1385
  const from = relationship.from
1243
- // @ts-ignore
1244
1386
  this.orOn(
1245
1387
  `${fromAlias}.${fromPrimary}`,
1246
1388
  "=",
@@ -1252,7 +1394,6 @@ class InternalBuilder {
1252
1394
  for (let relationship of columns) {
1253
1395
  const toPrimary = relationship.toPrimary
1254
1396
  const to = relationship.to
1255
- // @ts-ignore
1256
1397
  this.orOn(`${toAlias}.${toPrimary}`, `${throughAlias}.${to}`)
1257
1398
  }
1258
1399
  })