diff --git a/lerna.json b/lerna.json
index abce1679c8..9d04750a0d 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "2.28.7",
+ "version": "2.29.0",
"npmClient": "yarn",
"packages": [
"packages/*",
diff --git a/packages/account-portal b/packages/account-portal
index 247f56d455..b600cca314 160000
--- a/packages/account-portal
+++ b/packages/account-portal
@@ -1 +1 @@
-Subproject commit 247f56d455abbd64da17d865275ed978f577549f
+Subproject commit b600cca314a5cc9971e44d46047d1a0019b46b08
diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/db/constants.ts
index bfa7595d62..69c98fe569 100644
--- a/packages/backend-core/src/db/constants.ts
+++ b/packages/backend-core/src/db/constants.ts
@@ -1,14 +1,5 @@
-export const CONSTANT_INTERNAL_ROW_COLS = [
- "_id",
- "_rev",
- "type",
- "createdAt",
- "updatedAt",
- "tableId",
-] as const
-
-export const CONSTANT_EXTERNAL_ROW_COLS = ["_id", "_rev", "tableId"] as const
-
-export function isInternalColumnName(name: string): boolean {
- return (CONSTANT_INTERNAL_ROW_COLS as readonly string[]).includes(name)
-}
+export {
+ CONSTANT_INTERNAL_ROW_COLS,
+ CONSTANT_EXTERNAL_ROW_COLS,
+ isInternalColumnName,
+} from "@budibase/shared-core"
diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts
index 7d52665f39..74201d1e34 100644
--- a/packages/backend-core/src/sql/sql.ts
+++ b/packages/backend-core/src/sql/sql.ts
@@ -1,10 +1,10 @@
import { Knex, knex } from "knex"
import * as dbCore from "../db"
import {
- isIsoDateString,
- isValidFilter,
getNativeSql,
isExternalTable,
+ isIsoDateString,
+ isValidFilter,
} from "./utils"
import { SqlStatements } from "./sqlStatements"
import SqlTableQueryBuilder from "./sqlTable"
@@ -12,21 +12,21 @@ import {
BBReferenceFieldMetadata,
FieldSchema,
FieldType,
+ INTERNAL_TABLE_SOURCE_ID,
JsonFieldMetadata,
+ JsonTypes,
Operation,
+ prefixed,
QueryJson,
- SqlQuery,
+ QueryOptions,
RelationshipsJson,
SearchFilters,
+ SortOrder,
+ SqlClient,
+ SqlQuery,
SqlQueryBinding,
Table,
TableSourceType,
- INTERNAL_TABLE_SOURCE_ID,
- SqlClient,
- QueryOptions,
- JsonTypes,
- prefixed,
- SortOrder,
} from "@budibase/types"
import environment from "../environment"
import { helpers } from "@budibase/shared-core"
@@ -114,7 +114,7 @@ function generateSelectStatement(
): (string | Knex.Raw)[] | "*" {
const { resource, meta } = json
- if (!resource) {
+ if (!resource || !resource.fields || resource.fields.length === 0) {
return "*"
}
@@ -410,13 +410,32 @@ class InternalBuilder {
return query
}
- addSorting(query: Knex.QueryBuilder, json: QueryJson): Knex.QueryBuilder {
- let { sort, paginate } = json
+ addDistinctCount(
+ query: Knex.QueryBuilder,
+ json: QueryJson
+ ): Knex.QueryBuilder {
const table = json.meta.table
+ const primary = table.primary
+ const aliases = json.tableAliases
+ const aliased =
+ table.name && aliases?.[table.name] ? aliases[table.name] : table.name
+ if (!primary) {
+ throw new Error("SQL counting requires primary key to be supplied")
+ }
+ return query.countDistinct(`${aliased}.${primary[0]} as total`)
+ }
+
+ addSorting(query: Knex.QueryBuilder, json: QueryJson): Knex.QueryBuilder {
+ let { sort } = json
+ const table = json.meta.table
+ const primaryKey = table.primary
const tableName = getTableName(table)
const aliases = json.tableAliases
const aliased =
tableName && aliases?.[tableName] ? aliases[tableName] : table?.name
+ if (!Array.isArray(primaryKey)) {
+ throw new Error("Sorting requires primary key to be specified for table")
+ }
if (sort && Object.keys(sort || {}).length > 0) {
for (let [key, value] of Object.entries(sort)) {
const direction =
@@ -429,10 +448,9 @@ class InternalBuilder {
query = query.orderBy(`${aliased}.${key}`, direction, nulls)
}
- } else if (this.client === SqlClient.MS_SQL && paginate?.limit) {
- // @ts-ignore
- query = query.orderBy(`${aliased}.${table?.primary[0]}`)
}
+ // always add sorting by the primary key - make sure result is deterministic
+ query = query.orderBy(`${aliased}.${primaryKey[0]}`)
return query
}
@@ -522,7 +540,7 @@ class InternalBuilder {
})
}
}
- return query.limit(BASE_LIMIT)
+ return query
}
knexWithAlias(
@@ -533,13 +551,12 @@ class InternalBuilder {
const tableName = endpoint.entityId
const tableAlias = aliases?.[tableName]
- const query = knex(
+ return knex(
this.tableNameWithSchema(tableName, {
alias: tableAlias,
schema: endpoint.schema,
})
)
- return query
}
create(knex: Knex, json: QueryJson, opts: QueryOptions): Knex.QueryBuilder {
@@ -597,25 +614,23 @@ class InternalBuilder {
return query.upsert(parsedBody)
}
- read(knex: Knex, json: QueryJson, limit: number): Knex.QueryBuilder {
- let { endpoint, resource, filters, paginate, relationships, tableAliases } =
- json
+ read(
+ knex: Knex,
+ json: QueryJson,
+ opts: {
+ limits?: { base: number; query: number }
+ } = {}
+ ): Knex.QueryBuilder {
+ let { endpoint, filters, paginate, relationships, tableAliases } = json
+ const { limits } = opts
+ const counting = endpoint.operation === Operation.COUNT
const tableName = endpoint.entityId
- // select all if not specified
- if (!resource) {
- resource = { fields: [] }
- }
- let selectStatement: string | (string | Knex.Raw)[] = "*"
- // handle select
- if (resource.fields && resource.fields.length > 0) {
- // select the resources as the format "table.columnName" - this is what is provided
- // by the resource builder further up
- selectStatement = generateSelectStatement(json, knex)
- }
- let foundLimit = limit || BASE_LIMIT
+ // start building the query
+ let query = this.knexWithAlias(knex, endpoint, tableAliases)
// handle pagination
let foundOffset: number | null = null
+ let foundLimit = limits?.query || limits?.base
if (paginate && paginate.page && paginate.limit) {
// @ts-ignore
const page = paginate.page <= 1 ? 0 : paginate.page - 1
@@ -628,24 +643,39 @@ class InternalBuilder {
} else if (paginate && paginate.limit) {
foundLimit = paginate.limit
}
- // start building the query
- let query = this.knexWithAlias(knex, endpoint, tableAliases)
- query = query.limit(foundLimit)
- if (foundOffset) {
- query = query.offset(foundOffset)
+ // counting should not sort, limit or offset
+ if (!counting) {
+ // add the found limit if supplied
+ if (foundLimit != null) {
+ query = query.limit(foundLimit)
+ }
+ // add overall pagination
+ if (foundOffset != null) {
+ query = query.offset(foundOffset)
+ }
+ // add sorting to pre-query
+ // no point in sorting when counting
+ query = this.addSorting(query, json)
}
+ // add filters to the query (where)
query = this.addFilters(query, filters, json.meta.table, {
aliases: tableAliases,
})
- // add sorting to pre-query
- query = this.addSorting(query, json)
const alias = tableAliases?.[tableName] || tableName
- let preQuery = knex({
- [alias]: query,
- } as any).select(selectStatement) as any
+ let preQuery: Knex.QueryBuilder = knex({
+ // the typescript definition for the knex constructor doesn't support this
+ // syntax, but it is the only way to alias a pre-query result as part of
+ // a query - there is an alias dictionary type, but it assumes it can only
+ // be a table name, not a pre-query
+ [alias]: query as any,
+ })
+ // if counting, use distinct count, else select
+ preQuery = !counting
+ ? preQuery.select(generateSelectStatement(json, knex))
+ : this.addDistinctCount(preQuery, json)
// have to add after as well (this breaks MS-SQL)
- if (this.client !== SqlClient.MS_SQL) {
+ if (this.client !== SqlClient.MS_SQL && !counting) {
preQuery = this.addSorting(preQuery, json)
}
// handle joins
@@ -656,6 +686,13 @@ class InternalBuilder {
endpoint.schema,
tableAliases
)
+
+ // add a base limit over the whole query
+ // if counting we can't set this limit
+ if (limits?.base) {
+ query = query.limit(limits.base)
+ }
+
return this.addFilters(query, filters, json.meta.table, {
relationship: true,
aliases: tableAliases,
@@ -700,6 +737,19 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
this.limit = limit
}
+ private convertToNative(query: Knex.QueryBuilder, opts: QueryOptions = {}) {
+ const sqlClient = this.getSqlClient()
+ if (opts?.disableBindings) {
+ return { sql: query.toString() }
+ } else {
+ let native = getNativeSql(query)
+ if (sqlClient === SqlClient.SQL_LITE) {
+ native = convertBooleans(native)
+ }
+ return native
+ }
+ }
+
/**
* @param json The JSON query DSL which is to be converted to SQL.
* @param opts extra options which are to be passed into the query builder, e.g. disableReturning
@@ -723,7 +773,16 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
query = builder.create(client, json, opts)
break
case Operation.READ:
- query = builder.read(client, json, this.limit)
+ query = builder.read(client, json, {
+ limits: {
+ query: this.limit,
+ base: BASE_LIMIT,
+ },
+ })
+ break
+ case Operation.COUNT:
+ // read without any limits to count
+ query = builder.read(client, json)
break
case Operation.UPDATE:
query = builder.update(client, json, opts)
@@ -745,15 +804,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
throw `Operation type is not supported by SQL query builder`
}
- if (opts?.disableBindings) {
- return { sql: query.toString() }
- } else {
- let native = getNativeSql(query)
- if (sqlClient === SqlClient.SQL_LITE) {
- native = convertBooleans(native)
- }
- return native
- }
+ return this.convertToNative(query, opts)
}
async getReturningRow(queryFn: QueryFunction, json: QueryJson) {
@@ -829,6 +880,9 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
await this.getReturningRow(queryFn, this.checkLookupKeys(id, json))
)
}
+ if (operation === Operation.COUNT) {
+ return results
+ }
if (operation !== Operation.READ) {
return row
}
diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
index 17ecd8f844..d79eedd194 100644
--- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
+++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
@@ -17,6 +17,8 @@
SWITCHABLE_TYPES,
ValidColumnNameRegex,
helpers,
+ CONSTANT_INTERNAL_ROW_COLS,
+ CONSTANT_EXTERNAL_ROW_COLS,
} from "@budibase/shared-core"
import { createEventDispatcher, getContext, onMount } from "svelte"
import { cloneDeep } from "lodash/fp"
@@ -52,7 +54,6 @@
const DATE_TYPE = FieldType.DATETIME
const dispatch = createEventDispatcher()
- const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
const { dispatch: gridDispatch, rows } = getContext("grid")
export let field
@@ -487,20 +488,27 @@
})
}
const newError = {}
+ const prohibited = externalTable
+ ? CONSTANT_EXTERNAL_ROW_COLS
+ : CONSTANT_INTERNAL_ROW_COLS
if (!externalTable && fieldInfo.name?.startsWith("_")) {
newError.name = `Column name cannot start with an underscore.`
} else if (fieldInfo.name && !fieldInfo.name.match(ValidColumnNameRegex)) {
newError.name = `Illegal character; must be alpha-numeric.`
- } else if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) {
- newError.name = `${PROHIBITED_COLUMN_NAMES.join(
+ } else if (
+ prohibited.some(
+ name => fieldInfo?.name?.toLowerCase() === name.toLowerCase()
+ )
+ ) {
+ newError.name = `${prohibited.join(
", "
- )} are not allowed as column names`
+ )} are not allowed as column names - case insensitive.`
} else if (inUse($tables.selected, fieldInfo.name, originalName)) {
newError.name = `Column name already in use.`
}
if (fieldInfo.type === FieldType.AUTO && !fieldInfo.subtype) {
- newError.subtype = `Auto Column requires a type`
+ newError.subtype = `Auto Column requires a type.`
}
if (fieldInfo.fieldName && fieldInfo.tableId) {
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte
index 5b7844ce53..fd3521d597 100644
--- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte
+++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte
@@ -53,6 +53,12 @@
placeholder="Are you sure you want to delete?"
bind:value={parameters.confirmText}
/>
+
+
+
+
+
+
{/if}
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DuplicateRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DuplicateRow.svelte
index 3b4a7c2d38..b6cdd663fd 100644
--- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DuplicateRow.svelte
+++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DuplicateRow.svelte
@@ -83,6 +83,12 @@
placeholder="Are you sure you want to duplicate this row?"
bind:value={parameters.confirmText}
/>
+
+
+
+
+
+
{/if}
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ExecuteQuery.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ExecuteQuery.svelte
index 54295d8b0f..43797f6369 100644
--- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ExecuteQuery.svelte
+++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ExecuteQuery.svelte
@@ -74,6 +74,18 @@
placeholder="Are you sure you want to execute this query?"
bind:value={parameters.confirmText}
/>
+
+
+
+
{/if}
{#if query?.parameters?.length > 0}
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte
index d834e9aac9..aed2618778 100644
--- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte
+++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte
@@ -80,6 +80,12 @@
placeholder="Are you sure you want to save this row?"
bind:value={parameters.confirmText}
/>
+
+
+
+
+
+
{/if}
diff --git a/packages/client/src/components/overlay/ConfirmationDisplay.svelte b/packages/client/src/components/overlay/ConfirmationDisplay.svelte
index e7a1046191..b96af502df 100644
--- a/packages/client/src/components/overlay/ConfirmationDisplay.svelte
+++ b/packages/client/src/components/overlay/ConfirmationDisplay.svelte
@@ -8,6 +8,8 @@
{$confirmationStore.text}
diff --git a/packages/client/src/stores/confirmation.js b/packages/client/src/stores/confirmation.js
index bb9a54386f..3fbf3d5deb 100644
--- a/packages/client/src/stores/confirmation.js
+++ b/packages/client/src/stores/confirmation.js
@@ -4,6 +4,8 @@ const initialState = {
showConfirmation: false,
title: null,
text: null,
+ confirmButtonText: null,
+ cancelButtonText: null,
onConfirm: null,
onCancel: null,
}
@@ -11,11 +13,20 @@ const initialState = {
const createConfirmationStore = () => {
const store = writable(initialState)
- const showConfirmation = (title, text, onConfirm, onCancel) => {
+ const showConfirmation = (
+ title,
+ text,
+ onConfirm,
+ onCancel,
+ confirmButtonText,
+ cancelButtonText
+ ) => {
store.set({
showConfirmation: true,
title,
text,
+ confirmButtonText,
+ cancelButtonText,
onConfirm,
onCancel,
})
diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js
index bd220b8e85..8f0cb575a7 100644
--- a/packages/client/src/utils/buttonActions.js
+++ b/packages/client/src/utils/buttonActions.js
@@ -522,6 +522,7 @@ const confirmTextMap = {
["Execute Query"]: "Are you sure you want to execute this query?",
["Trigger Automation"]: "Are you sure you want to trigger this automation?",
["Prompt User"]: "Are you sure you want to continue?",
+ ["Duplicate Row"]: "Are you sure you want to duplicate this row?",
}
/**
@@ -582,6 +583,11 @@ export const enrichButtonActions = (actions, context) => {
const defaultTitleText = action["##eventHandlerType"]
const customTitleText =
action.parameters?.customTitleText || defaultTitleText
+ const cancelButtonText =
+ action.parameters?.cancelButtonText || "Cancel"
+ const confirmButtonText =
+ action.parameters?.confirmButtonText || "Confirm"
+
confirmationStore.actions.showConfirmation(
customTitleText,
confirmText,
@@ -612,7 +618,9 @@ export const enrichButtonActions = (actions, context) => {
},
() => {
resolve(false)
- }
+ },
+ confirmButtonText,
+ cancelButtonText
)
})
}
diff --git a/packages/server/package.json b/packages/server/package.json
index 28b1e876c2..e146bd081c 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -109,7 +109,7 @@
"serialize-error": "^7.0.1",
"server-destroy": "1.0.1",
"snowflake-promise": "^4.5.0",
- "socket.io": "4.6.1",
+ "socket.io": "4.6.2",
"tar": "6.2.1",
"to-json-schema": "0.2.5",
"uuid": "^8.3.2",
diff --git a/packages/server/scripts/integrations/postgres/init.sql b/packages/server/scripts/integrations/postgres/init.sql
index b7ce1b7d5b..9624208deb 100644
--- a/packages/server/scripts/integrations/postgres/init.sql
+++ b/packages/server/scripts/integrations/postgres/init.sql
@@ -54,8 +54,31 @@ INSERT INTO Persons (FirstName, LastName, Address, City, Type, Year) VALUES ('Mi
INSERT INTO Persons (FirstName, LastName, Address, City, Type, Year) VALUES ('John', 'Smith', '64 Updown Road', 'Dublin', 'programmer', 1996);
INSERT INTO Persons (FirstName, LastName, Address, City, Type, Age, Year) VALUES ('Foo', 'Bar', 'Foo Street', 'Bartown', 'support', 0, 1993);
INSERT INTO Persons (FirstName, LastName, Address, City, Type) VALUES ('Jonny', 'Muffin', 'Muffin Street', 'Cork', 'support');
-INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed) VALUES (1, 2, 'assembling', TRUE);
-INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed) VALUES (2, 1, 'processing', FALSE);
+INSERT INTO Persons (FirstName, LastName, Address, City, Type, Age, Year) VALUES ('Dave', 'Bar', '2 Foo Street', 'Bartown', 'support', 0, 1993);
+INSERT INTO Persons (FirstName, LastName, Address, City, Type, Age, Year) VALUES ('James', 'Bar', '3 Foo Street', 'Bartown', 'support', 0, 1993);
+INSERT INTO Persons (FirstName, LastName, Address, City, Type, Age, Year) VALUES ('Jenny', 'Bar', '4 Foo Street', 'Bartown', 'support', 0, 1993);
+INSERT INTO Persons (FirstName, LastName, Address, City, Type, Age, Year) VALUES ('Grace', 'Bar', '5 Foo Street', 'Bartown', 'support', 0, 1993);
+INSERT INTO Persons (FirstName, LastName, Address, City, Type, Age, Year) VALUES ('Sarah', 'Bar', '6 Foo Street', 'Bartown', 'support', 0, 1993);
+INSERT INTO Persons (FirstName, LastName, Address, City, Type, Age, Year) VALUES ('Kelly', 'Bar', '7 Foo Street', 'Bartown', 'support', 0, 1993);
+
+-- insert a lot of tasks for testing
+WITH RECURSIVE generate_series AS (
+ SELECT 1 AS n
+ UNION ALL
+ SELECT n + 1 FROM generate_series WHERE n < 6000
+),
+random_data AS (
+ SELECT
+ n,
+ (random() * 9 + 1)::int AS ExecutorID,
+ (random() * 9 + 1)::int AS QaID,
+ 'assembling' AS TaskName,
+ (random() < 0.5) AS Completed
+ FROM generate_series
+)
+INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed)
+SELECT ExecutorID, QaID, TaskName, Completed
+FROM random_data;
INSERT INTO Products (ProductName) VALUES ('Computers');
INSERT INTO Products (ProductName) VALUES ('Laptops');
INSERT INTO Products (ProductName) VALUES ('Chairs');
diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts
index 121a1b6b9b..0ad55a78c3 100644
--- a/packages/server/src/api/controllers/row/ExternalRequest.ts
+++ b/packages/server/src/api/controllers/row/ExternalRequest.ts
@@ -7,6 +7,7 @@ import {
FieldType,
FilterType,
IncludeRelationship,
+ isManyToOne,
OneToManyRelationshipFieldMetadata,
Operation,
PaginationJson,
@@ -16,29 +17,33 @@ import {
SortJson,
SortType,
Table,
- isManyToOne,
} from "@budibase/types"
import {
breakExternalTableId,
breakRowIdField,
convertRowId,
+ generateRowIdField,
isRowId,
isSQL,
- generateRowIdField,
} from "../../../integrations/utils"
import {
buildExternalRelationships,
buildSqlFieldList,
generateIdForRow,
- sqlOutputProcessing,
+ isKnexEmptyReadResponse,
isManyToMany,
+ sqlOutputProcessing,
} from "./utils"
-import { getDatasourceAndQuery } from "../../../sdk/app/rows/utils"
+import {
+ getDatasourceAndQuery,
+ processRowCountResponse,
+} from "../../../sdk/app/rows/utils"
import { processObjectSync } from "@budibase/string-templates"
import { cloneDeep } from "lodash/fp"
import { db as dbCore } from "@budibase/backend-core"
import sdk from "../../../sdk"
import env from "../../../environment"
+import { makeExternalQuery } from "../../../integrations/base/query"
export interface ManyRelationship {
tableId?: string
@@ -60,6 +65,13 @@ export interface RunConfig {
includeSqlRelationships?: IncludeRelationship
}
+export type ExternalRequestReturnType =
+ T extends Operation.READ
+ ? Row[]
+ : T extends Operation.COUNT
+ ? number
+ : { row: Row; table: Table }
+
function buildFilters(
id: string | undefined | string[],
filters: SearchFilters,
@@ -223,9 +235,6 @@ function isEditableColumn(column: FieldSchema) {
return !(isExternalAutoColumn || isFormula)
}
-export type ExternalRequestReturnType =
- T extends Operation.READ ? Row[] : { row: Row; table: Table }
-
export class ExternalRequest {
private readonly operation: T
private readonly tableId: string
@@ -428,7 +437,9 @@ export class ExternalRequest {
})
// this is the response from knex if no rows found
const rows: Row[] =
- !Array.isArray(response) || response?.[0].read ? [] : response
+ !Array.isArray(response) || isKnexEmptyReadResponse(response)
+ ? []
+ : response
const storeTo = isManyToMany(field)
? field.throughFrom || linkPrimaryKey
: fieldName
@@ -517,7 +528,7 @@ export class ExternalRequest {
// finally cleanup anything that needs to be removed
for (let [colName, { isMany, rows, tableId }] of Object.entries(related)) {
const table: Table | undefined = this.getTable(tableId)
- // if its not the foreign key skip it, nothing to do
+ // if it's not the foreign key skip it, nothing to do
if (
!table ||
(!isMany && table.primary && table.primary.indexOf(colName) !== -1)
@@ -663,12 +674,14 @@ export class ExternalRequest {
}
// aliasing can be disabled fully if desired
- let response
- if (env.SQL_ALIASING_DISABLE) {
- response = await getDatasourceAndQuery(json)
- } else {
- const aliasing = new sdk.rows.AliasTables(Object.keys(this.tables))
- response = await aliasing.queryWithAliasing(json)
+ const aliasing = new sdk.rows.AliasTables(Object.keys(this.tables))
+ let response = env.SQL_ALIASING_DISABLE
+ ? await getDatasourceAndQuery(json)
+ : await aliasing.queryWithAliasing(json, makeExternalQuery)
+
+ // if it's a counting operation there will be no more processing, just return the number
+ if (this.operation === Operation.COUNT) {
+ return processRowCountResponse(response) as ExternalRequestReturnType
}
const responseRows = Array.isArray(response) ? response : []
diff --git a/packages/server/src/api/controllers/row/utils/sqlUtils.ts b/packages/server/src/api/controllers/row/utils/sqlUtils.ts
index 372b8394ff..6f7bdc7335 100644
--- a/packages/server/src/api/controllers/row/utils/sqlUtils.ts
+++ b/packages/server/src/api/controllers/row/utils/sqlUtils.ts
@@ -1,4 +1,6 @@
import {
+ DatasourcePlusQueryResponse,
+ DSPlusOperation,
FieldType,
ManyToManyRelationshipFieldMetadata,
RelationshipFieldMetadata,
@@ -192,3 +194,11 @@ export function buildSqlFieldList(
}
return fields
}
+
+export function isKnexEmptyReadResponse(resp: DatasourcePlusQueryResponse) {
+ return (
+ !Array.isArray(resp) ||
+ resp.length === 0 ||
+ (DSPlusOperation.READ in resp[0] && resp[0].read === true)
+ )
+}
diff --git a/packages/server/src/api/controllers/row/utils/utils.ts b/packages/server/src/api/controllers/row/utils/utils.ts
index c2d62e0204..ae34034221 100644
--- a/packages/server/src/api/controllers/row/utils/utils.ts
+++ b/packages/server/src/api/controllers/row/utils/utils.ts
@@ -14,7 +14,7 @@ import {
processDates,
processFormulas,
} from "../../../../utilities/rowProcessor"
-import { updateRelationshipColumns } from "./sqlUtils"
+import { isKnexEmptyReadResponse, updateRelationshipColumns } from "./sqlUtils"
import {
basicProcessing,
generateIdForRow,
@@ -137,7 +137,7 @@ export async function sqlOutputProcessing(
relationships: RelationshipsJson[],
opts?: { sqs?: boolean }
): Promise {
- if (!Array.isArray(rows) || rows.length === 0 || rows[0].read === true) {
+ if (isKnexEmptyReadResponse(rows)) {
return []
}
let finalRows: { [key: string]: Row } = {}
diff --git a/packages/server/src/api/controllers/row/views.ts b/packages/server/src/api/controllers/row/views.ts
index 80aa97d8c0..63ce12f0ab 100644
--- a/packages/server/src/api/controllers/row/views.ts
+++ b/packages/server/src/api/controllers/row/views.ts
@@ -69,6 +69,7 @@ export async function searchView(
limit: body.limit,
bookmark: body.bookmark,
paginate: body.paginate,
+ countRows: body.countRows,
}
const result = await sdk.rows.search(searchOptions)
diff --git a/packages/server/src/api/routes/row.ts b/packages/server/src/api/routes/row.ts
index f1aa39a461..e443b2daeb 100644
--- a/packages/server/src/api/routes/row.ts
+++ b/packages/server/src/api/routes/row.ts
@@ -86,6 +86,7 @@ router
router.post(
"/api/v2/views/:viewId/search",
+ internalSearchValidator(),
authorizedResource(PermissionType.VIEW, PermissionLevel.READ, "viewId"),
rowController.views.searchView
)
diff --git a/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts b/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts
index b060a099d8..e72a091688 100644
--- a/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts
+++ b/packages/server/src/api/routes/tests/queries/generic-sql.spec.ts
@@ -734,6 +734,7 @@ describe.each(
name: entityId,
schema: {},
type: "table",
+ primary: ["id"],
sourceId: datasource._id!,
sourceType: TableSourceType.EXTERNAL,
},
diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts
index f651908c01..cff966ab89 100644
--- a/packages/server/src/api/routes/tests/search.spec.ts
+++ b/packages/server/src/api/routes/tests/search.spec.ts
@@ -18,6 +18,7 @@ import {
User,
Row,
RelationshipType,
+ SearchResponse,
} from "@budibase/types"
import _ from "lodash"
import tk from "timekeeper"
@@ -92,16 +93,14 @@ describe.each([
class SearchAssertion {
constructor(private readonly query: RowSearchParams) {}
- private async performSearch(): Promise {
+ private async performSearch(): Promise> {
if (isInMemory) {
return dataFilters.search(_.cloneDeep(rows), this.query)
} else {
- return (
- await config.api.row.search(table._id!, {
- ...this.query,
- tableId: table._id!,
- })
- ).rows
+ return config.api.row.search(table._id!, {
+ ...this.query,
+ tableId: table._id!,
+ })
}
}
@@ -175,7 +174,7 @@ describe.each([
// different to the one passed in will cause the assertion to fail. Extra
// rows returned by the query will also cause the assertion to fail.
async toMatchExactly(expectedRows: any[]) {
- const foundRows = await this.performSearch()
+ const { rows: foundRows } = await this.performSearch()
// eslint-disable-next-line jest/no-standalone-expect
expect(foundRows).toHaveLength(expectedRows.length)
@@ -191,7 +190,7 @@ describe.each([
// passed in. The order of the rows is not important, but extra rows will
// cause the assertion to fail.
async toContainExactly(expectedRows: any[]) {
- const foundRows = await this.performSearch()
+ const { rows: foundRows } = await this.performSearch()
// eslint-disable-next-line jest/no-standalone-expect
expect(foundRows).toHaveLength(expectedRows.length)
@@ -205,11 +204,36 @@ describe.each([
)
}
+ // Asserts that the query returns some property values - this cannot be used
+ // to check row values, however this shouldn't be important for checking properties
+ // typing for this has to be any, Jest doesn't expose types for matchers like expect.any(...)
+ async toMatch(properties: Record) {
+ const response = await this.performSearch()
+ const keys = Object.keys(properties) as Array>
+ for (let key of keys) {
+ // eslint-disable-next-line jest/no-standalone-expect
+ expect(response[key]).toBeDefined()
+ if (properties[key]) {
+ // eslint-disable-next-line jest/no-standalone-expect
+ expect(response[key]).toEqual(properties[key])
+ }
+ }
+ }
+
+ // Asserts that the query doesn't return a property, e.g. pagination parameters.
+ async toNotHaveProperty(properties: (keyof SearchResponse)[]) {
+ const response = await this.performSearch()
+ for (let property of properties) {
+ // eslint-disable-next-line jest/no-standalone-expect
+ expect(response[property]).toBeUndefined()
+ }
+ }
+
// Asserts that the query returns rows matching the set of rows passed in.
// The order of the rows is not important. Extra rows will not cause the
// assertion to fail.
async toContain(expectedRows: any[]) {
- const foundRows = await this.performSearch()
+ const { rows: foundRows } = await this.performSearch()
// eslint-disable-next-line jest/no-standalone-expect
expect([...foundRows]).toEqual(
@@ -226,7 +250,7 @@ describe.each([
}
async toHaveLength(length: number) {
- const foundRows = await this.performSearch()
+ const { rows: foundRows } = await this.performSearch()
// eslint-disable-next-line jest/no-standalone-expect
expect(foundRows).toHaveLength(length)
@@ -250,55 +274,63 @@ describe.each([
})
describe("equal", () => {
- it("successfully finds true row", () =>
- expectQuery({ equal: { isTrue: true } }).toMatchExactly([
+ it("successfully finds true row", async () => {
+ await expectQuery({ equal: { isTrue: true } }).toMatchExactly([
{ isTrue: true },
- ]))
+ ])
+ })
- it("successfully finds false row", () =>
- expectQuery({ equal: { isTrue: false } }).toMatchExactly([
+ it("successfully finds false row", async () => {
+ await expectQuery({ equal: { isTrue: false } }).toMatchExactly([
{ isTrue: false },
- ]))
+ ])
+ })
})
describe("notEqual", () => {
- it("successfully finds false row", () =>
- expectQuery({ notEqual: { isTrue: true } }).toContainExactly([
+ it("successfully finds false row", async () => {
+ await expectQuery({ notEqual: { isTrue: true } }).toContainExactly([
{ isTrue: false },
- ]))
+ ])
+ })
- it("successfully finds true row", () =>
- expectQuery({ notEqual: { isTrue: false } }).toContainExactly([
+ it("successfully finds true row", async () => {
+ await expectQuery({ notEqual: { isTrue: false } }).toContainExactly([
{ isTrue: true },
- ]))
+ ])
+ })
})
describe("oneOf", () => {
- it("successfully finds true row", () =>
- expectQuery({ oneOf: { isTrue: [true] } }).toContainExactly([
+ it("successfully finds true row", async () => {
+ await expectQuery({ oneOf: { isTrue: [true] } }).toContainExactly([
{ isTrue: true },
- ]))
+ ])
+ })
- it("successfully finds false row", () =>
- expectQuery({ oneOf: { isTrue: [false] } }).toContainExactly([
+ it("successfully finds false row", async () => {
+ await expectQuery({ oneOf: { isTrue: [false] } }).toContainExactly([
{ isTrue: false },
- ]))
+ ])
+ })
})
describe("sort", () => {
- it("sorts ascending", () =>
- expectSearch({
+ it("sorts ascending", async () => {
+ await expectSearch({
query: {},
sort: "isTrue",
sortOrder: SortOrder.ASCENDING,
- }).toMatchExactly([{ isTrue: false }, { isTrue: true }]))
+ }).toMatchExactly([{ isTrue: false }, { isTrue: true }])
+ })
- it("sorts descending", () =>
- expectSearch({
+ it("sorts descending", async () => {
+ await expectSearch({
query: {},
sort: "isTrue",
sortOrder: SortOrder.DESCENDING,
- }).toMatchExactly([{ isTrue: true }, { isTrue: false }]))
+ }).toMatchExactly([{ isTrue: true }, { isTrue: false }])
+ })
})
})
@@ -652,191 +684,230 @@ describe.each([
})
describe("misc", () => {
- it("should return all if no query is passed", () =>
- expectSearch({} as RowSearchParams).toContainExactly([
+ it("should return all if no query is passed", async () => {
+ await expectSearch({} as RowSearchParams).toContainExactly([
{ name: "foo" },
{ name: "bar" },
- ]))
+ ])
+ })
- it("should return all if empty query is passed", () =>
- expectQuery({}).toContainExactly([{ name: "foo" }, { name: "bar" }]))
+ it("should return all if empty query is passed", async () => {
+ await expectQuery({}).toContainExactly([
+ { name: "foo" },
+ { name: "bar" },
+ ])
+ })
- it("should return all if onEmptyFilter is RETURN_ALL", () =>
- expectQuery({
+ it("should return all if onEmptyFilter is RETURN_ALL", async () => {
+ await expectQuery({
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
- }).toContainExactly([{ name: "foo" }, { name: "bar" }]))
+ }).toContainExactly([{ name: "foo" }, { name: "bar" }])
+ })
- it("should return nothing if onEmptyFilter is RETURN_NONE", () =>
- expectQuery({
+ it("should return nothing if onEmptyFilter is RETURN_NONE", async () => {
+ await expectQuery({
onEmptyFilter: EmptyFilterOption.RETURN_NONE,
- }).toFindNothing())
+ }).toFindNothing()
+ })
- it("should respect limit", () =>
- expectSearch({ limit: 1, paginate: true, query: {} }).toHaveLength(1))
+ it("should respect limit", async () => {
+ await expectSearch({
+ limit: 1,
+ paginate: true,
+ query: {},
+ }).toHaveLength(1)
+ })
})
describe("equal", () => {
- it("successfully finds a row", () =>
- expectQuery({ equal: { name: "foo" } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ equal: { name: "foo" } }).toContainExactly([
{ name: "foo" },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ equal: { name: "none" } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ equal: { name: "none" } }).toFindNothing()
+ })
- it("works as an or condition", () =>
- expectQuery({
+ it("works as an or condition", async () => {
+ await expectQuery({
allOr: true,
equal: { name: "foo" },
oneOf: { name: ["bar"] },
- }).toContainExactly([{ name: "foo" }, { name: "bar" }]))
+ }).toContainExactly([{ name: "foo" }, { name: "bar" }])
+ })
- it("can have multiple values for same column", () =>
- expectQuery({
+ it("can have multiple values for same column", async () => {
+ await expectQuery({
allOr: true,
equal: { "1:name": "foo", "2:name": "bar" },
- }).toContainExactly([{ name: "foo" }, { name: "bar" }]))
+ }).toContainExactly([{ name: "foo" }, { name: "bar" }])
+ })
})
describe("notEqual", () => {
- it("successfully finds a row", () =>
- expectQuery({ notEqual: { name: "foo" } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ notEqual: { name: "foo" } }).toContainExactly([
{ name: "bar" },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ notEqual: { name: "bar" } }).toContainExactly([
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ notEqual: { name: "bar" } }).toContainExactly([
{ name: "foo" },
- ]))
+ ])
+ })
})
describe("oneOf", () => {
- it("successfully finds a row", () =>
- expectQuery({ oneOf: { name: ["foo"] } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ oneOf: { name: ["foo"] } }).toContainExactly([
{ name: "foo" },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ oneOf: { name: ["none"] } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ oneOf: { name: ["none"] } }).toFindNothing()
+ })
})
describe("fuzzy", () => {
- it("successfully finds a row", () =>
- expectQuery({ fuzzy: { name: "oo" } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ fuzzy: { name: "oo" } }).toContainExactly([
{ name: "foo" },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ fuzzy: { name: "none" } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ fuzzy: { name: "none" } }).toFindNothing()
+ })
})
describe("string", () => {
- it("successfully finds a row", () =>
- expectQuery({ string: { name: "fo" } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ string: { name: "fo" } }).toContainExactly([
{ name: "foo" },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ string: { name: "none" } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ string: { name: "none" } }).toFindNothing()
+ })
- it("is case-insensitive", () =>
- expectQuery({ string: { name: "FO" } }).toContainExactly([
+ it("is case-insensitive", async () => {
+ await expectQuery({ string: { name: "FO" } }).toContainExactly([
{ name: "foo" },
- ]))
+ ])
+ })
})
describe("range", () => {
- it("successfully finds multiple rows", () =>
- expectQuery({
+ it("successfully finds multiple rows", async () => {
+ await expectQuery({
range: { name: { low: "a", high: "z" } },
- }).toContainExactly([{ name: "bar" }, { name: "foo" }]))
+ }).toContainExactly([{ name: "bar" }, { name: "foo" }])
+ })
- it("successfully finds a row with a high bound", () =>
- expectQuery({
+ it("successfully finds a row with a high bound", async () => {
+ await expectQuery({
range: { name: { low: "a", high: "c" } },
- }).toContainExactly([{ name: "bar" }]))
+ }).toContainExactly([{ name: "bar" }])
+ })
- it("successfully finds a row with a low bound", () =>
- expectQuery({
+ it("successfully finds a row with a low bound", async () => {
+ await expectQuery({
range: { name: { low: "f", high: "z" } },
- }).toContainExactly([{ name: "foo" }]))
+ }).toContainExactly([{ name: "foo" }])
+ })
- it("successfully finds no rows", () =>
- expectQuery({
+ it("successfully finds no rows", async () => {
+ await expectQuery({
range: { name: { low: "g", high: "h" } },
- }).toFindNothing())
+ }).toFindNothing()
+ })
!isLucene &&
- it("ignores low if it's an empty object", () =>
- expectQuery({
+ it("ignores low if it's an empty object", async () => {
+ await expectQuery({
// @ts-ignore
range: { name: { low: {}, high: "z" } },
- }).toContainExactly([{ name: "foo" }, { name: "bar" }]))
+ }).toContainExactly([{ name: "foo" }, { name: "bar" }])
+ })
!isLucene &&
- it("ignores high if it's an empty object", () =>
- expectQuery({
+ it("ignores high if it's an empty object", async () => {
+ await expectQuery({
// @ts-ignore
range: { name: { low: "a", high: {} } },
- }).toContainExactly([{ name: "foo" }, { name: "bar" }]))
+ }).toContainExactly([{ name: "foo" }, { name: "bar" }])
+ })
})
describe("empty", () => {
- it("finds no empty rows", () =>
- expectQuery({ empty: { name: null } }).toFindNothing())
+ it("finds no empty rows", async () => {
+ await expectQuery({ empty: { name: null } }).toFindNothing()
+ })
- it("should not be affected by when filter empty behaviour", () =>
- expectQuery({
+ it("should not be affected by when filter empty behaviour", async () => {
+ await expectQuery({
empty: { name: null },
onEmptyFilter: EmptyFilterOption.RETURN_ALL,
- }).toFindNothing())
+ }).toFindNothing()
+ })
})
describe("notEmpty", () => {
- it("finds all non-empty rows", () =>
- expectQuery({ notEmpty: { name: null } }).toContainExactly([
+ it("finds all non-empty rows", async () => {
+ await expectQuery({ notEmpty: { name: null } }).toContainExactly([
{ name: "foo" },
{ name: "bar" },
- ]))
+ ])
+ })
- it("should not be affected by when filter empty behaviour", () =>
- expectQuery({
+ it("should not be affected by when filter empty behaviour", async () => {
+ await expectQuery({
notEmpty: { name: null },
onEmptyFilter: EmptyFilterOption.RETURN_NONE,
- }).toContainExactly([{ name: "foo" }, { name: "bar" }]))
+ }).toContainExactly([{ name: "foo" }, { name: "bar" }])
+ })
})
describe("sort", () => {
- it("sorts ascending", () =>
- expectSearch({
+ it("sorts ascending", async () => {
+ await expectSearch({
query: {},
sort: "name",
sortOrder: SortOrder.ASCENDING,
- }).toMatchExactly([{ name: "bar" }, { name: "foo" }]))
+ }).toMatchExactly([{ name: "bar" }, { name: "foo" }])
+ })
- it("sorts descending", () =>
- expectSearch({
+ it("sorts descending", async () => {
+ await expectSearch({
query: {},
sort: "name",
sortOrder: SortOrder.DESCENDING,
- }).toMatchExactly([{ name: "foo" }, { name: "bar" }]))
+ }).toMatchExactly([{ name: "foo" }, { name: "bar" }])
+ })
describe("sortType STRING", () => {
- it("sorts ascending", () =>
- expectSearch({
+ it("sorts ascending", async () => {
+ await expectSearch({
query: {},
sort: "name",
sortType: SortType.STRING,
sortOrder: SortOrder.ASCENDING,
- }).toMatchExactly([{ name: "bar" }, { name: "foo" }]))
+ }).toMatchExactly([{ name: "bar" }, { name: "foo" }])
+ })
- it("sorts descending", () =>
- expectSearch({
+ it("sorts descending", async () => {
+ await expectSearch({
query: {},
sort: "name",
sortType: SortType.STRING,
sortOrder: SortOrder.DESCENDING,
- }).toMatchExactly([{ name: "foo" }, { name: "bar" }]))
+ }).toMatchExactly([{ name: "foo" }, { name: "bar" }])
+ })
})
})
})
@@ -850,97 +921,119 @@ describe.each([
})
describe("equal", () => {
- it("successfully finds a row", () =>
- expectQuery({ equal: { age: 1 } }).toContainExactly([{ age: 1 }]))
+ it("successfully finds a row", async () => {
+ await expectQuery({ equal: { age: 1 } }).toContainExactly([{ age: 1 }])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ equal: { age: 2 } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ equal: { age: 2 } }).toFindNothing()
+ })
})
describe("notEqual", () => {
- it("successfully finds a row", () =>
- expectQuery({ notEqual: { age: 1 } }).toContainExactly([{ age: 10 }]))
+ it("successfully finds a row", async () => {
+ await expectQuery({ notEqual: { age: 1 } }).toContainExactly([
+ { age: 10 },
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ notEqual: { age: 10 } }).toContainExactly([{ age: 1 }]))
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ notEqual: { age: 10 } }).toContainExactly([
+ { age: 1 },
+ ])
+ })
})
describe("oneOf", () => {
- it("successfully finds a row", () =>
- expectQuery({ oneOf: { age: [1] } }).toContainExactly([{ age: 1 }]))
+ it("successfully finds a row", async () => {
+ await expectQuery({ oneOf: { age: [1] } }).toContainExactly([
+ { age: 1 },
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ oneOf: { age: [2] } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ oneOf: { age: [2] } }).toFindNothing()
+ })
})
describe("range", () => {
- it("successfully finds a row", () =>
- expectQuery({
+ it("successfully finds a row", async () => {
+ await expectQuery({
range: { age: { low: 1, high: 5 } },
- }).toContainExactly([{ age: 1 }]))
+ }).toContainExactly([{ age: 1 }])
+ })
- it("successfully finds multiple rows", () =>
- expectQuery({
+ it("successfully finds multiple rows", async () => {
+ await expectQuery({
range: { age: { low: 1, high: 10 } },
- }).toContainExactly([{ age: 1 }, { age: 10 }]))
+ }).toContainExactly([{ age: 1 }, { age: 10 }])
+ })
- it("successfully finds a row with a high bound", () =>
- expectQuery({
+ it("successfully finds a row with a high bound", async () => {
+ await expectQuery({
range: { age: { low: 5, high: 10 } },
- }).toContainExactly([{ age: 10 }]))
+ }).toContainExactly([{ age: 10 }])
+ })
- it("successfully finds no rows", () =>
- expectQuery({
+ it("successfully finds no rows", async () => {
+ await expectQuery({
range: { age: { low: 5, high: 9 } },
- }).toFindNothing())
+ }).toFindNothing()
+ })
// We never implemented half-open ranges in Lucene.
!isLucene &&
- it("can search using just a low value", () =>
- expectQuery({
+ it("can search using just a low value", async () => {
+ await expectQuery({
range: { age: { low: 5 } },
- }).toContainExactly([{ age: 10 }]))
+ }).toContainExactly([{ age: 10 }])
+ })
// We never implemented half-open ranges in Lucene.
!isLucene &&
- it("can search using just a high value", () =>
- expectQuery({
+ it("can search using just a high value", async () => {
+ await expectQuery({
range: { age: { high: 5 } },
- }).toContainExactly([{ age: 1 }]))
+ }).toContainExactly([{ age: 1 }])
+ })
})
describe("sort", () => {
- it("sorts ascending", () =>
- expectSearch({
+ it("sorts ascending", async () => {
+ await expectSearch({
query: {},
sort: "age",
sortOrder: SortOrder.ASCENDING,
- }).toMatchExactly([{ age: 1 }, { age: 10 }]))
+ }).toMatchExactly([{ age: 1 }, { age: 10 }])
+ })
- it("sorts descending", () =>
- expectSearch({
+ it("sorts descending", async () => {
+ await expectSearch({
query: {},
sort: "age",
sortOrder: SortOrder.DESCENDING,
- }).toMatchExactly([{ age: 10 }, { age: 1 }]))
+ }).toMatchExactly([{ age: 10 }, { age: 1 }])
+ })
})
describe("sortType NUMBER", () => {
- it("sorts ascending", () =>
- expectSearch({
+ it("sorts ascending", async () => {
+ await expectSearch({
query: {},
sort: "age",
sortType: SortType.NUMBER,
sortOrder: SortOrder.ASCENDING,
- }).toMatchExactly([{ age: 1 }, { age: 10 }]))
+ }).toMatchExactly([{ age: 1 }, { age: 10 }])
+ })
- it("sorts descending", () =>
- expectSearch({
+ it("sorts descending", async () => {
+ await expectSearch({
query: {},
sort: "age",
sortType: SortType.NUMBER,
sortOrder: SortOrder.DESCENDING,
- }).toMatchExactly([{ age: 10 }, { age: 1 }]))
+ }).toMatchExactly([{ age: 10 }, { age: 1 }])
+ })
})
})
@@ -960,104 +1053,120 @@ describe.each([
})
describe("equal", () => {
- it("successfully finds a row", () =>
- expectQuery({ equal: { dob: JAN_1ST } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ equal: { dob: JAN_1ST } }).toContainExactly([
{ dob: JAN_1ST },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ equal: { dob: JAN_2ND } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ equal: { dob: JAN_2ND } }).toFindNothing()
+ })
})
describe("notEqual", () => {
- it("successfully finds a row", () =>
- expectQuery({ notEqual: { dob: JAN_1ST } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ notEqual: { dob: JAN_1ST } }).toContainExactly([
{ dob: JAN_10TH },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ notEqual: { dob: JAN_10TH } }).toContainExactly([
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ notEqual: { dob: JAN_10TH } }).toContainExactly([
{ dob: JAN_1ST },
- ]))
+ ])
+ })
})
describe("oneOf", () => {
- it("successfully finds a row", () =>
- expectQuery({ oneOf: { dob: [JAN_1ST] } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ oneOf: { dob: [JAN_1ST] } }).toContainExactly([
{ dob: JAN_1ST },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ oneOf: { dob: [JAN_2ND] } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ oneOf: { dob: [JAN_2ND] } }).toFindNothing()
+ })
})
describe("range", () => {
- it("successfully finds a row", () =>
- expectQuery({
+ it("successfully finds a row", async () => {
+ await expectQuery({
range: { dob: { low: JAN_1ST, high: JAN_5TH } },
- }).toContainExactly([{ dob: JAN_1ST }]))
+ }).toContainExactly([{ dob: JAN_1ST }])
+ })
- it("successfully finds multiple rows", () =>
- expectQuery({
+ it("successfully finds multiple rows", async () => {
+ await expectQuery({
range: { dob: { low: JAN_1ST, high: JAN_10TH } },
- }).toContainExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }]))
+ }).toContainExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }])
+ })
- it("successfully finds a row with a high bound", () =>
- expectQuery({
+ it("successfully finds a row with a high bound", async () => {
+ await expectQuery({
range: { dob: { low: JAN_5TH, high: JAN_10TH } },
- }).toContainExactly([{ dob: JAN_10TH }]))
+ }).toContainExactly([{ dob: JAN_10TH }])
+ })
- it("successfully finds no rows", () =>
- expectQuery({
+ it("successfully finds no rows", async () => {
+ await expectQuery({
range: { dob: { low: JAN_5TH, high: JAN_9TH } },
- }).toFindNothing())
+ }).toFindNothing()
+ })
// We never implemented half-open ranges in Lucene.
!isLucene &&
- it("can search using just a low value", () =>
- expectQuery({
+ it("can search using just a low value", async () => {
+ await expectQuery({
range: { dob: { low: JAN_5TH } },
- }).toContainExactly([{ dob: JAN_10TH }]))
+ }).toContainExactly([{ dob: JAN_10TH }])
+ })
// We never implemented half-open ranges in Lucene.
!isLucene &&
- it("can search using just a high value", () =>
- expectQuery({
+ it("can search using just a high value", async () => {
+ await expectQuery({
range: { dob: { high: JAN_5TH } },
- }).toContainExactly([{ dob: JAN_1ST }]))
+ }).toContainExactly([{ dob: JAN_1ST }])
+ })
})
describe("sort", () => {
- it("sorts ascending", () =>
- expectSearch({
+ it("sorts ascending", async () => {
+ await expectSearch({
query: {},
sort: "dob",
sortOrder: SortOrder.ASCENDING,
- }).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }]))
+ }).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }])
+ })
- it("sorts descending", () =>
- expectSearch({
+ it("sorts descending", async () => {
+ await expectSearch({
query: {},
sort: "dob",
sortOrder: SortOrder.DESCENDING,
- }).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }]))
+ }).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }])
+ })
describe("sortType STRING", () => {
- it("sorts ascending", () =>
- expectSearch({
+ it("sorts ascending", async () => {
+ await expectSearch({
query: {},
sort: "dob",
sortType: SortType.STRING,
sortOrder: SortOrder.ASCENDING,
- }).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }]))
+ }).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }])
+ })
- it("sorts descending", () =>
- expectSearch({
+ it("sorts descending", async () => {
+ await expectSearch({
query: {},
sort: "dob",
sortType: SortType.STRING,
sortOrder: SortOrder.DESCENDING,
- }).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }]))
+ }).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }])
+ })
})
})
})
@@ -1091,72 +1200,85 @@ describe.each([
})
describe("equal", () => {
- it("successfully finds a row", () =>
- expectQuery({ equal: { time: T_1000 } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ equal: { time: T_1000 } }).toContainExactly([
{ time: "10:00:00" },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ equal: { time: UNEXISTING_TIME } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({
+ equal: { time: UNEXISTING_TIME },
+ }).toFindNothing()
+ })
})
describe("notEqual", () => {
- it("successfully finds a row", () =>
- expectQuery({ notEqual: { time: T_1000 } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ notEqual: { time: T_1000 } }).toContainExactly([
{ timeid: NULL_TIME__ID },
{ time: "10:45:00" },
{ time: "12:00:00" },
{ time: "15:30:00" },
{ time: "00:00:00" },
- ]))
+ ])
+ })
- it("return all when requesting non-existing", () =>
- expectQuery({ notEqual: { time: UNEXISTING_TIME } }).toContainExactly(
- [
- { timeid: NULL_TIME__ID },
- { time: "10:00:00" },
- { time: "10:45:00" },
- { time: "12:00:00" },
- { time: "15:30:00" },
- { time: "00:00:00" },
- ]
- ))
+ it("return all when requesting non-existing", async () => {
+ await expectQuery({
+ notEqual: { time: UNEXISTING_TIME },
+ }).toContainExactly([
+ { timeid: NULL_TIME__ID },
+ { time: "10:00:00" },
+ { time: "10:45:00" },
+ { time: "12:00:00" },
+ { time: "15:30:00" },
+ { time: "00:00:00" },
+ ])
+ })
})
describe("oneOf", () => {
- it("successfully finds a row", () =>
- expectQuery({ oneOf: { time: [T_1000] } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ oneOf: { time: [T_1000] } }).toContainExactly([
{ time: "10:00:00" },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ oneOf: { time: [UNEXISTING_TIME] } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({
+ oneOf: { time: [UNEXISTING_TIME] },
+ }).toFindNothing()
+ })
})
describe("range", () => {
- it("successfully finds a row", () =>
- expectQuery({
+ it("successfully finds a row", async () => {
+ await expectQuery({
range: { time: { low: T_1045, high: T_1045 } },
- }).toContainExactly([{ time: "10:45:00" }]))
+ }).toContainExactly([{ time: "10:45:00" }])
+ })
- it("successfully finds multiple rows", () =>
- expectQuery({
+ it("successfully finds multiple rows", async () => {
+ await expectQuery({
range: { time: { low: T_1045, high: T_1530 } },
}).toContainExactly([
{ time: "10:45:00" },
{ time: "12:00:00" },
{ time: "15:30:00" },
- ]))
+ ])
+ })
- it("successfully finds no rows", () =>
- expectQuery({
+ it("successfully finds no rows", async () => {
+ await expectQuery({
range: { time: { low: UNEXISTING_TIME, high: UNEXISTING_TIME } },
- }).toFindNothing())
+ }).toFindNothing()
+ })
})
describe("sort", () => {
- it("sorts ascending", () =>
- expectSearch({
+ it("sorts ascending", async () => {
+ await expectSearch({
query: {},
sort: "time",
sortOrder: SortOrder.ASCENDING,
@@ -1167,10 +1289,11 @@ describe.each([
{ time: "10:45:00" },
{ time: "12:00:00" },
{ time: "15:30:00" },
- ]))
+ ])
+ })
- it("sorts descending", () =>
- expectSearch({
+ it("sorts descending", async () => {
+ await expectSearch({
query: {},
sort: "time",
sortOrder: SortOrder.DESCENDING,
@@ -1181,11 +1304,12 @@ describe.each([
{ time: "10:00:00" },
{ time: "00:00:00" },
{ timeid: NULL_TIME__ID },
- ]))
+ ])
+ })
describe("sortType STRING", () => {
- it("sorts ascending", () =>
- expectSearch({
+ it("sorts ascending", async () => {
+ await expectSearch({
query: {},
sort: "time",
sortType: SortType.STRING,
@@ -1197,10 +1321,11 @@ describe.each([
{ time: "10:45:00" },
{ time: "12:00:00" },
{ time: "15:30:00" },
- ]))
+ ])
+ })
- it("sorts descending", () =>
- expectSearch({
+ it("sorts descending", async () => {
+ await expectSearch({
query: {},
sort: "time",
sortType: SortType.STRING,
@@ -1212,7 +1337,8 @@ describe.each([
{ time: "10:00:00" },
{ time: "00:00:00" },
{ timeid: NULL_TIME__ID },
- ]))
+ ])
+ })
})
})
})
@@ -1230,66 +1356,78 @@ describe.each([
})
describe("contains", () => {
- it("successfully finds a row", () =>
- expectQuery({ contains: { numbers: ["one"] } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ contains: { numbers: ["one"] } }).toContainExactly([
{ numbers: ["one", "two"] },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ contains: { numbers: ["none"] } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ contains: { numbers: ["none"] } }).toFindNothing()
+ })
- it("fails to find row containing all", () =>
- expectQuery({
+ it("fails to find row containing all", async () => {
+ await expectQuery({
contains: { numbers: ["one", "two", "three"] },
- }).toFindNothing())
+ }).toFindNothing()
+ })
- it("finds all with empty list", () =>
- expectQuery({ contains: { numbers: [] } }).toContainExactly([
+ it("finds all with empty list", async () => {
+ await expectQuery({ contains: { numbers: [] } }).toContainExactly([
{ numbers: ["one", "two"] },
{ numbers: ["three"] },
- ]))
+ ])
+ })
})
describe("notContains", () => {
- it("successfully finds a row", () =>
- expectQuery({ notContains: { numbers: ["one"] } }).toContainExactly([
- { numbers: ["three"] },
- ]))
+ it("successfully finds a row", async () => {
+ await expectQuery({
+ notContains: { numbers: ["one"] },
+ }).toContainExactly([{ numbers: ["three"] }])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({
notContains: { numbers: ["one", "two", "three"] },
}).toContainExactly([
{ numbers: ["one", "two"] },
{ numbers: ["three"] },
- ]))
+ ])
+ })
// Not sure if this is correct behaviour but changing it would be a
// breaking change.
- it("finds all with empty list", () =>
- expectQuery({ notContains: { numbers: [] } }).toContainExactly([
+ it("finds all with empty list", async () => {
+ await expectQuery({ notContains: { numbers: [] } }).toContainExactly([
{ numbers: ["one", "two"] },
{ numbers: ["three"] },
- ]))
+ ])
+ })
})
describe("containsAny", () => {
- it("successfully finds rows", () =>
- expectQuery({
+ it("successfully finds rows", async () => {
+ await expectQuery({
containsAny: { numbers: ["one", "two", "three"] },
}).toContainExactly([
{ numbers: ["one", "two"] },
{ numbers: ["three"] },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ containsAny: { numbers: ["none"] } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({
+ containsAny: { numbers: ["none"] },
+ }).toFindNothing()
+ })
- it("finds all with empty list", () =>
- expectQuery({ containsAny: { numbers: [] } }).toContainExactly([
+ it("finds all with empty list", async () => {
+ await expectQuery({ containsAny: { numbers: [] } }).toContainExactly([
{ numbers: ["one", "two"] },
{ numbers: ["three"] },
- ]))
+ ])
+ })
})
})
@@ -1308,48 +1446,56 @@ describe.each([
})
describe("equal", () => {
- it("successfully finds a row", () =>
- expectQuery({ equal: { num: SMALL } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ equal: { num: SMALL } }).toContainExactly([
{ num: SMALL },
- ]))
+ ])
+ })
- it("successfully finds a big value", () =>
- expectQuery({ equal: { num: BIG } }).toContainExactly([{ num: BIG }]))
+ it("successfully finds a big value", async () => {
+ await expectQuery({ equal: { num: BIG } }).toContainExactly([
+ { num: BIG },
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ equal: { num: "2" } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ equal: { num: "2" } }).toFindNothing()
+ })
})
describe("notEqual", () => {
- it("successfully finds a row", () =>
- expectQuery({ notEqual: { num: SMALL } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ notEqual: { num: SMALL } }).toContainExactly([
{ num: MEDIUM },
{ num: BIG },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ notEqual: { num: 10 } }).toContainExactly([
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ notEqual: { num: 10 } }).toContainExactly([
{ num: SMALL },
{ num: MEDIUM },
{ num: BIG },
- ]))
+ ])
+ })
})
describe("oneOf", () => {
- it("successfully finds a row", () =>
- expectQuery({ oneOf: { num: [SMALL] } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ oneOf: { num: [SMALL] } }).toContainExactly([
{ num: SMALL },
- ]))
+ ])
+ })
- it("successfully finds all rows", () =>
- expectQuery({ oneOf: { num: [SMALL, MEDIUM, BIG] } }).toContainExactly([
- { num: SMALL },
- { num: MEDIUM },
- { num: BIG },
- ]))
+ it("successfully finds all rows", async () => {
+ await expectQuery({
+ oneOf: { num: [SMALL, MEDIUM, BIG] },
+ }).toContainExactly([{ num: SMALL }, { num: MEDIUM }, { num: BIG }])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ oneOf: { num: [2] } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ oneOf: { num: [2] } }).toFindNothing()
+ })
})
// Range searches against bigints don't seem to work at all in Lucene, and I
@@ -1357,35 +1503,41 @@ describe.each([
// we've decided not to spend time on it.
!isLucene &&
describe("range", () => {
- it("successfully finds a row", () =>
- expectQuery({
+ it("successfully finds a row", async () => {
+ await expectQuery({
range: { num: { low: SMALL, high: "5" } },
- }).toContainExactly([{ num: SMALL }]))
+ }).toContainExactly([{ num: SMALL }])
+ })
- it("successfully finds multiple rows", () =>
- expectQuery({
+ it("successfully finds multiple rows", async () => {
+ await expectQuery({
range: { num: { low: SMALL, high: MEDIUM } },
- }).toContainExactly([{ num: SMALL }, { num: MEDIUM }]))
+ }).toContainExactly([{ num: SMALL }, { num: MEDIUM }])
+ })
- it("successfully finds a row with a high bound", () =>
- expectQuery({
+ it("successfully finds a row with a high bound", async () => {
+ await expectQuery({
range: { num: { low: MEDIUM, high: BIG } },
- }).toContainExactly([{ num: MEDIUM }, { num: BIG }]))
+ }).toContainExactly([{ num: MEDIUM }, { num: BIG }])
+ })
- it("successfully finds no rows", () =>
- expectQuery({
+ it("successfully finds no rows", async () => {
+ await expectQuery({
range: { num: { low: "5", high: "5" } },
- }).toFindNothing())
+ }).toFindNothing()
+ })
- it("can search using just a low value", () =>
- expectQuery({
+ it("can search using just a low value", async () => {
+ await expectQuery({
range: { num: { low: MEDIUM } },
- }).toContainExactly([{ num: MEDIUM }, { num: BIG }]))
+ }).toContainExactly([{ num: MEDIUM }, { num: BIG }])
+ })
- it("can search using just a high value", () =>
- expectQuery({
+ it("can search using just a high value", async () => {
+ await expectQuery({
range: { num: { high: MEDIUM } },
- }).toContainExactly([{ num: SMALL }, { num: MEDIUM }]))
+ }).toContainExactly([{ num: SMALL }, { num: MEDIUM }])
+ })
})
})
@@ -1404,16 +1556,20 @@ describe.each([
})
describe("equal", () => {
- it("successfully finds a row", () =>
- expectQuery({ equal: { auto: 1 } }).toContainExactly([{ auto: 1 }]))
+ it("successfully finds a row", async () => {
+ await expectQuery({ equal: { auto: 1 } }).toContainExactly([
+ { auto: 1 },
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ equal: { auto: 0 } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ equal: { auto: 0 } }).toFindNothing()
+ })
})
describe("not equal", () => {
- it("successfully finds a row", () =>
- expectQuery({ notEqual: { auto: 1 } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ notEqual: { auto: 1 } }).toContainExactly([
{ auto: 2 },
{ auto: 3 },
{ auto: 4 },
@@ -1423,10 +1579,11 @@ describe.each([
{ auto: 8 },
{ auto: 9 },
{ auto: 10 },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ notEqual: { auto: 0 } }).toContainExactly([
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ notEqual: { auto: 0 } }).toContainExactly([
{ auto: 1 },
{ auto: 2 },
{ auto: 3 },
@@ -1437,55 +1594,66 @@ describe.each([
{ auto: 8 },
{ auto: 9 },
{ auto: 10 },
- ]))
+ ])
+ })
})
describe("oneOf", () => {
- it("successfully finds a row", () =>
- expectQuery({ oneOf: { auto: [1] } }).toContainExactly([{ auto: 1 }]))
+ it("successfully finds a row", async () => {
+ await expectQuery({ oneOf: { auto: [1] } }).toContainExactly([
+ { auto: 1 },
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ oneOf: { auto: [0] } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ oneOf: { auto: [0] } }).toFindNothing()
+ })
})
describe("range", () => {
- it("successfully finds a row", () =>
- expectQuery({
+ it("successfully finds a row", async () => {
+ await expectQuery({
range: { auto: { low: 1, high: 1 } },
- }).toContainExactly([{ auto: 1 }]))
+ }).toContainExactly([{ auto: 1 }])
+ })
- it("successfully finds multiple rows", () =>
- expectQuery({
+ it("successfully finds multiple rows", async () => {
+ await expectQuery({
range: { auto: { low: 1, high: 2 } },
- }).toContainExactly([{ auto: 1 }, { auto: 2 }]))
+ }).toContainExactly([{ auto: 1 }, { auto: 2 }])
+ })
- it("successfully finds a row with a high bound", () =>
- expectQuery({
+ it("successfully finds a row with a high bound", async () => {
+ await expectQuery({
range: { auto: { low: 2, high: 2 } },
- }).toContainExactly([{ auto: 2 }]))
+ }).toContainExactly([{ auto: 2 }])
+ })
- it("successfully finds no rows", () =>
- expectQuery({
+ it("successfully finds no rows", async () => {
+ await expectQuery({
range: { auto: { low: 0, high: 0 } },
- }).toFindNothing())
+ }).toFindNothing()
+ })
isSqs &&
- it("can search using just a low value", () =>
- expectQuery({
+ it("can search using just a low value", async () => {
+ await expectQuery({
range: { auto: { low: 9 } },
- }).toContainExactly([{ auto: 9 }, { auto: 10 }]))
+ }).toContainExactly([{ auto: 9 }, { auto: 10 }])
+ })
isSqs &&
- it("can search using just a high value", () =>
- expectQuery({
+ it("can search using just a high value", async () => {
+ await expectQuery({
range: { auto: { high: 2 } },
- }).toContainExactly([{ auto: 1 }, { auto: 2 }]))
+ }).toContainExactly([{ auto: 1 }, { auto: 2 }])
+ })
})
isSqs &&
describe("sort", () => {
- it("sorts ascending", () =>
- expectSearch({
+ it("sorts ascending", async () => {
+ await expectSearch({
query: {},
sort: "auto",
sortOrder: SortOrder.ASCENDING,
@@ -1500,10 +1668,11 @@ describe.each([
{ auto: 8 },
{ auto: 9 },
{ auto: 10 },
- ]))
+ ])
+ })
- it("sorts descending", () =>
- expectSearch({
+ it("sorts descending", async () => {
+ await expectSearch({
query: {},
sort: "auto",
sortOrder: SortOrder.DESCENDING,
@@ -1518,25 +1687,39 @@ describe.each([
{ auto: 3 },
{ auto: 2 },
{ auto: 1 },
- ]))
+ ])
+ })
// This is important for pagination. The order of results must always
// be stable or pagination will break. We don't want the user to need
// to specify an order for pagination to work.
it("is stable without a sort specified", async () => {
- let { rows } = await config.api.row.search(table._id!, {
- tableId: table._id!,
- query: {},
- })
+ let { rows: fullRowList } = await config.api.row.search(
+ table._id!,
+ {
+ tableId: table._id!,
+ query: {},
+ }
+ )
- for (let i = 0; i < 10; i++) {
+ // repeat the search many times to check the first row is always the same
+ let bookmark: string | number | undefined,
+ hasNextPage: boolean | undefined = true,
+ rowCount: number = 0
+ do {
const response = await config.api.row.search(table._id!, {
tableId: table._id!,
limit: 1,
+ paginate: true,
query: {},
+ bookmark,
})
- expect(response.rows).toEqual(rows)
- }
+ bookmark = response.bookmark
+ hasNextPage = response.hasNextPage
+ expect(response.rows.length).toEqual(1)
+ const foundRow = response.rows[0]
+ expect(foundRow).toEqual(fullRowList[rowCount++])
+ } while (hasNextPage)
})
})
@@ -1578,13 +1761,15 @@ describe.each([
await createRows([{ "1:name": "bar" }, { "1:name": "foo" }])
})
- it("successfully finds a row", () =>
- expectQuery({ equal: { "1:1:name": "bar" } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ equal: { "1:1:name": "bar" } }).toContainExactly([
{ "1:name": "bar" },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ equal: { "1:1:name": "none" } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ equal: { "1:1:name": "none" } }).toFindNothing()
+ })
})
describe("user", () => {
@@ -1611,51 +1796,59 @@ describe.each([
})
describe("equal", () => {
- it("successfully finds a row", () =>
- expectQuery({ equal: { user: user1._id } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ equal: { user: user1._id } }).toContainExactly([
{ user: { _id: user1._id } },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ equal: { user: "us_none" } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ equal: { user: "us_none" } }).toFindNothing()
+ })
})
describe("notEqual", () => {
- it("successfully finds a row", () =>
- expectQuery({ notEqual: { user: user1._id } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ notEqual: { user: user1._id } }).toContainExactly([
{ user: { _id: user2._id } },
{},
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ notEqual: { user: "us_none" } }).toContainExactly([
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ notEqual: { user: "us_none" } }).toContainExactly([
{ user: { _id: user1._id } },
{ user: { _id: user2._id } },
{},
- ]))
+ ])
+ })
})
describe("oneOf", () => {
- it("successfully finds a row", () =>
- expectQuery({ oneOf: { user: [user1._id] } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({ oneOf: { user: [user1._id] } }).toContainExactly([
{ user: { _id: user1._id } },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ oneOf: { user: ["us_none"] } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ oneOf: { user: ["us_none"] } }).toFindNothing()
+ })
})
describe("empty", () => {
- it("finds empty rows", () =>
- expectQuery({ empty: { user: null } }).toContainExactly([{}]))
+ it("finds empty rows", async () => {
+ await expectQuery({ empty: { user: null } }).toContainExactly([{}])
+ })
})
describe("notEmpty", () => {
- it("finds non-empty rows", () =>
- expectQuery({ notEmpty: { user: null } }).toContainExactly([
+ it("finds non-empty rows", async () => {
+ await expectQuery({ notEmpty: { user: null } }).toContainExactly([
{ user: { _id: user1._id } },
{ user: { _id: user2._id } },
- ]))
+ ])
+ })
})
})
@@ -1689,58 +1882,71 @@ describe.each([
})
describe("contains", () => {
- it("successfully finds a row", () =>
- expectQuery({ contains: { users: [user1._id] } }).toContainExactly([
+ it("successfully finds a row", async () => {
+ await expectQuery({
+ contains: { users: [user1._id] },
+ }).toContainExactly([
{ users: [{ _id: user1._id }] },
{ users: [{ _id: user1._id }, { _id: user2._id }] },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ contains: { users: ["us_none"] } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({ contains: { users: ["us_none"] } }).toFindNothing()
+ })
})
describe("notContains", () => {
- it("successfully finds a row", () =>
- expectQuery({ notContains: { users: [user1._id] } }).toContainExactly([
- { users: [{ _id: user2._id }] },
- {},
- ]))
+ it("successfully finds a row", async () => {
+ await expectQuery({
+ notContains: { users: [user1._id] },
+ }).toContainExactly([{ users: [{ _id: user2._id }] }, {}])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ notContains: { users: ["us_none"] } }).toContainExactly([
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({
+ notContains: { users: ["us_none"] },
+ }).toContainExactly([
{ users: [{ _id: user1._id }] },
{ users: [{ _id: user2._id }] },
{ users: [{ _id: user1._id }, { _id: user2._id }] },
{},
- ]))
+ ])
+ })
})
describe("containsAny", () => {
- it("successfully finds rows", () =>
- expectQuery({
+ it("successfully finds rows", async () => {
+ await expectQuery({
containsAny: { users: [user1._id, user2._id] },
}).toContainExactly([
{ users: [{ _id: user1._id }] },
{ users: [{ _id: user2._id }] },
{ users: [{ _id: user1._id }, { _id: user2._id }] },
- ]))
+ ])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({ containsAny: { users: ["us_none"] } }).toFindNothing())
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({
+ containsAny: { users: ["us_none"] },
+ }).toFindNothing()
+ })
})
describe("multi-column equals", () => {
- it("successfully finds a row", () =>
- expectQuery({
+ it("successfully finds a row", async () => {
+ await expectQuery({
equal: { number: 1 },
contains: { users: [user1._id] },
- }).toContainExactly([{ users: [{ _id: user1._id }], number: 1 }]))
+ }).toContainExactly([{ users: [{ _id: user1._id }], number: 1 }])
+ })
- it("fails to find nonexistent row", () =>
- expectQuery({
+ it("fails to find nonexistent row", async () => {
+ await expectQuery({
equal: { number: 2 },
contains: { users: [user1._id] },
- }).toFindNothing())
+ }).toFindNothing()
+ })
})
})
@@ -1790,11 +1996,48 @@ describe.each([
rows = await config.api.row.fetch(table._id!)
})
- it("can search through relations", () =>
- expectQuery({
+ it("can search through relations", async () => {
+ await expectQuery({
equal: { [`${otherTable.name}.one`]: "foo" },
}).toContainExactly([
{ two: "foo", other: [{ _id: otherRows[0]._id }] },
- ]))
+ ])
+ })
+ })
+
+ // lucene can't count the total rows
+ !isLucene &&
+ describe("row counting", () => {
+ beforeAll(async () => {
+ table = await createTable({
+ name: {
+ name: "name",
+ type: FieldType.STRING,
+ },
+ })
+ await createRows([{ name: "a" }, { name: "b" }])
+ })
+
+ it("should be able to count rows when option set", async () => {
+ await expectSearch({
+ countRows: true,
+ query: {
+ notEmpty: {
+ name: true,
+ },
+ },
+ }).toMatch({ totalRows: 2, rows: expect.any(Array) })
+ })
+
+ it("shouldn't count rows when option is not set", async () => {
+ await expectSearch({
+ countRows: false,
+ query: {
+ notEmpty: {
+ name: true,
+ },
+ },
+ }).toNotHaveProperty(["totalRows"])
+ })
})
})
diff --git a/packages/server/src/api/routes/tests/table.spec.ts b/packages/server/src/api/routes/tests/table.spec.ts
index f23e0de6db..e75e5e23e7 100644
--- a/packages/server/src/api/routes/tests/table.spec.ts
+++ b/packages/server/src/api/routes/tests/table.spec.ts
@@ -276,6 +276,34 @@ describe.each([
})
})
+ isInternal &&
+ it("shouldn't allow duplicate column names", async () => {
+ const saveTableRequest: SaveTableRequest = {
+ ...basicTable(),
+ }
+ saveTableRequest.schema["Type"] = {
+ type: FieldType.STRING,
+ name: "Type",
+ }
+ await config.api.table.save(saveTableRequest, {
+ status: 400,
+ body: {
+ message:
+ 'Column(s) "type" are duplicated - check for other columns with these name (case in-sensitive)',
+ },
+ })
+ saveTableRequest.schema.foo = { type: FieldType.STRING, name: "foo" }
+ saveTableRequest.schema.FOO = { type: FieldType.STRING, name: "FOO" }
+
+ await config.api.table.save(saveTableRequest, {
+ status: 400,
+ body: {
+ message:
+ 'Column(s) "type, foo" are duplicated - check for other columns with these name (case in-sensitive)',
+ },
+ })
+ })
+
it("should add a new column for an internal DB table", async () => {
const saveTableRequest: SaveTableRequest = {
...basicTable(),
diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts
index 06921037dd..99ff4f8db7 100644
--- a/packages/server/src/api/routes/tests/viewV2.spec.ts
+++ b/packages/server/src/api/routes/tests/viewV2.spec.ts
@@ -7,6 +7,7 @@ import {
INTERNAL_TABLE_SOURCE_ID,
PermissionLevel,
QuotaUsageType,
+ Row,
SaveTableRequest,
SearchFilterOperator,
SortOrder,
@@ -17,6 +18,7 @@ import {
UpdateViewRequest,
ViewUIFieldMetadata,
ViewV2,
+ SearchResponse,
} from "@budibase/types"
import { generator, mocks } from "@budibase/backend-core/tests"
import { DatabaseName, getDatasource } from "../../../integrations/tests/utils"
@@ -25,17 +27,21 @@ import { quotas } from "@budibase/pro"
import { db, roles } from "@budibase/backend-core"
describe.each([
- ["internal", undefined],
+ ["lucene", undefined],
+ ["sqs", undefined],
[DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)],
[DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)],
[DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)],
[DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)],
-])("/v2/views (%s)", (_, dsProvider) => {
+])("/v2/views (%s)", (name, dsProvider) => {
const config = setup.getConfig()
- const isInternal = !dsProvider
+ const isSqs = name === "sqs"
+ const isLucene = name === "lucene"
+ const isInternal = isSqs || isLucene
let table: Table
let datasource: Datasource
+ let envCleanup: (() => void) | undefined
function saveTableRequest(
...overrides: Partial>[]
@@ -82,6 +88,9 @@ describe.each([
}
beforeAll(async () => {
+ if (isSqs) {
+ envCleanup = config.setEnv({ SQS_SEARCH_ENABLE: "true" })
+ }
await config.init()
if (dsProvider) {
@@ -94,6 +103,9 @@ describe.each([
afterAll(async () => {
setup.afterAll()
+ if (envCleanup) {
+ envCleanup()
+ }
})
beforeEach(() => {
@@ -1252,12 +1264,13 @@ describe.each([
paginate: true,
limit: 4,
query: {},
+ countRows: true,
})
expect(page1).toEqual({
rows: expect.arrayContaining(rows.slice(0, 4)),
- totalRows: isInternal ? 10 : undefined,
hasNextPage: true,
bookmark: expect.anything(),
+ totalRows: 10,
})
const page2 = await config.api.viewV2.search(view.id, {
@@ -1265,12 +1278,13 @@ describe.each([
limit: 4,
bookmark: page1.bookmark,
query: {},
+ countRows: true,
})
expect(page2).toEqual({
rows: expect.arrayContaining(rows.slice(4, 8)),
- totalRows: isInternal ? 10 : undefined,
hasNextPage: true,
bookmark: expect.anything(),
+ totalRows: 10,
})
const page3 = await config.api.viewV2.search(view.id, {
@@ -1278,13 +1292,17 @@ describe.each([
limit: 4,
bookmark: page2.bookmark,
query: {},
+ countRows: true,
})
- expect(page3).toEqual({
+ const expectation: SearchResponse = {
rows: expect.arrayContaining(rows.slice(8)),
- totalRows: isInternal ? 10 : undefined,
hasNextPage: false,
- bookmark: expect.anything(),
- })
+ totalRows: 10,
+ }
+ if (isLucene) {
+ expectation.bookmark = expect.anything()
+ }
+ expect(page3).toEqual(expectation)
})
const sortTestOptions: [
diff --git a/packages/server/src/api/routes/utils/validators.ts b/packages/server/src/api/routes/utils/validators.ts
index a63b29fe5a..671ce95038 100644
--- a/packages/server/src/api/routes/utils/validators.ts
+++ b/packages/server/src/api/routes/utils/validators.ts
@@ -109,6 +109,7 @@ export function internalSearchValidator() {
sortOrder: OPTIONAL_STRING,
sortType: OPTIONAL_STRING,
paginate: Joi.boolean(),
+ countRows: Joi.boolean(),
bookmark: Joi.alternatives()
.try(OPTIONAL_STRING, OPTIONAL_NUMBER)
.optional(),
diff --git a/packages/server/src/integrations/base/query.ts b/packages/server/src/integrations/base/query.ts
index 371592bece..55886cd20f 100644
--- a/packages/server/src/integrations/base/query.ts
+++ b/packages/server/src/integrations/base/query.ts
@@ -22,6 +22,9 @@ export async function makeExternalQuery(
) {
throw new Error("Entity ID and table metadata do not align")
}
+ if (!datasource) {
+ throw new Error("No datasource provided for external query")
+ }
datasource = await sdk.datasources.enrich(datasource)
const Integration = await getIntegration(datasource.source)
// query is the opinionated function
diff --git a/packages/server/src/integrations/tests/sql.spec.ts b/packages/server/src/integrations/tests/sql.spec.ts
index cad1b346c0..b595508093 100644
--- a/packages/server/src/integrations/tests/sql.spec.ts
+++ b/packages/server/src/integrations/tests/sql.spec.ts
@@ -142,7 +142,7 @@ describe("SQL query builder", () => {
const query = sql._query(generateRelationshipJson({ schema: "production" }))
expect(query).toEqual({
bindings: [500, 5000],
- sql: `select "brands"."brand_id" as "brands.brand_id", "brands"."brand_name" as "brands.brand_name", "products"."product_id" as "products.product_id", "products"."product_name" as "products.product_name", "products"."brand_id" as "products.brand_id" from (select * from "production"."brands" limit $1) as "brands" left join "production"."products" as "products" on "brands"."brand_id" = "products"."brand_id" limit $2`,
+ sql: `select "brands"."brand_id" as "brands.brand_id", "brands"."brand_name" as "brands.brand_name", "products"."product_id" as "products.product_id", "products"."product_name" as "products.product_name", "products"."brand_id" as "products.brand_id" from (select * from "production"."brands" order by "test"."id" asc limit $1) as "brands" left join "production"."products" as "products" on "brands"."brand_id" = "products"."brand_id" order by "test"."id" asc limit $2`,
})
})
@@ -150,7 +150,7 @@ describe("SQL query builder", () => {
const query = sql._query(generateRelationshipJson())
expect(query).toEqual({
bindings: [500, 5000],
- sql: `select "brands"."brand_id" as "brands.brand_id", "brands"."brand_name" as "brands.brand_name", "products"."product_id" as "products.product_id", "products"."product_name" as "products.product_name", "products"."brand_id" as "products.brand_id" from (select * from "brands" limit $1) as "brands" left join "products" as "products" on "brands"."brand_id" = "products"."brand_id" limit $2`,
+ sql: `select "brands"."brand_id" as "brands.brand_id", "brands"."brand_name" as "brands.brand_name", "products"."product_id" as "products.product_id", "products"."product_name" as "products.product_name", "products"."brand_id" as "products.brand_id" from (select * from "brands" order by "test"."id" asc limit $1) as "brands" left join "products" as "products" on "brands"."brand_id" = "products"."brand_id" order by "test"."id" asc limit $2`,
})
})
@@ -160,7 +160,7 @@ describe("SQL query builder", () => {
)
expect(query).toEqual({
bindings: [500, 5000],
- sql: `select "stores"."store_id" as "stores.store_id", "stores"."store_name" as "stores.store_name", "products"."product_id" as "products.product_id", "products"."product_name" as "products.product_name" from (select * from "production"."stores" limit $1) as "stores" left join "production"."stocks" as "stocks" on "stores"."store_id" = "stocks"."store_id" left join "production"."products" as "products" on "products"."product_id" = "stocks"."product_id" limit $2`,
+ sql: `select "stores"."store_id" as "stores.store_id", "stores"."store_name" as "stores.store_name", "products"."product_id" as "products.product_id", "products"."product_name" as "products.product_name" from (select * from "production"."stores" order by "test"."id" asc limit $1) as "stores" left join "production"."stocks" as "stocks" on "stores"."store_id" = "stocks"."store_id" left join "production"."products" as "products" on "products"."product_id" = "stocks"."product_id" order by "test"."id" asc limit $2`,
})
})
@@ -175,8 +175,8 @@ describe("SQL query builder", () => {
})
)
expect(query).toEqual({
- bindings: ["john%", limit],
- sql: `select * from (select * from (select * from "test" where LOWER("test"."name") LIKE :1) where rownum <= :2) "test"`,
+ bindings: ["john%", limit, 5000],
+ sql: `select * from (select * from (select * from (select * from "test" where LOWER("test"."name") LIKE :1 order by "test"."id" asc) where rownum <= :2) "test" order by "test"."id" asc) where rownum <= :3`,
})
query = new Sql(SqlClient.ORACLE, limit)._query(
@@ -190,8 +190,8 @@ describe("SQL query builder", () => {
})
)
expect(query).toEqual({
- bindings: ["%20%", "%25%", `%"john"%`, `%"mary"%`, limit],
- sql: `select * from (select * from (select * from "test" where (COALESCE(LOWER("test"."age"), '') LIKE :1 AND COALESCE(LOWER("test"."age"), '') LIKE :2) and (COALESCE(LOWER("test"."name"), '') LIKE :3 AND COALESCE(LOWER("test"."name"), '') LIKE :4)) where rownum <= :5) "test"`,
+ bindings: ["%20%", "%25%", `%"john"%`, `%"mary"%`, limit, 5000],
+ sql: `select * from (select * from (select * from (select * from "test" where (COALESCE(LOWER("test"."age"), '') LIKE :1 AND COALESCE(LOWER("test"."age"), '') LIKE :2) and (COALESCE(LOWER("test"."name"), '') LIKE :3 AND COALESCE(LOWER("test"."name"), '') LIKE :4) order by "test"."id" asc) where rownum <= :5) "test" order by "test"."id" asc) where rownum <= :6`,
})
query = new Sql(SqlClient.ORACLE, limit)._query(
@@ -204,8 +204,8 @@ describe("SQL query builder", () => {
})
)
expect(query).toEqual({
- bindings: [`%jo%`, limit],
- sql: `select * from (select * from (select * from "test" where LOWER("test"."name") LIKE :1) where rownum <= :2) "test"`,
+ bindings: [`%jo%`, limit, 5000],
+ sql: `select * from (select * from (select * from (select * from "test" where LOWER("test"."name") LIKE :1 order by "test"."id" asc) where rownum <= :2) "test" order by "test"."id" asc) where rownum <= :3`,
})
})
})
diff --git a/packages/server/src/integrations/tests/sqlAlias.spec.ts b/packages/server/src/integrations/tests/sqlAlias.spec.ts
index 67f3d1d05d..0b433896bf 100644
--- a/packages/server/src/integrations/tests/sqlAlias.spec.ts
+++ b/packages/server/src/integrations/tests/sqlAlias.spec.ts
@@ -57,15 +57,14 @@ describe("Captures of real examples", () => {
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
expect(query).toEqual({
bindings: [relationshipLimit, limit],
- sql: multiline(`select "a"."year" as "a.year", "a"."firstname" as "a.firstname", "a"."personid" as "a.personid",
+ sql: expect.stringContaining(
+ multiline(`select "a"."year" as "a.year", "a"."firstname" as "a.firstname", "a"."personid" as "a.personid",
"a"."address" as "a.address", "a"."age" as "a.age", "a"."type" as "a.type", "a"."city" as "a.city",
"a"."lastname" as "a.lastname", "b"."executorid" as "b.executorid", "b"."taskname" as "b.taskname",
"b"."taskid" as "b.taskid", "b"."completed" as "b.completed", "b"."qaid" as "b.qaid",
"b"."executorid" as "b.executorid", "b"."taskname" as "b.taskname", "b"."taskid" as "b.taskid",
- "b"."completed" as "b.completed", "b"."qaid" as "b.qaid"
- from (select * from "persons" as "a" order by "a"."firstname" asc nulls first limit $1) as "a"
- left join "tasks" as "b" on "a"."personid" = "b"."qaid" or "a"."personid" = "b"."executorid"
- order by "a"."firstname" asc nulls first limit $2`),
+ "b"."completed" as "b.completed", "b"."qaid" as "b.qaid"`)
+ ),
})
})
@@ -74,13 +73,10 @@ describe("Captures of real examples", () => {
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
expect(query).toEqual({
bindings: [relationshipLimit, "assembling", limit],
- sql: multiline(`select "a"."productname" as "a.productname", "a"."productid" as "a.productid",
- "b"."executorid" as "b.executorid", "b"."taskname" as "b.taskname", "b"."taskid" as "b.taskid",
- "b"."completed" as "b.completed", "b"."qaid" as "b.qaid"
- from (select * from "products" as "a" order by "a"."productname" asc nulls first limit $1) as "a"
- left join "products_tasks" as "c" on "a"."productid" = "c"."productid"
- left join "tasks" as "b" on "b"."taskid" = "c"."taskid" where COALESCE("b"."taskname" = $2, FALSE)
- order by "a"."productname" asc nulls first limit $3`),
+ sql: expect.stringContaining(
+ multiline(`where COALESCE("b"."taskname" = $2, FALSE)
+ order by "a"."productname" asc nulls first, "a"."productid" asc limit $3`)
+ ),
})
})
@@ -89,13 +85,10 @@ describe("Captures of real examples", () => {
let query = new Sql(SqlClient.POSTGRES, limit)._query(queryJson)
expect(query).toEqual({
bindings: [relationshipLimit, limit],
- sql: multiline(`select "a"."productname" as "a.productname", "a"."productid" as "a.productid",
- "b"."executorid" as "b.executorid", "b"."taskname" as "b.taskname", "b"."taskid" as "b.taskid",
- "b"."completed" as "b.completed", "b"."qaid" as "b.qaid"
- from (select * from "products" as "a" order by "a"."productname" asc nulls first limit $1) as "a"
- left join "products_tasks" as "c" on "a"."productid" = "c"."productid"
- left join "tasks" as "b" on "b"."taskid" = "c"."taskid"
- order by "a"."productname" asc nulls first limit $2`),
+ sql: expect.stringContaining(
+ multiline(`left join "products_tasks" as "c" on "a"."productid" = "c"."productid"
+ left join "tasks" as "b" on "b"."taskid" = "c"."taskid" `)
+ ),
})
})
@@ -106,11 +99,11 @@ describe("Captures of real examples", () => {
expect(query).toEqual({
bindings: [...filters, limit, limit],
sql: multiline(`select "a"."executorid" as "a.executorid", "a"."taskname" as "a.taskname",
- "a"."taskid" as "a.taskid", "a"."completed" as "a.completed", "a"."qaid" as "a.qaid",
- "b"."productname" as "b.productname", "b"."productid" as "b.productid"
- from (select * from "tasks" as "a" where "a"."taskid" in ($1, $2) limit $3) as "a"
- left join "products_tasks" as "c" on "a"."taskid" = "c"."taskid"
- left join "products" as "b" on "b"."productid" = "c"."productid" limit $4`),
+ "a"."taskid" as "a.taskid", "a"."completed" as "a.completed", "a"."qaid" as "a.qaid",
+ "b"."productname" as "b.productname", "b"."productid" as "b.productid"
+ from (select * from "tasks" as "a" where "a"."taskid" in ($1, $2) order by "a"."taskid" asc limit $3) as "a"
+ left join "products_tasks" as "c" on "a"."taskid" = "c"."taskid"
+ left join "products" as "b" on "b"."productid" = "c"."productid" order by "a"."taskid" asc limit $4`),
})
})
@@ -132,19 +125,11 @@ describe("Captures of real examples", () => {
equalValue,
limit,
],
- sql: multiline(`select "a"."executorid" as "a.executorid", "a"."taskname" as "a.taskname", "a"."taskid" as "a.taskid",
- "a"."completed" as "a.completed", "a"."qaid" as "a.qaid", "b"."productname" as "b.productname",
- "b"."productid" as "b.productid", "c"."year" as "c.year", "c"."firstname" as "c.firstname",
- "c"."personid" as "c.personid", "c"."address" as "c.address", "c"."age" as "c.age", "c"."type" as "c.type",
- "c"."city" as "c.city", "c"."lastname" as "c.lastname", "c"."year" as "c.year", "c"."firstname" as "c.firstname",
- "c"."personid" as "c.personid", "c"."address" as "c.address", "c"."age" as "c.age", "c"."type" as "c.type",
- "c"."city" as "c.city", "c"."lastname" as "c.lastname"
- from (select * from "tasks" as "a" where COALESCE("a"."completed" != $1, TRUE)
- order by "a"."taskname" asc nulls first limit $2) as "a"
- left join "products_tasks" as "d" on "a"."taskid" = "d"."taskid"
- left join "products" as "b" on "b"."productid" = "d"."productid"
- left join "persons" as "c" on "a"."executorid" = "c"."personid" or "a"."qaid" = "c"."personid"
- where "c"."year" between $3 and $4 and COALESCE("b"."productname" = $5, FALSE) order by "a"."taskname" asc nulls first limit $6`),
+ sql: expect.stringContaining(
+ multiline(
+ `where "c"."year" between $3 and $4 and COALESCE("b"."productname" = $5, FALSE)`
+ )
+ ),
})
})
})
@@ -200,8 +185,9 @@ describe("Captures of real examples", () => {
returningQuery = input
}, queryJson)
expect(returningQuery).toEqual({
- sql: "select * from (select top (@p0) * from [people] where CASE WHEN [people].[name] = @p1 THEN 1 ELSE 0 END = 1 and CASE WHEN [people].[age] = @p2 THEN 1 ELSE 0 END = 1 order by [people].[name] asc) as [people]",
- bindings: [1, "Test", 22],
+ sql: multiline(`select top (@p0) * from (select top (@p1) * from [people] where CASE WHEN [people].[name] = @p2
+ THEN 1 ELSE 0 END = 1 and CASE WHEN [people].[age] = @p3 THEN 1 ELSE 0 END = 1 order by [people].[name] asc) as [people]`),
+ bindings: [5000, 1, "Test", 22],
})
})
})
diff --git a/packages/server/src/sdk/app/rows/search/external.ts b/packages/server/src/sdk/app/rows/search/external.ts
index 0430965ec7..9fc3487f62 100644
--- a/packages/server/src/sdk/app/rows/search/external.ts
+++ b/packages/server/src/sdk/app/rows/search/external.ts
@@ -1,14 +1,14 @@
import {
- SortJson,
+ IncludeRelationship,
Operation,
PaginationJson,
- IncludeRelationship,
Row,
- SearchFilters,
RowSearchParams,
+ SearchFilters,
SearchResponse,
- Table,
+ SortJson,
SortOrder,
+ Table,
} from "@budibase/types"
import * as exporters from "../../../../api/controllers/view/exporters"
import { handleRequest } from "../../../../api/controllers/row/external"
@@ -18,7 +18,7 @@ import {
} from "../../../../integrations/utils"
import { utils } from "@budibase/shared-core"
import { ExportRowsParams, ExportRowsResult } from "./types"
-import { HTTPError, db } from "@budibase/backend-core"
+import { db, HTTPError } from "@budibase/backend-core"
import pick from "lodash/pick"
import { outputProcessing } from "../../../../utilities/rowProcessor"
import sdk from "../../../"
@@ -28,20 +28,26 @@ export async function search(
table: Table
): Promise> {
const { tableId } = options
- const { paginate, query, ...params } = options
+ const { countRows, paginate, query, ...params } = options
const { limit } = params
let bookmark =
(params.bookmark && parseInt(params.bookmark as string)) || undefined
if (paginate && !bookmark) {
- bookmark = 1
+ bookmark = 0
}
- let paginateObj = {}
+ let paginateObj: PaginationJson | undefined
- if (paginate) {
+ if (paginate && !limit) {
+ throw new Error("Cannot paginate query without a limit")
+ }
+
+ if (paginate && limit) {
paginateObj = {
// add one so we can track if there is another page
- limit: limit,
- page: bookmark,
+ limit: limit + 1,
+ }
+ if (bookmark) {
+ paginateObj.offset = limit * bookmark
}
} else if (params && limit) {
paginateObj = {
@@ -69,24 +75,27 @@ export async function search(
}
try {
- let rows = await handleRequest(Operation.READ, tableId, {
+ const parameters = {
filters: query,
sort,
paginate: paginateObj as PaginationJson,
includeSqlRelationships: IncludeRelationship.INCLUDE,
- })
+ }
+ const queries: Promise[] = []
+ queries.push(handleRequest(Operation.READ, tableId, parameters))
+ if (countRows) {
+ queries.push(handleRequest(Operation.COUNT, tableId, parameters))
+ }
+ const responses = await Promise.all(queries)
+ let rows = responses[0] as Row[]
+ const totalRows =
+ responses.length > 1 ? (responses[1] as number) : undefined
+
let hasNextPage = false
- if (paginate && rows.length === limit) {
- const nextRows = await handleRequest(Operation.READ, tableId, {
- filters: query,
- sort,
- paginate: {
- limit: 1,
- page: bookmark! * limit + 1,
- },
- includeSqlRelationships: IncludeRelationship.INCLUDE,
- })
- hasNextPage = nextRows.length > 0
+ // remove the extra row if it's there
+ if (paginate && limit && rows.length > limit) {
+ rows.pop()
+ hasNextPage = true
}
if (options.fields) {
@@ -100,7 +109,17 @@ export async function search(
})
// need wrapper object for bookmarks etc when paginating
- return { rows, hasNextPage, bookmark: bookmark && bookmark + 1 }
+ const response: SearchResponse = { rows, hasNextPage }
+ if (hasNextPage && bookmark != null) {
+ response.bookmark = bookmark + 1
+ }
+ if (totalRows != null) {
+ response.totalRows = totalRows
+ }
+ if (paginate && !hasNextPage) {
+ response.hasNextPage = false
+ }
+ return response
} catch (err: any) {
if (err.message && err.message.includes("does not exist")) {
throw new Error(
diff --git a/packages/server/src/sdk/app/rows/search/sqs.ts b/packages/server/src/sdk/app/rows/search/sqs.ts
index 98b4053931..bb1d62affc 100644
--- a/packages/server/src/sdk/app/rows/search/sqs.ts
+++ b/packages/server/src/sdk/app/rows/search/sqs.ts
@@ -12,6 +12,7 @@ import {
SortType,
SqlClient,
Table,
+ Datasource,
} from "@budibase/types"
import {
buildInternalRelationships,
@@ -28,6 +29,7 @@ import { CONSTANT_INTERNAL_ROW_COLS } from "../../../../db/utils"
import AliasTables from "../sqlAlias"
import { outputProcessing } from "../../../../utilities/rowProcessor"
import pick from "lodash/pick"
+import { processRowCountResponse } from "../utils"
const builder = new sql.Sql(SqlClient.SQL_LITE)
@@ -95,14 +97,29 @@ function buildTableMap(tables: Table[]) {
// update the table name, should never query by name for SQLite
table.originalName = table.name
table.name = table._id!
+ // need a primary for sorting, lookups etc
+ table.primary = ["_id"]
tableMap[table._id!] = table
}
return tableMap
}
-async function runSqlQuery(json: QueryJson, tables: Table[]) {
+function runSqlQuery(json: QueryJson, tables: Table[]): Promise
+function runSqlQuery(
+ json: QueryJson,
+ tables: Table[],
+ opts: { countTotalRows: true }
+): Promise
+async function runSqlQuery(
+ json: QueryJson,
+ tables: Table[],
+ opts?: { countTotalRows?: boolean }
+) {
const alias = new AliasTables(tables.map(table => table.name))
- return await alias.queryWithAliasing(json, async json => {
+ if (opts?.countTotalRows) {
+ json.endpoint.operation = Operation.COUNT
+ }
+ const processSQLQuery = async (_: Datasource, json: QueryJson) => {
const query = builder._query(json, {
disableReturning: true,
})
@@ -124,17 +141,27 @@ async function runSqlQuery(json: QueryJson, tables: Table[]) {
const db = context.getAppDB()
return await db.sql(sql, bindings)
- })
+ }
+ const response = await alias.queryWithAliasing(json, processSQLQuery)
+ if (opts?.countTotalRows) {
+ return processRowCountResponse(response)
+ } else {
+ return response
+ }
}
export async function search(
options: RowSearchParams,
table: Table
): Promise> {
- const { paginate, query, ...params } = options
+ let { paginate, query, ...params } = options
const allTables = await sdk.tables.getAllInternalTables()
const allTablesMap = buildTableMap(allTables)
+ // make sure we have the mapped/latest table
+ if (table?._id) {
+ table = allTablesMap[table?._id]
+ }
if (!table) {
throw new Error("Unable to find table")
}
@@ -169,7 +196,7 @@ export async function search(
sortField.type === FieldType.NUMBER ? SortType.NUMBER : SortType.STRING
request.sort = {
[sortField.name]: {
- direction: params.sortOrder || SortOrder.DESCENDING,
+ direction: params.sortOrder || SortOrder.ASCENDING,
type: sortType as SortType,
},
}
@@ -180,7 +207,8 @@ export async function search(
}
const bookmark: number = (params.bookmark as number) || 0
- if (paginate && params.limit) {
+ if (params.limit) {
+ paginate = true
request.paginate = {
limit: params.limit + 1,
offset: bookmark * params.limit,
@@ -188,7 +216,20 @@ export async function search(
}
try {
- const rows = await runSqlQuery(request, allTables)
+ const queries: Promise[] = []
+ queries.push(runSqlQuery(request, allTables))
+ if (options.countRows) {
+ // get the total count of rows
+ queries.push(
+ runSqlQuery(request, allTables, {
+ countTotalRows: true,
+ })
+ )
+ }
+ const responses = await Promise.all(queries)
+ let rows = responses[0] as Row[]
+ const totalRows =
+ responses.length > 1 ? (responses[1] as number) : undefined
// process from the format of tableId.column to expected format also
// make sure JSON columns corrected
@@ -201,7 +242,8 @@ export async function search(
// check for pagination final row
let nextRow: Row | undefined
- if (paginate && params.limit && processed.length > params.limit) {
+ if (paginate && params.limit && rows.length > params.limit) {
+ // remove the extra row that confirmed if there is another row to move to
nextRow = processed.pop()
}
@@ -217,21 +259,21 @@ export async function search(
finalRows = finalRows.map((r: any) => pick(r, fields))
}
- // check for pagination
- if (paginate) {
- const response: SearchResponse = {
- rows: finalRows,
- }
- if (nextRow) {
- response.hasNextPage = true
- response.bookmark = bookmark + 1
- }
- return response
- } else {
- return {
- rows: finalRows,
- }
+ const response: SearchResponse = {
+ rows: finalRows,
}
+ if (totalRows != null) {
+ response.totalRows = totalRows
+ }
+ // check for pagination
+ if (paginate && nextRow) {
+ response.hasNextPage = true
+ response.bookmark = bookmark + 1
+ }
+ if (paginate && !nextRow) {
+ response.hasNextPage = false
+ }
+ return response
} catch (err: any) {
const msg = typeof err === "string" ? err : err.message
if (err.status === 404 && msg?.includes(SQLITE_DESIGN_DOC_ID)) {
diff --git a/packages/server/src/sdk/app/rows/sqlAlias.ts b/packages/server/src/sdk/app/rows/sqlAlias.ts
index ab4f5d2844..bc8fc56d5e 100644
--- a/packages/server/src/sdk/app/rows/sqlAlias.ts
+++ b/packages/server/src/sdk/app/rows/sqlAlias.ts
@@ -11,7 +11,12 @@ import { SQS_DATASOURCE_INTERNAL } from "@budibase/backend-core"
import { getSQLClient } from "./utils"
import { cloneDeep } from "lodash"
import datasources from "../datasources"
-import { makeExternalQuery } from "../../../integrations/base/query"
+import { BudibaseInternalDB } from "../../../db/utils"
+
+type PerformQueryFunction = (
+ datasource: Datasource,
+ json: QueryJson
+) => Promise
const WRITE_OPERATIONS: Operation[] = [
Operation.CREATE,
@@ -65,7 +70,7 @@ export default class AliasTables {
this.charSeq = new CharSequence()
}
- isAliasingEnabled(json: QueryJson, datasource: Datasource) {
+ isAliasingEnabled(json: QueryJson, datasource?: Datasource) {
const operation = json.endpoint.operation
const fieldLength = json.resource?.fields?.length
if (
@@ -75,6 +80,10 @@ export default class AliasTables {
) {
return false
}
+ // SQS - doesn't have a datasource
+ if (!datasource) {
+ return true
+ }
try {
const sqlClient = getSQLClient(datasource)
const isWrite = WRITE_OPERATIONS.includes(operation)
@@ -167,13 +176,14 @@ export default class AliasTables {
async queryWithAliasing(
json: QueryJson,
- queryFn?: (json: QueryJson) => Promise
+ queryFn: PerformQueryFunction
): Promise {
const datasourceId = json.endpoint.datasourceId
const isSqs = datasourceId === SQS_DATASOURCE_INTERNAL
- let aliasingEnabled: boolean, datasource: Datasource | undefined
+ let aliasingEnabled: boolean, datasource: Datasource
if (isSqs) {
- aliasingEnabled = true
+ aliasingEnabled = this.isAliasingEnabled(json)
+ datasource = BudibaseInternalDB
} else {
datasource = await datasources.get(datasourceId)
aliasingEnabled = this.isAliasingEnabled(json, datasource)
@@ -225,14 +235,7 @@ export default class AliasTables {
json.tableAliases = invertedTableAliases
}
- let response: DatasourcePlusQueryResponse
- if (datasource && !isSqs) {
- response = await makeExternalQuery(datasource, json)
- } else if (queryFn) {
- response = await queryFn(json)
- } else {
- throw new Error("No supplied method to perform aliased query")
- }
+ let response: DatasourcePlusQueryResponse = await queryFn(datasource, json)
if (Array.isArray(response) && aliasingEnabled) {
return this.reverse(response)
} else {
diff --git a/packages/server/src/sdk/app/rows/utils.ts b/packages/server/src/sdk/app/rows/utils.ts
index bb37fd99f3..cd1b663f6a 100644
--- a/packages/server/src/sdk/app/rows/utils.ts
+++ b/packages/server/src/sdk/app/rows/utils.ts
@@ -50,6 +50,17 @@ export function getSQLClient(datasource: Datasource): SqlClient {
throw new Error("Unable to determine client for SQL datasource")
}
+export function processRowCountResponse(
+ response: DatasourcePlusQueryResponse
+): number {
+ if (response && response.length === 1 && "total" in response[0]) {
+ const total = response[0].total
+ return typeof total === "number" ? total : parseInt(total)
+ } else {
+ throw new Error("Unable to count rows in query - no count response")
+ }
+}
+
export async function getDatasourceAndQuery(
json: QueryJson
): Promise {
diff --git a/packages/server/src/sdk/app/tables/internal/index.ts b/packages/server/src/sdk/app/tables/internal/index.ts
index ea40d2bfe9..fc32708708 100644
--- a/packages/server/src/sdk/app/tables/internal/index.ts
+++ b/packages/server/src/sdk/app/tables/internal/index.ts
@@ -17,6 +17,7 @@ import { cloneDeep } from "lodash/fp"
import isEqual from "lodash/isEqual"
import { runStaticFormulaChecks } from "../../../../api/controllers/table/bulkFormula"
import { context } from "@budibase/backend-core"
+import { findDuplicateInternalColumns } from "@budibase/shared-core"
import { getTable } from "../getters"
import { checkAutoColumns } from "./utils"
import * as viewsSdk from "../../views"
@@ -44,6 +45,17 @@ export async function save(
if (hasTypeChanged(table, oldTable)) {
throw new Error("A column type has changed.")
}
+
+ // check for case sensitivity - we don't want to allow duplicated columns
+ const duplicateColumn = findDuplicateInternalColumns(table)
+ if (duplicateColumn.length) {
+ throw new Error(
+ `Column(s) "${duplicateColumn.join(
+ ", "
+ )}" are duplicated - check for other columns with these name (case in-sensitive)`
+ )
+ }
+
// check that subtypes have been maintained
table = checkAutoColumns(table, oldTable)
diff --git a/packages/shared-core/src/constants/index.ts b/packages/shared-core/src/constants/index.ts
index afb7e659e1..084bc8fe9e 100644
--- a/packages/shared-core/src/constants/index.ts
+++ b/packages/shared-core/src/constants/index.ts
@@ -1,5 +1,6 @@
export * from "./api"
export * from "./fields"
+export * from "./rows"
export const OperatorOptions = {
Equals: {
diff --git a/packages/shared-core/src/constants/rows.ts b/packages/shared-core/src/constants/rows.ts
new file mode 100644
index 0000000000..bfa7595d62
--- /dev/null
+++ b/packages/shared-core/src/constants/rows.ts
@@ -0,0 +1,14 @@
+export const CONSTANT_INTERNAL_ROW_COLS = [
+ "_id",
+ "_rev",
+ "type",
+ "createdAt",
+ "updatedAt",
+ "tableId",
+] as const
+
+export const CONSTANT_EXTERNAL_ROW_COLS = ["_id", "_rev", "tableId"] as const
+
+export function isInternalColumnName(name: string): boolean {
+ return (CONSTANT_INTERNAL_ROW_COLS as readonly string[]).includes(name)
+}
diff --git a/packages/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts
index 52ab3ed626..bd75406e26 100644
--- a/packages/shared-core/src/filters.ts
+++ b/packages/shared-core/src/filters.ts
@@ -12,6 +12,7 @@ import {
SortOrder,
RowSearchParams,
EmptyFilterOption,
+ SearchResponse,
} from "@budibase/types"
import dayjs from "dayjs"
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
@@ -262,15 +263,23 @@ export const buildQuery = (filter: SearchFilter[]) => {
return query
}
-export const search = (docs: Record[], query: RowSearchParams) => {
+export const search = (
+ docs: Record[],
+ query: RowSearchParams
+): SearchResponse> => {
let result = runQuery(docs, query.query)
if (query.sort) {
result = sort(result, query.sort, query.sortOrder || SortOrder.ASCENDING)
}
+ let totalRows = result.length
if (query.limit) {
result = limit(result, query.limit.toString())
}
- return result
+ const response: SearchResponse> = { rows: result }
+ if (query.countRows) {
+ response.totalRows = totalRows
+ }
+ return response
}
/**
diff --git a/packages/shared-core/src/table.ts b/packages/shared-core/src/table.ts
index 7706b78037..4b578a2aef 100644
--- a/packages/shared-core/src/table.ts
+++ b/packages/shared-core/src/table.ts
@@ -1,4 +1,5 @@
-import { FieldType } from "@budibase/types"
+import { FieldType, Table } from "@budibase/types"
+import { CONSTANT_INTERNAL_ROW_COLS } from "./constants"
const allowDisplayColumnByType: Record = {
[FieldType.STRING]: true,
@@ -51,3 +52,22 @@ export function canBeDisplayColumn(type: FieldType): boolean {
export function canBeSortColumn(type: FieldType): boolean {
return !!allowSortColumnByType[type]
}
+
+export function findDuplicateInternalColumns(table: Table): string[] {
+ // get the column names
+ const columnNames = Object.keys(table.schema)
+ .concat(CONSTANT_INTERNAL_ROW_COLS)
+ .map(colName => colName.toLowerCase())
+ // there are duplicates
+ const set = new Set(columnNames)
+ let duplicates: string[] = []
+ if (set.size !== columnNames.length) {
+ for (let key of set.keys()) {
+ const count = columnNames.filter(name => name === key).length
+ if (count > 1) {
+ duplicates.push(key)
+ }
+ }
+ }
+ return duplicates
+}
diff --git a/packages/types/src/api/web/app/rows.ts b/packages/types/src/api/web/app/rows.ts
index 5d49f01bfc..c120af0628 100644
--- a/packages/types/src/api/web/app/rows.ts
+++ b/packages/types/src/api/web/app/rows.ts
@@ -25,6 +25,7 @@ export interface SearchViewRowRequest
| "bookmark"
| "paginate"
| "query"
+ | "countRows"
> {}
export interface SearchRowResponse {
diff --git a/packages/types/src/sdk/datasources.ts b/packages/types/src/sdk/datasources.ts
index 23caa2c618..bdad9c4825 100644
--- a/packages/types/src/sdk/datasources.ts
+++ b/packages/types/src/sdk/datasources.ts
@@ -8,6 +8,7 @@ export enum Operation {
READ = "READ",
UPDATE = "UPDATE",
DELETE = "DELETE",
+ COUNT = "COUNT",
BULK_CREATE = "BULK_CREATE",
BULK_UPSERT = "BULK_UPSERT",
CREATE_TABLE = "CREATE_TABLE",
@@ -188,7 +189,7 @@ export interface Schema {
}
// return these when an operation occurred but we got no response
-enum DSPlusOperation {
+export enum DSPlusOperation {
CREATE = "create",
READ = "read",
UPDATE = "update",
@@ -198,6 +199,7 @@ enum DSPlusOperation {
export type DatasourcePlusQueryResponse =
| Row[]
| Record[]
+ | { total: number }[]
| void
export interface DatasourcePlus extends IntegrationBase {
diff --git a/packages/types/src/sdk/row.ts b/packages/types/src/sdk/row.ts
index 7f3fc1f391..b0b137034b 100644
--- a/packages/types/src/sdk/row.ts
+++ b/packages/types/src/sdk/row.ts
@@ -17,6 +17,7 @@ export interface SearchParams {
fields?: string[]
indexer?: () => Promise
rows?: Row[]
+ countRows?: boolean
}
// when searching for rows we want a more extensive search type that requires certain properties
diff --git a/yarn.lock b/yarn.lock
index 3606f068b1..9914c334df 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10296,7 +10296,7 @@ engine.io-parser@~5.0.3:
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45"
integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==
-engine.io@~6.4.1:
+engine.io@~6.4.2:
version "6.4.2"
resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.4.2.tgz#ffeaf68f69b1364b0286badddf15ff633476473f"
integrity sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==
@@ -20160,17 +20160,25 @@ socket.io-parser@~4.2.1:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
-socket.io@4.6.1:
- version "4.6.1"
- resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.6.1.tgz#62ec117e5fce0692fa50498da9347cfb52c3bc70"
- integrity sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==
+socket.io-parser@~4.2.4:
+ version "4.2.4"
+ resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83"
+ integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==
+ dependencies:
+ "@socket.io/component-emitter" "~3.1.0"
+ debug "~4.3.1"
+
+socket.io@4.6.2:
+ version "4.6.2"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.6.2.tgz#d597db077d4df9cbbdfaa7a9ed8ccc3d49439786"
+ integrity sha512-Vp+lSks5k0dewYTfwgPT9UeGGd+ht7sCpB7p0e83VgO4X/AHYWhXITMrNk/pg8syY2bpx23ptClCQuHhqi2BgQ==
dependencies:
accepts "~1.3.4"
base64id "~2.0.0"
debug "~4.3.2"
- engine.io "~6.4.1"
+ engine.io "~6.4.2"
socket.io-adapter "~2.5.2"
- socket.io-parser "~4.2.1"
+ socket.io-parser "~4.2.4"
socks-proxy-agent@^7.0.0:
version "7.0.0"
@@ -21102,18 +21110,6 @@ tar@6.1.11:
mkdirp "^1.0.3"
yallist "^4.0.0"
-tar@6.1.15:
- version "6.1.15"
- resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.15.tgz#c9738b0b98845a3b344d334b8fa3041aaba53a69"
- integrity sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==
- dependencies:
- chownr "^2.0.0"
- fs-minipass "^2.0.0"
- minipass "^5.0.0"
- minizlib "^2.1.1"
- mkdirp "^1.0.3"
- yallist "^4.0.0"
-
tar@6.2.1, tar@^6.1.11, tar@^6.1.2:
version "6.2.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"