(field.tableId)
- if (field.fieldName) {
- delete linkedTable.schema[field.fieldName]
+ try {
+ // remove schema from other table, if it exists
+ let linkedTable = await this._db.get
(field.tableId)
+ if (field.fieldName) {
+ delete linkedTable.schema[field.fieldName]
+ }
+ await this._db.put(linkedTable)
+ } catch (error: any) {
+ // ignore missing to ensure broken relationship columns can be deleted
+ if (error.statusCode !== 404) {
+ throw error
+ }
}
- await this._db.put(linkedTable)
}
/**
diff --git a/packages/server/src/db/tests/linkController.spec.js b/packages/server/src/db/tests/linkController.spec.js
index 59d0f3f983..5caf35f61a 100644
--- a/packages/server/src/db/tests/linkController.spec.js
+++ b/packages/server/src/db/tests/linkController.spec.js
@@ -233,4 +233,19 @@ describe("test the link controller", () => {
}
await config.updateTable(table)
})
+
+ it("should be able to remove a linked field from a table, even if the linked table does not exist", async () => {
+ await createLinkedRow()
+ await createLinkedRow("link2")
+ table1.schema["link"].tableId = "not_found"
+ const controller = await createLinkController(table1, null, table1)
+ await context.doInAppContext(appId, async () => {
+ let before = await controller.getTableLinkDocs()
+ await controller.removeFieldFromTable("link")
+ let after = await controller.getTableLinkDocs()
+ expect(before.length).toEqual(2)
+ // shouldn't delete the other field
+ expect(after.length).toEqual(1)
+ })
+ })
})
diff --git a/packages/server/src/sdk/app/rows/search/external.ts b/packages/server/src/sdk/app/rows/search/external.ts
index f908be0b3c..8dd141f8ef 100644
--- a/packages/server/src/sdk/app/rows/search/external.ts
+++ b/packages/server/src/sdk/app/rows/search/external.ts
@@ -16,6 +16,7 @@ import { cleanExportRows } from "../utils"
import { utils } from "@budibase/shared-core"
import { ExportRowsParams, ExportRowsResult } from "../search"
import { HTTPError, db } from "@budibase/backend-core"
+import { searchInputMapping } from "./utils"
import pick from "lodash/pick"
import { outputProcessing } from "../../../../utilities/rowProcessor"
@@ -50,7 +51,10 @@ export async function search(options: SearchParams) {
[params.sort]: { direction },
}
}
+
try {
+ const table = await sdk.tables.getTable(tableId)
+ options = searchInputMapping(table, options)
let rows = (await handleRequest(Operation.READ, tableId, {
filters: query,
sort,
@@ -76,7 +80,6 @@ export async function search(options: SearchParams) {
rows = rows.map((r: any) => pick(r, fields))
}
- const table = await sdk.tables.getTable(tableId)
rows = await outputProcessing(table, rows, { preserveLinks: true })
// need wrapper object for bookmarks etc when paginating
diff --git a/packages/server/src/sdk/app/rows/search/internal.ts b/packages/server/src/sdk/app/rows/search/internal.ts
index 4cdeca87f6..d78c0213b3 100644
--- a/packages/server/src/sdk/app/rows/search/internal.ts
+++ b/packages/server/src/sdk/app/rows/search/internal.ts
@@ -29,6 +29,7 @@ import {
} from "../../../../api/controllers/view/utils"
import sdk from "../../../../sdk"
import { ExportRowsParams, ExportRowsResult } from "../search"
+import { searchInputMapping } from "./utils"
import pick from "lodash/pick"
export async function search(options: SearchParams) {
@@ -47,9 +48,9 @@ export async function search(options: SearchParams) {
disableEscaping: options.disableEscaping,
}
- let table
+ let table = await sdk.tables.getTable(tableId)
+ options = searchInputMapping(table, options)
if (params.sort && !params.sortType) {
- table = await sdk.tables.getTable(tableId)
const schema = table.schema
const sortField = schema[params.sort]
params.sortType = sortField.type === "number" ? "number" : "string"
@@ -68,7 +69,6 @@ export async function search(options: SearchParams) {
if (tableId === InternalTables.USER_METADATA) {
response.rows = await getGlobalUsersFromMetadata(response.rows)
}
- table = table || (await sdk.tables.getTable(tableId))
if (options.fields) {
const fields = [...options.fields, ...db.CONSTANT_INTERNAL_ROW_COLS]
diff --git a/packages/server/src/sdk/app/rows/search/tests/utils.spec.ts b/packages/server/src/sdk/app/rows/search/tests/utils.spec.ts
new file mode 100644
index 0000000000..08d1f1b1cb
--- /dev/null
+++ b/packages/server/src/sdk/app/rows/search/tests/utils.spec.ts
@@ -0,0 +1,77 @@
+import { searchInputMapping } from "../utils"
+import { db as dbCore } from "@budibase/backend-core"
+import {
+ FieldType,
+ FieldTypeSubtypes,
+ Table,
+ SearchParams,
+} from "@budibase/types"
+
+const tableId = "ta_a"
+const tableWithUserCol: Table = {
+ _id: tableId,
+ name: "table",
+ schema: {
+ user: {
+ name: "user",
+ type: FieldType.BB_REFERENCE,
+ subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
+ },
+ },
+}
+
+describe("searchInputMapping", () => {
+ const globalUserId = dbCore.generateGlobalUserID()
+ const userMedataId = dbCore.generateUserMetadataID(globalUserId)
+
+ it("should be able to map ro_ to global user IDs", () => {
+ const params: SearchParams = {
+ tableId,
+ query: {
+ equal: {
+ "1:user": userMedataId,
+ },
+ },
+ }
+ const output = searchInputMapping(tableWithUserCol, params)
+ expect(output.query.equal!["1:user"]).toBe(globalUserId)
+ })
+
+ it("should handle array of user IDs", () => {
+ const params: SearchParams = {
+ tableId,
+ query: {
+ oneOf: {
+ "1:user": [userMedataId, globalUserId],
+ },
+ },
+ }
+ const output = searchInputMapping(tableWithUserCol, params)
+ expect(output.query.oneOf!["1:user"]).toStrictEqual([
+ globalUserId,
+ globalUserId,
+ ])
+ })
+
+ it("shouldn't change any other input", () => {
+ const email = "test@test.com"
+ const params: SearchParams = {
+ tableId,
+ query: {
+ equal: {
+ "1:user": email,
+ },
+ },
+ }
+ const output = searchInputMapping(tableWithUserCol, params)
+ expect(output.query.equal!["1:user"]).toBe(email)
+ })
+
+ it("shouldn't error if no query supplied", () => {
+ const params: any = {
+ tableId,
+ }
+ const output = searchInputMapping(tableWithUserCol, params)
+ expect(output.query).toBeUndefined()
+ })
+})
diff --git a/packages/server/src/sdk/app/rows/search/utils.ts b/packages/server/src/sdk/app/rows/search/utils.ts
new file mode 100644
index 0000000000..14f7907e4f
--- /dev/null
+++ b/packages/server/src/sdk/app/rows/search/utils.ts
@@ -0,0 +1,76 @@
+import {
+ FieldType,
+ FieldTypeSubtypes,
+ SearchParams,
+ Table,
+ DocumentType,
+ SEPARATOR,
+} from "@budibase/types"
+import { db as dbCore } from "@budibase/backend-core"
+
+function findColumnInQueries(
+ column: string,
+ options: SearchParams,
+ callback: (filter: any) => any
+) {
+ if (!options.query) {
+ return
+ }
+ for (let filterBlock of Object.values(options.query)) {
+ if (typeof filterBlock !== "object") {
+ continue
+ }
+ for (let [key, filter] of Object.entries(filterBlock)) {
+ if (key.endsWith(column)) {
+ filterBlock[key] = callback(filter)
+ }
+ }
+ }
+}
+
+function userColumnMapping(column: string, options: SearchParams) {
+ findColumnInQueries(column, options, (filterValue: any): any => {
+ const isArray = Array.isArray(filterValue),
+ isString = typeof filterValue === "string"
+ if (!isString && !isArray) {
+ return filterValue
+ }
+ const processString = (input: string) => {
+ const rowPrefix = DocumentType.ROW + SEPARATOR
+ if (input.startsWith(rowPrefix)) {
+ return dbCore.getGlobalIDFromUserMetadataID(input)
+ } else {
+ return input
+ }
+ }
+ if (isArray) {
+ return filterValue.map(el => {
+ if (typeof el === "string") {
+ return processString(el)
+ } else {
+ return el
+ }
+ })
+ } else {
+ return processString(filterValue)
+ }
+ })
+}
+
+// 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: SearchParams) {
+ if (!table?.schema) {
+ return options
+ }
+ for (let [key, column] of Object.entries(table.schema)) {
+ switch (column.type) {
+ case FieldType.BB_REFERENCE:
+ if (column.subtype === FieldTypeSubtypes.BB_REFERENCE.USER) {
+ userColumnMapping(key, options)
+ }
+ break
+ }
+ }
+ return options
+}
diff --git a/packages/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts
index 2cd6fa8c13..e443f35dbe 100644
--- a/packages/shared-core/src/filters.ts
+++ b/packages/shared-core/src/filters.ts
@@ -14,7 +14,6 @@ const HBS_REGEX = /{{([^{].*?)}}/g
/**
* Returns the valid operator options for a certain data type
- * @param type the data type
*/
export const getValidOperatorsForType = (
type: FieldType,
@@ -44,22 +43,24 @@ export const getValidOperatorsForType = (
value: string
label: string
}[] = []
- if (type === "string") {
+ if (type === FieldType.STRING) {
ops = stringOps
- } else if (type === "number" || type === "bigint") {
+ } else if (type === FieldType.NUMBER || type === FieldType.BIGINT) {
ops = numOps
- } else if (type === "options") {
+ } else if (type === FieldType.OPTIONS) {
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
- } else if (type === "array") {
+ } else if (type === FieldType.ARRAY) {
ops = [Op.Contains, Op.NotContains, Op.Empty, Op.NotEmpty, Op.ContainsAny]
- } else if (type === "boolean") {
+ } else if (type === FieldType.BOOLEAN) {
ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
- } else if (type === "longform") {
+ } else if (type === FieldType.LONGFORM) {
ops = stringOps
- } else if (type === "datetime") {
+ } else if (type === FieldType.DATETIME) {
ops = numOps
- } else if (type === "formula") {
+ } else if (type === FieldType.FORMULA) {
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
+ } else if (type === FieldType.BB_REFERENCE) {
+ ops = [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty, Op.In]
}
// Only allow equal/not equal for _id in SQL tables