From 107bd08e21dfefa5f6f82cb1a7ab41778a727a21 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 10 Jul 2024 10:46:15 +0200 Subject: [PATCH 01/63] Endpoint scaffolding --- .../src/api/controllers/rowAction/crud.ts | 15 +++++++ .../src/api/controllers/rowAction/index.ts | 2 + .../src/api/controllers/rowAction/run.ts | 3 ++ packages/server/src/api/routes/rowAction.ts | 41 +++++++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 packages/server/src/api/controllers/rowAction/crud.ts create mode 100644 packages/server/src/api/controllers/rowAction/index.ts create mode 100644 packages/server/src/api/controllers/rowAction/run.ts create mode 100644 packages/server/src/api/routes/rowAction.ts diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts new file mode 100644 index 0000000000..3bea2de73c --- /dev/null +++ b/packages/server/src/api/controllers/rowAction/crud.ts @@ -0,0 +1,15 @@ +export function find() { + throw new Error("Function not implemented.") +} + +export function create() { + throw new Error("Function not implemented.") +} + +export function update() { + throw new Error("Function not implemented.") +} + +export function remove() { + throw new Error("Function not implemented.") +} diff --git a/packages/server/src/api/controllers/rowAction/index.ts b/packages/server/src/api/controllers/rowAction/index.ts new file mode 100644 index 0000000000..bf65119f1a --- /dev/null +++ b/packages/server/src/api/controllers/rowAction/index.ts @@ -0,0 +1,2 @@ +export * from "./crud" +export * from "./run" diff --git a/packages/server/src/api/controllers/rowAction/run.ts b/packages/server/src/api/controllers/rowAction/run.ts new file mode 100644 index 0000000000..06c4b36f86 --- /dev/null +++ b/packages/server/src/api/controllers/rowAction/run.ts @@ -0,0 +1,3 @@ +export function run() { + throw new Error("Function not implemented.") +} diff --git a/packages/server/src/api/routes/rowAction.ts b/packages/server/src/api/routes/rowAction.ts new file mode 100644 index 0000000000..7bc50377b8 --- /dev/null +++ b/packages/server/src/api/routes/rowAction.ts @@ -0,0 +1,41 @@ +import Router from "@koa/router" +import * as rowActionController from "../controllers/rowAction" +import { authorizedResource } from "../../middleware/authorized" + +import { permissions } from "@budibase/backend-core" + +const { PermissionLevel, PermissionType } = permissions + +const router: Router = new Router() + +// CRUD endpoints +router + .get( + "/api/tables/:tableId/actions", + authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"), + rowActionController.find + ) + .post( + "/api/tables/:tableId/actions", + authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"), + rowActionController.create + ) + .put( + "/api/tables/:tableId/actions/:actionId", + authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"), + rowActionController.update + ) + .delete( + "/api/tables/:tableId/actions/:actionId", + authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"), + rowActionController.remove + ) + + // Other endpoints + .post( + "/api/tables/:tableId/actions/:actionId/run", + authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"), + rowActionController.run + ) + +export default router From dfda2f0f548909bf67cd63d729b11612aa964b52 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 10 Jul 2024 13:22:28 +0200 Subject: [PATCH 02/63] Register router --- packages/server/src/api/routes/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/server/src/api/routes/index.ts b/packages/server/src/api/routes/index.ts index 5a42c258cf..2079eb01fd 100644 --- a/packages/server/src/api/routes/index.ts +++ b/packages/server/src/api/routes/index.ts @@ -28,6 +28,7 @@ import opsRoutes from "./ops" import debugRoutes from "./debug" import Router from "@koa/router" import { api as pro } from "@budibase/pro" +import rowActionRoutes from "./rowAction" export { default as staticRoutes } from "./static" export { default as publicRoutes } from "./public" @@ -65,6 +66,7 @@ export const mainRoutes: Router[] = [ opsRoutes, debugRoutes, environmentVariableRoutes, + rowActionRoutes, // these need to be handled last as they still use /api/:tableId // this could be breaking as koa may recognise other routes as this tableRoutes, From 38718968b003079fa5cbfc89b0d8858c95a75971 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 10 Jul 2024 13:24:00 +0200 Subject: [PATCH 03/63] Basic implementation --- .../server/src/api/controllers/rowAction/crud.ts | 16 ++++++++++++++-- packages/types/src/api/web/app/index.ts | 1 + packages/types/src/api/web/app/rowAction.ts | 3 +++ 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 packages/types/src/api/web/app/rowAction.ts diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts index 3bea2de73c..79a86233a8 100644 --- a/packages/server/src/api/controllers/rowAction/crud.ts +++ b/packages/server/src/api/controllers/rowAction/crud.ts @@ -1,9 +1,21 @@ +import { CreateRowActionRequest, Ctx, RowAction } from "@budibase/types" +import sdk from "../../../sdk" + export function find() { throw new Error("Function not implemented.") } -export function create() { - throw new Error("Function not implemented.") +export async function create(ctx: Ctx) { + const { tableId } = ctx.params + + const table = await sdk.tables.getTable(tableId) + if (!table) { + ctx.throw(404) + } + + // TODO + + ctx.status = 201 } export function update() { diff --git a/packages/types/src/api/web/app/index.ts b/packages/types/src/api/web/app/index.ts index cb1cea2b08..55e1428fb9 100644 --- a/packages/types/src/api/web/app/index.ts +++ b/packages/types/src/api/web/app/index.ts @@ -7,3 +7,4 @@ export * from "./table" export * from "./permission" export * from "./attachment" export * from "./user" +export * from "./rowAction" diff --git a/packages/types/src/api/web/app/rowAction.ts b/packages/types/src/api/web/app/rowAction.ts new file mode 100644 index 0000000000..9a7f2a85b1 --- /dev/null +++ b/packages/types/src/api/web/app/rowAction.ts @@ -0,0 +1,3 @@ +export interface CreateRowActionRequest {} + +export interface RowAction {} From de2938799b67ee11ba6bdcaadf04185c33f266d1 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 10 Jul 2024 13:24:25 +0200 Subject: [PATCH 04/63] Initial test --- .../src/api/routes/tests/rowAction.spec.ts | 65 +++++++++++++++++++ .../server/src/tests/utilities/api/index.ts | 3 + .../src/tests/utilities/api/rowAction.ts | 17 +++++ 3 files changed, 85 insertions(+) create mode 100644 packages/server/src/api/routes/tests/rowAction.spec.ts create mode 100644 packages/server/src/tests/utilities/api/rowAction.ts diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts new file mode 100644 index 0000000000..46abadd397 --- /dev/null +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -0,0 +1,65 @@ +import _ from "lodash" +import { Table } from "@budibase/types" +import * as setup from "./utilities" +import { generator } from "@budibase/backend-core/tests" + +describe("/rowsActions", () => { + const config = setup.getConfig() + + let table: Table + + beforeAll(async () => { + await config.init() + + table = await config.api.table.save(setup.structures.basicTable()) + }) + + afterAll(setup.afterAll) + + beforeAll(async () => { + table = await config.api.table.save(setup.structures.basicTable()) + }) + + function unauthorisedTests() { + it("returns unauthorised (401) for unauthenticated requests", async () => { + await config.api.rowAction.save( + table._id!, + {}, + { + status: 401, + body: { + message: "Session not authenticated", + }, + }, + { publicUser: true } + ) + }) + + it("returns forbidden (403) for non-builder users", async () => { + const user = await config.createUser({ + builder: {}, + }) + await config.withUser(user, async () => { + await config.api.rowAction.save(generator.guid(), {}, { status: 403 }) + }) + }) + } + + describe("create", () => { + unauthorisedTests() + + it("rejects when using a non-existing table", async () => { + const res = await config.api.rowAction.save( + table._id!, + {}, + { status: 201 } + ) + + expect(res).toEqual({}) + }) + + it("rejects (404) for a non-existing table", async () => { + await config.api.rowAction.save(generator.guid(), {}, { status: 404 }) + }) + }) +}) diff --git a/packages/server/src/tests/utilities/api/index.ts b/packages/server/src/tests/utilities/api/index.ts index 554fa36588..a19b68a872 100644 --- a/packages/server/src/tests/utilities/api/index.ts +++ b/packages/server/src/tests/utilities/api/index.ts @@ -13,6 +13,7 @@ import { UserAPI } from "./user" import { QueryAPI } from "./query" import { RoleAPI } from "./role" import { TemplateAPI } from "./template" +import { RowActionAPI } from "./rowAction" export default class API { table: TableAPI @@ -29,6 +30,7 @@ export default class API { query: QueryAPI roles: RoleAPI templates: TemplateAPI + rowAction: RowActionAPI constructor(config: TestConfiguration) { this.table = new TableAPI(config) @@ -45,5 +47,6 @@ export default class API { this.query = new QueryAPI(config) this.roles = new RoleAPI(config) this.templates = new TemplateAPI(config) + this.rowAction = new RowActionAPI(config) } } diff --git a/packages/server/src/tests/utilities/api/rowAction.ts b/packages/server/src/tests/utilities/api/rowAction.ts new file mode 100644 index 0000000000..c6b8df5d12 --- /dev/null +++ b/packages/server/src/tests/utilities/api/rowAction.ts @@ -0,0 +1,17 @@ +import { CreateRowActionRequest, Row, RowAction } from "@budibase/types" +import { Expectations, TestAPI } from "./base" + +export class RowActionAPI extends TestAPI { + save = async ( + tableId: string, + rowAction: CreateRowActionRequest, + expectations?: Expectations, + config?: { publicUser?: boolean } + ): Promise => { + return await this._post(`/api/tables/${tableId}/actions`, { + body: rowAction, + expectations, + ...config, + }) + } +} From 0c2024bf6a5186de2ff4f12ef29f37ef935d5f68 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 10 Jul 2024 13:56:41 +0200 Subject: [PATCH 05/63] Basic get --- .../src/api/controllers/rowAction/crud.ts | 27 ++++++++++++++----- .../src/api/routes/tests/rowAction.spec.ts | 20 +++++++++----- .../src/tests/utilities/api/rowAction.ts | 24 +++++++++++++++-- packages/types/src/api/web/app/rowAction.ts | 4 +++ 4 files changed, 59 insertions(+), 16 deletions(-) diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts index 79a86233a8..1e6b4f4671 100644 --- a/packages/server/src/api/controllers/rowAction/crud.ts +++ b/packages/server/src/api/controllers/rowAction/crud.ts @@ -1,17 +1,30 @@ -import { CreateRowActionRequest, Ctx, RowAction } from "@budibase/types" +import { + CreateRowActionRequest, + Ctx, + RowAction, + RowActionsResponse, +} from "@budibase/types" import sdk from "../../../sdk" -export function find() { - throw new Error("Function not implemented.") -} - -export async function create(ctx: Ctx) { +async function getTable(ctx: Ctx) { const { tableId } = ctx.params - const table = await sdk.tables.getTable(tableId) if (!table) { ctx.throw(404) } + return table +} + +export async function find(ctx: Ctx) { + const table = await getTable(ctx) + + // TODO + + ctx.body = { actions: [] } +} + +export async function create(ctx: Ctx) { + const table = await getTable(ctx) // TODO diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index 46abadd397..edf6d64393 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -16,10 +16,6 @@ describe("/rowsActions", () => { afterAll(setup.afterAll) - beforeAll(async () => { - table = await config.api.table.save(setup.structures.basicTable()) - }) - function unauthorisedTests() { it("returns unauthorised (401) for unauthenticated requests", async () => { await config.api.rowAction.save( @@ -43,12 +39,16 @@ describe("/rowsActions", () => { await config.api.rowAction.save(generator.guid(), {}, { status: 403 }) }) }) + + it("rejects (404) for a non-existing table", async () => { + await config.api.rowAction.save(generator.guid(), {}, { status: 404 }) + }) } describe("create", () => { unauthorisedTests() - it("rejects when using a non-existing table", async () => { + it("accepts creating new row actions", async () => { const res = await config.api.rowAction.save( table._id!, {}, @@ -57,9 +57,15 @@ describe("/rowsActions", () => { expect(res).toEqual({}) }) + }) - it("rejects (404) for a non-existing table", async () => { - await config.api.rowAction.save(generator.guid(), {}, { status: 404 }) + describe("find", () => { + unauthorisedTests() + + it("returns empty for tables without row actions", async () => { + const res = await config.api.rowAction.find(table._id!, {}) + + expect(res).toEqual({ actions: [] }) }) }) }) diff --git a/packages/server/src/tests/utilities/api/rowAction.ts b/packages/server/src/tests/utilities/api/rowAction.ts index c6b8df5d12..7a85db21e9 100644 --- a/packages/server/src/tests/utilities/api/rowAction.ts +++ b/packages/server/src/tests/utilities/api/rowAction.ts @@ -1,4 +1,8 @@ -import { CreateRowActionRequest, Row, RowAction } from "@budibase/types" +import { + CreateRowActionRequest, + RowAction, + RowActionsResponse, +} from "@budibase/types" import { Expectations, TestAPI } from "./base" export class RowActionAPI extends TestAPI { @@ -7,11 +11,27 @@ export class RowActionAPI extends TestAPI { rowAction: CreateRowActionRequest, expectations?: Expectations, config?: { publicUser?: boolean } - ): Promise => { + ) => { return await this._post(`/api/tables/${tableId}/actions`, { body: rowAction, expectations, ...config, }) } + + find = async ( + tableId: string, + rowAction: CreateRowActionRequest, + expectations?: Expectations, + config?: { publicUser?: boolean } + ) => { + return await this._get( + `/api/tables/${tableId}/actions`, + { + body: rowAction, + expectations, + ...config, + } + ) + } } diff --git a/packages/types/src/api/web/app/rowAction.ts b/packages/types/src/api/web/app/rowAction.ts index 9a7f2a85b1..d1935a5b51 100644 --- a/packages/types/src/api/web/app/rowAction.ts +++ b/packages/types/src/api/web/app/rowAction.ts @@ -1,3 +1,7 @@ export interface CreateRowActionRequest {} export interface RowAction {} + +export interface RowActionsResponse { + actions: RowAction[] +} From bf161d9d933d62307e5aaba48c70719577dc626c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 10 Jul 2024 15:41:55 +0200 Subject: [PATCH 06/63] More types --- .../src/api/controllers/rowAction/crud.ts | 10 +++-- .../src/api/routes/tests/rowAction.spec.ts | 37 +++++++++++++------ .../src/tests/utilities/api/rowAction.ts | 2 - packages/types/src/api/web/app/rowAction.ts | 9 ++++- 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts index 1e6b4f4671..83b4215f35 100644 --- a/packages/server/src/api/controllers/rowAction/crud.ts +++ b/packages/server/src/api/controllers/rowAction/crud.ts @@ -1,7 +1,6 @@ import { CreateRowActionRequest, Ctx, - RowAction, RowActionsResponse, } from "@budibase/types" import sdk from "../../../sdk" @@ -20,15 +19,18 @@ export async function find(ctx: Ctx) { // TODO - ctx.body = { actions: [] } + ctx.body = { + tableId: table._id!, + actions: [], + } } -export async function create(ctx: Ctx) { +export async function create(ctx: Ctx) { const table = await getTable(ctx) // TODO - ctx.status = 201 + ctx.status = 204 } export function update() { diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index edf6d64393..ac0bff4781 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -1,5 +1,5 @@ import _ from "lodash" -import { Table } from "@budibase/types" +import { CreateRowActionRequest, Table } from "@budibase/types" import * as setup from "./utilities" import { generator } from "@budibase/backend-core/tests" @@ -16,11 +16,17 @@ describe("/rowsActions", () => { afterAll(setup.afterAll) + function createRowActionRequest(): CreateRowActionRequest { + return { + name: generator.word(), + } + } + function unauthorisedTests() { it("returns unauthorised (401) for unauthenticated requests", async () => { await config.api.rowAction.save( table._id!, - {}, + createRowActionRequest(), { status: 401, body: { @@ -36,12 +42,20 @@ describe("/rowsActions", () => { builder: {}, }) await config.withUser(user, async () => { - await config.api.rowAction.save(generator.guid(), {}, { status: 403 }) + await config.api.rowAction.save( + generator.guid(), + createRowActionRequest(), + { status: 403 } + ) }) }) it("rejects (404) for a non-existing table", async () => { - await config.api.rowAction.save(generator.guid(), {}, { status: 404 }) + await config.api.rowAction.save( + generator.guid(), + createRowActionRequest(), + { status: 404 } + ) }) } @@ -49,11 +63,11 @@ describe("/rowsActions", () => { unauthorisedTests() it("accepts creating new row actions", async () => { - const res = await config.api.rowAction.save( - table._id!, - {}, - { status: 201 } - ) + const rowAction = createRowActionRequest() + + const res = await config.api.rowAction.save(table._id!, rowAction, { + status: 204, + }) expect(res).toEqual({}) }) @@ -63,9 +77,10 @@ describe("/rowsActions", () => { unauthorisedTests() it("returns empty for tables without row actions", async () => { - const res = await config.api.rowAction.find(table._id!, {}) + const tableId = table._id! + const res = await config.api.rowAction.find(tableId) - expect(res).toEqual({ actions: [] }) + expect(res).toEqual({ tableId, actions: [] }) }) }) }) diff --git a/packages/server/src/tests/utilities/api/rowAction.ts b/packages/server/src/tests/utilities/api/rowAction.ts index 7a85db21e9..a78633681e 100644 --- a/packages/server/src/tests/utilities/api/rowAction.ts +++ b/packages/server/src/tests/utilities/api/rowAction.ts @@ -21,14 +21,12 @@ export class RowActionAPI extends TestAPI { find = async ( tableId: string, - rowAction: CreateRowActionRequest, expectations?: Expectations, config?: { publicUser?: boolean } ) => { return await this._get( `/api/tables/${tableId}/actions`, { - body: rowAction, expectations, ...config, } diff --git a/packages/types/src/api/web/app/rowAction.ts b/packages/types/src/api/web/app/rowAction.ts index d1935a5b51..fceb606699 100644 --- a/packages/types/src/api/web/app/rowAction.ts +++ b/packages/types/src/api/web/app/rowAction.ts @@ -1,7 +1,12 @@ -export interface CreateRowActionRequest {} +export interface CreateRowActionRequest { + name: string +} -export interface RowAction {} +interface RowAction { + name: string +} export interface RowActionsResponse { + tableId: string actions: RowAction[] } From fe31f88cc8d4a01833c892921b05f474d51a1b8c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 10 Jul 2024 15:48:16 +0200 Subject: [PATCH 07/63] Add validation --- packages/server/src/api/routes/rowAction.ts | 13 ++++++++++++- .../server/src/api/routes/tests/rowAction.spec.ts | 13 +++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/routes/rowAction.ts b/packages/server/src/api/routes/rowAction.ts index 7bc50377b8..18a87cd677 100644 --- a/packages/server/src/api/routes/rowAction.ts +++ b/packages/server/src/api/routes/rowAction.ts @@ -2,10 +2,19 @@ import Router from "@koa/router" import * as rowActionController from "../controllers/rowAction" import { authorizedResource } from "../../middleware/authorized" -import { permissions } from "@budibase/backend-core" +import { middleware, permissions } from "@budibase/backend-core" +import Joi from "joi" const { PermissionLevel, PermissionType } = permissions +export function rowActionValidator() { + return middleware.joiValidator.body( + Joi.object({ + name: Joi.string().required(), + }) + ) +} + const router: Router = new Router() // CRUD endpoints @@ -18,11 +27,13 @@ router .post( "/api/tables/:tableId/actions", authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"), + rowActionValidator(), rowActionController.create ) .put( "/api/tables/:tableId/actions/:actionId", authorizedResource(PermissionType.TABLE, PermissionLevel.READ, "tableId"), + rowActionValidator(), rowActionController.update ) .delete( diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index ac0bff4781..f372938b23 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -71,6 +71,19 @@ describe("/rowsActions", () => { expect(res).toEqual({}) }) + + it("rejects with bad request when creating with no name", async () => { + const rowAction: CreateRowActionRequest = { + name: undefined as any, + } + + await config.api.rowAction.save(table._id!, rowAction, { + status: 400, + body: { + message: 'Invalid body - "name" is required', + }, + }) + }) }) describe("find", () => { From 063eeeb6df0411762a5b6bd561b5d9f8b33570e7 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 10 Jul 2024 15:49:13 +0200 Subject: [PATCH 08/63] Fix --- packages/server/src/api/routes/tests/rowAction.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index f372938b23..ea97e526d5 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -74,13 +74,13 @@ describe("/rowsActions", () => { it("rejects with bad request when creating with no name", async () => { const rowAction: CreateRowActionRequest = { - name: undefined as any, + name: "", } await config.api.rowAction.save(table._id!, rowAction, { status: 400, body: { - message: 'Invalid body - "name" is required', + message: 'Invalid body - "name" is not allowed to be empty', }, }) }) From 7dbfcc398e4c63824a738ea67a948b765652fc12 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Jul 2024 10:04:25 +0200 Subject: [PATCH 09/63] Implement create --- .../src/api/controllers/rowAction/crud.ts | 12 ++++++-- .../src/api/routes/tests/rowAction.spec.ts | 16 ++++++++-- packages/server/src/db/utils.ts | 8 +++++ packages/server/src/sdk/app/rowActions.ts | 30 +++++++++++++++++++ packages/server/src/sdk/index.ts | 2 ++ packages/types/src/api/web/app/rowAction.ts | 8 ++--- packages/types/src/documents/app/index.ts | 1 + .../types/src/documents/app/rowActions.ts | 8 +++++ 8 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 packages/server/src/sdk/app/rowActions.ts create mode 100644 packages/types/src/documents/app/rowActions.ts diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts index 83b4215f35..53a417b223 100644 --- a/packages/server/src/api/controllers/rowAction/crud.ts +++ b/packages/server/src/api/controllers/rowAction/crud.ts @@ -25,12 +25,18 @@ export async function find(ctx: Ctx) { } } -export async function create(ctx: Ctx) { +export async function create( + ctx: Ctx +) { const table = await getTable(ctx) - // TODO + const created = await sdk.rowActions.create(table._id!, ctx.request.body) - ctx.status = 204 + ctx.body = { + tableId: table._id!, + ...created, + } + ctx.status = 201 } export function update() { diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index ea97e526d5..bd7932d352 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -1,4 +1,6 @@ import _ from "lodash" +import tk from "timekeeper" + import { CreateRowActionRequest, Table } from "@budibase/types" import * as setup from "./utilities" import { generator } from "@budibase/backend-core/tests" @@ -9,6 +11,7 @@ describe("/rowsActions", () => { let table: Table beforeAll(async () => { + tk.freeze(new Date()) await config.init() table = await config.api.table.save(setup.structures.basicTable()) @@ -62,14 +65,21 @@ describe("/rowsActions", () => { describe("create", () => { unauthorisedTests() - it("accepts creating new row actions", async () => { + it("accepts creating new row actions for", async () => { const rowAction = createRowActionRequest() const res = await config.api.rowAction.save(table._id!, rowAction, { - status: 204, + status: 201, }) - expect(res).toEqual({}) + expect(res).toEqual({ + _id: `${table._id}_row_actions`, + _rev: expect.stringMatching(/^1-\w+/), + actions: [{ name: rowAction.name }], + tableId: table._id, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }) }) it("rejects with bad request when creating with no name", async () => { diff --git a/packages/server/src/db/utils.ts b/packages/server/src/db/utils.ts index 3bd1749d77..32e72ffad7 100644 --- a/packages/server/src/db/utils.ts +++ b/packages/server/src/db/utils.ts @@ -348,3 +348,11 @@ export function isRelationshipColumn( ): column is RelationshipFieldMetadata { return column.type === FieldType.LINK } + +/** + * Generates a new row actions ID. + * @returns The new row actions ID which the row actions doc can be stored under. + */ +export function generateRowActionsID(tableId: string) { + return `${tableId}${SEPARATOR}row_actions` +} diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts new file mode 100644 index 0000000000..bfc4155289 --- /dev/null +++ b/packages/server/src/sdk/app/rowActions.ts @@ -0,0 +1,30 @@ +import { context } from "@budibase/backend-core" + +import { generateRowActionsID } from "../../db/utils" +import { TableRowActions } from "@budibase/types" + +export async function create(tableId: string, rowAction: { name: string }) { + const db = context.getAppDB() + const rowActionsId = generateRowActionsID(tableId) + let doc: TableRowActions + try { + doc = await db.get(rowActionsId) + } catch (e: any) { + if (e.status !== 404) { + throw e + } + + doc = { _id: rowActionsId, actions: [] } + } + + doc.actions.push(rowAction) + await db.put(doc) + + return await get(tableId) +} + +export async function get(tableId: string) { + const db = context.getAppDB() + const rowActionsId = generateRowActionsID(tableId) + return await db.get(rowActionsId) +} diff --git a/packages/server/src/sdk/index.ts b/packages/server/src/sdk/index.ts index c3057e3d4f..a871546b60 100644 --- a/packages/server/src/sdk/index.ts +++ b/packages/server/src/sdk/index.ts @@ -10,6 +10,7 @@ import { default as users } from "./users" import { default as plugins } from "./plugins" import * as views from "./app/views" import * as permissions from "./app/permissions" +import * as rowActions from "./app/rowActions" const sdk = { backups, @@ -24,6 +25,7 @@ const sdk = { views, permissions, links, + rowActions, } // default export for TS diff --git a/packages/types/src/api/web/app/rowAction.ts b/packages/types/src/api/web/app/rowAction.ts index fceb606699..fd42de20f8 100644 --- a/packages/types/src/api/web/app/rowAction.ts +++ b/packages/types/src/api/web/app/rowAction.ts @@ -2,11 +2,9 @@ export interface CreateRowActionRequest { name: string } -interface RowAction { - name: string -} - export interface RowActionsResponse { tableId: string - actions: RowAction[] + actions: { + name: string + }[] } diff --git a/packages/types/src/documents/app/index.ts b/packages/types/src/documents/app/index.ts index 3809fba6e5..f6726fd53c 100644 --- a/packages/types/src/documents/app/index.ts +++ b/packages/types/src/documents/app/index.ts @@ -16,3 +16,4 @@ export * from "./links" export * from "./component" export * from "./sqlite" export * from "./snippet" +export * from "./rowActions" diff --git a/packages/types/src/documents/app/rowActions.ts b/packages/types/src/documents/app/rowActions.ts new file mode 100644 index 0000000000..d6dea34f2e --- /dev/null +++ b/packages/types/src/documents/app/rowActions.ts @@ -0,0 +1,8 @@ +import { Document } from "../document" + +export interface TableRowActions extends Document { + _id: string + actions: { + name: string + }[] +} From 0831b7cf3fb6fc206fd4f0f45ec60018145d89bb Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Jul 2024 10:13:28 +0200 Subject: [PATCH 10/63] Add extra tests --- .../src/api/routes/tests/rowAction.spec.ts | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index bd7932d352..e817259315 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -1,20 +1,23 @@ import _ from "lodash" import tk from "timekeeper" -import { CreateRowActionRequest, Table } from "@budibase/types" +import { CreateRowActionRequest } from "@budibase/types" import * as setup from "./utilities" import { generator } from "@budibase/backend-core/tests" describe("/rowsActions", () => { const config = setup.getConfig() - let table: Table + let tableId: string beforeAll(async () => { tk.freeze(new Date()) await config.init() + }) - table = await config.api.table.save(setup.structures.basicTable()) + beforeEach(async () => { + const table = await config.api.table.save(setup.structures.basicTable()) + tableId = table._id! }) afterAll(setup.afterAll) @@ -28,7 +31,7 @@ describe("/rowsActions", () => { function unauthorisedTests() { it("returns unauthorised (401) for unauthenticated requests", async () => { await config.api.rowAction.save( - table._id!, + tableId, createRowActionRequest(), { status: 401, @@ -65,18 +68,41 @@ describe("/rowsActions", () => { describe("create", () => { unauthorisedTests() - it("accepts creating new row actions for", async () => { + it("creates new row actions for tables without existing actions", async () => { const rowAction = createRowActionRequest() - const res = await config.api.rowAction.save(table._id!, rowAction, { + const res = await config.api.rowAction.save(tableId, rowAction, { status: 201, }) expect(res).toEqual({ - _id: `${table._id}_row_actions`, + _id: `${tableId}_row_actions`, _rev: expect.stringMatching(/^1-\w+/), actions: [{ name: rowAction.name }], - tableId: table._id, + tableId: tableId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }) + }) + + it("can create multiple row actions for the same tables", async () => { + const rowActions = generator.unique(() => createRowActionRequest(), 3) + + await config.api.rowAction.save(tableId, rowActions[0], { + status: 201, + }) + await config.api.rowAction.save(tableId, rowActions[1], { + status: 201, + }) + const res = await config.api.rowAction.save(tableId, rowActions[2], { + status: 201, + }) + + expect(res).toEqual({ + _id: `${tableId}_row_actions`, + _rev: expect.stringMatching(/^3-\w+/), + actions: rowActions, + tableId: tableId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }) @@ -87,7 +113,7 @@ describe("/rowsActions", () => { name: "", } - await config.api.rowAction.save(table._id!, rowAction, { + await config.api.rowAction.save(tableId, rowAction, { status: 400, body: { message: 'Invalid body - "name" is not allowed to be empty', @@ -100,7 +126,6 @@ describe("/rowsActions", () => { unauthorisedTests() it("returns empty for tables without row actions", async () => { - const tableId = table._id! const res = await config.api.rowAction.find(tableId) expect(res).toEqual({ tableId, actions: [] }) From 645abea2cdb4e27f53829d7636bbb7f3117c0f9e Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Jul 2024 10:19:11 +0200 Subject: [PATCH 11/63] Add extra tests --- .../src/api/routes/tests/rowAction.spec.ts | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index e817259315..6281023991 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -85,7 +85,7 @@ describe("/rowsActions", () => { }) }) - it("can create multiple row actions for the same tables", async () => { + it("can create multiple row actions for the same table", async () => { const rowActions = generator.unique(() => createRowActionRequest(), 3) await config.api.rowAction.save(tableId, rowActions[0], { @@ -108,6 +108,41 @@ describe("/rowsActions", () => { }) }) + it("can create row actions for different tables", async () => { + const otherTable = await config.api.table.save( + setup.structures.basicTable() + ) + const otherTableId = otherTable._id! + + const rowAction1 = createRowActionRequest() + const rowAction2 = createRowActionRequest() + + const res1 = await config.api.rowAction.save(tableId, rowAction1, { + status: 201, + }) + const res2 = await config.api.rowAction.save(otherTableId, rowAction2, { + status: 201, + }) + + expect(res1).toEqual({ + _id: `${tableId}_row_actions`, + _rev: expect.stringMatching(/^1-\w+/), + actions: [rowAction1], + tableId: tableId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }) + + expect(res2).toEqual({ + _id: `${otherTableId}_row_actions`, + _rev: expect.stringMatching(/^1-\w+/), + actions: [rowAction2], + tableId: otherTableId, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }) + }) + it("rejects with bad request when creating with no name", async () => { const rowAction: CreateRowActionRequest = { name: "", From fac9c35bce61017fdc081383b249449fd2e83e99 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Jul 2024 10:46:29 +0200 Subject: [PATCH 12/63] Simplify tests --- .../src/api/routes/tests/rowAction.spec.ts | 75 +++++++++++-------- .../src/tests/utilities/api/rowAction.ts | 19 +++-- 2 files changed, 54 insertions(+), 40 deletions(-) diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index 6281023991..ebf0e76c09 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -4,6 +4,7 @@ import tk from "timekeeper" import { CreateRowActionRequest } from "@budibase/types" import * as setup from "./utilities" import { generator } from "@budibase/backend-core/tests" +import { Expectations } from "src/tests/utilities/api/base" describe("/rowsActions", () => { const config = setup.getConfig() @@ -22,6 +23,23 @@ describe("/rowsActions", () => { afterAll(setup.afterAll) + async function createRowAction( + tableId: string, + rowAction: CreateRowActionRequest, + expectations?: Expectations, + opts?: { publicUser?: boolean } + ) { + return await config.api.rowAction.save( + tableId, + rowAction, + { + ...expectations, + status: expectations?.status || 201, + }, + opts + ) + } + function createRowActionRequest(): CreateRowActionRequest { return { name: generator.word(), @@ -30,7 +48,7 @@ describe("/rowsActions", () => { function unauthorisedTests() { it("returns unauthorised (401) for unauthenticated requests", async () => { - await config.api.rowAction.save( + await createRowAction( tableId, createRowActionRequest(), { @@ -48,20 +66,16 @@ describe("/rowsActions", () => { builder: {}, }) await config.withUser(user, async () => { - await config.api.rowAction.save( - generator.guid(), - createRowActionRequest(), - { status: 403 } - ) + await createRowAction(generator.guid(), createRowActionRequest(), { + status: 403, + }) }) }) it("rejects (404) for a non-existing table", async () => { - await config.api.rowAction.save( - generator.guid(), - createRowActionRequest(), - { status: 404 } - ) + await createRowAction(generator.guid(), createRowActionRequest(), { + status: 404, + }) }) } @@ -71,9 +85,7 @@ describe("/rowsActions", () => { it("creates new row actions for tables without existing actions", async () => { const rowAction = createRowActionRequest() - const res = await config.api.rowAction.save(tableId, rowAction, { - status: 201, - }) + const res = await createRowAction(tableId, rowAction, { status: 201 }) expect(res).toEqual({ _id: `${tableId}_row_actions`, @@ -88,15 +100,9 @@ describe("/rowsActions", () => { it("can create multiple row actions for the same table", async () => { const rowActions = generator.unique(() => createRowActionRequest(), 3) - await config.api.rowAction.save(tableId, rowActions[0], { - status: 201, - }) - await config.api.rowAction.save(tableId, rowActions[1], { - status: 201, - }) - const res = await config.api.rowAction.save(tableId, rowActions[2], { - status: 201, - }) + await createRowAction(tableId, rowActions[0]) + await createRowAction(tableId, rowActions[1]) + const res = await createRowAction(tableId, rowActions[2]) expect(res).toEqual({ _id: `${tableId}_row_actions`, @@ -117,12 +123,8 @@ describe("/rowsActions", () => { const rowAction1 = createRowActionRequest() const rowAction2 = createRowActionRequest() - const res1 = await config.api.rowAction.save(tableId, rowAction1, { - status: 201, - }) - const res2 = await config.api.rowAction.save(otherTableId, rowAction2, { - status: 201, - }) + const res1 = await createRowAction(tableId, rowAction1) + const res2 = await createRowAction(otherTableId, rowAction2) expect(res1).toEqual({ _id: `${tableId}_row_actions`, @@ -148,7 +150,7 @@ describe("/rowsActions", () => { name: "", } - await config.api.rowAction.save(tableId, rowAction, { + await createRowAction(tableId, rowAction, { status: 400, body: { message: 'Invalid body - "name" is not allowed to be empty', @@ -165,5 +167,18 @@ describe("/rowsActions", () => { expect(res).toEqual({ tableId, actions: [] }) }) + + it("returns only the", async () => { + const rowActions = generator.unique(() => createRowActionRequest(), 5) + for (const rowAction of rowActions) { + await createRowAction(tableId, rowAction) + } + + const otherTable = await config.api.table.save( + setup.structures.basicTable() + ) + const otherTableId = otherTable._id! + await createRowAction(otherTableId, createRowActionRequest()) + }) }) }) diff --git a/packages/server/src/tests/utilities/api/rowAction.ts b/packages/server/src/tests/utilities/api/rowAction.ts index a78633681e..583475b666 100644 --- a/packages/server/src/tests/utilities/api/rowAction.ts +++ b/packages/server/src/tests/utilities/api/rowAction.ts @@ -1,8 +1,4 @@ -import { - CreateRowActionRequest, - RowAction, - RowActionsResponse, -} from "@budibase/types" +import { CreateRowActionRequest, RowActionsResponse } from "@budibase/types" import { Expectations, TestAPI } from "./base" export class RowActionAPI extends TestAPI { @@ -12,11 +8,14 @@ export class RowActionAPI extends TestAPI { expectations?: Expectations, config?: { publicUser?: boolean } ) => { - return await this._post(`/api/tables/${tableId}/actions`, { - body: rowAction, - expectations, - ...config, - }) + return await this._post( + `/api/tables/${tableId}/actions`, + { + body: rowAction, + expectations, + ...config, + } + ) } find = async ( From c565e35b5309350e41ce6bc9f6735c49ece38efc Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Jul 2024 10:59:11 +0200 Subject: [PATCH 13/63] Implement find --- .../src/api/controllers/rowAction/crud.ts | 4 ++-- .../src/api/routes/tests/rowAction.spec.ts | 20 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts index 53a417b223..e58a3030fc 100644 --- a/packages/server/src/api/controllers/rowAction/crud.ts +++ b/packages/server/src/api/controllers/rowAction/crud.ts @@ -17,11 +17,11 @@ async function getTable(ctx: Ctx) { export async function find(ctx: Ctx) { const table = await getTable(ctx) - // TODO + const actions = await sdk.rowActions.get(table._id!) ctx.body = { tableId: table._id!, - actions: [], + ...actions, } } diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index ebf0e76c09..cec0410282 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -162,13 +162,7 @@ describe("/rowsActions", () => { describe("find", () => { unauthorisedTests() - it("returns empty for tables without row actions", async () => { - const res = await config.api.rowAction.find(tableId) - - expect(res).toEqual({ tableId, actions: [] }) - }) - - it("returns only the", async () => { + it("returns only the actions for the requested table", async () => { const rowActions = generator.unique(() => createRowActionRequest(), 5) for (const rowAction of rowActions) { await createRowAction(tableId, rowAction) @@ -179,6 +173,18 @@ describe("/rowsActions", () => { ) const otherTableId = otherTable._id! await createRowAction(otherTableId, createRowActionRequest()) + + const response = await config.api.rowAction.find(tableId) + expect(response).toEqual( + expect.objectContaining({ + tableId, + actions: rowActions, + }) + ) + }) + + it("returns 404 for tables without row actions", async () => { + await config.api.rowAction.find(tableId, { status: 404 }) }) }) }) From 2d8361d6fded6f5ea3200978fa08233247f92707 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Jul 2024 11:06:36 +0200 Subject: [PATCH 14/63] Always return when table exists --- packages/server/src/api/controllers/rowAction/crud.ts | 9 ++++++++- packages/server/src/api/routes/tests/rowAction.spec.ts | 10 ++++++++-- packages/server/src/sdk/app/rowActions.ts | 7 +++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts index e58a3030fc..b54a4966bf 100644 --- a/packages/server/src/api/controllers/rowAction/crud.ts +++ b/packages/server/src/api/controllers/rowAction/crud.ts @@ -17,8 +17,15 @@ async function getTable(ctx: Ctx) { export async function find(ctx: Ctx) { const table = await getTable(ctx) - const actions = await sdk.rowActions.get(table._id!) + if (!(await sdk.rowActions.docExists(table._id!))) { + ctx.body = { + tableId: table._id!, + actions: [], + } + return + } + const actions = await sdk.rowActions.get(table._id!) ctx.body = { tableId: table._id!, ...actions, diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index cec0410282..b2f322c01f 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -183,8 +183,14 @@ describe("/rowsActions", () => { ) }) - it("returns 404 for tables without row actions", async () => { - await config.api.rowAction.find(tableId, { status: 404 }) + it("returns empty for tables without row actions", async () => { + const response = await config.api.rowAction.find(tableId) + expect(response).toEqual( + expect.objectContaining({ + tableId, + actions: [], + }) + ) }) }) }) diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts index bfc4155289..6d1a98d052 100644 --- a/packages/server/src/sdk/app/rowActions.ts +++ b/packages/server/src/sdk/app/rowActions.ts @@ -28,3 +28,10 @@ export async function get(tableId: string) { const rowActionsId = generateRowActionsID(tableId) return await db.get(rowActionsId) } + +export async function docExists(tableId: string) { + const db = context.getAppDB() + const rowActionsId = generateRowActionsID(tableId) + const result = await db.exists(rowActionsId) + return result +} From 65d7656097935a6593408e9fae0cc3dce2a4c554 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Jul 2024 15:27:48 +0200 Subject: [PATCH 15/63] Unify newid --- packages/server/src/api/controllers/deploy/Deployment.ts | 5 ++--- packages/server/src/automations/utils.ts | 5 ++--- packages/server/src/db/inMemoryView.ts | 5 ++--- packages/server/src/db/newid.ts | 5 ----- packages/server/src/db/utils.ts | 5 +++-- packages/server/src/tests/utilities/TestConfiguration.ts | 4 +++- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 packages/server/src/db/newid.ts diff --git a/packages/server/src/api/controllers/deploy/Deployment.ts b/packages/server/src/api/controllers/deploy/Deployment.ts index 611c82f28b..fe817730b6 100644 --- a/packages/server/src/api/controllers/deploy/Deployment.ts +++ b/packages/server/src/api/controllers/deploy/Deployment.ts @@ -1,5 +1,4 @@ -import newid from "../../../db/newid" -import { context } from "@budibase/backend-core" +import { context, utils } from "@budibase/backend-core" /** * This is used to pass around information about the deployment that is occurring @@ -12,7 +11,7 @@ export default class Deployment { appUrl?: string constructor(id = null) { - this._id = id || newid() + this._id = id || utils.newid() } setVerification(verification: any) { diff --git a/packages/server/src/automations/utils.ts b/packages/server/src/automations/utils.ts index 4d7e169f52..784632b626 100644 --- a/packages/server/src/automations/utils.ts +++ b/packages/server/src/automations/utils.ts @@ -1,10 +1,9 @@ import { Thread, ThreadType } from "../threads" import { definitions } from "./triggerInfo" import { automationQueue } from "./bullboard" -import newid from "../db/newid" import { updateEntityMetadata } from "../utilities" import { MetadataTypes } from "../constants" -import { db as dbCore, context } from "@budibase/backend-core" +import { db as dbCore, context, utils } from "@budibase/backend-core" import { getAutomationMetadataParams } from "../db/utils" import { cloneDeep } from "lodash/fp" import { quotas } from "@budibase/pro" @@ -207,7 +206,7 @@ export async function enableCronTrigger(appId: any, automation: Automation) { ) } // make a job id rather than letting Bull decide, makes it easier to handle on way out - const jobId = `${appId}_cron_${newid()}` + const jobId = `${appId}_cron_${utils.newid()}` const job: any = await automationQueue.add( { automation, diff --git a/packages/server/src/db/inMemoryView.ts b/packages/server/src/db/inMemoryView.ts index 73e5c622eb..525c4b456e 100644 --- a/packages/server/src/db/inMemoryView.ts +++ b/packages/server/src/db/inMemoryView.ts @@ -1,9 +1,8 @@ -import newid from "./newid" import { Row, Document, DBView } from "@budibase/types" // bypass the main application db config // use in memory pouchdb directly -import { db as dbCore } from "@budibase/backend-core" +import { db as dbCore, utils } from "@budibase/backend-core" const Pouch = dbCore.getPouch({ inMemory: true }) @@ -16,7 +15,7 @@ export async function runView( // use a different ID each time for the DB, make sure they // are always unique for each query, don't want overlap // which could cause 409s - const db = new Pouch(newid()) + const db = new Pouch(utils.newid()) try { // write all the docs to the in memory Pouch (remove revs) await db.bulkDocs( diff --git a/packages/server/src/db/newid.ts b/packages/server/src/db/newid.ts deleted file mode 100644 index bc8f3bb04b..0000000000 --- a/packages/server/src/db/newid.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { v4 } from "uuid" - -export default function (): string { - return v4().replace(/-/g, "") -} diff --git a/packages/server/src/db/utils.ts b/packages/server/src/db/utils.ts index 32e72ffad7..3ee787b50d 100644 --- a/packages/server/src/db/utils.ts +++ b/packages/server/src/db/utils.ts @@ -1,5 +1,4 @@ -import newid from "./newid" -import { context, db as dbCore } from "@budibase/backend-core" +import { context, db as dbCore, utils } from "@budibase/backend-core" import { DatabaseQueryOpts, Datasource, @@ -15,6 +14,8 @@ import { export { DocumentType, VirtualDocumentType } from "@budibase/types" +const newid = utils.newid + type Optional = string | null export const enum AppStatus { diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index 828b389add..3d53149385 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -26,6 +26,7 @@ import { roles, sessions, tenancy, + utils, } from "@budibase/backend-core" import { app as appController, @@ -40,7 +41,6 @@ import { } from "./controllers" import { cleanup } from "../../utilities/fileSystem" -import newid from "../../db/newid" import { generateUserMetadataID } from "../../db/utils" import { startup } from "../../startup" import supertest from "supertest" @@ -74,6 +74,8 @@ import { cloneDeep } from "lodash" import jwt, { Secret } from "jsonwebtoken" import { Server } from "http" +const newid = utils.newid + mocks.licenses.init(pro) // use unlimited license by default From 3bcbb57baaa3d71a8f8e455e08771f9e0ce53479 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Jul 2024 15:32:25 +0200 Subject: [PATCH 16/63] Add id to each individual action --- .../src/api/routes/tests/rowAction.spec.ts | 31 ++++++++++++++++--- packages/server/src/sdk/app/rowActions.ts | 7 +++-- packages/types/src/api/web/app/rowAction.ts | 1 + .../types/src/documents/app/rowActions.ts | 1 + 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index b2f322c01f..e459c45185 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -90,7 +90,12 @@ describe("/rowsActions", () => { expect(res).toEqual({ _id: `${tableId}_row_actions`, _rev: expect.stringMatching(/^1-\w+/), - actions: [{ name: rowAction.name }], + actions: [ + { + id: expect.any(String), + name: rowAction.name, + }, + ], tableId: tableId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -107,7 +112,10 @@ describe("/rowsActions", () => { expect(res).toEqual({ _id: `${tableId}_row_actions`, _rev: expect.stringMatching(/^3-\w+/), - actions: rowActions, + actions: rowActions.map(a => ({ + id: expect.any(String), + ...a, + })), tableId: tableId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -129,7 +137,12 @@ describe("/rowsActions", () => { expect(res1).toEqual({ _id: `${tableId}_row_actions`, _rev: expect.stringMatching(/^1-\w+/), - actions: [rowAction1], + actions: [ + { + id: expect.any(String), + ...rowAction1, + }, + ], tableId: tableId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -138,7 +151,12 @@ describe("/rowsActions", () => { expect(res2).toEqual({ _id: `${otherTableId}_row_actions`, _rev: expect.stringMatching(/^1-\w+/), - actions: [rowAction2], + actions: [ + { + id: expect.any(String), + ...rowAction2, + }, + ], tableId: otherTableId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -178,7 +196,10 @@ describe("/rowsActions", () => { expect(response).toEqual( expect.objectContaining({ tableId, - actions: rowActions, + actions: rowActions.map(a => ({ + id: expect.any(String), + ...a, + })), }) ) }) diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts index 6d1a98d052..c283c183b8 100644 --- a/packages/server/src/sdk/app/rowActions.ts +++ b/packages/server/src/sdk/app/rowActions.ts @@ -1,4 +1,4 @@ -import { context } from "@budibase/backend-core" +import { context, utils } from "@budibase/backend-core" import { generateRowActionsID } from "../../db/utils" import { TableRowActions } from "@budibase/types" @@ -17,7 +17,10 @@ export async function create(tableId: string, rowAction: { name: string }) { doc = { _id: rowActionsId, actions: [] } } - doc.actions.push(rowAction) + doc.actions.push({ + id: utils.newid(), + ...rowAction, + }) await db.put(doc) return await get(tableId) diff --git a/packages/types/src/api/web/app/rowAction.ts b/packages/types/src/api/web/app/rowAction.ts index fd42de20f8..dc2731f9b6 100644 --- a/packages/types/src/api/web/app/rowAction.ts +++ b/packages/types/src/api/web/app/rowAction.ts @@ -5,6 +5,7 @@ export interface CreateRowActionRequest { export interface RowActionsResponse { tableId: string actions: { + id: string name: string }[] } diff --git a/packages/types/src/documents/app/rowActions.ts b/packages/types/src/documents/app/rowActions.ts index d6dea34f2e..5eaf5de5d6 100644 --- a/packages/types/src/documents/app/rowActions.ts +++ b/packages/types/src/documents/app/rowActions.ts @@ -3,6 +3,7 @@ import { Document } from "../document" export interface TableRowActions extends Document { _id: string actions: { + id: string name: string }[] } From de04a6f76debf3a10bd2cd7ae05a99346c8c1ab6 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Jul 2024 15:34:48 +0200 Subject: [PATCH 17/63] Change id --- packages/server/src/api/routes/tests/rowAction.spec.ts | 8 ++++---- packages/server/src/db/utils.ts | 2 +- packages/types/src/documents/document.ts | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index e459c45185..a241a23c6b 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -88,7 +88,7 @@ describe("/rowsActions", () => { const res = await createRowAction(tableId, rowAction, { status: 201 }) expect(res).toEqual({ - _id: `${tableId}_row_actions`, + _id: `ra_${tableId}`, _rev: expect.stringMatching(/^1-\w+/), actions: [ { @@ -110,7 +110,7 @@ describe("/rowsActions", () => { const res = await createRowAction(tableId, rowActions[2]) expect(res).toEqual({ - _id: `${tableId}_row_actions`, + _id: `ra_${tableId}`, _rev: expect.stringMatching(/^3-\w+/), actions: rowActions.map(a => ({ id: expect.any(String), @@ -135,7 +135,7 @@ describe("/rowsActions", () => { const res2 = await createRowAction(otherTableId, rowAction2) expect(res1).toEqual({ - _id: `${tableId}_row_actions`, + _id: `ra_${tableId}`, _rev: expect.stringMatching(/^1-\w+/), actions: [ { @@ -149,7 +149,7 @@ describe("/rowsActions", () => { }) expect(res2).toEqual({ - _id: `${otherTableId}_row_actions`, + _id: `ra_${otherTableId}`, _rev: expect.stringMatching(/^1-\w+/), actions: [ { diff --git a/packages/server/src/db/utils.ts b/packages/server/src/db/utils.ts index 3ee787b50d..e3fe945863 100644 --- a/packages/server/src/db/utils.ts +++ b/packages/server/src/db/utils.ts @@ -355,5 +355,5 @@ export function isRelationshipColumn( * @returns The new row actions ID which the row actions doc can be stored under. */ export function generateRowActionsID(tableId: string) { - return `${tableId}${SEPARATOR}row_actions` + return `${DocumentType.ROW_ACTIONS}${SEPARATOR}${tableId}` } diff --git a/packages/types/src/documents/document.ts b/packages/types/src/documents/document.ts index 0de4337f4b..23aec05ee5 100644 --- a/packages/types/src/documents/document.ts +++ b/packages/types/src/documents/document.ts @@ -39,6 +39,7 @@ export enum DocumentType { AUDIT_LOG = "al", APP_MIGRATION_METADATA = "_design/migrations", SCIM_LOG = "scimlog", + ROW_ACTIONS = "ra", } // these are the core documents that make up the data, design From 17fc605e4f6b61c376fe7305f7d60a3b1cc4b346 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Jul 2024 16:57:32 +0200 Subject: [PATCH 18/63] Persist as object instead of array --- .../src/api/controllers/rowAction/crud.ts | 13 ++- .../src/api/routes/tests/rowAction.spec.ts | 110 +++++++----------- packages/server/src/sdk/app/rowActions.ts | 19 +-- .../src/tests/utilities/api/rowAction.ts | 8 +- packages/types/src/api/web/app/rowAction.ts | 16 ++- packages/types/src/documents/app/index.ts | 2 +- .../app/{rowActions.ts => rowAction.ts} | 10 +- packages/types/src/documents/document.ts | 1 + 8 files changed, 85 insertions(+), 94 deletions(-) rename packages/types/src/documents/app/{rowActions.ts => rowAction.ts} (62%) diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts index b54a4966bf..7f3123e120 100644 --- a/packages/server/src/api/controllers/rowAction/crud.ts +++ b/packages/server/src/api/controllers/rowAction/crud.ts @@ -1,6 +1,7 @@ import { CreateRowActionRequest, Ctx, + RowActionResponse, RowActionsResponse, } from "@budibase/types" import sdk from "../../../sdk" @@ -20,7 +21,7 @@ export async function find(ctx: Ctx) { if (!(await sdk.rowActions.docExists(table._id!))) { ctx.body = { tableId: table._id!, - actions: [], + actions: {}, } return } @@ -33,15 +34,19 @@ export async function find(ctx: Ctx) { } export async function create( - ctx: Ctx + ctx: Ctx ) { const table = await getTable(ctx) - const created = await sdk.rowActions.create(table._id!, ctx.request.body) + const { id, ...createdAction } = await sdk.rowActions.create( + table._id!, + ctx.request.body + ) ctx.body = { tableId: table._id!, - ...created, + actionId: id, + ...createdAction, } ctx.status = 201 } diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index a241a23c6b..bee6964956 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -1,7 +1,7 @@ import _ from "lodash" import tk from "timekeeper" -import { CreateRowActionRequest } from "@budibase/types" +import { CreateRowActionRequest, RowActionResponse } from "@budibase/types" import * as setup from "./utilities" import { generator } from "@budibase/backend-core/tests" import { Expectations } from "src/tests/utilities/api/base" @@ -46,6 +46,12 @@ describe("/rowsActions", () => { } } + function createRowActionRequests(count: number): CreateRowActionRequest[] { + return generator + .unique(() => generator.word(), count) + .map(name => ({ name })) + } + function unauthorisedTests() { it("returns unauthorised (401) for unauthenticated requests", async () => { await createRowAction( @@ -84,85 +90,49 @@ describe("/rowsActions", () => { it("creates new row actions for tables without existing actions", async () => { const rowAction = createRowActionRequest() - - const res = await createRowAction(tableId, rowAction, { status: 201 }) + const res = await createRowAction(tableId, rowAction, { + status: 201, + }) expect(res).toEqual({ + tableId: tableId, + actionId: expect.stringMatching(/^row_action_\w+/), + ...rowAction, + }) + + expect(await config.api.rowAction.find(tableId)).toEqual({ _id: `ra_${tableId}`, _rev: expect.stringMatching(/^1-\w+/), - actions: [ - { - id: expect.any(String), - name: rowAction.name, - }, - ], tableId: tableId, + actions: { + [res.actionId]: rowAction, + }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }) }) it("can create multiple row actions for the same table", async () => { - const rowActions = generator.unique(() => createRowActionRequest(), 3) + const rowActions = createRowActionRequests(3) + const responses: RowActionResponse[] = [] + for (const action of rowActions) { + responses.push(await createRowAction(tableId, action)) + } - await createRowAction(tableId, rowActions[0]) - await createRowAction(tableId, rowActions[1]) - const res = await createRowAction(tableId, rowActions[2]) - - expect(res).toEqual({ + expect(await config.api.rowAction.find(tableId)).toEqual({ _id: `ra_${tableId}`, _rev: expect.stringMatching(/^3-\w+/), - actions: rowActions.map(a => ({ - id: expect.any(String), - ...a, - })), + actions: { + [responses[0].actionId]: rowActions[0], + [responses[1].actionId]: rowActions[1], + [responses[2].actionId]: rowActions[2], + }, tableId: tableId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }) }) - it("can create row actions for different tables", async () => { - const otherTable = await config.api.table.save( - setup.structures.basicTable() - ) - const otherTableId = otherTable._id! - - const rowAction1 = createRowActionRequest() - const rowAction2 = createRowActionRequest() - - const res1 = await createRowAction(tableId, rowAction1) - const res2 = await createRowAction(otherTableId, rowAction2) - - expect(res1).toEqual({ - _id: `ra_${tableId}`, - _rev: expect.stringMatching(/^1-\w+/), - actions: [ - { - id: expect.any(String), - ...rowAction1, - }, - ], - tableId: tableId, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }) - - expect(res2).toEqual({ - _id: `ra_${otherTableId}`, - _rev: expect.stringMatching(/^1-\w+/), - actions: [ - { - id: expect.any(String), - ...rowAction2, - }, - ], - tableId: otherTableId, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }) - }) - it("rejects with bad request when creating with no name", async () => { const rowAction: CreateRowActionRequest = { name: "", @@ -181,25 +151,25 @@ describe("/rowsActions", () => { unauthorisedTests() it("returns only the actions for the requested table", async () => { - const rowActions = generator.unique(() => createRowActionRequest(), 5) - for (const rowAction of rowActions) { - await createRowAction(tableId, rowAction) + const rowActions: RowActionResponse[] = [] + for (const action of createRowActionRequests(3)) { + rowActions.push(await createRowAction(tableId, action)) } const otherTable = await config.api.table.save( setup.structures.basicTable() ) - const otherTableId = otherTable._id! - await createRowAction(otherTableId, createRowActionRequest()) + await createRowAction(otherTable._id!, createRowActionRequest()) const response = await config.api.rowAction.find(tableId) expect(response).toEqual( expect.objectContaining({ tableId, - actions: rowActions.map(a => ({ - id: expect.any(String), - ...a, - })), + actions: { + [rowActions[0].actionId]: expect.any(Object), + [rowActions[1].actionId]: expect.any(Object), + [rowActions[2].actionId]: expect.any(Object), + }, }) ) }) @@ -209,7 +179,7 @@ describe("/rowsActions", () => { expect(response).toEqual( expect.objectContaining({ tableId, - actions: [], + actions: {}, }) ) }) diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts index c283c183b8..fa60a075c9 100644 --- a/packages/server/src/sdk/app/rowActions.ts +++ b/packages/server/src/sdk/app/rowActions.ts @@ -1,7 +1,11 @@ import { context, utils } from "@budibase/backend-core" import { generateRowActionsID } from "../../db/utils" -import { TableRowActions } from "@budibase/types" +import { + SEPARATOR, + TableRowActions, + VirtualDocumentType, +} from "@budibase/types" export async function create(tableId: string, rowAction: { name: string }) { const db = context.getAppDB() @@ -14,16 +18,17 @@ export async function create(tableId: string, rowAction: { name: string }) { throw e } - doc = { _id: rowActionsId, actions: [] } + doc = { _id: rowActionsId, actions: {} } } - doc.actions.push({ - id: utils.newid(), - ...rowAction, - }) + const newId = `${VirtualDocumentType.ROW_ACTION}${SEPARATOR}${utils.newid()}` + doc.actions[newId] = rowAction await db.put(doc) - return await get(tableId) + return { + id: newId, + ...rowAction, + } } export async function get(tableId: string) { diff --git a/packages/server/src/tests/utilities/api/rowAction.ts b/packages/server/src/tests/utilities/api/rowAction.ts index 583475b666..a7c3932bc4 100644 --- a/packages/server/src/tests/utilities/api/rowAction.ts +++ b/packages/server/src/tests/utilities/api/rowAction.ts @@ -1,4 +1,8 @@ -import { CreateRowActionRequest, RowActionsResponse } from "@budibase/types" +import { + CreateRowActionRequest, + RowActionResponse, + RowActionsResponse, +} from "@budibase/types" import { Expectations, TestAPI } from "./base" export class RowActionAPI extends TestAPI { @@ -8,7 +12,7 @@ export class RowActionAPI extends TestAPI { expectations?: Expectations, config?: { publicUser?: boolean } ) => { - return await this._post( + return await this._post( `/api/tables/${tableId}/actions`, { body: rowAction, diff --git a/packages/types/src/api/web/app/rowAction.ts b/packages/types/src/api/web/app/rowAction.ts index dc2731f9b6..3219a5e4b7 100644 --- a/packages/types/src/api/web/app/rowAction.ts +++ b/packages/types/src/api/web/app/rowAction.ts @@ -1,11 +1,15 @@ -export interface CreateRowActionRequest { - name: string +export interface CreateRowActionRequest extends RowActionData {} + +export interface RowActionResponse extends RowActionData { + tableId: string + actionId: string } export interface RowActionsResponse { tableId: string - actions: { - id: string - name: string - }[] + actions: Record +} + +interface RowActionData { + name: string } diff --git a/packages/types/src/documents/app/index.ts b/packages/types/src/documents/app/index.ts index f6726fd53c..2b13676ba1 100644 --- a/packages/types/src/documents/app/index.ts +++ b/packages/types/src/documents/app/index.ts @@ -16,4 +16,4 @@ export * from "./links" export * from "./component" export * from "./sqlite" export * from "./snippet" -export * from "./rowActions" +export * from "./rowAction" diff --git a/packages/types/src/documents/app/rowActions.ts b/packages/types/src/documents/app/rowAction.ts similarity index 62% rename from packages/types/src/documents/app/rowActions.ts rename to packages/types/src/documents/app/rowAction.ts index 5eaf5de5d6..ea55d5dcd2 100644 --- a/packages/types/src/documents/app/rowActions.ts +++ b/packages/types/src/documents/app/rowAction.ts @@ -2,8 +2,10 @@ import { Document } from "../document" export interface TableRowActions extends Document { _id: string - actions: { - id: string - name: string - }[] + actions: Record< + string, + { + name: string + } + > } diff --git a/packages/types/src/documents/document.ts b/packages/types/src/documents/document.ts index 23aec05ee5..f5facfae9d 100644 --- a/packages/types/src/documents/document.ts +++ b/packages/types/src/documents/document.ts @@ -69,6 +69,7 @@ export enum InternalTable { // documents or enriched into existence as part of get requests export enum VirtualDocumentType { VIEW = "view", + ROW_ACTION = "row_action", } export interface Document { From d03a0ebb6814523fbf7218c6cec52434770564fb Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Jul 2024 17:08:57 +0200 Subject: [PATCH 19/63] Implement update --- .../src/api/controllers/rowAction/crud.ts | 23 +++++++++-- .../src/api/routes/tests/rowAction.spec.ts | 41 +++++++++++++++++++ packages/server/src/sdk/app/rowActions.ts | 27 +++++++++++- .../src/tests/utilities/api/rowAction.ts | 17 ++++++++ packages/types/src/api/web/app/rowAction.ts | 5 +++ 5 files changed, 107 insertions(+), 6 deletions(-) diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts index 7f3123e120..1b93228f59 100644 --- a/packages/server/src/api/controllers/rowAction/crud.ts +++ b/packages/server/src/api/controllers/rowAction/crud.ts @@ -3,6 +3,7 @@ import { Ctx, RowActionResponse, RowActionsResponse, + UpdateRowActionRequest, } from "@budibase/types" import sdk from "../../../sdk" @@ -38,21 +39,35 @@ export async function create( ) { const table = await getTable(ctx) - const { id, ...createdAction } = await sdk.rowActions.create( + const createdAction = await sdk.rowActions.create( table._id!, ctx.request.body ) ctx.body = { tableId: table._id!, - actionId: id, ...createdAction, } ctx.status = 201 } -export function update() { - throw new Error("Function not implemented.") +export async function update( + ctx: Ctx +) { + const table = await getTable(ctx) + const { actionId } = ctx.params + + const actions = await sdk.rowActions.update( + table._id!, + actionId, + ctx.request.body + ) + + ctx.body = { + tableId: table._id!, + + ...actions, + } } export function remove() { diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index bee6964956..e53d39b553 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -184,4 +184,45 @@ describe("/rowsActions", () => { ) }) }) + + describe("update", () => { + unauthorisedTests() + + it("can update existing actions", async () => { + for (const rowAction of createRowActionRequests(3)) { + await createRowAction(tableId, rowAction) + } + + const persisted = await config.api.rowAction.find(tableId) + + const [actionId, actionData] = _.sample( + Object.entries(persisted.actions) + )! + + const updatedName = generator.word() + + const res = await config.api.rowAction.update(tableId, actionId, { + ...actionData, + name: updatedName, + }) + + expect(res).toEqual({ + tableId, + actionId, + ...actionData, + name: updatedName, + }) + + expect(await config.api.rowAction.find(tableId)).toEqual( + expect.objectContaining({ + actions: expect.objectContaining({ + [actionId]: { + ...actionData, + name: updatedName, + }, + }), + }) + ) + }) + }) }) diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts index fa60a075c9..1d9ed92441 100644 --- a/packages/server/src/sdk/app/rowActions.ts +++ b/packages/server/src/sdk/app/rowActions.ts @@ -1,4 +1,4 @@ -import { context, utils } from "@budibase/backend-core" +import { context, HTTPError, utils } from "@budibase/backend-core" import { generateRowActionsID } from "../../db/utils" import { @@ -26,7 +26,7 @@ export async function create(tableId: string, rowAction: { name: string }) { await db.put(doc) return { - id: newId, + actionId: newId, ...rowAction, } } @@ -43,3 +43,26 @@ export async function docExists(tableId: string) { const result = await db.exists(rowActionsId) return result } +export async function update( + tableId: string, + rowActionId: string, + rowAction: { name: string } +) { + const actionsDoc = await get(tableId) + + if (!actionsDoc.actions[rowActionId]) { + throw new HTTPError( + `Row action '${rowActionId}' not found in '${tableId}'`, + 400 + ) + } + actionsDoc.actions[rowActionId] = rowAction + + const db = context.getAppDB() + await db.put(actionsDoc) + + return { + actionId: rowActionId, + ...rowAction, + } +} diff --git a/packages/server/src/tests/utilities/api/rowAction.ts b/packages/server/src/tests/utilities/api/rowAction.ts index a7c3932bc4..1ff352faee 100644 --- a/packages/server/src/tests/utilities/api/rowAction.ts +++ b/packages/server/src/tests/utilities/api/rowAction.ts @@ -35,4 +35,21 @@ export class RowActionAPI extends TestAPI { } ) } + + update = async ( + tableId: string, + rowActionId: string, + rowAction: CreateRowActionRequest, + expectations?: Expectations, + config?: { publicUser?: boolean } + ) => { + return await this._put( + `/api/tables/${tableId}/actions/${rowActionId}`, + { + body: rowAction, + expectations, + ...config, + } + ) + } } diff --git a/packages/types/src/api/web/app/rowAction.ts b/packages/types/src/api/web/app/rowAction.ts index 3219a5e4b7..760ab697f8 100644 --- a/packages/types/src/api/web/app/rowAction.ts +++ b/packages/types/src/api/web/app/rowAction.ts @@ -13,3 +13,8 @@ export interface RowActionsResponse { interface RowActionData { name: string } + +export interface UpdateRowActionRequest { + id: string + name: string +} From 9ff3d8cf77dc00fe23726c8916c82d8360e69af2 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Jul 2024 17:14:14 +0200 Subject: [PATCH 20/63] Add extra tests --- .../src/api/routes/tests/rowAction.spec.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index e53d39b553..7a3bc26068 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -224,5 +224,31 @@ describe("/rowsActions", () => { }) ) }) + + it("throws Bad Request when trying to update by a non-existing id", async () => { + await createRowAction(tableId, createRowActionRequest()) + + await config.api.rowAction.update( + tableId, + generator.guid(), + createRowActionRequest(), + { status: 400 } + ) + }) + + it("throws Bad Request when trying to update by a via another table id", async () => { + const otherTable = await config.api.table.save( + setup.structures.basicTable() + ) + await createRowAction(otherTable._id!, createRowActionRequest()) + + const action = await createRowAction(tableId, createRowActionRequest()) + await config.api.rowAction.update( + otherTable._id!, + action.actionId, + createRowActionRequest(), + { status: 400 } + ) + }) }) }) From ba2d6fd73b7886977dcf07643c9dc20a7b4ffdf1 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Jul 2024 17:16:14 +0200 Subject: [PATCH 21/63] Renames --- .../src/api/controllers/rowAction/crud.ts | 1 - .../src/api/routes/tests/rowAction.spec.ts | 20 +++++++++---------- packages/server/src/sdk/app/rowActions.ts | 4 ++-- packages/types/src/api/web/app/rowAction.ts | 7 ++----- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts index 1b93228f59..b4e5612a7f 100644 --- a/packages/server/src/api/controllers/rowAction/crud.ts +++ b/packages/server/src/api/controllers/rowAction/crud.ts @@ -65,7 +65,6 @@ export async function update( ctx.body = { tableId: table._id!, - ...actions, } } diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index 7a3bc26068..a1c2973b66 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -95,8 +95,8 @@ describe("/rowsActions", () => { }) expect(res).toEqual({ + id: expect.stringMatching(/^row_action_\w+/), tableId: tableId, - actionId: expect.stringMatching(/^row_action_\w+/), ...rowAction, }) @@ -105,7 +105,7 @@ describe("/rowsActions", () => { _rev: expect.stringMatching(/^1-\w+/), tableId: tableId, actions: { - [res.actionId]: rowAction, + [res.id]: rowAction, }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -123,9 +123,9 @@ describe("/rowsActions", () => { _id: `ra_${tableId}`, _rev: expect.stringMatching(/^3-\w+/), actions: { - [responses[0].actionId]: rowActions[0], - [responses[1].actionId]: rowActions[1], - [responses[2].actionId]: rowActions[2], + [responses[0].id]: rowActions[0], + [responses[1].id]: rowActions[1], + [responses[2].id]: rowActions[2], }, tableId: tableId, createdAt: new Date().toISOString(), @@ -166,9 +166,9 @@ describe("/rowsActions", () => { expect.objectContaining({ tableId, actions: { - [rowActions[0].actionId]: expect.any(Object), - [rowActions[1].actionId]: expect.any(Object), - [rowActions[2].actionId]: expect.any(Object), + [rowActions[0].id]: expect.any(Object), + [rowActions[1].id]: expect.any(Object), + [rowActions[2].id]: expect.any(Object), }, }) ) @@ -207,8 +207,8 @@ describe("/rowsActions", () => { }) expect(res).toEqual({ + id: actionId, tableId, - actionId, ...actionData, name: updatedName, }) @@ -245,7 +245,7 @@ describe("/rowsActions", () => { const action = await createRowAction(tableId, createRowActionRequest()) await config.api.rowAction.update( otherTable._id!, - action.actionId, + action.id, createRowActionRequest(), { status: 400 } ) diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts index 1d9ed92441..b6fce219c2 100644 --- a/packages/server/src/sdk/app/rowActions.ts +++ b/packages/server/src/sdk/app/rowActions.ts @@ -26,7 +26,7 @@ export async function create(tableId: string, rowAction: { name: string }) { await db.put(doc) return { - actionId: newId, + id: newId, ...rowAction, } } @@ -62,7 +62,7 @@ export async function update( await db.put(actionsDoc) return { - actionId: rowActionId, + id: rowActionId, ...rowAction, } } diff --git a/packages/types/src/api/web/app/rowAction.ts b/packages/types/src/api/web/app/rowAction.ts index 760ab697f8..72662fcb49 100644 --- a/packages/types/src/api/web/app/rowAction.ts +++ b/packages/types/src/api/web/app/rowAction.ts @@ -1,8 +1,8 @@ export interface CreateRowActionRequest extends RowActionData {} export interface RowActionResponse extends RowActionData { + id: string tableId: string - actionId: string } export interface RowActionsResponse { @@ -14,7 +14,4 @@ interface RowActionData { name: string } -export interface UpdateRowActionRequest { - id: string - name: string -} +export interface UpdateRowActionRequest extends RowActionData {} From 2035713b9c9a84a2907036f9de838471815aef1d Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 11 Jul 2024 17:33:40 +0200 Subject: [PATCH 22/63] Implement delete --- .../src/api/controllers/rowAction/crud.ts | 8 +++- .../src/api/routes/tests/rowAction.spec.ts | 45 +++++++++++++++++++ packages/server/src/sdk/app/rowActions.ts | 17 +++++++ .../src/tests/utilities/api/rowAction.ts | 15 +++++++ 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts index b4e5612a7f..c946515150 100644 --- a/packages/server/src/api/controllers/rowAction/crud.ts +++ b/packages/server/src/api/controllers/rowAction/crud.ts @@ -69,6 +69,10 @@ export async function update( } } -export function remove() { - throw new Error("Function not implemented.") +export async function remove(ctx: Ctx) { + const table = await getTable(ctx) + const { actionId } = ctx.params + + await sdk.rowActions.remove(table._id!, actionId) + ctx.status = 204 } diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index a1c2973b66..2af3be00b5 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -251,4 +251,49 @@ describe("/rowsActions", () => { ) }) }) + + describe("delete", () => { + unauthorisedTests() + + it("can delete existing actions", async () => { + const actions: RowActionResponse[] = [] + for (const rowAction of createRowActionRequests(3)) { + actions.push(await createRowAction(tableId, rowAction)) + } + + const actionToDelete = _.sample(actions)! + + await config.api.rowAction.delete(tableId, actionToDelete.id, { + status: 204, + }) + + expect(await config.api.rowAction.find(tableId)).toEqual( + expect.objectContaining({ + actions: actions + .filter(a => a.id !== actionToDelete.id) + .reduce((acc, c) => ({ ...acc, [c.id]: expect.any(Object) }), {}), + }) + ) + }) + + it("throws Bad Request when trying to delete by a non-existing id", async () => { + await createRowAction(tableId, createRowActionRequest()) + + await config.api.rowAction.delete(tableId, generator.guid(), { + status: 400, + }) + }) + + it("throws Bad Request when trying to delete by a via another table id", async () => { + const otherTable = await config.api.table.save( + setup.structures.basicTable() + ) + await createRowAction(otherTable._id!, createRowActionRequest()) + + const action = await createRowAction(tableId, createRowActionRequest()) + await config.api.rowAction.delete(otherTable._id!, action.id, { + status: 400, + }) + }) + }) }) diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts index b6fce219c2..4e96339b06 100644 --- a/packages/server/src/sdk/app/rowActions.ts +++ b/packages/server/src/sdk/app/rowActions.ts @@ -43,6 +43,7 @@ export async function docExists(tableId: string) { const result = await db.exists(rowActionsId) return result } + export async function update( tableId: string, rowActionId: string, @@ -66,3 +67,19 @@ export async function update( ...rowAction, } } + +export async function remove(tableId: string, rowActionId: string) { + const actionsDoc = await get(tableId) + + if (!actionsDoc.actions[rowActionId]) { + throw new HTTPError( + `Row action '${rowActionId}' not found in '${tableId}'`, + 400 + ) + } + + delete actionsDoc.actions[rowActionId] + + const db = context.getAppDB() + await db.put(actionsDoc) +} diff --git a/packages/server/src/tests/utilities/api/rowAction.ts b/packages/server/src/tests/utilities/api/rowAction.ts index 1ff352faee..b3a07e0a65 100644 --- a/packages/server/src/tests/utilities/api/rowAction.ts +++ b/packages/server/src/tests/utilities/api/rowAction.ts @@ -52,4 +52,19 @@ export class RowActionAPI extends TestAPI { } ) } + + delete = async ( + tableId: string, + rowActionId: string, + expectations?: Expectations, + config?: { publicUser?: boolean } + ) => { + return await this._delete( + `/api/tables/${tableId}/actions/${rowActionId}`, + { + expectations, + ...config, + } + ) + } } From b44397d0275bd0dbf6b0d80a1d2e03ebf970da14 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 12 Jul 2024 11:29:00 +0200 Subject: [PATCH 23/63] Dont return couch fields --- .../src/api/controllers/rowAction/crud.ts | 30 ++++++------ packages/server/src/api/routes/rowAction.ts | 2 + .../src/api/routes/tests/rowAction.spec.ts | 49 +++++++------------ packages/types/src/api/web/app/rowAction.ts | 13 ++--- 4 files changed, 41 insertions(+), 53 deletions(-) diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts index c946515150..640bc35378 100644 --- a/packages/server/src/api/controllers/rowAction/crud.ts +++ b/packages/server/src/api/controllers/rowAction/crud.ts @@ -21,17 +21,22 @@ export async function find(ctx: Ctx) { if (!(await sdk.rowActions.docExists(table._id!))) { ctx.body = { - tableId: table._id!, actions: {}, } return } - const actions = await sdk.rowActions.get(table._id!) - ctx.body = { - tableId: table._id!, - ...actions, + const { actions } = await sdk.rowActions.get(table._id!) + const result: RowActionsResponse = { + actions: Object.entries(actions).reduce>( + (acc, [key, action]) => ({ + ...acc, + [key]: { id: key, tableId: table._id!, ...action }, + }), + {} + ), } + ctx.body = result } export async function create( @@ -39,10 +44,9 @@ export async function create( ) { const table = await getTable(ctx) - const createdAction = await sdk.rowActions.create( - table._id!, - ctx.request.body - ) + const createdAction = await sdk.rowActions.create(table._id!, { + name: ctx.request.body.name, + }) ctx.body = { tableId: table._id!, @@ -57,11 +61,9 @@ export async function update( const table = await getTable(ctx) const { actionId } = ctx.params - const actions = await sdk.rowActions.update( - table._id!, - actionId, - ctx.request.body - ) + const actions = await sdk.rowActions.update(table._id!, actionId, { + name: ctx.request.body.name, + }) ctx.body = { tableId: table._id!, diff --git a/packages/server/src/api/routes/rowAction.ts b/packages/server/src/api/routes/rowAction.ts index 18a87cd677..3ec00dff4d 100644 --- a/packages/server/src/api/routes/rowAction.ts +++ b/packages/server/src/api/routes/rowAction.ts @@ -11,6 +11,8 @@ export function rowActionValidator() { return middleware.joiValidator.body( Joi.object({ name: Joi.string().required(), + id: Joi.optional(), + tableId: Joi.optional(), }) ) } diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index 2af3be00b5..976dc950e4 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -101,14 +101,13 @@ describe("/rowsActions", () => { }) expect(await config.api.rowAction.find(tableId)).toEqual({ - _id: `ra_${tableId}`, - _rev: expect.stringMatching(/^1-\w+/), - tableId: tableId, actions: { - [res.id]: rowAction, + [res.id]: { + ...rowAction, + id: res.id, + tableId: tableId, + }, }, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), }) }) @@ -120,16 +119,11 @@ describe("/rowsActions", () => { } expect(await config.api.rowAction.find(tableId)).toEqual({ - _id: `ra_${tableId}`, - _rev: expect.stringMatching(/^3-\w+/), actions: { - [responses[0].id]: rowActions[0], - [responses[1].id]: rowActions[1], - [responses[2].id]: rowActions[2], + [responses[0].id]: { ...rowActions[0], id: responses[0].id, tableId }, + [responses[1].id]: { ...rowActions[1], id: responses[1].id, tableId }, + [responses[2].id]: { ...rowActions[2], id: responses[2].id, tableId }, }, - tableId: tableId, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), }) }) @@ -162,26 +156,20 @@ describe("/rowsActions", () => { await createRowAction(otherTable._id!, createRowActionRequest()) const response = await config.api.rowAction.find(tableId) - expect(response).toEqual( - expect.objectContaining({ - tableId, - actions: { - [rowActions[0].id]: expect.any(Object), - [rowActions[1].id]: expect.any(Object), - [rowActions[2].id]: expect.any(Object), - }, - }) - ) + expect(response).toEqual({ + actions: { + [rowActions[0].id]: expect.any(Object), + [rowActions[1].id]: expect.any(Object), + [rowActions[2].id]: expect.any(Object), + }, + }) }) it("returns empty for tables without row actions", async () => { const response = await config.api.rowAction.find(tableId) - expect(response).toEqual( - expect.objectContaining({ - tableId, - actions: {}, - }) - ) + expect(response).toEqual({ + actions: {}, + }) }) }) @@ -209,7 +197,6 @@ describe("/rowsActions", () => { expect(res).toEqual({ id: actionId, tableId, - ...actionData, name: updatedName, }) diff --git a/packages/types/src/api/web/app/rowAction.ts b/packages/types/src/api/web/app/rowAction.ts index 72662fcb49..ba95ba6b95 100644 --- a/packages/types/src/api/web/app/rowAction.ts +++ b/packages/types/src/api/web/app/rowAction.ts @@ -1,4 +1,8 @@ +interface RowActionData { + name: string +} export interface CreateRowActionRequest extends RowActionData {} +export interface UpdateRowActionRequest extends RowActionData {} export interface RowActionResponse extends RowActionData { id: string @@ -6,12 +10,5 @@ export interface RowActionResponse extends RowActionData { } export interface RowActionsResponse { - tableId: string - actions: Record + actions: Record } - -interface RowActionData { - name: string -} - -export interface UpdateRowActionRequest extends RowActionData {} From 50c8449f4ba5157587f604ef8b8d5955d15ba7e8 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 12 Jul 2024 12:17:05 +0200 Subject: [PATCH 24/63] Add extra test --- .../src/middleware/joi-validator.ts | 11 +++++--- packages/server/src/api/routes/rowAction.ts | 5 ++-- .../src/api/routes/tests/rowAction.spec.ts | 28 +++++++++++++++++++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/backend-core/src/middleware/joi-validator.ts b/packages/backend-core/src/middleware/joi-validator.ts index 5047cdbbc1..7575e7e778 100644 --- a/packages/backend-core/src/middleware/joi-validator.ts +++ b/packages/backend-core/src/middleware/joi-validator.ts @@ -4,8 +4,9 @@ import { Ctx } from "@budibase/types" function validate( schema: Joi.ObjectSchema | Joi.ArraySchema, property: string, - opts: { errorPrefix: string } = { errorPrefix: `Invalid ${property}` } + opts?: { errorPrefix?: string; allowUnknown?: boolean } ) { + const errorPrefix = opts?.errorPrefix || `Invalid ${property}` // Return a Koa middleware function return (ctx: Ctx, next: any) => { if (!schema) { @@ -28,10 +29,12 @@ function validate( }) } - const { error } = schema.validate(params) + const { error } = schema.validate(params, { + allowUnknown: opts?.allowUnknown, + }) if (error) { let message = error.message - if (opts.errorPrefix) { + if (errorPrefix) { message = `Invalid ${property} - ${message}` } ctx.throw(400, message) @@ -42,7 +45,7 @@ function validate( export function body( schema: Joi.ObjectSchema | Joi.ArraySchema, - opts?: { errorPrefix: string } + opts?: { errorPrefix?: string; allowUnknown?: boolean } ) { return validate(schema, "body", opts) } diff --git a/packages/server/src/api/routes/rowAction.ts b/packages/server/src/api/routes/rowAction.ts index 3ec00dff4d..f4f20822d1 100644 --- a/packages/server/src/api/routes/rowAction.ts +++ b/packages/server/src/api/routes/rowAction.ts @@ -11,9 +11,8 @@ export function rowActionValidator() { return middleware.joiValidator.body( Joi.object({ name: Joi.string().required(), - id: Joi.optional(), - tableId: Joi.optional(), - }) + }), + { allowUnknown: true } ) } diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index 976dc950e4..68dcc484c4 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -139,6 +139,34 @@ describe("/rowsActions", () => { }, }) }) + + it("ignores not valid row action data", async () => { + const rowAction = createRowActionRequest() + const dirtyRowAction = { + ...rowAction, + id: generator.guid(), + valueToIgnore: generator.word(), + } + const res = await createRowAction(tableId, dirtyRowAction, { + status: 201, + }) + + expect(res).toEqual({ + id: expect.any(String), + tableId, + ...rowAction, + }) + + expect(await config.api.rowAction.find(tableId)).toEqual({ + actions: { + [res.id]: { + ...rowAction, + id: res.id, + tableId: tableId, + }, + }, + }) + }) }) describe("find", () => { From f06d0a5cd6e5fa3e8ecd334e4ef7b17ffe843b8f Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 12 Jul 2024 12:17:32 +0200 Subject: [PATCH 25/63] Clean --- packages/server/src/api/routes/tests/rowAction.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index 68dcc484c4..6fa0b0b86f 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -160,9 +160,9 @@ describe("/rowsActions", () => { expect(await config.api.rowAction.find(tableId)).toEqual({ actions: { [res.id]: { - ...rowAction, id: res.id, tableId: tableId, + ...rowAction, }, }, }) From 1c69cfaeda18e81640fcf50ae2c6657e324e4b18 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Fri, 12 Jul 2024 12:32:36 +0200 Subject: [PATCH 26/63] Fix initialisation --- packages/backend-core/src/middleware/joi-validator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-core/src/middleware/joi-validator.ts b/packages/backend-core/src/middleware/joi-validator.ts index 7575e7e778..a85c0e7108 100644 --- a/packages/backend-core/src/middleware/joi-validator.ts +++ b/packages/backend-core/src/middleware/joi-validator.ts @@ -6,7 +6,7 @@ function validate( property: string, opts?: { errorPrefix?: string; allowUnknown?: boolean } ) { - const errorPrefix = opts?.errorPrefix || `Invalid ${property}` + const errorPrefix = opts?.errorPrefix ?? `Invalid ${property}` // Return a Koa middleware function return (ctx: Ctx, next: any) => { if (!schema) { From 43de204ca28d15e66ef4671c11f8d18bf53004d2 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 15 Jul 2024 09:58:31 +0100 Subject: [PATCH 27/63] Default values failing test. --- .../server/src/api/routes/tests/row.spec.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 27a0d0983e..b96fdd836c 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -207,6 +207,24 @@ describe.each([ await assertRowUsage(isInternal ? rowUsage + 1 : rowUsage) }) + it("creates a new row with a default value successfully", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + description: { + name: "description", + type: FieldType.STRING, + default: "default description", + }, + }, + }) + ) + + const row = await config.api.row.save(table._id!, {}) + expect(row.name).toEqual("Test Contact") + expect(row.description).toEqual("default description") + }) + it("fails to create a row for a table that does not exist", async () => { const rowUsage = await getRowUsage() await config.api.row.save("1234567", {}, { status: 404 }) From 12911db06e3153a1ee8278622c6397bf73b76538 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 15 Jul 2024 10:43:58 +0100 Subject: [PATCH 28/63] Process primitive default values. --- packages/server/src/api/routes/tests/row.spec.ts | 3 +-- .../server/src/utilities/rowProcessor/index.ts | 15 ++++++++++++--- packages/types/src/documents/app/table/schema.ts | 12 ++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index b96fdd836c..482f80a587 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -207,7 +207,7 @@ describe.each([ await assertRowUsage(isInternal ? rowUsage + 1 : rowUsage) }) - it("creates a new row with a default value successfully", async () => { + it.only("creates a new row with a default value successfully", async () => { const table = await config.api.table.save( saveTableRequest({ schema: { @@ -221,7 +221,6 @@ describe.each([ ) const row = await config.api.row.save(table._id!, {}) - expect(row.name).toEqual("Test Contact") expect(row.description).toEqual("default description") }) diff --git a/packages/server/src/utilities/rowProcessor/index.ts b/packages/server/src/utilities/rowProcessor/index.ts index 73176af6d8..402295ba96 100644 --- a/packages/server/src/utilities/rowProcessor/index.ts +++ b/packages/server/src/utilities/rowProcessor/index.ts @@ -88,7 +88,14 @@ export async function processAutoColumn( break } } - return { table, row } +} + +async function processDeafultValues(table: Table, row: Row) { + for (let [key, schema] of Object.entries(table.schema)) { + if ("default" in schema && row[key] == null) { + row[key] = schema.default + } + } } /** @@ -182,8 +189,10 @@ export async function inputProcessing( clonedRow._rev = row._rev } - // handle auto columns - this returns an object like {table, row} - return processAutoColumn(userId, table, clonedRow, opts) + await processAutoColumn(userId, table, clonedRow, opts) + await processDeafultValues(table, clonedRow) + + return { table, row: clonedRow } } /** diff --git a/packages/types/src/documents/app/table/schema.ts b/packages/types/src/documents/app/table/schema.ts index 38424b26b6..3a2ddf097f 100644 --- a/packages/types/src/documents/app/table/schema.ts +++ b/packages/types/src/documents/app/table/schema.ts @@ -81,11 +81,13 @@ export interface NumberFieldMetadata extends Omit { toTable: string toKey: string } + default?: string } export interface JsonFieldMetadata extends Omit { type: FieldType.JSON subtype?: JsonFieldSubType.ARRAY + default?: string } export interface DateFieldMetadata extends Omit { @@ -94,17 +96,25 @@ export interface DateFieldMetadata extends Omit { timeOnly?: boolean dateOnly?: boolean subtype?: AutoFieldSubType.CREATED_AT | AutoFieldSubType.UPDATED_AT + default?: string } export interface LongFormFieldMetadata extends BaseFieldSchema { type: FieldType.LONGFORM useRichText?: boolean | null + default?: string +} + +export interface StringFieldMetadata extends BaseFieldSchema { + type: FieldType.STRING + default?: string } export interface FormulaFieldMetadata extends BaseFieldSchema { type: FieldType.FORMULA formula: string formulaType?: FormulaType + default?: string } export interface BBReferenceFieldMetadata @@ -171,6 +181,7 @@ interface OtherFieldMetadata extends BaseFieldSchema { | FieldType.BB_REFERENCE | FieldType.BB_REFERENCE_SINGLE | FieldType.ATTACHMENTS + | FieldType.STRING > } @@ -182,6 +193,7 @@ export type FieldSchema = | FormulaFieldMetadata | NumberFieldMetadata | LongFormFieldMetadata + | StringFieldMetadata | BBReferenceFieldMetadata | JsonFieldMetadata | AttachmentFieldMetadata From 297e9003ca0470d63790a533044f80ec57cf27a6 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 15 Jul 2024 11:10:30 +0100 Subject: [PATCH 29/63] Support bindings in default values. --- .../server/src/api/routes/tests/row.spec.ts | 55 +++++++++++++------ .../src/utilities/rowProcessor/index.ts | 6 +- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 482f80a587..27c363e223 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -207,23 +207,6 @@ describe.each([ await assertRowUsage(isInternal ? rowUsage + 1 : rowUsage) }) - it.only("creates a new row with a default value successfully", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - description: { - name: "description", - type: FieldType.STRING, - default: "default description", - }, - }, - }) - ) - - const row = await config.api.row.save(table._id!, {}) - expect(row.description).toEqual("default description") - }) - it("fails to create a row for a table that does not exist", async () => { const rowUsage = await getRowUsage() await config.api.row.save("1234567", {}, { status: 404 }) @@ -567,6 +550,44 @@ describe.each([ expect(row.name).toEqual(`{ "foo": "2023-01-26T11:48:57.000Z" }`) }) + + describe.only("default values", () => { + it("creates a new row with a default value successfully", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + description: { + name: "description", + type: FieldType.STRING, + default: "default description", + }, + }, + }) + ) + + const row = await config.api.row.save(table._id!, {}) + expect(row.description).toEqual("default description") + }) + + it("can use bindings in default values", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + description: { + name: "description", + type: FieldType.STRING, + default: `{{ date now "YYYY-MM-DDTHH:mm:ss" }}`, + }, + }, + }) + ) + + await tk.withFreeze(new Date("2023-01-26T11:48:57.000Z"), async () => { + const row = await config.api.row.save(table._id!, {}) + expect(row.description).toEqual("2023-01-26T11:48:57") + }) + }) + }) }) describe("get", () => { diff --git a/packages/server/src/utilities/rowProcessor/index.ts b/packages/server/src/utilities/rowProcessor/index.ts index 402295ba96..0765873389 100644 --- a/packages/server/src/utilities/rowProcessor/index.ts +++ b/packages/server/src/utilities/rowProcessor/index.ts @@ -19,6 +19,7 @@ import { } from "./bbReferenceProcessor" import { isExternalTableID } from "../../integrations/utils" import { helpers } from "@budibase/shared-core" +import { processString } from "@budibase/string-templates" export * from "./utils" export * from "./attachments" @@ -92,8 +93,9 @@ export async function processAutoColumn( async function processDeafultValues(table: Table, row: Row) { for (let [key, schema] of Object.entries(table.schema)) { - if ("default" in schema && row[key] == null) { - row[key] = schema.default + if ("default" in schema && schema.default != null && row[key] == null) { + const processed = await processString(schema.default, {}) + row[key] = coerce(processed, schema.type) } } } From ee0c4187c8aff973105156b3e89a5b166b1eeaef Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 15 Jul 2024 12:09:01 +0100 Subject: [PATCH 30/63] Better error handling of invalid default values. --- .../server/src/api/routes/tests/row.spec.ts | 169 +++++++++++++++--- .../src/utilities/rowProcessor/index.ts | 12 +- .../server/src/utilities/rowProcessor/map.ts | 8 +- 3 files changed, 160 insertions(+), 29 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 27c363e223..d96059697a 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -552,39 +552,156 @@ describe.each([ }) describe.only("default values", () => { - it("creates a new row with a default value successfully", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - description: { - name: "description", - type: FieldType.STRING, - default: "default description", + describe("string column", () => { + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + description: { + name: "description", + type: FieldType.STRING, + default: "default description", + }, }, - }, - }) - ) + }) + ) + }) - const row = await config.api.row.save(table._id!, {}) - expect(row.description).toEqual("default description") + it("creates a new row with a default value successfully", async () => { + const row = await config.api.row.save(table._id!, {}) + expect(row.description).toEqual("default description") + }) + + it("does not use default value if value specified", async () => { + const row = await config.api.row.save(table._id!, { + description: "specified description", + }) + expect(row.description).toEqual("specified description") + }) + + it("uses the default value if value is null", async () => { + const row = await config.api.row.save(table._id!, { + description: null, + }) + expect(row.description).toEqual("default description") + }) + + it("uses the default value if value is undefined", async () => { + const row = await config.api.row.save(table._id!, { + description: undefined, + }) + expect(row.description).toEqual("default description") + }) }) - it("can use bindings in default values", async () => { - const table = await config.api.table.save( - saveTableRequest({ - schema: { - description: { - name: "description", - type: FieldType.STRING, - default: `{{ date now "YYYY-MM-DDTHH:mm:ss" }}`, + describe("number column", () => { + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + age: { + name: "age", + type: FieldType.NUMBER, + default: "25", + }, }, - }, - }) - ) + }) + ) + }) - await tk.withFreeze(new Date("2023-01-26T11:48:57.000Z"), async () => { + it("creates a new row with a default value successfully", async () => { const row = await config.api.row.save(table._id!, {}) - expect(row.description).toEqual("2023-01-26T11:48:57") + expect(row.age).toEqual(25) + }) + + it("does not use default value if value specified", async () => { + const row = await config.api.row.save(table._id!, { + age: 30, + }) + expect(row.age).toEqual(30) + }) + }) + + describe("bindings", () => { + describe("string column", () => { + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + description: { + name: "description", + type: FieldType.STRING, + default: `{{ date now "YYYY-MM-DDTHH:mm:ss" }}`, + }, + }, + }) + ) + }) + + it("can use bindings in default values", async () => { + const row = await config.api.row.save(table._id!, {}) + expect(row.description).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/ + ) + }) + + it("does not use default value if value specified", async () => { + const row = await config.api.row.save(table._id!, { + description: "specified description", + }) + expect(row.description).toEqual("specified description") + }) + }) + + describe("number column", () => { + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + age: { + name: "age", + type: FieldType.NUMBER, + default: `{{ sum 10 10 5 }}`, + }, + }, + }) + ) + }) + + it("can use bindings in default values", async () => { + const row = await config.api.row.save(table._id!, {}) + expect(row.age).toEqual(25) + }) + + describe("invalid default value", () => { + beforeAll(async () => { + table = await config.api.table.save( + saveTableRequest({ + schema: { + age: { + name: "age", + type: FieldType.NUMBER, + default: `{{ capitalize "invalid" }}`, + }, + }, + }) + ) + }) + + it("throws an error when invalid default value", async () => { + await config.api.row.save( + table._id!, + {}, + { + status: 400, + body: { + message: + "Invalid default value for field 'age' - Invalid number value \"Invalid\"", + }, + } + ) + }) + }) }) }) }) diff --git a/packages/server/src/utilities/rowProcessor/index.ts b/packages/server/src/utilities/rowProcessor/index.ts index 0765873389..308a34fa9c 100644 --- a/packages/server/src/utilities/rowProcessor/index.ts +++ b/packages/server/src/utilities/rowProcessor/index.ts @@ -1,6 +1,6 @@ import * as linkRows from "../../db/linkedRows" import { fixAutoColumnSubType, processFormulas } from "./utils" -import { objectStore, utils } from "@budibase/backend-core" +import { HTTPError, objectStore, utils } from "@budibase/backend-core" import { InternalTables } from "../../db/utils" import { TYPE_TRANSFORM_MAP } from "./map" import { @@ -95,7 +95,15 @@ async function processDeafultValues(table: Table, row: Row) { for (let [key, schema] of Object.entries(table.schema)) { if ("default" in schema && schema.default != null && row[key] == null) { const processed = await processString(schema.default, {}) - row[key] = coerce(processed, schema.type) + + try { + row[key] = coerce(processed, schema.type) + } catch (err: any) { + throw new HTTPError( + `Invalid default value for field '${key}' - ${err.message}`, + 400 + ) + } } } } diff --git a/packages/server/src/utilities/rowProcessor/map.ts b/packages/server/src/utilities/rowProcessor/map.ts index ccaf07ee96..18c53744f9 100644 --- a/packages/server/src/utilities/rowProcessor/map.ts +++ b/packages/server/src/utilities/rowProcessor/map.ts @@ -91,7 +91,13 @@ export const TYPE_TRANSFORM_MAP: any = { [null]: null, //@ts-ignore [undefined]: undefined, - parse: (n: any) => parseFloat(n), + parse: (n: any) => { + const parsed = parseFloat(n) + if (isNaN(parsed)) { + throw new Error(`Invalid number value "${n}"`) + } + return parsed + }, }, [FieldType.BIGINT]: { "": null, From 18acaccfcb57a3e08bccb44d944f9797cd63d3f2 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 15 Jul 2024 16:26:15 +0100 Subject: [PATCH 31/63] Current User binding and tests. --- .../server/src/api/routes/tests/row.spec.ts | 76 +++++++++++++++++++ .../src/utilities/rowProcessor/index.ts | 25 +++++- .../server/src/utilities/rowProcessor/map.ts | 7 +- 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index d96059697a..c4399560af 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -622,6 +622,48 @@ describe.each([ }) }) + describe("date column", () => { + it("creates a row with a default value successfully", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + date: { + name: "date", + type: FieldType.DATETIME, + default: "2023-01-26T11:48:57.000Z", + }, + }, + }) + ) + const row = await config.api.row.save(table._id!, {}) + expect(row.date).toEqual("2023-01-26T11:48:57.000Z") + }) + + it("gives an error if the default value is invalid", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + date: { + name: "date", + type: FieldType.DATETIME, + default: "invalid", + }, + }, + }) + ) + await config.api.row.save( + table._id!, + {}, + { + status: 400, + body: { + message: `Invalid default value for field 'date' - Invalid date value: "invalid"`, + }, + } + ) + }) + }) + describe("bindings", () => { describe("string column", () => { beforeAll(async () => { @@ -651,6 +693,40 @@ describe.each([ }) expect(row.description).toEqual("specified description") }) + + it("can bind the current user", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + user: { + name: "user", + type: FieldType.STRING, + default: `{{ [Current User]._id }}`, + }, + }, + }) + ) + const row = await config.api.row.save(table._id!, {}) + expect(row.user).toEqual(config.getUser()._id) + }) + + it("cannot access current user password", async () => { + const table = await config.api.table.save( + saveTableRequest({ + schema: { + user: { + name: "user", + type: FieldType.STRING, + default: `{{ user.password }}`, + }, + }, + }) + ) + const row = await config.api.row.save(table._id!, {}) + // For some reason it's null for internal tables, and undefined for + // external. + expect(row.user == null).toBe(true) + }) }) describe("number column", () => { diff --git a/packages/server/src/utilities/rowProcessor/index.ts b/packages/server/src/utilities/rowProcessor/index.ts index 308a34fa9c..54d86d55ff 100644 --- a/packages/server/src/utilities/rowProcessor/index.ts +++ b/packages/server/src/utilities/rowProcessor/index.ts @@ -1,14 +1,22 @@ import * as linkRows from "../../db/linkedRows" import { fixAutoColumnSubType, processFormulas } from "./utils" -import { HTTPError, objectStore, utils } from "@budibase/backend-core" +import { + cache, + context, + HTTPError, + objectStore, + utils, +} from "@budibase/backend-core" import { InternalTables } from "../../db/utils" import { TYPE_TRANSFORM_MAP } from "./map" import { AutoFieldSubType, FieldType, + IdentityType, Row, RowAttachment, Table, + User, } from "@budibase/types" import { cloneDeep } from "lodash/fp" import { @@ -92,9 +100,22 @@ export async function processAutoColumn( } async function processDeafultValues(table: Table, row: Row) { + const ctx: { ["Current User"]?: User; user?: User } = {} + + const identity = context.getIdentity() + if (identity) { + if (identity._id && identity.type === IdentityType.USER) { + const user = await cache.user.getUser(identity._id) + delete user.password + + ctx["Current User"] = user + ctx.user = user + } + } + for (let [key, schema] of Object.entries(table.schema)) { if ("default" in schema && schema.default != null && row[key] == null) { - const processed = await processString(schema.default, {}) + const processed = await processString(schema.default, ctx) try { row[key] = coerce(processed, schema.type) diff --git a/packages/server/src/utilities/rowProcessor/map.ts b/packages/server/src/utilities/rowProcessor/map.ts index 18c53744f9..8d1c4a9ebd 100644 --- a/packages/server/src/utilities/rowProcessor/map.ts +++ b/packages/server/src/utilities/rowProcessor/map.ts @@ -115,8 +115,13 @@ export const TYPE_TRANSFORM_MAP: any = { parse: (date: any) => { if (date instanceof Date) { return date.toISOString() + } else { + const parsed = new Date(date) + if (isNaN(parsed.getTime())) { + throw new Error(`Invalid date value: "${date}"`) + } + return date } - return date }, }, [FieldType.ATTACHMENTS]: { From e39a5b0d7ecc45b6cfc3bad449f6fcaac55d6d65 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 15 Jul 2024 16:44:43 +0100 Subject: [PATCH 32/63] Add test for creating a row through views. --- packages/server/src/api/routes/tests/row.spec.ts | 4 +++- packages/server/src/api/routes/tests/viewV2.spec.ts | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index c4399560af..8cabdf5e0f 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -551,7 +551,9 @@ describe.each([ expect(row.name).toEqual(`{ "foo": "2023-01-26T11:48:57.000Z" }`) }) - describe.only("default values", () => { + describe("default values", () => { + let table: Table + describe("string column", () => { beforeAll(async () => { table = await config.api.table.save( diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index e9853e5dff..3edbc24365 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -1022,6 +1022,11 @@ describe.each([ schema: { one: { type: FieldType.STRING, name: "one" }, two: { type: FieldType.STRING, name: "two" }, + default: { + type: FieldType.STRING, + name: "default", + default: "default", + }, }, }) ) @@ -1042,11 +1047,13 @@ describe.each([ _viewId: view.id, one: "foo", two: "bar", + default: "ohnoes", }) const row = await config.api.row.get(table._id!, newRow._id!) expect(row.one).toBeUndefined() expect(row.two).toEqual("bar") + expect(row.default).toEqual("default") }) it("can't persist readonly columns", async () => { From 9542c497deab71753b4540cea010bfc3148109d1 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 15 Jul 2024 16:46:20 +0100 Subject: [PATCH 33/63] Create a test for required column with default value. --- packages/server/src/api/routes/tests/row.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 8cabdf5e0f..5ce16a54d5 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -563,6 +563,9 @@ describe.each([ name: "description", type: FieldType.STRING, default: "default description", + constraints: { + presence: true, + }, }, }, }) From 372153bc978e2923511e23c6e9546c12f970645b Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Mon, 15 Jul 2024 17:27:03 +0100 Subject: [PATCH 34/63] Accommodate time values in datetime coercion. --- packages/server/src/utilities/rowProcessor/map.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/server/src/utilities/rowProcessor/map.ts b/packages/server/src/utilities/rowProcessor/map.ts index 8d1c4a9ebd..d86e0fe4d2 100644 --- a/packages/server/src/utilities/rowProcessor/map.ts +++ b/packages/server/src/utilities/rowProcessor/map.ts @@ -1,5 +1,7 @@ import { FieldType } from "@budibase/types" +const TIME_REGEX = /^(?:\d{2}:)?(?:\d{2}:)(?:\d{2})$/ + const parseArrayString = (value: any) => { if (typeof value === "string") { if (value === "") { @@ -115,6 +117,8 @@ export const TYPE_TRANSFORM_MAP: any = { parse: (date: any) => { if (date instanceof Date) { return date.toISOString() + } else if (typeof date === "string" && TIME_REGEX.test(date)) { + return date } else { const parsed = new Date(date) if (isNaN(parsed.getTime())) { From b1cfdc4f253842601f61ca8f1fa35806da91007c Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 17 Jul 2024 11:15:55 +0200 Subject: [PATCH 35/63] Clean code --- .../src/api/routes/tests/rowAction.spec.ts | 18 +----------------- .../src/tests/utilities/api/rowAction.ts | 5 ++++- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index 6fa0b0b86f..123e36a7ce 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -4,7 +4,6 @@ import tk from "timekeeper" import { CreateRowActionRequest, RowActionResponse } from "@budibase/types" import * as setup from "./utilities" import { generator } from "@budibase/backend-core/tests" -import { Expectations } from "src/tests/utilities/api/base" describe("/rowsActions", () => { const config = setup.getConfig() @@ -23,22 +22,7 @@ describe("/rowsActions", () => { afterAll(setup.afterAll) - async function createRowAction( - tableId: string, - rowAction: CreateRowActionRequest, - expectations?: Expectations, - opts?: { publicUser?: boolean } - ) { - return await config.api.rowAction.save( - tableId, - rowAction, - { - ...expectations, - status: expectations?.status || 201, - }, - opts - ) - } + const createRowAction = config.api.rowAction.save function createRowActionRequest(): CreateRowActionRequest { return { diff --git a/packages/server/src/tests/utilities/api/rowAction.ts b/packages/server/src/tests/utilities/api/rowAction.ts index b3a07e0a65..80535e5853 100644 --- a/packages/server/src/tests/utilities/api/rowAction.ts +++ b/packages/server/src/tests/utilities/api/rowAction.ts @@ -16,7 +16,10 @@ export class RowActionAPI extends TestAPI { `/api/tables/${tableId}/actions`, { body: rowAction, - expectations, + expectations: { + ...expectations, + status: expectations?.status || 201, + }, ...config, } ) From 7fb13b757be53fdcafc0fc5bff11a511b5783537 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 17 Jul 2024 11:52:29 +0200 Subject: [PATCH 36/63] Use .string instead of .work --- packages/server/src/api/routes/tests/rowAction.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index 123e36a7ce..08a57eccb8 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -26,13 +26,13 @@ describe("/rowsActions", () => { function createRowActionRequest(): CreateRowActionRequest { return { - name: generator.word(), + name: generator.string(), } } function createRowActionRequests(count: number): CreateRowActionRequest[] { return generator - .unique(() => generator.word(), count) + .unique(() => generator.string(), count) .map(name => ({ name })) } @@ -129,7 +129,7 @@ describe("/rowsActions", () => { const dirtyRowAction = { ...rowAction, id: generator.guid(), - valueToIgnore: generator.word(), + valueToIgnore: generator.string(), } const res = await createRowAction(tableId, dirtyRowAction, { status: 201, @@ -199,7 +199,7 @@ describe("/rowsActions", () => { Object.entries(persisted.actions) )! - const updatedName = generator.word() + const updatedName = generator.string() const res = await config.api.rowAction.update(tableId, actionId, { ...actionData, From c51d2cd431e01d3639e501e20096545a9db02eb9 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 17 Jul 2024 12:16:14 +0200 Subject: [PATCH 37/63] Trims names --- .../src/api/routes/tests/rowAction.spec.ts | 52 +++++++++++++++++++ packages/server/src/sdk/app/rowActions.ts | 11 ++-- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index 08a57eccb8..6fbdce3afa 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -95,6 +95,31 @@ describe("/rowsActions", () => { }) }) + it("trims row action names", async () => { + const name = " action name " + const res = await createRowAction( + tableId, + { name }, + { + status: 201, + } + ) + + expect(res).toEqual({ + id: expect.stringMatching(/^row_action_\w+/), + tableId: tableId, + name: "action name", + }) + + expect(await config.api.rowAction.find(tableId)).toEqual({ + actions: { + [res.id]: expect.objectContaining({ + name: "action name", + }), + }, + }) + }) + it("can create multiple row actions for the same table", async () => { const rowActions = createRowActionRequests(3) const responses: RowActionResponse[] = [] @@ -224,6 +249,33 @@ describe("/rowsActions", () => { ) }) + it("trims row action names", async () => { + const rowAction = await createRowAction( + tableId, + createRowActionRequest(), + { + status: 201, + } + ) + + const res = await config.api.rowAction.update(tableId, rowAction.id, { + ...rowAction, + name: " action name ", + }) + + expect(res).toEqual(expect.objectContaining({ name: "action name" })) + + expect(await config.api.rowAction.find(tableId)).toEqual( + expect.objectContaining({ + actions: expect.objectContaining({ + [rowAction.id]: expect.objectContaining({ + name: "action name", + }), + }), + }) + ) + }) + it("throws Bad Request when trying to update by a non-existing id", async () => { await createRowAction(tableId, createRowActionRequest()) diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts index 4e96339b06..f47d4348ea 100644 --- a/packages/server/src/sdk/app/rowActions.ts +++ b/packages/server/src/sdk/app/rowActions.ts @@ -8,6 +8,8 @@ import { } from "@budibase/types" export async function create(tableId: string, rowAction: { name: string }) { + const action = { name: rowAction.name.trim() } + const db = context.getAppDB() const rowActionsId = generateRowActionsID(tableId) let doc: TableRowActions @@ -22,12 +24,12 @@ export async function create(tableId: string, rowAction: { name: string }) { } const newId = `${VirtualDocumentType.ROW_ACTION}${SEPARATOR}${utils.newid()}` - doc.actions[newId] = rowAction + doc.actions[newId] = action await db.put(doc) return { id: newId, - ...rowAction, + ...action, } } @@ -49,6 +51,7 @@ export async function update( rowActionId: string, rowAction: { name: string } ) { + const action = { name: rowAction.name.trim() } const actionsDoc = await get(tableId) if (!actionsDoc.actions[rowActionId]) { @@ -57,14 +60,14 @@ export async function update( 400 ) } - actionsDoc.actions[rowActionId] = rowAction + actionsDoc.actions[rowActionId] = action const db = context.getAppDB() await db.put(actionsDoc) return { id: rowActionId, - ...rowAction, + ...action, } } From 8297a58270ff0eaf6b34286113eb06ace78a320a Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 17 Jul 2024 12:18:09 +0200 Subject: [PATCH 38/63] Ensure unique names --- .../src/api/routes/tests/rowAction.spec.ts | 27 +++++++++++++++++++ packages/server/src/sdk/app/rowActions.ts | 12 +++++++++ 2 files changed, 39 insertions(+) diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index 6fbdce3afa..d4169562d3 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -176,6 +176,33 @@ describe("/rowsActions", () => { }, }) }) + + it("can not create multiple row actions with the same name (for the same table)", async () => { + const action = await createRowAction(tableId, { + name: "Row action name ", + }) + + await createRowAction( + tableId, + { name: action.name }, + { + status: 409, + body: { + message: "A row action with the same name already exists.", + }, + } + ) + await createRowAction( + tableId, + { name: "row action name" }, + { + status: 409, + body: { + message: "A row action with the same name already exists.", + }, + } + ) + }) }) describe("find", () => { diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts index f47d4348ea..a247e62ec6 100644 --- a/packages/server/src/sdk/app/rowActions.ts +++ b/packages/server/src/sdk/app/rowActions.ts @@ -7,6 +7,16 @@ import { VirtualDocumentType, } from "@budibase/types" +function ensureUnique(doc: TableRowActions, newName: string) { + if ( + Object.values(doc.actions).find( + a => a.name.toLowerCase() === newName.toLowerCase() + ) + ) { + throw new HTTPError("A row action with the same name already exists.", 409) + } +} + export async function create(tableId: string, rowAction: { name: string }) { const action = { name: rowAction.name.trim() } @@ -23,6 +33,8 @@ export async function create(tableId: string, rowAction: { name: string }) { doc = { _id: rowActionsId, actions: {} } } + ensureUnique(doc, action.name) + const newId = `${VirtualDocumentType.ROW_ACTION}${SEPARATOR}${utils.newid()}` doc.actions[newId] = action await db.put(doc) From 371a3ad8ecc2abfb349e8acd02e50324b23cbc8a Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 17 Jul 2024 12:26:36 +0200 Subject: [PATCH 39/63] Ensure unique on updates --- .../src/api/routes/tests/rowAction.spec.ts | 27 +++++++++++++++++++ packages/server/src/sdk/app/rowActions.ts | 17 +++++++++--- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index d4169562d3..98eb7f7699 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -203,6 +203,16 @@ describe("/rowsActions", () => { } ) }) + + it("can reuse row action names between different tables", async () => { + const otherTable = await config.api.table.save( + setup.structures.basicTable() + ) + + const action = await createRowAction(tableId, createRowActionRequest()) + + await createRowAction(otherTable._id!, { name: action.name }) + }) }) describe("find", () => { @@ -328,6 +338,23 @@ describe("/rowsActions", () => { { status: 400 } ) }) + + it("can not use existing row action names (for the same table)", async () => { + const action1 = await createRowAction(tableId, createRowActionRequest()) + const action2 = await createRowAction(tableId, createRowActionRequest()) + + await config.api.rowAction.update( + tableId, + action1.id, + { name: action2.name }, + { + status: 409, + body: { + message: "A row action with the same name already exists.", + }, + } + ) + }) }) describe("delete", () => { diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts index a247e62ec6..8bff216ab9 100644 --- a/packages/server/src/sdk/app/rowActions.ts +++ b/packages/server/src/sdk/app/rowActions.ts @@ -7,10 +7,16 @@ import { VirtualDocumentType, } from "@budibase/types" -function ensureUnique(doc: TableRowActions, newName: string) { +function ensureUniqueAndThrow( + doc: TableRowActions, + name: string, + existingRowActionId?: string +) { if ( - Object.values(doc.actions).find( - a => a.name.toLowerCase() === newName.toLowerCase() + Object.entries(doc.actions).find( + ([id, a]) => + a.name.toLowerCase() === name.toLowerCase() && + id !== existingRowActionId ) ) { throw new HTTPError("A row action with the same name already exists.", 409) @@ -33,7 +39,7 @@ export async function create(tableId: string, rowAction: { name: string }) { doc = { _id: rowActionsId, actions: {} } } - ensureUnique(doc, action.name) + ensureUniqueAndThrow(doc, action.name) const newId = `${VirtualDocumentType.ROW_ACTION}${SEPARATOR}${utils.newid()}` doc.actions[newId] = action @@ -72,6 +78,9 @@ export async function update( 400 ) } + + ensureUniqueAndThrow(actionsDoc, action.name, rowActionId) + actionsDoc.actions[rowActionId] = action const db = context.getAppDB() From 13c6ad9924ab0669d8a48f2c815322c1d0b8d486 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 17 Jul 2024 12:30:31 +0200 Subject: [PATCH 40/63] More tests --- packages/server/src/api/routes/tests/rowAction.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts index 98eb7f7699..efdfdd2392 100644 --- a/packages/server/src/api/routes/tests/rowAction.spec.ts +++ b/packages/server/src/api/routes/tests/rowAction.spec.ts @@ -355,6 +355,14 @@ describe("/rowsActions", () => { } ) }) + + it("does not throw with name conflicts for the same row action", async () => { + const action1 = await createRowAction(tableId, createRowActionRequest()) + + await config.api.rowAction.update(tableId, action1.id, { + name: action1.name, + }) + }) }) describe("delete", () => { From 373aeac00fa6011aaecc7f6441d514b07e714fb2 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 17 Jul 2024 13:10:19 +0200 Subject: [PATCH 41/63] Move automation creation to sdk --- .../server/src/api/controllers/automation.ts | 29 +--------- .../server/src/sdk/app/automations/crud.ts | 57 +++++++++++++++++++ .../server/src/sdk/app/automations/index.ts | 2 + 3 files changed, 62 insertions(+), 26 deletions(-) create mode 100644 packages/server/src/sdk/app/automations/crud.ts diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts index a49fe834d3..4f29b27544 100644 --- a/packages/server/src/api/controllers/automation.ts +++ b/packages/server/src/api/controllers/automation.ts @@ -1,9 +1,5 @@ import * as triggers from "../../automations/triggers" -import { - getAutomationParams, - generateAutomationID, - DocumentType, -} from "../../db/utils" +import { getAutomationParams, DocumentType } from "../../db/utils" import { checkForWebhooks, updateTestHistory, @@ -76,7 +72,6 @@ function cleanAutomationInputs(automation: Automation) { export async function create( ctx: UserCtx ) { - const db = context.getAppDB() let automation = ctx.request.body automation.appId = ctx.appId @@ -86,30 +81,12 @@ export async function create( return } - // Respect existing IDs if recreating a deleted automation - if (!automation._id) { - automation._id = generateAutomationID() - } - - automation.type = "automation" - automation = cleanAutomationInputs(automation) - automation = await checkForWebhooks({ - newAuto: automation, - }) - const response = await db.put(automation) - await events.automation.created(automation) - for (let step of automation.definition.steps) { - await events.automation.stepCreated(automation, step) - } - automation._rev = response.rev + const response = await sdk.automations.create(automation) ctx.status = 200 ctx.body = { message: "Automation created successfully", - automation: { - ...automation, - ...response, - }, + automation: response, } builderSocket?.emitAutomationUpdate(ctx, automation) } diff --git a/packages/server/src/sdk/app/automations/crud.ts b/packages/server/src/sdk/app/automations/crud.ts new file mode 100644 index 0000000000..5726e8ad28 --- /dev/null +++ b/packages/server/src/sdk/app/automations/crud.ts @@ -0,0 +1,57 @@ +import { context, events } from "@budibase/backend-core" +import { Automation } from "@budibase/types" +import { checkForWebhooks } from "src/automations/utils" +import { generateAutomationID } from "src/db/utils" + +function getDb() { + return context.getAppDB() +} + +function cleanAutomationInputs(automation: Automation) { + if (automation == null) { + return automation + } + let steps = automation.definition.steps + let trigger = automation.definition.trigger + let allSteps = [...steps, trigger] + // live is not a property used anymore + if (automation.live != null) { + delete automation.live + } + for (let step of allSteps) { + if (step == null) { + continue + } + for (let inputName of Object.keys(step.inputs)) { + if (!step.inputs[inputName] || step.inputs[inputName] === "") { + delete step.inputs[inputName] + } + } + } + return automation +} + +export async function create(automation: Automation) { + automation = { ...automation } + const db = getDb() + + // Respect existing IDs if recreating a deleted automation + if (!automation._id) { + automation._id = generateAutomationID() + } + + automation.type = "automation" + automation = cleanAutomationInputs(automation) + automation = await checkForWebhooks({ + newAuto: automation, + }) + const response = await db.put(automation) + await events.automation.created(automation) + for (let step of automation.definition.steps) { + await events.automation.stepCreated(automation, step) + } + automation._rev = response.rev + automation._id = response.id + + return automation +} diff --git a/packages/server/src/sdk/app/automations/index.ts b/packages/server/src/sdk/app/automations/index.ts index 16530cf085..215fb2197e 100644 --- a/packages/server/src/sdk/app/automations/index.ts +++ b/packages/server/src/sdk/app/automations/index.ts @@ -1,7 +1,9 @@ +import * as crud from "./crud" import * as webhook from "./webhook" import * as utils from "./utils" export default { + ...crud, webhook, utils, } From dc47037dbb8b435058957c5c9f066cfdc7a85be7 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 17 Jul 2024 13:26:40 +0200 Subject: [PATCH 42/63] Move update and remove --- .../server/src/api/controllers/automation.ts | 120 +----------------- .../server/src/sdk/app/automations/crud.ts | 99 ++++++++++++++- 2 files changed, 105 insertions(+), 114 deletions(-) diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts index 4f29b27544..e47d5565bc 100644 --- a/packages/server/src/api/controllers/automation.ts +++ b/packages/server/src/api/controllers/automation.ts @@ -1,12 +1,6 @@ import * as triggers from "../../automations/triggers" import { getAutomationParams, DocumentType } from "../../db/utils" -import { - checkForWebhooks, - updateTestHistory, - removeDeprecated, -} from "../../automations/utils" -import { deleteEntityMetadata } from "../../utilities" -import { MetadataTypes } from "../../constants" +import { updateTestHistory, removeDeprecated } from "../../automations/utils" import { setTestFlag, clearTestFlag } from "../../utilities/redis" import { context, cache, events, db as dbCore } from "@budibase/backend-core" import { automations, features } from "@budibase/pro" @@ -37,38 +31,6 @@ function getTriggerDefinitions() { * * *************************/ -async function cleanupAutomationMetadata(automationId: string) { - await deleteEntityMetadata(MetadataTypes.AUTOMATION_TEST_INPUT, automationId) - await deleteEntityMetadata( - MetadataTypes.AUTOMATION_TEST_HISTORY, - automationId - ) -} - -function cleanAutomationInputs(automation: Automation) { - if (automation == null) { - return automation - } - let steps = automation.definition.steps - let trigger = automation.definition.trigger - let allSteps = [...steps, trigger] - // live is not a property used anymore - if (automation.live != null) { - delete automation.live - } - for (let step of allSteps) { - if (step == null) { - continue - } - for (let inputName of Object.keys(step.inputs)) { - if (!step.inputs[inputName] || step.inputs[inputName] === "") { - delete step.inputs[inputName] - } - } - } - return automation -} - export async function create( ctx: UserCtx ) { @@ -81,48 +43,17 @@ export async function create( return } - const response = await sdk.automations.create(automation) + const createdAutomation = await sdk.automations.create(automation) ctx.status = 200 ctx.body = { message: "Automation created successfully", - automation: response, + automation: createdAutomation, } builderSocket?.emitAutomationUpdate(ctx, automation) } -export function getNewSteps(oldAutomation: Automation, automation: Automation) { - const oldStepIds = oldAutomation.definition.steps.map(s => s.id) - return automation.definition.steps.filter(s => !oldStepIds.includes(s.id)) -} - -export function getDeletedSteps( - oldAutomation: Automation, - automation: Automation -) { - const stepIds = automation.definition.steps.map(s => s.id) - return oldAutomation.definition.steps.filter(s => !stepIds.includes(s.id)) -} - -export async function handleStepEvents( - oldAutomation: Automation, - automation: Automation -) { - // new steps - const newSteps = getNewSteps(oldAutomation, automation) - for (let step of newSteps) { - await events.automation.stepCreated(automation, step) - } - - // old steps - const deletedSteps = getDeletedSteps(oldAutomation, automation) - for (let step of deletedSteps) { - await events.automation.stepDeleted(automation, step) - } -} - export async function update(ctx: UserCtx) { - const db = context.getAppDB() let automation = ctx.request.body automation.appId = ctx.appId @@ -132,42 +63,12 @@ export async function update(ctx: UserCtx) { return } - const oldAutomation = await db.get(automation._id) - automation = cleanAutomationInputs(automation) - automation = await checkForWebhooks({ - oldAuto: oldAutomation, - newAuto: automation, - }) - const response = await db.put(automation) - automation._rev = response.rev - - const oldAutoTrigger = - oldAutomation && oldAutomation.definition.trigger - ? oldAutomation.definition.trigger - : undefined - const newAutoTrigger = - automation && automation.definition.trigger - ? automation.definition.trigger - : {} - // trigger has been updated, remove the test inputs - if (oldAutoTrigger && oldAutoTrigger.id !== newAutoTrigger.id) { - await events.automation.triggerUpdated(automation) - await deleteEntityMetadata( - MetadataTypes.AUTOMATION_TEST_INPUT, - automation._id! - ) - } - - await handleStepEvents(oldAutomation, automation) + const updatedAutomation = await sdk.automations.update(automation) ctx.status = 200 ctx.body = { message: `Automation ${automation._id} updated successfully.`, - automation: { - ...automation, - _rev: response.rev, - _id: response.id, - }, + automation: updatedAutomation, } builderSocket?.emitAutomationUpdate(ctx, automation) } @@ -188,16 +89,9 @@ export async function find(ctx: UserCtx) { } export async function destroy(ctx: UserCtx) { - const db = context.getAppDB() const automationId = ctx.params.id - const oldAutomation = await db.get(automationId) - await checkForWebhooks({ - oldAuto: oldAutomation, - }) - // delete metadata first - await cleanupAutomationMetadata(automationId) - ctx.body = await db.remove(automationId, ctx.params.rev) - await events.automation.deleted(oldAutomation) + + ctx.body = await sdk.automations.remove(automationId, ctx.params.rev) builderSocket?.emitAutomationDeletion(ctx, automationId) } diff --git a/packages/server/src/sdk/app/automations/crud.ts b/packages/server/src/sdk/app/automations/crud.ts index 5726e8ad28..3cbbff0a0a 100644 --- a/packages/server/src/sdk/app/automations/crud.ts +++ b/packages/server/src/sdk/app/automations/crud.ts @@ -1,7 +1,9 @@ -import { context, events } from "@budibase/backend-core" +import { context, events, HTTPError } from "@budibase/backend-core" import { Automation } from "@budibase/types" import { checkForWebhooks } from "src/automations/utils" +import { MetadataTypes } from "src/constants" import { generateAutomationID } from "src/db/utils" +import { deleteEntityMetadata } from "src/utilities" function getDb() { return context.getAppDB() @@ -31,6 +33,36 @@ function cleanAutomationInputs(automation: Automation) { return automation } +async function handleStepEvents( + oldAutomation: Automation, + automation: Automation +) { + const getNewSteps = (oldAutomation: Automation, automation: Automation) => { + const oldStepIds = oldAutomation.definition.steps.map(s => s.id) + return automation.definition.steps.filter(s => !oldStepIds.includes(s.id)) + } + + const getDeletedSteps = ( + oldAutomation: Automation, + automation: Automation + ) => { + const stepIds = automation.definition.steps.map(s => s.id) + return oldAutomation.definition.steps.filter(s => !stepIds.includes(s.id)) + } + + // new steps + const newSteps = getNewSteps(oldAutomation, automation) + for (let step of newSteps) { + await events.automation.stepCreated(automation, step) + } + + // old steps + const deletedSteps = getDeletedSteps(oldAutomation, automation) + for (let step of deletedSteps) { + await events.automation.stepDeleted(automation, step) + } +} + export async function create(automation: Automation) { automation = { ...automation } const db = getDb() @@ -55,3 +87,68 @@ export async function create(automation: Automation) { return automation } + +export async function update(automation: Automation) { + automation = { ...automation } + + if (!automation._id || !automation._rev) { + throw new HTTPError("_id or _rev fields missing", 400) + } + + const db = getDb() + + const oldAutomation = await db.get(automation._id) + automation = cleanAutomationInputs(automation) + automation = await checkForWebhooks({ + oldAuto: oldAutomation, + newAuto: automation, + }) + const response = await db.put(automation) + automation._rev = response.rev + + const oldAutoTrigger = + oldAutomation && oldAutomation.definition.trigger + ? oldAutomation.definition.trigger + : undefined + const newAutoTrigger = + automation && automation.definition.trigger + ? automation.definition.trigger + : undefined + // trigger has been updated, remove the test inputs + if (oldAutoTrigger && oldAutoTrigger.id !== newAutoTrigger?.id) { + await events.automation.triggerUpdated(automation) + await deleteEntityMetadata( + MetadataTypes.AUTOMATION_TEST_INPUT, + automation._id! + ) + } + + await handleStepEvents(oldAutomation, automation) + + return { + ...automation, + _rev: response.rev, + _id: response.id, + } +} + +export async function remove(automationId: string, rev: string) { + const db = context.getAppDB() + const existing = await db.get(automationId) + await checkForWebhooks({ + oldAuto: existing, + }) + + // delete metadata first + await deleteEntityMetadata(MetadataTypes.AUTOMATION_TEST_INPUT, automationId) + await deleteEntityMetadata( + MetadataTypes.AUTOMATION_TEST_HISTORY, + automationId + ) + + const result = await db.remove(automationId, rev) + + await events.automation.deleted(existing) + + return result +} From 53d6219b797a8a4fb7ae20195be08d18d9a4be49 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 17 Jul 2024 13:32:45 +0200 Subject: [PATCH 43/63] Extract find and fetch --- .../server/src/api/controllers/automation.ts | 13 +++--------- .../server/src/sdk/app/automations/crud.ts | 20 +++++++++++++++++-- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts index e47d5565bc..7eca17e0e8 100644 --- a/packages/server/src/api/controllers/automation.ts +++ b/packages/server/src/api/controllers/automation.ts @@ -1,5 +1,5 @@ import * as triggers from "../../automations/triggers" -import { getAutomationParams, DocumentType } from "../../db/utils" +import { DocumentType } from "../../db/utils" import { updateTestHistory, removeDeprecated } from "../../automations/utils" import { setTestFlag, clearTestFlag } from "../../utilities/redis" import { context, cache, events, db as dbCore } from "@budibase/backend-core" @@ -74,18 +74,11 @@ export async function update(ctx: UserCtx) { } export async function fetch(ctx: UserCtx) { - const db = context.getAppDB() - const response = await db.allDocs( - getAutomationParams(null, { - include_docs: true, - }) - ) - ctx.body = response.rows.map(row => row.doc) + ctx.body = await sdk.automations.fetch() } export async function find(ctx: UserCtx) { - const db = context.getAppDB() - ctx.body = await db.get(ctx.params.id) + ctx.body = await sdk.automations.get(ctx.params.id) } export async function destroy(ctx: UserCtx) { diff --git a/packages/server/src/sdk/app/automations/crud.ts b/packages/server/src/sdk/app/automations/crud.ts index 3cbbff0a0a..74a9d3d5d1 100644 --- a/packages/server/src/sdk/app/automations/crud.ts +++ b/packages/server/src/sdk/app/automations/crud.ts @@ -2,7 +2,7 @@ import { context, events, HTTPError } from "@budibase/backend-core" import { Automation } from "@budibase/types" import { checkForWebhooks } from "src/automations/utils" import { MetadataTypes } from "src/constants" -import { generateAutomationID } from "src/db/utils" +import { generateAutomationID, getAutomationParams } from "src/db/utils" import { deleteEntityMetadata } from "src/utilities" function getDb() { @@ -63,6 +63,22 @@ async function handleStepEvents( } } +export async function fetch() { + const db = getDb() + const response = await db.allDocs( + getAutomationParams(null, { + include_docs: true, + }) + ) + return response.rows.map(row => row.doc) +} + +export async function get(automationId: string) { + const db = getDb() + const result = await db.get(automationId) + return result +} + export async function create(automation: Automation) { automation = { ...automation } const db = getDb() @@ -133,7 +149,7 @@ export async function update(automation: Automation) { } export async function remove(automationId: string, rev: string) { - const db = context.getAppDB() + const db = getDb() const existing = await db.get(automationId) await checkForWebhooks({ oldAuto: existing, From 166d8fac5e10b542a4a9bbdaf61ff5c7b6c268f0 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 17 Jul 2024 13:43:25 +0200 Subject: [PATCH 44/63] Use relative paths --- packages/server/src/sdk/app/automations/crud.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server/src/sdk/app/automations/crud.ts b/packages/server/src/sdk/app/automations/crud.ts index 74a9d3d5d1..ec686cc868 100644 --- a/packages/server/src/sdk/app/automations/crud.ts +++ b/packages/server/src/sdk/app/automations/crud.ts @@ -1,9 +1,9 @@ import { context, events, HTTPError } from "@budibase/backend-core" import { Automation } from "@budibase/types" -import { checkForWebhooks } from "src/automations/utils" -import { MetadataTypes } from "src/constants" -import { generateAutomationID, getAutomationParams } from "src/db/utils" -import { deleteEntityMetadata } from "src/utilities" +import { checkForWebhooks } from "../../../automations/utils" +import { MetadataTypes } from "../../../constants" +import { generateAutomationID, getAutomationParams } from "../../../db/utils" +import { deleteEntityMetadata } from "../../../utilities" function getDb() { return context.getAppDB() From 812afdb85eaf5b82583d46f0f1e28652e9906771 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 17 Jul 2024 16:28:46 +0200 Subject: [PATCH 45/63] Fix tests (breaking because of mutating objects...) --- packages/server/src/api/routes/tests/automation.spec.ts | 9 +++------ packages/server/src/tests/utilities/structures.ts | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/server/src/api/routes/tests/automation.spec.ts b/packages/server/src/api/routes/tests/automation.spec.ts index 8cbd14d8b3..80ef08f4c9 100644 --- a/packages/server/src/api/routes/tests/automation.spec.ts +++ b/packages/server/src/api/routes/tests/automation.spec.ts @@ -267,8 +267,7 @@ describe("/automations", () => { } it("updates a automations name", async () => { - let automation = newAutomation() - await config.createAutomation(automation) + const automation = await config.createAutomation(newAutomation()) automation.name = "Updated Name" jest.clearAllMocks() @@ -294,8 +293,7 @@ describe("/automations", () => { }) it("updates a automations name using POST request", async () => { - let automation = newAutomation() - await config.createAutomation(automation) + const automation = await config.createAutomation(newAutomation()) automation.name = "Updated Name" jest.clearAllMocks() @@ -392,8 +390,7 @@ describe("/automations", () => { describe("fetch", () => { it("return all the automations for an instance", async () => { await clearAllAutomations(config) - const autoConfig = basicAutomation() - await config.createAutomation(autoConfig) + const autoConfig = await config.createAutomation(basicAutomation()) const res = await request .get(`/api/automations`) .set(config.defaultHeaders()) diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts index e65ce12873..e00130c617 100644 --- a/packages/server/src/tests/utilities/structures.ts +++ b/packages/server/src/tests/utilities/structures.ts @@ -159,7 +159,7 @@ export function automationTrigger( } export function newAutomation({ steps, trigger }: any = {}) { - const automation: any = basicAutomation() + const automation = basicAutomation() if (trigger) { automation.definition.trigger = trigger From 88aa0fc9cf15f365ac489d05ae6ef28fbd404e75 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 17 Jul 2024 16:40:19 +0200 Subject: [PATCH 46/63] Fix types --- packages/server/src/api/routes/tests/automation.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/api/routes/tests/automation.spec.ts b/packages/server/src/api/routes/tests/automation.spec.ts index 80ef08f4c9..b8a09ec684 100644 --- a/packages/server/src/api/routes/tests/automation.spec.ts +++ b/packages/server/src/api/routes/tests/automation.spec.ts @@ -154,7 +154,7 @@ describe("/automations", () => { tableId: table._id, }, } - automation.appId = config.appId + automation.appId = config.getAppId() automation = await config.createAutomation(automation) await setup.delay(500) const res = await testAutomation(config, automation, { From aea9cda8f5408fe4eab068d89c2b9d1b50cdf761 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 17 Jul 2024 15:45:35 +0100 Subject: [PATCH 47/63] wip --- packages/backend-core/src/sql/sql.ts | 101 ++++++++++-------- packages/backend-core/src/sql/sqlTable.ts | 6 +- .../src/api/routes/tests/search.spec.ts | 35 ++++-- 3 files changed, 90 insertions(+), 52 deletions(-) diff --git a/packages/backend-core/src/sql/sql.ts b/packages/backend-core/src/sql/sql.ts index 4936e4da68..161c2a7488 100644 --- a/packages/backend-core/src/sql/sql.ts +++ b/packages/backend-core/src/sql/sql.ts @@ -42,27 +42,28 @@ const envLimit = environment.SQL_MAX_ROWS : null const BASE_LIMIT = envLimit || 5000 -function likeKey(client: string | string[], key: string): string { - let start: string, end: string +// Takes a string like foo and returns a quoted string like [foo] for SQL Server +// and "foo" for Postgres. +function quote(client: SqlClient, str: string) { switch (client) { - case SqlClient.MY_SQL: - start = end = "`" - break case SqlClient.SQL_LITE: case SqlClient.ORACLE: case SqlClient.POSTGRES: - start = end = '"' - break + return `"${str}"` case SqlClient.MS_SQL: - start = "[" - end = "]" - break + return `[${str}]` default: - throw new Error("Unknown client generating like key") + return `\`${str}\`` } - const parts = key.split(".") - key = parts.map(part => `${start}${part}${end}`).join(".") +} + +// Takes a string like a.b.c and returns a quoted identifier like [a].[b].[c] +// for SQL Server and `a`.`b`.`c` for MySQL. +function quotedIdentifier(client: SqlClient, key: string): string { return key + .split(".") + .map(part => quote(client, part)) + .join(".") } function parse(input: any) { @@ -113,34 +114,37 @@ function generateSelectStatement( knex: Knex ): (string | Knex.Raw)[] | "*" { const { resource, meta } = json + const client = knex.client.config.client as SqlClient if (!resource || !resource.fields || resource.fields.length === 0) { return "*" } - const schema = meta?.table?.schema + const schema = meta.table.schema return resource.fields.map(field => { - const fieldNames = field.split(/\./g) - const tableName = fieldNames[0] - const columnName = fieldNames[1] - const columnSchema = schema?.[columnName] - if (columnSchema && knex.client.config.client === SqlClient.POSTGRES) { - const externalType = schema[columnName].externalType - if (externalType?.includes("money")) { - return knex.raw( - `"${tableName}"."${columnName}"::money::numeric as "${field}"` - ) - } + const [table, column, ..._rest] = field.split(/\./g) + if ( + client === SqlClient.POSTGRES && + schema[column].externalType?.includes("money") + ) { + return knex.raw(`"${table}"."${column}"::money::numeric as "${field}"`) } if ( - knex.client.config.client === SqlClient.MS_SQL && - columnSchema?.type === FieldType.DATETIME && - columnSchema.timeOnly + client === SqlClient.MS_SQL && + schema[column]?.type === FieldType.DATETIME && + schema[column].timeOnly ) { - // Time gets returned as timestamp from mssql, not matching the expected HH:mm format + // Time gets returned as timestamp from mssql, not matching the expected + // HH:mm format return knex.raw(`CONVERT(varchar, ${field}, 108) as "${field}"`) } return `${field} as ${field}` + // return knex.raw( + // `${quote(client, table)}.${quote(client, column)} as ${quote( + // client, + // field + // )}` + // ) }) } @@ -173,9 +177,9 @@ function convertBooleans(query: SqlQuery | SqlQuery[]): SqlQuery | SqlQuery[] { } class InternalBuilder { - private readonly client: string + private readonly client: SqlClient - constructor(client: string) { + constructor(client: SqlClient) { this.client = client } @@ -250,9 +254,10 @@ class InternalBuilder { } else { const rawFnc = `${fnc}Raw` // @ts-ignore - query = query[rawFnc](`LOWER(${likeKey(this.client, key)}) LIKE ?`, [ - `%${value.toLowerCase()}%`, - ]) + query = query[rawFnc]( + `LOWER(${quotedIdentifier(this.client, key)}) LIKE ?`, + [`%${value.toLowerCase()}%`] + ) } } @@ -302,7 +307,10 @@ class InternalBuilder { } statement += (statement ? andOr : "") + - `COALESCE(LOWER(${likeKey(this.client, key)}), '') LIKE ?` + `COALESCE(LOWER(${quotedIdentifier( + this.client, + key + )}), '') LIKE ?` } if (statement === "") { @@ -336,9 +344,10 @@ class InternalBuilder { } else { const rawFnc = `${fnc}Raw` // @ts-ignore - query = query[rawFnc](`LOWER(${likeKey(this.client, key)}) LIKE ?`, [ - `${value.toLowerCase()}%`, - ]) + query = query[rawFnc]( + `LOWER(${quotedIdentifier(this.client, key)}) LIKE ?`, + [`${value.toLowerCase()}%`] + ) } }) } @@ -376,12 +385,15 @@ class InternalBuilder { const fnc = allOr ? "orWhereRaw" : "whereRaw" if (this.client === SqlClient.MS_SQL) { query = query[fnc]( - `CASE WHEN ${likeKey(this.client, key)} = ? THEN 1 ELSE 0 END = 1`, + `CASE WHEN ${quotedIdentifier( + this.client, + key + )} = ? THEN 1 ELSE 0 END = 1`, [value] ) } else { query = query[fnc]( - `COALESCE(${likeKey(this.client, key)} = ?, FALSE)`, + `COALESCE(${quotedIdentifier(this.client, key)} = ?, FALSE)`, [value] ) } @@ -392,12 +404,15 @@ class InternalBuilder { const fnc = allOr ? "orWhereRaw" : "whereRaw" if (this.client === SqlClient.MS_SQL) { query = query[fnc]( - `CASE WHEN ${likeKey(this.client, key)} = ? THEN 1 ELSE 0 END = 0`, + `CASE WHEN ${quotedIdentifier( + this.client, + key + )} = ? THEN 1 ELSE 0 END = 0`, [value] ) } else { query = query[fnc]( - `COALESCE(${likeKey(this.client, key)} != ?, TRUE)`, + `COALESCE(${quotedIdentifier(this.client, key)} != ?, TRUE)`, [value] ) } @@ -769,7 +784,7 @@ class SqlQueryBuilder extends SqlTableQueryBuilder { private readonly limit: number // pass through client to get flavour of SQL - constructor(client: string, limit: number = BASE_LIMIT) { + constructor(client: SqlClient, limit: number = BASE_LIMIT) { super(client) this.limit = limit } diff --git a/packages/backend-core/src/sql/sqlTable.ts b/packages/backend-core/src/sql/sqlTable.ts index bdc8a3dd69..02acc8af85 100644 --- a/packages/backend-core/src/sql/sqlTable.ts +++ b/packages/backend-core/src/sql/sqlTable.ts @@ -195,14 +195,14 @@ function buildDeleteTable(knex: SchemaBuilder, table: Table): SchemaBuilder { } class SqlTableQueryBuilder { - private readonly sqlClient: string + private readonly sqlClient: SqlClient // pass through client to get flavour of SQL - constructor(client: string) { + constructor(client: SqlClient) { this.sqlClient = client } - getSqlClient(): string { + getSqlClient(): SqlClient { return this.sqlClient } diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index ae35c4c5eb..24197462ee 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -38,13 +38,13 @@ import { structures } from "@budibase/backend-core/tests" import { DEFAULT_EMPLOYEE_TABLE_SCHEMA } from "../../../db/defaultData/datasource_bb_default" describe.each([ - ["in-memory", undefined], - ["lucene", undefined], + // ["in-memory", undefined], + // ["lucene", undefined], ["sqs", undefined], - [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], - [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], - [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], - [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], + // [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], + // [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], + // [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], + // [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], ])("search (%s)", (name, dsProvider) => { const isSqs = name === "sqs" const isLucene = name === "lucene" @@ -735,6 +735,29 @@ describe.each([ query: {}, }).toHaveLength(1) }) + + isInternal && + describe("space at end of column name", () => { + beforeAll(async () => { + table = await createTable({ + "name ": { + name: "name ", + type: FieldType.STRING, + }, + }) + await createRows([{ ["name "]: "foo" }, { ["name "]: "bar" }]) + }) + + it("should be able to query a column that starts with a space", async () => { + await expectSearch({ + query: { + string: { + "1:name ": "foo", + }, + }, + }).toContainExactly([{ ["name "]: "foo" }]) + }) + }) }) describe("equal", () => { From f57d8a61471e58760a6cb6f422003bebcb2ab035 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Wed, 17 Jul 2024 17:46:48 +0200 Subject: [PATCH 48/63] Fix jest circular references --- packages/server/src/automations/utils.ts | 79 +---------------- .../server/src/sdk/app/automations/crud.ts | 86 ++++++++++++++++++- 2 files changed, 83 insertions(+), 82 deletions(-) diff --git a/packages/server/src/automations/utils.ts b/packages/server/src/automations/utils.ts index 784632b626..c75cc5e8dc 100644 --- a/packages/server/src/automations/utils.ts +++ b/packages/server/src/automations/utils.ts @@ -7,18 +7,11 @@ import { db as dbCore, context, utils } from "@budibase/backend-core" import { getAutomationMetadataParams } from "../db/utils" import { cloneDeep } from "lodash/fp" import { quotas } from "@budibase/pro" -import { - Automation, - AutomationJob, - Webhook, - WebhookActionType, -} from "@budibase/types" -import sdk from "../sdk" +import { Automation, AutomationJob } from "@budibase/types" import { automationsEnabled } from "../features" import { helpers, REBOOT_CRON } from "@budibase/shared-core" import tracer from "dd-trace" -const WH_STEP_ID = definitions.WEBHOOK.stepId const CRON_STEP_ID = definitions.CRON.stepId let Runner: Thread if (automationsEnabled()) { @@ -229,76 +222,6 @@ export async function enableCronTrigger(appId: any, automation: Automation) { return { enabled, automation } } -/** - * This function handles checking if any webhooks need to be created or deleted for automations. - * @param appId The ID of the app in which we are checking for webhooks - * @param oldAuto The old automation object if updating/deleting - * @param newAuto The new automation object if creating/updating - * @returns After this is complete the new automation object may have been updated and should be - * written to DB (this does not write to DB as it would be wasteful to repeat). - */ -export async function checkForWebhooks({ oldAuto, newAuto }: any) { - const appId = context.getAppId() - if (!appId) { - throw new Error("Unable to check webhooks - no app ID in context.") - } - const oldTrigger = oldAuto ? oldAuto.definition.trigger : null - const newTrigger = newAuto ? newAuto.definition.trigger : null - const triggerChanged = - oldTrigger && newTrigger && oldTrigger.id !== newTrigger.id - function isWebhookTrigger(auto: any) { - return ( - auto && - auto.definition.trigger && - auto.definition.trigger.stepId === WH_STEP_ID - ) - } - // need to delete webhook - if ( - isWebhookTrigger(oldAuto) && - (!isWebhookTrigger(newAuto) || triggerChanged) && - oldTrigger.webhookId - ) { - try { - const db = context.getAppDB() - // need to get the webhook to get the rev - const webhook = await db.get(oldTrigger.webhookId) - // might be updating - reset the inputs to remove the URLs - if (newTrigger) { - delete newTrigger.webhookId - newTrigger.inputs = {} - } - await sdk.automations.webhook.destroy(webhook._id!, webhook._rev!) - } catch (err) { - // don't worry about not being able to delete, if it doesn't exist all good - } - } - // need to create webhook - if ( - (!isWebhookTrigger(oldAuto) || triggerChanged) && - isWebhookTrigger(newAuto) - ) { - const webhook = await sdk.automations.webhook.save( - sdk.automations.webhook.newDoc( - "Automation webhook", - WebhookActionType.AUTOMATION, - newAuto._id - ) - ) - const id = webhook._id - newTrigger.webhookId = id - // the app ID has to be development for this endpoint - // it can only be used when building the app - // but the trigger endpoint will always be used in production - const prodAppId = dbCore.getProdAppID(appId) - newTrigger.inputs = { - schemaUrl: `api/webhooks/schema/${appId}/${id}`, - triggerUrl: `api/webhooks/trigger/${prodAppId}/${id}`, - } - } - return newAuto -} - /** * When removing an app/unpublishing it need to make sure automations are cleaned up (cron). * @param appId the app that is being removed. diff --git a/packages/server/src/sdk/app/automations/crud.ts b/packages/server/src/sdk/app/automations/crud.ts index ec686cc868..c0f3df6f28 100644 --- a/packages/server/src/sdk/app/automations/crud.ts +++ b/packages/server/src/sdk/app/automations/crud.ts @@ -1,9 +1,15 @@ -import { context, events, HTTPError } from "@budibase/backend-core" -import { Automation } from "@budibase/types" -import { checkForWebhooks } from "../../../automations/utils" -import { MetadataTypes } from "../../../constants" +import { Automation, Webhook, WebhookActionType } from "@budibase/types" import { generateAutomationID, getAutomationParams } from "../../../db/utils" import { deleteEntityMetadata } from "../../../utilities" +import { MetadataTypes } from "../../../constants" +import { + context, + events, + HTTPError, + db as dbCore, +} from "@budibase/backend-core" +import { definitions } from "../../../automations/triggerInfo" +import automations from "." function getDb() { return context.getAppDB() @@ -168,3 +174,75 @@ export async function remove(automationId: string, rev: string) { return result } + +/** + * This function handles checking if any webhooks need to be created or deleted for automations. + * @param appId The ID of the app in which we are checking for webhooks + * @param oldAuto The old automation object if updating/deleting + * @param newAuto The new automation object if creating/updating + * @returns After this is complete the new automation object may have been updated and should be + * written to DB (this does not write to DB as it would be wasteful to repeat). + */ +async function checkForWebhooks({ oldAuto, newAuto }: any) { + const WH_STEP_ID = definitions.WEBHOOK.stepId + + const appId = context.getAppId() + if (!appId) { + throw new Error("Unable to check webhooks - no app ID in context.") + } + const oldTrigger = oldAuto ? oldAuto.definition.trigger : null + const newTrigger = newAuto ? newAuto.definition.trigger : null + const triggerChanged = + oldTrigger && newTrigger && oldTrigger.id !== newTrigger.id + function isWebhookTrigger(auto: any) { + return ( + auto && + auto.definition.trigger && + auto.definition.trigger.stepId === WH_STEP_ID + ) + } + // need to delete webhook + if ( + isWebhookTrigger(oldAuto) && + (!isWebhookTrigger(newAuto) || triggerChanged) && + oldTrigger.webhookId + ) { + try { + const db = getDb() + // need to get the webhook to get the rev + const webhook = await db.get(oldTrigger.webhookId) + // might be updating - reset the inputs to remove the URLs + if (newTrigger) { + delete newTrigger.webhookId + newTrigger.inputs = {} + } + await automations.webhook.destroy(webhook._id!, webhook._rev!) + } catch (err) { + // don't worry about not being able to delete, if it doesn't exist all good + } + } + // need to create webhook + if ( + (!isWebhookTrigger(oldAuto) || triggerChanged) && + isWebhookTrigger(newAuto) + ) { + const webhook = await automations.webhook.save( + automations.webhook.newDoc( + "Automation webhook", + WebhookActionType.AUTOMATION, + newAuto._id + ) + ) + const id = webhook._id + newTrigger.webhookId = id + // the app ID has to be development for this endpoint + // it can only be used when building the app + // but the trigger endpoint will always be used in production + const prodAppId = dbCore.getProdAppID(appId) + newTrigger.inputs = { + schemaUrl: `api/webhooks/schema/${appId}/${id}`, + triggerUrl: `api/webhooks/trigger/${prodAppId}/${id}`, + } + } + return newAuto +} From 7fd55fe27df9c0079295c072a7c98d5b1e89bd90 Mon Sep 17 00:00:00 2001 From: Peter Clement Date: Thu, 18 Jul 2024 10:38:15 +0100 Subject: [PATCH 49/63] Automation trigger filtering (#14123) * backend for triggering automation based on filters * frontend for handling triggering automations on filter / old row * lint and bug fix * fix issue with test header * make test data optional * improve safety on trigger gate * add support for running trigger with filter if no change happened but filter matches * update var naming to actually make sense * tests * fix lint * improve gating for shouldTrigger check * remove unecessary cast * unecessary tableId check * frontend text updates * resolving comments * pro * Update packages/types/src/documents/app/automation.ts Co-authored-by: Sam Rose * link out to docs for trigger filtering * fix pro * more pr comments * use getAppId --------- Co-authored-by: Sam Rose --- .../FlowChart/FlowItemHeader.svelte | 6 +- .../AutomationBuilder/TestDisplay.svelte | 44 +++++- .../SetupPanel/AutomationBlockSetup.svelte | 131 +++++++++++++----- .../FilterEditor/FilterBuilder.svelte | 3 +- .../builder/src/stores/builder/automations.js | 2 +- .../src/components/FilterBuilder.svelte | 4 +- .../src/api/routes/tests/automation.spec.ts | 93 ++++++++++++- .../src/automations/triggerInfo/rowSaved.ts | 5 + .../src/automations/triggerInfo/rowUpdated.ts | 5 + packages/server/src/automations/triggers.ts | 60 +++++++- .../server/src/tests/utilities/structures.ts | 59 +++++++- .../types/src/documents/app/automation.ts | 14 ++ packages/types/src/sdk/automations/index.ts | 4 +- 13 files changed, 364 insertions(+), 66 deletions(-) diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte index a409d85305..5533572511 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte @@ -16,13 +16,12 @@ export let enableNaming = true let validRegex = /^[A-Za-z0-9_\s]+$/ let typing = false - const dispatch = createEventDispatcher() $: stepNames = $selectedAutomation?.definition.stepNames $: automationName = stepNames?.[block.id] || block?.name || "" $: automationNameError = getAutomationNameError(automationName) - $: status = updateStatus(testResult, isTrigger) + $: status = updateStatus(testResult) $: isHeaderTrigger = isTrigger || block.type === "TRIGGER" $: { @@ -43,7 +42,7 @@ }) } - function updateStatus(results, isTrigger) { + function updateStatus(results) { if (!results) { return {} } @@ -56,7 +55,6 @@ return { negative: true, message: "Error" } } } - const getAutomationNameError = name => { if (stepNames) { for (const [key, value] of Object.entries(stepNames)) { diff --git a/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte b/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte index 2cad22c820..8487b7a519 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte @@ -12,14 +12,31 @@ let blocks function prepTestResults(results) { - return results?.steps.filter(x => x.stepId !== ActionStepID.LOOP || []) + if (results.message) { + return [ + { + inputs: {}, + outputs: { + success: results.outputs?.success || false, + status: results.outputs?.status || "unknown", + message: results.message, + }, + }, + ] + } else { + return results?.steps?.filter(x => x.stepId !== ActionStepID.LOOP) || [] + } } $: filteredResults = prepTestResults(testResults) $: { - blocks = [] - if (automation) { + if (testResults.message) { + blocks = automation?.definition?.trigger + ? [automation.definition.trigger] + : [] + } else if (automation) { + blocks = [] if (automation.definition.trigger) { blocks.push(automation.definition.trigger) } @@ -46,7 +63,9 @@ open={!!openBlocks[block.id]} on:toggle={() => (openBlocks[block.id] = !openBlocks[block.id])} isTrigger={idx === 0} - testResult={filteredResults?.[idx]} + testResult={testResults.message + ? testResults + : filteredResults?.[idx]} showTestStatus {block} {idx} @@ -68,7 +87,9 @@
- {#if filteredResults?.[idx]?.inputs} + {#if testResults.message} + No input + {:else if filteredResults?.[idx]?.inputs} {:else} No input @@ -77,13 +98,22 @@
- {#if filteredResults?.[idx]?.outputs} + {#if testResults.message} + + {:else if filteredResults?.[idx]?.outputs} {:else} - No input + No output {/if}
diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index 283bd14fc0..8a9d1e59ea 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -17,7 +17,9 @@ Helpers, Toggle, Divider, + Icon, } from "@budibase/bbui" + import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import { automationStore, selectedAutomation, tables } from "stores/builder" import { environment, licensing } from "stores/portal" @@ -365,41 +367,74 @@ /** * Handler for row trigger automation updates. - @param {object} update - An automation block.inputs update object - @example - onRowTriggerUpdate({ - "tableId" : "ta_bb_employee" - }) + * @param {object} update - An automation block.inputs update object + * @param {string} [update.tableId] - The ID of the table + * @param {object} [update.filters] - Filter configuration for the row trigger + * @param {object} [update.filters-def] - Filter definitions for the row trigger + * @example + * // Example with tableId + * onRowTriggerUpdate({ + * "tableId" : "ta_bb_employee" + * }) + * @example + * // Example with filters + * onRowTriggerUpdate({ + * filters: { + * equal: { "1:Approved": "true" } + * }, + * "filters-def": [{ + * id: "oH1T4S49n", + * field: "1:Approved", + * operator: "equal", + * value: "true", + * valueType: "Value", + * type: "string" + * }] + * }) */ const onRowTriggerUpdate = async update => { if ( - Object.hasOwn(update, "tableId") && - $selectedAutomation.testData?.row?.tableId !== update.tableId + ["tableId", "filters", "meta"].some(key => Object.hasOwn(update, key)) ) { try { - const reqSchema = getSchemaForDatasourcePlus(update.tableId, { - searchableSchema: true, - }).schema + let updatedAutomation - // Parse the block inputs as usual - const updatedAutomation = - await automationStore.actions.processBlockInputs(block, { - schema: reqSchema, - ...update, - }) + if ( + Object.hasOwn(update, "tableId") && + $selectedAutomation.testData?.row?.tableId !== update.tableId + ) { + const reqSchema = getSchemaForDatasourcePlus(update.tableId, { + searchableSchema: true, + }).schema - // Save the entire automation and reset the testData - await automationStore.actions.save({ - ...updatedAutomation, - testData: { - // Reset Core fields - row: { tableId: update.tableId }, - oldRow: { tableId: update.tableId }, - meta: {}, - id: "", - revision: "", - }, - }) + updatedAutomation = await automationStore.actions.processBlockInputs( + block, + { + schema: reqSchema, + ...update, + } + ) + + // Reset testData when tableId changes + updatedAutomation = { + ...updatedAutomation, + testData: { + row: { tableId: update.tableId }, + oldRow: { tableId: update.tableId }, + meta: {}, + id: "", + revision: "", + }, + } + } else { + // For filters update, just process block inputs without resetting testData + updatedAutomation = await automationStore.actions.processBlockInputs( + block, + update + ) + } + + await automationStore.actions.save(updatedAutomation) return } catch (e) { @@ -408,7 +443,6 @@ } } } - /** * Handler for App trigger automation updates. * Ensure updates to the field list are reflected in testData @@ -743,6 +777,7 @@ value.customType !== "triggerSchema" && value.customType !== "automationFields" && value.customType !== "fields" && + value.customType !== "trigger_filter_setting" && value.type !== "signature_single" && value.type !== "attachment" && value.type !== "attachment_single" @@ -807,13 +842,23 @@ {@const label = getFieldLabel(key, value)}
{#if key !== "fields" && value.type !== "boolean" && shouldRenderField(value)} - +
+ + {#if value.customType === "trigger_filter"} + + window.open( + "https://docs.budibase.com/docs/row-trigger-filters", + "_blank" + )} + size="XS" + name="InfoOutline" + /> + {/if} +
{/if}
{#if value.type === "string" && value.enum && canShowField(key, value)} @@ -932,8 +977,12 @@ {/if}
- {:else if value.customType === "filters"} - Define filters + {:else if value.customType === "filters" || value.customType === "trigger_filter"} + {filters.length > 0 + ? "Update Filter" + : "No Filter set"}