When searching by row ID with external DBs/SQS we can get into a situation where the limit of 1 which is applied by the frontend can cause problems, with many to many relationships we need to retrieve multiple rows (all of the joined related rows). This was raised by poirazis, it exhibits itself in one part of the platform, when attempting to a row by ID in a form block that has multiple many to many relationships. The frontend needs to be able to send a limit of 1 incase it is using a form block but hasn't gotten a row ID (this can happen in preview/the builder) and it just wants to populate with a row for display.

This commit is contained in:
mike12345567 2024-07-26 16:23:46 +01:00
parent 9a181c9707
commit 9fb1c6b988
4 changed files with 106 additions and 20 deletions

View File

@ -5,12 +5,12 @@ import {
knexClient,
} from "../../../integrations/tests/utils"
import {
db as dbCore,
context,
db as dbCore,
MAX_VALID_DATE,
MIN_VALID_DATE,
utils,
SQLITE_DESIGN_DOC_ID,
utils,
} from "@budibase/backend-core"
import * as setup from "./utilities"
@ -2560,4 +2560,48 @@ describe.each([
}).toContainExactly([{ name: "foo" }])
})
})
!isInMemory &&
describe("search by _id", () => {
let row: Row
beforeAll(async () => {
const toRelateTable = await createTable({
name: {
name: "name",
type: FieldType.STRING,
},
})
table = await createTable({
name: {
name: "name",
type: FieldType.STRING,
},
relationship: {
name: "relationship",
type: FieldType.LINK,
relationshipType: RelationshipType.MANY_TO_MANY,
tableId: toRelateTable._id!,
fieldName: "relationship",
},
})
const [row1, row2] = await Promise.all([
config.api.row.save(toRelateTable._id!, { name: "tag 1" }),
config.api.row.save(toRelateTable._id!, { name: "tag 2" }),
])
row = await config.api.row.save(table._id!, {
name: "product 1",
relationship: [row1._id, row2._id],
})
})
it("can filter by the row ID with limit 1", async () => {
await expectSearch({
query: {
equal: { _id: row._id },
},
limit: 1,
}).toContainExactly([row])
})
})
})

View File

@ -22,21 +22,21 @@ import { HTTPError } from "@budibase/backend-core"
import pick from "lodash/pick"
import { outputProcessing } from "../../../../utilities/rowProcessor"
import sdk from "../../../"
import { isSearchingByRowID } from "./utils"
export async function search(
options: RowSearchParams,
table: Table
): Promise<SearchResponse<Row>> {
const { tableId } = options
const { countRows, paginate, query, ...params } = options
const { limit } = params
let bookmark =
(params.bookmark && parseInt(params.bookmark as string)) || undefined
if (paginate && !bookmark) {
bookmark = 0
}
function getPaginationAndLimitParameters(
filters: SearchFilters,
paginate: boolean | undefined,
bookmark: number | undefined,
limit: number | undefined
): PaginationJson | undefined {
let paginateObj: PaginationJson | undefined
// only try set limits/pagination if we aren't doing a row ID search
if (!isSearchingByRowID(filters)) {
return
}
if (paginate && !limit) {
throw new Error("Cannot paginate query without a limit")
}
@ -49,11 +49,35 @@ export async function search(
if (bookmark) {
paginateObj.offset = limit * bookmark
}
} else if (params && limit) {
} else if (limit) {
paginateObj = {
limit: limit,
}
}
return paginateObj
}
export async function search(
options: RowSearchParams,
table: Table
): Promise<SearchResponse<Row>> {
const { tableId } = options
const { countRows, paginate, query, ...params } = options
const { limit } = params
let bookmark =
(params.bookmark && parseInt(params.bookmark as string)) || undefined
if (paginate && !bookmark) {
bookmark = 0
}
let paginateObj = getPaginationAndLimitParameters(
query,
paginate,
bookmark,
limit
)
let sort: SortJson | undefined
if (params.sort) {
const direction =

View File

@ -42,6 +42,7 @@ import {
getTableIDList,
} from "./filters"
import { dataFilters, PROTECTED_INTERNAL_COLUMNS } from "@budibase/shared-core"
import { isSearchingByRowID } from "./utils"
const builder = new sql.Sql(SqlClient.SQL_LITE)
const MISSING_COLUMN_REGEX = new RegExp(`no such column: .+`)
@ -264,6 +265,10 @@ export async function search(
const relationships = buildInternalRelationships(table)
const searchFilters: SearchFilters = {
...cleanupFilters(query, table, allTables),
documentType: DocumentType.ROW,
}
const request: QueryJson = {
endpoint: {
// not important, we query ourselves
@ -271,10 +276,7 @@ export async function search(
entityId: table._id!,
operation: Operation.READ,
},
filters: {
...cleanupFilters(query, table, allTables),
documentType: DocumentType.ROW,
},
filters: searchFilters,
table,
meta: {
table,
@ -304,7 +306,8 @@ export async function search(
}
const bookmark: number = (params.bookmark as number) || 0
if (params.limit) {
// limits don't apply if we doing a row ID search
if (!isSearchingByRowID(searchFilters) && params.limit) {
paginate = true
request.paginate = {
limit: params.limit + 1,

View File

@ -108,3 +108,18 @@ export function searchInputMapping(table: Table, options: RowSearchParams) {
}
return options
}
export function isSearchingByRowID(query: SearchFilters): boolean {
for (let searchField of Object.values(query)) {
if (typeof searchField !== "object") {
continue
}
const hasId = Object.keys(searchField).find(
key => key.endsWith("_id") && searchField[key]
)
if (hasId) {
return true
}
}
return false
}