1
0
Fork 0
mirror of synced 2024-09-02 18:51:36 +12:00

Merge pull request #11483 from Budibase/BUDI-7189/handle_tableschema_changes_on_views

Handle tableschema changes on views
This commit is contained in:
Michael Drury 2023-08-11 14:36:57 +01:00 committed by GitHub
commit ff620a496d
7 changed files with 436 additions and 58 deletions

View file

@ -22,9 +22,12 @@ import {
QueryJson, QueryJson,
RelationshipType, RelationshipType,
RenameColumn, RenameColumn,
SaveTableRequest,
SaveTableResponse,
Table, Table,
TableRequest, TableRequest,
UserCtx, UserCtx,
ViewV2,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { builderSocket } from "../../../websockets" import { builderSocket } from "../../../websockets"
@ -198,8 +201,8 @@ function isRelationshipSetup(column: FieldSchema) {
return column.foreignKey || column.through return column.foreignKey || column.through
} }
export async function save(ctx: UserCtx) { export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
const inputs: TableRequest = ctx.request.body const inputs = ctx.request.body
const renamed = inputs?._rename const renamed = inputs?._rename
// can't do this right now // can't do this right now
delete inputs.rows delete inputs.rows
@ -215,7 +218,7 @@ export async function save(ctx: UserCtx) {
...inputs, ...inputs,
} }
let oldTable let oldTable: Table | undefined
if (ctx.request.body && ctx.request.body._id) { if (ctx.request.body && ctx.request.body._id) {
oldTable = await sdk.tables.getTable(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.") 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 db = context.getAppDB()
const datasource = await sdk.datasources.get(datasourceId) const datasource = await sdk.datasources.get(datasourceId)
if (!datasource.entities) { if (!datasource.entities) {

View file

@ -9,6 +9,8 @@ import { isExternalTable, isSQL } from "../../../integrations/utils"
import { events } from "@budibase/backend-core" import { events } from "@budibase/backend-core"
import { import {
FetchTablesResponse, FetchTablesResponse,
SaveTableResponse,
SaveTableRequest,
Table, Table,
TableResponse, TableResponse,
UserCtx, UserCtx,
@ -60,7 +62,7 @@ export async function find(ctx: UserCtx<void, TableResponse>) {
ctx.body = sdk.tables.enrichViewSchemas(table) ctx.body = sdk.tables.enrichViewSchemas(table)
} }
export async function save(ctx: UserCtx) { export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
const appId = ctx.appId const appId = ctx.appId
const table = ctx.request.body const table = ctx.request.body
const isImport = table.rows const isImport = table.rows

View file

@ -9,7 +9,15 @@ import {
fixAutoColumnSubType, fixAutoColumnSubType,
} from "../../../utilities/rowProcessor" } from "../../../utilities/rowProcessor"
import { runStaticFormulaChecks } from "./bulkFormula" 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 { quotas } from "@budibase/pro"
import isEqual from "lodash/isEqual" import isEqual from "lodash/isEqual"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
@ -33,10 +41,10 @@ function checkAutoColumns(table: Table, oldTable?: Table) {
return table return table
} }
export async function save(ctx: any) { export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
const db = context.getAppDB() const db = context.getAppDB()
const { rows, ...rest } = ctx.request.body const { rows, ...rest } = ctx.request.body
let tableToSave = { let tableToSave: TableRequest = {
type: "table", type: "table",
_id: generateTableID(), _id: generateTableID(),
views: {}, views: {},
@ -44,7 +52,7 @@ export async function save(ctx: any) {
} }
// if the table obj had an _id then it will have been retrieved // 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) { if (ctx.request.body && ctx.request.body._id) {
oldTable = await sdk.tables.getTable(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 let { _rename } = tableToSave
/* istanbul ignore next */ /* istanbul ignore next */
if (_rename && _rename.old === _rename.updated) { if (_rename && _rename.old === _rename.updated) {
_rename = null _rename = undefined
delete tableToSave._rename delete tableToSave._rename
} }
@ -97,7 +105,20 @@ export async function save(ctx: any) {
const tableView = tableToSave.views[view] const tableView = tableToSave.views[view]
if (!tableView) continue 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 tableView.schema = tableToSave.schema
} }
@ -112,7 +133,7 @@ export async function save(ctx: any) {
tableToSave._rev = linkResp._rev tableToSave._rev = linkResp._rev
} }
} catch (err) { } catch (err) {
ctx.throw(400, err) ctx.throw(400, err as string)
} }
// don't perform any updates until relationships have been // don't perform any updates until relationships have been

View file

@ -418,7 +418,7 @@ export function areSwitchableTypes(type1: any, type2: any) {
return false return false
} }
export function hasTypeChanged(table: any, oldTable: any) { export function hasTypeChanged(table: Table, oldTable: Table | undefined) {
if (!oldTable) { if (!oldTable) {
return false return false
} }

View file

@ -1,5 +1,11 @@
import {
FieldSchema,
RenameColumn,
TableSchema,
View,
ViewV2,
} from "@budibase/types"
import { context, HTTPError } from "@budibase/backend-core" import { context, HTTPError } from "@budibase/backend-core"
import { FieldSchema, TableSchema, View, ViewV2 } from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import * as utils from "../../../db/utils" import * as utils from "../../../db/utils"
@ -103,3 +109,37 @@ export function enrichSchema(view: View | ViewV2, tableSchema: TableSchema) {
schema: schema, 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
}

View file

@ -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 { generator } from "@budibase/backend-core/tests"
import { enrichSchema } from ".." import { enrichSchema, syncSchema } from ".."
describe("table sdk", () => { describe("table sdk", () => {
describe("enrichViewSchemas", () => { const basicTable: Table = {
const basicTable: Table = { _id: generator.guid(),
_id: generator.guid(), name: "TestTable",
name: "TestTable", type: "table",
type: "table", schema: {
schema: { name: {
name: { type: FieldType.STRING,
type: FieldType.STRING, name: "name",
name: "name", visible: true,
visible: true, width: 80,
width: 80, order: 2,
order: 2, constraints: {
constraints: { type: "string",
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",
},
}, },
}, },
} 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 () => { it("should fetch the default schema if not overriden", async () => {
const tableId = basicTable._id! const tableId = basicTable._id!
const view: ViewV2 = { 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)
})
})
})
}) })

View file

@ -1,4 +1,10 @@
import { Table, TableSchema, View, ViewV2 } from "../../../documents" import {
Table,
TableRequest,
TableSchema,
View,
ViewV2,
} from "../../../documents"
interface ViewV2Response extends ViewV2 { interface ViewV2Response extends ViewV2 {
schema: TableSchema schema: TableSchema
@ -11,3 +17,7 @@ export interface TableResponse extends Table {
} }
export type FetchTablesResponse = TableResponse[] export type FetchTablesResponse = TableResponse[]
export interface SaveTableRequest extends TableRequest {}
export type SaveTableResponse = Table