2021-08-05 10:59:08 +02:00
|
|
|
const env = require("../environment")
|
|
|
|
const { Headers } = require("../../constants")
|
2022-03-24 14:04:49 +01:00
|
|
|
const { SEPARATOR, DocumentTypes } = require("../db/constants")
|
2022-04-19 20:42:52 +02:00
|
|
|
const { DEFAULT_TENANT_ID } = require("../constants")
|
2021-08-05 10:59:08 +02:00
|
|
|
const cls = require("./FunctionContext")
|
2022-04-20 18:33:42 +02:00
|
|
|
const { dangerousGetDB, closeDB } = require("../db")
|
2022-01-31 18:42:51 +01:00
|
|
|
const { getProdAppID, getDevelopmentAppID } = require("../db/conversions")
|
2022-04-19 20:42:52 +02:00
|
|
|
const { baseGlobalDBName } = require("../tenancy/utils")
|
2022-01-27 19:18:31 +01:00
|
|
|
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",
|
2022-04-19 20:42:52 +02:00
|
|
|
GLOBAL_DB: "globalDb",
|
2022-01-27 19:18:31 +01:00
|
|
|
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",
|
2022-04-21 00:10:39 +02:00
|
|
|
// check if something else is using the context, don't close DB
|
|
|
|
IN_USE: "inUse",
|
2022-01-27 19:18:31 +01:00
|
|
|
}
|
2021-08-05 10:59:08 +02:00
|
|
|
|
2022-04-19 20:42:52 +02:00
|
|
|
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
|
|
|
|
}
|
2022-04-20 18:33:42 +02:00
|
|
|
await closeDB(db)
|
2022-04-21 00:10:39 +02:00
|
|
|
// 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)
|
2022-04-19 20:42:52 +02:00
|
|
|
}
|
|
|
|
}
|
2021-08-05 10:59:08 +02:00
|
|
|
|
|
|
|
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) => {
|
2022-04-19 20:42:52 +02:00
|
|
|
// 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 }) {
|
2021-08-05 10:59:08 +02:00
|
|
|
// set the tenant id
|
2022-04-19 20:42:52 +02:00
|
|
|
if (!opts.existing) {
|
|
|
|
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
|
|
|
exports.setGlobalDB(tenantId)
|
|
|
|
}
|
2021-08-05 10:59:08 +02:00
|
|
|
|
2022-04-20 18:33:42 +02:00
|
|
|
try {
|
|
|
|
// invoke the task
|
|
|
|
return await task()
|
|
|
|
} finally {
|
2022-04-21 00:10:39 +02:00
|
|
|
const using = cls.getFromContext(ContextKeys.IN_USE)
|
|
|
|
if (!using || using <= 1) {
|
2022-04-20 18:33:42 +02:00
|
|
|
await closeDB(exports.getGlobalDB())
|
2022-04-21 00:10:39 +02:00
|
|
|
// clear from context now that database is closed/task is finished
|
|
|
|
cls.setOnContext(ContextKeys.TENANT_ID, null)
|
|
|
|
cls.setOnContext(ContextKeys.GLOBAL_DB, null)
|
|
|
|
} else {
|
|
|
|
cls.setOnContext(using - 1)
|
2022-04-20 18:33:42 +02:00
|
|
|
}
|
2022-04-19 20:42:52 +02:00
|
|
|
}
|
|
|
|
}
|
2022-04-21 00:10:39 +02:00
|
|
|
const using = cls.getFromContext(ContextKeys.IN_USE)
|
2022-04-21 00:13:08 +02:00
|
|
|
if (using && cls.getFromContext(ContextKeys.TENANT_ID) === tenantId) {
|
2022-04-21 00:10:39 +02:00
|
|
|
cls.setOnContext(ContextKeys.IN_USE, using + 1)
|
2022-04-19 20:42:52 +02:00
|
|
|
return internal({ existing: true })
|
|
|
|
} else {
|
|
|
|
return cls.run(async () => {
|
2022-04-21 00:10:39 +02:00
|
|
|
cls.setOnContext(ContextKeys.IN_USE, 1)
|
2022-04-19 20:42:52 +02:00
|
|
|
return internal()
|
|
|
|
})
|
|
|
|
}
|
2021-08-05 10:59:08 +02:00
|
|
|
}
|
|
|
|
|
2022-03-24 14:04:49 +01:00
|
|
|
/**
|
|
|
|
* 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 => {
|
2022-04-19 20:42:52 +02:00
|
|
|
const appTenantId =
|
|
|
|
exports.getTenantIDFromAppID(appId) || exports.DEFAULT_TENANT_ID
|
|
|
|
exports.updateTenantId(appTenantId)
|
2022-03-24 14:04:49 +01:00
|
|
|
}
|
|
|
|
|
2022-01-28 01:05:39 +01:00
|
|
|
exports.doInAppContext = (appId, task) => {
|
2022-03-24 14:04:49 +01:00
|
|
|
if (!appId) {
|
|
|
|
throw new Error("appId is required")
|
|
|
|
}
|
2022-04-21 00:10:39 +02:00
|
|
|
|
2022-04-19 20:42:52 +02:00
|
|
|
// 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 }) {
|
2022-03-24 14:04:49 +01:00
|
|
|
// set the app tenant id
|
2022-04-19 20:42:52 +02:00
|
|
|
if (!opts.existing) {
|
|
|
|
setAppTenantId(appId)
|
|
|
|
}
|
2022-01-28 01:05:39 +01:00
|
|
|
// set the app ID
|
|
|
|
cls.setOnContext(ContextKeys.APP_ID, appId)
|
2022-04-20 18:33:42 +02:00
|
|
|
try {
|
|
|
|
// invoke the task
|
|
|
|
return await task()
|
|
|
|
} finally {
|
2022-04-21 00:10:39 +02:00
|
|
|
const using = cls.getFromContext(ContextKeys.IN_USE)
|
|
|
|
if (!using || using <= 1) {
|
2022-04-20 18:33:42 +02:00
|
|
|
await closeAppDBs()
|
2022-04-21 00:10:39 +02:00
|
|
|
} else {
|
|
|
|
cls.setOnContext(using - 1)
|
2022-04-20 18:33:42 +02:00
|
|
|
}
|
2022-04-19 20:42:52 +02:00
|
|
|
}
|
|
|
|
}
|
2022-04-21 00:10:39 +02:00
|
|
|
const using = cls.getFromContext(ContextKeys.IN_USE)
|
2022-04-21 00:13:08 +02:00
|
|
|
if (using && cls.getFromContext(ContextKeys.APP_ID) === appId) {
|
2022-04-21 00:10:39 +02:00
|
|
|
cls.setOnContext(ContextKeys.IN_USE, using + 1)
|
2022-04-19 20:42:52 +02:00
|
|
|
return internal({ existing: true })
|
|
|
|
} else {
|
|
|
|
return cls.run(async () => {
|
2022-04-21 00:10:39 +02:00
|
|
|
cls.setOnContext(ContextKeys.IN_USE, 1)
|
2022-04-19 20:42:52 +02:00
|
|
|
return internal()
|
|
|
|
})
|
|
|
|
}
|
2022-01-28 01:05:39 +01:00
|
|
|
}
|
|
|
|
|
2021-08-05 10:59:08 +02:00
|
|
|
exports.updateTenantId = tenantId => {
|
2022-01-27 19:18:31 +01:00
|
|
|
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.updateAppId = appId => {
|
|
|
|
try {
|
2022-04-20 18:33:42 +02:00
|
|
|
// have to close first, before removing the databases from context
|
2022-04-19 20:42:52 +02:00
|
|
|
const promise = closeAppDBs()
|
2022-01-27 19:18:31 +01:00
|
|
|
cls.setOnContext(ContextKeys.APP_ID, appId)
|
2022-04-19 20:42:52 +02:00
|
|
|
return promise
|
2022-01-27 19:18:31 +01:00
|
|
|
} catch (err) {
|
|
|
|
if (env.isTest()) {
|
|
|
|
TEST_APP_ID = appId
|
|
|
|
} else {
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
}
|
2021-08-05 10:59:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
exports.setTenantId = (
|
|
|
|
ctx,
|
|
|
|
opts = { allowQs: false, allowNoTenant: false }
|
|
|
|
) => {
|
|
|
|
let tenantId
|
|
|
|
// exit early if not multi-tenant
|
|
|
|
if (!exports.isMultiTenant()) {
|
2022-04-19 20:42:52 +02:00
|
|
|
cls.setOnContext(ContextKeys.TENANT_ID, exports.DEFAULT_TENANT_ID)
|
|
|
|
return exports.DEFAULT_TENANT_ID
|
2021-08-05 10:59:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2021-09-28 17:40:03 +02:00
|
|
|
// Set the tenantId from the subdomain
|
|
|
|
if (!tenantId) {
|
|
|
|
tenantId = ctx.subdomains && ctx.subdomains[0]
|
|
|
|
}
|
|
|
|
|
2021-08-05 10:59:08 +02:00
|
|
|
if (!tenantId && !allowNoTenant) {
|
|
|
|
ctx.throw(403, "Tenant id not set")
|
|
|
|
}
|
|
|
|
// check tenant ID just incase no tenant was allowed
|
|
|
|
if (tenantId) {
|
2022-01-27 19:18:31 +01:00
|
|
|
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
2021-08-05 10:59:08 +02:00
|
|
|
}
|
2022-04-19 20:42:52 +02:00
|
|
|
return tenantId
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2021-08-05 10:59:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
exports.isTenantIdSet = () => {
|
2022-01-27 19:18:31 +01:00
|
|
|
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID)
|
2021-08-05 10:59:08 +02:00
|
|
|
return !!tenantId
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.getTenantId = () => {
|
|
|
|
if (!exports.isMultiTenant()) {
|
|
|
|
return exports.DEFAULT_TENANT_ID
|
|
|
|
}
|
2022-01-27 19:18:31 +01:00
|
|
|
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID)
|
2021-08-05 10:59:08 +02:00
|
|
|
if (!tenantId) {
|
2022-02-18 12:18:59 +01:00
|
|
|
throw new Error("Tenant id not found")
|
2021-08-05 10:59:08 +02:00
|
|
|
}
|
|
|
|
return tenantId
|
|
|
|
}
|
2022-01-27 19:18:31 +01:00
|
|
|
|
|
|
|
exports.getAppId = () => {
|
|
|
|
const foundId = cls.getFromContext(ContextKeys.APP_ID)
|
|
|
|
if (!foundId && env.isTest() && TEST_APP_ID) {
|
|
|
|
return TEST_APP_ID
|
|
|
|
} else {
|
|
|
|
return foundId
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-29 17:03:44 +02:00
|
|
|
function getContextDB(key, opts) {
|
2022-01-27 19:18:31 +01:00
|
|
|
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:
|
2022-01-31 18:42:51 +01:00
|
|
|
toUseAppId = getProdAppID(appId)
|
2022-01-27 19:18:31 +01:00
|
|
|
break
|
|
|
|
case ContextKeys.DEV_DB:
|
|
|
|
toUseAppId = getDevelopmentAppID(appId)
|
|
|
|
break
|
|
|
|
}
|
2022-04-19 20:42:52 +02:00
|
|
|
db = dangerousGetDB(toUseAppId, opts)
|
2022-01-27 19:18:31 +01:00
|
|
|
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 => {
|
2022-03-29 17:03:44 +02:00
|
|
|
return getContextDB(ContextKeys.CURRENT_DB, opts)
|
2022-01-27 19:18:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This specifically gets the prod app ID, if the request
|
|
|
|
* contained a development app ID, this will open the prod one.
|
|
|
|
*/
|
|
|
|
exports.getProdAppDB = opts => {
|
2022-03-29 17:03:44 +02:00
|
|
|
return getContextDB(ContextKeys.PROD_DB, opts)
|
2022-01-27 19:18:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This specifically gets the dev app ID, if the request
|
|
|
|
* contained a prod app ID, this will open the dev one.
|
|
|
|
*/
|
|
|
|
exports.getDevAppDB = opts => {
|
2022-03-29 17:03:44 +02:00
|
|
|
return getContextDB(ContextKeys.DEV_DB, opts)
|
2022-01-27 19:18:31 +01:00
|
|
|
}
|