From 249b10a60599f4b880d0964e8b5ea41115b8b135 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 19 Jan 2022 18:33:58 +0000 Subject: [PATCH 01/15] Adding in static/dynamic select, as well as the ability to set a tooltip on a select. --- packages/bbui/src/Form/Field.svelte | 3 +- packages/bbui/src/Form/FieldLabel.svelte | 17 ++++-- packages/bbui/src/Form/Select.svelte | 3 +- packages/bbui/src/Label/Label.svelte | 59 +----------------- .../bbui/src/Tooltip/TooltipWrapper.svelte | 60 +++++++++++++++++++ .../DataTable/modals/CreateEditColumn.svelte | 20 ++++++- 6 files changed, 96 insertions(+), 66 deletions(-) create mode 100644 packages/bbui/src/Tooltip/TooltipWrapper.svelte diff --git a/packages/bbui/src/Form/Field.svelte b/packages/bbui/src/Form/Field.svelte index a2e41072a5..5033c28f7d 100644 --- a/packages/bbui/src/Form/Field.svelte +++ b/packages/bbui/src/Form/Field.svelte @@ -6,11 +6,12 @@ export let label = null export let labelPosition = "above" export let error = null + export let tooltip = ""
{#if label} - + {/if}
diff --git a/packages/bbui/src/Form/FieldLabel.svelte b/packages/bbui/src/Form/FieldLabel.svelte index b070df8cae..3606d77c7b 100644 --- a/packages/bbui/src/Form/FieldLabel.svelte +++ b/packages/bbui/src/Form/FieldLabel.svelte @@ -1,19 +1,24 @@ - + + + diff --git a/packages/bbui/src/Tooltip/TooltipWrapper.svelte b/packages/bbui/src/Tooltip/TooltipWrapper.svelte new file mode 100644 index 0000000000..c587dec1dc --- /dev/null +++ b/packages/bbui/src/Tooltip/TooltipWrapper.svelte @@ -0,0 +1,60 @@ + + +
+ + {#if tooltip} +
+
(showTooltip = true)} + on:mouseleave={() => (showTooltip = false)} + > + +
+ {#if showTooltip} +
+ +
+ {/if} +
+ {/if} +
+ + diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 1fa5c6e073..370289f11f 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -150,6 +150,7 @@ delete field.subtype delete field.tableId delete field.relationshipType + delete field.formulaType // Add in defaults and initial definition const definition = fieldDefinitions[event.detail?.toUpperCase()] @@ -161,6 +162,9 @@ if (field.type === LINK_TYPE) { field.relationshipType = RelationshipTypes.MANY_TO_MANY } + if (field.type === FORMULA_TYPE) { + field.formulaType = "dynamic" + } } function onChangeRequired(e) { @@ -431,8 +435,20 @@ error={errors.relatedName} /> {:else if field.type === FORMULA_TYPE} + (field.subtype = e.detail)} options={Object.entries(getAutoColumnInformation())} From c04379eaea68eeaeff8f5c8432007899c264199f Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 20 Jan 2022 15:16:27 +0000 Subject: [PATCH 02/15] Fixing issue with dynamic/static formula types not being stored. --- .../components/backend/DataTable/modals/CreateEditColumn.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 370289f11f..24508c8ee6 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -437,7 +437,7 @@ {:else if field.type === FORMULA_TYPE} option.label} - getOptionValue={option => option.value} - tooltip="Dynamic formula are calculated when retrieved, but cannot be filtered, - while static formula are calculated when the row is saved." - /> + {#if !table.sql} + {:else if ["options", "array"].includes(filter.type)} { const Op = OperatorOptions + const stringOps = [ + Op.Equals, + Op.NotEquals, + Op.StartsWith, + Op.Like, + Op.Empty, + Op.NotEmpty, + ] + const numOps = [ + Op.Equals, + Op.NotEquals, + Op.MoreThan, + Op.LessThan, + Op.Empty, + Op.NotEmpty, + ] if (type === "string") { - return [ - Op.Equals, - Op.NotEquals, - Op.StartsWith, - Op.Like, - Op.Empty, - Op.NotEmpty, - ] + return stringOps } else if (type === "number") { - return [ - Op.Equals, - Op.NotEquals, - Op.MoreThan, - Op.LessThan, - Op.Empty, - Op.NotEmpty, - ] + return numOps } else if (type === "options") { return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty] } else if (type === "array") { @@ -84,23 +86,11 @@ export const getValidOperatorsForType = type => { } else if (type === "boolean") { return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty] } else if (type === "longform") { - return [ - Op.Equals, - Op.NotEquals, - Op.StartsWith, - Op.Like, - Op.Empty, - Op.NotEmpty, - ] + return stringOps } else if (type === "datetime") { - return [ - Op.Equals, - Op.NotEquals, - Op.MoreThan, - Op.LessThan, - Op.Empty, - Op.NotEmpty, - ] + return numOps + } else if (type === "formula") { + return stringOps.concat([Op.MoreThan, Op.LessThan]) } return [] } diff --git a/packages/builder/src/helpers/searchFields.js b/packages/builder/src/helpers/searchFields.js index 650e04a680..a9c837d570 100644 --- a/packages/builder/src/helpers/searchFields.js +++ b/packages/builder/src/helpers/searchFields.js @@ -27,5 +27,8 @@ export function getFields(fields, { allowLinks } = { allowLinks: true }) { filteredFields = filteredFields.concat(getTableFields(linkField)) } } - return filteredFields + const staticFormulaFields = fields.filter( + field => field.type === "formula" && field.formulaType === "static" + ) + return filteredFields.concat(staticFormulaFields) } diff --git a/packages/server/src/api/controllers/row/internal.js b/packages/server/src/api/controllers/row/internal.js index 75caaf2fda..db573f45a4 100644 --- a/packages/server/src/api/controllers/row/internal.js +++ b/packages/server/src/api/controllers/row/internal.js @@ -12,6 +12,7 @@ const { outputProcessing, processAutoColumn, cleanupAttachments, + processFormulas, } = require("../../../utilities/rowProcessor") const { FieldTypes } = require("../../../constants") const { isEqual } = require("lodash") @@ -36,6 +37,17 @@ const CALCULATION_TYPES = { async function storeResponse(ctx, db, row, oldTable, table) { row.type = "row" + let rowToSave = cloneDeep(row) + // process the row before return, to include relationships + let enrichedRow = await outputProcessing(ctx, 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 @@ -45,7 +57,7 @@ async function storeResponse(ctx, db, row, oldTable, table) { } catch (err) { if (err.status === 409) { const updatedTable = await db.get(table._id) - let response = processAutoColumn(null, updatedTable, row, { + let response = processAutoColumn(null, updatedTable, rowToSave, { reprocessing: true, }) await db.put(response.table) @@ -56,10 +68,10 @@ async function storeResponse(ctx, db, row, oldTable, table) { } } const response = await db.put(row) - row._rev = response.rev - // process the row before return, to include relationships - row = await outputProcessing(ctx, table, row, { squash: false }) - return { row, table } + // for response, calculate the formulas for the enriched row + enrichedRow._rev = response.rev + enrichedRow = await processFormulas(table, enrichedRow, { dynamic: false }) + return { row: enrichedRow, table } } // doesn't do the outputProcessing diff --git a/packages/server/src/api/controllers/table/index.js b/packages/server/src/api/controllers/table/index.js index 20dc10017d..5c36e5ad5e 100644 --- a/packages/server/src/api/controllers/table/index.js +++ b/packages/server/src/api/controllers/table/index.js @@ -3,12 +3,8 @@ const internal = require("./internal") const external = require("./external") const csvParser = require("../../../utilities/csvParser") const { isExternalTable, isSQL } = require("../../../integrations/utils") -const { - getTableParams, - getDatasourceParams, - BudibaseInternalDB, -} = require("../../../db/utils") -const { getTable } = require("./utils") +const { getDatasourceParams } = require("../../../db/utils") +const { getTable, getAllInternalTables } = require("./utils") function pickApi({ tableId, table }) { if (table && !tableId) { @@ -26,17 +22,7 @@ function pickApi({ tableId, table }) { exports.fetch = async function (ctx) { const db = new CouchDB(ctx.appId) - const internalTables = await db.allDocs( - getTableParams(null, { - include_docs: true, - }) - ) - - const internal = internalTables.rows.map(tableDoc => ({ - ...tableDoc.doc, - type: "internal", - sourceId: BudibaseInternalDB._id, - })) + const internal = await getAllInternalTables({ db }) const externalTables = await db.allDocs( getDatasourceParams("plus", { diff --git a/packages/server/src/api/controllers/table/internal.js b/packages/server/src/api/controllers/table/internal.js index 9f09e78219..9e48fd471a 100644 --- a/packages/server/src/api/controllers/table/internal.js +++ b/packages/server/src/api/controllers/table/internal.js @@ -1,14 +1,82 @@ const CouchDB = require("../../../db") const linkRows = require("../../../db/linkedRows") const { getRowParams, generateTableID } = require("../../../db/utils") -const { FieldTypes } = require("../../../constants") +const { FieldTypes, FormulaTypes } = require("../../../constants") const { TableSaveFunctions, hasTypeChanged, getTable, handleDataImport, + getAllInternalTables, } = require("./utils") const usageQuota = require("../../../utilities/usageQuota") +const { doesContainString } = require("@budibase/string-templates") +const { cloneDeep } = require("lodash/fp") +const { isEqual } = require("lodash") + +/** + * 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 updateRelatedTablesForFormula(db, table) { + // 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) + } + } + 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)) { + 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] + } + } + } + // 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.save = async function (ctx) { const appId = ctx.appId @@ -104,6 +172,8 @@ exports.save = async function (ctx) { tableToSave._rev = result.rev tableToSave = await tableSaveFunctions.after(tableToSave) + // has to run after, make sure it has _id + await updateRelatedTablesForFormula(db, tableToSave) return tableToSave } diff --git a/packages/server/src/api/controllers/table/utils.js b/packages/server/src/api/controllers/table/utils.js index 86e2837e15..3db7fd9d9f 100644 --- a/packages/server/src/api/controllers/table/utils.js +++ b/packages/server/src/api/controllers/table/utils.js @@ -4,6 +4,8 @@ const { getRowParams, generateRowID, InternalTables, + getTableParams, + BudibaseInternalDB, } = require("../../../db/utils") const { isEqual } = require("lodash/fp") const { AutoFieldSubTypes, FieldTypes } = require("../../../constants") @@ -230,8 +232,26 @@ class TableSaveFunctions { } } -exports.getAllExternalTables = async (appId, datasourceId) => { - const db = new CouchDB(appId) +exports.getAllInternalTables = async ({ db, appId }) => { + if (appId && !db) { + db = new CouchDB(appId) + } + const internalTables = await db.allDocs( + getTableParams(null, { + include_docs: true, + }) + ) + return internalTables.rows.map(tableDoc => ({ + ...tableDoc.doc, + type: "internal", + sourceId: BudibaseInternalDB._id, + })) +} + +exports.getAllExternalTables = async ({ db, appId }, datasourceId) => { + if (appId && !db) { + db = new CouchDB(appId) + } const datasource = await db.get(datasourceId) if (!datasource || !datasource.entities) { throw "Datasource is not configured fully." @@ -240,7 +260,7 @@ exports.getAllExternalTables = async (appId, datasourceId) => { } exports.getExternalTable = async (appId, datasourceId, tableName) => { - const entities = await exports.getAllExternalTables(appId, datasourceId) + const entities = await exports.getAllExternalTables({ appId }, datasourceId) return entities[tableName] } diff --git a/packages/server/src/db/linkedRows/LinkController.js b/packages/server/src/db/linkedRows/LinkController.js index b66e2debb5..832f6fecdd 100644 --- a/packages/server/src/db/linkedRows/LinkController.js +++ b/packages/server/src/db/linkedRows/LinkController.js @@ -156,6 +156,8 @@ 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/definitions/common.ts b/packages/server/src/definitions/common.ts index 472471855c..16885973f5 100644 --- a/packages/server/src/definitions/common.ts +++ b/packages/server/src/definitions/common.ts @@ -17,6 +17,8 @@ export interface FieldSchema { autocolumn?: boolean throughFrom?: string throughTo?: string + formula?: string + formulaType?: string main?: boolean meta?: { toTable: string @@ -46,6 +48,7 @@ export interface Table extends Base { schema: TableSchema primaryDisplay?: string sourceId?: string + relatedFormula?: string[] constrained?: string[] } diff --git a/packages/server/src/utilities/rowProcessor/index.js b/packages/server/src/utilities/rowProcessor/index.js index 330947090a..f6c3a66edf 100644 --- a/packages/server/src/utilities/rowProcessor/index.js +++ b/packages/server/src/utilities/rowProcessor/index.js @@ -180,6 +180,8 @@ function processAutoColumn( } exports.processAutoColumn = processAutoColumn +exports.processFormulas = processFormulas + /** * This will coerce a value to the correct types based on the type transform map * @param {object} row The value to coerce @@ -244,9 +246,6 @@ exports.inputProcessing = ( clonedRow._rev = row._rev } - // now process the static formulas - clonedRow = processFormulas(table, clonedRow, { dynamic: false }) - // handle auto columns - this returns an object like {table, row} return processAutoColumn(user, copiedTable, clonedRow, opts) } diff --git a/packages/server/src/utilities/rowProcessor/utils.js b/packages/server/src/utilities/rowProcessor/utils.js index 30e0b75e0f..30185caca6 100644 --- a/packages/server/src/utilities/rowProcessor/utils.js +++ b/packages/server/src/utilities/rowProcessor/utils.js @@ -4,10 +4,15 @@ const { processStringSync } = require("@budibase/string-templates") /** * Looks through the rows provided and finds formulas - which it then processes. */ -exports.processFormulas = (table, rows, { dynamic } = { dynamic: true }) => { +exports.processFormulas = ( + table, + rows, + { dynamic, contextRows } = { dynamic: true } +) => { const single = !Array.isArray(rows) if (single) { rows = [rows] + contextRows = contextRows ? [contextRows] : contextRows } for (let [column, schema] of Object.entries(table.schema)) { const isStatic = schema.formulaType === FormulaTypes.STATIC @@ -19,10 +24,14 @@ exports.processFormulas = (table, rows, { dynamic } = { dynamic: true }) => { continue } // iterate through rows and process formula - rows = rows.map(row => ({ - ...row, - [column]: processStringSync(schema.formula, row), - })) + for (let i = 0; i < rows.length; i++) { + let row = rows[i] + let context = contextRows ? contextRows[i] : row + rows[i] = { + ...row, + [column]: processStringSync(schema.formula, context), + } + } } return single ? rows[0] : rows } diff --git a/packages/string-templates/src/index.cjs b/packages/string-templates/src/index.cjs index 5d05f0f57f..e82e8a688d 100644 --- a/packages/string-templates/src/index.cjs +++ b/packages/string-templates/src/index.cjs @@ -15,6 +15,7 @@ module.exports.processStringSync = templates.processStringSync module.exports.processObjectSync = templates.processObjectSync module.exports.processString = templates.processString module.exports.processObject = templates.processObject +module.exports.doesContainString = templates.doesContainString /** * Use vm2 to run JS scripts in a node env diff --git a/packages/string-templates/src/index.js b/packages/string-templates/src/index.js index 820b8da290..514a762e92 100644 --- a/packages/string-templates/src/index.js +++ b/packages/string-templates/src/index.js @@ -3,6 +3,7 @@ const { registerAll } = require("./helpers/index") const processors = require("./processors") const { atob, btoa } = require("./utilities") const manifest = require("../manifest.json") +const { FIND_HBS_REGEX } = require("./utilities") const hbsInstance = handlebars.create() registerAll(hbsInstance) @@ -26,7 +27,7 @@ function testObject(object) { * @param {object|array} object The input structure which is to be recursed, it is important to note that * if the structure contains any cycles then this will fail. * @param {object} context The context that handlebars should fill data from. - * @param {object} opts optional - specify some options for processing. + * @param {object|null} opts optional - specify some options for processing. * @returns {Promise} The structure input, as fully updated as possible. */ module.exports.processObject = async (object, context, opts) => { @@ -57,7 +58,7 @@ module.exports.processObject = async (object, context, opts) => { * then nothing will occur. * @param {string} string The template string which is the filled from the context object. * @param {object} context An object of information which will be used to enrich the string. - * @param {object} opts optional - specify some options for processing. + * @param {object|null} opts optional - specify some options for processing. * @returns {Promise} The enriched string, all templates should have been replaced if they can be. */ module.exports.processString = async (string, context, opts) => { @@ -71,7 +72,7 @@ module.exports.processString = async (string, context, opts) => { * @param {object|array} object The input structure which is to be recursed, it is important to note that * if the structure contains any cycles then this will fail. * @param {object} context The context that handlebars should fill data from. - * @param {object} opts optional - specify some options for processing. + * @param {object|null} opts optional - specify some options for processing. * @returns {object|array} The structure input, as fully updated as possible. */ module.exports.processObjectSync = (object, context, opts) => { @@ -92,7 +93,7 @@ module.exports.processObjectSync = (object, context, opts) => { * then nothing will occur. This is a pure sync call and therefore does not have the full functionality of the async call. * @param {string} string The template string which is the filled from the context object. * @param {object} context An object of information which will be used to enrich the string. - * @param {object} opts optional - specify some options for processing. + * @param {object|null} opts optional - specify some options for processing. * @returns {string} The enriched string, all templates should have been replaced if they can be. */ module.exports.processStringSync = (string, context, opts) => { @@ -221,3 +222,30 @@ module.exports.decodeJSBinding = handlebars => { } return atob(match[1]) } + +/** + * 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) => { + let regexp = new RegExp(FIND_HBS_REGEX) + let matches = template.match(regexp) + if (matches == null) { + return false + } + for (let match of matches) { + let hbs = match + if (exports.isJSBinding(match)) { + hbs = exports.decodeJSBinding(match) + } + if (hbs.includes(string)) { + return true + } + } + return false +} diff --git a/packages/string-templates/src/index.mjs b/packages/string-templates/src/index.mjs index 446e71ef88..49eb191ab9 100644 --- a/packages/string-templates/src/index.mjs +++ b/packages/string-templates/src/index.mjs @@ -15,6 +15,7 @@ export const processStringSync = templates.processStringSync export const processObjectSync = templates.processObjectSync export const processString = templates.processString export const processObject = templates.processObject +export const doesContainString = templates.doesContainString /** * Use polyfilled vm to run JS scripts in a browser Env diff --git a/packages/string-templates/test/basic.spec.js b/packages/string-templates/test/basic.spec.js index 2fd6505410..490c0aa514 100644 --- a/packages/string-templates/test/basic.spec.js +++ b/packages/string-templates/test/basic.spec.js @@ -4,6 +4,8 @@ const { isValid, makePropSafe, getManifest, + encodeJSBinding, + doesContainString, } = require("../src/index.cjs") describe("Test that the string processing works correctly", () => { @@ -157,3 +159,20 @@ describe("check full stops that are safe", () => { expect(output).toEqual("1") }) }) + +describe("check does contain string function", () => { + it("should work for a simple case", () => { + const hbs = "hello {{ name }}" + expect(doesContainString(hbs, "name")).toEqual(true) + }) + + it("should reject a case where its in the string, but not the handlebars", () => { + const hbs = "hello {{ name }}" + expect(doesContainString(hbs, "hello")).toEqual(false) + }) + + it("should handle if its in javascript", () => { + const js = encodeJSBinding(`return $("foo")`) + expect(doesContainString(js, "foo")).toEqual(true) + }) +}) From 10a2915bcb273e5e794537007db44bd0848eac5b Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 21 Jan 2022 17:45:24 +0000 Subject: [PATCH 05/15] Getting relationship re-enrichment working, so that static formulas will update when the value of the relationship changes. --- .../src/api/controllers/row/internal.js | 90 +++++++++++++++++-- .../src/api/controllers/table/internal.js | 57 +++++++----- .../src/db/linkedRows/LinkController.js | 2 - packages/server/src/db/linkedRows/index.js | 9 +- .../src/utilities/rowProcessor/index.js | 7 +- 5 files changed, 122 insertions(+), 43 deletions(-) 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 }) From 3bc51864b4bd493e7d9338203beb970e6ef71e34 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 24 Jan 2022 16:31:18 +0000 Subject: [PATCH 06/15] Handling deletion of related rows in formula updates. --- .../src/api/controllers/row/internal.js | 82 ++++++++++--------- .../server/src/api/controllers/table/utils.js | 2 +- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/packages/server/src/api/controllers/row/internal.js b/packages/server/src/api/controllers/row/internal.js index 21859188ae..8ee908c9a9 100644 --- a/packages/server/src/api/controllers/row/internal.js +++ b/packages/server/src/api/controllers/row/internal.js @@ -36,53 +36,58 @@ const CALCULATION_TYPES = { } /** - * This function runs through the enriched row, looks at the rows which + * 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. */ -async function updateRelatedFormula(appId, db, table, enrichedRow) { +async function updateRelatedFormula(appId, db, table, enrichedRows) { // 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, - }) - ) - ) + 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) { + 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 } - } catch (err) { - // no error scenario, table doesn't seem to exist anymore, ignore } } await Promise.all(promises) @@ -388,6 +393,8 @@ exports.destroy = async function (ctx) { }) // remove any attachments that were on the row from object storage await cleanupAttachments(appId, table, { row }) + // remove any static formula + await updateRelatedFormula(appId, db, table, row) let response if (ctx.params.tableId === InternalTables.USER_METADATA) { @@ -436,6 +443,7 @@ exports.bulkDestroy = async ctx => { } // remove any attachments that were on the rows from object storage await cleanupAttachments(appId, table, { rows }) + await updateRelatedFormula(appId, db, table, rows) await Promise.all(updates) return { response: { ok: true }, rows } } diff --git a/packages/server/src/api/controllers/table/utils.js b/packages/server/src/api/controllers/table/utils.js index 3db7fd9d9f..9528487dee 100644 --- a/packages/server/src/api/controllers/table/utils.js +++ b/packages/server/src/api/controllers/table/utils.js @@ -29,7 +29,7 @@ exports.checkForColumnUpdates = async (db, oldTable, updatedTable) => { colName => updatedTable.schema[colName] == null ) } - // check for renaming of columns or deleted columns + // check for renaming of columns, deleted columns or static formula update if (rename || deletedColumns.length !== 0) { // Update all rows const rows = await db.allDocs( From da26761773b177da210fece2a693b685336805fb Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 24 Jan 2022 16:32:41 +0000 Subject: [PATCH 07/15] Make sure attachments are deleted when table is deleted, or column is removed. --- .../src/api/controllers/table/internal.js | 2 + .../server/src/api/controllers/table/utils.js | 43 +++++++++++++++---- .../src/utilities/rowProcessor/index.js | 18 ++++++-- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/packages/server/src/api/controllers/table/internal.js b/packages/server/src/api/controllers/table/internal.js index 193050f956..e56555e151 100644 --- a/packages/server/src/api/controllers/table/internal.js +++ b/packages/server/src/api/controllers/table/internal.js @@ -13,6 +13,7 @@ const usageQuota = require("../../../utilities/usageQuota") const { doesContainString } = require("@budibase/string-templates") const { cloneDeep } = require("lodash/fp") const { isEqual } = require("lodash") +const { cleanupAttachments } = require("../../../utilities/rowProcessor") /** * This function adds a note to related tables that they are @@ -220,6 +221,7 @@ exports.destroy = async function (ctx) { // has to run after, make sure it has _id await updateRelatedTablesForFormula(db, tableToDelete, { deletion: true }) + await cleanupAttachments(appId, tableToDelete, { rows }) return tableToDelete } diff --git a/packages/server/src/api/controllers/table/utils.js b/packages/server/src/api/controllers/table/utils.js index 9528487dee..16fbeba783 100644 --- a/packages/server/src/api/controllers/table/utils.js +++ b/packages/server/src/api/controllers/table/utils.js @@ -7,9 +7,12 @@ const { getTableParams, BudibaseInternalDB, } = require("../../../db/utils") -const { isEqual } = require("lodash/fp") +const { isEqual } = require("lodash") const { AutoFieldSubTypes, FieldTypes } = require("../../../constants") -const { inputProcessing } = require("../../../utilities/rowProcessor") +const { + inputProcessing, + cleanupAttachments, +} = require("../../../utilities/rowProcessor") const { USERS_TABLE_SCHEMA, SwitchableTypes } = require("../../../constants") const { isExternalTable, @@ -19,8 +22,25 @@ const { const { getViews, saveView } = require("../view/utils") const viewTemplate = require("../view/viewBuilder") const usageQuota = require("../../../utilities/usageQuota") +const { cloneDeep } = require("lodash/fp") -exports.checkForColumnUpdates = async (db, oldTable, updatedTable) => { +exports.deleteColumn = async (db, table, columns) => { + columns.forEach(colName => delete table.schema[colName]) + const rows = await db.allDocs( + getRowParams(table._id, null, { + include_docs: true, + }) + ) + await db.put(table) + return db.bulkDocs( + rows.rows.map(({ doc }) => { + columns.forEach(colName => delete doc[colName]) + return doc + }) + ) +} + +exports.checkForColumnUpdates = async (appId, db, oldTable, updatedTable) => { let updatedRows = [] const rename = updatedTable._rename let deletedColumns = [] @@ -29,7 +49,7 @@ exports.checkForColumnUpdates = async (db, oldTable, updatedTable) => { colName => updatedTable.schema[colName] == null ) } - // check for renaming of columns, deleted columns or static formula update + // check for renaming of columns or deleted columns if (rename || deletedColumns.length !== 0) { // Update all rows const rows = await db.allDocs( @@ -37,16 +57,20 @@ exports.checkForColumnUpdates = async (db, oldTable, updatedTable) => { include_docs: true, }) ) - updatedRows = rows.rows.map(({ doc }) => { + const rawRows = rows.rows.map(({ doc }) => doc) + updatedRows = rawRows.map(row => { + row = cloneDeep(row) if (rename) { - doc[rename.updated] = doc[rename.old] - delete doc[rename.old] + row[rename.updated] = row[rename.old] + delete row[rename.old] } else if (deletedColumns.length !== 0) { - deletedColumns.forEach(colName => delete doc[colName]) + deletedColumns.forEach(colName => delete row[colName]) } - return doc + return row }) + // cleanup any attachments from object storage for deleted attachment columns + await cleanupAttachments(appId, updatedTable, { oldTable, rows: rawRows }) // Update views await exports.checkForViewUpdates(db, updatedTable, rename, deletedColumns) delete updatedTable._rename @@ -207,6 +231,7 @@ class TableSaveFunctions { // when confirmed valid async mid(table) { let response = await exports.checkForColumnUpdates( + this.appId, this.db, this.oldTable, table diff --git a/packages/server/src/utilities/rowProcessor/index.js b/packages/server/src/utilities/rowProcessor/index.js index 4237855fb3..dc56312d63 100644 --- a/packages/server/src/utilities/rowProcessor/index.js +++ b/packages/server/src/utilities/rowProcessor/index.js @@ -307,9 +307,15 @@ exports.outputProcessing = async ( * @param {any} row optional - the row being removed. * @param {any} rows optional - if multiple rows being deleted can do this in bulk. * @param {any} oldRow optional - if updating a row this will determine the difference. + * @param {any} oldTable optional - if updating a table, can supply the old table to look for + * deleted attachment columns. * @return {Promise} When all attachments have been removed this will return. */ -exports.cleanupAttachments = async (appId, table, { row, rows, oldRow }) => { +exports.cleanupAttachments = async ( + appId, + table, + { row, rows, oldRow, oldTable } +) => { if (!isProdAppID(appId)) { const prodAppId = getDeployedAppID(appId) // if prod exists, then don't allow deleting @@ -324,12 +330,16 @@ exports.cleanupAttachments = async (appId, table, { row, rows, oldRow }) => { files = files.concat(row[key].map(attachment => attachment.key)) } } - for (let [key, schema] of Object.entries(table.schema)) { + const schemaToUse = oldTable ? oldTable.schema : table.schema + for (let [key, schema] of Object.entries(schemaToUse)) { if (schema.type !== FieldTypes.ATTACHMENT) { continue } - // if updating, need to manage the differences - if (oldRow && row) { + // old table had this column, new table doesn't - delete it + if (oldTable && !table.schema[key]) { + rows.forEach(row => addFiles(row, key)) + } else if (oldRow && row) { + // if updating, need to manage the differences files = files.concat(getRemovedAttachmentKeys(oldRow, row, key)) } else if (row) { addFiles(row, key) From 6a6fdbb8ffe7dac14f6c2bfc73ea185845be04d5 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 24 Jan 2022 17:06:45 +0000 Subject: [PATCH 08/15] Refactoring bulk table formula work a bit. --- .../src/api/controllers/table/bulkFormula.js | 89 +++++++++++++++++++ .../src/api/controllers/table/internal.js | 82 +---------------- 2 files changed, 93 insertions(+), 78 deletions(-) create mode 100644 packages/server/src/api/controllers/table/bulkFormula.js diff --git a/packages/server/src/api/controllers/table/bulkFormula.js b/packages/server/src/api/controllers/table/bulkFormula.js new file mode 100644 index 0000000000..c1dcaf532b --- /dev/null +++ b/packages/server/src/api/controllers/table/bulkFormula.js @@ -0,0 +1,89 @@ +const { FieldTypes, FormulaTypes } = require("../../../constants") +const { getAllInternalTables } = require("./utils") +const { doesContainString } = require("@budibase/string-templates") +const { cloneDeep } = require("lodash/fp") +const { isEqual } = require("lodash") + +/** + * This retrieves the formula columns from a table schema that use a specified column name + * in the formula. + */ +function getFormulaThatUseColumn(table, columnName) { + let formula = [] + 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 (!doesContainString(column.formula, columnName)) { + continue + } + formula.push(column.name) + } + return formula +} + +/** + * 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. + */ +exports.updateRelatedFormulaLinksOnTables = async ( + 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) + } + } +} diff --git a/packages/server/src/api/controllers/table/internal.js b/packages/server/src/api/controllers/table/internal.js index e56555e151..88a0a5f442 100644 --- a/packages/server/src/api/controllers/table/internal.js +++ b/packages/server/src/api/controllers/table/internal.js @@ -1,90 +1,16 @@ const CouchDB = require("../../../db") const linkRows = require("../../../db/linkedRows") const { getRowParams, generateTableID } = require("../../../db/utils") -const { FieldTypes, FormulaTypes } = require("../../../constants") +const { FieldTypes } = require("../../../constants") const { TableSaveFunctions, hasTypeChanged, getTable, handleDataImport, - getAllInternalTables, } = require("./utils") const usageQuota = require("../../../utilities/usageQuota") -const { doesContainString } = require("@budibase/string-templates") -const { cloneDeep } = require("lodash/fp") -const { isEqual } = require("lodash") const { cleanupAttachments } = require("../../../utilities/rowProcessor") - -/** - * 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 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 - ) - // 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 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)) { - 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) - } - } -} +const { updateRelatedFormulaLinksOnTables } = require("./bulkFormula") exports.save = async function (ctx) { const appId = ctx.appId @@ -181,7 +107,7 @@ exports.save = async function (ctx) { tableToSave = await tableSaveFunctions.after(tableToSave) // has to run after, make sure it has _id - await updateRelatedTablesForFormula(db, tableToSave) + await updateRelatedFormulaLinksOnTables(db, tableToSave) return tableToSave } @@ -220,7 +146,7 @@ exports.destroy = async function (ctx) { } // has to run after, make sure it has _id - await updateRelatedTablesForFormula(db, tableToDelete, { deletion: true }) + await updateRelatedFormulaLinksOnTables(db, tableToDelete, { deletion: true }) await cleanupAttachments(appId, tableToDelete, { rows }) return tableToDelete } From ab5c7766b464ad8cf44de3f17f45a17b31951708 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 24 Jan 2022 18:22:59 +0000 Subject: [PATCH 09/15] Bit of refactoring, adding in functionality to remove invalid static formula when the elements that the formula depends on are removed. --- .../src/api/controllers/table/bulkFormula.js | 81 +++++++++++++++++-- .../src/api/controllers/table/internal.js | 7 +- .../server/src/api/controllers/table/utils.js | 6 +- .../src/utilities/rowProcessor/utils.js | 12 +-- packages/string-templates/src/index.cjs | 1 + packages/string-templates/src/index.js | 35 +++++--- packages/string-templates/src/index.mjs | 1 + 7 files changed, 115 insertions(+), 28 deletions(-) diff --git a/packages/server/src/api/controllers/table/bulkFormula.js b/packages/server/src/api/controllers/table/bulkFormula.js index c1dcaf532b..330751ab54 100644 --- a/packages/server/src/api/controllers/table/bulkFormula.js +++ b/packages/server/src/api/controllers/table/bulkFormula.js @@ -1,15 +1,16 @@ const { FieldTypes, FormulaTypes } = require("../../../constants") -const { getAllInternalTables } = require("./utils") -const { doesContainString } = require("@budibase/string-templates") +const { getAllInternalTables, deleteColumns } = require("./utils") +const { doesContainStrings } = require("@budibase/string-templates") 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 * in the formula. */ -function getFormulaThatUseColumn(table, columnName) { +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 ( @@ -18,7 +19,7 @@ function getFormulaThatUseColumn(table, columnName) { ) { continue } - if (!doesContainString(column.formula, columnName)) { + if (!doesContainStrings(column.formula, columnNames)) { continue } formula.push(column.name) @@ -26,17 +27,78 @@ function getFormulaThatUseColumn(table, columnName) { 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 * used in a static formula - so that the link controller * can manage hydrating related rows formula fields. This is * specifically only for static formula. */ -exports.updateRelatedFormulaLinksOnTables = async ( +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 @@ -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 }) +} diff --git a/packages/server/src/api/controllers/table/internal.js b/packages/server/src/api/controllers/table/internal.js index 88a0a5f442..9a477d25e7 100644 --- a/packages/server/src/api/controllers/table/internal.js +++ b/packages/server/src/api/controllers/table/internal.js @@ -10,7 +10,7 @@ const { } = require("./utils") const usageQuota = require("../../../utilities/usageQuota") const { cleanupAttachments } = require("../../../utilities/rowProcessor") -const { updateRelatedFormulaLinksOnTables } = require("./bulkFormula") +const { runStaticFormulaChecks } = require("./bulkFormula") exports.save = async function (ctx) { const appId = ctx.appId @@ -107,8 +107,7 @@ exports.save = async function (ctx) { tableToSave = await tableSaveFunctions.after(tableToSave) // has to run after, make sure it has _id - await updateRelatedFormulaLinksOnTables(db, tableToSave) - + await runStaticFormulaChecks(db, tableToSave, { oldTable }) return tableToSave } @@ -146,7 +145,7 @@ exports.destroy = async function (ctx) { } // 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 }) return tableToDelete } diff --git a/packages/server/src/api/controllers/table/utils.js b/packages/server/src/api/controllers/table/utils.js index 16fbeba783..d1a8fd82f7 100644 --- a/packages/server/src/api/controllers/table/utils.js +++ b/packages/server/src/api/controllers/table/utils.js @@ -24,8 +24,8 @@ const viewTemplate = require("../view/viewBuilder") const usageQuota = require("../../../utilities/usageQuota") const { cloneDeep } = require("lodash/fp") -exports.deleteColumn = async (db, table, columns) => { - columns.forEach(colName => delete table.schema[colName]) +exports.deleteColumns = async (db, table, columnNames) => { + columnNames.forEach(colName => delete table.schema[colName]) const rows = await db.allDocs( getRowParams(table._id, null, { include_docs: true, @@ -34,7 +34,7 @@ exports.deleteColumn = async (db, table, columns) => { await db.put(table) return db.bulkDocs( rows.rows.map(({ doc }) => { - columns.forEach(colName => delete doc[colName]) + columnNames.forEach(colName => delete doc[colName]) return doc }) ) diff --git a/packages/server/src/utilities/rowProcessor/utils.js b/packages/server/src/utilities/rowProcessor/utils.js index 30185caca6..95b7828084 100644 --- a/packages/server/src/utilities/rowProcessor/utils.js +++ b/packages/server/src/utilities/rowProcessor/utils.js @@ -25,11 +25,13 @@ exports.processFormulas = ( } // iterate through rows and process formula for (let i = 0; i < rows.length; i++) { - let row = rows[i] - let context = contextRows ? contextRows[i] : row - rows[i] = { - ...row, - [column]: processStringSync(schema.formula, context), + if (schema.formula) { + let row = rows[i] + let context = contextRows ? contextRows[i] : row + rows[i] = { + ...row, + [column]: processStringSync(schema.formula, context), + } } } } diff --git a/packages/string-templates/src/index.cjs b/packages/string-templates/src/index.cjs index e82e8a688d..bc9a410813 100644 --- a/packages/string-templates/src/index.cjs +++ b/packages/string-templates/src/index.cjs @@ -15,6 +15,7 @@ module.exports.processStringSync = templates.processStringSync module.exports.processObjectSync = templates.processObjectSync module.exports.processString = templates.processString module.exports.processObject = templates.processObject +module.exports.doesContainStrings = templates.doesContainStrings module.exports.doesContainString = templates.doesContainString /** diff --git a/packages/string-templates/src/index.js b/packages/string-templates/src/index.js index 514a762e92..3545baa76f 100644 --- a/packages/string-templates/src/index.js +++ b/packages/string-templates/src/index.js @@ -224,15 +224,13 @@ module.exports.decodeJSBinding = handlebars => { } /** - * 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. + * Same as the doesContainString function, but will check for all the strings + * before confirming it contains. + * @param {string} template The template string to search. + * @param {string[]} strings The strings to look for. + * @returns {boolean} Will return true if all strings found in HBS statement. */ -module.exports.doesContainString = (template, string) => { +module.exports.doesContainStrings = (template, strings) => { let regexp = new RegExp(FIND_HBS_REGEX) let matches = template.match(regexp) if (matches == null) { @@ -243,9 +241,28 @@ module.exports.doesContainString = (template, string) => { if (exports.isJSBinding(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 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]) +} diff --git a/packages/string-templates/src/index.mjs b/packages/string-templates/src/index.mjs index 49eb191ab9..a592ae26d5 100644 --- a/packages/string-templates/src/index.mjs +++ b/packages/string-templates/src/index.mjs @@ -15,6 +15,7 @@ export const processStringSync = templates.processStringSync export const processObjectSync = templates.processObjectSync export const processString = templates.processString export const processObject = templates.processObject +export const doesContainStrings = templates.doesContainStrings export const doesContainString = templates.doesContainString /** From da04db39c30165a282476d98e2ac7a878c125e56 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Mon, 24 Jan 2022 18:34:55 +0000 Subject: [PATCH 10/15] Fixing a couple of issues after testing, moving from deleting column to clearing. --- packages/server/src/api/controllers/table/bulkFormula.js | 8 ++++---- packages/server/src/api/controllers/table/utils.js | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/server/src/api/controllers/table/bulkFormula.js b/packages/server/src/api/controllers/table/bulkFormula.js index 330751ab54..ea5abce249 100644 --- a/packages/server/src/api/controllers/table/bulkFormula.js +++ b/packages/server/src/api/controllers/table/bulkFormula.js @@ -1,5 +1,5 @@ const { FieldTypes, FormulaTypes } = require("../../../constants") -const { getAllInternalTables, deleteColumns } = require("./utils") +const { getAllInternalTables, clearColumns } = require("./utils") const { doesContainStrings } = require("@budibase/string-templates") const { cloneDeep } = require("lodash/fp") const { isEqual, uniq } = require("lodash") @@ -53,7 +53,7 @@ async function checkRequiredFormulaUpdates(db, table, { oldTable, deletion }) { } const columnsToDelete = getFormulaThatUseColumn(tableToUse, removed.name) if (columnsToDelete.length > 0) { - await deleteColumns(db, table, columnsToDelete) + 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 @@ -80,7 +80,7 @@ async function checkRequiredFormulaUpdates(db, table, { oldTable, deletion }) { ) } if (relatedFormulaToRemove.length > 0) { - await deleteColumns(db, relatedTable, uniq(relatedFormulaToRemove)) + await clearColumns(db, relatedTable, uniq(relatedFormulaToRemove)) } } } @@ -133,7 +133,7 @@ async function updateRelatedFormulaLinksOnTables( if ( relatedTable && (!relatedTable.relatedFormula || - relatedTable.relatedFormula.includes(table._id)) + !relatedTable.relatedFormula.includes(table._id)) ) { relatedTable.relatedFormula = relatedTable.relatedFormula ? [...relatedTable.relatedFormula, table._id] diff --git a/packages/server/src/api/controllers/table/utils.js b/packages/server/src/api/controllers/table/utils.js index d1a8fd82f7..5e3377901a 100644 --- a/packages/server/src/api/controllers/table/utils.js +++ b/packages/server/src/api/controllers/table/utils.js @@ -24,14 +24,12 @@ const viewTemplate = require("../view/viewBuilder") const usageQuota = require("../../../utilities/usageQuota") const { cloneDeep } = require("lodash/fp") -exports.deleteColumns = async (db, table, columnNames) => { - columnNames.forEach(colName => delete table.schema[colName]) +exports.clearColumns = async (db, table, columnNames) => { const rows = await db.allDocs( getRowParams(table._id, null, { include_docs: true, }) ) - await db.put(table) return db.bulkDocs( rows.rows.map(({ doc }) => { columnNames.forEach(colName => delete doc[colName]) From b4eef68d71717538a9f0c913534614576bf07b49 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 25 Jan 2022 16:01:04 +0000 Subject: [PATCH 11/15] Adding the formula bulk recalculation when adding/changing a formula field. --- .../src/api/controllers/row/internal.js | 161 +++--------------- .../src/api/controllers/row/staticFormula.js | 156 +++++++++++++++++ .../src/api/controllers/table/bulkFormula.js | 108 +++++++----- .../server/src/api/controllers/table/index.js | 2 +- .../src/api/controllers/table/internal.js | 4 +- .../server/src/api/controllers/table/utils.js | 17 +- 6 files changed, 257 insertions(+), 191 deletions(-) create mode 100644 packages/server/src/api/controllers/row/staticFormula.js diff --git a/packages/server/src/api/controllers/row/internal.js b/packages/server/src/api/controllers/row/internal.js index 8ee908c9a9..0e9c2e651d 100644 --- a/packages/server/src/api/controllers/row/internal.js +++ b/packages/server/src/api/controllers/row/internal.js @@ -1,8 +1,8 @@ const CouchDB = require("../../../db") const linkRows = require("../../../db/linkedRows") const { - getRowParams, generateRowID, + getRowParams, DocumentTypes, InternalTables, } = require("../../../db/utils") @@ -10,12 +10,9 @@ const userController = require("../user") const { inputProcessing, outputProcessing, - processAutoColumn, cleanupAttachments, - processFormulas, } = require("../../../utilities/rowProcessor") -const { FieldTypes, FormulaTypes } = require("../../../constants") -const { isEqual } = require("lodash") +const { FieldTypes } = require("../../../constants") const { validate, findRow } = require("./utils") const { fullSearch, paginatedSearch } = require("./internalSearch") const { getGlobalUsersFromMetadata } = require("../../../utilities/global") @@ -28,6 +25,7 @@ const { getFromMemoryDoc, } = require("../view/utils") const { cloneDeep } = require("lodash/fp") +const { finaliseRow, updateRelatedFormula } = require("./staticFormula") const CALCULATION_TYPES = { SUM: "sum", @@ -35,135 +33,6 @@ const CALCULATION_TYPES = { STATS: "stats", } -/** - * 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. - */ -async function updateRelatedFormula(appId, db, table, enrichedRows) { - // 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) { - 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" - // process the row before return, to include relationships - let enrichedRow = await outputProcessing({ appId }, 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 updateRelatedFormula(appId, db, table, enrichedRow) - } - return { row: enrichedRow, table } -} - -// doesn't do the outputProcessing -async function getRawTableData(ctx, db, tableId) { - let rows - if (tableId === InternalTables.USER_METADATA) { - await userController.fetchMetadata(ctx) - rows = ctx.body - } else { - const response = await db.allDocs( - getRowParams(tableId, null, { - include_docs: true, - }) - ) - rows = response.rows.map(row => row.doc) - } - return rows -} - async function getView(db, viewName) { let mainGetter = env.SELF_HOSTED ? getFromDesignDoc : getFromMemoryDoc let secondaryGetter = env.SELF_HOSTED ? getFromMemoryDoc : getFromDesignDoc @@ -190,6 +59,22 @@ async function getView(db, viewName) { return viewInfo } +async function getRawTableData(ctx, db, tableId) { + let rows + if (tableId === InternalTables.USER_METADATA) { + await userController.fetchMetadata(ctx) + rows = ctx.body + } else { + const response = await db.allDocs( + getRowParams(tableId, null, { + include_docs: true, + }) + ) + rows = response.rows.map(row => row.doc) + } + return rows +} + exports.patch = async ctx => { const appId = ctx.appId const db = new CouchDB(appId) @@ -247,7 +132,7 @@ exports.patch = async ctx => { return { row: ctx.body, table } } - return storeResponse(ctx.appId, db, table, row, { + return finaliseRow(ctx.appId, table, row, { oldTable: dbTable, updateFormula: true, }) @@ -284,7 +169,7 @@ exports.save = async function (ctx) { table, }) - return storeResponse(ctx.appId, db, table, row, { + return finaliseRow(ctx.appId, table, row, { oldTable: dbTable, updateFormula: true, }) @@ -394,7 +279,7 @@ exports.destroy = async function (ctx) { // remove any attachments that were on the row from object storage await cleanupAttachments(appId, table, { row }) // remove any static formula - await updateRelatedFormula(appId, db, table, row) + await updateRelatedFormula(appId, table, row) let response if (ctx.params.tableId === InternalTables.USER_METADATA) { @@ -443,7 +328,7 @@ exports.bulkDestroy = async ctx => { } // remove any attachments that were on the rows from object storage await cleanupAttachments(appId, table, { rows }) - await updateRelatedFormula(appId, db, table, rows) + await updateRelatedFormula(appId, table, rows) await Promise.all(updates) return { response: { ok: true }, rows } } diff --git a/packages/server/src/api/controllers/row/staticFormula.js b/packages/server/src/api/controllers/row/staticFormula.js new file mode 100644 index 0000000000..2508af3bdd --- /dev/null +++ b/packages/server/src/api/controllers/row/staticFormula.js @@ -0,0 +1,156 @@ +const CouchDB = require("../../../db") +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") + +/** + * 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 (appId, table, enrichedRows) => { + const db = new CouchDB(appId) + // 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) { + 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 => + exports.finaliseRow(appId, relatedTable, related, { + updateFormula: false, + }) + ) + ) + } + } + } catch (err) { + // no error scenario, table doesn't seem to exist anymore, ignore + } + } + } + await Promise.all(promises) +} + +exports.updateAllFormulasInTable = async (appId, table) => { + const db = new CouchDB(appId) + // 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({ appId }, 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 ( + appId, + table, + row, + { oldTable, updateFormula } = { updateFormula: true } +) => { + const db = new CouchDB(appId) + row.type = "row" + // process the row before return, to include relationships + let enrichedRow = await outputProcessing({ appId }, 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(appId, table, enrichedRow) + } + return { row: enrichedRow, table } +} diff --git a/packages/server/src/api/controllers/table/bulkFormula.js b/packages/server/src/api/controllers/table/bulkFormula.js index ea5abce249..cb2446eb73 100644 --- a/packages/server/src/api/controllers/table/bulkFormula.js +++ b/packages/server/src/api/controllers/table/bulkFormula.js @@ -1,8 +1,17 @@ +const CouchDB = require("../../../db") 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") +const { updateAllFormulasInTable } = require("../row/staticFormula") + +function isStaticFormula(column) { + return ( + column.type === FieldTypes.FORMULA && + column.formulaType === FormulaTypes.STATIC + ) +} /** * This retrieves the formula columns from a table schema that use a specified column name @@ -13,10 +22,7 @@ function getFormulaThatUseColumn(table, columnNames) { 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 - ) { + if (!isStaticFormula(column)) { continue } if (!doesContainStrings(column.formula, columnNames)) { @@ -28,16 +34,18 @@ function getFormulaThatUseColumn(table, columnNames) { } /** - * This functions checks two things: - * 1. when a related table, column or related column is deleted, if any + * This functions checks for 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 }) { +async function checkIfFormulaNeedsCleared( + appId, + table, + { oldTable, deletion } +) { + const db = new CouchDB(appId) // start by retrieving all tables, remove the current table from the list - const tables = (await getAllInternalTables({ db })).filter( + const tables = (await getAllInternalTables(appId)).filter( tbl => tbl._id !== table._id ) const schemaToUse = oldTable ? oldTable.schema : table.schema @@ -57,31 +65,28 @@ async function checkRequiredFormulaUpdates(db, table, { oldTable, deletion }) { } // 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)) - } + if (!table.relatedFormula) { + continue + } + 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)) } } } @@ -95,12 +100,13 @@ async function checkRequiredFormulaUpdates(db, table, { oldTable, deletion }) { * specifically only for static formula. */ async function updateRelatedFormulaLinksOnTables( - db, + appId, table, { deletion } = { deletion: false } ) { + const db = new CouchDB(appId) // start by retrieving all tables, remove the current table from the list - const tables = (await getAllInternalTables({ db })).filter( + const tables = (await getAllInternalTables(appId)).filter( tbl => tbl._id !== table._id ) // clone the tables, so we can compare at end @@ -150,7 +156,29 @@ async function updateRelatedFormulaLinksOnTables( } } -exports.runStaticFormulaChecks = async (db, table, { oldTable, deletion }) => { - await updateRelatedFormulaLinksOnTables(db, table, { deletion }) - await checkRequiredFormulaUpdates(db, table, { oldTable, deletion }) +async function checkIfFormulaUpdated(appId, table, { oldTable }) { + // look to see if any formula values have changed + const shouldUpdate = Object.values(table.schema).find( + column => + isStaticFormula(column) && + (!oldTable || + !oldTable.schema[column.name] || + !isEqual(oldTable.schema[column.name], column)) + ) + // if a static formula column has updated, then need to run the update + if (shouldUpdate != null) { + await updateAllFormulasInTable(appId, table) + } +} + +exports.runStaticFormulaChecks = async ( + appId, + table, + { oldTable, deletion } +) => { + await updateRelatedFormulaLinksOnTables(appId, table, { deletion }) + await checkIfFormulaNeedsCleared(appId, table, { oldTable, deletion }) + if (!deletion) { + await checkIfFormulaUpdated(appId, table, { oldTable }) + } } diff --git a/packages/server/src/api/controllers/table/index.js b/packages/server/src/api/controllers/table/index.js index 5c36e5ad5e..2f6bfd0cb3 100644 --- a/packages/server/src/api/controllers/table/index.js +++ b/packages/server/src/api/controllers/table/index.js @@ -22,7 +22,7 @@ function pickApi({ tableId, table }) { exports.fetch = async function (ctx) { const db = new CouchDB(ctx.appId) - const internal = await getAllInternalTables({ db }) + const internal = await getAllInternalTables(ctx.appId) const externalTables = await db.allDocs( getDatasourceParams("plus", { diff --git a/packages/server/src/api/controllers/table/internal.js b/packages/server/src/api/controllers/table/internal.js index 9a477d25e7..f38a114c25 100644 --- a/packages/server/src/api/controllers/table/internal.js +++ b/packages/server/src/api/controllers/table/internal.js @@ -107,7 +107,7 @@ exports.save = async function (ctx) { tableToSave = await tableSaveFunctions.after(tableToSave) // has to run after, make sure it has _id - await runStaticFormulaChecks(db, tableToSave, { oldTable }) + await runStaticFormulaChecks(appId, tableToSave, { oldTable }) return tableToSave } @@ -145,7 +145,7 @@ exports.destroy = async function (ctx) { } // has to run after, make sure it has _id - await runStaticFormulaChecks(db, tableToDelete, { deletion: true }) + await runStaticFormulaChecks(appId, tableToDelete, { deletion: true }) await cleanupAttachments(appId, tableToDelete, { rows }) return tableToDelete } diff --git a/packages/server/src/api/controllers/table/utils.js b/packages/server/src/api/controllers/table/utils.js index 5e3377901a..81c22ab9b7 100644 --- a/packages/server/src/api/controllers/table/utils.js +++ b/packages/server/src/api/controllers/table/utils.js @@ -24,7 +24,8 @@ const viewTemplate = require("../view/viewBuilder") const usageQuota = require("../../../utilities/usageQuota") const { cloneDeep } = require("lodash/fp") -exports.clearColumns = async (db, table, columnNames) => { +exports.clearColumns = async (appId, table, columnNames) => { + const db = new CouchDB(appId) const rows = await db.allDocs( getRowParams(table._id, null, { include_docs: true, @@ -255,10 +256,8 @@ class TableSaveFunctions { } } -exports.getAllInternalTables = async ({ db, appId }) => { - if (appId && !db) { - db = new CouchDB(appId) - } +exports.getAllInternalTables = async appId => { + const db = new CouchDB(appId) const internalTables = await db.allDocs( getTableParams(null, { include_docs: true, @@ -271,10 +270,8 @@ exports.getAllInternalTables = async ({ db, appId }) => { })) } -exports.getAllExternalTables = async ({ db, appId }, datasourceId) => { - if (appId && !db) { - db = new CouchDB(appId) - } +exports.getAllExternalTables = async (appId, datasourceId) => { + const db = new CouchDB(appId) const datasource = await db.get(datasourceId) if (!datasource || !datasource.entities) { throw "Datasource is not configured fully." @@ -283,7 +280,7 @@ exports.getAllExternalTables = async ({ db, appId }, datasourceId) => { } exports.getExternalTable = async (appId, datasourceId, tableName) => { - const entities = await exports.getAllExternalTables({ appId }, datasourceId) + const entities = await exports.getAllExternalTables(appId, datasourceId) return entities[tableName] } From ff6c81f26518af162d2a606c184d1b86e04be791 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 25 Jan 2022 18:28:55 +0000 Subject: [PATCH 12/15] Getting client side block search fields working with searching formulas. --- packages/client/src/components/app/blocks/CardsBlock.svelte | 1 + packages/client/src/components/app/blocks/TableBlock.svelte | 6 ++++-- packages/client/src/components/app/forms/Field.svelte | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/client/src/components/app/blocks/CardsBlock.svelte b/packages/client/src/components/app/blocks/CardsBlock.svelte index f0892ca447..fe2af2e12d 100644 --- a/packages/client/src/components/app/blocks/CardsBlock.svelte +++ b/packages/client/src/components/app/blocks/CardsBlock.svelte @@ -39,6 +39,7 @@ number: "numberfield", datetime: "datetimefield", boolean: "booleanfield", + formula: "stringfield", } let formId diff --git a/packages/client/src/components/app/blocks/TableBlock.svelte b/packages/client/src/components/app/blocks/TableBlock.svelte index 3de4497731..cd38908545 100644 --- a/packages/client/src/components/app/blocks/TableBlock.svelte +++ b/packages/client/src/components/app/blocks/TableBlock.svelte @@ -35,6 +35,7 @@ number: "numberfield", datetime: "datetimefield", boolean: "booleanfield", + formula: "stringfield", } let formId @@ -60,10 +61,11 @@ let enrichedFilter = [...(filter || [])] columns?.forEach(column => { const safePath = column.name.split(".").map(safe).join(".") + const stringType = column.type === "string" || column.type === "formula" enrichedFilter.push({ field: column.name, - operator: column.type === "string" ? "string" : "equal", - type: column.type === "string" ? "string" : "number", + operator: stringType ? "string" : "equal", + type: stringType ? "string" : "number", valueType: "Binding", value: `{{ ${safe(formId)}.${safePath} }}`, }) diff --git a/packages/client/src/components/app/forms/Field.svelte b/packages/client/src/components/app/forms/Field.svelte index 79de01351a..ca7bd3caaf 100644 --- a/packages/client/src/components/app/forms/Field.svelte +++ b/packages/client/src/components/app/forms/Field.svelte @@ -32,6 +32,7 @@ validation, formStep ) + $: schemaType = fieldSchema?.type !== "formula" ? fieldSchema?.type : "string" // Focus label when editing let labelNode @@ -72,7 +73,7 @@ - {:else if fieldSchema?.type && fieldSchema?.type !== type && type !== "options"} + {:else if schemaType && schemaType !== type && type !== "options"} From 223b5a0e16edbb9f778d36324d18f505f31424fc Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 26 Jan 2022 17:49:02 +0000 Subject: [PATCH 13/15] Fixing dynamic filter modal in client library to include static formula. --- .../components/app/dynamic-filter/FilterModal.svelte | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/client/src/components/app/dynamic-filter/FilterModal.svelte b/packages/client/src/components/app/dynamic-filter/FilterModal.svelte index f303c69aaf..16d5bb0ee5 100644 --- a/packages/client/src/components/app/dynamic-filter/FilterModal.svelte +++ b/packages/client/src/components/app/dynamic-filter/FilterModal.svelte @@ -19,10 +19,14 @@ export let schemaFields export let filters = [] - const BannedTypes = ["link", "attachment", "formula", "json"] + const BannedTypes = ["link", "attachment", "json"] $: fieldOptions = (schemaFields ?? []) - .filter(field => !BannedTypes.includes(field.type)) + .filter( + field => + !BannedTypes.includes(field.type) || + (field.type === "formula" && field.formulaType === "static") + ) .map(field => field.name) const addFilter = () => { @@ -114,7 +118,7 @@ on:change={e => onOperatorChange(filter, e.detail)} placeholder={null} /> - {#if ["string", "longform", "number"].includes(filter.type)} + {#if ["string", "longform", "number", "formula"].includes(filter.type)} {:else if ["options", "array"].includes(filter.type)} Date: Mon, 31 Jan 2022 14:16:03 +0000 Subject: [PATCH 14/15] Review comments. --- .../src/api/controllers/row/staticFormula.js | 35 ++++++++++--------- .../src/api/controllers/table/bulkFormula.js | 1 - 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/server/src/api/controllers/row/staticFormula.js b/packages/server/src/api/controllers/row/staticFormula.js index 2508af3bdd..fc0edd1cb4 100644 --- a/packages/server/src/api/controllers/row/staticFormula.js +++ b/packages/server/src/api/controllers/row/staticFormula.js @@ -38,31 +38,32 @@ exports.updateRelatedFormula = async (appId, table, enrichedRows) => { } } for (let tableId of table.relatedFormula) { + let relatedTable 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 => - exports.finaliseRow(appId, relatedTable, related, { - updateFormula: false, - }) - ) - ) - } - } + 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(appId, relatedTable, related, { + updateFormula: false, + }) + ) + ) + } + } } } await Promise.all(promises) diff --git a/packages/server/src/api/controllers/table/bulkFormula.js b/packages/server/src/api/controllers/table/bulkFormula.js index cb2446eb73..1866d8e650 100644 --- a/packages/server/src/api/controllers/table/bulkFormula.js +++ b/packages/server/src/api/controllers/table/bulkFormula.js @@ -37,7 +37,6 @@ function getFormulaThatUseColumn(table, columnNames) { * This functions checks for when a related table, column or related column is deleted, if any * tables need to have the formula column removed. */ - async function checkIfFormulaNeedsCleared( appId, table, From 4596464b5ab4bcd0e91ebf09f493956caeab7db8 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Mon, 31 Jan 2022 16:01:59 +0000 Subject: [PATCH 15/15] v1.0.49-alpha.1 --- lerna.json | 2 +- packages/backend-core/package.json | 2 +- packages/bbui/package.json | 2 +- packages/builder/package.json | 8 ++++---- packages/cli/package.json | 2 +- packages/client/package.json | 6 +++--- packages/server/package.json | 8 ++++---- packages/string-templates/package.json | 2 +- packages/worker/package.json | 6 +++--- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lerna.json b/lerna.json index 2d503eaf59..6dde2921dd 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.0.49-alpha.0", + "version": "1.0.49-alpha.1", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 18a6281aa0..637ac47a3a 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.0.49-alpha.0", + "version": "1.0.49-alpha.1", "description": "Budibase backend core libraries used in server and worker", "main": "src/index.js", "author": "Budibase", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index d7757d6181..aab6f8f3a3 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "1.0.49-alpha.0", + "version": "1.0.49-alpha.1", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", diff --git a/packages/builder/package.json b/packages/builder/package.json index 3982c0965d..3adb4c23d7 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "1.0.49-alpha.0", + "version": "1.0.49-alpha.1", "license": "GPL-3.0", "private": true, "scripts": { @@ -65,10 +65,10 @@ } }, "dependencies": { - "@budibase/bbui": "^1.0.49-alpha.0", - "@budibase/client": "^1.0.49-alpha.0", + "@budibase/bbui": "^1.0.49-alpha.1", + "@budibase/client": "^1.0.49-alpha.1", "@budibase/colorpicker": "1.1.2", - "@budibase/string-templates": "^1.0.49-alpha.0", + "@budibase/string-templates": "^1.0.49-alpha.1", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index 7e07be16b6..b8091c46ed 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "1.0.49-alpha.0", + "version": "1.0.49-alpha.1", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { diff --git a/packages/client/package.json b/packages/client/package.json index bcb3a870ed..094d5f1d3d 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "1.0.49-alpha.0", + "version": "1.0.49-alpha.1", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "^1.0.49-alpha.0", + "@budibase/bbui": "^1.0.49-alpha.1", "@budibase/standard-components": "^0.9.139", - "@budibase/string-templates": "^1.0.49-alpha.0", + "@budibase/string-templates": "^1.0.49-alpha.1", "regexparam": "^1.3.0", "rollup-plugin-polyfill-node": "^0.8.0", "shortid": "^2.2.15", diff --git a/packages/server/package.json b/packages/server/package.json index c795d3e1ed..deba95643c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "1.0.49-alpha.0", + "version": "1.0.49-alpha.1", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -70,9 +70,9 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "^10.0.3", - "@budibase/backend-core": "^1.0.49-alpha.0", - "@budibase/client": "^1.0.49-alpha.0", - "@budibase/string-templates": "^1.0.49-alpha.0", + "@budibase/backend-core": "^1.0.49-alpha.1", + "@budibase/client": "^1.0.49-alpha.1", + "@budibase/string-templates": "^1.0.49-alpha.1", "@bull-board/api": "^3.7.0", "@bull-board/koa": "^3.7.0", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 9e41666572..de452d9151 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "1.0.49-alpha.0", + "version": "1.0.49-alpha.1", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/worker/package.json b/packages/worker/package.json index a3fc4b8354..521479defd 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "1.0.49-alpha.0", + "version": "1.0.49-alpha.1", "description": "Budibase background service", "main": "src/index.js", "repository": { @@ -29,8 +29,8 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "^1.0.49-alpha.0", - "@budibase/string-templates": "^1.0.49-alpha.0", + "@budibase/backend-core": "^1.0.49-alpha.1", + "@budibase/string-templates": "^1.0.49-alpha.1", "@koa/router": "^8.0.0", "@sentry/node": "^6.0.0", "@techpass/passport-openidconnect": "^0.3.0",