@budibase/backend-core 2.28.7 → 2.29.1

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
@@ -1,10 +1,10 @@
1
1
  import { Knex, knex } from "knex"
2
2
  import * as dbCore from "../db"
3
3
  import {
4
- isIsoDateString,
5
- isValidFilter,
6
4
  getNativeSql,
7
5
  isExternalTable,
6
+ isIsoDateString,
7
+ isValidFilter,
8
8
  } from "./utils"
9
9
  import { SqlStatements } from "./sqlStatements"
10
10
  import SqlTableQueryBuilder from "./sqlTable"
@@ -12,21 +12,21 @@ import {
12
12
  BBReferenceFieldMetadata,
13
13
  FieldSchema,
14
14
  FieldType,
15
+ INTERNAL_TABLE_SOURCE_ID,
15
16
  JsonFieldMetadata,
17
+ JsonTypes,
16
18
  Operation,
19
+ prefixed,
17
20
  QueryJson,
18
- SqlQuery,
21
+ QueryOptions,
19
22
  RelationshipsJson,
20
23
  SearchFilters,
24
+ SortOrder,
25
+ SqlClient,
26
+ SqlQuery,
21
27
  SqlQueryBinding,
22
28
  Table,
23
29
  TableSourceType,
24
- INTERNAL_TABLE_SOURCE_ID,
25
- SqlClient,
26
- QueryOptions,
27
- JsonTypes,
28
- prefixed,
29
- SortOrder,
30
30
  } from "@budibase/types"
31
31
  import environment from "../environment"
32
32
  import { helpers } from "@budibase/shared-core"
@@ -114,7 +114,7 @@ function generateSelectStatement(
114
114
  ): (string | Knex.Raw)[] | "*" {
115
115
  const { resource, meta } = json
116
116
 
117
- if (!resource) {
117
+ if (!resource || !resource.fields || resource.fields.length === 0) {
118
118
  return "*"
119
119
  }
120
120
 
@@ -410,13 +410,32 @@ class InternalBuilder {
410
410
  return query
411
411
  }
412
412
 
413
+ addDistinctCount(
414
+ query: Knex.QueryBuilder,
415
+ json: QueryJson
416
+ ): Knex.QueryBuilder {
417
+ const table = json.meta.table
418
+ const primary = table.primary
419
+ const aliases = json.tableAliases
420
+ const aliased =
421
+ table.name && aliases?.[table.name] ? aliases[table.name] : table.name
422
+ if (!primary) {
423
+ throw new Error("SQL counting requires primary key to be supplied")
424
+ }
425
+ return query.countDistinct(`${aliased}.${primary[0]} as total`)
426
+ }
427
+
413
428
  addSorting(query: Knex.QueryBuilder, json: QueryJson): Knex.QueryBuilder {
414
- let { sort, paginate } = json
429
+ let { sort } = json
415
430
  const table = json.meta.table
431
+ const primaryKey = table.primary
416
432
  const tableName = getTableName(table)
417
433
  const aliases = json.tableAliases
418
434
  const aliased =
419
435
  tableName && aliases?.[tableName] ? aliases[tableName] : table?.name
436
+ if (!Array.isArray(primaryKey)) {
437
+ throw new Error("Sorting requires primary key to be specified for table")
438
+ }
420
439
  if (sort && Object.keys(sort || {}).length > 0) {
421
440
  for (let [key, value] of Object.entries(sort)) {
422
441
  const direction =
@@ -429,10 +448,9 @@ class InternalBuilder {
429
448
 
430
449
  query = query.orderBy(`${aliased}.${key}`, direction, nulls)
431
450
  }
432
- } else if (this.client === SqlClient.MS_SQL && paginate?.limit) {
433
- // @ts-ignore
434
- query = query.orderBy(`${aliased}.${table?.primary[0]}`)
435
451
  }
452
+ // always add sorting by the primary key - make sure result is deterministic
453
+ query = query.orderBy(`${aliased}.${primaryKey[0]}`)
436
454
  return query
437
455
  }
438
456
 
@@ -522,7 +540,7 @@ class InternalBuilder {
522
540
  })
523
541
  }
524
542
  }
525
- return query.limit(BASE_LIMIT)
543
+ return query
526
544
  }
527
545
 
