Re-building the context module to use a single object, meaning we can create new context frames and copy over whatever exists, then update.
This commit is contained in:
parent
9e01a9d1be
commit
45e7ef61ef
|
@ -1,4 +1,8 @@
|
|||
export enum ContextKey {
|
||||
MAIN = "main",
|
||||
}
|
||||
|
||||
export enum ContextElement {
|
||||
TENANT_ID = "tenantId",
|
||||
APP_ID = "appId",
|
||||
IDENTITY = "identity",
|
||||
|
|
|
@ -4,34 +4,36 @@ import cls from "./FunctionContext"
|
|||
import { baseGlobalDBName } from "../db/tenancy"
|
||||
import { IdentityContext } from "@budibase/types"
|
||||
import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants"
|
||||
import { ContextKey } from "./constants"
|
||||
import { ContextElement, ContextKey } from "./constants"
|
||||
import { PouchLike } from "../couch"
|
||||
import { getDevelopmentAppID, getProdAppID } from "../db/conversions"
|
||||
|
||||
type ContextMap = { [key in ContextElement]?: any }
|
||||
|
||||
export const DEFAULT_TENANT_ID = _DEFAULT_TENANT_ID
|
||||
|
||||
// some test cases call functions directly, need to
|
||||
// store an app ID to pretend there is a context
|
||||
let TEST_APP_ID: string | null = null
|
||||
|
||||
export const isMultiTenant = () => {
|
||||
export function isMultiTenant() {
|
||||
return env.MULTI_TENANCY
|
||||
}
|
||||
|
||||
const setAppTenantId = (appId: string) => {
|
||||
const appTenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
|
||||
updateTenantId(appTenantId)
|
||||
export function isTenantIdSet() {
|
||||
const context = cls.getFromContext(ContextKey.MAIN) as ContextMap
|
||||
return !!context?.[ContextElement.TENANT_ID]
|
||||
}
|
||||
|
||||
const setIdentity = (identity: IdentityContext | null) => {
|
||||
cls.setOnContext(ContextKey.IDENTITY, identity)
|
||||
export function isTenancyEnabled() {
|
||||
return env.MULTI_TENANCY
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an app ID this will attempt to retrieve the tenant ID from it.
|
||||
* @return {null|string} The tenant ID found within the app ID.
|
||||
*/
|
||||
export const getTenantIDFromAppID = (appId: string) => {
|
||||
export function getTenantIDFromAppID(appId: string) {
|
||||
if (!appId) {
|
||||
return null
|
||||
}
|
||||
|
@ -50,84 +52,134 @@ export const getTenantIDFromAppID = (appId: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const doInContext = async (appId: string, task: any) => {
|
||||
// gets the tenant ID from the app ID
|
||||
const tenantId = getTenantIDFromAppID(appId)
|
||||
return doInTenant(tenantId, async () => {
|
||||
return doInAppContext(appId, async () => {
|
||||
return task()
|
||||
})
|
||||
function updateContext(updates: ContextMap) {
|
||||
let context: ContextMap
|
||||
try {
|
||||
context = cls.getFromContext(ContextKey.MAIN)
|
||||
} catch (err) {
|
||||
// no context, start empty
|
||||
context = {}
|
||||
}
|
||||
context = {
|
||||
...context,
|
||||
...updates,
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
async function newContext(updates: ContextMap, task: any) {
|
||||
// see if there already is a context setup
|
||||
let context: ContextMap = updateContext(updates)
|
||||
return cls.run(async () => {
|
||||
cls.setOnContext(ContextKey.MAIN, context)
|
||||
return await task()
|
||||
})
|
||||
}
|
||||
|
||||
export const doInTenant = (tenantId: string | null, task: any): any => {
|
||||
export async function doInContext(appId: string, task: any): Promise<any> {
|
||||
const tenantId = getTenantIDFromAppID(appId)
|
||||
return newContext(
|
||||
{
|
||||
[ContextElement.TENANT_ID]: tenantId,
|
||||
[ContextElement.APP_ID]: appId,
|
||||
},
|
||||
task
|
||||
)
|
||||
}
|
||||
|
||||
export async function doInTenant(
|
||||
tenantId: string | null,
|
||||
task: any
|
||||
): Promise<any> {
|
||||
// make sure default always selected in single tenancy
|
||||
if (!env.MULTI_TENANCY) {
|
||||
tenantId = tenantId || DEFAULT_TENANT_ID
|
||||
}
|
||||
|
||||
return cls.run(async () => {
|
||||
updateTenantId(tenantId)
|
||||
return await task()
|
||||
})
|
||||
return newContext(
|
||||
{
|
||||
[ContextElement.TENANT_ID]: tenantId,
|
||||
},
|
||||
task
|
||||
)
|
||||
}
|
||||
|
||||
export const doInAppContext = (appId: string, task: any): any => {
|
||||
export async function doInAppContext(appId: string, task: any): Promise<any> {
|
||||
if (!appId) {
|
||||
throw new Error("appId is required")
|
||||
}
|
||||
|
||||
const identity = getIdentity()
|
||||
|
||||
return cls.run(async () => {
|
||||
// set the app tenant id
|
||||
setAppTenantId(appId)
|
||||
// set the app ID
|
||||
cls.setOnContext(ContextKey.APP_ID, appId)
|
||||
|
||||
// preserve the identity
|
||||
if (identity) {
|
||||
setIdentity(identity)
|
||||
}
|
||||
// invoke the task
|
||||
return await task()
|
||||
})
|
||||
const tenantId = getTenantIDFromAppID(appId)
|
||||
return newContext(
|
||||
{
|
||||
[ContextElement.TENANT_ID]: tenantId,
|
||||
[ContextElement.APP_ID]: appId,
|
||||
},
|
||||
task
|
||||
)
|
||||
}
|
||||
|
||||
export const doInIdentityContext = (
|
||||
export async function doInIdentityContext(
|
||||
identity: IdentityContext,
|
||||
task: any
|
||||
): any => {
|
||||
): Promise<any> {
|
||||
if (!identity) {
|
||||
throw new Error("identity is required")
|
||||
}
|
||||
|
||||
return cls.run(async () => {
|
||||
cls.setOnContext(ContextKey.IDENTITY, identity)
|
||||
// set the tenant so that doInTenant will preserve identity
|
||||
if (identity.tenantId) {
|
||||
updateTenantId(identity.tenantId)
|
||||
}
|
||||
// invoke the task
|
||||
return await task()
|
||||
})
|
||||
const context: ContextMap = {
|
||||
[ContextElement.IDENTITY]: identity,
|
||||
}
|
||||
if (identity.tenantId) {
|
||||
context[ContextElement.TENANT_ID] = identity.tenantId
|
||||
}
|
||||
return newContext(context, task)
|
||||
}
|
||||
|
||||
export const getIdentity = (): IdentityContext | undefined => {
|
||||
export function getIdentity(): IdentityContext | undefined {
|
||||
try {
|
||||
return cls.getFromContext(ContextKey.IDENTITY)
|
||||
const context = cls.getFromContext(ContextKey.MAIN) as ContextMap
|
||||
return context?.[ContextElement.IDENTITY]
|
||||
} catch (e) {
|
||||
// do nothing - identity is not in context
|
||||
}
|
||||
}
|
||||
|
||||
export const updateTenantId = (tenantId: string | null) => {
|
||||
cls.setOnContext(ContextKey.TENANT_ID, tenantId)
|
||||
export function getTenantId(): string {
|
||||
if (!isMultiTenant()) {
|
||||
return DEFAULT_TENANT_ID
|
||||
}
|
||||
const context = cls.getFromContext(ContextKey.MAIN) as ContextMap
|
||||
const tenantId = context?.[ContextElement.TENANT_ID]
|
||||
if (!tenantId) {
|
||||
throw new Error("Tenant id not found")
|
||||
}
|
||||
return tenantId
|
||||
}
|
||||
|
||||
export const updateAppId = async (appId: string) => {
|
||||
export function getAppId(): string | undefined {
|
||||
const context = cls.getFromContext(ContextKey.MAIN) as ContextMap
|
||||
const foundId = context?.[ContextElement.APP_ID]
|
||||
if (!foundId && env.isTest() && TEST_APP_ID) {
|
||||
return TEST_APP_ID
|
||||
} else {
|
||||
return foundId
|
||||
}
|
||||
}
|
||||
|
||||
export function updateTenantId(tenantId: string | null) {
|
||||
let context: ContextMap = updateContext({
|
||||
[ContextElement.TENANT_ID]: tenantId,
|
||||
})
|
||||
cls.setOnContext(ContextKey.MAIN, context)
|
||||
}
|
||||
|
||||
export function updateAppId(appId: string) {
|
||||
let context: ContextMap = updateContext({
|
||||
[ContextElement.APP_ID]: appId,
|
||||
})
|
||||
try {
|
||||
cls.setOnContext(ContextKey.APP_ID, appId)
|
||||
cls.setOnContext(ContextKey.MAIN, context)
|
||||
} catch (err) {
|
||||
if (env.isTest()) {
|
||||
TEST_APP_ID = appId
|
||||
|
@ -137,63 +189,34 @@ export const updateAppId = async (appId: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const getGlobalDB = (): PouchLike => {
|
||||
const tenantId = cls.getFromContext(ContextKey.TENANT_ID)
|
||||
return new PouchLike(baseGlobalDBName(tenantId))
|
||||
}
|
||||
|
||||
export const isTenantIdSet = () => {
|
||||
const tenantId = cls.getFromContext(ContextKey.TENANT_ID)
|
||||
return !!tenantId
|
||||
}
|
||||
|
||||
export const getTenantId = () => {
|
||||
if (!isMultiTenant()) {
|
||||
return DEFAULT_TENANT_ID
|
||||
}
|
||||
const tenantId = cls.getFromContext(ContextKey.TENANT_ID)
|
||||
if (!tenantId) {
|
||||
throw new Error("Tenant id not found")
|
||||
}
|
||||
return tenantId
|
||||
}
|
||||
|
||||
export const getAppId = () => {
|
||||
const foundId = cls.getFromContext(ContextKey.APP_ID)
|
||||
if (!foundId && env.isTest() && TEST_APP_ID) {
|
||||
return TEST_APP_ID
|
||||
} else {
|
||||
return foundId
|
||||
}
|
||||
}
|
||||
|
||||
export const isTenancyEnabled = () => {
|
||||
return env.MULTI_TENANCY
|
||||
export function getGlobalDB(): PouchLike {
|
||||
const context = cls.getFromContext(ContextKey.MAIN) as ContextMap
|
||||
return new PouchLike(baseGlobalDBName(context?.[ContextElement.TENANT_ID]))
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the app database based on whatever the request
|
||||
* Gets the app database based on whatever the request
|
||||
* contained, dev or prod.
|
||||
*/
|
||||
export const getAppDB = (opts?: any): PouchLike => {
|
||||
export function getAppDB(opts?: any): PouchLike {
|
||||
const appId = getAppId()
|
||||
return new PouchLike(appId, opts)
|
||||
}
|
||||
|
||||
/**
|
||||
* This specifically gets the prod app ID, if the request
|
||||
* contained a development app ID, this will open the prod one.
|
||||
* contained a development app ID, this will get the prod one.
|
||||
*/
|
||||
export const getProdAppDB = (opts?: any): PouchLike => {
|
||||
export function getProdAppDB(opts?: any): PouchLike {
|
||||
const appId = getAppId()
|
||||
return new PouchLike(getProdAppID(appId), opts)
|
||||
}
|
||||
|
||||
/**
|
||||
* This specifically gets the dev app ID, if the request
|
||||
* contained a prod app ID, this will open the dev one.
|
||||
* contained a prod app ID, this will get the dev one.
|
||||
*/
|
||||
export const getDevAppDB = (opts?: any): PouchLike => {
|
||||
export function getDevAppDB(opts?: any): PouchLike {
|
||||
const appId = getAppId()
|
||||
return new PouchLike(getDevelopmentAppID(appId), opts)
|
||||
}
|
||||
|
|
|
@ -28,7 +28,10 @@ export class PouchLike {
|
|||
private static nano: Nano.ServerScope
|
||||
private readonly pouchOpts: PouchLikeOpts
|
||||
|
||||
constructor(dbName: string, opts?: PouchLikeOpts) {
|
||||
constructor(dbName?: string, opts?: PouchLikeOpts) {
|
||||
if (dbName == null) {
|
||||
throw new Error("Database name cannot be undefined.")
|
||||
}
|
||||
this.name = dbName
|
||||
this.pouchOpts = opts || {}
|
||||
if (!PouchLike.nano) {
|
||||
|
|
|
@ -15,6 +15,7 @@ import { getAppMetadata } from "../cache/appMetadata"
|
|||
import { isDevApp, isDevAppID, getProdAppID } from "./conversions"
|
||||
import { APP_PREFIX } from "./constants"
|
||||
import * as events from "../events"
|
||||
import { PouchLike } from "../couch"
|
||||
|
||||
export * from "./constants"
|
||||
export * from "./conversions"
|
||||
|
@ -254,7 +255,7 @@ export function getRoleParams(roleId = null, otherProps = {}) {
|
|||
return getDocParams(DocumentType.ROLE, roleId, otherProps)
|
||||
}
|
||||
|
||||
export function getStartEndKeyURL(baseKey: any, tenantId = null) {
|
||||
export function getStartEndKeyURL(baseKey: any, tenantId?: string) {
|
||||
const tenancy = tenantId ? `${SEPARATOR}${tenantId}` : ""
|
||||
return `startkey="${baseKey}${tenancy}"&endkey="${baseKey}${tenancy}${UNICODE_MAX}"`
|
||||
}
|
||||
|
@ -388,20 +389,10 @@ export async function getDevAppIDs() {
|
|||
}
|
||||
|
||||
export async function dbExists(dbName: any) {
|
||||
let exists = false
|
||||
return doWithDB(
|
||||
dbName,
|
||||
async (db: any) => {
|
||||
try {
|
||||
// check if database exists
|
||||
const info = await db.info()
|
||||
if (info && !info.error) {
|
||||
exists = true
|
||||
}
|
||||
} catch (err) {
|
||||
exists = false
|
||||
}
|
||||
return exists
|
||||
async (db: PouchLike) => {
|
||||
return await db.exists()
|
||||
},
|
||||
{ skip_setup: true }
|
||||
)
|
||||
|
|
|
@ -41,7 +41,7 @@ export const runMigration = async (
|
|||
options: MigrationOptions = {}
|
||||
) => {
|
||||
const migrationType = migration.type
|
||||
let tenantId: string
|
||||
let tenantId: string | undefined
|
||||
if (migrationType !== MigrationType.INSTALLATION) {
|
||||
tenantId = getTenantId()
|
||||
}
|
||||
|
|
|
@ -1,29 +1,23 @@
|
|||
const fetch = require("node-fetch")
|
||||
const env = require("../../environment")
|
||||
const { checkSlashesInUrl } = require("../../utilities")
|
||||
const { request } = require("../../utilities/workerRequests")
|
||||
const { clearLock } = require("../../utilities/redis")
|
||||
const { Replication, getProdAppID } = require("@budibase/backend-core/db")
|
||||
const { DocumentType } = require("../../db/utils")
|
||||
const { app: appCache } = require("@budibase/backend-core/cache")
|
||||
const { getProdAppDB, getAppDB } = require("@budibase/backend-core/context")
|
||||
const { events } = require("@budibase/backend-core")
|
||||
import fetch from "node-fetch"
|
||||
import env from "../../environment"
|
||||
import { checkSlashesInUrl } from "../../utilities"
|
||||
import { request } from "../../utilities/workerRequests"
|
||||
import { clearLock as redisClearLock } from "../../utilities/redis"
|
||||
import { DocumentType } from "../../db/utils"
|
||||
import { context } from "@budibase/backend-core"
|
||||
import { events, db as dbCore, cache } from "@budibase/backend-core"
|
||||
|
||||
async function redirect(ctx, method, path = "global") {
|
||||
async function redirect(ctx: any, method: string, path: string = "global") {
|
||||
const { devPath } = ctx.params
|
||||
const queryString = ctx.originalUrl.split("?")[1] || ""
|
||||
const response = await fetch(
|
||||
checkSlashesInUrl(
|
||||
`${env.WORKER_URL}/api/${path}/${devPath}?${queryString}`
|
||||
),
|
||||
request(
|
||||
ctx,
|
||||
{
|
||||
method,
|
||||
body: ctx.request.body,
|
||||
},
|
||||
true
|
||||
)
|
||||
request(ctx, {
|
||||
method,
|
||||
body: ctx.request.body,
|
||||
})
|
||||
)
|
||||
if (response.status !== 200) {
|
||||
const err = await response.text()
|
||||
|
@ -46,28 +40,28 @@ async function redirect(ctx, method, path = "global") {
|
|||
ctx.cookies
|
||||
}
|
||||
|
||||
exports.buildRedirectGet = path => {
|
||||
return async ctx => {
|
||||
export function buildRedirectGet(path: string) {
|
||||
return async (ctx: any) => {
|
||||
await redirect(ctx, "GET", path)
|
||||
}
|
||||
}
|
||||
|
||||
exports.buildRedirectPost = path => {
|
||||
return async ctx => {
|
||||
export function buildRedirectPost(path: string) {
|
||||
return async (ctx: any) => {
|
||||
await redirect(ctx, "POST", path)
|
||||
}
|
||||
}
|
||||
|
||||
exports.buildRedirectDelete = path => {
|
||||
return async ctx => {
|
||||
export function buildRedirectDelete(path: string) {
|
||||
return async (ctx: any) => {
|
||||
await redirect(ctx, "DELETE", path)
|
||||
}
|
||||
}
|
||||
|
||||
exports.clearLock = async ctx => {
|
||||
export async function clearLock(ctx: any) {
|
||||
const { appId } = ctx.params
|
||||
try {
|
||||
await clearLock(appId, ctx.user)
|
||||
await redisClearLock(appId, ctx.user)
|
||||
} catch (err) {
|
||||
ctx.throw(400, `Unable to remove lock. ${err}`)
|
||||
}
|
||||
|
@ -76,16 +70,16 @@ exports.clearLock = async ctx => {
|
|||
}
|
||||
}
|
||||
|
||||
exports.revert = async ctx => {
|
||||
export async function revert(ctx: any) {
|
||||
const { appId } = ctx.params
|
||||
const productionAppId = getProdAppID(appId)
|
||||
const productionAppId = dbCore.getProdAppID(appId)
|
||||
|
||||
// App must have been deployed first
|
||||
try {
|
||||
const db = getProdAppDB({ skip_setup: true })
|
||||
const info = await db.info()
|
||||
if (info.error) {
|
||||
throw info.error
|
||||
const db = context.getProdAppDB({ skip_setup: true })
|
||||
const exists = await db.exists()
|
||||
if (!exists) {
|
||||
throw new Error("App must be deployed to be reverted.")
|
||||
}
|
||||
const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS)
|
||||
if (
|
||||
|
@ -98,7 +92,7 @@ exports.revert = async ctx => {
|
|||
return ctx.throw(400, "App has not yet been deployed")
|
||||
}
|
||||
|
||||
const replication = new Replication({
|
||||
const replication = new dbCore.Replication({
|
||||
source: productionAppId,
|
||||
target: appId,
|
||||
})
|
||||
|
@ -109,12 +103,12 @@ exports.revert = async ctx => {
|
|||
}
|
||||
|
||||
// update appID in reverted app to be dev version again
|
||||
const db = getAppDB()
|
||||
const db = context.getAppDB()
|
||||
const appDoc = await db.get(DocumentType.APP_METADATA)
|
||||
appDoc.appId = appId
|
||||
appDoc.instance._id = appId
|
||||
await db.put(appDoc)
|
||||
await appCache.invalidateAppMetadata(appId)
|
||||
await cache.app.invalidateAppMetadata(appId)
|
||||
ctx.body = {
|
||||
message: "Reverted changes successfully.",
|
||||
}
|
||||
|
@ -126,7 +120,7 @@ exports.revert = async ctx => {
|
|||
}
|
||||
}
|
||||
|
||||
exports.getBudibaseVersion = async ctx => {
|
||||
export async function getBudibaseVersion(ctx: any) {
|
||||
const version = require("../../../package.json").version
|
||||
ctx.body = {
|
||||
version,
|
|
@ -95,8 +95,7 @@ module.exports = async (ctx, next) => {
|
|||
// need to judge this only based on the request app ID,
|
||||
if (
|
||||
env.MULTI_TENANCY &&
|
||||
ctx.user &&
|
||||
requestAppId &&
|
||||
ctx.user & requestAppId &&
|
||||
!isUserInAppTenant(requestAppId, ctx.user)
|
||||
) {
|
||||
// don't error, simply remove the users rights (they are a public user)
|
||||
|
|
Loading…
Reference in New Issue