const { FieldTypes, FormulaTypes } = require("../../../constants") const { getAllInternalTables, clearColumns } = require("./utils") const { doesContainStrings } = require("@budibase/string-templates") const { cloneDeep } = require("lodash/fp") const { isEqual, uniq } = require("lodash") /** * This retrieves the formula columns from a table schema that use a specified column name * in the formula. */ function getFormulaThatUseColumn(table, columnNames) { let formula = [] columnNames = Array.isArray(columnNames) ? columnNames : [columnNames] for (let column of Object.values(table.schema)) { // not a static formula, or doesn't contain a relationship if ( column.type !== FieldTypes.FORMULA || column.formulaType !== FormulaTypes.STATIC ) { continue } if (!doesContainStrings(column.formula, columnNames)) { continue } formula.push(column.name) } return formula } /** * This functions checks two things: * 1. when a related table, column or related column is deleted, if any * tables need to have the formula column removed. * 2. If a formula has been added, or updated bulk update all the rows * in the table as per the new formula. */ async function checkRequiredFormulaUpdates(db, table, { oldTable, deletion }) { // start by retrieving all tables, remove the current table from the list const tables = (await getAllInternalTables({ db })).filter( tbl => tbl._id !== table._id ) const schemaToUse = oldTable ? oldTable.schema : table.schema let removedColumns = Object.values(schemaToUse).filter( column => deletion || !table.schema[column.name] ) // remove any formula columns that used related columns for (let removed of removedColumns) { let tableToUse = table // if relationship, get the related table if (removed.type === FieldTypes.LINK) { tableToUse = tables.find(table => table._id === removed.tableId) } const columnsToDelete = getFormulaThatUseColumn(tableToUse, removed.name) if (columnsToDelete.length > 0) { await clearColumns(db, table, columnsToDelete) } // need a special case, where a column has been removed from this table, but was used // in a different, related tables formula if (table.relatedFormula) { for (let relatedTableId of table.relatedFormula) { const relatedColumns = Object.values(table.schema).filter( column => column.tableId === relatedTableId ) const relatedTable = tables.find(table => table._id === relatedTableId) // look to see if the column was used in a relationship formula, // relationships won't be used for this if ( relatedTable && relatedColumns && removed.type !== FieldTypes.LINK ) { let relatedFormulaToRemove = [] for (let column of relatedColumns) { relatedFormulaToRemove = relatedFormulaToRemove.concat( getFormulaThatUseColumn(relatedTable, [ column.fieldName, removed.name, ]) ) } if (relatedFormulaToRemove.length > 0) { await clearColumns(db, relatedTable, uniq(relatedFormulaToRemove)) } } } } } } /** * This function adds a note to related tables that they are * used in a static formula - so that the link controller * can manage hydrating related rows formula fields. This is * specifically only for static formula. */ async function updateRelatedFormulaLinksOnTables( db, table, { deletion } = { deletion: false } ) { // start by retrieving all tables, remove the current table from the list const tables = (await getAllInternalTables({ db })).filter( tbl => tbl._id !== table._id ) // clone the tables, so we can compare at end const initialTables = cloneDeep(tables) // first find the related column names const relatedColumns = Object.values(table.schema).filter( col => col.type === FieldTypes.LINK ) // we start by removing the formula field from all tables for (let otherTable of tables) { if (!otherTable.relatedFormula) { continue } const index = otherTable.relatedFormula.indexOf(table._id) if (index !== -1) { otherTable.relatedFormula.splice(index, 1) } } // if deleting, just remove the table IDs, don't try add if (!deletion) { for (let relatedCol of relatedColumns) { let columns = getFormulaThatUseColumn(table, relatedCol.name) if (!columns || columns.length === 0) { continue } const relatedTable = tables.find( related => related._id === relatedCol.tableId ) // check if the table is already in the list of related formula, if it isn't, then add it if ( relatedTable && (!relatedTable.relatedFormula || !relatedTable.relatedFormula.includes(table._id)) ) { relatedTable.relatedFormula = relatedTable.relatedFormula ? [...relatedTable.relatedFormula, table._id] : [table._id] } } } // now we just need to compare all the tables and see if any need saved for (let initial of initialTables) { const found = tables.find(tbl => initial._id === tbl._id) if (found && !isEqual(initial, found)) { await db.put(found) } } } exports.runStaticFormulaChecks = async (db, table, { oldTable, deletion }) => { await updateRelatedFormulaLinksOnTables(db, table, { deletion }) await checkRequiredFormulaUpdates(db, table, { oldTable, deletion }) }