Merge pull request #14335 from Budibase/BUDI-8508/conditions-on-views

Support filtering views
This commit is contained in:
Adria Navarro 2024-08-07 15:09:05 +02:00 committed by GitHub
commit dac3fa0675
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 146 additions and 32 deletions

View File

@ -12,6 +12,7 @@ import { dataFilters } from "@budibase/shared-core"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { db, context } from "@budibase/backend-core" import { db, context } from "@budibase/backend-core"
import { enrichSearchContext } from "./utils" import { enrichSearchContext } from "./utils"
import { isExternalTableID } from "../../../integrations/utils"
export async function searchView( export async function searchView(
ctx: UserCtx<SearchViewRowRequest, SearchRowResponse> ctx: UserCtx<SearchViewRowRequest, SearchRowResponse>
@ -36,33 +37,33 @@ export async function searchView(
// that could let users find rows they should not be allowed to access. // that could let users find rows they should not be allowed to access.
let query = dataFilters.buildQuery(view.query || []) let query = dataFilters.buildQuery(view.query || [])
if (body.query) { if (body.query) {
// Extract existing fields
const existingFields =
view.query
?.filter(filter => filter.field)
.map(filter => db.removeKeyNumbering(filter.field)) || []
// Delete extraneous search params that cannot be overridden // Delete extraneous search params that cannot be overridden
delete body.query.allOr delete body.query.allOr
delete body.query.onEmptyFilter delete body.query.onEmptyFilter
// Carry over filters for unused fields if (!isExternalTableID(view.tableId) && !db.isSqsEnabledForTenant()) {
Object.keys(body.query).forEach(key => { // Extract existing fields
const operator = key as SearchFilterKey const existingFields =
view.query
?.filter(filter => filter.field)
.map(filter => db.removeKeyNumbering(filter.field)) || []
Object.keys(body.query[operator] || {}).forEach(field => { // Carry over filters for unused fields
if (!existingFields.includes(db.removeKeyNumbering(field))) { Object.keys(body.query).forEach(key => {
if ( const operator = key as Exclude<SearchFilterKey, LogicalOperator>
operator === LogicalOperator.AND || Object.keys(body.query[operator] || {}).forEach(field => {
operator === LogicalOperator.OR if (!existingFields.includes(db.removeKeyNumbering(field))) {
) {
// TODO
} else {
query[operator]![field] = body.query[operator]![field] query[operator]![field] = body.query[operator]![field]
} }
} })
}) })
}) } else {
query = {
$and: {
conditions: [query, body.query],
},
}
}
} }
await context.ensureSnippetContext(true) await context.ensureSnippetContext(true)

View File

@ -1485,6 +1485,119 @@ describe.each([
} }
) )
}) })
isLucene &&
it("in lucene, cannot override a view filter", async () => {
await config.api.row.save(table._id!, {
one: "foo",
two: "bar",
})
const two = await config.api.row.save(table._id!, {
one: "foo2",
two: "bar2",
})
const view = await config.api.viewV2.create({
tableId: table._id!,
name: generator.guid(),
query: [
{
operator: BasicOperator.EQUAL,
field: "two",
value: "bar2",
},
],
schema: {
id: { visible: true },
one: { visible: false },
two: { visible: true },
},
})
const response = await config.api.viewV2.search(view.id, {
query: {
equal: {
two: "bar",
},
},
})
expect(response.rows).toHaveLength(1)
expect(response.rows).toEqual([
expect.objectContaining({ _id: two._id }),
])
})
!isLucene &&
it("can filter a view without a view filter", async () => {
const one = await config.api.row.save(table._id!, {
one: "foo",
two: "bar",
})
await config.api.row.save(table._id!, {
one: "foo2",
two: "bar2",
})
const view = await config.api.viewV2.create({
tableId: table._id!,
name: generator.guid(),
schema: {
id: { visible: true },
one: { visible: false },
two: { visible: true },
},
})
const response = await config.api.viewV2.search(view.id, {
query: {
equal: {
two: "bar",
},
},
})
expect(response.rows).toHaveLength(1)
expect(response.rows).toEqual([
expect.objectContaining({ _id: one._id }),
])
})
!isLucene &&
it("cannot bypass a view filter", async () => {
await config.api.row.save(table._id!, {
one: "foo",
two: "bar",
})
await config.api.row.save(table._id!, {
one: "foo2",
two: "bar2",
})
const view = await config.api.viewV2.create({
tableId: table._id!,
name: generator.guid(),
query: [
{
operator: BasicOperator.EQUAL,
field: "two",
value: "bar2",
},
],
schema: {
id: { visible: true },
one: { visible: false },
two: { visible: true },
},
})
const response = await config.api.viewV2.search(view.id, {
query: {
equal: {
two: "bar",
},
},
})
expect(response.rows).toHaveLength(0)
})
}) })
describe("permissions", () => { describe("permissions", () => {

View File

@ -2,7 +2,7 @@ import {
Datasource, Datasource,
DocumentType, DocumentType,
FieldType, FieldType,
LogicalOperator, isLogicalSearchOperator,
Operation, Operation,
QueryJson, QueryJson,
RelationshipFieldMetadata, RelationshipFieldMetadata,
@ -141,10 +141,7 @@ function cleanupFilters(
const prefixFilters = (filters: SearchFilters) => { const prefixFilters = (filters: SearchFilters) => {
for (const filterKey of Object.keys(filters) as (keyof SearchFilters)[]) { for (const filterKey of Object.keys(filters) as (keyof SearchFilters)[]) {
if ( if (isLogicalSearchOperator(filterKey)) {
filterKey === LogicalOperator.AND ||
filterKey === LogicalOperator.OR
) {
for (const condition of filters[filterKey]!.conditions) { for (const condition of filters[filterKey]!.conditions) {
prefixFilters(condition) prefixFilters(condition)
} }

View File

@ -18,6 +18,7 @@ import {
BasicOperator, BasicOperator,
RangeOperator, RangeOperator,
LogicalOperator, LogicalOperator,
isLogicalSearchOperator,
} from "@budibase/types" } from "@budibase/types"
import dayjs from "dayjs" import dayjs from "dayjs"
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants" import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
@ -359,10 +360,7 @@ export const buildQuery = (filter: SearchFilter[]) => {
high: value, high: value,
} }
} }
} else if ( } else if (isLogicalSearchOperator(queryOperator)) {
queryOperator === LogicalOperator.AND ||
queryOperator === LogicalOperator.OR
) {
// TODO // TODO
} else if (query[queryOperator] && operator !== "onEmptyFilter") { } else if (query[queryOperator] && operator !== "onEmptyFilter") {
if (type === "boolean") { if (type === "boolean") {
@ -464,10 +462,9 @@ export const runQuery = (docs: Record<string, any>[], query: SearchFilters) => {
) => ) =>
(doc: Record<string, any>) => { (doc: Record<string, any>) => {
for (const [key, testValue] of Object.entries(query[type] || {})) { for (const [key, testValue] of Object.entries(query[type] || {})) {
const valueToCheck = const valueToCheck = isLogicalSearchOperator(type)
type === LogicalOperator.AND || type === LogicalOperator.OR ? doc
? doc : deepGet(doc, removeKeyNumbering(key))
: deepGet(doc, removeKeyNumbering(key))
const result = test(valueToCheck, testValue) const result = test(valueToCheck, testValue)
if (query.allOr && result) { if (query.allOr && result) {
return true return true

View File

@ -28,6 +28,12 @@ export enum LogicalOperator {
OR = "$or", OR = "$or",
} }
export function isLogicalSearchOperator(
value: string
): value is LogicalOperator {
return value === LogicalOperator.AND || value === LogicalOperator.OR
}
export type SearchFilterOperator = export type SearchFilterOperator =
| BasicOperator | BasicOperator
| ArrayOperator | ArrayOperator