Getting relationship re-enrichment working, so that static formulas will update when the value of the relationship changes.

This commit is contained in:
mike12345567 2022-01-21 17:45:24 +00:00
parent 04934a544a
commit faa82d8e0f
5 changed files with 122 additions and 43 deletions

View File

@ -14,7 +14,7 @@ const {
cleanupAttachments, cleanupAttachments,
processFormulas, processFormulas,
} = require("../../../utilities/rowProcessor") } = require("../../../utilities/rowProcessor")
const { FieldTypes } = require("../../../constants") const { FieldTypes, FormulaTypes } = require("../../../constants")
const { isEqual } = require("lodash") const { isEqual } = require("lodash")
const { validate, findRow } = require("./utils") const { validate, findRow } = require("./utils")
const { fullSearch, paginatedSearch } = require("./internalSearch") const { fullSearch, paginatedSearch } = require("./internalSearch")
@ -35,11 +35,75 @@ const CALCULATION_TYPES = {
STATS: "stats", 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" row.type = "row"
let rowToSave = cloneDeep(row)
// process the row before return, to include relationships // 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, squash: false,
}) })
// use enriched row to generate formulas for saving, specifically only use as context // 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 // don't worry about rev, tables handle rev/lastID updates
// if another row has been written since processing this will // if another row has been written since processing this will
// handle the auto ID clash // handle the auto ID clash
if (!isEqual(oldTable, table)) { if (oldTable && !isEqual(oldTable, table)) {
try { try {
await db.put(table) await db.put(table)
} catch (err) { } catch (err) {
if (err.status === 409) { if (err.status === 409) {
const updatedTable = await db.get(table._id) const updatedTable = await db.get(table._id)
let response = processAutoColumn(null, updatedTable, rowToSave, { let response = processAutoColumn(null, updatedTable, row, {
reprocessing: true, reprocessing: true,
}) })
await db.put(response.table) 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 // for response, calculate the formulas for the enriched row
enrichedRow._rev = response.rev enrichedRow._rev = response.rev
enrichedRow = await processFormulas(table, enrichedRow, { dynamic: false }) 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 } return { row: enrichedRow, table }
} }
@ -174,7 +242,10 @@ exports.patch = async ctx => {
return { row: ctx.body, table } 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) { exports.save = async function (ctx) {
@ -208,7 +279,10 @@ exports.save = async function (ctx) {
table, table,
}) })
return storeResponse(ctx, db, row, dbTable, table) return storeResponse(ctx.appId, db, table, row, {
oldTable: dbTable,
updateFormula: true,
})
} }
exports.fetchView = async ctx => { exports.fetchView = async ctx => {

View File

@ -20,7 +20,11 @@ const { isEqual } = require("lodash")
* can manage hydrating related rows formula fields. This is * can manage hydrating related rows formula fields. This is
* specifically only for static formula. * 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 // start by retrieving all tables, remove the current table from the list
const tables = (await getAllInternalTables({ db })).filter( const tables = (await getAllInternalTables({ db })).filter(
tbl => tbl._id !== table._id tbl => tbl._id !== table._id
@ -41,31 +45,34 @@ async function updateRelatedTablesForFormula(db, table) {
otherTable.relatedFormula.splice(index, 1) otherTable.relatedFormula.splice(index, 1)
} }
} }
for (let column of Object.values(table.schema)) { // if deleting, just remove the table IDs, don't try add
// not a static formula, or doesn't contain a relationship if (!deletion) {
if ( for (let column of Object.values(table.schema)) {
column.type !== FieldTypes.FORMULA || // not a static formula, or doesn't contain a relationship
column.formulaType !== FormulaTypes.STATIC if (
) { column.type !== FieldTypes.FORMULA ||
continue column.formulaType !== FormulaTypes.STATIC
} ) {
// check to see if any relationship columns are used in formula
for (let relatedCol of relatedColumns) {
if (!doesContainString(column.formula, relatedCol.name)) {
continue continue
} }
const relatedTable = tables.find( // check to see if any relationship columns are used in formula
related => related._id === relatedCol.tableId for (let relatedCol of relatedColumns) {
) if (!doesContainString(column.formula, relatedCol.name)) {
// check if the table is already in the list of related formula, if it isn't, then add it continue
if ( }
relatedTable && const relatedTable = tables.find(
(!relatedTable.relatedFormula || related => related._id === relatedCol.tableId
relatedTable.relatedFormula.indexOf(table._id) === -1) )
) { // check if the table is already in the list of related formula, if it isn't, then add it
relatedTable.relatedFormula = relatedTable.relatedFormula if (
? [...relatedTable.relatedFormula, table._id] relatedTable &&
: [table._id] (!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) await db.deleteIndex(existingIndex)
} }
// has to run after, make sure it has _id
await updateRelatedTablesForFormula(db, tableToDelete, { deletion: true })
return tableToDelete return tableToDelete
} }

View File

@ -156,8 +156,6 @@ class LinkController {
return true return true
} }
updateRelatedFormula() {}
/** /**
* Given the link field of this table, and the link field of the linked table, this makes sure * 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. * the state of relationship type is accurate on both.

View File

@ -72,7 +72,7 @@ async function getLinksForRows(appId, rows) {
) )
} }
async function getFullLinkedDocs(ctx, appId, links) { async function getFullLinkedDocs(appId, links) {
// create DBs // create DBs
const db = new CouchDB(appId) const db = new CouchDB(appId)
const linkedRowIds = links.map(link => link.id) 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. * 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). * 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 {object} table The table from which the rows originated.
* @param {array<object>} rows The rows which are to be enriched. * @param {array<object>} rows The rows which are to be enriched.
* @return {Promise<*>} returns the rows with all of the enriched relationships on it. * @return {Promise<*>} returns the rows with all of the enriched relationships on it.
*/ */
exports.attachFullLinkedDocs = async (ctx, table, rows) => { exports.attachFullLinkedDocs = async (appId, table, rows) => {
const appId = ctx.appId
const linkedTableIds = getLinkedTableIDs(table) const linkedTableIds = getLinkedTableIDs(table)
if (linkedTableIds.length === 0) { if (linkedTableIds.length === 0) {
return rows return rows
@ -166,7 +165,7 @@ exports.attachFullLinkedDocs = async (ctx, table, rows) => {
// clear any existing links that could be dupe'd // clear any existing links that could be dupe'd
rows = clearRelationshipFields(table, rows) rows = clearRelationshipFields(table, rows)
// now get the docs and combine into the 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 = [] const linkedTables = []
for (let row of rows) { for (let row of rows) {
for (let link of links.filter(link => link.thisId === row._id)) { for (let link of links.filter(link => link.thisId === row._id)) {

View File

@ -253,7 +253,7 @@ exports.inputProcessing = (
/** /**
* This function enriches the input rows with anything they are supposed to contain, for example * This function enriches the input rows with anything they are supposed to contain, for example
* link records or attachment links. * 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 * @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. * the schema of the rows and then enrich.
* @param {object[]|object} rows the rows which are to be enriched. * @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. * @returns {object[]|object} the enriched rows will be returned.
*/ */
exports.outputProcessing = async ( exports.outputProcessing = async (
ctx, { appId },
table, table,
rows, rows,
opts = { squash: true } opts = { squash: true }
) => { ) => {
const appId = ctx.appId
let wasArray = true let wasArray = true
if (!(rows instanceof Array)) { if (!(rows instanceof Array)) {
rows = [rows] rows = [rows]
wasArray = false wasArray = false
} }
// attach any linked row information // attach any linked row information
let enriched = await linkRows.attachFullLinkedDocs(ctx, table, rows) let enriched = await linkRows.attachFullLinkedDocs(appId, table, rows)
// process formulas // process formulas
enriched = processFormulas(table, enriched, { dynamic: true }) enriched = processFormulas(table, enriched, { dynamic: true })