UNPKG

@budibase/server

Version:
283 lines (243 loc) • 7.9 kB
import { csv, events, HTTPError } from "@budibase/backend-core" import { canBeDisplayColumn, helpers, PROTECTED_EXTERNAL_COLUMNS, PROTECTED_INTERNAL_COLUMNS, } from "@budibase/shared-core" import { BulkImportRequest, BulkImportResponse, CsvToJsonRequest, CsvToJsonResponse, DeleteTableResponse, EventType, FetchTablesResponse, FieldType, FindTableResponse, MigrateTableRequest, MigrateTableResponse, SaveTableRequest, SaveTableResponse, Table, TableSourceType, UserCtx, ValidateNewTableImportRequest, ValidateTableImportRequest, ValidateTableImportResponse, } from "@budibase/types" import { cloneDeep } from "lodash" import { isExternalTable, isExternalTableID, isSQL, } from "../../../integrations/utils" import sdk from "../../../sdk" import { processTable } from "../../../sdk/workspace/tables/getters" import { isRows, isSchema, validate as validateSchema, } from "../../../utilities/schema" import { builderSocket } from "../../../websockets" import * as external from "./external" import * as internal from "./internal" function pickApi({ tableId, table }: { tableId?: string; table?: Table }) { if (table && isExternalTable(table)) { return external } if (tableId && isExternalTableID(tableId)) { return external } return internal } function checkDefaultFields(table: Table) { for (const [key, field] of Object.entries(table.schema)) { if (!("default" in field) || field.default == null) { continue } if (helpers.schema.isRequired(field.constraints)) { throw new HTTPError( `Cannot make field "${key}" required, it has a default value.`, 400 ) } } } async function guardTable(table: Table, isCreate: boolean) { checkDefaultFields(table) if ( table.primaryDisplay && !canBeDisplayColumn(table.schema[table.primaryDisplay]?.type) ) { // Prevent throwing errors from existing badly configured tables. Only throw for new tables or if this setting is being updated if ( isCreate || (await sdk.tables.getTable(table._id!)).primaryDisplay !== table.primaryDisplay ) { throw new HTTPError( `Column "${table.primaryDisplay}" cannot be used as a display type.`, 400 ) } } } // covers both internal and external export async function fetch(ctx: UserCtx<void, FetchTablesResponse>) { const internal = await sdk.tables.getAllInternalTables() const datasources = await sdk.datasources.getExternalDatasources() const external: Table[] = [] for (const datasource of datasources) { let entities = datasource.entities if (entities) { for (const entity of Object.values(entities)) { external.push({ ...(await processTable(entity)), sourceType: TableSourceType.EXTERNAL, sourceId: datasource._id!, sql: isSQL(datasource), }) } } } const result: FetchTablesResponse = [] for (const table of [...internal, ...external]) { result.push(await sdk.tables.enrichViewSchemas(table)) } ctx.body = result } export async function find(ctx: UserCtx<void, FindTableResponse>) { const tableId = ctx.params.tableId const table = await sdk.tables.getTable(tableId) const result = await sdk.tables.enrichViewSchemas(table) ctx.body = result } export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) { const appId = ctx.appId const { rows, ...table } = ctx.request.body const isImport = rows const renaming = ctx.request.body._rename const isCreate = !table._id await guardTable(table, isCreate) let savedTable: Table if (isCreate) { savedTable = await sdk.tables.create(table, rows, ctx.user._id) savedTable = await sdk.tables.enrichViewSchemas(savedTable) savedTable = await processTable(savedTable) await events.table.created(savedTable) } else { const api = pickApi({ table }) const { table: updatedTable, oldTable } = await api.updateTable( ctx, renaming ) savedTable = updatedTable savedTable = await processTable(savedTable) if (oldTable) { await events.table.updated(oldTable, savedTable) } } if (renaming) { await sdk.views.renameLinkedViews(savedTable, renaming) } if (isImport) { await events.table.imported(savedTable) } ctx.message = `Table ${table.name} saved successfully.` ctx.eventEmitter?.emitTable(EventType.TABLE_SAVE, appId, { ...savedTable }) ctx.body = savedTable builderSocket?.emitTableUpdate(ctx, cloneDeep(savedTable)) } export async function destroy(ctx: UserCtx<void, DeleteTableResponse>) { const appId = ctx.appId const tableId = ctx.params.tableId await sdk.rowActions.deleteAll(tableId) const deletedTable = await pickApi({ tableId }).destroy(ctx) await events.table.deleted(deletedTable, appId) ctx.eventEmitter?.emitTable(EventType.TABLE_DELETE, appId, deletedTable) ctx.table = deletedTable ctx.body = { message: `Table ${tableId} deleted.` } builderSocket?.emitTableDeletion(ctx, deletedTable) } export async function bulkImport( ctx: UserCtx<BulkImportRequest, BulkImportResponse> ) { const tableId = ctx.params.tableId await pickApi({ tableId }).bulkImport(ctx) // right now we don't trigger anything for bulk import because it // can only be done in the builder, but in the future we may need to // think about events for bulk items ctx.body = { message: `Bulk rows created.` } } export async function csvToJson( ctx: UserCtx<CsvToJsonRequest, CsvToJsonResponse> ) { const { csvString } = ctx.request.body const result = await csv.jsonFromCsvString(csvString) ctx.body = result } export async function validateNewTableImport( ctx: UserCtx<ValidateNewTableImportRequest, ValidateTableImportResponse> ) { const { rows, schema } = ctx.request.body if (isRows(rows) && isSchema(schema)) { ctx.body = validateSchema(rows, schema, PROTECTED_INTERNAL_COLUMNS) } else { ctx.status = 422 } } export async function validateExistingTableImport( ctx: UserCtx<ValidateTableImportRequest, ValidateTableImportResponse> ) { const { rows, tableId } = ctx.request.body let schema = null let protectedColumnNames if (tableId) { const table = await sdk.tables.getTable(tableId) schema = table.schema if (!isExternalTable(table)) { schema._id = { name: "_id", type: FieldType.STRING, } protectedColumnNames = PROTECTED_INTERNAL_COLUMNS.filter(x => x !== "_id") } else { protectedColumnNames = PROTECTED_EXTERNAL_COLUMNS } } else { ctx.status = 422 return } if (tableId && isRows(rows) && isSchema(schema)) { ctx.body = validateSchema(rows, schema, protectedColumnNames) } else { ctx.status = 422 } } export async function migrate( ctx: UserCtx<MigrateTableRequest, MigrateTableResponse> ) { const { oldColumn, newColumn } = ctx.request.body let tableId = ctx.params.tableId as string const table = await sdk.tables.getTable(tableId) let result = await sdk.tables.migrate(table, oldColumn, newColumn) for (let table of result.tablesUpdated) { builderSocket?.emitTableUpdate(ctx, table, { includeOriginator: true, }) } ctx.body = { message: `Column ${oldColumn} migrated.` } } export async function duplicate(ctx: UserCtx<void, SaveTableResponse>) { const tableId = ctx.params.tableId as string const table = await sdk.tables.getTable(tableId) if (isExternalTable(table)) { throw new HTTPError("Cannot duplicate external tables", 422) } const duplicatedTable = await sdk.tables.duplicate(table, ctx.user._id) ctx.message = `Table ${table.name} duplicated successfully.` ctx.body = duplicatedTable const processedTable = await processTable(duplicatedTable) builderSocket?.emitTableUpdate(ctx, cloneDeep(processedTable)) }