1
0
Fork 0
mirror of synced 2024-06-22 04:10:54 +12:00

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:
mike12345567 2022-11-10 16:38:32 +00:00
parent 1ee6761a69
commit c356e0d152
7 changed files with 159 additions and 145 deletions

View file

@ -1,4 +1,8 @@
export enum ContextKey {
MAIN = "main",
}
export enum ContextElement {
TENANT_ID = "tenantId",
APP_ID = "appId",
IDENTITY = "identity",

View file

@ -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)
}

View file

@ -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) {

View file

@ -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 }
)

View file

@ -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()
}

View file

@ -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,

View file

@ -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)