Merge remote-tracking branch 'origin/master' into dean-fixes
This commit is contained in:
commit
b9aec43397
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||||
"version": "2.29.17",
|
"version": "2.29.18",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants"
|
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 { doWithDB, directCouchAllDbs } from "./db"
|
||||||
import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata"
|
import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata"
|
||||||
import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions"
|
import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions"
|
||||||
|
@ -213,6 +213,11 @@ export function isSqsEnabledForTenant(): boolean {
|
||||||
return false
|
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
|
// 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
|
// we're not actually using SQS, we're using Lucene and the tests pass due to
|
||||||
// parity.
|
// parity.
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { GenericContainer, StartedTestContainer } from "testcontainers"
|
||||||
import { generator, structures } from "../../../tests"
|
import { generator, structures } from "../../../tests"
|
||||||
import RedisWrapper from "../redis"
|
import RedisWrapper from "../redis"
|
||||||
import { env } from "../.."
|
import { env } from "../.."
|
||||||
|
import { randomUUID } from "crypto"
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
@ -52,10 +53,10 @@ describe("redis", () => {
|
||||||
describe("bulkStore", () => {
|
describe("bulkStore", () => {
|
||||||
function createRandomObject(
|
function createRandomObject(
|
||||||
keyLength: number,
|
keyLength: number,
|
||||||
valueGenerator: () => any = () => generator.word()
|
valueGenerator: () => any = () => randomUUID()
|
||||||
) {
|
) {
|
||||||
return generator
|
return generator
|
||||||
.unique(() => generator.word(), keyLength)
|
.unique(() => randomUUID(), keyLength)
|
||||||
.reduce((acc, key) => {
|
.reduce((acc, key) => {
|
||||||
acc[key] = valueGenerator()
|
acc[key] = valueGenerator()
|
||||||
return acc
|
return acc
|
||||||
|
|
|
@ -819,7 +819,10 @@ describe.each([
|
||||||
const table = await config.api.table.save(tableRequest)
|
const table = await config.api.table.save(tableRequest)
|
||||||
|
|
||||||
const stringValue = generator.word()
|
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!, {
|
const existing = await config.api.row.save(table._id!, {
|
||||||
string: stringValue,
|
string: stringValue,
|
||||||
|
|
|
@ -809,6 +809,20 @@ describe.each([
|
||||||
},
|
},
|
||||||
}).toContainExactly([{ name: "foo" }, { name: "bar" }])
|
}).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", () => {
|
describe("fuzzy", () => {
|
||||||
|
|
|
@ -24,16 +24,6 @@ export enum FilterTypes {
|
||||||
ONE_OF = "oneOf",
|
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 = [
|
export const CanSwitchTypes = [
|
||||||
[FieldType.JSON, FieldType.ARRAY],
|
[FieldType.JSON, FieldType.ARRAY],
|
||||||
[
|
[
|
||||||
|
|
|
@ -2,14 +2,12 @@ import {
|
||||||
EmptyFilterOption,
|
EmptyFilterOption,
|
||||||
Row,
|
Row,
|
||||||
RowSearchParams,
|
RowSearchParams,
|
||||||
SearchFilters,
|
|
||||||
SearchResponse,
|
SearchResponse,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { isExternalTableID } from "../../../integrations/utils"
|
import { isExternalTableID } from "../../../integrations/utils"
|
||||||
import * as internal from "./search/internal"
|
import * as internal from "./search/internal"
|
||||||
import * as external from "./search/external"
|
import * as external from "./search/external"
|
||||||
import { NoEmptyFilterStrings } from "../../../constants"
|
|
||||||
import * as sqs from "./search/sqs"
|
import * as sqs from "./search/sqs"
|
||||||
import { ExportRowsParams, ExportRowsResult } from "./search/types"
|
import { ExportRowsParams, ExportRowsResult } from "./search/types"
|
||||||
import { dataFilters } from "@budibase/shared-core"
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
|
@ -32,44 +30,11 @@ function pickApi(tableId: any) {
|
||||||
return internal
|
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(
|
export async function search(
|
||||||
options: RowSearchParams
|
options: RowSearchParams
|
||||||
): Promise<SearchResponse<Row>> {
|
): Promise<SearchResponse<Row>> {
|
||||||
const isExternalTable = isExternalTableID(options.tableId)
|
const isExternalTable = isExternalTableID(options.tableId)
|
||||||
options.query = removeEmptyFilters(options.query || {})
|
options.query = dataFilters.cleanupQuery(options.query || {})
|
||||||
options.query = dataFilters.fixupFilterArrays(options.query)
|
options.query = dataFilters.fixupFilterArrays(options.query)
|
||||||
if (
|
if (
|
||||||
!dataFilters.hasFilters(options.query) &&
|
!dataFilters.hasFilters(options.query) &&
|
||||||
|
|
|
@ -106,31 +106,49 @@ export const NoEmptyFilterStrings = [
|
||||||
OperatorOptions.NotEquals.value,
|
OperatorOptions.NotEquals.value,
|
||||||
OperatorOptions.Contains.value,
|
OperatorOptions.Contains.value,
|
||||||
OperatorOptions.NotContains.value,
|
OperatorOptions.NotContains.value,
|
||||||
|
OperatorOptions.ContainsAny.value,
|
||||||
|
OperatorOptions.In.value,
|
||||||
] as (keyof SearchQueryFields)[]
|
] as (keyof SearchQueryFields)[]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes any fields that contain empty strings that would cause inconsistent
|
* Removes any fields that contain empty strings that would cause inconsistent
|
||||||
* behaviour with how backend tables are filtered (no value means no filter).
|
* 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) {
|
if (!query) {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
for (let filterField of NoEmptyFilterStrings) {
|
for (let filterField of NoEmptyFilterStrings) {
|
||||||
const operator = filterField as SearchFilterOperator
|
if (!query[filterField]) {
|
||||||
if (!query[operator]) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let [key, value] of Object.entries(query[operator]!)) {
|
for (let filterType of Object.keys(query)) {
|
||||||
if (value == null || value === "") {
|
if (filterType !== filterField) {
|
||||||
delete query[operator]![key]
|
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
|
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
|
* Removes a numeric prefix on field names designed to give fields uniqueness
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue