Merge branch 'master' of github.com:Budibase/budibase into v3-ui

This commit is contained in:
Andrew Kingston 2024-10-17 14:36:34 +01:00
commit b57c8cc50d
No known key found for this signature in database
9 changed files with 230 additions and 190 deletions

View File

@ -1,6 +1,6 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "2.33.1", "version": "2.33.2",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*", "packages/*",

View File

@ -23,12 +23,14 @@ import {
InternalSearchFilterOperator, InternalSearchFilterOperator,
JsonFieldMetadata, JsonFieldMetadata,
JsonTypes, JsonTypes,
LogicalOperator,
Operation, Operation,
prefixed, prefixed,
QueryJson, QueryJson,
QueryOptions, QueryOptions,
RangeOperator, RangeOperator,
RelationshipsJson, RelationshipsJson,
SearchFilterKey,
SearchFilters, SearchFilters,
SortOrder, SortOrder,
SqlClient, SqlClient,
@ -96,6 +98,22 @@ function isSqs(table: Table): boolean {
) )
} }
const allowEmptyRelationships: Record<SearchFilterKey, boolean> = {
[BasicOperator.EQUAL]: false,
[BasicOperator.NOT_EQUAL]: true,
[BasicOperator.EMPTY]: false,
[BasicOperator.NOT_EMPTY]: true,
[BasicOperator.FUZZY]: false,
[BasicOperator.STRING]: false,
[RangeOperator.RANGE]: false,
[ArrayOperator.CONTAINS]: false,
[ArrayOperator.NOT_CONTAINS]: true,
[ArrayOperator.CONTAINS_ANY]: false,
[ArrayOperator.ONE_OF]: false,
[LogicalOperator.AND]: false,
[LogicalOperator.OR]: false,
}
class InternalBuilder { class InternalBuilder {
private readonly client: SqlClient private readonly client: SqlClient
private readonly query: QueryJson private readonly query: QueryJson
@ -405,31 +423,48 @@ class InternalBuilder {
addRelationshipForFilter( addRelationshipForFilter(
query: Knex.QueryBuilder, query: Knex.QueryBuilder,
allowEmptyRelationships: boolean,
filterKey: string, filterKey: string,
whereCb: (query: Knex.QueryBuilder) => Knex.QueryBuilder whereCb: (filterKey: string, query: Knex.QueryBuilder) => Knex.QueryBuilder
): Knex.QueryBuilder { ): Knex.QueryBuilder {
const mainKnex = this.knex const mainKnex = this.knex
const { relationships, endpoint, tableAliases: aliases } = this.query const { relationships, endpoint, tableAliases: aliases } = this.query
const tableName = endpoint.entityId const tableName = endpoint.entityId
const fromAlias = aliases?.[tableName] || tableName const fromAlias = aliases?.[tableName] || tableName
const matches = (possibleTable: string) => const matches = (value: string) =>
filterKey.startsWith(`${possibleTable}`) filterKey.match(new RegExp(`^${value}\\.`))
if (!relationships) { if (!relationships) {
return query return query
} }
for (const relationship of relationships) { for (const relationship of relationships) {
const relatedTableName = relationship.tableName const relatedTableName = relationship.tableName
const toAlias = aliases?.[relatedTableName] || relatedTableName const toAlias = aliases?.[relatedTableName] || relatedTableName
const matchesTableName = matches(relatedTableName) || matches(toAlias)
const matchesRelationName = matches(relationship.column)
// this is the relationship which is being filtered // this is the relationship which is being filtered
if ( if (
(matches(relatedTableName) || matches(toAlias)) && (matchesTableName || matchesRelationName) &&
relationship.to && relationship.to &&
relationship.tableName relationship.tableName
) { ) {
let subQuery = mainKnex const joinTable = mainKnex
.select(mainKnex.raw(1)) .select(mainKnex.raw(1))
.from({ [toAlias]: relatedTableName }) .from({ [toAlias]: relatedTableName })
let subQuery = joinTable.clone()
const manyToMany = validateManyToMany(relationship) const manyToMany = validateManyToMany(relationship)
let updatedKey
if (!matchesTableName) {
updatedKey = filterKey.replace(
new RegExp(`^${relationship.column}.`),
`${aliases![relationship.tableName]}.`
)
} else {
updatedKey = filterKey
}
if (manyToMany) { if (manyToMany) {
const throughAlias = const throughAlias =
aliases?.[manyToMany.through] || relationship.through aliases?.[manyToMany.through] || relationship.through
@ -440,7 +475,6 @@ class InternalBuilder {
subQuery = subQuery subQuery = subQuery
// add a join through the junction table // add a join through the junction table
.innerJoin(throughTable, function () { .innerJoin(throughTable, function () {
// @ts-ignore
this.on( this.on(
`${toAlias}.${manyToMany.toPrimary}`, `${toAlias}.${manyToMany.toPrimary}`,
"=", "=",
@ -460,18 +494,38 @@ class InternalBuilder {
if (this.client === SqlClient.SQL_LITE) { if (this.client === SqlClient.SQL_LITE) {
subQuery = this.addJoinFieldCheck(subQuery, manyToMany) subQuery = this.addJoinFieldCheck(subQuery, manyToMany)
} }
query = query.where(q => {
q.whereExists(whereCb(updatedKey, subQuery))
if (allowEmptyRelationships) {
q.orWhereNotExists(
joinTable.clone().innerJoin(throughTable, function () {
this.on(
`${fromAlias}.${manyToMany.fromPrimary}`,
"=",
`${throughAlias}.${manyToMany.from}`
)
})
)
}
})
} else { } else {
const toKey = `${toAlias}.${relationship.to}`
const foreignKey = `${fromAlias}.${relationship.from}`
// "join" to the main table, making sure the ID matches that of the main // "join" to the main table, making sure the ID matches that of the main
subQuery = subQuery.where( subQuery = subQuery.where(
`${toAlias}.${relationship.to}`, toKey,
"=", "=",
mainKnex.raw( mainKnex.raw(this.quotedIdentifier(foreignKey))
this.quotedIdentifier(`${fromAlias}.${relationship.from}`)
)
) )
query = query.where(q => {
q.whereExists(whereCb(updatedKey, subQuery.clone()))
if (allowEmptyRelationships) {
q.orWhereNotExists(subQuery)
}
})
} }
query = query.whereExists(whereCb(subQuery))
break
} }
} }
return query return query
@ -502,6 +556,7 @@ class InternalBuilder {
} }
function iterate( function iterate(
structure: AnySearchFilter, structure: AnySearchFilter,
operation: SearchFilterKey,
fn: ( fn: (
query: Knex.QueryBuilder, query: Knex.QueryBuilder,
key: string, key: string,
@ -558,9 +613,14 @@ class InternalBuilder {
if (allOr) { if (allOr) {
query = query.or query = query.or
} }
query = builder.addRelationshipForFilter(query, updatedKey, q => { query = builder.addRelationshipForFilter(
return handleRelationship(q, updatedKey, value) query,
}) allowEmptyRelationships[operation],
updatedKey,
(updatedKey, q) => {
return handleRelationship(q, updatedKey, value)
}
)
} }
} }
} }
@ -592,7 +652,7 @@ class InternalBuilder {
return `[${value.join(",")}]` return `[${value.join(",")}]`
} }
if (this.client === SqlClient.POSTGRES) { if (this.client === SqlClient.POSTGRES) {
iterate(mode, (q, key, value) => { iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
const wrap = any ? "" : "'" const wrap = any ? "" : "'"
const op = any ? "\\?| array" : "@>" const op = any ? "\\?| array" : "@>"
const fieldNames = key.split(/\./g) const fieldNames = key.split(/\./g)
@ -610,7 +670,7 @@ class InternalBuilder {
this.client === SqlClient.MARIADB this.client === SqlClient.MARIADB
) { ) {
const jsonFnc = any ? "JSON_OVERLAPS" : "JSON_CONTAINS" const jsonFnc = any ? "JSON_OVERLAPS" : "JSON_CONTAINS"
iterate(mode, (q, key, value) => { iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
return q[rawFnc]( return q[rawFnc](
`${not}COALESCE(${jsonFnc}(${key}, '${stringifyArray( `${not}COALESCE(${jsonFnc}(${key}, '${stringifyArray(
value value
@ -619,7 +679,7 @@ class InternalBuilder {
}) })
} else { } else {
const andOr = mode === filters?.containsAny ? " OR " : " AND " const andOr = mode === filters?.containsAny ? " OR " : " AND "
iterate(mode, (q, key, value) => { iterate(mode, ArrayOperator.CONTAINS, (q, key, value) => {
let statement = "" let statement = ""
const identifier = this.quotedIdentifier(key) const identifier = this.quotedIdentifier(key)
for (let i in value) { for (let i in value) {
@ -673,6 +733,7 @@ class InternalBuilder {
const fnc = allOr ? "orWhereIn" : "whereIn" const fnc = allOr ? "orWhereIn" : "whereIn"
iterate( iterate(
filters.oneOf, filters.oneOf,
ArrayOperator.ONE_OF,
(q, key: string, array) => { (q, key: string, array) => {
if (this.client === SqlClient.ORACLE) { if (this.client === SqlClient.ORACLE) {
key = this.convertClobs(key) key = this.convertClobs(key)
@ -697,7 +758,7 @@ class InternalBuilder {
) )
} }
if (filters.string) { if (filters.string) {
iterate(filters.string, (q, key, value) => { iterate(filters.string, BasicOperator.STRING, (q, key, value) => {
const fnc = allOr ? "orWhere" : "where" const fnc = allOr ? "orWhere" : "where"
// postgres supports ilike, nothing else does // postgres supports ilike, nothing else does
if (this.client === SqlClient.POSTGRES) { if (this.client === SqlClient.POSTGRES) {
@ -712,10 +773,10 @@ class InternalBuilder {
}) })
} }
if (filters.fuzzy) { if (filters.fuzzy) {
iterate(filters.fuzzy, like) iterate(filters.fuzzy, BasicOperator.FUZZY, like)
} }
if (filters.range) { if (filters.range) {
iterate(filters.range, (q, key, value) => { iterate(filters.range, RangeOperator.RANGE, (q, key, value) => {
const isEmptyObject = (val: any) => { const isEmptyObject = (val: any) => {
return ( return (
val && val &&
@ -781,7 +842,7 @@ class InternalBuilder {
}) })
} }
if (filters.equal) { if (filters.equal) {
iterate(filters.equal, (q, key, value) => { iterate(filters.equal, BasicOperator.EQUAL, (q, key, value) => {
const fnc = allOr ? "orWhereRaw" : "whereRaw" const fnc = allOr ? "orWhereRaw" : "whereRaw"
if (this.client === SqlClient.MS_SQL) { if (this.client === SqlClient.MS_SQL) {
return q[fnc]( return q[fnc](
@ -801,7 +862,7 @@ class InternalBuilder {
}) })
} }
if (filters.notEqual) { if (filters.notEqual) {
iterate(filters.notEqual, (q, key, value) => { iterate(filters.notEqual, BasicOperator.NOT_EQUAL, (q, key, value) => {
const fnc = allOr ? "orWhereRaw" : "whereRaw" const fnc = allOr ? "orWhereRaw" : "whereRaw"
if (this.client === SqlClient.MS_SQL) { if (this.client === SqlClient.MS_SQL) {
return q[fnc]( return q[fnc](
@ -822,13 +883,13 @@ class InternalBuilder {
}) })
} }
if (filters.empty) { if (filters.empty) {
iterate(filters.empty, (q, key) => { iterate(filters.empty, BasicOperator.EMPTY, (q, key) => {
const fnc = allOr ? "orWhereNull" : "whereNull" const fnc = allOr ? "orWhereNull" : "whereNull"
return q[fnc](key) return q[fnc](key)
}) })
} }
if (filters.notEmpty) { if (filters.notEmpty) {
iterate(filters.notEmpty, (q, key) => { iterate(filters.notEmpty, BasicOperator.NOT_EMPTY, (q, key) => {
const fnc = allOr ? "orWhereNotNull" : "whereNotNull" const fnc = allOr ? "orWhereNotNull" : "whereNotNull"
return q[fnc](key) return q[fnc](key)
}) })
@ -1224,12 +1285,10 @@ class InternalBuilder {
}) })
: undefined : undefined
if (!throughTable) { if (!throughTable) {
// @ts-ignore
query = query.leftJoin(toTableWithSchema, function () { query = query.leftJoin(toTableWithSchema, function () {
for (let relationship of columns) { for (let relationship of columns) {
const from = relationship.from, const from = relationship.from,
to = relationship.to to = relationship.to
// @ts-ignore
this.orOn(`${fromAlias}.${from}`, "=", `${toAlias}.${to}`) this.orOn(`${fromAlias}.${from}`, "=", `${toAlias}.${to}`)
} }
}) })
@ -1240,7 +1299,6 @@ class InternalBuilder {
for (let relationship of columns) { for (let relationship of columns) {
const fromPrimary = relationship.fromPrimary const fromPrimary = relationship.fromPrimary
const from = relationship.from const from = relationship.from
// @ts-ignore
this.orOn( this.orOn(
`${fromAlias}.${fromPrimary}`, `${fromAlias}.${fromPrimary}`,
"=", "=",
@ -1252,7 +1310,6 @@ class InternalBuilder {
for (let relationship of columns) { for (let relationship of columns) {
const toPrimary = relationship.toPrimary const toPrimary = relationship.toPrimary
const to = relationship.to const to = relationship.to
// @ts-ignore
this.orOn(`${toAlias}.${toPrimary}`, `${throughAlias}.${to}`) this.orOn(`${toAlias}.${toPrimary}`, `${throughAlias}.${to}`)
} }
}) })

View File

@ -205,18 +205,6 @@ export class ExternalRequest<T extends Operation> {
filters: SearchFilters, filters: SearchFilters,
table: Table table: Table
): SearchFilters { ): SearchFilters {
// replace any relationship columns initially, table names and relationship column names are acceptable
const relationshipColumns = sdk.rows.filters.getRelationshipColumns(table)
filters = sdk.rows.filters.updateFilterKeys(
filters,
relationshipColumns.map(({ name, definition }) => {
const { tableName } = breakExternalTableId(definition.tableId)
return {
original: name,
updated: tableName,
}
})
)
const primary = table.primary const primary = table.primary
// if passed in array need to copy for shifting etc // if passed in array need to copy for shifting etc
let idCopy: undefined | string | any[] = cloneDeep(id) let idCopy: undefined | string | any[] = cloneDeep(id)

View File

@ -15,13 +15,16 @@ import {
ExportRowsResponse, ExportRowsResponse,
FieldType, FieldType,
GetRowResponse, GetRowResponse,
isRelationshipField,
PatchRowRequest, PatchRowRequest,
PatchRowResponse, PatchRowResponse,
Row, Row,
RowAttachment, RowAttachment,
RowSearchParams, RowSearchParams,
SearchFilters,
SearchRowRequest, SearchRowRequest,
SearchRowResponse, SearchRowResponse,
Table,
UserCtx, UserCtx,
ValidateResponse, ValidateResponse,
} from "@budibase/types" } from "@budibase/types"
@ -33,6 +36,7 @@ import sdk from "../../../sdk"
import * as exporters from "../view/exporters" import * as exporters from "../view/exporters"
import { Format } from "../view/exporters" import { Format } from "../view/exporters"
import { apiFileReturn } from "../../../utilities/fileSystem" import { apiFileReturn } from "../../../utilities/fileSystem"
import { dataFilters } from "@budibase/shared-core"
export * as views from "./views" export * as views from "./views"
@ -211,12 +215,15 @@ export async function search(ctx: Ctx<SearchRowRequest, SearchRowResponse>) {
await context.ensureSnippetContext(true) await context.ensureSnippetContext(true)
const enrichedQuery = await utils.enrichSearchContext( let { query } = ctx.request.body
{ ...ctx.request.body.query }, if (query) {
{ const allTables = await sdk.tables.getAllTables()
user: sdk.users.getUserContextBindings(ctx.user), query = replaceTableNamesInFilters(tableId, query, allTables)
} }
)
let enrichedQuery: SearchFilters = await utils.enrichSearchContext(query, {
user: sdk.users.getUserContextBindings(ctx.user),
})
const searchParams: RowSearchParams = { const searchParams: RowSearchParams = {
...ctx.request.body, ...ctx.request.body,
@ -229,6 +236,47 @@ export async function search(ctx: Ctx<SearchRowRequest, SearchRowResponse>) {
ctx.body = await sdk.rows.search(searchParams) ctx.body = await sdk.rows.search(searchParams)
} }
function replaceTableNamesInFilters(
tableId: string,
filters: SearchFilters,
allTables: Table[]
): SearchFilters {
for (const filter of Object.values(filters)) {
for (const key of Object.keys(filter)) {
const matches = key.match(`^(?<relation>.+)\\.(?<field>.+)`)
const relation = matches?.groups?.["relation"]
const field = matches?.groups?.["field"]
if (!relation || !field) {
continue
}
const table = allTables.find(r => r._id === tableId)!
if (Object.values(table.schema).some(f => f.name === relation)) {
continue
}
const matchedTable = allTables.find(t => t.name === relation)
const relationship = Object.values(table.schema).find(
f => isRelationshipField(f) && f.tableId === matchedTable?._id
)
if (!relationship) {
continue
}
const updatedField = `${relationship.name}.${field}`
if (updatedField && updatedField !== key) {
filter[updatedField] = filter[key]
delete filter[key]
}
}
}
return dataFilters.recurseLogicalOperators(filters, (f: SearchFilters) => {
return replaceTableNamesInFilters(tableId, f, allTables)
})
}
export async function validate(ctx: Ctx<Row, ValidateResponse>) { export async function validate(ctx: Ctx<Row, ValidateResponse>) {
const source = await utils.getSource(ctx) const source = await utils.getSource(ctx)
const table = await utils.getTableFromSource(source) const table = await utils.getTableFromSource(source)

View File

@ -2364,12 +2364,16 @@ describe.each([
// It also can't work for in-memory searching because the related table name // It also can't work for in-memory searching because the related table name
// isn't available. // isn't available.
!isInMemory && !isInMemory &&
describe("relations", () => { describe.each([
RelationshipType.ONE_TO_MANY,
RelationshipType.MANY_TO_ONE,
RelationshipType.MANY_TO_MANY,
])("relations (%s)", relationshipType => {
let productCategoryTable: Table, productCatRows: Row[] let productCategoryTable: Table, productCatRows: Row[]
beforeAll(async () => { beforeAll(async () => {
const { relatedTable, tableId } = await basicRelationshipTables( const { relatedTable, tableId } = await basicRelationshipTables(
RelationshipType.ONE_TO_MANY relationshipType
) )
tableOrViewId = tableId tableOrViewId = tableId
productCategoryTable = relatedTable productCategoryTable = relatedTable
@ -2466,7 +2470,10 @@ describe.each([
], ],
}, },
}).toContainExactly([ }).toContainExactly([
{ name: "foo", productCat: [{ _id: productCatRows[0]._id }] }, {
name: "foo",
productCat: [{ _id: productCatRows[0]._id }],
},
]) ])
} }
) )
@ -2544,7 +2551,7 @@ describe.each([
}).toContainExactly([ }).toContainExactly([
{ name: "foo", productCat: [{ _id: productCatRows[0]._id }] }, { name: "foo", productCat: [{ _id: productCatRows[0]._id }] },
{ name: "bar", productCat: [{ _id: productCatRows[1]._id }] }, { name: "bar", productCat: [{ _id: productCatRows[1]._id }] },
// { name: "baz", productCat: undefined }, // TODO { name: "baz", productCat: undefined },
]) ])
}) })
@ -2566,7 +2573,10 @@ describe.each([
], ],
}, },
}).toContainExactly([ }).toContainExactly([
{ name: "foo", productCat: [{ _id: productCatRows[0]._id }] }, {
name: "foo",
productCat: [{ _id: productCatRows[0]._id }],
},
]) ])
} }
) )
@ -2590,9 +2600,15 @@ describe.each([
], ],
}, },
}).toContainExactly([ }).toContainExactly([
{ name: "foo", productCat: [{ _id: productCatRows[0]._id }] }, {
{ name: "bar", productCat: [{ _id: productCatRows[1]._id }] }, name: "foo",
// { name: "baz", productCat: undefined }, // TODO productCat: [{ _id: productCatRows[0]._id }],
},
{
name: "bar",
productCat: [{ _id: productCatRows[1]._id }],
},
{ name: "baz", productCat: undefined },
]) ])
} }
) )
@ -2616,7 +2632,7 @@ describe.each([
}).toContainExactly([ }).toContainExactly([
{ name: "foo", productCat: [{ _id: productCatRows[0]._id }] }, { name: "foo", productCat: [{ _id: productCatRows[0]._id }] },
{ name: "bar", productCat: [{ _id: productCatRows[1]._id }] }, { name: "bar", productCat: [{ _id: productCatRows[1]._id }] },
// { name: "baz", productCat: undefined }, // TODO { name: "baz", productCat: undefined },
]) ])
}) })
}) })
@ -2624,10 +2640,13 @@ describe.each([
}) })
isSql && isSql &&
describe("big relations", () => { describe.each([
RelationshipType.MANY_TO_ONE,
RelationshipType.MANY_TO_MANY,
])("big relations (%s)", relationshipType => {
beforeAll(async () => { beforeAll(async () => {
const { relatedTable, tableId } = await basicRelationshipTables( const { relatedTable, tableId } = await basicRelationshipTables(
RelationshipType.MANY_TO_ONE relationshipType
) )
tableOrViewId = tableId tableOrViewId = tableId
const mainRow = await config.api.row.save(tableOrViewId, { const mainRow = await config.api.row.save(tableOrViewId, {
@ -2653,7 +2672,8 @@ describe.each([
expect(response.rows[0].productCat).toBeArrayOfSize(11) expect(response.rows[0].productCat).toBeArrayOfSize(11)
}) })
}) })
;(isSqs || isLucene) &&
isSql &&
describe("relations to same table", () => { describe("relations to same table", () => {
let relatedTable: string, relatedRows: Row[] let relatedTable: string, relatedRows: Row[]
@ -2695,6 +2715,11 @@ describe.each([
related1: [relatedRows[2]._id!], related1: [relatedRows[2]._id!],
related2: [relatedRows[3]._id!], related2: [relatedRows[3]._id!],
}), }),
config.api.row.save(tableOrViewId, {
name: "test3",
related1: [relatedRows[1]._id],
related2: [relatedRows[2]._id!],
}),
]) ])
}) })
@ -2712,42 +2737,59 @@ describe.each([
related1: [{ _id: relatedRows[2]._id }], related1: [{ _id: relatedRows[2]._id }],
related2: [{ _id: relatedRows[3]._id }], related2: [{ _id: relatedRows[3]._id }],
}, },
{
name: "test3",
related1: [{ _id: relatedRows[1]._id }],
related2: [{ _id: relatedRows[2]._id }],
},
]) ])
}) })
isSqs && it("should be able to filter via the first relation field with equal", async () => {
it("should be able to filter down to second row with equal", async () => { await expectSearch({
await expectSearch({ query: {
query: { equal: {
equal: { ["related1.name"]: "baz",
["related1.name"]: "baz",
},
}, },
}).toContainExactly([ },
{ }).toContainExactly([
name: "test2", {
related1: [{ _id: relatedRows[2]._id }], name: "test2",
}, related1: [{ _id: relatedRows[2]._id }],
]) },
}) ])
})
isSqs && it("should be able to filter via the second relation field with not equal", async () => {
it("should be able to filter down to first row with not equal", async () => { await expectSearch({
await expectSearch({ query: {
query: { notEqual: {
notEqual: { ["1:related2.name"]: "foo",
["1:related2.name"]: "bar", ["2:related2.name"]: "baz",
["2:related2.name"]: "baz", ["3:related2.name"]: "boo",
["3:related2.name"]: "boo",
},
}, },
}).toContainExactly([ },
{ }).toContainExactly([
name: "test", {
related1: [{ _id: relatedRows[0]._id }], name: "test",
},
])
})
it("should be able to filter on both fields", async () => {
await expectSearch({
query: {
notEqual: {
["related1.name"]: "foo",
["related2.name"]: "baz",
}, },
]) },
}) }).toContainExactly([
{
name: "test2",
},
])
})
}) })
isInternal && isInternal &&

View File

@ -78,8 +78,7 @@ describe("Captures of real examples", () => {
bindings: ["assembling", primaryLimit, relationshipLimit], bindings: ["assembling", primaryLimit, relationshipLimit],
sql: expect.stringContaining( sql: expect.stringContaining(
multiline( multiline(
`where exists (select 1 from "tasks" as "b" inner join "products_tasks" as "c" on "b"."taskid" = "c"."taskid" where "c"."productid" = "a"."productid" `where (exists (select 1 from "tasks" as "b" inner join "products_tasks" as "c" on "b"."taskid" = "c"."taskid" where "c"."productid" = "a"."productid" and (COALESCE("b"."taskname" = $1, FALSE)))`
and (COALESCE("b"."taskname" = $1, FALSE))`
) )
), ),
}) })
@ -133,6 +132,8 @@ describe("Captures of real examples", () => {
expect(query).toEqual({ expect(query).toEqual({
bindings: [ bindings: [
rangeValue.low,
rangeValue.high,
rangeValue.low, rangeValue.low,
rangeValue.high, rangeValue.high,
equalValue, equalValue,
@ -144,7 +145,7 @@ describe("Captures of real examples", () => {
], ],
sql: expect.stringContaining( sql: expect.stringContaining(
multiline( multiline(
`where exists (select 1 from "persons" as "c" where "c"."personid" = "a"."executorid" and ("c"."year" between $1 and $2))` `where (exists (select 1 from "persons" as "c" where "c"."personid" = "a"."executorid" and ("c"."year" between $1 and $2))) and (exists (select 1 from "persons" as "c" where "c"."personid" = "a"."qaid" and ("c"."year" between $3 and $4))) and (exists (select 1 from "products" as "b" inner join "products_tasks" as "d" on "b"."productid" = "d"."productid" where "d"."taskid" = "a"."taskid" and (COALESCE("b"."productname" = $5, FALSE))))`
) )
), ),
}) })

View File

@ -3,14 +3,12 @@ import * as rows from "./rows"
import * as search from "./search" import * as search from "./search"
import * as utils from "./utils" import * as utils from "./utils"
import * as external from "./external" import * as external from "./external"
import * as filters from "./search/filters"
import AliasTables from "./sqlAlias" import AliasTables from "./sqlAlias"
export default { export default {
...attachments, ...attachments,
...rows, ...rows,
...search, ...search,
filters,
utils, utils,
external, external,
AliasTables, AliasTables,

View File

@ -1,65 +0,0 @@
import {
FieldType,
RelationshipFieldMetadata,
SearchFilters,
Table,
} from "@budibase/types"
import { isPlainObject } from "lodash"
import { dataFilters } from "@budibase/shared-core"
export function getRelationshipColumns(table: Table): {
name: string
definition: RelationshipFieldMetadata
}[] {
// performing this with a for loop rather than an array filter improves
// type guarding, as no casts are required
const linkEntries: [string, RelationshipFieldMetadata][] = []
for (let entry of Object.entries(table.schema)) {
if (entry[1].type === FieldType.LINK) {
const linkColumn: RelationshipFieldMetadata = entry[1]
linkEntries.push([entry[0], linkColumn])
}
}
return linkEntries.map(entry => ({
name: entry[0],
definition: entry[1],
}))
}
export function getTableIDList(
tables: Table[]
): { name: string; id: string }[] {
return tables
.filter(table => table.originalName && table._id)
.map(table => ({ id: table._id!, name: table.originalName! }))
}
export function updateFilterKeys(
filters: SearchFilters,
updates: { original: string; updated: string }[]
): SearchFilters {
const makeFilterKeyRegex = (str: string) =>
new RegExp(`^${str}\\.|:${str}\\.`)
for (let filter of Object.values(filters)) {
if (!isPlainObject(filter)) {
continue
}
for (let [key, keyFilter] of Object.entries(filter)) {
if (keyFilter === "") {
delete filter[key]
}
const possibleKey = updates.find(({ original }) =>
key.match(makeFilterKeyRegex(original))
)
if (possibleKey && possibleKey.original !== possibleKey.updated) {
// only replace the first, not replaceAll
filter[key.replace(possibleKey.original, possibleKey.updated)] =
filter[key]
delete filter[key]
}
}
}
return dataFilters.recurseLogicalOperators(filters, (f: SearchFilters) => {
return updateFilterKeys(f, updates)
})
}

View File

@ -39,11 +39,6 @@ import AliasTables from "../../sqlAlias"
import { outputProcessing } from "../../../../../utilities/rowProcessor" import { outputProcessing } from "../../../../../utilities/rowProcessor"
import pick from "lodash/pick" import pick from "lodash/pick"
import { processRowCountResponse } from "../../utils" import { processRowCountResponse } from "../../utils"
import {
getRelationshipColumns,
getTableIDList,
updateFilterKeys,
} from "../filters"
import { import {
dataFilters, dataFilters,
helpers, helpers,
@ -133,31 +128,7 @@ async function buildInternalFieldList(
return [...new Set(fieldList)] return [...new Set(fieldList)]
} }
function cleanupFilters( function cleanupFilters(filters: SearchFilters, allTables: Table[]) {
filters: SearchFilters,
table: Table,
allTables: Table[]
) {
// get a list of all relationship columns in the table for updating
const relationshipColumns = getRelationshipColumns(table)
// get table names to ID map for relationships
const tableNameToID = getTableIDList(allTables)
// all should be applied at once
filters = updateFilterKeys(
filters,
relationshipColumns
.map(({ name, definition }) => ({
original: name,
updated: definition.tableId,
}))
.concat(
tableNameToID.map(({ name, id }) => ({
original: name,
updated: id,
}))
)
)
// generate a map of all possible column names (these can be duplicated across tables // generate a map of all possible column names (these can be duplicated across tables
// the map of them will always be the same // the map of them will always be the same
const userColumnMap: Record<string, string> = {} const userColumnMap: Record<string, string> = {}
@ -356,7 +327,7 @@ export async function search(
const relationships = buildInternalRelationships(table, allTables) const relationships = buildInternalRelationships(table, allTables)
const searchFilters: SearchFilters = { const searchFilters: SearchFilters = {
...cleanupFilters(query, table, allTables), ...cleanupFilters(query, allTables),
documentType: DocumentType.ROW, documentType: DocumentType.ROW,
} }