Merge branch 'master' of github.com:Budibase/budibase into new-datepicker

This commit is contained in:
Andrew Kingston 2024-04-11 11:24:08 +01:00
commit 8598f53816
14 changed files with 187 additions and 71 deletions

View File

@ -51,11 +51,11 @@ http {
proxy_buffering off; proxy_buffering off;
set $csp_default "default-src 'self'"; set $csp_default "default-src 'self'";
set $csp_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.budibase.net https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io https://d2l5prqdbvm3op.cloudfront.net"; set $csp_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.budibase.net https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io https://d2l5prqdbvm3op.cloudfront.net https://us-assets.i.posthog.com";
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com"; set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
set $csp_object "object-src 'none'"; set $csp_object "object-src 'none'";
set $csp_base_uri "base-uri 'self'"; set $csp_base_uri "base-uri 'self'";
set $csp_connect "connect-src 'self' https://*.budibase.app https://*.budibaseqa.app https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com"; set $csp_connect "connect-src 'self' https://*.budibase.app https://*.budibaseqa.app https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com https://us.i.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com";
set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com"; set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com";
set $csp_frame "frame-src 'self' https:"; set $csp_frame "frame-src 'self' https:";
set $csp_img "img-src http: https: data: blob:"; set $csp_img "img-src http: https: data: blob:";

View File

@ -1,5 +1,5 @@
{ {
"version": "2.23.0", "version": "2.23.2",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*", "packages/*",

View File

@ -12,6 +12,7 @@ import {
isDocument, isDocument,
RowResponse, RowResponse,
RowValue, RowValue,
SqlQueryBinding,
} from "@budibase/types" } from "@budibase/types"
import { getCouchInfo } from "./connections" import { getCouchInfo } from "./connections"
import { directCouchUrlCall } from "./utils" import { directCouchUrlCall } from "./utils"
@ -248,14 +249,20 @@ export class DatabaseImpl implements Database {
}) })
} }
async sql<T extends Document>(sql: string): Promise<T[]> { async sql<T extends Document>(
sql: string,
parameters?: SqlQueryBinding
): Promise<T[]> {
const dbName = this.name const dbName = this.name
const url = `/${dbName}/${SQLITE_DESIGN_DOC_ID}` const url = `/${dbName}/${SQLITE_DESIGN_DOC_ID}`
const response = await directCouchUrlCall({ const response = await directCouchUrlCall({
url: `${this.couchInfo.sqlUrl}/${url}`, url: `${this.couchInfo.sqlUrl}/${url}`,
method: "POST", method: "POST",
cookie: this.couchInfo.cookie, cookie: this.couchInfo.cookie,
body: sql, body: {
query: sql,
args: parameters,
},
}) })
if (response.status > 300) { if (response.status > 300) {
throw new Error(await response.text()) throw new Error(await response.text())

View File

@ -13,6 +13,7 @@ import {
DatabaseQueryOpts, DatabaseQueryOpts,
Document, Document,
RowValue, RowValue,
SqlQueryBinding,
} from "@budibase/types" } from "@budibase/types"
import tracer from "dd-trace" import tracer from "dd-trace"
import { Writable } from "stream" import { Writable } from "stream"
@ -150,10 +151,13 @@ export class DDInstrumentedDatabase implements Database {
}) })
} }
sql<T extends Document>(sql: string): Promise<T[]> { sql<T extends Document>(
sql: string,
parameters?: SqlQueryBinding
): Promise<T[]> {
return tracer.trace("db.sql", span => { return tracer.trace("db.sql", span => {
span?.addTags({ db_name: this.name }) span?.addTags({ db_name: this.name })
return this.db.sql(sql) return this.db.sql(sql, parameters)
}) })
} }
} }

View File

@ -853,6 +853,7 @@
"array", "array",
"datetime", "datetime",
"attachment", "attachment",
"attachment_single",
"link", "link",
"formula", "formula",
"auto", "auto",
@ -1059,6 +1060,7 @@
"array", "array",
"datetime", "datetime",
"attachment", "attachment",
"attachment_single",
"link", "link",
"formula", "formula",
"auto", "auto",
@ -1276,6 +1278,7 @@
"array", "array",
"datetime", "datetime",
"attachment", "attachment",
"attachment_single",
"link", "link",
"formula", "formula",
"auto", "auto",
@ -1752,7 +1755,7 @@
}, },
"fuzzy": { "fuzzy": {
"type": "object", "type": "object",
"description": "A fuzzy search, only supported by internal tables." "description": "Searches for a sub-string within a string column, e.g. searching for 'dib' will match 'Budibase'."
}, },
"range": { "range": {
"type": "object", "type": "object",
@ -1786,6 +1789,36 @@
"oneOf": { "oneOf": {
"type": "object", "type": "object",
"description": "Searches for rows which have a column value that is any of the specified values. The format of this must be columnName -> [value1, value2]." "description": "Searches for rows which have a column value that is any of the specified values. The format of this must be columnName -> [value1, value2]."
},
"contains": {
"type": "object",
"description": "Searches for a value, or set of values in array column types (such as a multi-select). If an array of search options is provided then it must match all.",
"example": {
"arrayColumn": [
"a",
"b"
]
}
},
"notContains": {
"type": "object",
"description": "The logical inverse of contains. Only works on array column types. If an array of values is passed, the row must not match any of them to be returned in the response.",
"example": {
"arrayColumn": [
"a",
"b"
]
}
},
"containsAny": {
"type": "object",
"description": "As with the contains search, only works on array column types and searches for any of the provided values when given an array.",
"example": {
"arrayColumn": [
"a",
"b"
]
}
} }
} }
}, },

View File

@ -775,6 +775,7 @@ components:
- array - array
- datetime - datetime
- attachment - attachment
- attachment_single
- link - link
- formula - formula
- auto - auto
@ -940,6 +941,7 @@ components:
- array - array
- datetime - datetime
- attachment - attachment
- attachment_single
- link - link
- formula - formula
- auto - auto
@ -1112,6 +1114,7 @@ components:
- array - array
- datetime - datetime
- attachment - attachment
- attachment_single
- link - link
- formula - formula
- auto - auto
@ -1492,7 +1495,8 @@ components:
description: The value to search for in the column. description: The value to search for in the column.
fuzzy: fuzzy:
type: object type: object
description: A fuzzy search, only supported by internal tables. description: Searches for a sub-string within a string column, e.g. searching
for 'dib' will match 'Budibase'.
range: range:
type: object type: object
description: Searches within a range, the format of this must be in the format description: Searches within a range, the format of this must be in the format
@ -1524,6 +1528,32 @@ components:
description: Searches for rows which have a column value that is any of the description: Searches for rows which have a column value that is any of the
specified values. The format of this must be columnName -> specified values. The format of this must be columnName ->
[value1, value2]. [value1, value2].
contains:
type: object
description: Searches for a value, or set of values in array column types (such
as a multi-select). If an array of search options is provided
then it must match all.
example:
arrayColumn:
- a
- b
notContains:
type: object
description: The logical inverse of contains. Only works on array column types.
If an array of values is passed, the row must not match any of
them to be returned in the response.
example:
arrayColumn:
- a
- b
containsAny:
type: object
description: As with the contains search, only works on array column types and
searches for any of the provided values when given an array.
example:
arrayColumn:
- a
- b
paginate: paginate:
type: boolean type: boolean
description: Enables pagination, by default this is disabled. description: Enables pagination, by default this is disabled.

View File

@ -27,7 +27,8 @@ export default new Resource().setSchemas({
}, },
fuzzy: { fuzzy: {
type: "object", type: "object",
description: "A fuzzy search, only supported by internal tables.", description:
"Searches for a sub-string within a string column, e.g. searching for 'dib' will match 'Budibase'.",
}, },
range: { range: {
type: "object", type: "object",
@ -67,6 +68,30 @@ export default new Resource().setSchemas({
description: description:
"Searches for rows which have a column value that is any of the specified values. The format of this must be columnName -> [value1, value2].", "Searches for rows which have a column value that is any of the specified values. The format of this must be columnName -> [value1, value2].",
}, },
contains: {
type: "object",
description:
"Searches for a value, or set of values in array column types (such as a multi-select). If an array of search options is provided then it must match all.",
example: {
arrayColumn: ["a", "b"],
},
},
notContains: {
type: "object",
description:
"The logical inverse of contains. Only works on array column types. If an array of values is passed, the row must not match any of them to be returned in the response.",
example: {
arrayColumn: ["a", "b"],
},
},
containsAny: {
type: "object",
description:
"As with the contains search, only works on array column types and searches for any of the provided values when given an array.",
example: {
arrayColumn: ["a", "b"],
},
},
}, },
}, },
paginate: { paginate: {

View File

@ -17,11 +17,9 @@ import {
Row, Row,
Table, Table,
UserCtx, UserCtx,
EmptyFilterOption,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import * as utils from "./utils" import * as utils from "./utils"
import { dataFilters } from "@budibase/shared-core"
import { import {
inputProcessing, inputProcessing,
outputProcessing, outputProcessing,
@ -33,17 +31,6 @@ export async function handleRequest<T extends Operation>(
tableId: string, tableId: string,
opts?: RunConfig opts?: RunConfig
): Promise<ExternalRequestReturnType<T>> { ): Promise<ExternalRequestReturnType<T>> {
// make sure the filters are cleaned up, no empty strings for equals, fuzzy or string
if (opts && opts.filters) {
opts.filters = sdk.rows.removeEmptyFilters(opts.filters)
}
if (
!dataFilters.hasFilters(opts?.filters) &&
opts?.filters?.onEmptyFilter === EmptyFilterOption.RETURN_NONE
) {
return [] as any
}
return new ExternalRequest<T>(operation, tableId, opts?.datasource).run( return new ExternalRequest<T>(operation, tableId, opts?.datasource).run(
opts || {} opts || {}
) )

View File

@ -6,6 +6,7 @@ import {
Datasource, Datasource,
EmptyFilterOption, EmptyFilterOption,
FieldType, FieldType,
Row,
SearchFilters, SearchFilters,
Table, Table,
} from "@budibase/types" } from "@budibase/types"
@ -47,7 +48,7 @@ describe.each([
}) })
describe("strings", () => { describe("strings", () => {
beforeEach(async () => { beforeAll(async () => {
table = await config.api.table.save( table = await config.api.table.save(
tableForDatasource(datasource, { tableForDatasource(datasource, {
schema: { schema: {
@ -61,6 +62,13 @@ describe.each([
}) })
const rows = [{ name: "foo" }, { name: "bar" }] const rows = [{ name: "foo" }, { name: "bar" }]
let savedRows: Row[]
beforeAll(async () => {
savedRows = await Promise.all(
rows.map(r => config.api.row.save(table._id!, r))
)
})
interface StringSearchTest { interface StringSearchTest {
query: SearchFilters query: SearchFilters
@ -68,6 +76,8 @@ describe.each([
} }
const stringSearchTests: StringSearchTest[] = [ const stringSearchTests: StringSearchTest[] = [
// These three test cases are generic and don't really need
// to be repeated for all data types, so we just do them here.
{ query: {}, expected: rows }, { query: {}, expected: rows },
{ {
query: { onEmptyFilter: EmptyFilterOption.RETURN_ALL }, query: { onEmptyFilter: EmptyFilterOption.RETURN_ALL },
@ -77,27 +87,23 @@ describe.each([
query: { onEmptyFilter: EmptyFilterOption.RETURN_NONE }, query: { onEmptyFilter: EmptyFilterOption.RETURN_NONE },
expected: [], expected: [],
}, },
// The rest of these tests are specific to strings.
{ query: { string: { name: "foo" } }, expected: [rows[0]] }, { query: { string: { name: "foo" } }, expected: [rows[0]] },
{ query: { string: { name: "none" } }, expected: [] }, { query: { string: { name: "none" } }, expected: [] },
{ query: { fuzzy: { name: "oo" } }, expected: [rows[0]] }, { query: { fuzzy: { name: "oo" } }, expected: [rows[0]] },
{ query: { equal: { name: "foo" } }, expected: [rows[0]] }, { query: { equal: { name: "foo" } }, expected: [rows[0]] },
{ query: { notEqual: { name: "foo" } }, expected: [rows[1]] }, { query: { notEqual: { name: "foo" } }, expected: [rows[1]] },
{ query: { oneOf: { name: ["foo"] } }, expected: [rows[0]] }, { query: { oneOf: { name: ["foo"] } }, expected: [rows[0]] },
// { query: { contains: { name: "f" } }, expected: [0] },
// { query: { notContains: { name: ["f"] } }, expected: [1] },
// { query: { containsAny: { name: ["f"] } }, expected: [0] },
] ]
it.each(stringSearchTests)( it.each(stringSearchTests)(
`should be able to run query: $query`, `should be able to run query: $query`,
async ({ query, expected }) => { async ({ query, expected }) => {
const savedRows = await Promise.all(
rows.map(r => config.api.row.save(table._id!, r))
)
const { rows: foundRows } = await config.api.row.search(table._id!, { const { rows: foundRows } = await config.api.row.search(table._id!, {
tableId: table._id!, tableId: table._id!,
query, query,
}) })
expect(foundRows).toHaveLength(expected.length)
expect(foundRows).toEqual( expect(foundRows).toEqual(
expect.arrayContaining( expect.arrayContaining(
expected.map(r => expected.map(r =>
@ -110,7 +116,7 @@ describe.each([
}) })
describe("number", () => { describe("number", () => {
beforeEach(async () => { beforeAll(async () => {
table = await config.api.table.save( table = await config.api.table.save(
tableForDatasource(datasource, { tableForDatasource(datasource, {
schema: { schema: {
@ -124,6 +130,13 @@ describe.each([
}) })
const rows = [{ age: 1 }, { age: 10 }] const rows = [{ age: 1 }, { age: 10 }]
let savedRows: Row[]
beforeAll(async () => {
savedRows = await Promise.all(
rows.map(r => config.api.row.save(table._id!, r))
)
})
interface NumberSearchTest { interface NumberSearchTest {
query: SearchFilters query: SearchFilters
@ -131,15 +144,6 @@ describe.each([
} }
const numberSearchTests: NumberSearchTest[] = [ const numberSearchTests: NumberSearchTest[] = [
{ query: {}, expected: rows },
{
query: { onEmptyFilter: EmptyFilterOption.RETURN_ALL },
expected: rows,
},
{
query: { onEmptyFilter: EmptyFilterOption.RETURN_NONE },
expected: [],
},
{ query: { equal: { age: 1 } }, expected: [rows[0]] }, { query: { equal: { age: 1 } }, expected: [rows[0]] },
{ query: { equal: { age: 2 } }, expected: [] }, { query: { equal: { age: 2 } }, expected: [] },
{ query: { notEqual: { age: 1 } }, expected: [rows[1]] }, { query: { notEqual: { age: 1 } }, expected: [rows[1]] },
@ -153,13 +157,11 @@ describe.each([
it.each(numberSearchTests)( it.each(numberSearchTests)(
`should be able to run query: $query`, `should be able to run query: $query`,
async ({ query, expected }) => { async ({ query, expected }) => {
const savedRows = await Promise.all(
rows.map(r => config.api.row.save(table._id!, r))
)
const { rows: foundRows } = await config.api.row.search(table._id!, { const { rows: foundRows } = await config.api.row.search(table._id!, {
tableId: table._id!, tableId: table._id!,
query, query,
}) })
expect(foundRows).toHaveLength(expected.length)
expect(foundRows).toEqual( expect(foundRows).toEqual(
expect.arrayContaining( expect.arrayContaining(
expected.map(r => expected.map(r =>
@ -186,9 +188,16 @@ describe.each([
}) })
const rows = [ const rows = [
{ dob: new Date("2020-01-01") }, { dob: new Date("2020-01-01").toISOString() },
{ dob: new Date("2020-01-10") }, { dob: new Date("2020-01-10").toISOString() },
] ]
let savedRows: Row[]
beforeEach(async () => {
savedRows = await Promise.all(
rows.map(r => config.api.row.save(table._id!, r))
)
})
interface DateSearchTest { interface DateSearchTest {
query: SearchFilters query: SearchFilters
@ -196,26 +205,20 @@ describe.each([
} }
const dateSearchTests: DateSearchTest[] = [ const dateSearchTests: DateSearchTest[] = [
{ query: {}, expected: rows },
{ {
query: { onEmptyFilter: EmptyFilterOption.RETURN_ALL }, query: { equal: { dob: new Date("2020-01-01").toISOString() } },
expected: rows, expected: [rows[0]],
}, },
{ {
query: { onEmptyFilter: EmptyFilterOption.RETURN_NONE }, query: { equal: { dob: new Date("2020-01-02").toISOString() } },
expected: [], expected: [],
}, },
{ {
query: { equal: { dob: new Date("2020-01-01") } }, query: { notEqual: { dob: new Date("2020-01-01").toISOString() } },
expected: [rows[0]],
},
{ query: { equal: { dob: new Date("2020-01-02") } }, expected: [] },
{
query: { notEqual: { dob: new Date("2020-01-01") } },
expected: [rows[1]], expected: [rows[1]],
}, },
{ {
query: { oneOf: { dob: [new Date("2020-01-01")] } }, query: { oneOf: { dob: [new Date("2020-01-01").toISOString()] } },
expected: [rows[0]], expected: [rows[0]],
}, },
{ {
@ -256,23 +259,15 @@ describe.each([
it.each(dateSearchTests)( it.each(dateSearchTests)(
`should be able to run query: $query`, `should be able to run query: $query`,
async ({ query, expected }) => { async ({ query, expected }) => {
// TODO(samwho): most of these work for SQS, but not all. Fix 'em.
if (isSqs) {
return
}
const savedRows = await Promise.all(
rows.map(r => config.api.row.save(table._id!, r))
)
const { rows: foundRows } = await config.api.row.search(table._id!, { const { rows: foundRows } = await config.api.row.search(table._id!, {
tableId: table._id!, tableId: table._id!,
query, query,
}) })
expect(foundRows).toHaveLength(expected.length)
expect(foundRows).toEqual( expect(foundRows).toEqual(
expect.arrayContaining( expect.arrayContaining(
expected.map(r => expected.map(r =>
expect.objectContaining( expect.objectContaining(savedRows.find(sr => sr.dob === r.dob)!)
savedRows.find(sr => sr.dob === r.dob.toISOString())!
)
) )
) )
) )

View File

@ -273,6 +273,7 @@ export interface components {
| "array" | "array"
| "datetime" | "datetime"
| "attachment" | "attachment"
| "attachment_single"
| "link" | "link"
| "formula" | "formula"
| "auto" | "auto"
@ -381,6 +382,7 @@ export interface components {
| "array" | "array"
| "datetime" | "datetime"
| "attachment" | "attachment"
| "attachment_single"
| "link" | "link"
| "formula" | "formula"
| "auto" | "auto"
@ -491,6 +493,7 @@ export interface components {
| "array" | "array"
| "datetime" | "datetime"
| "attachment" | "attachment"
| "attachment_single"
| "link" | "link"
| "formula" | "formula"
| "auto" | "auto"
@ -693,7 +696,7 @@ export interface components {
* @example [object Object] * @example [object Object]
*/ */
string?: { [key: string]: string }; string?: { [key: string]: string };
/** @description A fuzzy search, only supported by internal tables. */ /** @description Searches for a sub-string within a string column, e.g. searching for 'dib' will match 'Budibase'. */
fuzzy?: { [key: string]: unknown }; fuzzy?: { [key: string]: unknown };
/** /**
* @description Searches within a range, the format of this must be in the format of an object with a "low" and "high" property. * @description Searches within a range, the format of this must be in the format of an object with a "low" and "high" property.
@ -713,6 +716,21 @@ export interface components {
notEmpty?: { [key: string]: unknown }; notEmpty?: { [key: string]: unknown };
/** @description Searches for rows which have a column value that is any of the specified values. The format of this must be columnName -> [value1, value2]. */ /** @description Searches for rows which have a column value that is any of the specified values. The format of this must be columnName -> [value1, value2]. */
oneOf?: { [key: string]: unknown }; oneOf?: { [key: string]: unknown };
/**
* @description Searches for a value, or set of values in array column types (such as a multi-select). If an array of search options is provided then it must match all.
* @example [object Object]
*/
contains?: { [key: string]: unknown };
/**
* @description The logical inverse of contains. Only works on array column types. If an array of values is passed, the row must not match any of them to be returned in the response.
* @example [object Object]
*/
notContains?: { [key: string]: unknown };
/**
* @description As with the contains search, only works on array column types and searches for any of the provided values when given an array.
* @example [object Object]
*/
containsAny?: { [key: string]: unknown };
}; };
/** @description Enables pagination, by default this is disabled. */ /** @description Enables pagination, by default this is disabled. */
paginate?: boolean; paginate?: boolean;

View File

@ -334,6 +334,7 @@ class InternalBuilder {
if (filters.containsAny) { if (filters.containsAny) {
contains(filters.containsAny, true) contains(filters.containsAny, true)
} }
return query return query
} }