528
546
  knexWithAlias(
@@ -533,13 +551,12 @@ class InternalBuilder {
533
551
  const tableName = endpoint.entityId
534
552
  const tableAlias = aliases?.[tableName]
535
553
 
536
- const query = knex(
554
+ return knex(
537
555
  this.tableNameWithSchema(tableName, {
538
556
  alias: tableAlias,
539
557
  schema: endpoint.schema,
540
558
  })
541
559
  )
542
- return query
543
560
  }
544
561
 
545
562
  create(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
@@ -571,52 +588,93 @@ class InternalBuilder {
571
588
  return query.insert(parsedBody)
572
589
  }
573
590
 
574
- read(knex: Knex, json: QueryJson, limit: number): Knex.QueryBuilder {
575
- let { endpoint, resource, filters, paginate, relationships, tableAliases } =
576
- json
591
+ bulkUpsert(knex: Knex, json: QueryJson): Knex.QueryBuilder {
592
+ const { endpoint, body } = json
593
+ let query = this.knexWithAlias(knex, endpoint)
594
+ if (!Array.isArray(body)) {
595
+ return query
596
+ }
597
+ const parsedBody = body.map(row => parseBody(row))
598
+ if (
599
+ this.client === SqlClient.POSTGRES ||
600
+ this.client === SqlClient.SQL_LITE ||
601
+ this.client === SqlClient.MY_SQL
602
+ ) {
603
+ const primary = json.meta.table.primary
604
+ if (!primary) {
605
+ throw new Error("Primary key is required for upsert")
606
+ }
607
+ return query.insert(parsedBody).onConflict(primary).merge()
608
+ } else if (this.client === SqlClient.MS_SQL) {
609
+ // No upsert or onConflict support in MSSQL yet, see:
610
+ // https://github.com/knex/knex/pull/6050
611
+ return query.insert(parsedBody)
612
+ }
613
+ return query.upsert(parsedBody)
614
+ }
615
+
616
+ read(
617
+ knex: Knex,
618
+ json: QueryJson,
619
+ opts: {
620
+ limits?: { base: number; query: number }
621
+ } = {}
622
+ ): Knex.QueryBuilder {
623
+ let { endpoint, filters, paginate, relationships, tableAliases } = json
624
+ const { limits } = opts
625
+ const counting = endpoint.operation === Operation.COUNT
577
626
 
578
627
  const tableName = endpoint.entityId
579
- // select all if not specified
580
- if (!resource) {
581
- resource = { fields: [] }
582
- }
583
- let selectStatement: string | (string | Knex.Raw)[] = "*"
584
- // handle select
585
- if (resource.fields && resource.fields.length > 0) {
586
- // select the resources as the format "table.columnName" - this is what is provided
587
- // by the resource builder further up
588
- selectStatement = generateSelectStatement(json, knex)
589
- }
590
- let foundLimit = limit || BASE_LIMIT
628
+ // start building the query
629
+ let query = this.knexWithAlias(knex, endpoint, tableAliases)
591
630
  // handle pagination
592
631
  let foundOffset: number | null = null
632
+ let foundLimit = limits?.query || limits?.base
593
633
  if (paginate && paginate.page && paginate.limit) {
594
634
  // @ts-ignore
595
635
  const page = paginate.page <= 1 ? 0 : paginate.page - 1
596
636
  const offset = page * paginate.limit
597
637
  foundLimit = paginate.limit
598
638
  foundOffset = offset
639
+ } else if (paginate && paginate.offset && paginate.limit) {
640
+ foundLimit = paginate.limit
641
+ foundOffset = paginate.offset
599
642
  } else if (paginate && paginate.limit) {
600
643
  foundLimit = paginate.limit
601
644
  }
602
- // start building the query
603
- let query = this.knexWithAlias(knex, endpoint, tableAliases)
604
- query = query.limit(foundLimit)
605
- if (foundOffset) {
606
- query = query.offset(foundOffset)
645
+ // counting should not sort, limit or offset
646
+ if (!counting) {
647
+ // add the found limit if supplied
648
+ if (foundLimit != null) {
649
+ query = query.limit(foundLimit)
650
+ }
651
+ // add overall pagination
652
+ if (foundOffset != null) {
653
+ query = query.offset(foundOffset)
654
+ }
655
+ // add sorting to pre-query
656
+ // no point in sorting when counting
657
+ query = this.addSorting(query, json)
607
658
  }
659
+ // add filters to the query (where)
608
660
  query = this.addFilters(query, filters, json.meta.table, {
609
661
  aliases: tableAliases,
610
662
  })
611
663
 
612
- // add sorting to pre-query
613
- query = this.addSorting(query, json)
614
664
  const alias = tableAliases?.[tableName] || tableName
615
- let preQuery = knex({
616
- [alias]: query,
617
- } as any).select(selectStatement) as any
665
+ let preQuery: Knex.QueryBuilder = knex({
666
+ // the typescript definition for the knex constructor doesn't support this
667
+ // syntax, but it is the only way to alias a pre-query result as part of
668
+ // a query - there is an alias dictionary type, but it assumes it can only
669
+ // be a table name, not a pre-query
670
+ [alias]: query as any,
671
+ })
672
+ // if counting, use distinct count, else select
673
+ preQuery = !counting
674
+ ? preQuery.select(generateSelectStatement(json, knex))
675
+ : this.addDistinctCount(preQuery, json)
618
676
  // have to add after as well (this breaks MS-SQL)
619
- if (this.client !== SqlClient.MS_SQL) {
677
+ if (this.client !== SqlClient.MS_SQL && !counting) {
620
678
  preQuery = this.addSorting(preQuery, json)
621
679
  }
622
680
  // handle joins
@@ -627,6 +685,13 @@ class InternalBuilder {
627
685
  endpoint.schema,
628
686
  tableAliases
629
687
  )
688
+
689
+ // add a base limit over the whole query
690
+ // if counting we can't set this limit
691
+ if (limits?.base) {
692
+ query = query.limit(limits.base)
693
+ }
694
+
630
695
  return this.addFilters(query, filters, json.meta.table, {
631
696
  relationship: true,
632
697
  aliases: tableAliases,
@@ -671,6 +736,19 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
671
736
  this.limit = limit
672
737
  }
673
738
 
739
+ private convertToNative(query: Knex.QueryBuilder, opts: QueryOptions = {}) {
740
+ const sqlClient = this.getSqlClient()
741
+ if (opts?.disableBindings) {
742
+ return { sql: query.toString() }
743
+ } else {
744
+ let native = getNativeSql(query)
745
+ if (sqlClient === SqlClient.SQL_LITE) {
746
+ native = convertBooleans(native)
747
+ }
748
+ return native
749
+ }
750
+ }
751
+
674
752
  /**
675
753
  * @param json The JSON query DSL which is to be converted to SQL.
676
754
  * @param opts extra options which are to be passed into the query builder, e.g. disableReturning
@@ -694,7 +772,16 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
694
772
  query = builder.create(client, json, opts)
695
773
  break
696
774
  case Operation.READ:
697
- query = builder.read(client, json, this.limit)
775
+ query = builder.read(client, json, {
776
+ limits: {
777
+ query: this.limit,
778
+ base: BASE_LIMIT,
779
+ },
780
+ })
781
+ break
782
+ case Operation.COUNT:
783
+ // read without any limits to count
784
+ query = builder.read(client, json)
698
785
  break
699
786
  case Operation.UPDATE:
700
787
  query = builder.update(client, json, opts)
@@ -705,6 +792,9 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
705
792
  case Operation.BULK_CREATE:
706
793
  query = builder.bulkCreate(client, json)
707
794
  break
795
+ case Operation.BULK_UPSERT:
796
+ query = builder.bulkUpsert(client, json)
797
+ break
708
798
  case Operation.CREATE_TABLE:
709
799
  case Operation.UPDATE_TABLE:
710
800
  case Operation.DELETE_TABLE:
@@ -713,15 +803,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
713
803
  throw `Operation type is not supported by SQL query builder`
714
804
  }
715
805
 
716
- if (opts?.disableBindings) {
717
- return { sql: query.toString() }
718
- } else {
719
- let native = getNativeSql(query)
720
- if (sqlClient === SqlClient.SQL_LITE) {
721
- native = convertBooleans(native)
722
- }
723
- return native
724
- }
806
+ return this.convertToNative(query, opts)
725
807
  }
726
808
 
727
809
  async getReturningRow(queryFn: QueryFunction, json: QueryJson) {
@@ -797,6 +879,9 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
797
879
  await this.getReturningRow(queryFn, this.checkLookupKeys(id, json))
798
880
  )
799
881
  }
882
+ if (operation === Operation.COUNT) {
883
+ return results
884
+ }
800
885
  if (operation !== Operation.READ) {
801
886
  return row
802
887
  }
package/src/tenancy/db.ts CHANGED
@@ -9,8 +9,13 @@ export function getTenantDB(tenantId: string) {
9
9
  export async function saveTenantInfo(tenantInfo: TenantInfo) {
10
10
  const db = getTenantDB(tenantInfo.tenantId)
11
11
  // save the tenant info to db
12
- return await db.put({
12
+ return db.put({
13
13
  _id: "tenant_info",
14
14
  ...tenantInfo,
15
15
  })
16
16
  }
17
+
18
+ export async function getTenantInfo(tenantId: string): Promise<TenantInfo> {
19
+ const db = getTenantDB(tenantId)
20
+ return db.get("tenant_info")
21
+ }