2021-02-22 12:39:58 +01:00
|
|
|
const csvParser = require("../../../utilities/csvParser")
|
2021-04-09 18:33:21 +02:00
|
|
|
const {
|
|
|
|
getRowParams,
|
|
|
|
generateRowID,
|
|
|
|
InternalTables,
|
2022-01-21 17:24:24 +01:00
|
|
|
getTableParams,
|
|
|
|
BudibaseInternalDB,
|
2021-04-09 18:33:21 +02:00
|
|
|
} = require("../../../db/utils")
|
2022-01-24 17:32:41 +01:00
|
|
|
const { isEqual } = require("lodash")
|
2021-05-18 23:14:27 +02:00
|
|
|
const { AutoFieldSubTypes, FieldTypes } = require("../../../constants")
|
2022-01-24 17:32:41 +01:00
|
|
|
const {
|
|
|
|
inputProcessing,
|
|
|
|
cleanupAttachments,
|
|
|
|
} = require("../../../utilities/rowProcessor")
|
2022-01-26 19:50:13 +01:00
|
|
|
const {
|
|
|
|
USERS_TABLE_SCHEMA,
|
|
|
|
SwitchableTypes,
|
|
|
|
CanSwitchTypes,
|
|
|
|
} = require("../../../constants")
|
2021-10-19 18:00:54 +02:00
|
|
|
const {
|
|
|
|
isExternalTable,
|
|
|
|
breakExternalTableId,
|
2022-01-12 18:55:28 +01:00
|
|
|
isSQL,
|
2021-10-19 18:00:54 +02:00
|
|
|
} = require("../../../integrations/utils")
|
2021-10-20 21:01:49 +02:00
|
|
|
const { getViews, saveView } = require("../view/utils")
|
|
|
|
const viewTemplate = require("../view/viewBuilder")
|
2022-03-08 15:21:41 +01:00
|
|
|
const { quotas, StaticQuotaName, QuotaUsageType } = require("@budibase/pro")
|
2022-01-27 19:18:31 +01:00
|
|
|
const { getAppDB } = require("@budibase/backend-core/context")
|
2022-01-24 17:32:41 +01:00
|
|
|
const { cloneDeep } = require("lodash/fp")
|
2021-02-22 12:39:58 +01:00
|
|
|
|
2022-01-31 18:00:22 +01:00
|
|
|
exports.clearColumns = async (table, columnNames) => {
|
|
|
|
const db = getAppDB()
|
2022-01-24 17:32:41 +01:00
|
|
|
const rows = await db.allDocs(
|
|
|
|
getRowParams(table._id, null, {
|
|
|
|
include_docs: true,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
return db.bulkDocs(
|
|
|
|
rows.rows.map(({ doc }) => {
|
2022-01-24 19:22:59 +01:00
|
|
|
columnNames.forEach(colName => delete doc[colName])
|
2022-01-24 17:32:41 +01:00
|
|
|
return doc
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
2021-02-22 12:39:58 +01:00
|
|
|
|
2022-01-27 19:18:31 +01:00
|
|
|
exports.checkForColumnUpdates = async (oldTable, updatedTable) => {
|
|
|
|
const db = getAppDB()
|
2021-02-22 13:05:59 +01:00
|
|
|
let updatedRows = []
|
2021-02-22 12:39:58 +01:00
|
|
|
const rename = updatedTable._rename
|
|
|
|
let deletedColumns = []
|
|
|
|
if (oldTable && oldTable.schema && updatedTable.schema) {
|
|
|
|
deletedColumns = Object.keys(oldTable.schema).filter(
|
2021-05-04 12:32:22 +02:00
|
|
|
colName => updatedTable.schema[colName] == null
|
2021-02-22 12:39:58 +01:00
|
|
|
)
|
|
|
|
}
|
|
|
|
// check for renaming of columns or deleted columns
|
|
|
|
if (rename || deletedColumns.length !== 0) {
|
2021-10-20 21:01:49 +02:00
|
|
|
// Update all rows
|
2021-02-22 12:39:58 +01:00
|
|
|
const rows = await db.allDocs(
|
|
|
|
getRowParams(updatedTable._id, null, {
|
|
|
|
include_docs: true,
|
|
|
|
})
|
|
|
|
)
|
2022-01-24 17:32:41 +01:00
|
|
|
const rawRows = rows.rows.map(({ doc }) => doc)
|
|
|
|
updatedRows = rawRows.map(row => {
|
|
|
|
row = cloneDeep(row)
|
2021-02-22 12:39:58 +01:00
|
|
|
if (rename) {
|
2022-01-24 17:32:41 +01:00
|
|
|
row[rename.updated] = row[rename.old]
|
|
|
|
delete row[rename.old]
|
2021-02-22 12:39:58 +01:00
|
|
|
} else if (deletedColumns.length !== 0) {
|
2022-01-24 17:32:41 +01:00
|
|
|
deletedColumns.forEach(colName => delete row[colName])
|
2021-02-22 12:39:58 +01:00
|
|
|
}
|
2022-01-24 17:32:41 +01:00
|
|
|
return row
|
2021-02-22 12:39:58 +01:00
|
|
|
})
|
2021-10-20 21:01:49 +02:00
|
|
|
|
2022-01-24 17:32:41 +01:00
|
|
|
// cleanup any attachments from object storage for deleted attachment columns
|
2022-01-31 18:00:22 +01:00
|
|
|
await cleanupAttachments(updatedTable, { oldTable, rows: rawRows })
|
2021-10-20 21:01:49 +02:00
|
|
|
// Update views
|
2022-01-27 19:18:31 +01:00
|
|
|
await exports.checkForViewUpdates(updatedTable, rename, deletedColumns)
|
2021-02-22 12:39:58 +01:00
|
|
|
delete updatedTable._rename
|
|
|
|
}
|
|
|
|
return { rows: updatedRows, table: updatedTable }
|
|
|
|
}
|
|
|
|
|
|
|
|
// makes sure the passed in table isn't going to reset the auto ID
|
|
|
|
exports.makeSureTableUpToDate = (table, tableToSave) => {
|
|
|
|
if (!table) {
|
|
|
|
return tableToSave
|
|
|
|
}
|
|
|
|
// sure sure rev is up to date
|
|
|
|
tableToSave._rev = table._rev
|
|
|
|
// make sure auto IDs are always updated - these are internal
|
|
|
|
// so the client may not know they have changed
|
|
|
|
for (let [field, column] of Object.entries(table.schema)) {
|
|
|
|
if (
|
|
|
|
column.autocolumn &&
|
|
|
|
column.subtype === AutoFieldSubTypes.AUTO_ID &&
|
|
|
|
tableToSave.schema[field]
|
|
|
|
) {
|
|
|
|
tableToSave.schema[field].lastID = column.lastID
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return tableToSave
|
|
|
|
}
|
|
|
|
|
2022-01-27 19:18:31 +01:00
|
|
|
exports.handleDataImport = async (user, table, dataImport) => {
|
2021-11-12 19:26:57 +01:00
|
|
|
if (!dataImport || !dataImport.csvString) {
|
|
|
|
return table
|
|
|
|
}
|
2021-12-02 17:17:10 +01:00
|
|
|
|
2022-01-27 19:18:31 +01:00
|
|
|
const db = getAppDB()
|
2021-11-12 19:26:57 +01:00
|
|
|
// Populate the table with rows imported from CSV in a bulk update
|
|
|
|
const data = await csvParser.transform({
|
|
|
|
...dataImport,
|
|
|
|
existingTable: table,
|
|
|
|
})
|
2021-02-22 12:39:58 +01:00
|
|
|
|
2021-11-12 19:26:57 +01:00
|
|
|
let finalData = []
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
|
|
let row = data[i]
|
|
|
|
row._id = generateRowID(table._id)
|
|
|
|
row.tableId = table._id
|
|
|
|
const processed = inputProcessing(user, table, row, {
|
|
|
|
noAutoRelationships: true,
|
|
|
|
})
|
|
|
|
table = processed.table
|
|
|
|
row = processed.row
|
2021-08-26 15:13:30 +02:00
|
|
|
|
2021-11-12 19:26:57 +01:00
|
|
|
for (let [fieldName, schema] of Object.entries(table.schema)) {
|
|
|
|
// check whether the options need to be updated for inclusion as part of the data import
|
|
|
|
if (
|
|
|
|
schema.type === FieldTypes.OPTIONS &&
|
|
|
|
(!schema.constraints.inclusion ||
|
|
|
|
schema.constraints.inclusion.indexOf(row[fieldName]) === -1)
|
|
|
|
) {
|
|
|
|
schema.constraints.inclusion = [
|
|
|
|
...schema.constraints.inclusion,
|
|
|
|
row[fieldName],
|
|
|
|
]
|
2021-02-22 12:39:58 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-12 19:26:57 +01:00
|
|
|
finalData.push(row)
|
2021-02-22 12:39:58 +01:00
|
|
|
}
|
2021-11-12 19:26:57 +01:00
|
|
|
|
2022-03-08 15:21:41 +01:00
|
|
|
await quotas.updateUsage(
|
|
|
|
finalData.length,
|
|
|
|
StaticQuotaName.ROWS,
|
|
|
|
QuotaUsageType.STATIC,
|
|
|
|
{
|
|
|
|
dryRun: true,
|
|
|
|
}
|
|
|
|
)
|
2021-11-12 19:26:57 +01:00
|
|
|
await db.bulkDocs(finalData)
|
2022-03-08 15:21:41 +01:00
|
|
|
await quotas.updateUsage(
|
|
|
|
finalData.length,
|
|
|
|
StaticQuotaName.ROWS,
|
|
|
|
QuotaUsageType.STATIC
|
|
|
|
)
|
2021-11-12 19:26:57 +01:00
|
|
|
let response = await db.put(table)
|
|
|
|
table._rev = response._rev
|
2021-02-22 12:39:58 +01:00
|
|
|
return table
|
|
|
|
}
|
|
|
|
|
2022-01-27 19:18:31 +01:00
|
|
|
exports.handleSearchIndexes = async table => {
|
|
|
|
const db = getAppDB()
|
2021-02-22 12:39:58 +01:00
|
|
|
// create relevant search indexes
|
|
|
|
if (table.indexes && table.indexes.length > 0) {
|
|
|
|
const currentIndexes = await db.getIndexes()
|
|
|
|
const indexName = `search:${table._id}`
|
|
|
|
|
|
|
|
const existingIndex = currentIndexes.indexes.find(
|
2021-05-04 12:32:22 +02:00
|
|
|
existing => existing.name === indexName
|
2021-02-22 12:39:58 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
if (existingIndex) {
|
|
|
|
const currentFields = existingIndex.def.fields.map(
|
2021-05-04 12:32:22 +02:00
|
|
|
field => Object.keys(field)[0]
|
2021-02-22 12:39:58 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
// if index fields have changed, delete the original index
|
|
|
|
if (!isEqual(currentFields, table.indexes)) {
|
|
|
|
await db.deleteIndex(existingIndex)
|
|
|
|
// create/recreate the index with fields
|
|
|
|
await db.createIndex({
|
|
|
|
index: {
|
|
|
|
fields: table.indexes,
|
|
|
|
name: indexName,
|
|
|
|
ddoc: "search_ddoc",
|
|
|
|
type: "json",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// create/recreate the index with fields
|
|
|
|
await db.createIndex({
|
|
|
|
index: {
|
|
|
|
fields: table.indexes,
|
|
|
|
name: indexName,
|
|
|
|
ddoc: "search_ddoc",
|
|
|
|
type: "json",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return table
|
|
|
|
}
|
|
|
|
|
2021-05-04 12:32:22 +02:00
|
|
|
exports.checkStaticTables = table => {
|
2021-02-22 12:39:58 +01:00
|
|
|
// check user schema has all required elements
|
2021-04-09 18:33:21 +02:00
|
|
|
if (table._id === InternalTables.USER_METADATA) {
|
2021-02-22 12:39:58 +01:00
|
|
|
for (let [key, schema] of Object.entries(USERS_TABLE_SCHEMA.schema)) {
|
|
|
|
// check if the schema exists on the table to be created/updated
|
|
|
|
if (table.schema[key] == null) {
|
|
|
|
table.schema[key] = schema
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return table
|
|
|
|
}
|
|
|
|
|
|
|
|
class TableSaveFunctions {
|
2022-01-27 19:18:31 +01:00
|
|
|
constructor({ user, oldTable, dataImport }) {
|
|
|
|
this.db = getAppDB()
|
|
|
|
this.user = user
|
2021-02-22 12:39:58 +01:00
|
|
|
this.oldTable = oldTable
|
|
|
|
this.dataImport = dataImport
|
|
|
|
// any rows that need updated
|
|
|
|
this.rows = []
|
|
|
|
}
|
|
|
|
|
|
|
|
// before anything is done
|
|
|
|
async before(table) {
|
|
|
|
if (this.oldTable) {
|
|
|
|
table = exports.makeSureTableUpToDate(this.oldTable, table)
|
|
|
|
}
|
|
|
|
table = exports.checkStaticTables(table)
|
|
|
|
return table
|
|
|
|
}
|
|
|
|
|
|
|
|
// when confirmed valid
|
|
|
|
async mid(table) {
|
2022-01-27 19:18:31 +01:00
|
|
|
let response = await exports.checkForColumnUpdates(this.oldTable, table)
|
2021-02-22 13:05:59 +01:00
|
|
|
this.rows = this.rows.concat(response.rows)
|
2021-02-22 12:39:58 +01:00
|
|
|
return table
|
|
|
|
}
|
|
|
|
|
|
|
|
// after saving
|
|
|
|
async after(table) {
|
2022-01-27 19:18:31 +01:00
|
|
|
table = await exports.handleSearchIndexes(table)
|
|
|
|
table = await exports.handleDataImport(this.user, table, this.dataImport)
|
2021-02-22 12:39:58 +01:00
|
|
|
return table
|
|
|
|
}
|
|
|
|
|
|
|
|
getUpdatedRows() {
|
|
|
|
return this.rows
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-31 18:00:22 +01:00
|
|
|
exports.getAllInternalTables = async () => {
|
|
|
|
const db = getAppDB()
|
2022-01-21 17:24:24 +01:00
|
|
|
const internalTables = await db.allDocs(
|
|
|
|
getTableParams(null, {
|
|
|
|
include_docs: true,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
return internalTables.rows.map(tableDoc => ({
|
|
|
|
...tableDoc.doc,
|
|
|
|
type: "internal",
|
|
|
|
sourceId: BudibaseInternalDB._id,
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
2022-01-27 19:18:31 +01:00
|
|
|
exports.getAllExternalTables = async datasourceId => {
|
|
|
|
const db = getAppDB()
|
2021-06-16 17:27:33 +02:00
|
|
|
const datasource = await db.get(datasourceId)
|
|
|
|
if (!datasource || !datasource.entities) {
|
|
|
|
throw "Datasource is not configured fully."
|
|
|
|
}
|
2021-06-23 20:05:32 +02:00
|
|
|
return datasource.entities
|
|
|
|
}
|
|
|
|
|
2022-01-27 19:18:31 +01:00
|
|
|
exports.getExternalTable = async (datasourceId, tableName) => {
|
|
|
|
const entities = await exports.getAllExternalTables(datasourceId)
|
2021-06-23 20:05:32 +02:00
|
|
|
return entities[tableName]
|
2021-06-16 17:27:33 +02:00
|
|
|
}
|
|
|
|
|
2022-01-27 19:18:31 +01:00
|
|
|
exports.getTable = async tableId => {
|
|
|
|
const db = getAppDB()
|
2021-10-19 18:00:54 +02:00
|
|
|
if (isExternalTable(tableId)) {
|
|
|
|
let { datasourceId, tableName } = breakExternalTableId(tableId)
|
2022-01-12 18:55:28 +01:00
|
|
|
const datasource = await db.get(datasourceId)
|
2022-01-27 19:18:31 +01:00
|
|
|
const table = await exports.getExternalTable(datasourceId, tableName)
|
2022-01-12 18:55:28 +01:00
|
|
|
return { ...table, sql: isSQL(datasource) }
|
2021-10-19 18:00:54 +02:00
|
|
|
} else {
|
|
|
|
return db.get(tableId)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-27 19:18:31 +01:00
|
|
|
exports.checkForViewUpdates = async (table, rename, deletedColumns) => {
|
|
|
|
const views = await getViews()
|
2021-10-20 21:01:49 +02:00
|
|
|
const tableViews = views.filter(view => view.meta.tableId === table._id)
|
|
|
|
|
|
|
|
// Check each table view to see if impacted by this table action
|
|
|
|
for (let view of tableViews) {
|
|
|
|
let needsUpdated = false
|
|
|
|
|
|
|
|
// First check for renames, otherwise check for deletions
|
|
|
|
if (rename) {
|
|
|
|
// Update calculation field if required
|
|
|
|
if (view.meta.field === rename.old) {
|
|
|
|
view.meta.field = rename.updated
|
|
|
|
needsUpdated = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update group by field if required
|
|
|
|
if (view.meta.groupBy === rename.old) {
|
|
|
|
view.meta.groupBy = rename.updated
|
|
|
|
needsUpdated = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update filters if required
|
2021-10-21 11:24:41 +02:00
|
|
|
if (view.meta.filters) {
|
|
|
|
view.meta.filters.forEach(filter => {
|
|
|
|
if (filter.key === rename.old) {
|
|
|
|
filter.key = rename.updated
|
|
|
|
needsUpdated = true
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} else if (deletedColumns) {
|
2021-10-20 21:01:49 +02:00
|
|
|
deletedColumns.forEach(column => {
|
|
|
|
// Remove calculation statement if required
|
|
|
|
if (view.meta.field === column) {
|
|
|
|
delete view.meta.field
|
|
|
|
delete view.meta.calculation
|
|
|
|
delete view.meta.groupBy
|
|
|
|
needsUpdated = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove group by field if required
|
|
|
|
if (view.meta.groupBy === column) {
|
|
|
|
delete view.meta.groupBy
|
|
|
|
needsUpdated = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove filters referencing deleted field if required
|
2021-10-21 11:24:41 +02:00
|
|
|
if (view.meta.filters && view.meta.filters.length) {
|
2021-10-20 21:01:49 +02:00
|
|
|
const initialLength = view.meta.filters.length
|
|
|
|
view.meta.filters = view.meta.filters.filter(filter => {
|
|
|
|
return filter.key !== column
|
|
|
|
})
|
|
|
|
if (initialLength !== view.meta.filters.length) {
|
|
|
|
needsUpdated = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update view if required
|
|
|
|
if (needsUpdated) {
|
|
|
|
const newViewTemplate = viewTemplate(view.meta)
|
2022-01-27 19:18:31 +01:00
|
|
|
await saveView(null, view.name, newViewTemplate)
|
2021-10-20 21:01:49 +02:00
|
|
|
if (!newViewTemplate.meta.schema) {
|
|
|
|
newViewTemplate.meta.schema = table.schema
|
|
|
|
}
|
|
|
|
table.views[view.name] = newViewTemplate.meta
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-29 19:37:29 +02:00
|
|
|
exports.generateForeignKey = (column, relatedTable) => {
|
|
|
|
return `fk_${relatedTable.name}_${column.fieldName}`
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.generateJunctionTableName = (column, table, relatedTable) => {
|
|
|
|
return `jt_${table.name}_${relatedTable.name}_${column.name}_${column.fieldName}`
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.foreignKeyStructure = (keyName, meta = null) => {
|
|
|
|
const structure = {
|
|
|
|
type: FieldTypes.NUMBER,
|
|
|
|
constraints: {},
|
|
|
|
name: keyName,
|
|
|
|
}
|
|
|
|
if (meta) {
|
|
|
|
structure.meta = meta
|
|
|
|
}
|
|
|
|
return structure
|
|
|
|
}
|
|
|
|
|
2022-01-26 19:50:13 +01:00
|
|
|
exports.areSwitchableTypes = (type1, type2) => {
|
|
|
|
if (
|
|
|
|
SwitchableTypes.indexOf(type1) === -1 &&
|
|
|
|
SwitchableTypes.indexOf(type2) === -1
|
|
|
|
) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for (let option of CanSwitchTypes) {
|
|
|
|
const index1 = option.indexOf(type1),
|
|
|
|
index2 = option.indexOf(type2)
|
|
|
|
if (index1 !== -1 && index2 !== -1 && index1 !== index2) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-11-10 16:01:44 +01:00
|
|
|
exports.hasTypeChanged = (table, oldTable) => {
|
|
|
|
if (!oldTable) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for (let [key, field] of Object.entries(oldTable.schema)) {
|
|
|
|
const oldType = field.type
|
|
|
|
if (!table.schema[key]) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
const newType = table.schema[key].type
|
2022-01-26 19:50:13 +01:00
|
|
|
if (oldType !== newType && !exports.areSwitchableTypes(oldType, newType)) {
|
2021-11-10 16:01:44 +01:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-02-22 12:39:58 +01:00
|
|
|
exports.TableSaveFunctions = TableSaveFunctions
|