View File

@ -1,4 +1,5 @@
import { import {
EmptyFilterOption,
Row, Row,
RowSearchParams, RowSearchParams,
SearchFilters, SearchFilters,
@ -11,6 +12,7 @@ import { NoEmptyFilterStrings } from "../../../constants"
import * as sqs from "./search/sqs" import * as sqs from "./search/sqs"
import env from "../../../environment" import env from "../../../environment"
import { ExportRowsParams, ExportRowsResult } from "./search/types" import { ExportRowsParams, ExportRowsResult } from "./search/types"
import { dataFilters } from "@budibase/shared-core"
export { isValidFilter } from "../../../integrations/utils" export { isValidFilter } from "../../../integrations/utils"
@ -60,6 +62,16 @@ 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 || {})
if (
!dataFilters.hasFilters(options.query) &&
options.query.onEmptyFilter === EmptyFilterOption.RETURN_NONE
) {
return {
rows: [],
}
}
if (isExternalTable) { if (isExternalTable) {
return external.search(options) return external.search(options)
} else if (env.SQS_SEARCH_ENABLE) { } else if (env.SQS_SEARCH_ENABLE) {

View File

@ -156,21 +156,21 @@ export async function search(
try { try {
const query = builder._query(request, { const query = builder._query(request, {
disableReturning: true, disableReturning: true,
disableBindings: true,
}) })
if (Array.isArray(query)) { if (Array.isArray(query)) {
throw new Error("SQS cannot currently handle multiple queries") throw new Error("SQS cannot currently handle multiple queries")
} }
let sql = query.sql let sql = query.sql,
bindings = query.bindings
// quick hack for docIds // quick hack for docIds
sql = sql.replace(/`doc1`.`rowId`/g, "`doc1.rowId`") sql = sql.replace(/`doc1`.`rowId`/g, "`doc1.rowId`")
sql = sql.replace(/`doc2`.`rowId`/g, "`doc2.rowId`") sql = sql.replace(/`doc2`.`rowId`/g, "`doc2.rowId`")
const db = context.getAppDB() const db = context.getAppDB()
const rows = await db.sql<Row>(sql) const rows = await db.sql<Row>(sql, bindings)
return { return {
rows: await sqlOutputProcessing( rows: await sqlOutputProcessing(

View File

@ -4,6 +4,7 @@ import {
AnyDocument, AnyDocument,
Document, Document,
RowValue, RowValue,
SqlQueryBinding,
ViewTemplateOpts, ViewTemplateOpts,
} from "../" } from "../"
import { Writable } from "stream" import { Writable } from "stream"
@ -143,7 +144,10 @@ export interface Database {
opts?: DatabasePutOpts opts?: DatabasePutOpts
): Promise<Nano.DocumentInsertResponse> ): Promise<Nano.DocumentInsertResponse>
bulkDocs(documents: AnyDocument[]): Promise<Nano.DocumentBulkResponse[]> bulkDocs(documents: AnyDocument[]): Promise<Nano.DocumentBulkResponse[]>
sql<T extends Document>(sql: string): Promise<T[]> sql<T extends Document>(
sql: string,
parameters?: SqlQueryBinding
): Promise<T[]>
allDocs<T extends Document | RowValue>( allDocs<T extends Document | RowValue>(
params: DatabaseQueryOpts params: DatabaseQueryOpts
): Promise<AllDocsResponse<T>> ): Promise<AllDocsResponse<T>>