diff --git a/lerna.json b/lerna.json index 91a758507a..ff532def0b 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "2.29.17", + "version": "2.29.18", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index 5aad05d3e3..a7ad453066 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -1,6 +1,6 @@ import env from "../environment" import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants" -import { getTenantId, getGlobalDBName } from "../context" +import { getTenantId, getGlobalDBName, isMultiTenant } from "../context" import { doWithDB, directCouchAllDbs } from "./db" import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata" import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions" @@ -213,6 +213,11 @@ export function isSqsEnabledForTenant(): boolean { return false } + // single tenant (self host and dev) always enabled if flag set + if (!isMultiTenant()) { + return true + } + // This is to guard against the situation in tests where tests pass because // we're not actually using SQS, we're using Lucene and the tests pass due to // parity. diff --git a/packages/backend-core/src/redis/tests/redis.spec.ts b/packages/backend-core/src/redis/tests/redis.spec.ts index 4d11caf220..9160c6a6dd 100644 --- a/packages/backend-core/src/redis/tests/redis.spec.ts +++ b/packages/backend-core/src/redis/tests/redis.spec.ts @@ -2,6 +2,7 @@ import { GenericContainer, StartedTestContainer } from "testcontainers" import { generator, structures } from "../../../tests" import RedisWrapper from "../redis" import { env } from "../.." +import { randomUUID } from "crypto" jest.setTimeout(30000) @@ -52,10 +53,10 @@ describe("redis", () => { describe("bulkStore", () => { function createRandomObject( keyLength: number, - valueGenerator: () => any = () => generator.word() + valueGenerator: () => any = () => randomUUID() ) { return generator - .unique(() => generator.word(), keyLength) + .unique(() => randomUUID(), keyLength) .reduce((acc, key) => { acc[key] = valueGenerator() return acc diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 9fc7fb05c1..96be59a7e1 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -819,7 +819,10 @@ describe.each([ const table = await config.api.table.save(tableRequest) const stringValue = generator.word() - const naturalValue = generator.integer({ min: 0, max: 1000 }) + + // MySQL and MariaDB auto-increment fields have a minimum value of 1. If + // you try to save a row with a value of 0 it will use 1 instead. + const naturalValue = generator.integer({ min: 1, max: 1000 }) const existing = await config.api.row.save(table._id!, { string: stringValue, diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index 8d3722300f..e2335cb71c 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -809,6 +809,20 @@ describe.each([ }, }).toContainExactly([{ name: "foo" }, { name: "bar" }]) }) + + it("empty arrays returns all when onEmptyFilter is set to return 'all'", async () => { + await expectQuery({ + onEmptyFilter: EmptyFilterOption.RETURN_ALL, + oneOf: { name: [] }, + }).toContainExactly([{ name: "foo" }, { name: "bar" }]) + }) + + it("empty arrays returns all when onEmptyFilter is set to return 'none'", async () => { + await expectQuery({ + onEmptyFilter: EmptyFilterOption.RETURN_NONE, + oneOf: { name: [] }, + }).toContainExactly([]) + }) }) describe("fuzzy", () => { diff --git a/packages/server/src/constants/index.ts b/packages/server/src/constants/index.ts index 4fcc9c5d06..316d27b3fc 100644 --- a/packages/server/src/constants/index.ts +++ b/packages/server/src/constants/index.ts @@ -24,16 +24,6 @@ export enum FilterTypes { ONE_OF = "oneOf", } -export const NoEmptyFilterStrings = [ - FilterTypes.STRING, - FilterTypes.FUZZY, - FilterTypes.EQUAL, - FilterTypes.NOT_EQUAL, - FilterTypes.CONTAINS, - FilterTypes.NOT_CONTAINS, - FilterTypes.CONTAINS_ANY, -] - export const CanSwitchTypes = [ [FieldType.JSON, FieldType.ARRAY], [ diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts index 763fbb46ef..d074e8e2ac 100644 --- a/packages/server/src/sdk/app/rows/search.ts +++ b/packages/server/src/sdk/app/rows/search.ts @@ -2,14 +2,12 @@ import { EmptyFilterOption, Row, RowSearchParams, - SearchFilters, SearchResponse, SortOrder, } from "@budibase/types" import { isExternalTableID } from "../../../integrations/utils" import * as internal from "./search/internal" import * as external from "./search/external" -import { NoEmptyFilterStrings } from "../../../constants" import * as sqs from "./search/sqs" import { ExportRowsParams, ExportRowsResult } from "./search/types" import { dataFilters } from "@budibase/shared-core" @@ -32,44 +30,11 @@ function pickApi(tableId: any) { return internal } -function isEmptyArray(value: any) { - return Array.isArray(value) && value.length === 0 -} - -// don't do a pure falsy check, as 0 is included -// https://github.com/Budibase/budibase/issues/10118 -export function removeEmptyFilters(filters: SearchFilters) { - for (let filterField of NoEmptyFilterStrings) { - if (!filters[filterField]) { - continue - } - - for (let filterType of Object.keys(filters)) { - if (filterType !== filterField) { - continue - } - // don't know which one we're checking, type could be anything - const value = filters[filterType] as unknown - if (typeof value === "object") { - for (let [key, value] of Object.entries( - filters[filterType] as object - )) { - if (value == null || value === "" || isEmptyArray(value)) { - // @ts-ignore - delete filters[filterField][key] - } - } - } - } - } - return filters -} - export async function search( options: RowSearchParams ): Promise> { const isExternalTable = isExternalTableID(options.tableId) - options.query = removeEmptyFilters(options.query || {}) + options.query = dataFilters.cleanupQuery(options.query || {}) options.query = dataFilters.fixupFilterArrays(options.query) if ( !dataFilters.hasFilters(options.query) && diff --git a/packages/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts index d3fcae11e2..6db89dd2f3 100644 --- a/packages/shared-core/src/filters.ts +++ b/packages/shared-core/src/filters.ts @@ -106,31 +106,49 @@ export const NoEmptyFilterStrings = [ OperatorOptions.NotEquals.value, OperatorOptions.Contains.value, OperatorOptions.NotContains.value, + OperatorOptions.ContainsAny.value, + OperatorOptions.In.value, ] as (keyof SearchQueryFields)[] /** * Removes any fields that contain empty strings that would cause inconsistent * behaviour with how backend tables are filtered (no value means no filter). + * + * don't do a pure falsy check, as 0 is included + * https://github.com/Budibase/budibase/issues/10118 */ -const cleanupQuery = (query: SearchFilters) => { +export const cleanupQuery = (query: SearchFilters) => { if (!query) { return query } for (let filterField of NoEmptyFilterStrings) { - const operator = filterField as SearchFilterOperator - if (!query[operator]) { + if (!query[filterField]) { continue } - for (let [key, value] of Object.entries(query[operator]!)) { - if (value == null || value === "") { - delete query[operator]![key] + for (let filterType of Object.keys(query)) { + if (filterType !== filterField) { + continue + } + // don't know which one we're checking, type could be anything + const value = query[filterType] as unknown + if (typeof value === "object") { + for (let [key, value] of Object.entries(query[filterType] as object)) { + if (value == null || value === "" || isEmptyArray(value)) { + // @ts-ignore + delete query[filterField][key] + } + } } } } return query } +function isEmptyArray(value: any) { + return Array.isArray(value) && value.length === 0 +} + /** * Removes a numeric prefix on field names designed to give fields uniqueness */