Bit of refactoring, adding in functionality to remove invalid static formula when the elements that the formula depends on are removed.
This commit is contained in:
parent
a2a24b8a96
commit
135aff4a31
|
@ -1,15 +1,16 @@
|
||||||
const { FieldTypes, FormulaTypes } = require("../../../constants")
|
const { FieldTypes, FormulaTypes } = require("../../../constants")
|
||||||
const { getAllInternalTables } = require("./utils")
|
const { getAllInternalTables, deleteColumns } = require("./utils")
|
||||||
const { doesContainString } = require("@budibase/string-templates")
|
const { doesContainStrings } = require("@budibase/string-templates")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
const { isEqual } = require("lodash")
|
const { isEqual, uniq } = require("lodash")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This retrieves the formula columns from a table schema that use a specified column name
|
* This retrieves the formula columns from a table schema that use a specified column name
|
||||||
* in the formula.
|
* in the formula.
|
||||||
*/
|
*/
|
||||||
function getFormulaThatUseColumn(table, columnName) {
|
function getFormulaThatUseColumn(table, columnNames) {
|
||||||
let formula = []
|
let formula = []
|
||||||
|
columnNames = Array.isArray(columnNames) ? columnNames : [columnNames]
|
||||||
for (let column of Object.values(table.schema)) {
|
for (let column of Object.values(table.schema)) {
|
||||||
// not a static formula, or doesn't contain a relationship
|
// not a static formula, or doesn't contain a relationship
|
||||||
if (
|
if (
|
||||||
|
@ -18,7 +19,7 @@ function getFormulaThatUseColumn(table, columnName) {
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (!doesContainString(column.formula, columnName)) {
|
if (!doesContainStrings(column.formula, columnNames)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
formula.push(column.name)
|
formula.push(column.name)
|
||||||
|
@ -26,17 +27,78 @@ function getFormulaThatUseColumn(table, columnName) {
|
||||||
return formula
|
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 deleteColumns(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 deleteColumns(db, relatedTable, uniq(relatedFormulaToRemove))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function adds a note to related tables that they are
|
* This function adds a note to related tables that they are
|
||||||
* used in a static formula - so that the link controller
|
* used in a static formula - so that the link controller
|
||||||
* 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.
|
||||||
*/
|
*/
|
||||||
exports.updateRelatedFormulaLinksOnTables = async (
|
async function updateRelatedFormulaLinksOnTables(
|
||||||
db,
|
db,
|
||||||
table,
|
table,
|
||||||
{ deletion } = { deletion: false }
|
{ 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
|
||||||
|
@ -87,3 +149,8 @@ exports.updateRelatedFormulaLinksOnTables = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.runStaticFormulaChecks = async (db, table, { oldTable, deletion }) => {
|
||||||
|
await updateRelatedFormulaLinksOnTables(db, table, { deletion })
|
||||||
|
await checkRequiredFormulaUpdates(db, table, { oldTable, deletion })
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ const {
|
||||||
} = require("./utils")
|
} = require("./utils")
|
||||||
const usageQuota = require("../../../utilities/usageQuota")
|
const usageQuota = require("../../../utilities/usageQuota")
|
||||||
const { cleanupAttachments } = require("../../../utilities/rowProcessor")
|
const { cleanupAttachments } = require("../../../utilities/rowProcessor")
|
||||||
const { updateRelatedFormulaLinksOnTables } = require("./bulkFormula")
|
const { runStaticFormulaChecks } = require("./bulkFormula")
|
||||||
|
|
||||||
exports.save = async function (ctx) {
|
exports.save = async function (ctx) {
|
||||||
const appId = ctx.appId
|
const appId = ctx.appId
|
||||||
|
@ -107,8 +107,7 @@ exports.save = async function (ctx) {
|
||||||
|
|
||||||
tableToSave = await tableSaveFunctions.after(tableToSave)
|
tableToSave = await tableSaveFunctions.after(tableToSave)
|
||||||
// has to run after, make sure it has _id
|
// has to run after, make sure it has _id
|
||||||
await updateRelatedFormulaLinksOnTables(db, tableToSave)
|
await runStaticFormulaChecks(db, tableToSave, { oldTable })
|
||||||
|
|
||||||
return tableToSave
|
return tableToSave
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +145,7 @@ exports.destroy = async function (ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// has to run after, make sure it has _id
|
// has to run after, make sure it has _id
|
||||||
await updateRelatedFormulaLinksOnTables(db, tableToDelete, { deletion: true })
|
await runStaticFormulaChecks(db, tableToDelete, { deletion: true })
|
||||||
await cleanupAttachments(appId, tableToDelete, { rows })
|
await cleanupAttachments(appId, tableToDelete, { rows })
|
||||||
return tableToDelete
|
return tableToDelete
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,8 @@ const viewTemplate = require("../view/viewBuilder")
|
||||||
const usageQuota = require("../../../utilities/usageQuota")
|
const usageQuota = require("../../../utilities/usageQuota")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
|
|
||||||
exports.deleteColumn = async (db, table, columns) => {
|
exports.deleteColumns = async (db, table, columnNames) => {
|
||||||
columns.forEach(colName => delete table.schema[colName])
|
columnNames.forEach(colName => delete table.schema[colName])
|
||||||
const rows = await db.allDocs(
|
const rows = await db.allDocs(
|
||||||
getRowParams(table._id, null, {
|
getRowParams(table._id, null, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
|
@ -34,7 +34,7 @@ exports.deleteColumn = async (db, table, columns) => {
|
||||||
await db.put(table)
|
await db.put(table)
|
||||||
return db.bulkDocs(
|
return db.bulkDocs(
|
||||||
rows.rows.map(({ doc }) => {
|
rows.rows.map(({ doc }) => {
|
||||||
columns.forEach(colName => delete doc[colName])
|
columnNames.forEach(colName => delete doc[colName])
|
||||||
return doc
|
return doc
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,6 +25,7 @@ exports.processFormulas = (
|
||||||
}
|
}
|
||||||
// iterate through rows and process formula
|
// iterate through rows and process formula
|
||||||
for (let i = 0; i < rows.length; i++) {
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
if (schema.formula) {
|
||||||
let row = rows[i]
|
let row = rows[i]
|
||||||
let context = contextRows ? contextRows[i] : row
|
let context = contextRows ? contextRows[i] : row
|
||||||
rows[i] = {
|
rows[i] = {
|
||||||
|
@ -33,5 +34,6 @@ exports.processFormulas = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return single ? rows[0] : rows
|
return single ? rows[0] : rows
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ module.exports.processStringSync = templates.processStringSync
|
||||||
module.exports.processObjectSync = templates.processObjectSync
|
module.exports.processObjectSync = templates.processObjectSync
|
||||||
module.exports.processString = templates.processString
|
module.exports.processString = templates.processString
|
||||||
module.exports.processObject = templates.processObject
|
module.exports.processObject = templates.processObject
|
||||||
|
module.exports.doesContainStrings = templates.doesContainStrings
|
||||||
module.exports.doesContainString = templates.doesContainString
|
module.exports.doesContainString = templates.doesContainString
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -224,15 +224,13 @@ module.exports.decodeJSBinding = handlebars => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function looks in the supplied template for handlebars instances, if they contain
|
* Same as the doesContainString function, but will check for all the strings
|
||||||
* JS the JS will be decoded and then the supplied string will be looked for. For example
|
* before confirming it contains.
|
||||||
* if the template "Hello, your name is {{ related }}" this function would return that true
|
* @param {string} template The template string to search.
|
||||||
* for the string "related" but not for "name" as it is not within the handlebars statement.
|
* @param {string[]} strings The strings to look for.
|
||||||
* @param {string} template A template string to search for handlebars instances.
|
* @returns {boolean} Will return true if all strings found in HBS statement.
|
||||||
* @param {string} string The word or sentence to search for.
|
|
||||||
* @returns {boolean} The this return true if the string is found, false if not.
|
|
||||||
*/
|
*/
|
||||||
module.exports.doesContainString = (template, string) => {
|
module.exports.doesContainStrings = (template, strings) => {
|
||||||
let regexp = new RegExp(FIND_HBS_REGEX)
|
let regexp = new RegExp(FIND_HBS_REGEX)
|
||||||
let matches = template.match(regexp)
|
let matches = template.match(regexp)
|
||||||
if (matches == null) {
|
if (matches == null) {
|
||||||
|
@ -243,9 +241,28 @@ module.exports.doesContainString = (template, string) => {
|
||||||
if (exports.isJSBinding(match)) {
|
if (exports.isJSBinding(match)) {
|
||||||
hbs = exports.decodeJSBinding(match)
|
hbs = exports.decodeJSBinding(match)
|
||||||
}
|
}
|
||||||
if (hbs.includes(string)) {
|
let allFound = true
|
||||||
|
for (let string of strings) {
|
||||||
|
if (!hbs.includes(string)) {
|
||||||
|
allFound = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allFound) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function looks in the supplied template for handlebars instances, if they contain
|
||||||
|
* JS the JS will be decoded and then the supplied string will be looked for. For example
|
||||||
|
* if the template "Hello, your name is {{ related }}" this function would return that true
|
||||||
|
* for the string "related" but not for "name" as it is not within the handlebars statement.
|
||||||
|
* @param {string} template A template string to search for handlebars instances.
|
||||||
|
* @param {string} string The word or sentence to search for.
|
||||||
|
* @returns {boolean} The this return true if the string is found, false if not.
|
||||||
|
*/
|
||||||
|
module.exports.doesContainString = (template, string) => {
|
||||||
|
return exports.doesContainStrings(template, [string])
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ export const processStringSync = templates.processStringSync
|
||||||
export const processObjectSync = templates.processObjectSync
|
export const processObjectSync = templates.processObjectSync
|
||||||
export const processString = templates.processString
|
export const processString = templates.processString
|
||||||
export const processObject = templates.processObject
|
export const processObject = templates.processObject
|
||||||
|
export const doesContainStrings = templates.doesContainStrings
|
||||||
export const doesContainString = templates.doesContainString
|
export const doesContainString = templates.doesContainString
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue