From 33f7792522906e3dd326bafd59ee669cb0771ff8 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 11 Oct 2024 15:33:33 +0100 Subject: [PATCH 1/4] Potential fix for view search problem. --- .../server/src/api/controllers/row/views.ts | 1 + packages/server/src/sdk/app/rows/search.ts | 19 +++---- .../server/src/sdk/app/rows/search/utils.ts | 54 ++++++++++--------- 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/packages/server/src/api/controllers/row/views.ts b/packages/server/src/api/controllers/row/views.ts index 622688deb6..9163c401df 100644 --- a/packages/server/src/api/controllers/row/views.ts +++ b/packages/server/src/api/controllers/row/views.ts @@ -8,6 +8,7 @@ import { } from "@budibase/types" import sdk from "../../../sdk" import { context } from "@budibase/backend-core" +import * as utils from "./utils" export async function searchView( ctx: UserCtx diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts index f68f78f0bc..ee40f6f4e6 100644 --- a/packages/server/src/sdk/app/rows/search.ts +++ b/packages/server/src/sdk/app/rows/search.ts @@ -16,7 +16,7 @@ import * as external from "./search/external" import { ExportRowsParams, ExportRowsResult } from "./search/types" import { dataFilters } from "@budibase/shared-core" import sdk from "../../index" -import { searchInputMapping } from "./search/utils" +import { checkFilters, searchInputMapping } from "./search/utils" import { db, features } from "@budibase/backend-core" import tracer from "dd-trace" import { getQueryableFields, removeInvalidFilters } from "./queryUtils" @@ -92,8 +92,10 @@ export async function search( // Enrich saved query with ephemeral query params. // We prevent searching on any fields that are saved as part of the query, as // that could let users find rows they should not be allowed to access. - let viewQuery = dataFilters.buildQueryLegacy(view.query) || {} - delete viewQuery?.onEmptyFilter + let query = await enrichSearchContext(view.query || {}, context) + query = dataFilters.buildQueryLegacy(query) || {} + query = checkFilters(table, query) + delete query?.onEmptyFilter const sqsEnabled = await features.flags.isEnabled("SQS") const supportsLogicalOperators = @@ -113,26 +115,25 @@ export async function search( ?.filter(filter => filter.field) .map(filter => db.removeKeyNumbering(filter.field)) || [] - viewQuery ??= {} // Carry over filters for unused fields Object.keys(options.query).forEach(key => { const operator = key as Exclude Object.keys(options.query[operator] || {}).forEach(field => { if (!existingFields.includes(db.removeKeyNumbering(field))) { - viewQuery![operator]![field] = options.query[operator]![field] + query[operator]![field] = options.query[operator]![field] } }) }) - options.query = viewQuery + options.query = query } else { - const conditions = viewQuery ? [viewQuery] : [] + const conditions = query ? [query] : [] options.query = { $and: { conditions: [...conditions, options.query], }, } - if (viewQuery.onEmptyFilter) { - options.query.onEmptyFilter = viewQuery.onEmptyFilter + if (query.onEmptyFilter) { + options.query.onEmptyFilter = query.onEmptyFilter } } } diff --git a/packages/server/src/sdk/app/rows/search/utils.ts b/packages/server/src/sdk/app/rows/search/utils.ts index 90303a6ca7..c7de7eef37 100644 --- a/packages/server/src/sdk/app/rows/search/utils.ts +++ b/packages/server/src/sdk/app/rows/search/utils.ts @@ -80,35 +80,41 @@ function userColumnMapping(column: string, filters: SearchFilters) { }) } +export function checkFilters( + table: Table, + filters: SearchFilters +): SearchFilters { + for (let [key, column] of Object.entries(table.schema || {})) { + switch (column.type) { + case FieldType.BB_REFERENCE_SINGLE: { + const subtype = column.subtype + switch (subtype) { + case BBReferenceFieldSubType.USER: + userColumnMapping(key, filters) + break + + default: + utils.unreachable(subtype) + } + break + } + case FieldType.BB_REFERENCE: { + userColumnMapping(key, filters) + break + } + } + } + return dataFilters.recurseLogicalOperators(filters, filters => + checkFilters(table, filters) + ) +} + // maps through the search parameters to check if any of the inputs are invalid // based on the table schema, converts them to something that is valid. export function searchInputMapping(table: Table, options: RowSearchParams) { // need an internal function to loop over filters, because this takes the full options - function checkFilters(filters: SearchFilters) { - for (let [key, column] of Object.entries(table.schema || {})) { - switch (column.type) { - case FieldType.BB_REFERENCE_SINGLE: { - const subtype = column.subtype - switch (subtype) { - case BBReferenceFieldSubType.USER: - userColumnMapping(key, filters) - break - - default: - utils.unreachable(subtype) - } - break - } - case FieldType.BB_REFERENCE: { - userColumnMapping(key, filters) - break - } - } - } - return dataFilters.recurseLogicalOperators(filters, checkFilters) - } if (options.query) { - options.query = checkFilters(options.query) + options.query = checkFilters(table, options.query) } return options } From e967e62f1dd9dd2ecf60ed6acb15847f46fbeb6f Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 11 Oct 2024 15:55:25 +0100 Subject: [PATCH 2/4] Add tests. --- .../src/api/routes/tests/viewV2.spec.ts | 44 +++++++++++++++++++ packages/server/src/sdk/app/rows/search.ts | 9 ++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 0b4237406f..abb2a4c83c 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -3428,6 +3428,50 @@ describe.each([ expect(response.rows).toHaveLength(1) expect(response.rows[0].sum).toEqual(61) }) + + it("should be able to filter on a single user field in both the view query and search query", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + user: { + name: "user", + type: FieldType.BB_REFERENCE_SINGLE, + subtype: BBReferenceFieldSubType.USER, + }, + }, + }) + ) + + await config.api.row.save(table._id!, { + user: config.getUser()._id, + }) + + const view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + query: { + equal: { + user: "{{ [user].[_id] }}", + }, + }, + schema: { + user: { + visible: true, + }, + }, + }) + + const { rows } = await config.api.viewV2.search(view.id, { + query: { + equal: { + user: "{{ [user].[_id] }}", + }, + }, + }) + + expect(rows).toHaveLength(1) + expect(rows[0].user._id).toEqual(config.getUser()._id) + }) }) describe("permissions", () => { diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts index ee40f6f4e6..94bab9f7f3 100644 --- a/packages/server/src/sdk/app/rows/search.ts +++ b/packages/server/src/sdk/app/rows/search.ts @@ -81,6 +81,10 @@ export async function search( options.query = {} } + if (context) { + options.query = await enrichSearchContext(options.query, context) + } + // need to make sure filters in correct shape before checking for view options = searchInputMapping(table, options) @@ -100,6 +104,7 @@ export async function search( const sqsEnabled = await features.flags.isEnabled("SQS") const supportsLogicalOperators = isExternalTableID(view.tableId) || sqsEnabled + if (!supportsLogicalOperators) { // In the unlikely event that a Grouped Filter is in a non-SQS environment // It needs to be ignored entirely @@ -138,10 +143,6 @@ export async function search( } } - if (context) { - options.query = await enrichSearchContext(options.query, context) - } - options.query = dataFilters.cleanupQuery(options.query) options.query = dataFilters.fixupFilterArrays(options.query) From 1ad371cd7a61380d4f4fdd50a92fcca9c25be709 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 11 Oct 2024 15:58:04 +0100 Subject: [PATCH 3/4] Fix lint. --- packages/server/src/api/controllers/row/views.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/src/api/controllers/row/views.ts b/packages/server/src/api/controllers/row/views.ts index 9163c401df..622688deb6 100644 --- a/packages/server/src/api/controllers/row/views.ts +++ b/packages/server/src/api/controllers/row/views.ts @@ -8,7 +8,6 @@ import { } from "@budibase/types" import sdk from "../../../sdk" import { context } from "@budibase/backend-core" -import * as utils from "./utils" export async function searchView( ctx: UserCtx From b24c3378342d44e39b1773a4a885476f577d7257 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 11 Oct 2024 16:22:04 +0100 Subject: [PATCH 4/4] Respond to PR comment. --- packages/server/src/sdk/app/rows/search.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts index 94bab9f7f3..7ac3bb8ead 100644 --- a/packages/server/src/sdk/app/rows/search.ts +++ b/packages/server/src/sdk/app/rows/search.ts @@ -96,10 +96,10 @@ export async function search( // Enrich saved query with ephemeral query params. // We prevent searching on any fields that are saved as part of the query, as // that could let users find rows they should not be allowed to access. - let query = await enrichSearchContext(view.query || {}, context) - query = dataFilters.buildQueryLegacy(query) || {} - query = checkFilters(table, query) - delete query?.onEmptyFilter + let viewQuery = await enrichSearchContext(view.query || {}, context) + viewQuery = dataFilters.buildQueryLegacy(viewQuery) || {} + viewQuery = checkFilters(table, viewQuery) + delete viewQuery?.onEmptyFilter const sqsEnabled = await features.flags.isEnabled("SQS") const supportsLogicalOperators = @@ -125,20 +125,20 @@ export async function search( const operator = key as Exclude Object.keys(options.query[operator] || {}).forEach(field => { if (!existingFields.includes(db.removeKeyNumbering(field))) { - query[operator]![field] = options.query[operator]![field] + viewQuery[operator]![field] = options.query[operator]![field] } }) }) - options.query = query + options.query = viewQuery } else { - const conditions = query ? [query] : [] + const conditions = viewQuery ? [viewQuery] : [] options.query = { $and: { conditions: [...conditions, options.query], }, } - if (query.onEmptyFilter) { - options.query.onEmptyFilter = query.onEmptyFilter + if (viewQuery.onEmptyFilter) { + options.query.onEmptyFilter = viewQuery.onEmptyFilter } } }