Merge branch 'master' into crud-quotas

This commit is contained in:
Rory Powell 2022-05-11 11:42:26 +01:00
commit f55a3bfa3d
23 changed files with 850 additions and 159 deletions

View File

@ -1,5 +1,5 @@
{
"version": "1.0.149",
"version": "1.0.151",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "1.0.149",
"version": "1.0.151",
"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",

View File

@ -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;
}
}*/

View File

@ -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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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==

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "1.0.149",
"version": "1.0.151",
"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.149",
"@budibase/string-templates": "^1.0.151",
"@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2",

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "1.0.149",
"version": "1.0.151",
"license": "GPL-3.0",
"private": true,
"scripts": {
@ -67,10 +67,10 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.0.149",
"@budibase/client": "^1.0.149",
"@budibase/frontend-core": "^1.0.149",
"@budibase/string-templates": "^1.0.149",
"@budibase/bbui": "^1.0.151",
"@budibase/client": "^1.0.151",
"@budibase/frontend-core": "^1.0.151",
"@budibase/string-templates": "^1.0.151",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",

View File

@ -1,5 +1,8 @@
<script>
import { Input, ModalContent, Modal, Body } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let dynamicVariables
export let datasource
@ -35,6 +38,7 @@
name = null
binding = null
dynamicVariables[copiedName] = copiedBinding
dispatch("change", dynamicVariables)
}
</script>

View File

