305 lines
8.9 KiB
JavaScript
305 lines
8.9 KiB
JavaScript
const {
|
|
buildExternalTableId,
|
|
breakExternalTableId,
|
|
} = require("../../../integrations/utils")
|
|
const {
|
|
getTable,
|
|
generateForeignKey,
|
|
generateJunctionTableName,
|
|
foreignKeyStructure,
|
|
hasTypeChanged,
|
|
} = require("./utils")
|
|
const {
|
|
DataSourceOperation,
|
|
FieldTypes,
|
|
RelationshipTypes,
|
|
} = require("../../../constants")
|
|
const { makeExternalQuery } = require("../../../integrations/base/utils")
|
|
const { cloneDeep } = require("lodash/fp")
|
|
const csvParser = require("../../../utilities/csvParser")
|
|
const { handleRequest } = require("../row/external")
|
|
const { getAppDB } = require("@budibase/backend-core/context")
|
|
|
|
async function makeTableRequest(
|
|
datasource,
|
|
operation,
|
|
table,
|
|
tables,
|
|
oldTable = null
|
|
) {
|
|
const json = {
|
|
endpoint: {
|
|
datasourceId: datasource._id,
|
|
entityId: table._id,
|
|
operation,
|
|
},
|
|
meta: {
|
|
tables,
|
|
},
|
|
table,
|
|
}
|
|
if (oldTable) {
|
|
json.meta.table = oldTable
|
|
}
|
|
return makeExternalQuery(datasource, json)
|
|
}
|
|
|
|
function cleanupRelationships(table, tables, oldTable = null) {
|
|
const tableToIterate = oldTable ? oldTable : table
|
|
// clean up relationships in couch table schemas
|
|
for (let [key, schema] of Object.entries(tableToIterate.schema)) {
|
|
if (
|
|
schema.type === FieldTypes.LINK &&
|
|
(!oldTable || table.schema[key] == null)
|
|
) {
|
|
const relatedTable = Object.values(tables).find(
|
|
table => table._id === schema.tableId
|
|
)
|
|
const foreignKey = schema.foreignKey
|
|
if (!relatedTable || !foreignKey) {
|
|
continue
|
|
}
|
|
for (let [relatedKey, relatedSchema] of Object.entries(
|
|
relatedTable.schema
|
|
)) {
|
|
if (
|
|
relatedSchema.type === FieldTypes.LINK &&
|
|
relatedSchema.fieldName === foreignKey
|
|
) {
|
|
delete relatedTable.schema[relatedKey]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function getDatasourceId(table) {
|
|
if (!table) {
|
|
throw "No table supplied"
|
|
}
|
|
if (table.sourceId) {
|
|
return table.sourceId
|
|
}
|
|
return breakExternalTableId(table._id).datasourceId
|
|
}
|
|
|
|
function otherRelationshipType(type) {
|
|
if (type === RelationshipTypes.MANY_TO_MANY) {
|
|
return RelationshipTypes.MANY_TO_MANY
|
|
}
|
|
return type === RelationshipTypes.ONE_TO_MANY
|
|
? RelationshipTypes.MANY_TO_ONE
|
|
: RelationshipTypes.ONE_TO_MANY
|
|
}
|
|
|
|
function generateManyLinkSchema(datasource, column, table, relatedTable) {
|
|
const primary = table.name + table.primary[0]
|
|
const relatedPrimary = relatedTable.name + relatedTable.primary[0]
|
|
const jcTblName = generateJunctionTableName(column, table, relatedTable)
|
|
// first create the new table
|
|
const junctionTable = {
|
|
_id: buildExternalTableId(datasource._id, jcTblName),
|
|
name: jcTblName,
|
|
primary: [primary, relatedPrimary],
|
|
constrained: [primary, relatedPrimary],
|
|
schema: {
|
|
[primary]: foreignKeyStructure(primary, {
|
|
toTable: table.name,
|
|
toKey: table.primary[0],
|
|
}),
|
|
[relatedPrimary]: foreignKeyStructure(relatedPrimary, {
|
|
toTable: relatedTable.name,
|
|
toKey: relatedTable.primary[0],
|
|
}),
|
|
},
|
|
}
|
|
column.through = junctionTable._id
|
|
column.throughFrom = primary
|
|
column.throughTo = relatedPrimary
|
|
column.fieldName = relatedPrimary
|
|
return junctionTable
|
|
}
|
|
|
|
function generateLinkSchema(column, table, relatedTable, type) {
|
|
const isOneSide = type === RelationshipTypes.ONE_TO_MANY
|
|
const primary = isOneSide ? relatedTable.primary[0] : table.primary[0]
|
|
// generate a foreign key
|
|
const foreignKey = generateForeignKey(column, relatedTable)
|
|
column.relationshipType = type
|
|
column.foreignKey = isOneSide ? foreignKey : primary
|
|
column.fieldName = isOneSide ? primary : foreignKey
|
|
return foreignKey
|
|
}
|
|
|
|
function generateRelatedSchema(linkColumn, table, relatedTable, columnName) {
|
|
// generate column for other table
|
|
const relatedSchema = cloneDeep(linkColumn)
|
|
// swap them from the main link
|
|
if (linkColumn.foreignKey) {
|
|
relatedSchema.fieldName = linkColumn.foreignKey
|
|
relatedSchema.foreignKey = linkColumn.fieldName
|
|
}
|
|
// is many to many
|
|
else {
|
|
// don't need to copy through, already got it
|
|
relatedSchema.fieldName = linkColumn.throughFrom
|
|
relatedSchema.throughTo = linkColumn.throughFrom
|
|
relatedSchema.throughFrom = linkColumn.throughTo
|
|
}
|
|
relatedSchema.relationshipType = otherRelationshipType(
|
|
linkColumn.relationshipType
|
|
)
|
|
relatedSchema.tableId = relatedTable._id
|
|
relatedSchema.name = columnName
|
|
table.schema[columnName] = relatedSchema
|
|
}
|
|
|
|
function isRelationshipSetup(column) {
|
|
return column.foreignKey || column.through
|
|
}
|
|
|
|
exports.save = async function (ctx) {
|
|
const table = ctx.request.body
|
|
// can't do this right now
|
|
delete table.dataImport
|
|
const datasourceId = getDatasourceId(ctx.request.body)
|
|
// table doesn't exist already, note that it is created
|
|
if (!table._id) {
|
|
table.created = true
|
|
}
|
|
let tableToSave = {
|
|
type: "table",
|
|
_id: buildExternalTableId(datasourceId, table.name),
|
|
...table,
|
|
}
|
|
|
|
let oldTable
|
|
if (ctx.request.body && ctx.request.body._id) {
|
|
oldTable = await getTable(ctx.request.body._id)
|
|
}
|
|
|
|
if (hasTypeChanged(tableToSave, oldTable)) {
|
|
ctx.throw(400, "A column type has changed.")
|
|
}
|
|
|
|
const db = getAppDB()
|
|
const datasource = await db.get(datasourceId)
|
|
const oldTables = cloneDeep(datasource.entities)
|
|
const tables = datasource.entities
|
|
|
|
const extraTablesToUpdate = []
|
|
|
|
// check if relations need setup
|
|
for (let schema of Object.values(tableToSave.schema)) {
|
|
if (schema.type !== FieldTypes.LINK || isRelationshipSetup(schema)) {
|
|
continue
|
|
}
|
|
const relatedTable = Object.values(tables).find(
|
|
table => table._id === schema.tableId
|
|
)
|
|
const relatedColumnName = schema.fieldName
|
|
const relationType = schema.relationshipType
|
|
if (relationType === RelationshipTypes.MANY_TO_MANY) {
|
|
const junctionTable = generateManyLinkSchema(
|
|
datasource,
|
|
schema,
|
|
table,
|
|
relatedTable
|
|
)
|
|
if (tables[junctionTable.name]) {
|
|
throw "Junction table already exists, cannot create another relationship."
|
|
}
|
|
tables[junctionTable.name] = junctionTable
|
|
extraTablesToUpdate.push(junctionTable)
|
|
} else {
|
|
const fkTable =
|
|
relationType === RelationshipTypes.ONE_TO_MANY ? table : relatedTable
|
|
const foreignKey = generateLinkSchema(
|
|
schema,
|
|
table,
|
|
relatedTable,
|
|
relationType
|
|
)
|
|
fkTable.schema[foreignKey] = foreignKeyStructure(foreignKey)
|
|
if (fkTable.constrained == null) {
|
|
fkTable.constrained = []
|
|
}
|
|
if (fkTable.constrained.indexOf(foreignKey) === -1) {
|
|
fkTable.constrained.push(foreignKey)
|
|
}
|
|
// foreign key is in other table, need to save it to external
|
|
if (fkTable._id !== table._id) {
|
|
extraTablesToUpdate.push(fkTable)
|
|
}
|
|
}
|
|
generateRelatedSchema(schema, relatedTable, table, relatedColumnName)
|
|
schema.main = true
|
|
}
|
|
|
|
cleanupRelationships(tableToSave, tables, oldTable)
|
|
|
|
const operation = oldTable
|
|
? DataSourceOperation.UPDATE_TABLE
|
|
: DataSourceOperation.CREATE_TABLE
|
|
await makeTableRequest(datasource, operation, tableToSave, tables, oldTable)
|
|
// update any extra tables (like foreign keys in other tables)
|
|
for (let extraTable of extraTablesToUpdate) {
|
|
const oldExtraTable = oldTables[extraTable.name]
|
|
let op = oldExtraTable
|
|
? DataSourceOperation.UPDATE_TABLE
|
|
: DataSourceOperation.CREATE_TABLE
|
|
await makeTableRequest(datasource, op, extraTable, tables, oldExtraTable)
|
|
}
|
|
|
|
// make sure the constrained list, all still exist
|
|
if (Array.isArray(tableToSave.constrained)) {
|
|
tableToSave.constrained = tableToSave.constrained.filter(constraint =>
|
|
Object.keys(tableToSave.schema).includes(constraint)
|
|
)
|
|
}
|
|
|
|
// store it into couch now for budibase reference
|
|
datasource.entities[tableToSave.name] = tableToSave
|
|
await db.put(datasource)
|
|
|
|
return tableToSave
|
|
}
|
|
|
|
exports.destroy = async function (ctx) {
|
|
const tableToDelete = await getTable(ctx.params.tableId)
|
|
if (!tableToDelete || !tableToDelete.created) {
|
|
ctx.throw(400, "Cannot delete tables which weren't created in Budibase.")
|
|
}
|
|
const datasourceId = getDatasourceId(tableToDelete)
|
|
|
|
const db = getAppDB()
|
|
const datasource = await db.get(datasourceId)
|
|
const tables = datasource.entities
|
|
|
|
const operation = DataSourceOperation.DELETE_TABLE
|
|
await makeTableRequest(datasource, operation, tableToDelete, tables)
|
|
|
|
cleanupRelationships(tableToDelete, tables)
|
|
|
|
delete datasource.entities[tableToDelete.name]
|
|
await db.put(datasource)
|
|
|
|
return tableToDelete
|
|
}
|
|
|
|
exports.bulkImport = async function (ctx) {
|
|
const table = await getTable(ctx.params.tableId)
|
|
const { dataImport } = ctx.request.body
|
|
if (!dataImport || !dataImport.schema || !dataImport.csvString) {
|
|
ctx.throw(400, "Provided data import information is invalid.")
|
|
}
|
|
const rows = await csvParser.transform({
|
|
...dataImport,
|
|
existingTable: table,
|
|
})
|
|
await handleRequest(DataSourceOperation.BULK_CREATE, table._id, {
|
|
rows,
|
|
})
|
|
return table
|
|
}
|