diff --git a/packages/server/src/api/controllers/table/internal.ts b/packages/server/src/api/controllers/table/internal.ts index eeb4a9eb5f..3a8b0b39a8 100644 --- a/packages/server/src/api/controllers/table/internal.ts +++ b/packages/server/src/api/controllers/table/internal.ts @@ -1,14 +1,5 @@ -import { updateLinks, EventType } from "../../../db/linkedRows" -import { getRowParams, generateTableID } from "../../../db/utils" -import { FieldTypes } from "../../../constants" -import { TableSaveFunctions, hasTypeChanged, handleDataImport } from "./utils" -import { context } from "@budibase/backend-core" -import env from "../../../environment" -import { - cleanupAttachments, - fixAutoColumnSubType, -} from "../../../utilities/rowProcessor" -import { runStaticFormulaChecks } from "./bulkFormula" +import { generateTableID } from "../../../db/utils" +import { handleDataImport } from "./utils" import { BulkImportRequest, BulkImportResponse, @@ -17,195 +8,52 @@ import { SaveTableResponse, Table, UserCtx, - ViewStatisticsSchema, - ViewV2, } from "@budibase/types" -import { quotas } from "@budibase/pro" -import isEqual from "lodash/isEqual" -import { cloneDeep } from "lodash/fp" import sdk from "../../../sdk" -function checkAutoColumns(table: Table, oldTable?: Table) { - if (!table.schema) { - return table - } - for (let [key, schema] of Object.entries(table.schema)) { - if (!schema.autocolumn || schema.subtype) { - continue - } - const oldSchema = oldTable && oldTable.schema[key] - if (oldSchema && oldSchema.subtype) { - table.schema[key].subtype = oldSchema.subtype - } else { - table.schema[key] = fixAutoColumnSubType(schema) - } - } - return table -} - export async function save(ctx: UserCtx) { - const db = context.getAppDB() const { rows, ...rest } = ctx.request.body let tableToSave: Table & { - _rename?: { old: string; updated: string } | undefined + _rename?: RenameColumn } = { type: "table", _id: generateTableID(), views: {}, ...rest, } + const renaming = tableToSave._rename + delete tableToSave._rename - // if the table obj had an _id then it will have been retrieved - let oldTable: Table | undefined - if (ctx.request.body && ctx.request.body._id) { - oldTable = await sdk.tables.getTable(ctx.request.body._id) - } - - // check all types are correct - if (hasTypeChanged(tableToSave, oldTable)) { - ctx.throw(400, "A column type has changed.") - } - // check that subtypes have been maintained - tableToSave = checkAutoColumns(tableToSave, oldTable) - - // saving a table is a complex operation, involving many different steps, this - // has been broken out into a utility to make it more obvious/easier to manipulate - const tableSaveFunctions = new TableSaveFunctions({ - user: ctx.user, - oldTable, - importRows: rows, - }) - tableToSave = await tableSaveFunctions.before(tableToSave) - - // make sure that types don't change of a column, have to remove - // the column if you want to change the type - if (oldTable && oldTable.schema) { - for (const propKey of Object.keys(tableToSave.schema)) { - let oldColumn = oldTable.schema[propKey] - if (oldColumn && oldColumn.type === FieldTypes.INTERNAL) { - oldTable.schema[propKey].type = FieldTypes.AUTO - } - } - } - - // Don't rename if the name is the same - let _rename: RenameColumn | undefined = tableToSave._rename - /* istanbul ignore next */ - if (_rename && _rename.old === _rename.updated) { - _rename = undefined - delete tableToSave._rename - } - - // rename row fields when table column is renamed - /* istanbul ignore next */ - if (_rename && tableToSave.schema[_rename.updated].type === FieldTypes.LINK) { - ctx.throw(400, "Cannot rename a linked column.") - } - - tableToSave = await tableSaveFunctions.mid(tableToSave) - - // update schema of non-statistics views when new columns are added - for (let view in tableToSave.views) { - const tableView = tableToSave.views[view] - if (!tableView) 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 - } - - // update linked rows try { - const linkResp: any = await updateLinks({ - eventType: oldTable ? EventType.TABLE_UPDATED : EventType.TABLE_SAVE, - table: tableToSave, - oldTable: oldTable, + const { table } = await sdk.tables.internal.save(tableToSave, { + user: ctx.user, + rowsToImport: rows, + tableId: ctx.request.body._id, + renaming: renaming, }) - if (linkResp != null && linkResp._rev) { - tableToSave._rev = linkResp._rev + + return table + } catch (err: any) { + if (err instanceof Error) { + ctx.throw(400, err.message) + } else { + ctx.throw(err.status || 500, err.message || err) } - } catch (err) { - ctx.throw(400, err as string) } - - // don't perform any updates until relationships have been - // checked by the updateLinks function - const updatedRows = tableSaveFunctions.getUpdatedRows() - if (updatedRows && updatedRows.length !== 0) { - await db.bulkDocs(updatedRows) - } - let result = await db.put(tableToSave) - tableToSave._rev = result.rev - const savedTable = cloneDeep(tableToSave) - - tableToSave = await tableSaveFunctions.after(tableToSave) - // the table may be updated as part of the table save after functionality - need to write it - if (!isEqual(savedTable, tableToSave)) { - result = await db.put(tableToSave) - tableToSave._rev = result.rev - } - // has to run after, make sure it has _id - await runStaticFormulaChecks(tableToSave, { oldTable, deletion: false }) - return tableToSave } -export async function destroy(ctx: any) { - const db = context.getAppDB() +export async function destroy(ctx: UserCtx) { const tableToDelete = await sdk.tables.getTable(ctx.params.tableId) - - // Delete all rows for that table - const rowsData = await db.allDocs( - getRowParams(ctx.params.tableId, null, { - include_docs: true, - }) - ) - await db.bulkDocs( - rowsData.rows.map((row: any) => ({ ...row.doc, _deleted: true })) - ) - await quotas.removeRows(rowsData.rows.length, { - tableId: ctx.params.tableId, - }) - - // update linked rows - await updateLinks({ - eventType: EventType.TABLE_DELETE, - table: tableToDelete, - }) - - // don't remove the table itself until very end - await db.remove(tableToDelete._id!, tableToDelete._rev) - - // remove table search index - if (!env.isTest() || env.COUCH_DB_URL) { - const currentIndexes = await db.getIndexes() - const existingIndex = currentIndexes.indexes.find( - (existing: any) => existing.name === `search:${ctx.params.tableId}` - ) - if (existingIndex) { - await db.deleteIndex(existingIndex) + try { + const { table } = await sdk.tables.internal.destroy(tableToDelete) + return table + } catch (err: any) { + if (err instanceof Error) { + ctx.throw(400, err.message) + } else { + ctx.throw(err.status || 500, err.message || err) } } - - // has to run after, make sure it has _id - await runStaticFormulaChecks(tableToDelete, { - deletion: true, - }) - await cleanupAttachments(tableToDelete, { - rows: rowsData.rows.map((row: any) => row.doc), - }) - return tableToDelete } export async function bulkImport( diff --git a/packages/server/src/sdk/app/tables/internal/index.ts b/packages/server/src/sdk/app/tables/internal/index.ts index e69de29bb2..c5c7822ffe 100644 --- a/packages/server/src/sdk/app/tables/internal/index.ts +++ b/packages/server/src/sdk/app/tables/internal/index.ts @@ -0,0 +1,187 @@ +import { + RenameColumn, + Table, + ViewStatisticsSchema, + ViewV2, + Row, + ContextUser, +} from "@budibase/types" +import { + hasTypeChanged, + TableSaveFunctions, +} from "../../../../api/controllers/table/utils" +import { FieldTypes } from "../../../../constants" +import { EventType, updateLinks } from "../../../../db/linkedRows" +import { cloneDeep } from "lodash/fp" +import isEqual from "lodash/isEqual" +import { runStaticFormulaChecks } from "../../../../api/controllers/table/bulkFormula" +import { context } from "@budibase/backend-core" +import { getTable } from "../getters" +import { checkAutoColumns } from "./utils" +import * as viewsSdk from "../../views" +import sdk from "../../../index" +import { getRowParams } from "../../../../db/utils" +import { quotas } from "@budibase/pro" +import env from "../../../../environment" +import { cleanupAttachments } from "../../../../utilities/rowProcessor" + +export async function save( + table: Table, + opts?: { + user?: ContextUser + tableId?: string + rowsToImport?: Row[] + renaming?: RenameColumn + } +) { + const db = context.getAppDB() + + // if the table obj had an _id then it will have been retrieved + let oldTable: Table | undefined + if (opts?.tableId) { + oldTable = await getTable(opts.tableId) + } + + // check all types are correct + if (hasTypeChanged(table, oldTable)) { + throw new Error("A column type has changed.") + } + // check that subtypes have been maintained + table = checkAutoColumns(table, oldTable) + + // saving a table is a complex operation, involving many different steps, this + // has been broken out into a utility to make it more obvious/easier to manipulate + const tableSaveFunctions = new TableSaveFunctions({ + user: opts?.user, + oldTable, + importRows: opts?.rowsToImport, + }) + table = await tableSaveFunctions.before(table) + + // make sure that types don't change of a column, have to remove + // the column if you want to change the type + if (oldTable && oldTable.schema) { + for (const propKey of Object.keys(table.schema)) { + let oldColumn = oldTable.schema[propKey] + if (oldColumn && oldColumn.type === FieldTypes.INTERNAL) { + oldTable.schema[propKey].type = FieldTypes.AUTO + } + } + } + + let renaming = opts?.renaming + if (renaming && renaming.old === renaming.updated) { + renaming = undefined + } + + // rename row fields when table column is renamed + if (renaming && table.schema[renaming.updated].type === FieldTypes.LINK) { + throw new Error("Cannot rename a linked column.") + } + + table = await tableSaveFunctions.mid(table) + + // update schema of non-statistics views when new columns are added + for (let view in table.views) { + const tableView = table.views[view] + if (!tableView) continue + + if (viewsSdk.isV2(tableView)) { + table.views[view] = viewsSdk.syncSchema( + oldTable!.views![view] as ViewV2, + table.schema, + renaming + ) + continue + } + + if ( + (tableView.schema as ViewStatisticsSchema).group || + tableView.schema.field + ) + continue + tableView.schema = table.schema + } + + // update linked rows + try { + const linkResp: any = await updateLinks({ + eventType: oldTable ? EventType.TABLE_UPDATED : EventType.TABLE_SAVE, + table: table, + oldTable: oldTable, + }) + if (linkResp != null && linkResp._rev) { + table._rev = linkResp._rev + } + } catch (err) { + throw new Error(err as string) + } + + // don't perform any updates until relationships have been + // checked by the updateLinks function + const updatedRows = tableSaveFunctions.getUpdatedRows() + if (updatedRows && updatedRows.length !== 0) { + await db.bulkDocs(updatedRows) + } + let result = await db.put(table) + table._rev = result.rev + const savedTable = cloneDeep(table) + + table = await tableSaveFunctions.after(table) + // the table may be updated as part of the table save after functionality - need to write it + if (!isEqual(savedTable, table)) { + result = await db.put(table) + table._rev = result.rev + } + // has to run after, make sure it has _id + await runStaticFormulaChecks(table, { oldTable, deletion: false }) + return { table } +} + +export async function destroy(table: Table) { + const db = context.getAppDB() + const tableId = table._id! + + // Delete all rows for that table + const rowsData = await db.allDocs( + getRowParams(tableId, null, { + include_docs: true, + }) + ) + await db.bulkDocs( + rowsData.rows.map((row: any) => ({ ...row.doc, _deleted: true })) + ) + await quotas.removeRows(rowsData.rows.length, { + tableId, + }) + + // update linked rows + await updateLinks({ + eventType: EventType.TABLE_DELETE, + table: table, + }) + + // don't remove the table itself until very end + await db.remove(tableId, table._rev) + + // remove table search index + if (!env.isTest() || env.COUCH_DB_URL) { + const currentIndexes = await db.getIndexes() + const existingIndex = currentIndexes.indexes.find( + (existing: any) => existing.name === `search:${tableId}` + ) + if (existingIndex) { + await db.deleteIndex(existingIndex) + } + } + + // has to run after, make sure it has _id + await runStaticFormulaChecks(table, { + deletion: true, + }) + await cleanupAttachments(table, { + rows: rowsData.rows.map((row: any) => row.doc), + }) + + return { table } +} diff --git a/packages/server/src/sdk/app/tables/internal/utils.ts b/packages/server/src/sdk/app/tables/internal/utils.ts index e69de29bb2..2d892ac272 100644 --- a/packages/server/src/sdk/app/tables/internal/utils.ts +++ b/packages/server/src/sdk/app/tables/internal/utils.ts @@ -0,0 +1,20 @@ +import { Table } from "@budibase/types" +import { fixAutoColumnSubType } from "../../../../utilities/rowProcessor" + +export function checkAutoColumns(table: Table, oldTable?: Table) { + if (!table.schema) { + return table + } + for (let [key, schema] of Object.entries(table.schema)) { + if (!schema.autocolumn || schema.subtype) { + continue + } + const oldSchema = oldTable && oldTable.schema[key] + if (oldSchema && oldSchema.subtype) { + table.schema[key].subtype = oldSchema.subtype + } else { + table.schema[key] = fixAutoColumnSubType(schema) + } + } + return table +}