From a00a64bb6e5451d2b4b9f93e748b9e25a37cf171 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 10 Oct 2024 14:56:38 +0100 Subject: [PATCH 1/2] Properly clean up row actions on table deletion. --- .../server/src/api/controllers/table/index.ts | 1 + .../src/api/routes/tests/rowAction.spec.ts | 40 +++++++++++++++++++ packages/server/src/sdk/app/rowActions.ts | 15 +++++++ .../src/tests/utilities/api/automation.ts | 22 +++++++++- 4 files changed, 77 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/controllers/table/index.ts b/packages/server/src/api/controllers/table/index.ts index 404f82e57c..efe1a88e4a 100644 --- a/packages/server/src/api/controllers/table/index.ts +++ b/packages/server/src/api/controllers/table/index.ts @@ -139,6 +139,7 @@ export async function save(ctx: UserCtx) { export async function destroy(ctx: UserCtx) { const appId = ctx.appId const tableId = ctx.params.tableId + await sdk.rowActions.deleteAll(tableId) const deletedTable = await pickApi({ tableId }).destroy(ctx) await events.table.deleted(deletedTable) ctx.eventEmitter && diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index fb59ce73c3..7140f3486e 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -1041,4 +1041,44 @@ describe("/rowsActions", () => { ) }) }) + + describe("scenarios", () => { + // https://linear.app/budibase/issue/BUDI-8717/ + it("should not brick the app when deleting a table with row actions", async () => { + const view = await config.api.viewV2.create({ + tableId, + name: generator.guid(), + schema: { + name: { visible: true }, + }, + }) + + const rowAction = await config.api.rowAction.save(tableId, { + name: generator.guid(), + }) + + await config.api.rowAction.setViewPermission( + tableId, + view.id, + rowAction.id + ) + + let actionsResp = await config.api.rowAction.find(tableId) + expect(actionsResp.actions[rowAction.id]).toEqual({ + ...rowAction, + allowedSources: [tableId, view.id], + }) + + const table = await config.api.table.get(tableId) + await config.api.table.destroy(table._id!, table._rev!) + + // In the bug reported by Conor, when a delete got deleted its row action + // document was not being cleaned up. This meant there existed code paths + // that would find it and try to reference the tables within it, resulting + // in errors. + await config.api.automation.fetchEnriched({ + status: 200, + }) + }) + }) }) diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts index 418a906c00..958401b8b9 100644 --- a/packages/server/src/sdk/app/rowActions.ts +++ b/packages/server/src/sdk/app/rowActions.ts @@ -1,5 +1,6 @@ import { context, docIds, HTTPError, utils } from "@budibase/backend-core" import { + Automation, AutomationTriggerStepId, SEPARATOR, TableRowActions, @@ -105,6 +106,20 @@ export async function getAll(tableId: string) { return await db.get(rowActionsId) } +export async function deleteAll(tableId: string) { + const db = context.getAppDB() + + const doc = await getAll(tableId) + const automationIds = Object.values(doc.actions).map(a => a.automationId) + const automations = await db.getMultiple(automationIds) + + for (const automation of automations) { + await sdk.automations.remove(automation._id!, automation._rev!) + } + + await db.remove(doc) +} + export async function docExists(tableId: string) { const db = context.getAppDB() const rowActionsId = generateRowActionsID(tableId) diff --git a/packages/server/src/tests/utilities/api/automation.ts b/packages/server/src/tests/utilities/api/automation.ts index 61bd915647..11c041d52b 100644 --- a/packages/server/src/tests/utilities/api/automation.ts +++ b/packages/server/src/tests/utilities/api/automation.ts @@ -1,4 +1,4 @@ -import { Automation } from "@budibase/types" +import { Automation, FetchAutomationResponse } from "@budibase/types" import { Expectations, TestAPI } from "./base" export class AutomationAPI extends TestAPI { @@ -14,6 +14,26 @@ export class AutomationAPI extends TestAPI { ) return result } + + fetch = async ( + expectations?: Expectations + ): Promise => { + return await this._get(`/api/automations`, { + expectations, + }) + } + + fetchEnriched = async ( + expectations?: Expectations + ): Promise => { + return await this._get( + `/api/automations?enrich=true`, + { + expectations, + } + ) + } + post = async ( body: Automation, expectations?: Expectations From 4c4429b88ade15ee8ed98e6233d814d2a5041360 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 10 Oct 2024 15:49:00 +0100 Subject: [PATCH 2/2] Fix tests. --- packages/backend-core/src/db/couch/DatabaseImpl.ts | 11 +++++++++++ packages/backend-core/src/db/instrumentation.ts | 7 +++++++ packages/server/src/api/controllers/rowAction/crud.ts | 5 +++-- packages/server/src/sdk/app/automations/utils.ts | 8 ++++++-- packages/server/src/sdk/app/rowActions.ts | 11 +++++++---- packages/types/src/sdk/db.ts | 5 +++++ 6 files changed, 39 insertions(+), 8 deletions(-) diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index 2b37526dde..8ca20bf8e1 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -211,6 +211,17 @@ export class DatabaseImpl implements Database { }) } + async tryGet(id?: string): Promise { + try { + return await this.get(id) + } catch (err: any) { + if (err.statusCode === 404) { + return undefined + } + throw err + } + } + async getMultiple( ids: string[], opts?: { allowMissing?: boolean; excludeDocs?: boolean } diff --git a/packages/backend-core/src/db/instrumentation.ts b/packages/backend-core/src/db/instrumentation.ts index 7026224564..e08bfc0362 100644 --- a/packages/backend-core/src/db/instrumentation.ts +++ b/packages/backend-core/src/db/instrumentation.ts @@ -42,6 +42,13 @@ export class DDInstrumentedDatabase implements Database { }) } + tryGet(id?: string | undefined): Promise { + return tracer.trace("db.tryGet", span => { + span?.addTags({ db_name: this.name, doc_id: id }) + return this.db.tryGet(id) + }) + } + getMultiple( ids: string[], opts?: { allowMissing?: boolean | undefined } | undefined diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts index 67b6d9383e..ca84b2ea30 100644 --- a/packages/server/src/api/controllers/rowAction/crud.ts +++ b/packages/server/src/api/controllers/rowAction/crud.ts @@ -21,14 +21,15 @@ export async function find(ctx: Ctx) { const table = await getTable(ctx) const tableId = table._id! - if (!(await sdk.rowActions.docExists(tableId))) { + const rowActions = await sdk.rowActions.getAll(tableId) + if (!rowActions) { ctx.body = { actions: {}, } return } - const { actions } = await sdk.rowActions.getAll(tableId) + const { actions } = rowActions const result: RowActionsResponse = { actions: Object.entries(actions).reduce>( (acc, [key, action]) => ({ diff --git a/packages/server/src/sdk/app/automations/utils.ts b/packages/server/src/sdk/app/automations/utils.ts index 2ce0012b6a..f3bd77c012 100644 --- a/packages/server/src/sdk/app/automations/utils.ts +++ b/packages/server/src/sdk/app/automations/utils.ts @@ -26,13 +26,13 @@ export async function getBuilderData( return tableNameCache[tableId] } - const rowActionNameCache: Record = {} + const rowActionNameCache: Record = {} async function getRowActionName(tableId: string, rowActionId: string) { if (!rowActionNameCache[tableId]) { rowActionNameCache[tableId] = await sdk.rowActions.getAll(tableId) } - return rowActionNameCache[tableId].actions[rowActionId]?.name + return rowActionNameCache[tableId]?.actions[rowActionId]?.name } const result: Record = {} @@ -51,6 +51,10 @@ export async function getBuilderData( const tableName = await getTableName(tableId) const rowActionName = await getRowActionName(tableId, rowActionId) + if (!rowActionName) { + throw new Error(`Row action not found: ${rowActionId}`) + } + result[automation._id!] = { displayName: `${tableName}: ${automation.name}`, triggerInfo: { diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts index 958401b8b9..3d8a6fd9be 100644 --- a/packages/server/src/sdk/app/rowActions.ts +++ b/packages/server/src/sdk/app/rowActions.ts @@ -103,13 +103,17 @@ export async function get(tableId: string, rowActionId: string) { export async function getAll(tableId: string) { const db = context.getAppDB() const rowActionsId = generateRowActionsID(tableId) - return await db.get(rowActionsId) + return await db.tryGet(rowActionsId) } export async function deleteAll(tableId: string) { const db = context.getAppDB() const doc = await getAll(tableId) + if (!doc) { + return + } + const automationIds = Object.values(doc.actions).map(a => a.automationId) const automations = await db.getMultiple(automationIds) @@ -238,9 +242,8 @@ export async function run(tableId: any, rowActionId: any, rowId: string) { throw new HTTPError("Table not found", 404) } - const { actions } = await getAll(tableId) - - const rowAction = actions[rowActionId] + const rowActions = await getAll(tableId) + const rowAction = rowActions?.actions[rowActionId] if (!rowAction) { throw new HTTPError("Row action not found", 404) } diff --git a/packages/types/src/sdk/db.ts b/packages/types/src/sdk/db.ts index 49baaa5bb1..b679d6e182 100644 --- a/packages/types/src/sdk/db.ts +++ b/packages/types/src/sdk/db.ts @@ -129,7 +129,12 @@ export interface Database { name: string exists(): Promise + /** + * @deprecated the plan is to get everything using `tryGet` instead, then rename + * `tryGet` to `get`. + */ get(id?: string): Promise + tryGet(id?: string): Promise exists(docId: string): Promise getMultiple( ids: string[],