@budibase/backend-core 2.29.0 → 2.29.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.
@@ -1,14 +1,5 @@
1
- export const CONSTANT_INTERNAL_ROW_COLS = [
2
- "_id",
3
- "_rev",
4
- "type",
5
- "createdAt",
6
- "updatedAt",
7
- "tableId",
8
- ] as const
9
-
10
- export const CONSTANT_EXTERNAL_ROW_COLS = ["_id", "_rev", "tableId"] as const
11
-
12
- export function isInternalColumnName(name: string): boolean {
13
- return (CONSTANT_INTERNAL_ROW_COLS as readonly string[]).includes(name)
14
- }
1
+ export {
2
+ CONSTANT_INTERNAL_ROW_COLS,
3
+ CONSTANT_EXTERNAL_ROW_COLS,
4
+ isInternalColumnName,
5
+ } from "@budibase/shared-core"
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,9 +448,12 @@ 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]}`)
451
+ }
452
+
453
+ // add sorting by the primary key if the result isn't already sorted by it,
454
+ // to make sure result is deterministic
455
+ if (!sort || sort[primaryKey[0]] === undefined) {
456
+ query = query.orderBy(`${aliased}.${primaryKey[0]}`)
435
457
  }
436
458
  return query
437
459
  }
@@ -522,7 +544,7 @@ class InternalBuilder {
522
544
  })
523
545
  }
524
546
  }
525
- return query.limit(BASE_LIMIT)
547
+ return query
526
548
  }
527
549
 
528
550
  knexWithAlias(
@@ -533,13 +555,12 @@ class InternalBuilder {
533
555
  const tableName = endpoint.entityId
534
556
  const tableAlias = aliases?.[tableName]
535
557
 
536
- const query = knex(
558
+ return knex(
537
559
  this.tableNameWithSchema(tableName, {
538
560
  alias: tableAlias,
539
561
  schema: endpoint.schema,
540
562
  })
541
563
  )
542
- return query
543
564
  }
544
565
 
545
566
  create(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
@@ -587,7 +608,8 @@ class InternalBuilder {
587
608
  if (!primary) {
588
609
  throw new Error("Primary key is required for upsert")
589
610
  }
590
- return query.insert(parsedBody).onConflict(primary).merge()
611
+ const ret = query.insert(parsedBody).onConflict(primary).merge()
612
+ return ret
591
613
  } else if (this.client === SqlClient.MS_SQL) {
592
614
  // No upsert or onConflict support in MSSQL yet, see:
593
615
  // https://github.com/knex/knex/pull/6050
@@ -596,25 +618,23 @@ class InternalBuilder {
596
618
  return query.upsert(parsedBody)
597
619
  }
598
620
 
599
- read(knex: Knex, json: QueryJson, limit: number): Knex.QueryBuilder {
600
- let { endpoint, resource, filters, paginate, relationships, tableAliases } =
601
- json
621
+ read(
622
+ knex: Knex,
623
+ json: QueryJson,
624
+ opts: {
625
+ limits?: { base: number; query: number }
626
+ } = {}
627
+ ): Knex.QueryBuilder {
628
+ let { endpoint, filters, paginate, relationships, tableAliases } = json
629
+ const { limits } = opts
630
+ const counting = endpoint.operation === Operation.COUNT
602
631
 
603
632
  const tableName = endpoint.entityId
604
- // select all if not specified
605
- if (!resource) {
606
- resource = { fields: [] }
607
- }
608
- let selectStatement: string | (string | Knex.Raw)[] = "*"
609
- // handle select
610
- if (resource.fields && resource.fields.length > 0) {
611
- // select the resources as the format "table.columnName" - this is what is provided
612
- // by the resource builder further up
613
- selectStatement = generateSelectStatement(json, knex)
614
- }
615
- let foundLimit = limit || BASE_LIMIT
633
+ // start building the query
634
+ let query = this.knexWithAlias(knex, endpoint, tableAliases)
616
635
  // handle pagination
617
636
  let foundOffset: number | null = null
637
+ let foundLimit = limits?.query || limits?.base
618
638
  if (paginate && paginate.page && paginate.limit) {
619
639
  // @ts-ignore
620
640
  const page = paginate.page <= 1 ? 0 : paginate.page - 1
@@ -627,24 +647,39 @@ class InternalBuilder {
627
647
  } else if (paginate && paginate.limit) {
628
648
  foundLimit = paginate.limit
629
649
  }
630
- // start building the query
631
- let query = this.knexWithAlias(knex, endpoint, tableAliases)
632
- query = query.limit(foundLimit)
633
- if (foundOffset) {
634
- query = query.offset(foundOffset)
650
+ // counting should not sort, limit or offset
651
+ if (!counting) {
652
+ // add the found limit if supplied
653
+ if (foundLimit != null) {
654
+ query = query.limit(foundLimit)
655
+ }
656
+ // add overall pagination
657
+ if (foundOffset != null) {
658
+ query = query.offset(foundOffset)
659
+ }
660
+ // add sorting to pre-query
661
+ // no point in sorting when counting
662
+ query = this.addSorting(query, json)
635
663
  }
664
+ // add filters to the query (where)
636
665
  query = this.addFilters(query, filters, json.meta.table, {
637
666
  aliases: tableAliases,
638
667
  })
639
668
 
640
- // add sorting to pre-query
641
- query = this.addSorting(query, json)
642
669
  const alias = tableAliases?.[tableName] || tableName
643
- let preQuery = knex({
644
- [alias]: query,
645
- } as any).select(selectStatement) as any
670
+ let preQuery: Knex.QueryBuilder = knex({
671
+ // the typescript definition for the knex constructor doesn't support this
672
+ // syntax, but it is the only way to alias a pre-query result as part of
673
+ // a query - there is an alias dictionary type, but it assumes it can only
674
+ // be a table name, not a pre-query
675
+ [alias]: query as any,
676
+ })
677
+ // if counting, use distinct count, else select
678
+ preQuery = !counting
679
+ ? preQuery.select(generateSelectStatement(json, knex))
680
+ : this.addDistinctCount(preQuery, json)
646
681
  // have to add after as well (this breaks MS-SQL)
647
- if (this.client !== SqlClient.MS_SQL) {
682
+ if (this.client !== SqlClient.MS_SQL && !counting) {
648
683
  preQuery = this.addSorting(preQuery, json)
649
684
  }
650
685
  // handle joins
@@ -655,6 +690,13 @@ class InternalBuilder {
655
690
  endpoint.schema,
656
691
  tableAliases
657
692
  )
693
+
694
+ // add a base limit over the whole query
695
+ // if counting we can't set this limit
696
+ if (limits?.base) {
697
+ query = query.limit(limits.base)
698
+ }
699
+
658
700
  return this.addFilters(query, filters, json.meta.table, {
659
701
  relationship: true,
660
702
  aliases: tableAliases,
@@ -699,6 +741,19 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
699
741
  this.limit = limit
700
742
  }
701
743
 
744
+ private convertToNative(query: Knex.QueryBuilder, opts: QueryOptions = {}) {
745
+ const sqlClient = this.getSqlClient()
746
+ if (opts?.disableBindings) {
747
+ return { sql: query.toString() }
748
+ } else {
749
+ let native = getNativeSql(query)
750
+ if (sqlClient === SqlClient.SQL_LITE) {
751
+ native = convertBooleans(native)
752
+ }
753
+ return native
754
+ }
755
+ }
756
+
702
757
  /**
703
758
  * @param json The JSON query DSL which is to be converted to SQL.
704
759
  * @param opts extra options which are to be passed into the query builder, e.g. disableReturning
@@ -722,7 +777,16 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
722
777
  query = builder.create(client, json, opts)
723
778
  break
724
779
  case Operation.READ:
725
- query = builder.read(client, json, this.limit)
780
+ query = builder.read(client, json, {
781
+ limits: {
782
+ query: this.limit,
783
+ base: BASE_LIMIT,
784
+ },
785
+ })
786
+ break
787
+ case Operation.COUNT:
788
+ // read without any limits to count
789
+ query = builder.read(client, json)
726
790
  break
727
791
  case Operation.UPDATE:
728
792
  query = builder.update(client, json, opts)
@@ -744,15 +808,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
744
808
  throw `Operation type is not supported by SQL query builder`
745
809
  }
746
810
 
747
- if (opts?.disableBindings) {
748
- return { sql: query.toString() }
749
- } else {
750
- let native = getNativeSql(query)
751
- if (sqlClient === SqlClient.SQL_LITE) {
752
- native = convertBooleans(native)
753
- }
754
- return native
755
- }
811
+ return this.convertToNative(query, opts)
756
812
  }
757
813
 
758
814
  async getReturningRow(queryFn: QueryFunction, json: QueryJson) {
@@ -828,6 +884,9 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
828
884
  await this.getReturningRow(queryFn, this.checkLookupKeys(id, json))
829
885
  )
830
886
  }
887
+ if (operation === Operation.COUNT) {
888
+ return results
889
+ }
831
890
  if (operation !== Operation.READ) {
832
891
  return row
833
892
  }
@@ -109,8 +109,10 @@ function generateSchema(
109
109
  const { tableName } = breakExternalTableId(column.tableId)
110
110
  // @ts-ignore
111
111
  const relatedTable = tables[tableName]
112
- if (!relatedTable) {
113
- throw new Error("Referenced table doesn't exist")
112
+ if (!relatedTable || !relatedTable.primary) {
113
+ throw new Error(
114
+ "Referenced table doesn't exist or has no primary keys"
115
+ )
114
116
  }
115
117
  const relatedPrimary = relatedTable.primary[0]
116
118
  const externalType = relatedTable.schema[relatedPrimary].externalType
package/src/sql/utils.ts CHANGED
@@ -55,10 +55,7 @@ export function buildExternalTableId(datasourceId: string, tableName: string) {
55
55
  return `${datasourceId}${DOUBLE_SEPARATOR}${tableName}`
56
56
  }
57
57
 
58
- export function breakExternalTableId(tableId: string | undefined) {
59
- if (!tableId) {
60
- return {}
61
- }
58
+ export function breakExternalTableId(tableId: string) {
62
59
  const parts = tableId.split(DOUBLE_SEPARATOR)
63
60
  let datasourceId = parts.shift()
64
61
  // if they need joined
@@ -67,6 +64,9 @@ export function breakExternalTableId(tableId: string | undefined) {
67
64
  if (tableName.includes(ENCODED_SPACE)) {
68
65
  tableName = decodeURIComponent(tableName)
69
66
  }
67
+ if (!datasourceId || !tableName) {
68
+ throw new Error("Unable to get datasource/table name from table ID")
69
+ }
70
70
  return { datasourceId, tableName }
71
71
  }
72
72
 
@@ -24,7 +24,6 @@ export const account = (partial: Partial<Account> = {}): Account => {
24
24
  createdAt: Date.now(),
25
25
  verified: true,
26
26
  verificationSent: true,
27
- tier: "FREE", // DEPRECATED
28
27
  authType: AuthType.PASSWORD,
29
28
  name: generator.name(),
30
29
  size: "10+",