const { getRowParams } = require("../../../db/utils") const { outputProcessing, processAutoColumn, processFormulas, } = require("../../../utilities/rowProcessor") const { FieldTypes, FormulaTypes } = require("../../../constants") const { isEqual } = require("lodash") const { cloneDeep } = require("lodash/fp") const { getAppDB } = require("@budibase/backend-core/context") /** * This function runs through a list of enriched rows, looks at the rows which * are related and then checks if they need the state of their formulas * updated. * NOTE: this will only for affect static formulas. */ exports.updateRelatedFormula = async (table, enrichedRows) => { const db = getAppDB() // no formula to update, we're done if (!table.relatedFormula) { return } let promises = [] for (let enrichedRow of Array.isArray(enrichedRows) ? enrichedRows : [enrichedRows]) { // the related rows by tableId let relatedRows = {} for (let [key, field] of Object.entries(enrichedRow)) { const columnDefinition = table.schema[key] if (columnDefinition && columnDefinition.type === FieldTypes.LINK) { const relatedTableId = columnDefinition.tableId if (!relatedRows[relatedTableId]) { relatedRows[relatedTableId] = [] } relatedRows[relatedTableId] = relatedRows[relatedTableId].concat(field) } } for (let tableId of table.relatedFormula) { let relatedTable try { // no rows to update, skip if (!relatedRows[tableId] || relatedRows[tableId].length === 0) { continue } relatedTable = await db.get(tableId) } catch (err) { // no error scenario, table doesn't seem to exist anymore, ignore } for (let column of Object.values(relatedTable.schema)) { // needs updated in related rows if ( column.type === FieldTypes.FORMULA && column.formulaType === FormulaTypes.STATIC ) { // re-enrich rows for all the related, don't update the related formula for them promises = promises.concat( relatedRows[tableId].map(related => exports.finaliseRow(relatedTable, related, { updateFormula: false, }) ) ) } } } } await Promise.all(promises) } exports.updateAllFormulasInTable = async table => { const db = getAppDB() // start by getting the raw rows (which will be written back to DB after update) let rows = ( await db.allDocs( getRowParams(table._id, null, { include_docs: true, }) ) ).rows.map(row => row.doc) // now enrich the rows, note the clone so that we have the base state of the // rows so that we don't write any of the enriched information back let enrichedRows = await outputProcessing(table, cloneDeep(rows), { squash: false, }) const updatedRows = [] for (let row of rows) { // find the enriched row, if found process the formulas const enrichedRow = enrichedRows.find(enriched => enriched._id === row._id) if (enrichedRow) { const processed = processFormulas(table, cloneDeep(row), { dynamic: false, contextRows: enrichedRow, }) // values have changed, need to add to bulk docs to update if (!isEqual(processed, row)) { updatedRows.push(processed) } } } await db.bulkDocs(updatedRows) } /** * This function runs at the end of the save/patch functions of the row controller, all this * really does is enrich the row, handle any static formula processing, then return the enriched * row. The reason we need to return the enriched row is that the automation row created trigger * expects the row to be totally enriched/contain all relationships. */ exports.finaliseRow = async ( table, row, { oldTable, updateFormula } = { updateFormula: true } ) => { const db = getAppDB() row.type = "row" // process the row before return, to include relationships let enrichedRow = await outputProcessing(table, cloneDeep(row), { squash: false, }) // use enriched row to generate formulas for saving, specifically only use as context row = processFormulas(table, row, { dynamic: false, contextRows: enrichedRow, }) // don't worry about rev, tables handle rev/lastID updates // if another row has been written since processing this will // handle the auto ID clash if (oldTable && !isEqual(oldTable, table)) { try { await db.put(table) } catch (err) { if (err.status === 409) { const updatedTable = await db.get(table._id) let response = processAutoColumn(null, updatedTable, row, { reprocessing: true, }) await db.put(response.table) row = response.row } else { throw err } } } const response = await db.put(row) // for response, calculate the formulas for the enriched row enrichedRow._rev = response.rev enrichedRow = await processFormulas(table, enrichedRow, { dynamic: false }) // this updates the related formulas in other rows based on the relations to this row if (updateFormula) { await exports.updateRelatedFormula(table, enrichedRow) } return { row: enrichedRow, table } }