diff --git a/packages/server/src/api/controllers/row/internal.js b/packages/server/src/api/controllers/row/internal.js index db573f45a4..21859188ae 100644 --- a/packages/server/src/api/controllers/row/internal.js +++ b/packages/server/src/api/controllers/row/internal.js @@ -14,7 +14,7 @@ const { cleanupAttachments, processFormulas, } = require("../../../utilities/rowProcessor") -const { FieldTypes } = require("../../../constants") +const { FieldTypes, FormulaTypes } = require("../../../constants") const { isEqual } = require("lodash") const { validate, findRow } = require("./utils") const { fullSearch, paginatedSearch } = require("./internalSearch") @@ -35,11 +35,75 @@ const CALCULATION_TYPES = { STATS: "stats", } -async function storeResponse(ctx, db, row, oldTable, table) { +/** + * This function runs through the enriched row, looks at the rows which + * are related and then checks if they need the state of their formulas + * updated. + */ +async function updateRelatedFormula(appId, db, table, enrichedRow) { + // no formula to update, we're done + if (!table.relatedFormula) { + return + } + // 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) + } + } + let promises = [] + for (let tableId of table.relatedFormula) { + try { + // no rows to update, skip + if (!relatedRows[tableId] || relatedRows[tableId].length === 0) { + continue + } + const relatedTable = await db.get(tableId) + 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 => + storeResponse(appId, db, relatedTable, related, { + updateFormula: false, + }) + ) + ) + } + } + } catch (err) { + // no error scenario, table doesn't seem to exist anymore, ignore + } + } + await Promise.all(promises) +} + +/** + * 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. + */ +async function storeResponse( + appId, + db, + table, + row, + { oldTable, updateFormula } = { updateFormula: true } +) { row.type = "row" - let rowToSave = cloneDeep(row) // process the row before return, to include relationships - let enrichedRow = await outputProcessing(ctx, table, cloneDeep(row), { + let enrichedRow = await outputProcessing({ appId }, table, cloneDeep(row), { squash: false, }) // use enriched row to generate formulas for saving, specifically only use as context @@ -51,13 +115,13 @@ async function storeResponse(ctx, db, row, oldTable, table) { // 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 (!isEqual(oldTable, table)) { + 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, rowToSave, { + let response = processAutoColumn(null, updatedTable, row, { reprocessing: true, }) await db.put(response.table) @@ -71,6 +135,10 @@ async function storeResponse(ctx, db, row, oldTable, table) { // 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 updateRelatedFormula(appId, db, table, enrichedRow) + } return { row: enrichedRow, table } } @@ -174,7 +242,10 @@ exports.patch = async ctx => { return { row: ctx.body, table } } - return storeResponse(ctx, db, row, dbTable, table) + return storeResponse(ctx.appId, db, table, row, { + oldTable: dbTable, + updateFormula: true, + }) } exports.save = async function (ctx) { @@ -208,7 +279,10 @@ exports.save = async function (ctx) { table, }) - return storeResponse(ctx, db, row, dbTable, table) + return storeResponse(ctx.appId, db, table, row, { + oldTable: dbTable, + updateFormula: true, + }) } exports.fetchView = async ctx => { diff --git a/packages/server/src/api/controllers/table/internal.js b/packages/server/src/api/controllers/table/internal.js index 9e48fd471a..193050f956 100644 --- a/packages/server/src/api/controllers/table/internal.js +++ b/packages/server/src/api/controllers/table/internal.js @@ -20,7 +20,11 @@ const { isEqual } = require("lodash") * can manage hydrating related rows formula fields. This is * specifically only for static formula. */ -async function updateRelatedTablesForFormula(db, table) { +async function updateRelatedTablesForFormula( + 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 @@ -41,31 +45,34 @@ async function updateRelatedTablesForFormula(db, table) { otherTable.relatedFormula.splice(index, 1) } } - 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 - } - // check to see if any relationship columns are used in formula - for (let relatedCol of relatedColumns) { - if (!doesContainString(column.formula, relatedCol.name)) { + // if deleting, just remove the table IDs, don't try add + if (!deletion) { + 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 } - 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.indexOf(table._id) === -1) - ) { - relatedTable.relatedFormula = relatedTable.relatedFormula - ? [...relatedTable.relatedFormula, table._id] - : [table._id] + // check to see if any relationship columns are used in formula + for (let relatedCol of relatedColumns) { + if (!doesContainString(column.formula, relatedCol.name)) { + 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] + } } } } @@ -211,6 +218,8 @@ exports.destroy = async function (ctx) { await db.deleteIndex(existingIndex) } + // has to run after, make sure it has _id + await updateRelatedTablesForFormula(db, tableToDelete, { deletion: true }) return tableToDelete } diff --git a/packages/server/src/db/linkedRows/LinkController.js b/packages/server/src/db/linkedRows/LinkController.js index 832f6fecdd..b66e2debb5 100644 --- a/packages/server/src/db/linkedRows/LinkController.js +++ b/packages/server/src/db/linkedRows/LinkController.js @@ -156,8 +156,6 @@ class LinkController { return true } - updateRelatedFormula() {} - /** * Given the link field of this table, and the link field of the linked table, this makes sure * the state of relationship type is accurate on both. diff --git a/packages/server/src/db/linkedRows/index.js b/packages/server/src/db/linkedRows/index.js index 6835719e5f..eab287aa33 100644 --- a/packages/server/src/db/linkedRows/index.js +++ b/packages/server/src/db/linkedRows/index.js @@ -72,7 +72,7 @@ async function getLinksForRows(appId, rows) { ) } -async function getFullLinkedDocs(ctx, appId, links) { +async function getFullLinkedDocs(appId, links) { // create DBs const db = new CouchDB(appId) const linkedRowIds = links.map(link => link.id) @@ -146,13 +146,12 @@ exports.updateLinks = async function (args) { /** * Given a table and a list of rows this will retrieve all of the attached docs and enrich them into the row. * This is required for formula fields, this may only be utilised internally (for now). - * @param {object} ctx The request which is looking for rows. + * @param {string} appId The ID of the app which this request is in the context of. * @param {object} table The table from which the rows originated. * @param {array} rows The rows which are to be enriched. * @return {Promise<*>} returns the rows with all of the enriched relationships on it. */ -exports.attachFullLinkedDocs = async (ctx, table, rows) => { - const appId = ctx.appId +exports.attachFullLinkedDocs = async (appId, table, rows) => { const linkedTableIds = getLinkedTableIDs(table) if (linkedTableIds.length === 0) { return rows @@ -166,7 +165,7 @@ exports.attachFullLinkedDocs = async (ctx, table, rows) => { // clear any existing links that could be dupe'd rows = clearRelationshipFields(table, rows) // now get the docs and combine into the rows - let linked = await getFullLinkedDocs(ctx, appId, links) + let linked = await getFullLinkedDocs(appId, links) const linkedTables = [] for (let row of rows) { for (let link of links.filter(link => link.thisId === row._id)) { diff --git a/packages/server/src/utilities/rowProcessor/index.js b/packages/server/src/utilities/rowProcessor/index.js index f6c3a66edf..4237855fb3 100644 --- a/packages/server/src/utilities/rowProcessor/index.js +++ b/packages/server/src/utilities/rowProcessor/index.js @@ -253,7 +253,7 @@ exports.inputProcessing = ( /** * This function enriches the input rows with anything they are supposed to contain, for example * link records or attachment links. - * @param {object} ctx the request which is looking for enriched rows. + * @param {string} appId the app in which the request is looking for enriched rows. * @param {object} table the table from which these rows came from originally, this is used to determine * the schema of the rows and then enrich. * @param {object[]|object} rows the rows which are to be enriched. @@ -261,19 +261,18 @@ exports.inputProcessing = ( * @returns {object[]|object} the enriched rows will be returned. */ exports.outputProcessing = async ( - ctx, + { appId }, table, rows, opts = { squash: true } ) => { - const appId = ctx.appId let wasArray = true if (!(rows instanceof Array)) { rows = [rows] wasArray = false } // attach any linked row information - let enriched = await linkRows.attachFullLinkedDocs(ctx, table, rows) + let enriched = await linkRows.attachFullLinkedDocs(appId, table, rows) // process formulas enriched = processFormulas(table, enriched, { dynamic: true })