budibase/packages/server/src/api/controllers/table/index.ts

239 lines
6.4 KiB
TypeScript

import * as internal from "./internal"
import * as external from "./external"
import {
isRows,
isSchema,
validate as validateSchema,
} from "../../../utilities/schema"
import {
isExternalTable,
isExternalTableID,
isSQL,
} from "../../../integrations/utils"
import { events, HTTPError } from "@budibase/backend-core"
import {
BulkImportRequest,
BulkImportResponse,
CsvToJsonRequest,
CsvToJsonResponse,
FetchTablesResponse,
FieldType,
MigrateRequest,
MigrateResponse,
SaveTableRequest,
SaveTableResponse,
Table,
TableResponse,
TableSourceType,
UserCtx,
ValidateNewTableImportRequest,
ValidateTableImportRequest,
ValidateTableImportResponse,
} from "@budibase/types"
import sdk from "../../../sdk"
import { jsonFromCsvString } from "../../../utilities/csv"
import { builderSocket } from "../../../websockets"
import { cloneDeep } from "lodash"
import {
helpers,
PROTECTED_EXTERNAL_COLUMNS,
PROTECTED_INTERNAL_COLUMNS,
} from "@budibase/shared-core"
import { processTable } from "../../../sdk/app/tables/getters"
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
)
}
}
}
// 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 = datasources.flatMap(datasource => {
let entities = datasource.entities
if (entities) {
return Object.values(entities).map<Table>((entity: Table) => ({
...entity,
sourceType: TableSourceType.EXTERNAL,
sourceId: datasource._id!,
sql: isSQL(datasource),
}))
} else {
return []
}
})
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, TableResponse>) {
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
checkDefaultFields(table)
let savedTable: Table
if (isCreate) {
savedTable = await sdk.tables.create(table, rows, ctx.user._id)
savedTable = await sdk.tables.enrichViewSchemas(savedTable)
await events.table.created(savedTable)
} else {
const api = pickApi({ table })
savedTable = await api.updateTable(ctx, renaming)
await events.table.updated(savedTable)
}
if (renaming) {
await sdk.views.renameLinkedViews(savedTable, renaming)
}
if (isImport) {
await events.table.imported(savedTable)
}
ctx.status = 200
ctx.message = `Table ${table.name} saved successfully.`
ctx.eventEmitter &&
ctx.eventEmitter.emitTable(`table:save`, appId, { ...savedTable })
ctx.body = savedTable
savedTable = await processTable(savedTable)
builderSocket?.emitTableUpdate(ctx, cloneDeep(savedTable))
}
export async function destroy(ctx: UserCtx) {
const appId = ctx.appId
const tableId = ctx.params.tableId
const deletedTable = await pickApi({ tableId }).destroy(ctx)
await events.table.deleted(deletedTable)
ctx.eventEmitter &&
ctx.eventEmitter.emitTable(`table:delete`, appId, deletedTable)
ctx.status = 200
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.status = 200
ctx.body = { message: `Bulk rows created.` }
}
export async function csvToJson(
ctx: UserCtx<CsvToJsonRequest, CsvToJsonResponse>
) {
const { csvString } = ctx.request.body
const result = await jsonFromCsvString(csvString)
ctx.status = 200
ctx.body = result
}
export async function validateNewTableImport(
ctx: UserCtx<ValidateNewTableImportRequest, ValidateTableImportResponse>
) {
const { rows, schema } = ctx.request.body
if (isRows(rows) && isSchema(schema)) {
ctx.status = 200
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.status = 200
ctx.body = validateSchema(rows, schema, protectedColumnNames)
} else {
ctx.status = 422
}
}
export async function migrate(ctx: UserCtx<MigrateRequest, MigrateResponse>) {
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.status = 200
ctx.body = { message: `Column ${oldColumn} migrated.` }
}