Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring app DB usage - one DB load per request #4276

Merged
merged 14 commits into from
Feb 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/backend-core/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const {
getAppDB,
getDevAppDB,
getProdAppDB,
getAppId,
updateAppId,
doInAppContext,
} = require("./src/context")

module.exports = {
getAppDB,
getDevAppDB,
getProdAppDB,
getAppId,
updateAppId,
doInAppContext,
}
1 change: 1 addition & 0 deletions packages/backend-core/db.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module.exports = {
...require("./src/db/utils"),
...require("./src/db/constants"),
...require("./src/db"),
...require("./src/db/views"),
}
2 changes: 1 addition & 1 deletion packages/backend-core/deprovision.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
module.exports = require("./src/tenancy/deprovision")
module.exports = require("./src/context/deprovision")
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ const { newid } = require("../hashing")
const REQUEST_ID_KEY = "requestId"

class FunctionContext {
static getMiddleware(updateCtxFn = null) {
const namespace = this.createNamespace()
static getMiddleware(updateCtxFn = null, contextName = "session") {
const namespace = this.createNamespace(contextName)

return async function (ctx, next) {
await new Promise(
Expand All @@ -24,14 +24,14 @@ class FunctionContext {
}
}

static run(callback) {
const namespace = this.createNamespace()
static run(callback, contextName = "session") {
const namespace = this.createNamespace(contextName)

return namespace.runAndReturn(callback)
}

static setOnContext(key, value) {
const namespace = this.createNamespace()
static setOnContext(key, value, contextName = "session") {
const namespace = this.createNamespace(contextName)
namespace.set(key, value)
}

Expand All @@ -55,16 +55,16 @@ class FunctionContext {
}
}

static destroyNamespace() {
static destroyNamespace(name = "session") {
if (this._namespace) {
cls.destroyNamespace("session")
cls.destroyNamespace(name)
this._namespace = null
}
}

static createNamespace() {
static createNamespace(name = "session") {
if (!this._namespace) {
this._namespace = cls.createNamespace("session")
this._namespace = cls.createNamespace(name)
}
return this._namespace
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { getGlobalUserParams, getAllApps } = require("../db/utils")
const { getDB, getCouch } = require("../db")
const { getGlobalDB } = require("./tenancy")
const { getGlobalDB } = require("../tenancy")
const { StaticDatabases } = require("../db/constants")

const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
Expand Down
195 changes: 195 additions & 0 deletions packages/backend-core/src/context/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
const env = require("../environment")
const { Headers } = require("../../constants")
const cls = require("./FunctionContext")
const { getCouch } = require("../db")
const { getProdAppID, getDevelopmentAppID } = require("../db/conversions")
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",
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",
}

exports.DEFAULT_TENANT_ID = "default"

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) => {
return cls.run(() => {
// set the tenant id
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)

// invoke the task
return task()
})
}

exports.doInAppContext = (appId, task) => {
return cls.run(() => {
// set the app ID
cls.setOnContext(ContextKeys.APP_ID, appId)

// invoke the task
return task()
})
}

exports.updateTenantId = tenantId => {
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
}

exports.updateAppId = appId => {
try {
cls.setOnContext(ContextKeys.APP_ID, appId)
cls.setOnContext(ContextKeys.PROD_DB, null)
cls.setOnContext(ContextKeys.DEV_DB, null)
cls.setOnContext(ContextKeys.CURRENT_DB, null)
cls.setOnContext(ContextKeys.DB_OPTS, null)
} catch (err) {
if (env.isTest()) {
TEST_APP_ID = appId
} else {
throw err
}
}
}

exports.setTenantId = (
ctx,
opts = { allowQs: false, allowNoTenant: false }
) => {
let tenantId
// exit early if not multi-tenant
if (!exports.isMultiTenant()) {
cls.setOnContext(ContextKeys.TENANT_ID, this.DEFAULT_TENANT_ID)
return
}

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

// Set the tenantId from the subdomain
if (!tenantId) {
tenantId = ctx.subdomains && ctx.subdomains[0]
}

if (!tenantId && !allowNoTenant) {
ctx.throw(403, "Tenant id not set")
}
// check tenant ID just incase no tenant was allowed
if (tenantId) {
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
}
}

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 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 getDB(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()
const CouchDB = getCouch()
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 = new CouchDB(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 => {
return getDB(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 => {
return getDB(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 => {
return getDB(ContextKeys.DEV_DB, opts)
}
4 changes: 4 additions & 0 deletions packages/backend-core/src/db/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,7 @@ exports.StaticDatabases = {
},
},
}

exports.APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR
exports.APP_DEV = exports.APP_DEV_PREFIX =
exports.DocumentTypes.APP_DEV + exports.SEPARATOR
46 changes: 46 additions & 0 deletions packages/backend-core/src/db/conversions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const NO_APP_ERROR = "No app provided"
const { APP_DEV_PREFIX, APP_PREFIX } = require("./constants")

exports.isDevAppID = appId => {
if (!appId) {
throw NO_APP_ERROR
}
return appId.startsWith(APP_DEV_PREFIX)
}

exports.isProdAppID = appId => {
mike12345567 marked this conversation as resolved.
Show resolved Hide resolved
if (!appId) {
throw NO_APP_ERROR
}
return appId.startsWith(APP_PREFIX) && !exports.isDevAppID(appId)
}

exports.isDevApp = app => {
if (!app) {
throw NO_APP_ERROR
}
return exports.isDevAppID(app.appId)
}

/**
* Convert a development app ID to a deployed app ID.
*/
exports.getProdAppID = appId => {
// if dev, convert it
if (appId.startsWith(APP_DEV_PREFIX)) {
const id = appId.split(APP_DEV_PREFIX)[1]
return `${APP_PREFIX}${id}`
}
return appId
}

/**
* Convert a deployed app ID to a development app ID.
*/
exports.getDevelopmentAppID = appId => {
if (!appId.startsWith(APP_DEV_PREFIX)) {
const id = appId.split(APP_PREFIX)[1]
return `${APP_DEV_PREFIX}${id}`
}
return appId
}
Loading