forked from tsai/budibase
committed by
GitHub
19 changed files with 3213 additions and 2789 deletions
@ -0,0 +1,17 @@ |
|||
export enum ContextKeys { |
|||
TENANT_ID = "tenantId", |
|||
GLOBAL_DB = "globalDb", |
|||
APP_ID = "appId", |
|||
IDENTITY = "identity", |
|||
// 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", |
|||
// check if something else is using the context, don't close DB
|
|||
TENANCY_IN_USE = "tenancyInUse", |
|||
APP_IN_USE = "appInUse", |
|||
IDENTITY_IN_USE = "identityInUse", |
|||
} |
|||
@ -1,354 +0,0 @@ |
|||
const env = require("../environment") |
|||
const { SEPARATOR, DocumentTypes } = require("../db/constants") |
|||
const { DEFAULT_TENANT_ID } = require("../constants") |
|||
const cls = require("./FunctionContext") |
|||
const { dangerousGetDB, closeDB } = require("../db") |
|||
const { getProdAppID, getDevelopmentAppID } = require("../db/conversions") |
|||
const { baseGlobalDBName } = require("../tenancy/utils") |
|||
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", |
|||
GLOBAL_DB: "globalDb", |
|||
APP_ID: "appId", |
|||
IDENTITY: "identity", |
|||
// 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", |
|||
// check if something else is using the context, don't close DB
|
|||
IN_USE: "inUse", |
|||
} |
|||
|
|||
exports.DEFAULT_TENANT_ID = DEFAULT_TENANT_ID |
|||
|
|||
// this function makes sure the PouchDB objects are closed and
|
|||
// fully deleted when finished - this protects against memory leaks
|
|||
async function closeAppDBs() { |
|||
const dbKeys = [ |
|||
ContextKeys.CURRENT_DB, |
|||
ContextKeys.PROD_DB, |
|||
ContextKeys.DEV_DB, |
|||
] |
|||
for (let dbKey of dbKeys) { |
|||
const db = cls.getFromContext(dbKey) |
|||
if (!db) { |
|||
continue |
|||
} |
|||
await closeDB(db) |
|||
// clear the DB from context, incase someone tries to use it again
|
|||
cls.setOnContext(dbKey, null) |
|||
} |
|||
// clear the app ID now that the databases are closed
|
|||
if (cls.getFromContext(ContextKeys.APP_ID)) { |
|||
cls.setOnContext(ContextKeys.APP_ID, null) |
|||
} |
|||
if (cls.getFromContext(ContextKeys.DB_OPTS)) { |
|||
cls.setOnContext(ContextKeys.DB_OPTS, null) |
|||
} |
|||
} |
|||
|
|||
exports.closeTenancy = async () => { |
|||
if (env.USE_COUCH) { |
|||
await closeDB(exports.getGlobalDB()) |
|||
} |
|||
// clear from context now that database is closed/task is finished
|
|||
cls.setOnContext(ContextKeys.TENANT_ID, null) |
|||
cls.setOnContext(ContextKeys.GLOBAL_DB, null) |
|||
} |
|||
|
|||
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, { forceNew } = {}) => { |
|||
// the internal function is so that we can re-use an existing
|
|||
// context - don't want to close DB on a parent context
|
|||
async function internal(opts = { existing: false }) { |
|||
// set the tenant id
|
|||
if (!opts.existing) { |
|||
exports.updateTenantId(tenantId) |
|||
} |
|||
|
|||
try { |
|||
// invoke the task
|
|||
return await task() |
|||
} finally { |
|||
const using = cls.getFromContext(ContextKeys.IN_USE) |
|||
if (!using || using <= 1) { |
|||
await exports.closeTenancy() |
|||
} else { |
|||
cls.setOnContext(using - 1) |
|||
} |
|||
} |
|||
} |
|||
|
|||
const using = cls.getFromContext(ContextKeys.IN_USE) |
|||
if ( |
|||
!forceNew && |
|||
using && |
|||
cls.getFromContext(ContextKeys.TENANT_ID) === tenantId |
|||
) { |
|||
cls.setOnContext(ContextKeys.IN_USE, using + 1) |
|||
return internal({ existing: true }) |
|||
} else { |
|||
return cls.run(async () => { |
|||
cls.setOnContext(ContextKeys.IN_USE, 1) |
|||
return internal() |
|||
}) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 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. |
|||
*/ |
|||
exports.getTenantIDFromAppID = appId => { |
|||
if (!appId) { |
|||
return null |
|||
} |
|||
const split = appId.split(SEPARATOR) |
|||
const hasDev = split[1] === DocumentTypes.DEV |
|||
if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) { |
|||
return null |
|||
} |
|||
if (hasDev) { |
|||
return split[2] |
|||
} else { |
|||
return split[1] |
|||
} |
|||
} |
|||
|
|||
const setAppTenantId = appId => { |
|||
const appTenantId = |
|||
exports.getTenantIDFromAppID(appId) || exports.DEFAULT_TENANT_ID |
|||
exports.updateTenantId(appTenantId) |
|||
} |
|||
|
|||
exports.doInAppContext = (appId, task, { forceNew } = {}) => { |
|||
if (!appId) { |
|||
throw new Error("appId is required") |
|||
} |
|||
|
|||
const identity = exports.getIdentity() |
|||
|
|||
// the internal function is so that we can re-use an existing
|
|||
// context - don't want to close DB on a parent context
|
|||
async function internal(opts = { existing: false }) { |
|||
// set the app tenant id
|
|||
if (!opts.existing) { |
|||
setAppTenantId(appId) |
|||
} |
|||
// set the app ID
|
|||
cls.setOnContext(ContextKeys.APP_ID, appId) |
|||
// preserve the identity
|
|||
exports.setIdentity(identity) |
|||
try { |
|||
// invoke the task
|
|||
return await task() |
|||
} finally { |
|||
const using = cls.getFromContext(ContextKeys.IN_USE) |
|||
if (!using || using <= 1) { |
|||
await closeAppDBs() |
|||
} else { |
|||
cls.setOnContext(using - 1) |
|||
} |
|||
} |
|||
} |
|||
const using = cls.getFromContext(ContextKeys.IN_USE) |
|||
if (!forceNew && using && cls.getFromContext(ContextKeys.APP_ID) === appId) { |
|||
cls.setOnContext(ContextKeys.IN_USE, using + 1) |
|||
return internal({ existing: true }) |
|||
} else { |
|||
return cls.run(async () => { |
|||
cls.setOnContext(ContextKeys.IN_USE, 1) |
|||
return internal() |
|||
}) |
|||
} |
|||
} |
|||
|
|||
exports.doInIdentityContext = (identity, task) => { |
|||
if (!identity) { |
|||
throw new Error("identity is required") |
|||
} |
|||
|
|||
async function internal(opts = { existing: false }) { |
|||
if (!opts.existing) { |
|||
cls.setOnContext(ContextKeys.IDENTITY, identity) |
|||
// set the tenant so that doInTenant will preserve identity
|
|||
if (identity.tenantId) { |
|||
exports.updateTenantId(identity.tenantId) |
|||
} |
|||
} |
|||
|
|||
try { |
|||
// invoke the task
|
|||
return await task() |
|||
} finally { |
|||
const using = cls.getFromContext(ContextKeys.IN_USE) |
|||
if (!using || using <= 1) { |
|||
exports.setIdentity(null) |
|||
} else { |
|||
cls.setOnContext(using - 1) |
|||
} |
|||
} |
|||
} |
|||
|
|||
const existing = cls.getFromContext(ContextKeys.IDENTITY) |
|||
const using = cls.getFromContext(ContextKeys.IN_USE) |
|||
if (using && existing && existing._id === identity._id) { |
|||
cls.setOnContext(ContextKeys.IN_USE, using + 1) |
|||
return internal({ existing: true }) |
|||
} else { |
|||
return cls.run(async () => { |
|||
cls.setOnContext(ContextKeys.IN_USE, 1) |
|||
return internal({ existing: false }) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
exports.setIdentity = identity => { |
|||
cls.setOnContext(ContextKeys.IDENTITY, identity) |
|||
} |
|||
|
|||
exports.getIdentity = () => { |
|||
try { |
|||
return cls.getFromContext(ContextKeys.IDENTITY) |
|||
} catch (e) { |
|||
// do nothing - identity is not in context
|
|||
} |
|||
} |
|||
|
|||
exports.updateTenantId = tenantId => { |
|||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId) |
|||
if (env.USE_COUCH) { |
|||
exports.setGlobalDB(tenantId) |
|||
} |
|||
} |
|||
|
|||
exports.updateAppId = async appId => { |
|||
try { |
|||
// have to close first, before removing the databases from context
|
|||
await closeAppDBs() |
|||
cls.setOnContext(ContextKeys.APP_ID, appId) |
|||
} catch (err) { |
|||
if (env.isTest()) { |
|||
TEST_APP_ID = appId |
|||
} else { |
|||
throw err |
|||
} |
|||
} |
|||
} |
|||
|
|||
exports.setGlobalDB = tenantId => { |
|||
const dbName = baseGlobalDBName(tenantId) |
|||
const db = dangerousGetDB(dbName) |
|||
cls.setOnContext(ContextKeys.GLOBAL_DB, db) |
|||
return db |
|||
} |
|||
|
|||
exports.getGlobalDB = () => { |
|||
const db = cls.getFromContext(ContextKeys.GLOBAL_DB) |
|||
if (!db) { |
|||
throw new Error("Global DB not found") |
|||
} |
|||
return db |
|||
} |
|||
|
|||
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 new 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 getContextDB(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() |
|||
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 = dangerousGetDB(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 = null) => { |
|||
return getContextDB(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 = null) => { |
|||
return getContextDB(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 = null) => { |
|||
return getContextDB(ContextKeys.DEV_DB, opts) |
|||
} |
|||
@ -0,0 +1,247 @@ |
|||
import env from "../environment" |
|||
import { SEPARATOR, DocumentTypes } from "../db/constants" |
|||
import cls from "./FunctionContext" |
|||
import { dangerousGetDB, closeDB } from "../db" |
|||
import { baseGlobalDBName } from "../tenancy/utils" |
|||
import { IdentityContext } from "@budibase/types" |
|||
import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants" |
|||
import { ContextKeys } from "./constants" |
|||
import { |
|||
updateUsing, |
|||
closeWithUsing, |
|||
setAppTenantId, |
|||
setIdentity, |
|||
closeAppDBs, |
|||
getContextDB, |
|||
} from "./utils" |
|||
|
|||
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 closeTenancy = async () => { |
|||
let db |
|||
try { |
|||
if (env.USE_COUCH) { |
|||
db = getGlobalDB() |
|||
} |
|||
} catch (err) { |
|||
// no DB found - skip closing
|
|||
return |
|||
} |
|||
await closeDB(db) |
|||
// clear from context now that database is closed/task is finished
|
|||
cls.setOnContext(ContextKeys.TENANT_ID, null) |
|||
cls.setOnContext(ContextKeys.GLOBAL_DB, null) |
|||
} |
|||
|
|||
// export const isDefaultTenant = () => {
|
|||
// return getTenantId() === DEFAULT_TENANT_ID
|
|||
// }
|
|||
|
|||
export const isMultiTenant = () => { |
|||
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) => { |
|||
if (!appId) { |
|||
return null |
|||
} |
|||
const split = appId.split(SEPARATOR) |
|||
const hasDev = split[1] === DocumentTypes.DEV |
|||
if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) { |
|||
return null |
|||
} |
|||
if (hasDev) { |
|||
return split[2] |
|||
} else { |
|||
return split[1] |
|||
} |
|||
} |
|||
|
|||
// used for automations, API endpoints should always be in context already
|
|||
export const doInTenant = (tenantId: string | null, task: any) => { |
|||
// the internal function is so that we can re-use an existing
|
|||
// context - don't want to close DB on a parent context
|
|||
async function internal(opts = { existing: false }) { |
|||
// set the tenant id + global db if this is a new context
|
|||
if (!opts.existing) { |
|||
updateTenantId(tenantId) |
|||
} |
|||
|
|||
try { |
|||
// invoke the task
|
|||
return await task() |
|||
} finally { |
|||
await closeWithUsing(ContextKeys.TENANCY_IN_USE, () => { |
|||
return closeTenancy() |
|||
}) |
|||
} |
|||
} |
|||
|
|||
const existing = cls.getFromContext(ContextKeys.TENANT_ID) === tenantId |
|||
return updateUsing(ContextKeys.TENANCY_IN_USE, existing, internal) |
|||
} |
|||
|
|||
export const doInAppContext = (appId: string, task: any) => { |
|||
if (!appId) { |
|||
throw new Error("appId is required") |
|||
} |
|||
|
|||
const identity = getIdentity() |
|||
|
|||
// the internal function is so that we can re-use an existing
|
|||
// context - don't want to close DB on a parent context
|
|||
async function internal(opts = { existing: false }) { |
|||
// set the app tenant id
|
|||
if (!opts.existing) { |
|||
setAppTenantId(appId) |
|||
} |
|||
// set the app ID
|
|||
cls.setOnContext(ContextKeys.APP_ID, appId) |
|||
|
|||
// preserve the identity
|
|||
if (identity) { |
|||
setIdentity(identity) |
|||
} |
|||
try { |
|||
// invoke the task
|
|||
return await task() |
|||
} finally { |
|||
await closeWithUsing(ContextKeys.APP_IN_USE, async () => { |
|||
await closeAppDBs() |
|||
await closeTenancy() |
|||
}) |
|||
} |
|||
} |
|||
const existing = cls.getFromContext(ContextKeys.APP_ID) === appId |
|||
return updateUsing(ContextKeys.APP_IN_USE, existing, internal) |
|||
} |
|||
|
|||
export const doInIdentityContext = (identity: IdentityContext, task: any) => { |
|||
if (!identity) { |
|||
throw new Error("identity is required") |
|||
} |
|||
|
|||
async function internal(opts = { existing: false }) { |
|||
if (!opts.existing) { |
|||
cls.setOnContext(ContextKeys.IDENTITY, identity) |
|||
// set the tenant so that doInTenant will preserve identity
|
|||
if (identity.tenantId) { |
|||
updateTenantId(identity.tenantId) |
|||
} |
|||
} |
|||
|
|||
try { |
|||
// invoke the task
|
|||
return await task() |
|||
} finally { |
|||
await closeWithUsing(ContextKeys.IDENTITY_IN_USE, async () => { |
|||
setIdentity(null) |
|||
await closeTenancy() |
|||
}) |
|||
} |
|||
} |
|||
|
|||
const existing = cls.getFromContext(ContextKeys.IDENTITY) |
|||
return updateUsing(ContextKeys.IDENTITY_IN_USE, existing, internal) |
|||
} |
|||
|
|||
export const getIdentity = (): IdentityContext | undefined => { |
|||
try { |
|||
return cls.getFromContext(ContextKeys.IDENTITY) |
|||
} catch (e) { |
|||
// do nothing - identity is not in context
|
|||
} |
|||
} |
|||
|
|||
export const updateTenantId = (tenantId: string | null) => { |
|||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId) |
|||
if (env.USE_COUCH) { |
|||
setGlobalDB(tenantId) |
|||
} |
|||
} |
|||
|
|||
export const updateAppId = async (appId: string) => { |
|||
try { |
|||
// have to close first, before removing the databases from context
|
|||
await closeAppDBs() |
|||
cls.setOnContext(ContextKeys.APP_ID, appId) |
|||
} catch (err) { |
|||
if (env.isTest()) { |
|||
TEST_APP_ID = appId |
|||
} else { |
|||
throw err |
|||
} |
|||
} |
|||
} |
|||
|
|||
export const setGlobalDB = (tenantId: string | null) => { |
|||
const dbName = baseGlobalDBName(tenantId) |
|||
const db = dangerousGetDB(dbName) |
|||
cls.setOnContext(ContextKeys.GLOBAL_DB, db) |
|||
return db |
|||
} |
|||
|
|||
export const getGlobalDB = () => { |
|||
const db = cls.getFromContext(ContextKeys.GLOBAL_DB) |
|||
if (!db) { |
|||
throw new Error("Global DB not found") |
|||
} |
|||
return db |
|||
} |
|||
|
|||
export const isTenantIdSet = () => { |
|||
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID) |
|||
return !!tenantId |
|||
} |
|||
|
|||
export const getTenantId = () => { |
|||
if (!isMultiTenant()) { |
|||
return DEFAULT_TENANT_ID |
|||
} |
|||
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID) |
|||
if (!tenantId) { |
|||
throw new Error("Tenant id not found") |
|||
} |
|||
return tenantId |
|||
} |
|||
|
|||
export const getAppId = () => { |
|||
const foundId = cls.getFromContext(ContextKeys.APP_ID) |
|||
if (!foundId && env.isTest() && TEST_APP_ID) { |
|||
return TEST_APP_ID |
|||
} else { |
|||
return foundId |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Opens the app database based on whatever the request |
|||
* contained, dev or prod. |
|||
*/ |
|||
export const getAppDB = (opts?: any) => { |
|||
return getContextDB(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. |
|||
*/ |
|||
export const getProdAppDB = (opts?: any) => { |
|||
return getContextDB(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. |
|||
*/ |
|||
export const getDevAppDB = (opts?: any) => { |
|||
return getContextDB(ContextKeys.DEV_DB, opts) |
|||
} |
|||
@ -0,0 +1,148 @@ |
|||
import "../../../tests/utilities/TestConfiguration" |
|||
import * as context from ".." |
|||
import { DEFAULT_TENANT_ID } from "../../constants" |
|||
import env from "../../environment" |
|||
|
|||
// must use require to spy index file exports due to known issue in jest
|
|||
const dbUtils = require("../../db") |
|||
jest.spyOn(dbUtils, "closeDB") |
|||
jest.spyOn(dbUtils, "dangerousGetDB") |
|||
|
|||
describe("context", () => { |
|||
beforeEach(() => { |
|||
jest.clearAllMocks() |
|||
}) |
|||
|
|||
describe("doInTenant", () => { |
|||
describe("single-tenancy", () => { |
|||
it("defaults to the default tenant", () => { |
|||
const tenantId = context.getTenantId() |
|||
expect(tenantId).toBe(DEFAULT_TENANT_ID) |
|||
}) |
|||
|
|||
it("defaults to the default tenant db", async () => { |
|||
await context.doInTenant(DEFAULT_TENANT_ID, () => { |
|||
const db = context.getGlobalDB() |
|||
expect(db.name).toBe("global-db") |
|||
}) |
|||
expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1) |
|||
expect(dbUtils.closeDB).toHaveBeenCalledTimes(1) |
|||
}) |
|||
}) |
|||
|
|||
describe("multi-tenancy", () => { |
|||
beforeEach(() => { |
|||
env._set("MULTI_TENANCY", 1) |
|||
}) |
|||
|
|||
it("fails when no tenant id is set", () => { |
|||
const test = () => { |
|||
let error |
|||
try { |
|||
context.getTenantId() |
|||
} catch (e: any) { |
|||
error = e |
|||
} |
|||
expect(error.message).toBe("Tenant id not found") |
|||
} |
|||
|
|||
// test under no tenancy
|
|||
test() |
|||
|
|||
// test after tenancy has been accessed to ensure cleanup
|
|||
context.doInTenant("test", () => {}) |
|||
test() |
|||
}) |
|||
|
|||
it("fails when no tenant db is set", () => { |
|||
const test = () => { |
|||
let error |
|||
try { |
|||
context.getGlobalDB() |
|||
} catch (e: any) { |
|||
error = e |
|||
} |
|||
expect(error.message).toBe("Global DB not found") |
|||
} |
|||
|
|||
// test under no tenancy
|
|||
test() |
|||
|
|||
// test after tenancy has been accessed to ensure cleanup
|
|||
context.doInTenant("test", () => {}) |
|||
test() |
|||
}) |
|||
|
|||
it("sets tenant id", () => { |
|||
context.doInTenant("test", () => { |
|||
const tenantId = context.getTenantId() |
|||
expect(tenantId).toBe("test") |
|||
}) |
|||
}) |
|||
|
|||
it("initialises the tenant db", async () => { |
|||
await context.doInTenant("test", () => { |
|||
const db = context.getGlobalDB() |
|||
expect(db.name).toBe("test_global-db") |
|||
}) |
|||
expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1) |
|||
expect(dbUtils.closeDB).toHaveBeenCalledTimes(1) |
|||
}) |
|||
|
|||
it("sets the tenant id when nested with same tenant id", async () => { |
|||
await context.doInTenant("test", async () => { |
|||
const tenantId = context.getTenantId() |
|||
expect(tenantId).toBe("test") |
|||
|
|||
await context.doInTenant("test", async () => { |
|||
const tenantId = context.getTenantId() |
|||
expect(tenantId).toBe("test") |
|||
|
|||
await context.doInTenant("test", () => { |
|||
const tenantId = context.getTenantId() |
|||
expect(tenantId).toBe("test") |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
it("initialises the tenant db when nested with same tenant id", async () => { |
|||
await context.doInTenant("test", async () => { |
|||
const db = context.getGlobalDB() |
|||
expect(db.name).toBe("test_global-db") |
|||
|
|||
await context.doInTenant("test", async () => { |
|||
const db = context.getGlobalDB() |
|||
expect(db.name).toBe("test_global-db") |
|||
|
|||
await context.doInTenant("test", () => { |
|||
const db = context.getGlobalDB() |
|||
expect(db.name).toBe("test_global-db") |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
// only 1 db is opened and closed
|
|||
expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1) |
|||
expect(dbUtils.closeDB).toHaveBeenCalledTimes(1) |
|||
}) |
|||
|
|||
it("sets different tenant id inside another context", () => { |
|||
context.doInTenant("test", () => { |
|||
const tenantId = context.getTenantId() |
|||
expect(tenantId).toBe("test") |
|||
|
|||
context.doInTenant("nested", () => { |
|||
const tenantId = context.getTenantId() |
|||
expect(tenantId).toBe("nested") |
|||
|
|||
context.doInTenant("double-nested", () => { |
|||
const tenantId = context.getTenantId() |
|||
expect(tenantId).toBe("double-nested") |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,113 @@ |
|||
import { |
|||
DEFAULT_TENANT_ID, |
|||
getAppId, |
|||
getTenantIDFromAppID, |
|||
updateTenantId, |
|||
} from "./index" |
|||
import cls from "./FunctionContext" |
|||
import { IdentityContext } from "@budibase/types" |
|||
import { ContextKeys } from "./constants" |
|||
import { dangerousGetDB, closeDB } from "../db" |
|||
import { isEqual } from "lodash" |
|||
import { getDevelopmentAppID, getProdAppID } from "../db/conversions" |
|||
import env from "../environment" |
|||
|
|||
export async function updateUsing( |
|||
usingKey: string, |
|||
existing: boolean, |
|||
internal: (opts: { existing: boolean }) => Promise<any> |
|||
) { |
|||
const using = cls.getFromContext(usingKey) |
|||
if (using && existing) { |
|||
cls.setOnContext(usingKey, using + 1) |
|||
return internal({ existing: true }) |
|||
} else { |
|||
return cls.run(async () => { |
|||
cls.setOnContext(usingKey, 1) |
|||
return internal({ existing: false }) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
export async function closeWithUsing( |
|||
usingKey: string, |
|||
closeFn: () => Promise<any> |
|||
) { |
|||
const using = cls.getFromContext(usingKey) |
|||
if (!using || using <= 1) { |
|||
await closeFn() |
|||
} else { |
|||
cls.setOnContext(usingKey, using - 1) |
|||
} |
|||
} |
|||
|
|||
export const setAppTenantId = (appId: string) => { |
|||
const appTenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID |
|||
updateTenantId(appTenantId) |
|||
} |
|||
|
|||
export const setIdentity = (identity: IdentityContext | null) => { |
|||
cls.setOnContext(ContextKeys.IDENTITY, identity) |
|||
} |
|||
|
|||
// this function makes sure the PouchDB objects are closed and
|
|||
// fully deleted when finished - this protects against memory leaks
|
|||
export async function closeAppDBs() { |
|||
const dbKeys = [ |
|||
ContextKeys.CURRENT_DB, |
|||
ContextKeys.PROD_DB, |
|||
ContextKeys.DEV_DB, |
|||
] |
|||
for (let dbKey of dbKeys) { |
|||
const db = cls.getFromContext(dbKey) |
|||
if (!db) { |
|||
continue |
|||
} |
|||
await closeDB(db) |
|||
// clear the DB from context, incase someone tries to use it again
|
|||
cls.setOnContext(dbKey, null) |
|||
} |
|||
// clear the app ID now that the databases are closed
|
|||
if (cls.getFromContext(ContextKeys.APP_ID)) { |
|||
cls.setOnContext(ContextKeys.APP_ID, null) |
|||
} |
|||
if (cls.getFromContext(ContextKeys.DB_OPTS)) { |
|||
cls.setOnContext(ContextKeys.DB_OPTS, null) |
|||
} |
|||
} |
|||
|
|||
export function getContextDB(key: string, opts: any) { |
|||
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 = getAppId() |
|||
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 = dangerousGetDB(toUseAppId, opts) |
|||
try { |
|||
cls.setOnContext(key, db) |
|||
if (opts) { |
|||
cls.setOnContext(dbOptsKey, opts) |
|||
} |
|||
} catch (err) { |
|||
if (!env.isTest()) { |
|||
throw err |
|||
} |
|||
} |
|||
return db |
|||
} |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
Loading…
Reference in new issue