diff --git a/packages/server/src/api/controllers/view/viewsV2.ts b/packages/server/src/api/controllers/view/viewsV2.ts index 94e53e52fb..9a4949bb43 100644 --- a/packages/server/src/api/controllers/view/viewsV2.ts +++ b/packages/server/src/api/controllers/view/viewsV2.ts @@ -1,5 +1,10 @@ import sdk from "../../../sdk" -import { CreateViewRequest, Ctx, ViewResponse } from "@budibase/types" +import { + CreateViewRequest, + Ctx, + UpdateViewRequest, + ViewResponse, +} from "@budibase/types" export async function create(ctx: Ctx) { const view = ctx.request.body @@ -12,6 +17,25 @@ export async function create(ctx: Ctx) { } } +export async function update(ctx: Ctx) { + const view = ctx.request.body + + if (view.version !== 2) { + ctx.throw(400, "Only views V2 can be updated") + } + + if (ctx.params.viewId !== view.id) { + ctx.throw(400, "View id does not match between the body and the uri path") + } + + const { tableId } = view + + const result = await sdk.views.update(tableId, view) + ctx.body = { + data: result, + } +} + export async function remove(ctx: Ctx) { const { viewId } = ctx.params diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 480580eb86..e728af3e40 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -86,6 +86,124 @@ describe("/v2/views", () => { }) }) + describe("update", () => { + let view: ViewV2 + + beforeEach(async () => { + await config.createTable(priceTable()) + view = await config.api.viewV2.create({ name: "View A" }) + }) + + it("can update an existing view data", async () => { + const tableId = config.table!._id! + await config.api.viewV2.update({ + ...view, + query: { equal: { newField: "thatValue" } }, + }) + + expect(await config.api.table.get(tableId)).toEqual({ + ...config.table, + views: { + [view.name]: { + ...view, + query: { equal: { newField: "thatValue" } }, + schema: expect.anything(), + }, + }, + _rev: expect.any(String), + updatedAt: expect.any(String), + }) + }) + + it("can update an existing view name", async () => { + const tableId = config.table!._id! + await config.api.viewV2.update({ ...view, name: "View B" }) + + expect(await config.api.table.get(tableId)).toEqual( + expect.objectContaining({ + views: { + "View B": { ...view, name: "View B", schema: expect.anything() }, + }, + }) + ) + }) + + it("cannot update an unexisting views nor edit ids", async () => { + const tableId = config.table!._id! + await config.api.viewV2.update( + { ...view, id: generator.guid() }, + { expectStatus: 404 } + ) + + expect(await config.api.table.get(tableId)).toEqual( + expect.objectContaining({ + views: { + [view.name]: { + ...view, + schema: expect.anything(), + }, + }, + }) + ) + }) + + it("cannot update views with the wrong tableId", async () => { + const tableId = config.table!._id! + await config.api.viewV2.update( + { + ...view, + tableId: generator.guid(), + query: { equal: { newField: "thatValue" } }, + }, + { expectStatus: 404 } + ) + + expect(await config.api.table.get(tableId)).toEqual( + expect.objectContaining({ + views: { + [view.name]: { + ...view, + schema: expect.anything(), + }, + }, + }) + ) + }) + + it("cannot update views v1", async () => { + const viewV1 = await config.createView() + await config.api.viewV2.update( + { + ...viewV1, + }, + { + expectStatus: 400, + handleResponse: r => { + expect(r.body).toEqual({ + message: "Only views V2 can be updated", + status: 400, + }) + }, + } + ) + }) + + it("cannot update the a view with unmatching ids between url and body", async () => { + const anotherView = await config.api.viewV2.create() + const result = await config + .request!.put(`/api/v2/views/${anotherView.id}`) + .send(view) + .set(config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(400) + + expect(result.body).toEqual({ + message: "View id does not match between the body and the uri path", + status: 400, + }) + }) + }) + describe("delete", () => { let view: ViewV2 diff --git a/packages/server/src/api/routes/view.ts b/packages/server/src/api/routes/view.ts index f8ae4abf0d..18c58134b4 100644 --- a/packages/server/src/api/routes/view.ts +++ b/packages/server/src/api/routes/view.ts @@ -13,6 +13,11 @@ router authorized(permissions.BUILDER), viewController.v2.create ) + .put( + `/api/v2/views/:viewId`, + authorized(permissions.BUILDER), + viewController.v2.update + ) .delete( `/api/v2/views/:viewId`, authorized(permissions.BUILDER), diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index 17691c999d..5bd38cf2b2 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -33,6 +33,24 @@ export async function create( return view } +export async function update(tableId: string, view: ViewV2): Promise { + const db = context.getAppDB() + const table = await sdk.tables.getTable(tableId) + table.views ??= {} + + const existingView = Object.values(table.views).find( + v => isV2(v) && v.id === view.id + ) + if (!existingView) { + throw new HTTPError(`View ${view.id} not found in table ${tableId}`, 404) + } + + delete table.views[existingView.name] + table.views[view.name] = view + await db.put(table) + return view +} + export function isV2(view: View | ViewV2): view is ViewV2 { return (view as ViewV2).version === 2 } diff --git a/packages/server/src/tests/utilities/api/viewV2.ts b/packages/server/src/tests/utilities/api/viewV2.ts index 15111ad977..fae0850f79 100644 --- a/packages/server/src/tests/utilities/api/viewV2.ts +++ b/packages/server/src/tests/utilities/api/viewV2.ts @@ -1,7 +1,8 @@ -import { SortOrder, SortType, ViewV2 } from "@budibase/types" +import { CreateViewRequest, SortOrder, SortType, ViewV2 } from "@budibase/types" import TestConfiguration from "../TestConfiguration" import { TestAPI } from "./base" import { generator } from "@budibase/backend-core/tests" +import { Response } from "superagent" export class ViewV2API extends TestAPI { constructor(config: TestConfiguration) { @@ -9,7 +10,7 @@ export class ViewV2API extends TestAPI { } create = async ( - viewData?: Partial, + viewData?: Partial, { expectStatus } = { expectStatus: 201 } ): Promise => { let tableId = viewData?.tableId @@ -31,6 +32,29 @@ export class ViewV2API extends TestAPI { return result.body.data as ViewV2 } + update = async ( + view: ViewV2, + { + expectStatus, + handleResponse, + }: { + expectStatus: number + handleResponse?: (response: Response) => void + } = { expectStatus: 200 } + ): Promise => { + const result = await this.request + .put(`/api/v2/views/${view.id}`) + .send(view) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(expectStatus) + + if (handleResponse) { + handleResponse(result) + } + return result.body.data as ViewV2 + } + delete = async (viewId: string, { expectStatus } = { expectStatus: 204 }) => { return this.request .delete(`/api/v2/views/${viewId}`) diff --git a/packages/types/src/api/web/app/view.ts b/packages/types/src/api/web/app/view.ts index e5c5855c1b..6b516c0314 100644 --- a/packages/types/src/api/web/app/view.ts +++ b/packages/types/src/api/web/app/view.ts @@ -5,3 +5,5 @@ export interface ViewResponse { } export type CreateViewRequest = Omit + +export type UpdateViewRequest = ViewV2