Merge pull request #15172 from Budibase/fix/external-table-name-spaces

Relationship searching on tables with spaces
This commit is contained in:
Michael Drury 2024-12-12 18:20:45 +00:00 committed by GitHub
commit 8144f04e05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 83 additions and 26 deletions

View File

@ -4,8 +4,8 @@ import {
getContainerRuntimeClient, getContainerRuntimeClient,
} from "testcontainers" } from "testcontainers"
import { ContainerInfo } from "dockerode" import { ContainerInfo } from "dockerode"
import path from "path" import * as path from "path"
import lockfile from "proper-lockfile" import * as lockfile from "proper-lockfile"
import { execSync } from "child_process" import { execSync } from "child_process"
interface DockerContext { interface DockerContext {
@ -29,8 +29,8 @@ function getCurrentDockerContext(): DockerContext {
async function getBudibaseContainers() { async function getBudibaseContainers() {
const client = await getContainerRuntimeClient() const client = await getContainerRuntimeClient()
const conatiners = await client.container.list() const containers = await client.container.list()
return conatiners.filter( return containers.filter(
container => container =>
container.Labels["com.budibase"] === "true" && container.Labels["com.budibase"] === "true" &&
container.Labels["org.testcontainers"] === "true" container.Labels["org.testcontainers"] === "true"

View File

@ -59,11 +59,15 @@ export function isExternalTable(table: Table) {
} }
export function buildExternalTableId(datasourceId: string, tableName: string) { export function buildExternalTableId(datasourceId: string, tableName: string) {
// encode spaces return `${datasourceId}${DOUBLE_SEPARATOR}${encodeURIComponent(tableName)}`
if (tableName.includes(" ")) { }
tableName = encodeURIComponent(tableName)
export function encodeTableId(tableId: string) {
if (isExternalTableID(tableId)) {
return encodeURIComponent(tableId)
} else {
return tableId
} }
return `${datasourceId}${DOUBLE_SEPARATOR}${tableName}`
} }
export function breakExternalTableId(tableId: string) { export function breakExternalTableId(tableId: string) {

View File

@ -288,19 +288,21 @@ function replaceTableNamesInFilters(
for (const key of Object.keys(filter)) { for (const key of Object.keys(filter)) {
const matches = key.match(`^(?<relation>.+)\\.(?<field>.+)`) const matches = key.match(`^(?<relation>.+)\\.(?<field>.+)`)
const relation = matches?.groups?.["relation"] // this is the possible table name which we need to check if it needs to be converted
const relatedTableName = matches?.groups?.["relation"]
const field = matches?.groups?.["field"] const field = matches?.groups?.["field"]
if (!relation || !field) { if (!relatedTableName || !field) {
continue continue
} }
const table = allTables.find(r => r._id === tableId)! const table = allTables.find(r => r._id === tableId)
if (Object.values(table.schema).some(f => f.name === relation)) { const isColumnName = !!table?.schema[relatedTableName]
if (!table || isColumnName) {
continue continue
} }
const matchedTable = allTables.find(t => t.name === relation) const matchedTable = allTables.find(t => t.name === relatedTableName)
const relationship = Object.values(table.schema).find( const relationship = Object.values(table.schema).find(
f => isRelationshipField(f) && f.tableId === matchedTable?._id f => isRelationshipField(f) && f.tableId === matchedTable?._id
) )

View File

@ -1,6 +1,6 @@
import * as utils from "../../../../db/utils" import * as utils from "../../../../db/utils"
import { docIds } from "@budibase/backend-core" import { docIds, sql } from "@budibase/backend-core"
import { import {
Ctx, Ctx,
DatasourcePlusQueryResponse, DatasourcePlusQueryResponse,
@ -69,15 +69,15 @@ export function getSourceId(ctx: Ctx): { tableId: string; viewId?: string } {
viewId: sourceId, viewId: sourceId,
} }
} }
return { tableId: ctx.params.sourceId } return { tableId: sql.utils.encodeTableId(ctx.params.sourceId) }
} }
// now check for old way of specifying table ID // now check for old way of specifying table ID
if (ctx.params?.tableId) { if (ctx.params?.tableId) {
return { tableId: ctx.params.tableId } return { tableId: sql.utils.encodeTableId(ctx.params.tableId) }
} }
// check body for a table ID // check body for a table ID
if (ctx.request.body?.tableId) { if (ctx.request.body?.tableId) {
return { tableId: ctx.request.body.tableId } return { tableId: sql.utils.encodeTableId(ctx.request.body.tableId) }
} }
throw new Error("Unable to find table ID in request") throw new Error("Unable to find table ID in request")
} }

View File

@ -71,18 +71,27 @@ if (descriptions.length) {
let tableOrViewId: string let tableOrViewId: string
let rows: Row[] let rows: Row[]
async function basicRelationshipTables(type: RelationshipType) { async function basicRelationshipTables(
type: RelationshipType,
opts?: {
tableName?: string
primaryColumn?: string
otherColumn?: string
}
) {
const relatedTable = await createTable({ const relatedTable = await createTable({
name: { name: "name", type: FieldType.STRING }, name: { name: opts?.tableName || "name", type: FieldType.STRING },
}) })
const columnName = opts?.primaryColumn || "productCat"
//@ts-ignore - API accepts this structure, will build out rest of definition
const tableId = await createTable({ const tableId = await createTable({
name: { name: "name", type: FieldType.STRING }, name: { name: opts?.tableName || "name", type: FieldType.STRING },
//@ts-ignore - API accepts this structure, will build out rest of definition [columnName]: {
productCat: {
type: FieldType.LINK, type: FieldType.LINK,
relationshipType: type, relationshipType: type,
name: "productCat", name: columnName,
fieldName: "product", fieldName: opts?.otherColumn || "product",
tableId: relatedTable, tableId: relatedTable,
constraints: { constraints: {
type: "array", type: "array",
@ -2776,6 +2785,42 @@ if (descriptions.length) {
}) })
}) })
isSql &&
describe("relationship - table with spaces", () => {
let primaryTable: Table, row: Row
beforeAll(async () => {
const { relatedTable, tableId } =
await basicRelationshipTables(
RelationshipType.ONE_TO_MANY,
{
tableName: "table with spaces",
primaryColumn: "related",
otherColumn: "related",
}
)
tableOrViewId = tableId
primaryTable = relatedTable
row = await config.api.row.save(primaryTable._id!, {
name: "foo",
})
await config.api.row.save(tableOrViewId, {
name: "foo",
related: [row._id],
})
})
it("should be able to search by table name with spaces", async () => {
await expectQuery({
equal: {
["table with spaces.name"]: "foo",
},
}).toContain([{ name: "foo" }])
})
})
isSql && isSql &&
describe.each([ describe.each([
RelationshipType.MANY_TO_ONE, RelationshipType.MANY_TO_ONE,

View File

@ -1,4 +1,10 @@
import { context, db as dbCore, docIds, utils } from "@budibase/backend-core" import {
context,
db as dbCore,
docIds,
utils,
sql,
} from "@budibase/backend-core"
import { import {
DatabaseQueryOpts, DatabaseQueryOpts,
Datasource, Datasource,
@ -328,7 +334,7 @@ export function extractViewInfoFromID(viewId: string) {
const regex = new RegExp(`^(?<tableId>.+)${SEPARATOR}([^${SEPARATOR}]+)$`) const regex = new RegExp(`^(?<tableId>.+)${SEPARATOR}([^${SEPARATOR}]+)$`)
const res = regex.exec(viewId) const res = regex.exec(viewId)
return { return {
tableId: res!.groups!["tableId"], tableId: sql.utils.encodeTableId(res!.groups!["tableId"]),
} }
} }