Merge branch 'develop' of github.com:Budibase/budibase into feature/automation-logs

This commit is contained in:
mike12345567 2022-05-16 12:33:01 +01:00
commit 8a6b9646fd
46 changed files with 1231 additions and 323 deletions

View File

@ -1,4 +1,5 @@
name: Budibase Release Staging name: Budibase Release Staging
concurrency: release-develop
on: on:
push: push:

View File

@ -87,3 +87,10 @@ jobs:
packages/cli/build/cli-macos packages/cli/build/cli-macos
packages/server/specs/openapi.yaml packages/server/specs/openapi.yaml
packages/server/specs/openapi.json packages/server/specs/openapi.json
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0
with:
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
content: "Self Host Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Self Host."
embed-title: ${{ env.RELEASE_VERSION }}

View File

@ -1,4 +1,5 @@
name: Budibase Release name: Budibase Release
concurrency: release
on: on:
push: push:

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.151-alpha.2", "version": "1.0.159-alpha.1",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "1.0.151-alpha.2", "version": "1.0.159-alpha.1",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "src/index.js", "main": "src/index.js",
"author": "Budibase", "author": "Budibase",
@ -13,7 +13,7 @@
"@techpass/passport-openidconnect": "^0.3.0", "@techpass/passport-openidconnect": "^0.3.0",
"aws-sdk": "^2.901.0", "aws-sdk": "^2.901.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cls-hooked": "^4.2.2", "emitter-listener": "^1.1.2",
"ioredis": "^4.27.1", "ioredis": "^4.27.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"koa-passport": "^4.1.4", "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 { newid } = require("../hashing")
const REQUEST_ID_KEY = "requestId" 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 { class FunctionContext {
static getMiddleware( static run(callback) {
updateCtxFn = null, return MAIN_CTX.runAndReturn(async () => {
destroyFn = null, const namespaceId = newid()
contextName = "session" MAIN_CTX.set(REQUEST_ID_KEY, namespaceId)
) { const namespace = cls.createNamespace(namespaceId)
const namespace = this.createNamespace(contextName) let response = await namespace.runAndReturn(callback)
cls.destroyNamespace(namespaceId)
return async function (ctx, next) { return response
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, contextName = "session") { static setOnContext(key, value) {
const namespace = this.createNamespace(contextName) const namespaceId = MAIN_CTX.get(REQUEST_ID_KEY)
const namespace = cls.getNamespace(namespaceId)
return namespace.runAndReturn(callback)
}
static setOnContext(key, value, contextName = "session") {
const namespace = this.createNamespace(contextName)
namespace.set(key, value) 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) { 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) { if (context) {
return context[key] return context[key]
} else { } else {
return null 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 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 = () => { exports.isDefaultTenant = () => {
return exports.getTenantId() === exports.DEFAULT_TENANT_ID return exports.getTenantId() === exports.DEFAULT_TENANT_ID
} }
@ -82,12 +91,7 @@ exports.doInTenant = (tenantId, task) => {
} finally { } finally {
const using = cls.getFromContext(ContextKeys.IN_USE) const using = cls.getFromContext(ContextKeys.IN_USE)
if (!using || using <= 1) { if (!using || using <= 1) {
if (env.USE_COUCH) { await exports.closeTenancy()
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)
} else { } else {
cls.setOnContext(using - 1) cls.setOnContext(using - 1)
} }

View File

@ -1,6 +1,5 @@
const { setTenantId, setGlobalDB, getGlobalDB } = require("../tenancy") const { setTenantId, setGlobalDB, closeTenancy } = require("../tenancy")
const { closeDB } = require("../db") const cls = require("../context/FunctionContext")
const ContextFactory = require("../context/FunctionContext")
const { buildMatcherRegex, matches } = require("./matchers") const { buildMatcherRegex, matches } = require("./matchers")
module.exports = ( module.exports = (
@ -11,17 +10,16 @@ module.exports = (
const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns) const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns)
const noTenancyOptions = buildMatcherRegex(noTenancyPatterns) const noTenancyOptions = buildMatcherRegex(noTenancyPatterns)
const updateCtxFn = ctx => { return async function (ctx, next) {
const allowNoTenant = return cls.run(async () => {
opts.noTenancyRequired || !!matches(ctx, noTenancyOptions) const allowNoTenant =
const allowQs = !!matches(ctx, allowQsOptions) opts.noTenancyRequired || !!matches(ctx, noTenancyOptions)
const tenantId = setTenantId(ctx, { allowQs, allowNoTenant }) const allowQs = !!matches(ctx, allowQsOptions)
setGlobalDB(tenantId) 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

@ -96,6 +96,7 @@ const BUILTIN_PERMISSIONS = {
new Permission(PermissionTypes.QUERY, PermissionLevels.WRITE), new Permission(PermissionTypes.QUERY, PermissionLevels.WRITE),
new Permission(PermissionTypes.TABLE, PermissionLevels.WRITE), new Permission(PermissionTypes.TABLE, PermissionLevels.WRITE),
new Permission(PermissionTypes.VIEW, PermissionLevels.READ), new Permission(PermissionTypes.VIEW, PermissionLevels.READ),
new Permission(PermissionTypes.AUTOMATION, PermissionLevels.EXECUTE),
], ],
}, },
POWER: { POWER: {

View File

@ -805,13 +805,6 @@ ast-types@0.9.6:
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9"
integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk= integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=
async-hook-jl@^1.7.6:
version "1.7.6"
resolved "https://registry.yarnpkg.com/async-hook-jl/-/async-hook-jl-1.7.6.tgz#4fd25c2f864dbaf279c610d73bf97b1b28595e68"
integrity sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==
dependencies:
stack-chain "^1.3.7"
async@~2.1.4: async@~2.1.4:
version "2.1.5" version "2.1.5"
resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc" resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc"
@ -1205,15 +1198,6 @@ clone-buffer@1.0.0:
resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg=
cls-hooked@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/cls-hooked/-/cls-hooked-4.2.2.tgz#ad2e9a4092680cdaffeb2d3551da0e225eae1908"
integrity sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==
dependencies:
async-hook-jl "^1.7.6"
emitter-listener "^1.0.1"
semver "^5.4.1"
cluster-key-slot@^1.1.0: cluster-key-slot@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
@ -1533,7 +1517,7 @@ electron-to-chromium@^1.3.896:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.900.tgz#5be2c5818a2a012c511b4b43e87b6ab7a296d4f5" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.900.tgz#5be2c5818a2a012c511b4b43e87b6ab7a296d4f5"
integrity sha512-SuXbQD8D4EjsaBaJJxySHbC+zq8JrFfxtb4GIr4E9n1BcROyMcRrJCYQNpJ9N+Wjf5mFp7Wp0OHykd14JNEzzQ== integrity sha512-SuXbQD8D4EjsaBaJJxySHbC+zq8JrFfxtb4GIr4E9n1BcROyMcRrJCYQNpJ9N+Wjf5mFp7Wp0OHykd14JNEzzQ==
emitter-listener@^1.0.1: emitter-listener@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8" resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8"
integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ== integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==
@ -4466,7 +4450,7 @@ saxes@^5.0.1:
dependencies: dependencies:
xmlchars "^2.2.0" xmlchars "^2.2.0"
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: "semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0:
version "5.7.1" version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@ -4706,11 +4690,6 @@ sshpk@^1.7.0:
jsbn "~0.1.0" jsbn "~0.1.0"
tweetnacl "~0.14.0" tweetnacl "~0.14.0"
stack-chain@^1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285"
integrity sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=
stack-utils@^2.0.2: stack-utils@^2.0.2:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5"

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "1.0.151-alpha.2", "version": "1.0.159-alpha.1",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
], ],
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1", "@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "^1.0.151-alpha.2", "@budibase/string-templates": "^1.0.159-alpha.1",
"@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2", "@spectrum-css/avatar": "^3.0.2",

View File

@ -0,0 +1,68 @@
<script>
import "@spectrum-css/fieldgroup/dist/index-vars.css"
import "@spectrum-css/radio/dist/index-vars.css"
import { createEventDispatcher } from "svelte"
export let direction = "vertical"
export let value = []
export let options = []
export let error = null
export let disabled = false
export let getOptionLabel = option => option
export let getOptionValue = option => option
const dispatch = createEventDispatcher()
const onChange = e => {
let tempValue = value
let isChecked = e.target.checked
if (!tempValue.includes(e.target.value) && isChecked) {
tempValue.push(e.target.value)
}
value = tempValue
dispatch(
"change",
tempValue.filter(val => val !== e.target.value || isChecked)
)
}
</script>
<div class={`spectrum-FieldGroup spectrum-FieldGroup--${direction}`}>
{#if options && Array.isArray(options)}
{#each options as option}
<div
title={getOptionLabel(option)}
class="spectrum-Checkbox spectrum-FieldGroup-item"
class:is-invalid={!!error}
>
<label
class="spectrum-Checkbox spectrum-Checkbox--sizeM spectrum-FieldGroup-item"
>
<input
on:change={onChange}
value={getOptionValue(option)}
type="checkbox"
class="spectrum-Checkbox-input"
{disabled}
checked={value.includes(getOptionValue(option))}
/>
<span class="spectrum-Checkbox-box">
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Checkbox-checkmark"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</span>
<span class="spectrum-Checkbox-label">{getOptionLabel(option)}</span>
</label>
</div>
{/each}
{/if}
</div>
<style>
.spectrum-Checkbox-input {
opacity: 0;
}
</style>

View File

@ -156,8 +156,8 @@
<input <input
data-input data-input
type="text" type="text"
{disabled}
class="spectrum-Textfield-input spectrum-InputGroup-input" class="spectrum-Textfield-input spectrum-InputGroup-input"
class:is-disabled={disabled}
{placeholder} {placeholder}
{id} {id}
{value} {value}
@ -167,7 +167,7 @@
type="button" type="button"
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button" class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
tabindex="-1" tabindex="-1"
{disabled} class:is-disabled={disabled}
class:is-invalid={!!error} class:is-invalid={!!error}
on:click={flatpickr?.open} on:click={flatpickr?.open}
> >
@ -212,4 +212,7 @@
:global(.flatpickr-calendar) { :global(.flatpickr-calendar) {
font-family: "Source Sans Pro", sans-serif; font-family: "Source Sans Pro", sans-serif;
} }
.is-disabled {
pointer-events: none !important;
}
</style> </style>

View File

@ -43,7 +43,7 @@
return return
} }
searchTerm = null searchTerm = null
open = true open = !open
} }
const getSortedOptions = (options, getLabel, sort) => { const getSortedOptions = (options, getLabel, sort) => {
@ -71,105 +71,73 @@
} }
</script> </script>
<button <div use:clickOutside={() => (open = false)}>
{id} <button
class="spectrum-Picker spectrum-Picker--sizeM" {id}
class:spectrum-Picker--quiet={quiet} class="spectrum-Picker spectrum-Picker--sizeM"
{disabled} class:spectrum-Picker--quiet={quiet}
class:is-invalid={!!error} {disabled}
class:is-open={open} class:is-invalid={!!error}
aria-haspopup="listbox" class:is-open={open}
on:mousedown={onClick} aria-haspopup="listbox"
> on:mousedown={onClick}
{#if fieldIcon}
<span class="icon-Placeholder-Padding">
<img src={fieldIcon} alt="icon" width="20" height="15" />
</span>
{/if}
<span
class="spectrum-Picker-label"
class:is-placeholder={isPlaceholder}
class:auto-width={autoWidth}
> >
{fieldText} {#if fieldIcon}
</span> <span class="icon-Placeholder-Padding">
{#if error} <img src={fieldIcon} alt="icon" width="20" height="15" />
</span>
{/if}
<span
class="spectrum-Picker-label"
class:is-placeholder={isPlaceholder}
class:auto-width={autoWidth}
>
{fieldText}
</span>
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
focusable="false"
aria-hidden="true"
aria-label="Folder"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<svg <svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon" class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
focusable="false" focusable="false"
aria-hidden="true" aria-hidden="true"
aria-label="Folder"
> >
<use xlink:href="#spectrum-icon-18-Alert" /> <use xlink:href="#spectrum-css-icon-Chevron100" />
</svg> </svg>
{/if} </button>
<svg {#if open}
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon" <div
focusable="false" transition:fly|local={{ y: -20, duration: 200 }}
aria-hidden="true" class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
> class:auto-width={autoWidth}
<use xlink:href="#spectrum-css-icon-Chevron100" /> >
</svg> {#if autocomplete}
</button> <Search
{#if open} value={searchTerm}
<div on:change={event => (searchTerm = event.detail)}
use:clickOutside={() => (open = false)} {disabled}
transition:fly|local={{ y: -20, duration: 200 }} placeholder="Search"
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" />
class:auto-width={autoWidth}
>
{#if autocomplete}
<Search
value={searchTerm}
on:change={event => (searchTerm = event.detail)}
{disabled}
placeholder="Search"
/>
{/if}
<ul class="spectrum-Menu" role="listbox">
{#if placeholderOption}
<li
class="spectrum-Menu-item placeholder"
class:is-selected={isPlaceholder}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onSelectOption(null)}
>
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/if} {/if}
{#if filteredOptions.length} <ul class="spectrum-Menu" role="listbox">
{#each filteredOptions as option, idx} {#if placeholderOption}
<li <li
class="spectrum-Menu-item" class="spectrum-Menu-item placeholder"
class:is-selected={isOptionSelected(getOptionValue(option, idx))} class:is-selected={isPlaceholder}
role="option" role="option"
aria-selected="true" aria-selected="true"
tabindex="0" tabindex="0"
on:click={() => onSelectOption(getOptionValue(option, idx))} on:click={() => onSelectOption(null)}
> >
{#if getOptionIcon(option, idx)} <span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
<span class="icon-Padding">
<img
src={getOptionIcon(option, idx)}
alt="icon"
width="20"
height="15"
/>
</span>
{/if}
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
</span>
<svg <svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon" class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false" focusable="false"
@ -178,11 +146,44 @@
<use xlink:href="#spectrum-css-icon-Checkmark100" /> <use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg> </svg>
</li> </li>
{/each} {/if}
{/if} {#if filteredOptions.length}
</ul> {#each filteredOptions as option, idx}
</div> <li
{/if} class="spectrum-Menu-item"
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onSelectOption(getOptionValue(option, idx))}
>
{#if getOptionIcon(option, idx)}
<span class="icon-Padding">
<img
src={getOptionIcon(option, idx)}
alt="icon"
width="20"
height="15"
/>
</span>
{/if}
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/each}
{/if}
</ul>
</div>
{/if}
</div>
<style> <style>
.spectrum-Popover { .spectrum-Popover {

View File

@ -3,6 +3,7 @@ export { default as CoreSelect } from "./Select.svelte"
export { default as CoreMultiselect } from "./Multiselect.svelte" export { default as CoreMultiselect } from "./Multiselect.svelte"
export { default as CoreCheckbox } from "./Checkbox.svelte" export { default as CoreCheckbox } from "./Checkbox.svelte"
export { default as CoreRadioGroup } from "./RadioGroup.svelte" export { default as CoreRadioGroup } from "./RadioGroup.svelte"
export { default as CoreCheckboxGroup } from "./CheckboxGroup.svelte"
export { default as CoreTextArea } from "./TextArea.svelte" export { default as CoreTextArea } from "./TextArea.svelte"
export { default as CoreCombobox } from "./Combobox.svelte" export { default as CoreCombobox } from "./Combobox.svelte"
export { default as CoreSwitch } from "./Switch.svelte" export { default as CoreSwitch } from "./Switch.svelte"

View File

@ -1,6 +1,6 @@
{ {
"baseUrl": "http://localhost:4100", "baseUrl": "http://localhost:4100",
"video": false, "video": true,
"projectId": "bmbemn", "projectId": "bmbemn",
"env": { "env": {
"PORT": "4100", "PORT": "4100",

View File

@ -135,7 +135,7 @@ filterTests(['smoke', 'all'], () => {
cy.wait(5000) cy.wait(5000)
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(1000) cy.wait(2000)
cy.applicationInAppTable(templateNameText) cy.applicationInAppTable(templateNameText)
cy.deleteApp(templateNameText) cy.deleteApp(templateNameText)

View File

@ -24,7 +24,7 @@ filterTests(['smoke', 'all'], () => {
}) })
}) })
it("should add a URL param binding", () => { xit("should add a URL param binding", () => {
const paramName = "foo" const paramName = "foo"
cy.createScreen(`/test/:${paramName}`) cy.createScreen(`/test/:${paramName}`)
cy.addComponent("Elements", "Paragraph").then(componentId => { cy.addComponent("Elements", "Paragraph").then(componentId => {

View File

@ -11,7 +11,7 @@ filterTests(["all"], () => {
const queryName = "Cypress Test Query" const queryName = "Cypress Test Query"
const queryRename = "CT Query Rename" const queryRename = "CT Query Rename"
it("Should add PostgreSQL data source without configuration", () => { xit("Should add PostgreSQL data source without configuration", () => {
// Select PostgreSQL data source // Select PostgreSQL data source
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
// Attempt to fetch tables without applying configuration // Attempt to fetch tables without applying configuration
@ -107,7 +107,7 @@ filterTests(["all"], () => {
}) })
it("should delete a relationship", () => { it("should delete a relationship", () => {
cy.get(".hierarchy-items-container").contains("PostgreSQL-2").click() cy.get(".hierarchy-items-container").contains("PostgreSQL").click()
cy.reload() cy.reload()
// Delete one relationship // Delete one relationship
cy.get(".spectrum-Table") cy.get(".spectrum-Table")
@ -155,7 +155,7 @@ filterTests(["all"], () => {
it("should switch to schema with no tables", () => { it("should switch to schema with no tables", () => {
// Switch Schema - To one without any tables // Switch Schema - To one without any tables
cy.get(".hierarchy-items-container").contains("PostgreSQL-2").click() cy.get(".hierarchy-items-container").contains("PostgreSQL").click()
switchSchema("randomText") switchSchema("randomText")
// No tables displayed // No tables displayed

View File

@ -18,14 +18,14 @@ Cypress.Commands.add("login", () => {
cy.get("input").first().type("test@test.com") cy.get("input").first().type("test@test.com")
cy.get('input[type="password"]').first().type("test") cy.get('input[type="password"]').first().type("test")
cy.get('input[type="password"]').eq(1).type("test") cy.get('input[type="password"]').eq(1).type("test")
cy.contains("Create super admin user").click() cy.contains("Create super admin user").click({ force: true })
} }
if (url.includes("builder/auth/login") || url.includes("builder/admin")) { if (url.includes("builder/auth/login") || url.includes("builder/admin")) {
// login // login
cy.contains("Sign in to Budibase").then(() => { cy.contains("Sign in to Budibase").then(() => {
cy.get("input").first().type("test@test.com") cy.get("input").first().type("test@test.com")
cy.get('input[type="password"]').type("test") cy.get('input[type="password"]').type("test")
cy.get("button").first().click() cy.get("button").first().click({ force: true })
cy.wait(1000) cy.wait(1000)
}) })
} }
@ -58,7 +58,9 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => {
cy.get(".spectrum-Modal").within(() => { cy.get(".spectrum-Modal").within(() => {
cy.get("input").eq(0).type(name).should("have.value", name).blur() cy.get("input").eq(0).type(name).should("have.value", name).blur()
cy.get(".spectrum-ButtonGroup").contains("Create app").click() cy.get(".spectrum-ButtonGroup")
.contains("Create app")
.click({ force: true })
cy.wait(10000) cy.wait(10000)
}) })
if (shouldCreateDefaultTable) { if (shouldCreateDefaultTable) {
@ -75,9 +77,6 @@ Cypress.Commands.add("deleteApp", name => {
const findAppName = val.some(val => val.name == name) const findAppName = val.some(val => val.name == name)
if (findAppName) { if (findAppName) {
if (val.length > 0) { if (val.length > 0) {
if (Cypress.env("TEST_ENV")) {
cy.searchForApplication(name)
}
const appId = val.reduce((acc, app) => { const appId = val.reduce((acc, app) => {
if (name === app.name) { if (name === app.name) {
acc = app.appId acc = app.appId
@ -92,7 +91,7 @@ Cypress.Commands.add("deleteApp", name => {
const appIdParsed = appId.split("_").pop() const appIdParsed = appId.split("_").pop()
const actionEleId = `[data-cy=row_actions_${appIdParsed}]` const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
cy.get(actionEleId).within(() => { cy.get(actionEleId).within(() => {
cy.get(".spectrum-Icon").eq(0).click() cy.get(".spectrum-Icon").eq(0).click({ force: true })
}) })
cy.get(".spectrum-Menu").then($menu => { cy.get(".spectrum-Menu").then($menu => {
if ($menu.text().includes("Unpublish")) { if ($menu.text().includes("Unpublish")) {
@ -102,7 +101,7 @@ Cypress.Commands.add("deleteApp", name => {
}) })
cy.get(actionEleId).within(() => { cy.get(actionEleId).within(() => {
cy.get(".spectrum-Icon").eq(0).click() cy.get(".spectrum-Icon").eq(0).click({ force: true })
}) })
cy.get(".spectrum-Menu").contains("Delete").click() cy.get(".spectrum-Menu").contains("Delete").click()
cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Dialog-grid").within(() => {
@ -128,7 +127,7 @@ Cypress.Commands.add("deleteAllApps", () => {
const appIdParsed = val[i].appId.split("_").pop() const appIdParsed = val[i].appId.split("_").pop()
const actionEleId = `[data-cy=row_actions_${appIdParsed}]` const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
cy.get(actionEleId).within(() => { cy.get(actionEleId).within(() => {
cy.get(".spectrum-Icon").eq(0).click() cy.get(".spectrum-Icon").eq(0).click({ force: true })
}) })
cy.get(".spectrum-Menu").contains("Delete").click() cy.get(".spectrum-Menu").contains("Delete").click()
@ -145,6 +144,7 @@ Cypress.Commands.add("createTestApp", () => {
const appName = "Cypress Tests" const appName = "Cypress Tests"
cy.deleteApp(appName) cy.deleteApp(appName)
cy.createApp(appName, "This app is used for Cypress testing.") cy.createApp(appName, "This app is used for Cypress testing.")
//cy.createScreen("home")
}) })
Cypress.Commands.add("createTestTableWithData", () => { Cypress.Commands.add("createTestTableWithData", () => {
@ -245,12 +245,12 @@ Cypress.Commands.add("createUser", email => {
Cypress.Commands.add("addComponent", (category, component) => { Cypress.Commands.add("addComponent", (category, component) => {
if (category) { if (category) {
cy.get(`[data-cy="category-${category}"]`).click() cy.get(`[data-cy="category-${category}"]`).click({ force: true })
} }
if (component) { if (component) {
cy.get(`[data-cy="component-${component}"]`).click() cy.get(`[data-cy="component-${component}"]`).click({ force: true })
} }
cy.wait(1000) cy.wait(2000)
cy.location().then(loc => { cy.location().then(loc => {
const params = loc.pathname.split("/") const params = loc.pathname.split("/")
const componentId = params[params.length - 1] const componentId = params[params.length - 1]

View File

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

View File

@ -60,7 +60,7 @@ export function getBindings({
) )
const label = path == null ? column : `${path}.0.${column}` const label = path == null ? column : `${path}.0.${column}`
const binding = path == null ? `[${column}]` : `${path}.0.[${column}]` const binding = path == null ? `[${column}]` : `[${path}].0.[${column}]`
// only supply a description for relationship paths // only supply a description for relationship paths
const description = const description =
path == null path == null

View File

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

View File

@ -299,6 +299,7 @@
{dynamicVariables} {dynamicVariables}
bind:binding={varBinding} bind:binding={varBinding}
bind:this={addVariableModal} bind:this={addVariableModal}
on:change={saveQuery}
/> />
{#if query && queryConfig} {#if query && queryConfig}
<div class="inner"> <div class="inner">

View File

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

View File

@ -2338,7 +2338,11 @@
"type": "boolean", "type": "boolean",
"label": "Autocomplete", "label": "Autocomplete",
"key": "autocomplete", "key": "autocomplete",
"defaultValue": false "defaultValue": false,
"dependsOn": {
"setting": "optionsType",
"value": "select"
}
}, },
{ {
"type": "boolean", "type": "boolean",
@ -2346,6 +2350,43 @@
"key": "disabled", "key": "disabled",
"defaultValue": false "defaultValue": false
}, },
{
"type": "select",
"label": "Type",
"key": "optionsType",
"defaultValue": "select",
"placeholder": "Pick an options type",
"options": [
{
"label": "Select",
"value": "select"
},
{
"label": "Checkboxes",
"value": "checkbox"
}
]
},
{
"type": "select",
"label": "Direction",
"key": "direction",
"defaultValue": "vertical",
"options": [
{
"label": "Horizontal",
"value": "horizontal"
},
{
"label": "Vertical",
"value": "vertical"
}
],
"dependsOn": {
"setting": "optionsType",
"value": "checkbox"
}
},
{ {
"type": "select", "type": "select",
"label": "Options source", "label": "Options source",

View File

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

View File

@ -283,7 +283,8 @@
@media print { @media print {
#spectrum-root, #spectrum-root,
#clip-root, #clip-root,
#app-root { #app-root,
#app-body {
overflow: visible !important; overflow: visible !important;
} }
} }

View File

@ -1,5 +1,5 @@
<script> <script>
import { CoreMultiselect } from "@budibase/bbui" import { CoreMultiselect, CoreCheckboxGroup } from "@budibase/bbui"
import Field from "./Field.svelte" import Field from "./Field.svelte"
import { getOptions } from "./optionsParser" import { getOptions } from "./optionsParser"
export let field export let field
@ -15,6 +15,8 @@
export let customOptions export let customOptions
export let autocomplete = false export let autocomplete = false
export let onChange export let onChange
export let optionsType = "select"
export let direction = "vertical"
let fieldState let fieldState
let fieldApi let fieldApi
@ -61,17 +63,31 @@
bind:fieldSchema bind:fieldSchema
> >
{#if fieldState} {#if fieldState}
<CoreMultiselect {#if !optionsType || optionsType === "select"}
value={fieldState.value || []} <CoreMultiselect
error={fieldState.error} value={fieldState.value || []}
getOptionLabel={flatOptions ? x => x : x => x.label} error={fieldState.error}
getOptionValue={flatOptions ? x => x : x => x.value} getOptionLabel={flatOptions ? x => x : x => x.label}
id={fieldState.fieldId} getOptionValue={flatOptions ? x => x : x => x.value}
disabled={fieldState.disabled} id={fieldState.fieldId}
on:change={handleChange} disabled={fieldState.disabled}
{placeholder} on:change={handleChange}
{options} {placeholder}
{autocomplete} {options}
/> {autocomplete}
/>
{:else if optionsType === "checkbox"}
<CoreCheckboxGroup
value={fieldState.value || []}
id={fieldState.fieldId}
disabled={fieldState.disabled}
error={fieldState.error}
{options}
{direction}
on:change={handleChange}
getOptionLabel={flatOptions ? x => x : x => x.label}
getOptionValue={flatOptions ? x => x : x => x.value}
/>
{/if}
{/if} {/if}
</Field> </Field>

View File

@ -66,4 +66,9 @@
.tab-content { .tab-content {
padding: 0 var(--spacing-xl); padding: 0 var(--spacing-xl);
} }
@media print {
.devtools {
display: none;
}
}
</style> </style>

View File

@ -71,4 +71,9 @@
.dev-preview-header :global(.spectrum-Picker-label) { .dev-preview-header :global(.spectrum-Picker-label) {
color: white !important; color: white !important;
} }
@media print {
.dev-preview-header {
display: none;
}
}
</style> </style>

View File

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

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.151-alpha.2", "version": "1.0.159-alpha.1",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -10,6 +10,7 @@
}, },
"scripts": { "scripts": {
"build": "rimraf dist/ && tsc -p tsconfig.build.json && mv dist/src/* dist/ && rimraf dist/src/ && yarn postbuild", "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/", "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": "jest --coverage --maxWorkers=2",
"test:watch": "jest --watch", "test:watch": "jest --watch",
@ -68,10 +69,10 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "^10.0.3", "@apidevtools/swagger-parser": "^10.0.3",
"@budibase/backend-core": "^1.0.151-alpha.2", "@budibase/backend-core": "^1.0.159-alpha.1",
"@budibase/client": "^1.0.151-alpha.2", "@budibase/client": "^1.0.159-alpha.1",
"@budibase/pro": "1.0.151-alpha.2", "@budibase/pro": "1.0.159-alpha.1",
"@budibase/string-templates": "^1.0.151-alpha.2", "@budibase/string-templates": "^1.0.159-alpha.1",
"@bull-board/api": "^3.7.0", "@bull-board/api": "^3.7.0",
"@bull-board/koa": "^3.7.0", "@bull-board/koa": "^3.7.0",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",

View File

@ -31,7 +31,9 @@ export async function patch(ctx: any): Promise<any> {
return save(ctx) return save(ctx)
} }
try { try {
const { row, table } = await pickApi(tableId).patch(ctx) const { row, table } = await quotas.addQuery(() =>
pickApi(tableId).patch(ctx)
)
ctx.status = 200 ctx.status = 200
ctx.eventEmitter && ctx.eventEmitter &&
ctx.eventEmitter.emitRow(`row:update`, appId, row, table) ctx.eventEmitter.emitRow(`row:update`, appId, row, table)
@ -42,7 +44,7 @@ export async function patch(ctx: any): Promise<any> {
} }
} }
const saveRow = async (ctx: any) => { export const save = async (ctx: any) => {
const appId = ctx.appId const appId = ctx.appId
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
const body = ctx.request.body const body = ctx.request.body
@ -51,7 +53,9 @@ const saveRow = async (ctx: any) => {
return patch(ctx) return patch(ctx)
} }
try { try {
const { row, table } = await pickApi(tableId).save(ctx) const { row, table } = await quotas.addRow(() =>
quotas.addQuery(() => pickApi(tableId).save(ctx))
)
ctx.status = 200 ctx.status = 200
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table) ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table)
ctx.message = `${table.name} saved successfully` ctx.message = `${table.name} saved successfully`
@ -61,14 +65,10 @@ const saveRow = async (ctx: any) => {
} }
} }
export async function save(ctx: any) {
await quotas.addRow(() => saveRow(ctx))
}
export async function fetchView(ctx: any) { export async function fetchView(ctx: any) {
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
try { try {
ctx.body = await pickApi(tableId).fetchView(ctx) ctx.body = await quotas.addQuery(() => pickApi(tableId).fetchView(ctx))
} catch (err) { } catch (err) {
ctx.throw(400, err) ctx.throw(400, err)
} }
@ -77,7 +77,7 @@ export async function fetchView(ctx: any) {
export async function fetch(ctx: any) { export async function fetch(ctx: any) {
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
try { try {
ctx.body = await pickApi(tableId).fetch(ctx) ctx.body = await quotas.addQuery(() => pickApi(tableId).fetch(ctx))
} catch (err) { } catch (err) {
ctx.throw(400, err) ctx.throw(400, err)
} }
@ -86,7 +86,7 @@ export async function fetch(ctx: any) {
export async function find(ctx: any) { export async function find(ctx: any) {
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
try { try {
ctx.body = await pickApi(tableId).find(ctx) ctx.body = await quotas.addQuery(() => pickApi(tableId).find(ctx))
} catch (err) { } catch (err) {
ctx.throw(400, err) ctx.throw(400, err)
} }
@ -98,14 +98,16 @@ export async function destroy(ctx: any) {
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
let response, row let response, row
if (inputs.rows) { if (inputs.rows) {
let { rows } = await pickApi(tableId).bulkDestroy(ctx) let { rows } = await quotas.addQuery(() =>
pickApi(tableId).bulkDestroy(ctx)
)
await quotas.removeRows(rows.length) await quotas.removeRows(rows.length)
response = rows response = rows
for (let row of rows) { for (let row of rows) {
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row) ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
} }
} else { } else {
let resp = await pickApi(tableId).destroy(ctx) let resp = await quotas.addQuery(() => pickApi(tableId).destroy(ctx))
await quotas.removeRow() await quotas.removeRow()
response = resp.response response = resp.response
row = resp.row row = resp.row
@ -121,7 +123,7 @@ export async function search(ctx: any) {
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
try { try {
ctx.status = 200 ctx.status = 200
ctx.body = await pickApi(tableId).search(ctx) ctx.body = await quotas.addQuery(() => pickApi(tableId).search(ctx))
} catch (err) { } catch (err) {
ctx.throw(400, err) ctx.throw(400, err)
} }
@ -139,7 +141,9 @@ export async function validate(ctx: any) {
export async function fetchEnrichedRow(ctx: any) { export async function fetchEnrichedRow(ctx: any) {
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
try { try {
ctx.body = await pickApi(tableId).fetchEnrichedRow(ctx) ctx.body = await quotas.addQuery(() =>
pickApi(tableId).fetchEnrichedRow(ctx)
)
} catch (err) { } catch (err) {
ctx.throw(400, err) ctx.throw(400, err)
} }
@ -148,7 +152,7 @@ export async function fetchEnrichedRow(ctx: any) {
export const exportRows = async (ctx: any) => { export const exportRows = async (ctx: any) => {
const tableId = getTableId(ctx) const tableId = getTableId(ctx)
try { try {
ctx.body = await pickApi(tableId).exportRows(ctx) ctx.body = await quotas.addQuery(() => pickApi(tableId).exportRows(ctx))
} catch (err) { } catch (err) {
ctx.throw(400, err) ctx.throw(400, err)
} }

View File

@ -3,6 +3,7 @@ const setup = require("./utilities")
const { basicRow } = setup.structures const { basicRow } = setup.structures
const { doInAppContext } = require("@budibase/backend-core/context") const { doInAppContext } = require("@budibase/backend-core/context")
const { doInTenant } = require("@budibase/backend-core/tenancy") const { doInTenant } = require("@budibase/backend-core/tenancy")
const { quotas, QuotaUsageType, StaticQuotaName, MonthlyQuotaName } = require("@budibase/pro")
// mock the fetch for the search system // mock the fetch for the search system
jest.mock("node-fetch") jest.mock("node-fetch")
@ -28,9 +29,29 @@ describe("/rows", () => {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(status) .expect(status)
const getRowUsage = async () => {
return config.doInContext(null, () => quotas.getCurrentUsageValue(QuotaUsageType.STATIC, StaticQuotaName.ROWS))
}
const getQueryUsage = async () => {
return config.doInContext(null, () => quotas.getCurrentUsageValue(QuotaUsageType.MONTHLY, MonthlyQuotaName.QUERIES))
}
const assertRowUsage = async (expected) => {
const usage = await getRowUsage()
expect(usage).toBe(expected)
}
const assertQueryUsage = async (expected) => {
const usage = await getQueryUsage()
expect(usage).toBe(expected)
}
describe("save, load, update", () => { describe("save, load, update", () => {
it("returns a success message when the row is created", async () => { it("returns a success message when the row is created", async () => {
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await request const res = await request
.post(`/api/${row.tableId}/rows`) .post(`/api/${row.tableId}/rows`)
.send(row) .send(row)
@ -40,10 +61,14 @@ describe("/rows", () => {
expect(res.res.statusMessage).toEqual(`${table.name} saved successfully`) expect(res.res.statusMessage).toEqual(`${table.name} saved successfully`)
expect(res.body.name).toEqual("Test Contact") expect(res.body.name).toEqual("Test Contact")
expect(res.body._rev).toBeDefined() expect(res.body._rev).toBeDefined()
await assertRowUsage(rowUsage + 1)
await assertQueryUsage(queryUsage + 1)
}) })
it("updates a row successfully", async () => { it("updates a row successfully", async () => {
const existing = await config.createRow() const existing = await config.createRow()
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await request const res = await request
.post(`/api/${table._id}/rows`) .post(`/api/${table._id}/rows`)
@ -59,10 +84,13 @@ describe("/rows", () => {
expect(res.res.statusMessage).toEqual(`${table.name} updated successfully.`) expect(res.res.statusMessage).toEqual(`${table.name} updated successfully.`)
expect(res.body.name).toEqual("Updated Name") expect(res.body.name).toEqual("Updated Name")
await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage + 1)
}) })
it("should load a row", async () => { it("should load a row", async () => {
const existing = await config.createRow() const existing = await config.createRow()
const queryUsage = await getQueryUsage()
const res = await request const res = await request
.get(`/api/${table._id}/rows/${existing._id}`) .get(`/api/${table._id}/rows/${existing._id}`)
@ -76,6 +104,7 @@ describe("/rows", () => {
_rev: existing._rev, _rev: existing._rev,
type: "row", type: "row",
}) })
await assertQueryUsage(queryUsage + 1)
}) })
it("should list all rows for given tableId", async () => { it("should list all rows for given tableId", async () => {
@ -86,6 +115,7 @@ describe("/rows", () => {
} }
await config.createRow() await config.createRow()
await config.createRow(newRow) await config.createRow(newRow)
const queryUsage = await getQueryUsage()
const res = await request const res = await request
.get(`/api/${table._id}/rows`) .get(`/api/${table._id}/rows`)
@ -96,15 +126,19 @@ describe("/rows", () => {
expect(res.body.length).toBe(2) expect(res.body.length).toBe(2)
expect(res.body.find(r => r.name === newRow.name)).toBeDefined() expect(res.body.find(r => r.name === newRow.name)).toBeDefined()
expect(res.body.find(r => r.name === row.name)).toBeDefined() expect(res.body.find(r => r.name === row.name)).toBeDefined()
await assertQueryUsage(queryUsage + 1)
}) })
it("load should return 404 when row does not exist", async () => { it("load should return 404 when row does not exist", async () => {
await config.createRow() await config.createRow()
const queryUsage = await getQueryUsage()
await request await request
.get(`/api/${table._id}/rows/not-a-valid-id`) .get(`/api/${table._id}/rows/not-a-valid-id`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(404) .expect(404)
await assertQueryUsage(queryUsage) // no change
}) })
it("row values are coerced", async () => { it("row values are coerced", async () => {
@ -202,6 +236,9 @@ describe("/rows", () => {
it("should update only the fields that are supplied", async () => { it("should update only the fields that are supplied", async () => {
const existing = await config.createRow() const existing = await config.createRow()
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await request const res = await request
.patch(`/api/${table._id}/rows`) .patch(`/api/${table._id}/rows`)
.send({ .send({
@ -222,10 +259,15 @@ describe("/rows", () => {
expect(savedRow.body.description).toEqual(existing.description) expect(savedRow.body.description).toEqual(existing.description)
expect(savedRow.body.name).toEqual("Updated Name") expect(savedRow.body.name).toEqual("Updated Name")
await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage + 2) // account for the second load
}) })
it("should throw an error when given improper types", async () => { it("should throw an error when given improper types", async () => {
const existing = await config.createRow() const existing = await config.createRow()
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
await request await request
.patch(`/api/${table._id}/rows`) .patch(`/api/${table._id}/rows`)
.send({ .send({
@ -236,12 +278,18 @@ describe("/rows", () => {
}) })
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect(400) .expect(400)
await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage)
}) })
}) })
describe("destroy", () => { describe("destroy", () => {
it("should be able to delete a row", async () => { it("should be able to delete a row", async () => {
const createdRow = await config.createRow(row) const createdRow = await config.createRow(row)
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await request const res = await request
.delete(`/api/${table._id}/rows`) .delete(`/api/${table._id}/rows`)
.send({ .send({
@ -253,11 +301,16 @@ describe("/rows", () => {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
expect(res.body[0]._id).toEqual(createdRow._id) expect(res.body[0]._id).toEqual(createdRow._id)
await assertRowUsage(rowUsage -1)
await assertQueryUsage(queryUsage +1)
}) })
}) })
describe("validate", () => { describe("validate", () => {
it("should return no errors on valid row", async () => { it("should return no errors on valid row", async () => {
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await request const res = await request
.post(`/api/${table._id}/rows/validate`) .post(`/api/${table._id}/rows/validate`)
.send({ name: "ivan" }) .send({ name: "ivan" })
@ -267,9 +320,14 @@ describe("/rows", () => {
expect(res.body.valid).toBe(true) expect(res.body.valid).toBe(true)
expect(Object.keys(res.body.errors)).toEqual([]) expect(Object.keys(res.body.errors)).toEqual([])
await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage)
}) })
it("should errors on invalid row", async () => { it("should errors on invalid row", async () => {
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await request const res = await request
.post(`/api/${table._id}/rows/validate`) .post(`/api/${table._id}/rows/validate`)
.send({ name: 1 }) .send({ name: 1 })
@ -279,7 +337,8 @@ describe("/rows", () => {
expect(res.body.valid).toBe(false) expect(res.body.valid).toBe(false)
expect(Object.keys(res.body.errors)).toEqual(["name"]) expect(Object.keys(res.body.errors)).toEqual(["name"])
await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage)
}) })
}) })
@ -287,6 +346,9 @@ describe("/rows", () => {
it("should be able to delete a bulk set of rows", async () => { it("should be able to delete a bulk set of rows", async () => {
const row1 = await config.createRow() const row1 = await config.createRow()
const row2 = await config.createRow() const row2 = await config.createRow()
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await request const res = await request
.delete(`/api/${table._id}/rows`) .delete(`/api/${table._id}/rows`)
.send({ .send({
@ -298,14 +360,20 @@ describe("/rows", () => {
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
expect(res.body.length).toEqual(2) expect(res.body.length).toEqual(2)
await loadRow(row1._id, 404) await loadRow(row1._id, 404)
await assertRowUsage(rowUsage - 2)
await assertQueryUsage(queryUsage +1)
}) })
}) })
describe("fetchView", () => { describe("fetchView", () => {
it("should be able to fetch tables contents via 'view'", async () => { it("should be able to fetch tables contents via 'view'", async () => {
const row = await config.createRow() const row = await config.createRow()
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await request const res = await request
.get(`/api/views/${table._id}`) .get(`/api/views/${table._id}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
@ -313,18 +381,29 @@ describe("/rows", () => {
.expect(200) .expect(200)
expect(res.body.length).toEqual(1) expect(res.body.length).toEqual(1)
expect(res.body[0]._id).toEqual(row._id) expect(res.body[0]._id).toEqual(row._id)
await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage +1)
}) })
it("should throw an error if view doesn't exist", async () => { it("should throw an error if view doesn't exist", async () => {
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
await request await request
.get(`/api/views/derp`) .get(`/api/views/derp`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect(404) .expect(404)
await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage)
}) })
it("should be able to run on a view", async () => { it("should be able to run on a view", async () => {
const view = await config.createView() const view = await config.createView()
const row = await config.createRow() const row = await config.createRow()
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
const res = await request const res = await request
.get(`/api/views/${view.name}`) .get(`/api/views/${view.name}`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
@ -332,13 +411,12 @@ describe("/rows", () => {
.expect(200) .expect(200)
expect(res.body.length).toEqual(1) expect(res.body.length).toEqual(1)
expect(res.body[0]._id).toEqual(row._id) expect(res.body[0]._id).toEqual(row._id)
await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage + 1)
}) })
}) })
describe("user testing", () => {
})
describe("fetchEnrichedRows", () => { describe("fetchEnrichedRows", () => {
it("should allow enriching some linked rows", async () => { it("should allow enriching some linked rows", async () => {
const { table, firstRow, secondRow } = await doInTenant(setup.structures.TENANT_ID, async () => { const { table, firstRow, secondRow } = await doInTenant(setup.structures.TENANT_ID, async () => {
@ -356,6 +434,8 @@ describe("/rows", () => {
}) })
return { table, firstRow, secondRow } return { table, firstRow, secondRow }
}) })
const rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage()
// test basic enrichment // test basic enrichment
const resBasic = await request const resBasic = await request
@ -376,6 +456,8 @@ describe("/rows", () => {
expect(resEnriched.body.link[0]._id).toBe(firstRow._id) expect(resEnriched.body.link[0]._id).toBe(firstRow._id)
expect(resEnriched.body.link[0].name).toBe("Test Contact") expect(resEnriched.body.link[0].name).toBe("Test Contact")
expect(resEnriched.body.link[0].description).toBe("original description") expect(resEnriched.body.link[0].description).toBe("original description")
await assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage +2)
}) })
}) })

View File

@ -167,7 +167,6 @@ exports.screenValidator = () => {
_id: Joi.string().required(), _id: Joi.string().required(),
_component: Joi.string().required(), _component: Joi.string().required(),
_children: Joi.array().required(), _children: Joi.array().required(),
_instanceName: Joi.string().required(),
_styles: Joi.object().required(), _styles: Joi.object().required(),
type: OPTIONAL_STRING, type: OPTIONAL_STRING,
table: OPTIONAL_STRING, table: OPTIONAL_STRING,

View File

@ -208,5 +208,10 @@ exports.AutomationErrors = {
FAILURE_CONDITION: "FAILURE_CONDITION_MET", FAILURE_CONDITION: "FAILURE_CONDITION_MET",
} }
exports.LoopStepTypes = {
ARRAY: "Array",
STRING: "String",
}
// pass through the list from the auth/core lib // pass through the list from the auth/core lib
exports.ObjectStoreBuckets = ObjectStoreBuckets exports.ObjectStoreBuckets = ObjectStoreBuckets

View File

@ -21,6 +21,31 @@ type KnexQuery = Knex.QueryBuilder | Knex
const MIN_ISO_DATE = "0000-00-00T00:00:00.000Z" const MIN_ISO_DATE = "0000-00-00T00:00:00.000Z"
const MAX_ISO_DATE = "9999-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) { function parse(input: any) {
if (Array.isArray(input)) { if (Array.isArray(input)) {
return JSON.stringify(input) return JSON.stringify(input)
@ -125,7 +150,9 @@ class InternalBuilder {
} else { } else {
const rawFnc = `${fnc}Raw` const rawFnc = `${fnc}Raw`
// @ts-ignore // @ts-ignore
query = query[rawFnc](`LOWER(${key}) LIKE ?`, [`%${value}%`]) query = query[rawFnc](`LOWER(${likeKey(this.client, key)}) LIKE ?`, [
`%${value}%`,
])
} }
}) })
} }

View File

@ -8,7 +8,7 @@ const { DocumentTypes } = require("../db/utils")
const { doInTenant } = require("@budibase/backend-core/tenancy") const { doInTenant } = require("@budibase/backend-core/tenancy")
const { definitions: triggerDefs } = require("../automations/triggerInfo") const { definitions: triggerDefs } = require("../automations/triggerInfo")
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context") const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
const { AutomationErrors } = require("../constants") const { AutomationErrors, LoopStepTypes } = require("../constants")
const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId
const LOOP_STEP_ID = actions.ACTION_DEFINITIONS.LOOP.stepId const LOOP_STEP_ID = actions.ACTION_DEFINITIONS.LOOP.stepId
@ -17,6 +17,41 @@ const STOPPED_STATUS = { success: false, status: "STOPPED" }
const { cloneDeep } = require("lodash/fp") const { cloneDeep } = require("lodash/fp")
const env = require("../environment") const env = require("../environment")
function typecastForLooping(loopStep, input) {
if (!input || !input.binding) {
return null
}
const isArray = Array.isArray(input.binding),
isString = typeof input.binding === "string"
try {
switch (loopStep.inputs.option) {
case LoopStepTypes.ARRAY:
if (isString) {
return JSON.parse(input.binding)
}
break
case LoopStepTypes.STRING:
if (isArray) {
return input.binding.join(",")
}
break
}
} catch (err) {
throw new Error("Unable to cast to correct type")
}
return input.binding
}
function getLoopIterations(loopStep, input) {
const binding = typecastForLooping(loopStep, input)
if (!loopStep || !binding) {
return 1
}
return Array.isArray(binding)
? binding.length
: automationUtils.stringSplit(binding).length
}
/** /**
* The automation orchestrator is a class responsible for executing automations. * The automation orchestrator is a class responsible for executing automations.
* It handles the context of the automation and makes sure each step gets the correct * It handles the context of the automation and makes sure each step gets the correct
@ -107,7 +142,9 @@ class Orchestrator {
let loopSteps = [] let loopSteps = []
for (let step of automation.definition.steps) { for (let step of automation.definition.steps) {
stepCount++ stepCount++
let input let input,
iterations = 1,
iterationCount = 0
if (step.stepId === LOOP_STEP_ID) { if (step.stepId === LOOP_STEP_ID) {
loopStep = step loopStep = step
loopStepNumber = stepCount loopStepNumber = stepCount
@ -116,13 +153,9 @@ class Orchestrator {
if (loopStep) { if (loopStep) {
input = await processObject(loopStep.inputs, this._context) input = await processObject(loopStep.inputs, this._context)
iterations = getLoopIterations(loopStep, input)
} }
let iterations = loopStep
? Array.isArray(input.binding)
? input.binding.length
: automationUtils.stringSplit(input.binding).length
: 1
let iterationCount = 0
for (let index = 0; index < iterations; index++) { for (let index = 0; index < iterations; index++) {
let originalStepInput = cloneDeep(step.inputs) let originalStepInput = cloneDeep(step.inputs)
@ -132,18 +165,11 @@ class Orchestrator {
loopStep.inputs, loopStep.inputs,
cloneDeep(this._context) cloneDeep(this._context)
) )
newInput = automationUtils.cleanInputValues(
newInput,
loopStep.schema.inputs
)
let tempOutput = { items: loopSteps, iterations: iterationCount } let tempOutput = { items: loopSteps, iterations: iterationCount }
if ( try {
(loopStep.inputs.option === "Array" && newInput.binding = typecastForLooping(loopStep, newInput)
!Array.isArray(newInput.binding)) || } catch (err) {
(loopStep.inputs.option === "String" &&
typeof newInput.binding !== "string")
) {
this.updateContextAndOutput(loopStepNumber, step, tempOutput, { this.updateContextAndOutput(loopStepNumber, step, tempOutput, {
status: AutomationErrors.INCORRECT_TYPE, status: AutomationErrors.INCORRECT_TYPE,
success: false, success: false,
@ -205,21 +231,13 @@ class Orchestrator {
} }
let isFailure = false let isFailure = false
if ( const currentItem = this._context.steps[loopStepNumber]?.currentItem
typeof this._context.steps[loopStepNumber]?.currentItem === "object" if (currentItem && typeof currentItem === "object") {
) { isFailure = Object.keys(currentItem).some(value => {
isFailure = Object.keys( return currentItem[value] === loopStep.inputs.failure
this._context.steps[loopStepNumber].currentItem
).some(value => {
return (
this._context.steps[loopStepNumber].currentItem[value] ===
loopStep.inputs.failure
)
}) })
} else { } else {
isFailure = isFailure = currentItem && currentItem === loopStep.inputs.failure
this._context.steps[loopStepNumber]?.currentItem ===
loopStep.inputs.failure
} }
if (isFailure) { if (isFailure) {

View File

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

View File

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

View File

@ -64,9 +64,10 @@ module.exports.processors = [
return statement return statement
} }
} }
const testHelper = possibleHelper.trim().toLowerCase()
if ( if (
!noHelpers && !noHelpers &&
HelperNames().some(option => option.includes(possibleHelper)) HelperNames().some(option => testHelper === option.toLowerCase())
) { ) {
insideStatement = `(${insideStatement})` 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", () => { describe("check the utility functions", () => {
it("should return false for an invalid template string", () => { it("should return false for an invalid template string", () => {
const valid = isValid("{{ table1.thing prop }}") 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") 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", () => { describe("attempt some complex problems", () => {

View File

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