@ -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}
<div class="inner">
@ -332,7 +346,7 @@
</div>
<Button primary disabled={!url} on:click={runQuery}>Send</Button>
<Button
disabled={!query.name}
disabled={!query.name || !hasChanged}
cta
on:click={saveQuery}
tooltip={!hasSchema

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/cli",
"version": "1.0.149",
"version": "1.0.151",
"description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js",
"bin": {

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "1.0.149",
"version": "1.0.151",
"license": "MPL-2.0",
"module": "dist/budibase-client.js",
"main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw"
},
"dependencies": {
"@budibase/bbui": "^1.0.149",
"@budibase/frontend-core": "^1.0.149",
"@budibase/string-templates": "^1.0.149",
"@budibase/bbui": "^1.0.151",
"@budibase/frontend-core": "^1.0.151",
"@budibase/string-templates": "^1.0.151",
"@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3",

View File

@ -1,12 +1,12 @@
{
"name": "@budibase/frontend-core",
"version": "1.0.149",
"version": "1.0.151",
"description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase",
"license": "MPL-2.0",
"svelte": "src/index.js",
"dependencies": {
"@budibase/bbui": "^1.0.149",
"@budibase/bbui": "^1.0.151",
"lodash": "^4.17.21",
"svelte": "^3.46.2"
}

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "1.0.149",
"version": "1.0.151",
"description": "Budibase Web Server",
"main": "src/index.ts",
"repository": {
@ -10,6 +10,7 @@
},
"scripts": {
"build": "rimraf dist/ && tsc -p tsconfig.build.json && mv dist/src/* dist/ && rimraf dist/src/ && yarn postbuild",
"debug": "yarn build && node --expose-gc --inspect=9222 dist/index.js",
"postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/",
"test": "jest --coverage --maxWorkers=2",
"test:watch": "jest --watch",
@ -68,10 +69,10 @@
"license": "GPL-3.0",
"dependencies": {
"@apidevtools/swagger-parser": "^10.0.3",
"@budibase/backend-core": "^1.0.149",
"@budibase/client": "^1.0.149",
"@budibase/pro": "1.0.149",
"@budibase/string-templates": "^1.0.149",
"@budibase/backend-core": "^1.0.151",
"@budibase/client": "^1.0.151",
"@budibase/pro": "1.0.151",
"@budibase/string-templates": "^1.0.151",
"@bull-board/api": "^3.7.0",
"@bull-board/koa": "^3.7.0",
"@elastic/elasticsearch": "7.10.0",

View File

@ -21,6 +21,31 @@ type KnexQuery = Knex.QueryBuilder | Knex
const MIN_ISO_DATE = "0000-00-00T00:00:00.000Z"
const MAX_ISO_DATE = "9999-00-00T00:00:00.000Z"
function likeKey(client: string, key: string): string {
if (!key.includes(" ")) {
return key
}
let start: string, end: string
switch (client) {
case SqlClients.MY_SQL:
start = end = "`"
break
case SqlClients.ORACLE:
case SqlClients.POSTGRES:
start = end = '"'
break
case SqlClients.MS_SQL:
start = "["
end = "]"
break
default:
throw "Unknown client"
}
const parts = key.split(".")
key = parts.map(part => `${start}${part}${end}`).join(".")
return key
}
function parse(input: any) {
if (Array.isArray(input)) {
return JSON.stringify(input)
@ -125,7 +150,9 @@ class InternalBuilder {
} else {
const rawFnc = `${fnc}Raw`
// @ts-ignore
query = query[rawFnc](`LOWER(${key}) LIKE ?`, [`%${value}%`])
query = query[rawFnc](`LOWER(${likeKey(this.client, key)}) LIKE ?`, [
`%${value}%`,
])
}
})
}

View File

@ -147,7 +147,7 @@ module PostgresModule {
return parts.join(" || ")
}
async internalQuery(query: SqlQuery) {
async internalQuery(query: SqlQuery, close: boolean = true) {
const client = this.client
this.index = 1
// need to handle a specific issue with json data types in postgres,
@ -164,10 +164,11 @@ module PostgresModule {
try {
return await client.query(query.sql, query.bindings || [])
} catch (err) {
await this.client.end()
// @ts-ignore
throw new Error(err)
} finally {
await this.client.end()
if (close) await this.client.end()
}
}
@ -204,48 +205,54 @@ module PostgresModule {
}
} catch (err) {
tableKeys = {}
} finally {
await this.client.close()
}
const columnsResponse = await this.client.query(this.COLUMNS_SQL)
const tables: { [key: string]: Table } = {}
try {
const columnsResponse = await this.client.query(this.COLUMNS_SQL)
for (let column of columnsResponse.rows) {
const tableName: string = column.table_name
const columnName: string = column.column_name
const tables: { [key: string]: Table } = {}
// table key doesn't exist yet
if (!tables[tableName] || !tables[tableName].schema) {
tables[tableName] = {
_id: buildExternalTableId(datasourceId, tableName),
primary: tableKeys[tableName] || [],
name: tableName,
schema: {},
for (let column of columnsResponse.rows) {
const tableName: string = column.table_name
const columnName: string = column.column_name
// table key doesn't exist yet
if (!tables[tableName] || !tables[tableName].schema) {
tables[tableName] = {
_id: buildExternalTableId(datasourceId, tableName),
primary: tableKeys[tableName] || [],
name: tableName,
schema: {},
}
}
const identity = !!(
column.identity_generation ||
column.identity_start ||
column.identity_increment
)
const hasDefault =
typeof column.column_default === "string" &&
column.column_default.startsWith("nextval")
const isGenerated =
column.is_generated && column.is_generated !== "NEVER"
const isAuto: boolean = hasDefault || identity || isGenerated
tables[tableName].schema[columnName] = {
autocolumn: isAuto,
name: columnName,
...convertSqlType(column.data_type),
}
}
const identity = !!(
column.identity_generation ||
column.identity_start ||
column.identity_increment
)
const hasDefault =
typeof column.column_default === "string" &&
column.column_default.startsWith("nextval")
const isGenerated =
column.is_generated && column.is_generated !== "NEVER"
const isAuto: boolean = hasDefault || identity || isGenerated
tables[tableName].schema[columnName] = {
autocolumn: isAuto,
name: columnName,
...convertSqlType(column.data_type),
}
const final = finaliseExternalTables(tables, entities)
this.tables = final.tables
this.schemaErrors = final.errors
} catch (err) {
// @ts-ignore
throw new Error(err)
} finally {
await this.client.end()
}
const final = finaliseExternalTables(tables, entities)
this.tables = final.tables
this.schemaErrors = final.errors
}
async create(query: SqlQuery | string) {
@ -274,8 +281,9 @@ module PostgresModule {
if (Array.isArray(input)) {
const responses = []
for (let query of input) {
responses.push(await this.internalQuery(query))
responses.push(await this.internalQuery(query, false))
}
await this.client.end()
return responses
} else {
const response = await this.internalQuery(input)

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
"version": "1.0.149",
"version": "1.0.151",
"description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs",
"module": "dist/bundle.mjs",

View File

@ -13,6 +13,16 @@ const HTML_SWAPS = {
">": "&gt;",
}
function isObject(value) {
if (value == null || typeof value !== "object") {
return false
}
return (
value.toString() === "[object Object]" ||
(value.length > 0 && typeof value[0] === "object")
)
}
const HELPERS = [
// external helpers
new Helper(HelperFunctionNames.OBJECT, value => {
@ -22,11 +32,7 @@ const HELPERS = [
new Helper(HelperFunctionNames.JS, processJS, false),
// this help is applied to all statements
new Helper(HelperFunctionNames.ALL, (value, { __opts }) => {
if (
value != null &&
typeof value === "object" &&
value.toString() === "[object Object]"
) {
if (isObject(value)) {
return new SafeString(JSON.stringify(value))
}
// null/undefined values produce bad results

View File

@ -64,9 +64,10 @@ module.exports.processors = [
return statement
}
}
const testHelper = possibleHelper.trim().toLowerCase()
if (
!noHelpers &&
HelperNames().some(option => option.includes(possibleHelper))
HelperNames().some(option => testHelper === option.toLowerCase())
) {
insideStatement = `(${insideStatement})`
}

View File

@ -106,6 +106,16 @@ describe("Test that the object processing works correctly", () => {
})
})
describe("check returning objects", () => {
it("should handle an array of objects", async () => {
const json = [{a: 1},{a: 2}]
const output = await processString("{{ testing }}", {
testing: json
})
expect(output).toEqual(JSON.stringify(json))
})
})
describe("check the utility functions", () => {
it("should return false for an invalid template string", () => {
const valid = isValid("{{ table1.thing prop }}")

View File

@ -30,6 +30,11 @@ describe("Handling context properties with spaces in their name", () => {
})
expect(output).toBe("testcase 1")
})
it("should allow the use of a", async () => {
const output = await processString("{{ a }}", { a: 1 })
expect(output).toEqual("1")
})
})
describe("attempt some complex problems", () => {

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/worker",
"email": "hi@budibase.com",
"version": "1.0.149",
"version": "1.0.151",
"description": "Budibase background service",
"main": "src/index.ts",
"repository": {
@ -31,9 +31,9 @@
"author": "Budibase",
"license": "GPL-3.0",
"dependencies": {
"@budibase/backend-core": "^1.0.149",
"@budibase/pro": "1.0.149",
"@budibase/string-templates": "^1.0.149",
"@budibase/backend-core": "^1.0.151",
"@budibase/pro": "1.0.151",
"@budibase/string-templates": "^1.0.151",
"@koa/router": "^8.0.0",
"@sentry/node": "6.17.7",
"@techpass/passport-openidconnect": "^0.3.0",