From d440291ebc5ddb9b6c439af44db3d6b38c69ef22 Mon Sep 17 00:00:00 2001
From: Adria Navarro <adria@budibase.com>
Date: Tue, 1 Aug 2023 11:31:58 +0200
Subject: [PATCH] Throw exception when updating non ui fields

---
 .../src/api/controllers/view/viewsV2.ts       | 45 ++++++++++++-
 .../src/api/routes/tests/viewV2.spec.ts       | 64 ++++++++++++++++---
 2 files changed, 98 insertions(+), 11 deletions(-)

diff --git a/packages/server/src/api/controllers/view/viewsV2.ts b/packages/server/src/api/controllers/view/viewsV2.ts
index c872abba39..f403d14180 100644
--- a/packages/server/src/api/controllers/view/viewsV2.ts
+++ b/packages/server/src/api/controllers/view/viewsV2.ts
@@ -9,9 +9,40 @@ import {
   RequiredKeys,
 } from "@budibase/types"
 
-export async function create(ctx: Ctx<CreateViewRequest, ViewResponse>) {
-  const view = ctx.request.body
-  const { tableId } = view
+async function parseSchemaUI(ctx: Ctx, view: CreateViewRequest) {
+  if (!view.schema) {
+    return
+  }
+
+  function hasOverrides(
+    newObj: Record<string, any>,
+    existingObj: Record<string, any>
+  ) {
+    for (const [key, value] of Object.entries(newObj)) {
+      if (typeof value === "object") {
+        if (hasOverrides(value, existingObj[key] || {})) {
+          return true
+        }
+      } else if (value !== existingObj[key]) {
+        return true
+      }
+    }
+    return false
+  }
+
+  const table = await sdk.tables.getTable(view.tableId)
+  for (const [
+    fieldName,
+    { order, width, visible, icon, ...schemaNonUI },
+  ] of Object.entries(view.schema)) {
+    const overrides = hasOverrides(schemaNonUI, table.schema[fieldName])
+    if (overrides) {
+      ctx.throw(
+        400,
+        "This endpoint does not support overriding non UI fields in the schema"
+      )
+    }
+  }
 
   const schemaUI =
     view.schema &&
@@ -24,6 +55,14 @@ export async function create(ctx: Ctx<CreateViewRequest, ViewResponse>) {
       }
       return p
     }, {} as Record<string, RequiredKeys<UIFieldMetadata>>)
+  return schemaUI
+}
+
+export async function create(ctx: Ctx<CreateViewRequest, ViewResponse>) {
+  const view = ctx.request.body
+  const { tableId } = view
+
+  const schemaUI = await parseSchemaUI(ctx, view)
 
   const parsedView: Omit<ViewV2, "id" | "version"> = {
     name: view.name,
diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts
index 14fe90b93e..e60e9a5126 100644
--- a/packages/server/src/api/routes/tests/viewV2.spec.ts
+++ b/packages/server/src/api/routes/tests/viewV2.spec.ts
@@ -95,15 +95,15 @@ describe("/v2/views", () => {
         name: generator.name(),
         tableId: config.table!._id!,
         schema: {
-          name: {
-            name: "name",
-            type: FieldType.STRING,
+          Price: {
+            name: "Price",
+            type: FieldType.NUMBER,
             visible: true,
             order: 1,
             width: 100,
           },
-          lastname: {
-            name: "lastname",
+          Category: {
+            name: "Category",
             type: FieldType.STRING,
             visible: false,
             icon: "ic",
@@ -116,14 +116,14 @@ describe("/v2/views", () => {
       expect(await config.api.viewV2.get(createdView.id)).toEqual({
         ...newView,
         schema: undefined,
-        columns: ["name", "lastname"],
+        columns: ["Price", "Category"],
         schemaUI: {
-          name: {
+          Price: {
             visible: true,
             order: 1,
             width: 100,
           },
-          lastname: {
+          Category: {
             visible: false,
             icon: "ic",
           },
@@ -132,6 +132,54 @@ describe("/v2/views", () => {
         version: 2,
       })
     })
+
+    it("throw an exception if the schema overrides a non UI field", async () => {
+      const newView: CreateViewRequest = {
+        name: generator.name(),
+        tableId: config.table!._id!,
+        schema: {
+          Price: {
+            name: "Price",
+            type: FieldType.NUMBER,
+            visible: true,
+          },
+          Category: {
+            name: "Category",
+            type: FieldType.STRING,
+            constraints: {
+              type: "string",
+              presence: true,
+            },
+          },
+        },
+      }
+
+      await config.api.viewV2.create(newView, {
+        expectStatus: 400,
+      })
+    })
+
+    it("will not throw an exception if the schema is 'deleting' non UI fields", async () => {
+      const newView: CreateViewRequest = {
+        name: generator.name(),
+        tableId: config.table!._id!,
+        schema: {
+          Price: {
+            name: "Price",
+            type: FieldType.NUMBER,
+            visible: true,
+          },
+          Category: {
+            name: "Category",
+            type: FieldType.STRING,
+          },
+        },
+      }
+
+      await config.api.viewV2.create(newView, {
+        expectStatus: 201,
+      })
+    })
   })
 
   describe("update", () => {