diff --git a/lerna.json b/lerna.json index 8a0649b8bb..1514e668d2 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.9.24-alpha.3", + "version": "2.9.24-alpha.4", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/server/src/api/controllers/table/external.ts b/packages/server/src/api/controllers/table/external.ts index eb1eeb7256..327904666d 100644 --- a/packages/server/src/api/controllers/table/external.ts +++ b/packages/server/src/api/controllers/table/external.ts @@ -22,9 +22,12 @@ import { QueryJson, RelationshipType, RenameColumn, + SaveTableRequest, + SaveTableResponse, Table, TableRequest, UserCtx, + ViewV2, } from "@budibase/types" import sdk from "../../../sdk" import { builderSocket } from "../../../websockets" @@ -198,8 +201,8 @@ function isRelationshipSetup(column: FieldSchema) { return column.foreignKey || column.through } -export async function save(ctx: UserCtx) { - const inputs: TableRequest = ctx.request.body +export async function save(ctx: UserCtx) { + const inputs = ctx.request.body const renamed = inputs?._rename // can't do this right now delete inputs.rows @@ -215,7 +218,7 @@ export async function save(ctx: UserCtx) { ...inputs, } - let oldTable + let oldTable: Table | undefined if (ctx.request.body && ctx.request.body._id) { oldTable = await sdk.tables.getTable(ctx.request.body._id) } @@ -224,6 +227,17 @@ export async function save(ctx: UserCtx) { ctx.throw(400, "A column type has changed.") } + for (let view in tableToSave.views) { + const tableView = tableToSave.views[view] + if (!tableView || !sdk.views.isV2(tableView)) continue + + tableToSave.views[view] = sdk.views.syncSchema( + oldTable!.views![view] as ViewV2, + tableToSave.schema, + renamed + ) + } + const db = context.getAppDB() const datasource = await sdk.datasources.get(datasourceId) if (!datasource.entities) { diff --git a/packages/server/src/api/controllers/table/index.ts b/packages/server/src/api/controllers/table/index.ts index 53202d6878..e44ac94881 100644 --- a/packages/server/src/api/controllers/table/index.ts +++ b/packages/server/src/api/controllers/table/index.ts @@ -9,6 +9,8 @@ import { isExternalTable, isSQL } from "../../../integrations/utils" import { events } from "@budibase/backend-core" import { FetchTablesResponse, + SaveTableResponse, + SaveTableRequest, Table, TableResponse, UserCtx, @@ -60,7 +62,7 @@ export async function find(ctx: UserCtx) { ctx.body = sdk.tables.enrichViewSchemas(table) } -export async function save(ctx: UserCtx) { +export async function save(ctx: UserCtx) { const appId = ctx.appId const table = ctx.request.body const isImport = table.rows diff --git a/packages/server/src/api/controllers/table/internal.ts b/packages/server/src/api/controllers/table/internal.ts index 7317d11e18..5f9a01bd0b 100644 --- a/packages/server/src/api/controllers/table/internal.ts +++ b/packages/server/src/api/controllers/table/internal.ts @@ -9,7 +9,15 @@ import { fixAutoColumnSubType, } from "../../../utilities/rowProcessor" import { runStaticFormulaChecks } from "./bulkFormula" -import { Table } from "@budibase/types" +import { + SaveTableRequest, + SaveTableResponse, + Table, + TableRequest, + UserCtx, + ViewStatisticsSchema, + ViewV2, +} from "@budibase/types" import { quotas } from "@budibase/pro" import isEqual from "lodash/isEqual" import { cloneDeep } from "lodash/fp" @@ -33,10 +41,10 @@ function checkAutoColumns(table: Table, oldTable?: Table) { return table } -export async function save(ctx: any) { +export async function save(ctx: UserCtx) { const db = context.getAppDB() const { rows, ...rest } = ctx.request.body - let tableToSave = { + let tableToSave: TableRequest = { type: "table", _id: generateTableID(), views: {}, @@ -44,7 +52,7 @@ export async function save(ctx: any) { } // if the table obj had an _id then it will have been retrieved - let oldTable + let oldTable: Table | undefined if (ctx.request.body && ctx.request.body._id) { oldTable = await sdk.tables.getTable(ctx.request.body._id) } @@ -80,7 +88,7 @@ export async function save(ctx: any) { let { _rename } = tableToSave /* istanbul ignore next */ if (_rename && _rename.old === _rename.updated) { - _rename = null + _rename = undefined delete tableToSave._rename } @@ -97,7 +105,20 @@ export async function save(ctx: any) { const tableView = tableToSave.views[view] if (!tableView) continue - if (tableView.schema.group || tableView.schema.field) continue + if (sdk.views.isV2(tableView)) { + tableToSave.views[view] = sdk.views.syncSchema( + oldTable!.views![view] as ViewV2, + tableToSave.schema, + _rename + ) + continue + } + + if ( + (tableView.schema as ViewStatisticsSchema).group || + tableView.schema.field + ) + continue tableView.schema = tableToSave.schema } @@ -112,7 +133,7 @@ export async function save(ctx: any) { tableToSave._rev = linkResp._rev } } catch (err) { - ctx.throw(400, err) + ctx.throw(400, err as string) } // don't perform any updates until relationships have been diff --git a/packages/server/src/api/controllers/table/utils.ts b/packages/server/src/api/controllers/table/utils.ts index e1469bb267..0e5b784c66 100644 --- a/packages/server/src/api/controllers/table/utils.ts +++ b/packages/server/src/api/controllers/table/utils.ts @@ -418,7 +418,7 @@ export function areSwitchableTypes(type1: any, type2: any) { return false } -export function hasTypeChanged(table: any, oldTable: any) { +export function hasTypeChanged(table: Table, oldTable: Table | undefined) { if (!oldTable) { return false } diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index 637caa06ee..a0776510cc 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -1,5 +1,11 @@ +import { + FieldSchema, + RenameColumn, + TableSchema, + View, + ViewV2, +} from "@budibase/types" import { context, HTTPError } from "@budibase/backend-core" -import { FieldSchema, TableSchema, View, ViewV2 } from "@budibase/types" import sdk from "../../../sdk" import * as utils from "../../../db/utils" @@ -103,3 +109,37 @@ export function enrichSchema(view: View | ViewV2, tableSchema: TableSchema) { schema: schema, } } + +export function syncSchema( + view: ViewV2, + schema: TableSchema, + renameColumn: RenameColumn | undefined +): ViewV2 { + if (renameColumn) { + if (view.columns) { + view.columns[view.columns.indexOf(renameColumn.old)] = + renameColumn.updated + } + if (view.schemaUI) { + view.schemaUI[renameColumn.updated] = view.schemaUI[renameColumn.old] + delete view.schemaUI[renameColumn.old] + } + } + + if (view.schemaUI) { + for (const fieldName of Object.keys(view.schemaUI)) { + if (!schema[fieldName]) { + delete view.schemaUI[fieldName] + } + } + for (const fieldName of Object.keys(schema)) { + if (!view.schemaUI[fieldName]) { + view.schemaUI[fieldName] = { visible: false } + } + } + } + + view.columns = view.columns?.filter(x => schema[x]) + + return view +} diff --git a/packages/server/src/sdk/app/views/tests/views.spec.ts b/packages/server/src/sdk/app/views/tests/views.spec.ts index 3b1cb84a42..d3d938f9cf 100644 --- a/packages/server/src/sdk/app/views/tests/views.spec.ts +++ b/packages/server/src/sdk/app/views/tests/views.spec.ts @@ -1,53 +1,54 @@ -import { FieldType, Table, ViewV2 } from "@budibase/types" +import _ from "lodash" +import { FieldType, Table, TableSchema, ViewV2 } from "@budibase/types" import { generator } from "@budibase/backend-core/tests" -import { enrichSchema } from ".." +import { enrichSchema, syncSchema } from ".." describe("table sdk", () => { - describe("enrichViewSchemas", () => { - const basicTable: Table = { - _id: generator.guid(), - name: "TestTable", - type: "table", - schema: { - name: { - type: FieldType.STRING, - name: "name", - visible: true, - width: 80, - order: 2, - constraints: { - type: "string", - }, - }, - description: { - type: FieldType.STRING, - name: "description", - visible: true, - width: 200, - constraints: { - type: "string", - }, - }, - id: { - type: FieldType.NUMBER, - name: "id", - visible: true, - order: 1, - constraints: { - type: "number", - }, - }, - hiddenField: { - type: FieldType.STRING, - name: "hiddenField", - visible: false, - constraints: { - type: "string", - }, + const basicTable: Table = { + _id: generator.guid(), + name: "TestTable", + type: "table", + schema: { + name: { + type: FieldType.STRING, + name: "name", + visible: true, + width: 80, + order: 2, + constraints: { + type: "string", }, }, - } + description: { + type: FieldType.STRING, + name: "description", + visible: true, + width: 200, + constraints: { + type: "string", + }, + }, + id: { + type: FieldType.NUMBER, + name: "id", + visible: true, + order: 1, + constraints: { + type: "number", + }, + }, + hiddenField: { + type: FieldType.STRING, + name: "hiddenField", + visible: false, + constraints: { + type: "string", + }, + }, + }, + } + describe("enrichViewSchemas", () => { it("should fetch the default schema if not overriden", async () => { const tableId = basicTable._id! const view: ViewV2 = { @@ -280,4 +281,294 @@ describe("table sdk", () => { ) }) }) + + describe("syncSchema", () => { + const basicView: ViewV2 = { + version: 2, + id: generator.guid(), + name: generator.guid(), + tableId: basicTable._id!, + } + + describe("view without schema", () => { + it("no table schema changes will not amend the view", () => { + const view: ViewV2 = { + ...basicView, + columns: ["name", "id", "description"], + } + const result = syncSchema( + _.cloneDeep(view), + basicTable.schema, + undefined + ) + expect(result).toEqual(view) + }) + + it("adding new columns will not change the view schema", () => { + const view: ViewV2 = { + ...basicView, + columns: ["name", "id", "description"], + } + + const newTableSchema = { + ...basicTable.schema, + newField1: { + type: FieldType.STRING, + name: "newField1", + visible: true, + }, + newField2: { + type: FieldType.NUMBER, + name: "newField2", + visible: false, + }, + } + + const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined) + expect(result).toEqual({ + ...view, + schemaUI: undefined, + }) + }) + + it("deleting columns will not change the view schema", () => { + const view: ViewV2 = { + ...basicView, + columns: ["name", "id", "description"], + } + const { name, description, ...newTableSchema } = basicTable.schema + + const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined) + expect(result).toEqual({ + ...view, + columns: ["id"], + schemaUI: undefined, + }) + }) + + it("renaming mapped columns will update the view column mapping", () => { + const view: ViewV2 = { + ...basicView, + columns: ["name", "id", "description"], + } + const { description, ...newTableSchema } = { + ...basicTable.schema, + updatedDescription: { + ...basicTable.schema.description, + name: "updatedDescription", + }, + } as TableSchema + + const result = syncSchema(_.cloneDeep(view), newTableSchema, { + old: "description", + updated: "updatedDescription", + }) + expect(result).toEqual({ + ...view, + columns: ["name", "id", "updatedDescription"], + schemaUI: undefined, + }) + }) + }) + + describe("view with schema", () => { + it("no table schema changes will not amend the view", () => { + const view: ViewV2 = { + ...basicView, + columns: ["name", "id", "description"], + schemaUI: { + name: { visible: true, width: 100 }, + id: { visible: true, width: 20 }, + description: { visible: false }, + hiddenField: { visible: false }, + }, + } + const result = syncSchema( + _.cloneDeep(view), + basicTable.schema, + undefined + ) + expect(result).toEqual(view) + }) + + it("adding new columns will add them as not visible to the view", () => { + const view: ViewV2 = { + ...basicView, + columns: ["name", "id", "description"], + schemaUI: { + name: { visible: true, width: 100 }, + id: { visible: true, width: 20 }, + description: { visible: false }, + hiddenField: { visible: false }, + }, + } + + const newTableSchema = { + ...basicTable.schema, + newField1: { + type: FieldType.STRING, + name: "newField1", + visible: true, + }, + newField2: { + type: FieldType.NUMBER, + name: "newField2", + visible: false, + }, + } + + const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined) + expect(result).toEqual({ + ...view, + schemaUI: { + ...view.schemaUI, + newField1: { visible: false }, + newField2: { visible: false }, + }, + }) + }) + + it("deleting columns will remove them from the UI", () => { + const view: ViewV2 = { + ...basicView, + columns: ["name", "id", "description"], + schemaUI: { + name: { visible: true, width: 100 }, + id: { visible: true, width: 20 }, + description: { visible: false }, + hiddenField: { visible: false }, + }, + } + const { name, description, ...newTableSchema } = basicTable.schema + + const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined) + expect(result).toEqual({ + ...view, + columns: ["id"], + schemaUI: { + ...view.schemaUI, + name: undefined, + description: undefined, + }, + }) + }) + + it("can handle additions and deletions at the same them UI", () => { + const view: ViewV2 = { + ...basicView, + columns: ["name", "id", "description"], + schemaUI: { + name: { visible: true, width: 100 }, + id: { visible: true, width: 20 }, + description: { visible: false }, + hiddenField: { visible: false }, + }, + } + const { name, description, ...newTableSchema } = { + ...basicTable.schema, + newField1: { + type: FieldType.STRING, + name: "newField1", + visible: true, + }, + } as TableSchema + + const result = syncSchema(_.cloneDeep(view), newTableSchema, undefined) + expect(result).toEqual({ + ...view, + columns: ["id"], + schemaUI: { + ...view.schemaUI, + name: undefined, + description: undefined, + newField1: { visible: false }, + }, + }) + }) + + it("renaming mapped columns will update the view column mapping and it's schema", () => { + const view: ViewV2 = { + ...basicView, + columns: ["name", "id", "description"], + schemaUI: { + name: { visible: true }, + id: { visible: true }, + description: { visible: true, width: 150, icon: "ic-any" }, + hiddenField: { visible: false }, + }, + } + const { description, ...newTableSchema } = { + ...basicTable.schema, + updatedDescription: { + ...basicTable.schema.description, + name: "updatedDescription", + }, + } as TableSchema + + const result = syncSchema(_.cloneDeep(view), newTableSchema, { + old: "description", + updated: "updatedDescription", + }) + expect(result).toEqual({ + ...view, + columns: ["name", "id", "updatedDescription"], + schemaUI: { + ...view.schemaUI, + description: undefined, + updatedDescription: { visible: true, width: 150, icon: "ic-any" }, + }, + }) + }) + + it("changing no UI schema will not affect the view", () => { + const view: ViewV2 = { + ...basicView, + columns: ["name", "id", "description"], + schemaUI: { + name: { visible: true, width: 100 }, + id: { visible: true, width: 20 }, + description: { visible: false }, + hiddenField: { visible: false }, + }, + } + const result = syncSchema( + _.cloneDeep(view), + { + ...basicTable.schema, + id: { + ...basicTable.schema.id, + type: FieldType.NUMBER, + }, + }, + undefined + ) + expect(result).toEqual(view) + }) + + it("changing table column UI fields will not affect the view schema", () => { + const view: ViewV2 = { + ...basicView, + columns: ["name", "id", "description"], + schemaUI: { + name: { visible: true, width: 100 }, + id: { visible: true, width: 20 }, + description: { visible: false }, + hiddenField: { visible: false }, + }, + } + const result = syncSchema( + _.cloneDeep(view), + { + ...basicTable.schema, + id: { + ...basicTable.schema.id, + visible: !basicTable.schema.id.visible, + }, + }, + undefined + ) + expect(result).toEqual(view) + }) + }) + }) }) diff --git a/packages/types/src/api/web/app/table.ts b/packages/types/src/api/web/app/table.ts index 178f758254..ff288811c9 100644 --- a/packages/types/src/api/web/app/table.ts +++ b/packages/types/src/api/web/app/table.ts @@ -1,4 +1,10 @@ -import { Table, TableSchema, View, ViewV2 } from "../../../documents" +import { + Table, + TableRequest, + TableSchema, + View, + ViewV2, +} from "../../../documents" interface ViewV2Response extends ViewV2 { schema: TableSchema @@ -11,3 +17,7 @@ export interface TableResponse extends Table { } export type FetchTablesResponse = TableResponse[] + +export interface SaveTableRequest extends TableRequest {} + +export type SaveTableResponse = Table