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 = { |
module.exports = { |
||||
...require("./src/db/utils"), |
...require("./src/db/utils"), |
||||
...require("./src/db/constants"), |
...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 { getGlobalUserParams, getAllApps } = require("../db/utils") |
||||
const { getDB, getCouch } = require("../db") |
const { getDB, getCouch } = require("../db") |
||||
const { getGlobalDB } = require("./tenancy") |
const { getGlobalDB } = require("../tenancy") |
||||
const { StaticDatabases } = require("../db/constants") |
const { StaticDatabases } = require("../db/constants") |
||||
|
|
||||
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants |
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 = { |
module.exports = { |
||||
...require("./context"), |
...require("../context"), |
||||
...require("./tenancy"), |
...require("./tenancy"), |
||||
} |
} |
||||
|
|||||
@ -1,74 +1,20 @@ |
|||||
<script> |
<script> |
||||
import "@spectrum-css/fieldlabel/dist/index-vars.css" |
import "@spectrum-css/fieldlabel/dist/index-vars.css" |
||||
import Tooltip from "../Tooltip/Tooltip.svelte" |
import TooltipWrapper from "../Tooltip/TooltipWrapper.svelte" |
||||
import Icon from "../Icon/Icon.svelte" |
|
||||
|
|
||||
export let size = "M" |
export let size = "M" |
||||
export let tooltip = "" |
export let tooltip = "" |
||||
export let showTooltip = false |
|
||||
</script> |
</script> |
||||
|
|
||||
{#if tooltip} |
<TooltipWrapper {tooltip} {size}> |
||||
<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} |
|
||||
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}> |
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}> |
||||
<slot /> |
<slot /> |
||||
</label> |
</label> |
||||
{/if} |
</TooltipWrapper> |
||||
|
|
||||
<style> |
<style> |
||||
label { |
label { |
||||
padding: 0; |
padding: 0; |
||||
white-space: nowrap; |
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> |
</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