mirror of https://github.com/Budibase/budibase.git
175 changed files with 4383 additions and 5333 deletions
@ -0,0 +1,17 @@ |
|||
const { |
|||
getAppDB, |
|||
getDevAppDB, |
|||
getProdAppDB, |
|||
getAppId, |
|||
updateAppId, |
|||
doInAppContext, |
|||
} = require("./src/context") |
|||
|
|||
module.exports = { |
|||
getAppDB, |
|||
getDevAppDB, |
|||
getProdAppDB, |
|||
getAppId, |
|||
updateAppId, |
|||
doInAppContext, |
|||
} |
|||
@ -1,4 +1,6 @@ |
|||
module.exports = { |
|||
...require("./src/db/utils"), |
|||
...require("./src/db/constants"), |
|||
...require("./src/db"), |
|||
...require("./src/db/views"), |
|||
} |
|||
|
|||
@ -1 +1 @@ |
|||
module.exports = require("./src/tenancy/deprovision") |
|||
module.exports = require("./src/context/deprovision") |
|||
|
|||
@ -1,6 +1,6 @@ |
|||
const { getGlobalUserParams, getAllApps } = require("../db/utils") |
|||
const { getDB, getCouch } = require("../db") |
|||
const { getGlobalDB } = require("./tenancy") |
|||
const { getGlobalDB } = require("../tenancy") |
|||
const { StaticDatabases } = require("../db/constants") |
|||
|
|||
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants |
|||
@ -0,0 +1,195 @@ |
|||
const env = require("../environment") |
|||
const { Headers } = require("../../constants") |
|||
const cls = require("./FunctionContext") |
|||
const { getCouch } = require("../db") |
|||
const { getProdAppID, getDevelopmentAppID } = require("../db/conversions") |
|||
const { isEqual } = require("lodash") |
|||
|
|||
// some test cases call functions directly, need to
|
|||
// store an app ID to pretend there is a context
|
|||
let TEST_APP_ID = null |
|||
|
|||
const ContextKeys = { |
|||
TENANT_ID: "tenantId", |
|||
APP_ID: "appId", |
|||
// whatever the request app DB was
|
|||
CURRENT_DB: "currentDb", |
|||
// get the prod app DB from the request
|
|||
PROD_DB: "prodDb", |
|||
// get the dev app DB from the request
|
|||
DEV_DB: "devDb", |
|||
DB_OPTS: "dbOpts", |
|||
} |
|||
|
|||
exports.DEFAULT_TENANT_ID = "default" |
|||
|
|||
exports.isDefaultTenant = () => { |
|||
return exports.getTenantId() === exports.DEFAULT_TENANT_ID |
|||
} |
|||
|
|||
exports.isMultiTenant = () => { |
|||
return env.MULTI_TENANCY |
|||
} |
|||
|
|||
// used for automations, API endpoints should always be in context already
|
|||
exports.doInTenant = (tenantId, task) => { |
|||
return cls.run(() => { |
|||
// set the tenant id
|
|||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId) |
|||
|
|||
// invoke the task
|
|||
return task() |
|||
}) |
|||
} |
|||
|
|||
exports.doInAppContext = (appId, task) => { |
|||
return cls.run(() => { |
|||
// set the app ID
|
|||
cls.setOnContext(ContextKeys.APP_ID, appId) |
|||
|
|||
// invoke the task
|
|||
return task() |
|||
}) |
|||
} |
|||
|
|||
exports.updateTenantId = tenantId => { |
|||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId) |
|||
} |
|||
|
|||
exports.updateAppId = appId => { |
|||
try { |
|||
cls.setOnContext(ContextKeys.APP_ID, appId) |
|||
cls.setOnContext(ContextKeys.PROD_DB, null) |
|||
cls.setOnContext(ContextKeys.DEV_DB, null) |
|||
cls.setOnContext(ContextKeys.CURRENT_DB, null) |
|||
cls.setOnContext(ContextKeys.DB_OPTS, null) |
|||
} catch (err) { |
|||
if (env.isTest()) { |
|||
TEST_APP_ID = appId |
|||
} else { |
|||
throw err |
|||
} |
|||
} |
|||
} |
|||
|
|||
exports.setTenantId = ( |
|||
ctx, |
|||
opts = { allowQs: false, allowNoTenant: false } |
|||
) => { |
|||
let tenantId |
|||
// exit early if not multi-tenant
|
|||
if (!exports.isMultiTenant()) { |
|||
cls.setOnContext(ContextKeys.TENANT_ID, this.DEFAULT_TENANT_ID) |
|||
return |
|||
} |
|||
|
|||
const allowQs = opts && opts.allowQs |
|||
const allowNoTenant = opts && opts.allowNoTenant |
|||
const header = ctx.request.headers[Headers.TENANT_ID] |
|||
const user = ctx.user || {} |
|||
if (allowQs) { |
|||
const query = ctx.request.query || {} |
|||
tenantId = query.tenantId |
|||
} |
|||
// override query string (if allowed) by user, or header
|
|||
// URL params cannot be used in a middleware, as they are
|
|||
// processed later in the chain
|
|||
tenantId = user.tenantId || header || tenantId |
|||
|
|||
// Set the tenantId from the subdomain
|
|||
if (!tenantId) { |
|||
tenantId = ctx.subdomains && ctx.subdomains[0] |
|||
} |
|||
|
|||
if (!tenantId && !allowNoTenant) { |
|||
ctx.throw(403, "Tenant id not set") |
|||
} |
|||
// check tenant ID just incase no tenant was allowed
|
|||
if (tenantId) { |
|||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId) |
|||
} |
|||
} |
|||
|
|||
exports.isTenantIdSet = () => { |
|||
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID) |
|||
return !!tenantId |
|||
} |
|||
|
|||
exports.getTenantId = () => { |
|||
if (!exports.isMultiTenant()) { |
|||
return exports.DEFAULT_TENANT_ID |
|||
} |
|||
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID) |
|||
if (!tenantId) { |
|||
throw Error("Tenant id not found") |
|||
} |
|||
return tenantId |
|||
} |
|||
|
|||
exports.getAppId = () => { |
|||
const foundId = cls.getFromContext(ContextKeys.APP_ID) |
|||
if (!foundId && env.isTest() && TEST_APP_ID) { |
|||
return TEST_APP_ID |
|||
} else { |
|||
return foundId |
|||
} |
|||
} |
|||
|
|||
function getDB(key, opts) { |
|||
const dbOptsKey = `${key}${ContextKeys.DB_OPTS}` |
|||
let storedOpts = cls.getFromContext(dbOptsKey) |
|||
let db = cls.getFromContext(key) |
|||
if (db && isEqual(opts, storedOpts)) { |
|||
return db |
|||
} |
|||
const appId = exports.getAppId() |
|||
const CouchDB = getCouch() |
|||
let toUseAppId |
|||
switch (key) { |
|||
case ContextKeys.CURRENT_DB: |
|||
toUseAppId = appId |
|||
break |
|||
case ContextKeys.PROD_DB: |
|||
toUseAppId = getProdAppID(appId) |
|||
break |
|||
case ContextKeys.DEV_DB: |
|||
toUseAppId = getDevelopmentAppID(appId) |
|||
break |
|||
} |
|||
db = new CouchDB(toUseAppId, opts) |
|||
try { |
|||
cls.setOnContext(key, db) |
|||
if (opts) { |
|||
cls.setOnContext(dbOptsKey, opts) |
|||
} |
|||
} catch (err) { |
|||
if (!env.isTest()) { |
|||
throw err |
|||
} |
|||
} |
|||
return db |
|||
} |
|||
|
|||
/** |
|||
* Opens the app database based on whatever the request |
|||
* contained, dev or prod. |
|||
*/ |
|||
exports.getAppDB = opts => { |
|||
return getDB(ContextKeys.CURRENT_DB, opts) |
|||
} |
|||
|
|||
/** |
|||
* This specifically gets the prod app ID, if the request |
|||
* contained a development app ID, this will open the prod one. |
|||
*/ |
|||
exports.getProdAppDB = opts => { |
|||
return getDB(ContextKeys.PROD_DB, opts) |
|||
} |
|||
|
|||
/** |
|||
* This specifically gets the dev app ID, if the request |
|||
* contained a prod app ID, this will open the dev one. |
|||
*/ |
|||
exports.getDevAppDB = opts => { |
|||
return getDB(ContextKeys.DEV_DB, opts) |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
const NO_APP_ERROR = "No app provided" |
|||
const { APP_DEV_PREFIX, APP_PREFIX } = require("./constants") |
|||
|
|||
exports.isDevAppID = appId => { |
|||
if (!appId) { |
|||
throw NO_APP_ERROR |
|||
} |
|||
return appId.startsWith(APP_DEV_PREFIX) |
|||
} |
|||
|
|||
exports.isProdAppID = appId => { |
|||
if (!appId) { |
|||
throw NO_APP_ERROR |
|||
} |
|||
return appId.startsWith(APP_PREFIX) && !exports.isDevAppID(appId) |
|||
} |
|||
|
|||
exports.isDevApp = app => { |
|||
if (!app) { |
|||
throw NO_APP_ERROR |
|||
} |
|||
return exports.isDevAppID(app.appId) |
|||
} |
|||
|
|||
/** |
|||
* Convert a development app ID to a deployed app ID. |
|||
*/ |
|||
exports.getProdAppID = appId => { |
|||
// if dev, convert it
|
|||
if (appId.startsWith(APP_DEV_PREFIX)) { |
|||
const id = appId.split(APP_DEV_PREFIX)[1] |
|||
return `${APP_PREFIX}${id}` |
|||
} |
|||
return appId |
|||
} |
|||
|
|||
/** |
|||
* Convert a deployed app ID to a development app ID. |
|||
*/ |
|||
exports.getDevelopmentAppID = appId => { |
|||
if (!appId.startsWith(APP_DEV_PREFIX)) { |
|||
const id = appId.split(APP_PREFIX)[1] |
|||
return `${APP_DEV_PREFIX}${id}` |
|||
} |
|||
return appId |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
const env = require("../environment") |
|||
const { Headers } = require("../constants") |
|||
|
|||
/** |
|||
* API Key only endpoint. |
|||
*/ |
|||
module.exports = async (ctx, next) => { |
|||
const apiKey = ctx.request.headers[Headers.API_KEY] |
|||
if (apiKey !== env.INTERNAL_API_KEY) { |
|||
ctx.throw(403, "Unauthorized") |
|||
} |
|||
|
|||
return next() |
|||
} |
|||
@ -1,84 +0,0 @@ |
|||
const env = require("../environment") |
|||
const { Headers } = require("../../constants") |
|||
const cls = require("./FunctionContext") |
|||
|
|||
exports.DEFAULT_TENANT_ID = "default" |
|||
|
|||
exports.isDefaultTenant = () => { |
|||
return exports.getTenantId() === exports.DEFAULT_TENANT_ID |
|||
} |
|||
|
|||
exports.isMultiTenant = () => { |
|||
return env.MULTI_TENANCY |
|||
} |
|||
|
|||
const TENANT_ID = "tenantId" |
|||
|
|||
// used for automations, API endpoints should always be in context already
|
|||
exports.doInTenant = (tenantId, task) => { |
|||
return cls.run(() => { |
|||
// set the tenant id
|
|||
cls.setOnContext(TENANT_ID, tenantId) |
|||
|
|||
// invoke the task
|
|||
return task() |
|||
}) |
|||
} |
|||
|
|||
exports.updateTenantId = tenantId => { |
|||
cls.setOnContext(TENANT_ID, tenantId) |
|||
} |
|||
|
|||
exports.setTenantId = ( |
|||
ctx, |
|||
opts = { allowQs: false, allowNoTenant: false } |
|||
) => { |
|||
let tenantId |
|||
// exit early if not multi-tenant
|
|||
if (!exports.isMultiTenant()) { |
|||
cls.setOnContext(TENANT_ID, this.DEFAULT_TENANT_ID) |
|||
return |
|||
} |
|||
|
|||
const allowQs = opts && opts.allowQs |
|||
const allowNoTenant = opts && opts.allowNoTenant |
|||
const header = ctx.request.headers[Headers.TENANT_ID] |
|||
const user = ctx.user || {} |
|||
if (allowQs) { |
|||
const query = ctx.request.query || {} |
|||
tenantId = query.tenantId |
|||
} |
|||
// override query string (if allowed) by user, or header
|
|||
// URL params cannot be used in a middleware, as they are
|
|||
// processed later in the chain
|
|||
tenantId = user.tenantId || header || tenantId |
|||
|
|||
// Set the tenantId from the subdomain
|
|||
if (!tenantId) { |
|||
tenantId = ctx.subdomains && ctx.subdomains[0] |
|||
} |
|||
|
|||
if (!tenantId && !allowNoTenant) { |
|||
ctx.throw(403, "Tenant id not set") |
|||
} |
|||
// check tenant ID just incase no tenant was allowed
|
|||
if (tenantId) { |
|||
cls.setOnContext(TENANT_ID, tenantId) |
|||
} |
|||
} |
|||
|
|||
exports.isTenantIdSet = () => { |
|||
const tenantId = cls.getFromContext(TENANT_ID) |
|||
return !!tenantId |
|||
} |
|||
|
|||
exports.getTenantId = () => { |
|||
if (!exports.isMultiTenant()) { |
|||
return exports.DEFAULT_TENANT_ID |
|||
} |
|||
const tenantId = cls.getFromContext(TENANT_ID) |
|||
if (!tenantId) { |
|||
throw Error("Tenant id not found") |
|||
} |
|||
return tenantId |
|||
} |
|||
@ -1,4 +1,4 @@ |
|||
module.exports = { |
|||
...require("./context"), |
|||
...require("../context"), |
|||
...require("./tenancy"), |
|||
} |
|||
|
|||
@ -1,74 +1,20 @@ |
|||
<script> |
|||
import "@spectrum-css/fieldlabel/dist/index-vars.css" |
|||
import Tooltip from "../Tooltip/Tooltip.svelte" |
|||
import Icon from "../Icon/Icon.svelte" |
|||
import TooltipWrapper from "../Tooltip/TooltipWrapper.svelte" |
|||
|
|||
export let size = "M" |
|||
export let tooltip = "" |
|||
export let showTooltip = false |
|||
</script> |
|||
|
|||
{#if tooltip} |
|||
<div class="container"> |
|||
<label |
|||
for="" |
|||
class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`} |
|||
> |
|||
<slot /> |
|||
</label> |
|||
<div class="icon-container"> |
|||
<div |
|||
class="icon" |
|||
class:icon-small={size === "M" || size === "S"} |
|||
on:mouseover={() => (showTooltip = true)} |
|||
on:focus={() => (showTooltip = true)} |
|||
on:mouseleave={() => (showTooltip = false)} |
|||
> |
|||
<Icon name="InfoOutline" size="S" disabled={true} /> |
|||
</div> |
|||
{#if showTooltip} |
|||
<div class="tooltip"> |
|||
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} /> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
{:else} |
|||
<TooltipWrapper {tooltip} {size}> |
|||
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}> |
|||
<slot /> |
|||
</label> |
|||
{/if} |
|||
</TooltipWrapper> |
|||
|
|||
<style> |
|||
label { |
|||
padding: 0; |
|||
white-space: nowrap; |
|||
} |
|||
.container { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
.icon-container { |
|||
position: relative; |
|||
display: flex; |
|||
justify-content: center; |
|||
margin-top: 1px; |
|||
margin-left: 5px; |
|||
margin-right: 5px; |
|||
} |
|||
.tooltip { |
|||
position: absolute; |
|||
display: flex; |
|||
justify-content: center; |
|||
top: 15px; |
|||
z-index: 1; |
|||
width: 160px; |
|||
} |
|||
.icon { |
|||
transform: scale(0.75); |
|||
} |
|||
.icon-small { |
|||
margin-top: -2px; |
|||
margin-bottom: -5px; |
|||
} |
|||
</style> |
|||
|
|||
@ -0,0 +1,60 @@ |
|||
<script> |
|||
import Tooltip from "./Tooltip.svelte" |
|||
import Icon from "../Icon/Icon.svelte" |
|||
|
|||
export let tooltip = "" |
|||
export let size = "M" |
|||
|
|||
let showTooltip = false |
|||
</script> |
|||
|
|||
<div class:container={!!tooltip}> |
|||
<slot /> |
|||
{#if tooltip} |
|||
<div class="icon-container"> |
|||
<div |
|||
class="icon" |
|||
class:icon-small={size === "M" || size === "S"} |
|||
on:mouseover={() => (showTooltip = true)} |
|||
on:mouseleave={() => (showTooltip = false)} |
|||
> |
|||
<Icon name="InfoOutline" size="S" disabled={true} /> |
|||
</div> |
|||
{#if showTooltip} |
|||
<div class="tooltip"> |
|||
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} /> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
.icon-container { |
|||
position: relative; |
|||
display: flex; |
|||
justify-content: center; |
|||
margin-top: 1px; |
|||
margin-left: 5px; |
|||
margin-right: 5px; |
|||
} |
|||
.tooltip { |
|||
position: absolute; |
|||
display: flex; |
|||
justify-content: center; |
|||
top: 15px; |
|||
z-index: 1; |
|||
width: 160px; |
|||
} |
|||
.icon { |
|||
transform: scale(0.75); |
|||
} |
|||
.icon-small { |
|||
margin-top: -2px; |
|||
margin-bottom: -5px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,4 @@ |
|||
// @ts-ignore
|
|||
import { run } from "../setup" |
|||
|
|||
run("../../server/src/index", "../../worker/src/index") |
|||
@ -0,0 +1,23 @@ |
|||
{ |
|||
"compilerOptions": { |
|||
"target": "es6", |
|||
"module": "commonjs", |
|||
"lib": ["es2019"], |
|||
"allowJs": true, |
|||
"outDir": "dist", |
|||
"strict": true, |
|||
"noImplicitAny": true, |
|||
"esModuleInterop": true, |
|||
"resolveJsonModule": true, |
|||
"incremental": true |
|||
}, |
|||
"include": [ |
|||
"./src/**/*" |
|||
], |
|||
"exclude": [ |
|||
"node_modules", |
|||
"**/*.json", |
|||
"**/*.spec.ts", |
|||
"**/*.spec.js" |
|||
] |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,13 @@ |
|||
const { migrate, MIGRATIONS } = require("../../migrations") |
|||
|
|||
exports.migrate = async ctx => { |
|||
const options = ctx.request.body |
|||
// don't await as can take a while, just return
|
|||
migrate(options) |
|||
ctx.status = 200 |
|||
} |
|||
|
|||
exports.fetchDefinitions = async ctx => { |
|||
ctx.body = MIGRATIONS |
|||
ctx.status = 200 |
|||
} |
|||
@ -0,0 +1,156 @@ |
|||
const { getRowParams } = require("../../../db/utils") |
|||
const { |
|||
outputProcessing, |
|||
processAutoColumn, |
|||
processFormulas, |
|||
} = require("../../../utilities/rowProcessor") |
|||
const { FieldTypes, FormulaTypes } = require("../../../constants") |
|||
const { isEqual } = require("lodash") |
|||
const { cloneDeep } = require("lodash/fp") |
|||
const { getAppDB } = require("@budibase/backend-core/context") |
|||
|
|||
/** |
|||
* This function runs through a list of enriched rows, looks at the rows which |
|||
* are related and then checks if they need the state of their formulas |
|||
* updated. |
|||
* NOTE: this will only for affect static formulas. |
|||
*/ |
|||
exports.updateRelatedFormula = async (table, enrichedRows) => { |
|||
const db = getAppDB() |
|||
// no formula to update, we're done
|
|||
if (!table.relatedFormula) { |
|||
return |
|||
} |
|||
let promises = [] |
|||
for (let enrichedRow of Array.isArray(enrichedRows) |
|||
? enrichedRows |
|||
: [enrichedRows]) { |
|||
// the related rows by tableId
|
|||
let relatedRows = {} |
|||
for (let [key, field] of Object.entries(enrichedRow)) { |
|||
const columnDefinition = table.schema[key] |
|||
if (columnDefinition && columnDefinition.type === FieldTypes.LINK) { |
|||
const relatedTableId = columnDefinition.tableId |
|||
if (!relatedRows[relatedTableId]) { |
|||
relatedRows[relatedTableId] = [] |
|||
} |
|||
relatedRows[relatedTableId] = relatedRows[relatedTableId].concat(field) |
|||
} |
|||
} |
|||
for (let tableId of table.relatedFormula) { |
|||
let relatedTable |
|||
try { |
|||
// no rows to update, skip
|
|||
if (!relatedRows[tableId] || relatedRows[tableId].length === 0) { |
|||
continue |
|||
} |
|||
relatedTable = await db.get(tableId) |
|||
} catch (err) { |
|||
// no error scenario, table doesn't seem to exist anymore, ignore
|
|||
} |
|||
for (let column of Object.values(relatedTable.schema)) { |
|||
// needs updated in related rows
|
|||
if ( |
|||
column.type === FieldTypes.FORMULA && |
|||
column.formulaType === FormulaTypes.STATIC |
|||
) { |
|||
// re-enrich rows for all the related, don't update the related formula for them
|
|||
promises = promises.concat( |
|||
relatedRows[tableId].map(related => |
|||
exports.finaliseRow(relatedTable, related, { |
|||
updateFormula: false, |
|||
}) |
|||
) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
await Promise.all(promises) |
|||
} |
|||
|
|||
exports.updateAllFormulasInTable = async table => { |
|||
const db = getAppDB() |
|||
// start by getting the raw rows (which will be written back to DB after update)
|
|||
let rows = ( |
|||
await db.allDocs( |
|||
getRowParams(table._id, null, { |
|||
include_docs: true, |
|||
}) |
|||
) |
|||
).rows.map(row => row.doc) |
|||
// now enrich the rows, note the clone so that we have the base state of the
|
|||
// rows so that we don't write any of the enriched information back
|
|||
let enrichedRows = await outputProcessing(table, cloneDeep(rows), { |
|||
squash: false, |
|||
}) |
|||
const updatedRows = [] |
|||
for (let row of rows) { |
|||
// find the enriched row, if found process the formulas
|
|||
const enrichedRow = enrichedRows.find(enriched => enriched._id === row._id) |
|||
if (enrichedRow) { |
|||
const processed = processFormulas(table, cloneDeep(row), { |
|||
dynamic: false, |
|||
contextRows: enrichedRow, |
|||
}) |
|||
// values have changed, need to add to bulk docs to update
|
|||
if (!isEqual(processed, row)) { |
|||
updatedRows.push(processed) |
|||
} |
|||
} |
|||
} |
|||
await db.bulkDocs(updatedRows) |
|||
} |
|||
|
|||
/** |
|||
* This function runs at the end of the save/patch functions of the row controller, all this |
|||
* really does is enrich the row, handle any static formula processing, then return the enriched |
|||
* row. The reason we need to return the enriched row is that the automation row created trigger |
|||
* expects the row to be totally enriched/contain all relationships. |
|||
*/ |
|||
exports.finaliseRow = async ( |
|||
table, |
|||
row, |
|||
{ oldTable, updateFormula } = { updateFormula: true } |
|||
) => { |
|||
const db = getAppDB() |
|||
row.type = "row" |
|||
// process the row before return, to include relationships
|
|||
let enrichedRow = await outputProcessing(table, cloneDeep(row), { |
|||
squash: false, |
|||
}) |
|||
// use enriched row to generate formulas for saving, specifically only use as context
|
|||
row = processFormulas(table, row, { |
|||
dynamic: false, |
|||
contextRows: enrichedRow, |
|||
}) |
|||
|
|||
// don't worry about rev, tables handle rev/lastID updates
|
|||
// if another row has been written since processing this will
|
|||
// handle the auto ID clash
|
|||
if (oldTable && !isEqual(oldTable, table)) { |
|||
try { |
|||
await db.put(table) |
|||
} catch (err) { |
|||
if (err.status === 409) { |
|||
const updatedTable = await db.get(table._id) |
|||
let response = processAutoColumn(null, updatedTable, row, { |
|||
reprocessing: true, |
|||
}) |
|||
await db.put(response.table) |
|||
row = response.row |
|||
} else { |
|||
throw err |
|||
} |
|||
} |
|||
} |
|||
const response = await db.put(row) |
|||
// for response, calculate the formulas for the enriched row
|
|||
enrichedRow._rev = response.rev |
|||
enrichedRow = await processFormulas(table, enrichedRow, { dynamic: false }) |
|||
// this updates the related formulas in other rows based on the relations to this row
|
|||
if (updateFormula) { |
|||
await exports.updateRelatedFormula(table, enrichedRow) |
|||
} |
|||
return { row: enrichedRow, table } |
|||
} |
|||
@ -0,0 +1,173 @@ |
|||
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") |
|||
const { getAppDB } = require("@budibase/backend-core/context") |
|||
|
|||
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 |
|||
* in the formula. |
|||
*/ |
|||
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 (!isStaticFormula(column)) { |
|||
continue |
|||
} |
|||
if (!doesContainStrings(column.formula, columnNames)) { |
|||
continue |
|||
} |
|||
formula.push(column.name) |
|||
} |
|||
return formula |
|||
} |
|||
|
|||
/** |
|||
* 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(table, { oldTable, deletion }) { |
|||
// start by retrieving all tables, remove the current table from the list
|
|||
const tables = (await getAllInternalTables()).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 clearColumns(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) { |
|||
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(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. |
|||
*/ |
|||
async function updateRelatedFormulaLinksOnTables( |
|||
table, |
|||
{ deletion } = { deletion: false } |
|||
) { |
|||
const db = getAppDB() |
|||
// start by retrieving all tables, remove the current table from the list
|
|||
const tables = (await getAllInternalTables()).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) |
|||
} |
|||
} |
|||
} |
|||
|
|||
async function checkIfFormulaUpdated(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(table) |
|||
} |
|||
} |
|||
|
|||
exports.runStaticFormulaChecks = async (table, { oldTable, deletion }) => { |
|||
await updateRelatedFormulaLinksOnTables(table, { deletion }) |
|||
await checkIfFormulaNeedsCleared(table, { oldTable, deletion }) |
|||
if (!deletion) { |
|||
await checkIfFormulaUpdated(table, { oldTable }) |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue