diff --git a/lerna.json b/lerna.json index dd50fdf909..018ee428c0 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.0.151-alpha.2", + "version": "1.0.155-alpha.0", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 858588c0ee..fb8e03cf5a 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "1.0.151-alpha.2", + "version": "1.0.155-alpha.0", "description": "Budibase backend core libraries used in server and worker", "main": "src/index.js", "author": "Budibase", @@ -13,7 +13,7 @@ "@techpass/passport-openidconnect": "^0.3.0", "aws-sdk": "^2.901.0", "bcryptjs": "^2.4.3", - "cls-hooked": "^4.2.2", + "emitter-listener": "^1.1.2", "ioredis": "^4.27.1", "jsonwebtoken": "^8.5.1", "koa-passport": "^4.1.4", diff --git a/packages/backend-core/src/clshooked/index.js b/packages/backend-core/src/clshooked/index.js new file mode 100644 index 0000000000..d69ffdd914 --- /dev/null +++ b/packages/backend-core/src/clshooked/index.js @@ -0,0 +1,650 @@ +const util = require("util") +const assert = require("assert") +const wrapEmitter = require("emitter-listener") +const async_hooks = require("async_hooks") + +const CONTEXTS_SYMBOL = "cls@contexts" +const ERROR_SYMBOL = "error@context" + +const DEBUG_CLS_HOOKED = process.env.DEBUG_CLS_HOOKED + +let currentUid = -1 + +module.exports = { + getNamespace: getNamespace, + createNamespace: createNamespace, + destroyNamespace: destroyNamespace, + reset: reset, + ERROR_SYMBOL: ERROR_SYMBOL, +} + +function Namespace(name) { + this.name = name + // changed in 2.7: no default context + this.active = null + this._set = [] + this.id = null + this._contexts = new Map() + this._indent = 0 + this._hook = null +} + +Namespace.prototype.set = function set(key, value) { + if (!this.active) { + throw new Error( + "No context available. ns.run() or ns.bind() must be called first." + ) + } + + this.active[key] = value + + if (DEBUG_CLS_HOOKED) { + const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) + debug2( + indentStr + + "CONTEXT-SET KEY:" + + key + + "=" + + value + + " in ns:" + + this.name + + " currentUid:" + + currentUid + + " active:" + + util.inspect(this.active, { showHidden: true, depth: 2, colors: true }) + ) + } + + return value +} + +Namespace.prototype.get = function get(key) { + if (!this.active) { + if (DEBUG_CLS_HOOKED) { + const asyncHooksCurrentId = async_hooks.currentId() + const triggerId = async_hooks.triggerAsyncId() + const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) + debug2( + `${indentStr}CONTEXT-GETTING KEY NO ACTIVE NS: (${this.name}) ${key}=undefined currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${this._set.length}` + ) + } + return undefined + } + if (DEBUG_CLS_HOOKED) { + const asyncHooksCurrentId = async_hooks.executionAsyncId() + const triggerId = async_hooks.triggerAsyncId() + const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) + debug2( + indentStr + + "CONTEXT-GETTING KEY:" + + key + + "=" + + this.active[key] + + " (" + + this.name + + ") currentUid:" + + currentUid + + " active:" + + util.inspect(this.active, { showHidden: true, depth: 2, colors: true }) + ) + debug2( + `${indentStr}CONTEXT-GETTING KEY: (${this.name}) ${key}=${ + this.active[key] + } currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${ + this._set.length + } active:${util.inspect(this.active)}` + ) + } + return this.active[key] +} + +Namespace.prototype.createContext = function createContext() { + // Prototype inherit existing context if created a new child context within existing context. + let context = Object.create(this.active ? this.active : Object.prototype) + context._ns_name = this.name + context.id = currentUid + + if (DEBUG_CLS_HOOKED) { + const asyncHooksCurrentId = async_hooks.executionAsyncId() + const triggerId = async_hooks.triggerAsyncId() + const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) + debug2( + `${indentStr}CONTEXT-CREATED Context: (${ + this.name + }) currentUid:${currentUid} asyncHooksCurrentId:${asyncHooksCurrentId} triggerId:${triggerId} len:${ + this._set.length + } context:${util.inspect(context, { + showHidden: true, + depth: 2, + colors: true, + })}` + ) + } + + return context +} + +Namespace.prototype.run = function run(fn) { + let context = this.createContext() + this.enter(context) + + try { + if (DEBUG_CLS_HOOKED) { + const triggerId = async_hooks.triggerAsyncId() + const asyncHooksCurrentId = async_hooks.executionAsyncId() + const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) + debug2( + `${indentStr}CONTEXT-RUN BEGIN: (${ + this.name + }) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ + this._set.length + } context:${util.inspect(context)}` + ) + } + fn(context) + return context + } catch (exception) { + if (exception) { + exception[ERROR_SYMBOL] = context + } + throw exception + } finally { + if (DEBUG_CLS_HOOKED) { + const triggerId = async_hooks.triggerAsyncId() + const asyncHooksCurrentId = async_hooks.executionAsyncId() + const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) + debug2( + `${indentStr}CONTEXT-RUN END: (${ + this.name + }) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ + this._set.length + } ${util.inspect(context)}` + ) + } + this.exit(context) + } +} + +Namespace.prototype.runAndReturn = function runAndReturn(fn) { + let value + this.run(function (context) { + value = fn(context) + }) + return value +} + +/** + * Uses global Promise and assumes Promise is cls friendly or wrapped already. + * @param {function} fn + * @returns {*} + */ +Namespace.prototype.runPromise = function runPromise(fn) { + let context = this.createContext() + this.enter(context) + + let promise = fn(context) + if (!promise || !promise.then || !promise.catch) { + throw new Error("fn must return a promise.") + } + + if (DEBUG_CLS_HOOKED) { + debug2( + "CONTEXT-runPromise BEFORE: (" + + this.name + + ") currentUid:" + + currentUid + + " len:" + + this._set.length + + " " + + util.inspect(context) + ) + } + + return promise + .then(result => { + if (DEBUG_CLS_HOOKED) { + debug2( + "CONTEXT-runPromise AFTER then: (" + + this.name + + ") currentUid:" + + currentUid + + " len:" + + this._set.length + + " " + + util.inspect(context) + ) + } + this.exit(context) + return result + }) + .catch(err => { + err[ERROR_SYMBOL] = context + if (DEBUG_CLS_HOOKED) { + debug2( + "CONTEXT-runPromise AFTER catch: (" + + this.name + + ") currentUid:" + + currentUid + + " len:" + + this._set.length + + " " + + util.inspect(context) + ) + } + this.exit(context) + throw err + }) +} + +Namespace.prototype.bind = function bindFactory(fn, context) { + if (!context) { + if (!this.active) { + context = this.createContext() + } else { + context = this.active + } + } + + let self = this + return function clsBind() { + self.enter(context) + try { + return fn.apply(this, arguments) + } catch (exception) { + if (exception) { + exception[ERROR_SYMBOL] = context + } + throw exception + } finally { + self.exit(context) + } + } +} + +Namespace.prototype.enter = function enter(context) { + assert.ok(context, "context must be provided for entering") + if (DEBUG_CLS_HOOKED) { + const asyncHooksCurrentId = async_hooks.executionAsyncId() + const triggerId = async_hooks.triggerAsyncId() + const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) + debug2( + `${indentStr}CONTEXT-ENTER: (${ + this.name + }) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ + this._set.length + } ${util.inspect(context)}` + ) + } + + this._set.push(this.active) + this.active = context +} + +Namespace.prototype.exit = function exit(context) { + assert.ok(context, "context must be provided for exiting") + if (DEBUG_CLS_HOOKED) { + const asyncHooksCurrentId = async_hooks.executionAsyncId() + const triggerId = async_hooks.triggerAsyncId() + const indentStr = " ".repeat(this._indent < 0 ? 0 : this._indent) + debug2( + `${indentStr}CONTEXT-EXIT: (${ + this.name + }) currentUid:${currentUid} triggerId:${triggerId} asyncHooksCurrentId:${asyncHooksCurrentId} len:${ + this._set.length + } ${util.inspect(context)}` + ) + } + + // Fast path for most exits that are at the top of the stack + if (this.active === context) { + assert.ok(this._set.length, "can't remove top context") + this.active = this._set.pop() + return + } + + // Fast search in the stack using lastIndexOf + let index = this._set.lastIndexOf(context) + + if (index < 0) { + if (DEBUG_CLS_HOOKED) { + debug2( + "??ERROR?? context exiting but not entered - ignoring: " + + util.inspect(context) + ) + } + assert.ok( + index >= 0, + "context not currently entered; can't exit. \n" + + util.inspect(this) + + "\n" + + util.inspect(context) + ) + } else { + assert.ok(index, "can't remove top context") + this._set.splice(index, 1) + } +} + +Namespace.prototype.bindEmitter = function bindEmitter(emitter) { + assert.ok( + emitter.on && emitter.addListener && emitter.emit, + "can only bind real EEs" + ) + + let namespace = this + let thisSymbol = "context@" + this.name + + // Capture the context active at the time the emitter is bound. + function attach(listener) { + if (!listener) { + return + } + if (!listener[CONTEXTS_SYMBOL]) { + listener[CONTEXTS_SYMBOL] = Object.create(null) + } + + listener[CONTEXTS_SYMBOL][thisSymbol] = { + namespace: namespace, + context: namespace.active, + } + } + + // At emit time, bind the listener within the correct context. + function bind(unwrapped) { + if (!(unwrapped && unwrapped[CONTEXTS_SYMBOL])) { + return unwrapped + } + + let wrapped = unwrapped + let unwrappedContexts = unwrapped[CONTEXTS_SYMBOL] + Object.keys(unwrappedContexts).forEach(function (name) { + let thunk = unwrappedContexts[name] + wrapped = thunk.namespace.bind(wrapped, thunk.context) + }) + return wrapped + } + + wrapEmitter(emitter, attach, bind) +} + +/** + * If an error comes out of a namespace, it will have a context attached to it. + * This function knows how to find it. + * + * @param {Error} exception Possibly annotated error. + */ +Namespace.prototype.fromException = function fromException(exception) { + return exception[ERROR_SYMBOL] +} + +function getNamespace(name) { + return process.namespaces[name] +} + +function createNamespace(name) { + assert.ok(name, "namespace must be given a name.") + + if (DEBUG_CLS_HOOKED) { + debug2(`NS-CREATING NAMESPACE (${name})`) + } + let namespace = new Namespace(name) + namespace.id = currentUid + + const hook = async_hooks.createHook({ + init(asyncId, type, triggerId, resource) { + currentUid = async_hooks.executionAsyncId() + + //CHAIN Parent's Context onto child if none exists. This is needed to pass net-events.spec + // let initContext = namespace.active; + // if(!initContext && triggerId) { + // let parentContext = namespace._contexts.get(triggerId); + // if (parentContext) { + // namespace.active = parentContext; + // namespace._contexts.set(currentUid, parentContext); + // if (DEBUG_CLS_HOOKED) { + // const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); + // debug2(`${indentStr}INIT [${type}] (${name}) WITH PARENT CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`); + // } + // } else if (DEBUG_CLS_HOOKED) { + // const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); + // debug2(`${indentStr}INIT [${type}] (${name}) MISSING CONTEXT asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`); + // } + // }else { + // namespace._contexts.set(currentUid, namespace.active); + // if (DEBUG_CLS_HOOKED) { + // const indentStr = ' '.repeat(namespace._indent < 0 ? 0 : namespace._indent); + // debug2(`${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect(namespace.active, true)} resource:${resource}`); + // } + // } + if (namespace.active) { + namespace._contexts.set(asyncId, namespace.active) + + if (DEBUG_CLS_HOOKED) { + const indentStr = " ".repeat( + namespace._indent < 0 ? 0 : namespace._indent + ) + debug2( + `${indentStr}INIT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( + namespace.active, + { showHidden: true, depth: 2, colors: true } + )} resource:${resource}` + ) + } + } else if (currentUid === 0) { + // CurrentId will be 0 when triggered from C++. Promise events + // https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid + const triggerId = async_hooks.triggerAsyncId() + const triggerIdContext = namespace._contexts.get(triggerId) + if (triggerIdContext) { + namespace._contexts.set(asyncId, triggerIdContext) + if (DEBUG_CLS_HOOKED) { + const indentStr = " ".repeat( + namespace._indent < 0 ? 0 : namespace._indent + ) + debug2( + `${indentStr}INIT USING CONTEXT FROM TRIGGERID [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( + namespace.active, + { showHidden: true, depth: 2, colors: true } + )} resource:${resource}` + ) + } + } else if (DEBUG_CLS_HOOKED) { + const indentStr = " ".repeat( + namespace._indent < 0 ? 0 : namespace._indent + ) + debug2( + `${indentStr}INIT MISSING CONTEXT [${type}] (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( + namespace.active, + { showHidden: true, depth: 2, colors: true } + )} resource:${resource}` + ) + } + } + + if (DEBUG_CLS_HOOKED && type === "PROMISE") { + debug2(util.inspect(resource, { showHidden: true })) + const parentId = resource.parentId + const indentStr = " ".repeat( + namespace._indent < 0 ? 0 : namespace._indent + ) + debug2( + `${indentStr}INIT RESOURCE-PROMISE [${type}] (${name}) parentId:${parentId} asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( + namespace.active, + { showHidden: true, depth: 2, colors: true } + )} resource:${resource}` + ) + } + }, + before(asyncId) { + currentUid = async_hooks.executionAsyncId() + let context + + /* + if(currentUid === 0){ + // CurrentId will be 0 when triggered from C++. Promise events + // https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid + //const triggerId = async_hooks.triggerAsyncId(); + context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId); + }else{ + context = namespace._contexts.get(currentUid); + } + */ + + //HACK to work with promises until they are fixed in node > 8.1.1 + context = + namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid) + + if (context) { + if (DEBUG_CLS_HOOKED) { + const triggerId = async_hooks.triggerAsyncId() + const indentStr = " ".repeat( + namespace._indent < 0 ? 0 : namespace._indent + ) + debug2( + `${indentStr}BEFORE (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( + namespace.active, + { showHidden: true, depth: 2, colors: true } + )} context:${util.inspect(context)}` + ) + namespace._indent += 2 + } + + namespace.enter(context) + } else if (DEBUG_CLS_HOOKED) { + const triggerId = async_hooks.triggerAsyncId() + const indentStr = " ".repeat( + namespace._indent < 0 ? 0 : namespace._indent + ) + debug2( + `${indentStr}BEFORE MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( + namespace.active, + { showHidden: true, depth: 2, colors: true } + )} namespace._contexts:${util.inspect(namespace._contexts, { + showHidden: true, + depth: 2, + colors: true, + })}` + ) + namespace._indent += 2 + } + }, + after(asyncId) { + currentUid = async_hooks.executionAsyncId() + let context // = namespace._contexts.get(currentUid); + /* + if(currentUid === 0){ + // CurrentId will be 0 when triggered from C++. Promise events + // https://github.com/nodejs/node/blob/master/doc/api/async_hooks.md#triggerid + //const triggerId = async_hooks.triggerAsyncId(); + context = namespace._contexts.get(asyncId); // || namespace._contexts.get(triggerId); + }else{ + context = namespace._contexts.get(currentUid); + } + */ + //HACK to work with promises until they are fixed in node > 8.1.1 + context = + namespace._contexts.get(asyncId) || namespace._contexts.get(currentUid) + + if (context) { + if (DEBUG_CLS_HOOKED) { + const triggerId = async_hooks.triggerAsyncId() + namespace._indent -= 2 + const indentStr = " ".repeat( + namespace._indent < 0 ? 0 : namespace._indent + ) + debug2( + `${indentStr}AFTER (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( + namespace.active, + { showHidden: true, depth: 2, colors: true } + )} context:${util.inspect(context)}` + ) + } + + namespace.exit(context) + } else if (DEBUG_CLS_HOOKED) { + const triggerId = async_hooks.triggerAsyncId() + namespace._indent -= 2 + const indentStr = " ".repeat( + namespace._indent < 0 ? 0 : namespace._indent + ) + debug2( + `${indentStr}AFTER MISSING CONTEXT (${name}) asyncId:${asyncId} currentUid:${currentUid} triggerId:${triggerId} active:${util.inspect( + namespace.active, + { showHidden: true, depth: 2, colors: true } + )} context:${util.inspect(context)}` + ) + } + }, + destroy(asyncId) { + currentUid = async_hooks.executionAsyncId() + if (DEBUG_CLS_HOOKED) { + const triggerId = async_hooks.triggerAsyncId() + const indentStr = " ".repeat( + namespace._indent < 0 ? 0 : namespace._indent + ) + debug2( + `${indentStr}DESTROY (${name}) currentUid:${currentUid} asyncId:${asyncId} triggerId:${triggerId} active:${util.inspect( + namespace.active, + { showHidden: true, depth: 2, colors: true } + )} context:${util.inspect(namespace._contexts.get(currentUid))}` + ) + } + + namespace._contexts.delete(asyncId) + }, + }) + + hook.enable() + namespace._hook = hook + + process.namespaces[name] = namespace + return namespace +} + +function destroyNamespace(name) { + let namespace = getNamespace(name) + + assert.ok(namespace, "can't delete nonexistent namespace! \"" + name + '"') + assert.ok( + namespace.id, + "don't assign to process.namespaces directly! " + util.inspect(namespace) + ) + + namespace._hook.disable() + namespace._contexts = null + process.namespaces[name] = null +} + +function reset() { + // must unregister async listeners + if (process.namespaces) { + Object.keys(process.namespaces).forEach(function (name) { + destroyNamespace(name) + }) + } + process.namespaces = Object.create(null) +} + +process.namespaces = process.namespaces || {} + +//const fs = require('fs'); +function debug2(...args) { + if (DEBUG_CLS_HOOKED) { + //fs.writeSync(1, `${util.format(...args)}\n`); + process._rawDebug(`${util.format(...args)}`) + } +} + +/*function getFunctionName(fn) { + if (!fn) { + return fn; + } + if (typeof fn === 'function') { + if (fn.name) { + return fn.name; + } + return (fn.toString().trim().match(/^function\s*([^\s(]+)/) || [])[1]; + } else if (fn.constructor && fn.constructor.name) { + return fn.constructor.name; + } +}*/ diff --git a/packages/backend-core/src/context/FunctionContext.js b/packages/backend-core/src/context/FunctionContext.js index 34d39492f9..c0ed34fe78 100644 --- a/packages/backend-core/src/context/FunctionContext.js +++ b/packages/backend-core/src/context/FunctionContext.js @@ -1,84 +1,47 @@ -const cls = require("cls-hooked") +const cls = require("../clshooked") const { newid } = require("../hashing") const REQUEST_ID_KEY = "requestId" +const MAIN_CTX = cls.createNamespace("main") + +function getContextStorage(namespace) { + if (namespace && namespace.active) { + let contextData = namespace.active + delete contextData.id + delete contextData._ns_name + return contextData + } + return {} +} class FunctionContext { - static getMiddleware( - updateCtxFn = null, - destroyFn = null, - contextName = "session" - ) { - const namespace = this.createNamespace(contextName) - - return async function (ctx, next) { - await new Promise( - namespace.bind(function (resolve, reject) { - // store a contextual request ID that can be used anywhere (audit logs) - namespace.set(REQUEST_ID_KEY, newid()) - namespace.bindEmitter(ctx.req) - namespace.bindEmitter(ctx.res) - - if (updateCtxFn) { - updateCtxFn(ctx) - } - next() - .then(resolve) - .catch(reject) - .finally(() => { - if (destroyFn) { - return destroyFn(ctx) - } - }) - }) - ) - } + static run(callback) { + return MAIN_CTX.runAndReturn(async () => { + const namespaceId = newid() + MAIN_CTX.set(REQUEST_ID_KEY, namespaceId) + const namespace = cls.createNamespace(namespaceId) + let response = await namespace.runAndReturn(callback) + cls.destroyNamespace(namespaceId) + return response + }) } - static run(callback, contextName = "session") { - const namespace = this.createNamespace(contextName) - - return namespace.runAndReturn(callback) - } - - static setOnContext(key, value, contextName = "session") { - const namespace = this.createNamespace(contextName) + static setOnContext(key, value) { + const namespaceId = MAIN_CTX.get(REQUEST_ID_KEY) + const namespace = cls.getNamespace(namespaceId) namespace.set(key, value) } - static getContextStorage() { - if (this._namespace && this._namespace.active) { - let contextData = this._namespace.active - delete contextData.id - delete contextData._ns_name - return contextData - } - - return {} - } - static getFromContext(key) { - const context = this.getContextStorage() + const namespaceId = MAIN_CTX.get(REQUEST_ID_KEY) + const namespace = cls.getNamespace(namespaceId) + const context = getContextStorage(namespace) if (context) { return context[key] } else { return null } } - - static destroyNamespace(name = "session") { - if (this._namespace) { - cls.destroyNamespace(name) - this._namespace = null - } - } - - static createNamespace(name = "session") { - if (!this._namespace) { - this._namespace = cls.createNamespace(name) - } - return this._namespace - } } module.exports = FunctionContext diff --git a/packages/backend-core/src/context/index.js b/packages/backend-core/src/context/index.js index b6b6f2380c..20e5e26693 100644 --- a/packages/backend-core/src/context/index.js +++ b/packages/backend-core/src/context/index.js @@ -55,6 +55,15 @@ async function closeAppDBs() { } } +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 } @@ -82,12 +91,7 @@ exports.doInTenant = (tenantId, task) => { } finally { const using = cls.getFromContext(ContextKeys.IN_USE) if (!using || using <= 1) { - 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) + await exports.closeTenancy() } else { cls.setOnContext(using - 1) } diff --git a/packages/backend-core/src/middleware/tenancy.js b/packages/backend-core/src/middleware/tenancy.js index f4053d1f5b..9a0cb8a0c6 100644 --- a/packages/backend-core/src/middleware/tenancy.js +++ b/packages/backend-core/src/middleware/tenancy.js @@ -1,6 +1,5 @@ -const { setTenantId, setGlobalDB, getGlobalDB } = require("../tenancy") -const { closeDB } = require("../db") -const ContextFactory = require("../context/FunctionContext") +const { setTenantId, setGlobalDB, closeTenancy } = require("../tenancy") +const cls = require("../context/FunctionContext") const { buildMatcherRegex, matches } = require("./matchers") module.exports = ( @@ -11,17 +10,16 @@ module.exports = ( const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns) const noTenancyOptions = buildMatcherRegex(noTenancyPatterns) - const updateCtxFn = ctx => { - const allowNoTenant = - opts.noTenancyRequired || !!matches(ctx, noTenancyOptions) - const allowQs = !!matches(ctx, allowQsOptions) - const tenantId = setTenantId(ctx, { allowQs, allowNoTenant }) - setGlobalDB(tenantId) + return async function (ctx, next) { + return cls.run(async () => { + const allowNoTenant = + opts.noTenancyRequired || !!matches(ctx, noTenancyOptions) + const allowQs = !!matches(ctx, allowQsOptions) + const tenantId = setTenantId(ctx, { allowQs, allowNoTenant }) + setGlobalDB(tenantId) + const res = await next() + await closeTenancy() + return res + }) } - const destroyFn = async () => { - const db = getGlobalDB() - await closeDB(db) - } - - return ContextFactory.getMiddleware(updateCtxFn, destroyFn) } diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock index 87db3761bc..fff682df50 100644 --- a/packages/backend-core/yarn.lock +++ b/packages/backend-core/yarn.lock @@ -1533,7 +1533,7 @@ electron-to-chromium@^1.3.896: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.900.tgz#5be2c5818a2a012c511b4b43e87b6ab7a296d4f5" integrity sha512-SuXbQD8D4EjsaBaJJxySHbC+zq8JrFfxtb4GIr4E9n1BcROyMcRrJCYQNpJ9N+Wjf5mFp7Wp0OHykd14JNEzzQ== -emitter-listener@^1.0.1: +emitter-listener@^1.0.1, emitter-listener@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8" integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ== diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 9cd9f2cbba..aef17d69a8 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "1.0.151-alpha.2", + "version": "1.0.155-alpha.0", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "^1.2.1", - "@budibase/string-templates": "^1.0.151-alpha.2", + "@budibase/string-templates": "^1.0.155-alpha.0", "@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", diff --git a/packages/builder/package.json b/packages/builder/package.json index 5a58f2066e..27259c003a 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "1.0.151-alpha.2", + "version": "1.0.155-alpha.0", "license": "GPL-3.0", "private": true, "scripts": { @@ -67,10 +67,10 @@ } }, "dependencies": { - "@budibase/bbui": "^1.0.151-alpha.2", - "@budibase/client": "^1.0.151-alpha.2", - "@budibase/frontend-core": "^1.0.151-alpha.2", - "@budibase/string-templates": "^1.0.151-alpha.2", + "@budibase/bbui": "^1.0.155-alpha.0", + "@budibase/client": "^1.0.155-alpha.0", + "@budibase/frontend-core": "^1.0.155-alpha.0", + "@budibase/string-templates": "^1.0.155-alpha.0", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/_components/DynamicVariableModal.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/_components/DynamicVariableModal.svelte index 61d0a1993c..5c0cd0f883 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/_components/DynamicVariableModal.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/_components/DynamicVariableModal.svelte @@ -1,5 +1,8 @@ diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte index e870c2f6db..d2914146ce 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte @@ -37,7 +37,7 @@ import AccessLevelSelect from "components/integration/AccessLevelSelect.svelte" import DynamicVariableModal from "../../_components/DynamicVariableModal.svelte" import Placeholder from "assets/bb-spaceship.svg" - import { cloneDeep } from "lodash/fp" + import { cloneDeep, isEqual } from "lodash/fp" import { RawRestBodyTypes } from "constants/backend" let query, datasource @@ -47,6 +47,7 @@ let response, schema, enabledHeaders let authConfigId let dynamicVariables, addVariableModal, varBinding + let baseQuery, baseDatasource, baseVariables $: datasourceType = datasource?.source $: integrationInfo = $integrations[datasourceType] @@ -62,6 +63,15 @@ $: hasSchema = Object.keys(schema || {}).length !== 0 || Object.keys(query?.schema || {}).length !== 0 + $: baseQuery = !baseQuery ? cloneDeep(query) : baseQuery + $: baseDatasource = !baseDatasource ? cloneDeep(datasource) : baseDatasource + $: baseVariables = !baseVariables + ? cloneDeep(dynamicVariables) + : baseVariables + $: hasChanged = + !isEqual(baseQuery, query) || + !isEqual(baseDatasource, datasource) || + !isEqual(baseVariables, dynamicVariables) function getSelectedQuery() { return cloneDeep( @@ -120,6 +130,9 @@ datasource.config.dynamicVariables = rebuildVariables(saveId) datasource = await datasources.save(datasource) } + baseQuery = query + baseDatasource = datasource + baseVariables = dynamicVariables } catch (err) { notifications.error(`Error saving query`) } @@ -299,6 +312,7 @@ {dynamicVariables} bind:binding={varBinding} bind:this={addVariableModal} + on:change={saveQuery} /> {#if query && queryConfig}
@@ -332,7 +346,7 @@