From c356e0d15275a07178de943c8d3b28e9e1a236d4 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 10 Nov 2022 16:38:32 +0000 Subject: [PATCH] Re-building the context module to use a single object, meaning we can create new context frames and copy over whatever exists, then update. --- .../backend-core/src/context/constants.ts | 4 + packages/backend-core/src/context/index.ts | 205 ++++++++++-------- packages/backend-core/src/couch/pouchLike.ts | 5 +- packages/backend-core/src/db/utils.ts | 17 +- .../backend-core/src/migrations/migrations.ts | 2 +- .../src/api/controllers/{dev.js => dev.ts} | 68 +++--- packages/server/src/middleware/currentapp.js | 3 +- 7 files changed, 159 insertions(+), 145 deletions(-) rename packages/server/src/api/controllers/{dev.js => dev.ts} (60%) diff --git a/packages/backend-core/src/context/constants.ts b/packages/backend-core/src/context/constants.ts index af30b1d241..14ab9a531c 100644 --- a/packages/backend-core/src/context/constants.ts +++ b/packages/backend-core/src/context/constants.ts @@ -1,4 +1,8 @@ export enum ContextKey { + MAIN = "main", +} + +export enum ContextElement { TENANT_ID = "tenantId", APP_ID = "appId", IDENTITY = "identity", diff --git a/packages/backend-core/src/context/index.ts b/packages/backend-core/src/context/index.ts index 30a53f1587..f88fe012a9 100644 --- a/packages/backend-core/src/context/index.ts +++ b/packages/backend-core/src/context/index.ts @@ -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 { + 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 { // 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 { 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 { 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) } diff --git a/packages/backend-core/src/couch/pouchLike.ts b/packages/backend-core/src/couch/pouchLike.ts index 73555b1b7c..fd5a94e1af 100644 --- a/packages/backend-core/src/couch/pouchLike.ts +++ b/packages/backend-core/src/couch/pouchLike.ts @@ -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) { diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index c04da5da4f..a59b2ffe05 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -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 } ) diff --git a/packages/backend-core/src/migrations/migrations.ts b/packages/backend-core/src/migrations/migrations.ts index 90a12acec2..2170c5983a 100644 --- a/packages/backend-core/src/migrations/migrations.ts +++ b/packages/backend-core/src/migrations/migrations.ts @@ -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() } diff --git a/packages/server/src/api/controllers/dev.js b/packages/server/src/api/controllers/dev.ts similarity index 60% rename from packages/server/src/api/controllers/dev.js rename to packages/server/src/api/controllers/dev.ts index c8f134756b..9dbbe90555 100644 --- a/packages/server/src/api/controllers/dev.js +++ b/packages/server/src/api/controllers/dev.ts @@ -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, diff --git a/packages/server/src/middleware/currentapp.js b/packages/server/src/middleware/currentapp.js index 8d55175d27..f9c4ae40db 100644 --- a/packages/server/src/middleware/currentapp.js +++ b/packages/server/src/middleware/currentapp.js @@ -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)