Merge remote-tracking branch 'origin/develop' into feature/automation-log-filter-by-license
This commit is contained in:
commit
e746383c16
|
@ -1,11 +1,8 @@
|
|||
name: Deploy Budibase Single Container Image to DockerHub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "omnibus-action"
|
||||
- "develop"
|
||||
- "master"
|
||||
- "main"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
BASE_BRANCH: ${{ github.event.pull_request.base.ref}}
|
||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
|
@ -40,7 +37,7 @@ jobs:
|
|||
- name: Runt Yarn Lint
|
||||
run: yarn lint
|
||||
- name: Run Yarn Build
|
||||
run: yarn build
|
||||
run: yarn build:docker:pre
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
|
|
|
@ -68,7 +68,7 @@ jobs:
|
|||
- name: Publish budibase packages to NPM
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
RELEASE_VERSION_TYPE: ${{ github.event.inputs.version }}
|
||||
RELEASE_VERSION_TYPE: ${{ github.event.inputs.versioning }}
|
||||
run: |
|
||||
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default
|
||||
git config --global user.name "Budibase Release Bot"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.0.220-alpha.4",
|
||||
"version": "1.1.10-alpha.3",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
"test:e2e:ci:notify": "lerna run cy:ci:notify",
|
||||
"build:specs": "lerna run specs",
|
||||
"build:docker": "lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
||||
"build:docker:pre": "lerna run build && lerna run predocker",
|
||||
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
|
||||
"build:docker:proxy:compose": "node scripts/proxy/generateProxyConfig compose && npm run build:docker:proxy",
|
||||
"build:docker:proxy:preprod": "node scripts/proxy/generateProxyConfig preprod && npm run build:docker:proxy",
|
||||
|
@ -65,7 +66,7 @@
|
|||
"build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -",
|
||||
"build:docker:single:multiarch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/single/Dockerfile -t budibase:latest .",
|
||||
"build:docker:single:image": "docker build -f hosting/single/Dockerfile -t budibase:latest .",
|
||||
"build:docker:single": "lerna run build && lerna run predocker && npm run build:docker:single:image",
|
||||
"build:docker:single": "npm run build:docker:pre && npm run build:docker:single:image",
|
||||
"build:docs": "lerna run build:docs",
|
||||
"release:helm": "node scripts/releaseHelmChart",
|
||||
"env:multi:enable": "lerna run env:multi:enable",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "1.0.220-alpha.4",
|
||||
"version": "1.1.10-alpha.3",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
@ -20,7 +20,7 @@
|
|||
"test:watch": "jest --watchAll"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/types": "^1.0.220-alpha.4",
|
||||
"@budibase/types": "^1.1.10-alpha.3",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
"aws-sdk": "2.1030.0",
|
||||
"bcrypt": "5.0.1",
|
||||
|
@ -59,10 +59,10 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@budibase/types": "^1.0.219",
|
||||
"@shopify/jest-koa-mocks": "3.1.5",
|
||||
"@types/jest": "27.5.1",
|
||||
"@types/koa": "2.0.52",
|
||||
"@types/lodash": "4.14.180",
|
||||
"@types/node": "14.18.20",
|
||||
"@types/node-fetch": "2.6.1",
|
||||
"@types/pouchdb": "6.4.0",
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
export enum ContextKeys {
|
||||
TENANT_ID = "tenantId",
|
||||
GLOBAL_DB = "globalDb",
|
||||
APP_ID = "appId",
|
||||
IDENTITY = "identity",
|
||||
// whatever the request app DB was
|
||||
CURRENT_DB = "currentDb",
|
||||
// get the prod app DB from the request
|
||||
PROD_DB = "prodDb",
|
||||
// get the dev app DB from the request
|
||||
DEV_DB = "devDb",
|
||||
DB_OPTS = "dbOpts",
|
||||
// check if something else is using the context, don't close DB
|
||||
TENANCY_IN_USE = "tenancyInUse",
|
||||
APP_IN_USE = "appInUse",
|
||||
IDENTITY_IN_USE = "identityInUse",
|
||||
}
|
|
@ -1,354 +0,0 @@
|
|||
const env = require("../environment")
|
||||
const { SEPARATOR, DocumentTypes } = require("../db/constants")
|
||||
const { DEFAULT_TENANT_ID } = require("../constants")
|
||||
const cls = require("./FunctionContext")
|
||||
const { dangerousGetDB, closeDB } = require("../db")
|
||||
const { getProdAppID, getDevelopmentAppID } = require("../db/conversions")
|
||||
const { baseGlobalDBName } = require("../tenancy/utils")
|
||||
const { isEqual } = require("lodash")
|
||||
|
||||
// some test cases call functions directly, need to
|
||||
// store an app ID to pretend there is a context
|
||||
let TEST_APP_ID = null
|
||||
|
||||
const ContextKeys = {
|
||||
TENANT_ID: "tenantId",
|
||||
GLOBAL_DB: "globalDb",
|
||||
APP_ID: "appId",
|
||||
IDENTITY: "identity",
|
||||
// whatever the request app DB was
|
||||
CURRENT_DB: "currentDb",
|
||||
// get the prod app DB from the request
|
||||
PROD_DB: "prodDb",
|
||||
// get the dev app DB from the request
|
||||
DEV_DB: "devDb",
|
||||
DB_OPTS: "dbOpts",
|
||||
// check if something else is using the context, don't close DB
|
||||
IN_USE: "inUse",
|
||||
}
|
||||
|
||||
exports.DEFAULT_TENANT_ID = DEFAULT_TENANT_ID
|
||||
|
||||
// this function makes sure the PouchDB objects are closed and
|
||||
// fully deleted when finished - this protects against memory leaks
|
||||
async function closeAppDBs() {
|
||||
const dbKeys = [
|
||||
ContextKeys.CURRENT_DB,
|
||||
ContextKeys.PROD_DB,
|
||||
ContextKeys.DEV_DB,
|
||||
]
|
||||
for (let dbKey of dbKeys) {
|
||||
const db = cls.getFromContext(dbKey)
|
||||
if (!db) {
|
||||
continue
|
||||
}
|
||||
await closeDB(db)
|
||||
// clear the DB from context, incase someone tries to use it again
|
||||
cls.setOnContext(dbKey, null)
|
||||
}
|
||||
// clear the app ID now that the databases are closed
|
||||
if (cls.getFromContext(ContextKeys.APP_ID)) {
|
||||
cls.setOnContext(ContextKeys.APP_ID, null)
|
||||
}
|
||||
if (cls.getFromContext(ContextKeys.DB_OPTS)) {
|
||||
cls.setOnContext(ContextKeys.DB_OPTS, null)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
exports.isMultiTenant = () => {
|
||||
return env.MULTI_TENANCY
|
||||
}
|
||||
|
||||
// used for automations, API endpoints should always be in context already
|
||||
exports.doInTenant = (tenantId, task, { forceNew } = {}) => {
|
||||
// the internal function is so that we can re-use an existing
|
||||
// context - don't want to close DB on a parent context
|
||||
async function internal(opts = { existing: false }) {
|
||||
// set the tenant id
|
||||
if (!opts.existing) {
|
||||
exports.updateTenantId(tenantId)
|
||||
}
|
||||
|
||||
try {
|
||||
// invoke the task
|
||||
return await task()
|
||||
} finally {
|
||||
const using = cls.getFromContext(ContextKeys.IN_USE)
|
||||
if (!using || using <= 1) {
|
||||
await exports.closeTenancy()
|
||||
} else {
|
||||
cls.setOnContext(using - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const using = cls.getFromContext(ContextKeys.IN_USE)
|
||||
if (
|
||||
!forceNew &&
|
||||
using &&
|
||||
cls.getFromContext(ContextKeys.TENANT_ID) === tenantId
|
||||
) {
|
||||
cls.setOnContext(ContextKeys.IN_USE, using + 1)
|
||||
return internal({ existing: true })
|
||||
} else {
|
||||
return cls.run(async () => {
|
||||
cls.setOnContext(ContextKeys.IN_USE, 1)
|
||||
return internal()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an app ID this will attempt to retrieve the tenant ID from it.
|
||||
* @return {null|string} The tenant ID found within the app ID.
|
||||
*/
|
||||
exports.getTenantIDFromAppID = appId => {
|
||||
if (!appId) {
|
||||
return null
|
||||
}
|
||||
const split = appId.split(SEPARATOR)
|
||||
const hasDev = split[1] === DocumentTypes.DEV
|
||||
if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) {
|
||||
return null
|
||||
}
|
||||
if (hasDev) {
|
||||
return split[2]
|
||||
} else {
|
||||
return split[1]
|
||||
}
|
||||
}
|
||||
|
||||
const setAppTenantId = appId => {
|
||||
const appTenantId =
|
||||
exports.getTenantIDFromAppID(appId) || exports.DEFAULT_TENANT_ID
|
||||
exports.updateTenantId(appTenantId)
|
||||
}
|
||||
|
||||
exports.doInAppContext = (appId, task, { forceNew } = {}) => {
|
||||
if (!appId) {
|
||||
throw new Error("appId is required")
|
||||
}
|
||||
|
||||
const identity = exports.getIdentity()
|
||||
|
||||
// the internal function is so that we can re-use an existing
|
||||
// context - don't want to close DB on a parent context
|
||||
async function internal(opts = { existing: false }) {
|
||||
// set the app tenant id
|
||||
if (!opts.existing) {
|
||||
setAppTenantId(appId)
|
||||
}
|
||||
// set the app ID
|
||||
cls.setOnContext(ContextKeys.APP_ID, appId)
|
||||
// preserve the identity
|
||||
exports.setIdentity(identity)
|
||||
try {
|
||||
// invoke the task
|
||||
return await task()
|
||||
} finally {
|
||||
const using = cls.getFromContext(ContextKeys.IN_USE)
|
||||
if (!using || using <= 1) {
|
||||
await closeAppDBs()
|
||||
} else {
|
||||
cls.setOnContext(using - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
const using = cls.getFromContext(ContextKeys.IN_USE)
|
||||
if (!forceNew && using && cls.getFromContext(ContextKeys.APP_ID) === appId) {
|
||||
cls.setOnContext(ContextKeys.IN_USE, using + 1)
|
||||
return internal({ existing: true })
|
||||
} else {
|
||||
return cls.run(async () => {
|
||||
cls.setOnContext(ContextKeys.IN_USE, 1)
|
||||
return internal()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.doInIdentityContext = (identity, task) => {
|
||||
if (!identity) {
|
||||
throw new Error("identity is required")
|
||||
}
|
||||
|
||||
async function internal(opts = { existing: false }) {
|
||||
if (!opts.existing) {
|
||||
cls.setOnContext(ContextKeys.IDENTITY, identity)
|
||||
// set the tenant so that doInTenant will preserve identity
|
||||
if (identity.tenantId) {
|
||||
exports.updateTenantId(identity.tenantId)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// invoke the task
|
||||
return await task()
|
||||
} finally {
|
||||
const using = cls.getFromContext(ContextKeys.IN_USE)
|
||||
if (!using || using <= 1) {
|
||||
exports.setIdentity(null)
|
||||
} else {
|
||||
cls.setOnContext(using - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const existing = cls.getFromContext(ContextKeys.IDENTITY)
|
||||
const using = cls.getFromContext(ContextKeys.IN_USE)
|
||||
if (using && existing && existing._id === identity._id) {
|
||||
cls.setOnContext(ContextKeys.IN_USE, using + 1)
|
||||
return internal({ existing: true })
|
||||
} else {
|
||||
return cls.run(async () => {
|
||||
cls.setOnContext(ContextKeys.IN_USE, 1)
|
||||
return internal({ existing: false })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.setIdentity = identity => {
|
||||
cls.setOnContext(ContextKeys.IDENTITY, identity)
|
||||
}
|
||||
|
||||
exports.getIdentity = () => {
|
||||
try {
|
||||
return cls.getFromContext(ContextKeys.IDENTITY)
|
||||
} catch (e) {
|
||||
// do nothing - identity is not in context
|
||||
}
|
||||
}
|
||||
|
||||
exports.updateTenantId = tenantId => {
|
||||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
||||
if (env.USE_COUCH) {
|
||||
exports.setGlobalDB(tenantId)
|
||||
}
|
||||
}
|
||||
|
||||
exports.updateAppId = async appId => {
|
||||
try {
|
||||
// have to close first, before removing the databases from context
|
||||
await closeAppDBs()
|
||||
cls.setOnContext(ContextKeys.APP_ID, appId)
|
||||
} catch (err) {
|
||||
if (env.isTest()) {
|
||||
TEST_APP_ID = appId
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.setGlobalDB = tenantId => {
|
||||
const dbName = baseGlobalDBName(tenantId)
|
||||
const db = dangerousGetDB(dbName)
|
||||
cls.setOnContext(ContextKeys.GLOBAL_DB, db)
|
||||
return db
|
||||
}
|
||||
|
||||
exports.getGlobalDB = () => {
|
||||
const db = cls.getFromContext(ContextKeys.GLOBAL_DB)
|
||||
if (!db) {
|
||||
throw new Error("Global DB not found")
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
exports.isTenantIdSet = () => {
|
||||
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID)
|
||||
return !!tenantId
|
||||
}
|
||||
|
||||
exports.getTenantId = () => {
|
||||
if (!exports.isMultiTenant()) {
|
||||
return exports.DEFAULT_TENANT_ID
|
||||
}
|
||||
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID)
|
||||
if (!tenantId) {
|
||||
throw new Error("Tenant id not found")
|
||||
}
|
||||
return tenantId
|
||||
}
|
||||
|
||||
exports.getAppId = () => {
|
||||
const foundId = cls.getFromContext(ContextKeys.APP_ID)
|
||||
if (!foundId && env.isTest() && TEST_APP_ID) {
|
||||
return TEST_APP_ID
|
||||
} else {
|
||||
return foundId
|
||||
}
|
||||
}
|
||||
|
||||
function getContextDB(key, opts) {
|
||||
const dbOptsKey = `${key}${ContextKeys.DB_OPTS}`
|
||||
let storedOpts = cls.getFromContext(dbOptsKey)
|
||||
let db = cls.getFromContext(key)
|
||||
if (db && isEqual(opts, storedOpts)) {
|
||||
return db
|
||||
}
|
||||
|
||||
const appId = exports.getAppId()
|
||||
let toUseAppId
|
||||
|
||||
switch (key) {
|
||||
case ContextKeys.CURRENT_DB:
|
||||
toUseAppId = appId
|
||||
break
|
||||
case ContextKeys.PROD_DB:
|
||||
toUseAppId = getProdAppID(appId)
|
||||
break
|
||||
case ContextKeys.DEV_DB:
|
||||
toUseAppId = getDevelopmentAppID(appId)
|
||||
break
|
||||
}
|
||||
|
||||
db = dangerousGetDB(toUseAppId, opts)
|
||||
try {
|
||||
cls.setOnContext(key, db)
|
||||
if (opts) {
|
||||
cls.setOnContext(dbOptsKey, opts)
|
||||
}
|
||||
} catch (err) {
|
||||
if (!env.isTest()) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the app database based on whatever the request
|
||||
* contained, dev or prod.
|
||||
*/
|
||||
exports.getAppDB = (opts = null) => {
|
||||
return getContextDB(ContextKeys.CURRENT_DB, opts)
|
||||
}
|
||||
|
||||
/**
|
||||
* This specifically gets the prod app ID, if the request
|
||||
* contained a development app ID, this will open the prod one.
|
||||
*/
|
||||
exports.getProdAppDB = (opts = null) => {
|
||||
return getContextDB(ContextKeys.PROD_DB, opts)
|
||||
}
|
||||
|
||||
/**
|
||||
* This specifically gets the dev app ID, if the request
|
||||
* contained a prod app ID, this will open the dev one.
|
||||
*/
|
||||
exports.getDevAppDB = (opts = null) => {
|
||||
return getContextDB(ContextKeys.DEV_DB, opts)
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
import env from "../environment"
|
||||
import { SEPARATOR, DocumentTypes } from "../db/constants"
|
||||
import cls from "./FunctionContext"
|
||||
import { dangerousGetDB, closeDB } from "../db"
|
||||
import { baseGlobalDBName } from "../tenancy/utils"
|
||||
import { IdentityContext } from "@budibase/types"
|
||||
import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants"
|
||||
import { ContextKeys } from "./constants"
|
||||
import {
|
||||
updateUsing,
|
||||
closeWithUsing,
|
||||
setAppTenantId,
|
||||
setIdentity,
|
||||
closeAppDBs,
|
||||
getContextDB,
|
||||
} from "./utils"
|
||||
|
||||
export const DEFAULT_TENANT_ID = _DEFAULT_TENANT_ID
|
||||
|
||||
// some test cases call functions directly, need to
|
||||
// store an app ID to pretend there is a context
|
||||
let TEST_APP_ID: string | null = null
|
||||
|
||||
export const closeTenancy = async () => {
|
||||
let db
|
||||
try {
|
||||
if (env.USE_COUCH) {
|
||||
db = getGlobalDB()
|
||||
}
|
||||
} catch (err) {
|
||||
// no DB found - skip closing
|
||||
return
|
||||
}
|
||||
await closeDB(db)
|
||||
// clear from context now that database is closed/task is finished
|
||||
cls.setOnContext(ContextKeys.TENANT_ID, null)
|
||||
cls.setOnContext(ContextKeys.GLOBAL_DB, null)
|
||||
}
|
||||
|
||||
// export const isDefaultTenant = () => {
|
||||
// return getTenantId() === DEFAULT_TENANT_ID
|
||||
// }
|
||||
|
||||
export const isMultiTenant = () => {
|
||||
return env.MULTI_TENANCY
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an app ID this will attempt to retrieve the tenant ID from it.
|
||||
* @return {null|string} The tenant ID found within the app ID.
|
||||
*/
|
||||
export const getTenantIDFromAppID = (appId: string) => {
|
||||
if (!appId) {
|
||||
return null
|
||||
}
|
||||
const split = appId.split(SEPARATOR)
|
||||
const hasDev = split[1] === DocumentTypes.DEV
|
||||
if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) {
|
||||
return null
|
||||
}
|
||||
if (hasDev) {
|
||||
return split[2]
|
||||
} else {
|
||||
return split[1]
|
||||
}
|
||||
}
|
||||
|
||||
// used for automations, API endpoints should always be in context already
|
||||
export const doInTenant = (tenantId: string | null, task: any) => {
|
||||
// the internal function is so that we can re-use an existing
|
||||
// context - don't want to close DB on a parent context
|
||||
async function internal(opts = { existing: false }) {
|
||||
// set the tenant id + global db if this is a new context
|
||||
if (!opts.existing) {
|
||||
updateTenantId(tenantId)
|
||||
}
|
||||
|
||||
try {
|
||||
// invoke the task
|
||||
return await task()
|
||||
} finally {
|
||||
await closeWithUsing(ContextKeys.TENANCY_IN_USE, () => {
|
||||
return closeTenancy()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const existing = cls.getFromContext(ContextKeys.TENANT_ID) === tenantId
|
||||
return updateUsing(ContextKeys.TENANCY_IN_USE, existing, internal)
|
||||
}
|
||||
|
||||
export const doInAppContext = (appId: string, task: any) => {
|
||||
if (!appId) {
|
||||
throw new Error("appId is required")
|
||||
}
|
||||
|
||||
const identity = getIdentity()
|
||||
|
||||
// the internal function is so that we can re-use an existing
|
||||
// context - don't want to close DB on a parent context
|
||||
async function internal(opts = { existing: false }) {
|
||||
// set the app tenant id
|
||||
if (!opts.existing) {
|
||||
setAppTenantId(appId)
|
||||
}
|
||||
// set the app ID
|
||||
cls.setOnContext(ContextKeys.APP_ID, appId)
|
||||
|
||||
// preserve the identity
|
||||
if (identity) {
|
||||
setIdentity(identity)
|
||||
}
|
||||
try {
|
||||
// invoke the task
|
||||
return await task()
|
||||
} finally {
|
||||
await closeWithUsing(ContextKeys.APP_IN_USE, async () => {
|
||||
await closeAppDBs()
|
||||
await closeTenancy()
|
||||
})
|
||||
}
|
||||
}
|
||||
const existing = cls.getFromContext(ContextKeys.APP_ID) === appId
|
||||
return updateUsing(ContextKeys.APP_IN_USE, existing, internal)
|
||||
}
|
||||
|
||||
export const doInIdentityContext = (identity: IdentityContext, task: any) => {
|
||||
if (!identity) {
|
||||
throw new Error("identity is required")
|
||||
}
|
||||
|
||||
async function internal(opts = { existing: false }) {
|
||||
if (!opts.existing) {
|
||||
cls.setOnContext(ContextKeys.IDENTITY, identity)
|
||||
// set the tenant so that doInTenant will preserve identity
|
||||
if (identity.tenantId) {
|
||||
updateTenantId(identity.tenantId)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// invoke the task
|
||||
return await task()
|
||||
} finally {
|
||||
await closeWithUsing(ContextKeys.IDENTITY_IN_USE, async () => {
|
||||
setIdentity(null)
|
||||
await closeTenancy()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const existing = cls.getFromContext(ContextKeys.IDENTITY)
|
||||
return updateUsing(ContextKeys.IDENTITY_IN_USE, existing, internal)
|
||||
}
|
||||
|
||||
export const getIdentity = (): IdentityContext | undefined => {
|
||||
try {
|
||||
return cls.getFromContext(ContextKeys.IDENTITY)
|
||||
} catch (e) {
|
||||
// do nothing - identity is not in context
|
||||
}
|
||||
}
|
||||
|
||||
export const updateTenantId = (tenantId: string | null) => {
|
||||
cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
|
||||
if (env.USE_COUCH) {
|
||||
setGlobalDB(tenantId)
|
||||
}
|
||||
}
|
||||
|
||||
export const updateAppId = async (appId: string) => {
|
||||
try {
|
||||
// have to close first, before removing the databases from context
|
||||
await closeAppDBs()
|
||||
cls.setOnContext(ContextKeys.APP_ID, appId)
|
||||
} catch (err) {
|
||||
if (env.isTest()) {
|
||||
TEST_APP_ID = appId
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const setGlobalDB = (tenantId: string | null) => {
|
||||
const dbName = baseGlobalDBName(tenantId)
|
||||
const db = dangerousGetDB(dbName)
|
||||
cls.setOnContext(ContextKeys.GLOBAL_DB, db)
|
||||
return db
|
||||
}
|
||||
|
||||
export const getGlobalDB = () => {
|
||||
const db = cls.getFromContext(ContextKeys.GLOBAL_DB)
|
||||
if (!db) {
|
||||
throw new Error("Global DB not found")
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
export const isTenantIdSet = () => {
|
||||
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID)
|
||||
return !!tenantId
|
||||
}
|
||||
|
||||
export const getTenantId = () => {
|
||||
if (!isMultiTenant()) {
|
||||
return DEFAULT_TENANT_ID
|
||||
}
|
||||
const tenantId = cls.getFromContext(ContextKeys.TENANT_ID)
|
||||
if (!tenantId) {
|
||||
throw new Error("Tenant id not found")
|
||||
}
|
||||
return tenantId
|
||||
}
|
||||
|
||||
export const getAppId = () => {
|
||||
const foundId = cls.getFromContext(ContextKeys.APP_ID)
|
||||
if (!foundId && env.isTest() && TEST_APP_ID) {
|
||||
return TEST_APP_ID
|
||||
} else {
|
||||
return foundId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the app database based on whatever the request
|
||||
* contained, dev or prod.
|
||||
*/
|
||||
export const getAppDB = (opts?: any) => {
|
||||
return getContextDB(ContextKeys.CURRENT_DB, opts)
|
||||
}
|
||||
|
||||
/**
|
||||
* This specifically gets the prod app ID, if the request
|
||||
* contained a development app ID, this will open the prod one.
|
||||
*/
|
||||
export const getProdAppDB = (opts?: any) => {
|
||||
return getContextDB(ContextKeys.PROD_DB, opts)
|
||||
}
|
||||
|
||||
/**
|
||||
* This specifically gets the dev app ID, if the request
|
||||
* contained a prod app ID, this will open the dev one.
|
||||
*/
|
||||
export const getDevAppDB = (opts?: any) => {
|
||||
return getContextDB(ContextKeys.DEV_DB, opts)
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
import "../../../tests/utilities/TestConfiguration"
|
||||
import * as context from ".."
|
||||
import { DEFAULT_TENANT_ID } from "../../constants"
|
||||
import env from "../../environment"
|
||||
|
||||
// must use require to spy index file exports due to known issue in jest
|
||||
const dbUtils = require("../../db")
|
||||
jest.spyOn(dbUtils, "closeDB")
|
||||
jest.spyOn(dbUtils, "dangerousGetDB")
|
||||
|
||||
describe("context", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("doInTenant", () => {
|
||||
describe("single-tenancy", () => {
|
||||
it("defaults to the default tenant", () => {
|
||||
const tenantId = context.getTenantId()
|
||||
expect(tenantId).toBe(DEFAULT_TENANT_ID)
|
||||
})
|
||||
|
||||
it("defaults to the default tenant db", async () => {
|
||||
await context.doInTenant(DEFAULT_TENANT_ID, () => {
|
||||
const db = context.getGlobalDB()
|
||||
expect(db.name).toBe("global-db")
|
||||
})
|
||||
expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1)
|
||||
expect(dbUtils.closeDB).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("multi-tenancy", () => {
|
||||
beforeEach(() => {
|
||||
env._set("MULTI_TENANCY", 1)
|
||||
})
|
||||
|
||||
it("fails when no tenant id is set", () => {
|
||||
const test = () => {
|
||||
let error
|
||||
try {
|
||||
context.getTenantId()
|
||||
} catch (e: any) {
|
||||
error = e
|
||||
}
|
||||
expect(error.message).toBe("Tenant id not found")
|
||||
}
|
||||
|
||||
// test under no tenancy
|
||||
test()
|
||||
|
||||
// test after tenancy has been accessed to ensure cleanup
|
||||
context.doInTenant("test", () => {})
|
||||
test()
|
||||
})
|
||||
|
||||
it("fails when no tenant db is set", () => {
|
||||
const test = () => {
|
||||
let error
|
||||
try {
|
||||
context.getGlobalDB()
|
||||
} catch (e: any) {
|
||||
error = e
|
||||
}
|
||||
expect(error.message).toBe("Global DB not found")
|
||||
}
|
||||
|
||||
// test under no tenancy
|
||||
test()
|
||||
|
||||
// test after tenancy has been accessed to ensure cleanup
|
||||
context.doInTenant("test", () => {})
|
||||
test()
|
||||
})
|
||||
|
||||
it("sets tenant id", () => {
|
||||
context.doInTenant("test", () => {
|
||||
const tenantId = context.getTenantId()
|
||||
expect(tenantId).toBe("test")
|
||||
})
|
||||
})
|
||||
|
||||
it("initialises the tenant db", async () => {
|
||||
await context.doInTenant("test", () => {
|
||||
const db = context.getGlobalDB()
|
||||
expect(db.name).toBe("test_global-db")
|
||||
})
|
||||
expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1)
|
||||
expect(dbUtils.closeDB).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("sets the tenant id when nested with same tenant id", async () => {
|
||||
await context.doInTenant("test", async () => {
|
||||
const tenantId = context.getTenantId()
|
||||
expect(tenantId).toBe("test")
|
||||
|
||||
await context.doInTenant("test", async () => {
|
||||
const tenantId = context.getTenantId()
|
||||
expect(tenantId).toBe("test")
|
||||
|
||||
await context.doInTenant("test", () => {
|
||||
const tenantId = context.getTenantId()
|
||||
expect(tenantId).toBe("test")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("initialises the tenant db when nested with same tenant id", async () => {
|
||||
await context.doInTenant("test", async () => {
|
||||
const db = context.getGlobalDB()
|
||||
expect(db.name).toBe("test_global-db")
|
||||
|
||||
await context.doInTenant("test", async () => {
|
||||
const db = context.getGlobalDB()
|
||||
expect(db.name).toBe("test_global-db")
|
||||
|
||||
await context.doInTenant("test", () => {
|
||||
const db = context.getGlobalDB()
|
||||
expect(db.name).toBe("test_global-db")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// only 1 db is opened and closed
|
||||
expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1)
|
||||
expect(dbUtils.closeDB).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("sets different tenant id inside another context", () => {
|
||||
context.doInTenant("test", () => {
|
||||
const tenantId = context.getTenantId()
|
||||
expect(tenantId).toBe("test")
|
||||
|
||||
context.doInTenant("nested", () => {
|
||||
const tenantId = context.getTenantId()
|
||||
expect(tenantId).toBe("nested")
|
||||
|
||||
context.doInTenant("double-nested", () => {
|
||||
const tenantId = context.getTenantId()
|
||||
expect(tenantId).toBe("double-nested")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,113 @@
|
|||
import {
|
||||
DEFAULT_TENANT_ID,
|
||||
getAppId,
|
||||
getTenantIDFromAppID,
|
||||
updateTenantId,
|
||||
} from "./index"
|
||||
import cls from "./FunctionContext"
|
||||
import { IdentityContext } from "@budibase/types"
|
||||
import { ContextKeys } from "./constants"
|
||||
import { dangerousGetDB, closeDB } from "../db"
|
||||
import { isEqual } from "lodash"
|
||||
import { getDevelopmentAppID, getProdAppID } from "../db/conversions"
|
||||
import env from "../environment"
|
||||
|
||||
export async function updateUsing(
|
||||
usingKey: string,
|
||||
existing: boolean,
|
||||
internal: (opts: { existing: boolean }) => Promise<any>
|
||||
) {
|
||||
const using = cls.getFromContext(usingKey)
|
||||
if (using && existing) {
|
||||
cls.setOnContext(usingKey, using + 1)
|
||||
return internal({ existing: true })
|
||||
} else {
|
||||
return cls.run(async () => {
|
||||
cls.setOnContext(usingKey, 1)
|
||||
return internal({ existing: false })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function closeWithUsing(
|
||||
usingKey: string,
|
||||
closeFn: () => Promise<any>
|
||||
) {
|
||||
const using = cls.getFromContext(usingKey)
|
||||
if (!using || using <= 1) {
|
||||
await closeFn()
|
||||
} else {
|
||||
cls.setOnContext(usingKey, using - 1)
|
||||
}
|
||||
}
|
||||
|
||||
export const setAppTenantId = (appId: string) => {
|
||||
const appTenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID
|
||||
updateTenantId(appTenantId)
|
||||
}
|
||||
|
||||
export const setIdentity = (identity: IdentityContext | null) => {
|
||||
cls.setOnContext(ContextKeys.IDENTITY, identity)
|
||||
}
|
||||
|
||||
// this function makes sure the PouchDB objects are closed and
|
||||
// fully deleted when finished - this protects against memory leaks
|
||||
export async function closeAppDBs() {
|
||||
const dbKeys = [
|
||||
ContextKeys.CURRENT_DB,
|
||||
ContextKeys.PROD_DB,
|
||||
ContextKeys.DEV_DB,
|
||||
]
|
||||
for (let dbKey of dbKeys) {
|
||||
const db = cls.getFromContext(dbKey)
|
||||
if (!db) {
|
||||
continue
|
||||
}
|
||||
await closeDB(db)
|
||||
// clear the DB from context, incase someone tries to use it again
|
||||
cls.setOnContext(dbKey, null)
|
||||
}
|
||||
// clear the app ID now that the databases are closed
|
||||
if (cls.getFromContext(ContextKeys.APP_ID)) {
|
||||
cls.setOnContext(ContextKeys.APP_ID, null)
|
||||
}
|
||||
if (cls.getFromContext(ContextKeys.DB_OPTS)) {
|
||||
cls.setOnContext(ContextKeys.DB_OPTS, null)
|
||||
}
|
||||
}
|
||||
|
||||
export function getContextDB(key: string, opts: any) {
|
||||
const dbOptsKey = `${key}${ContextKeys.DB_OPTS}`
|
||||
let storedOpts = cls.getFromContext(dbOptsKey)
|
||||
let db = cls.getFromContext(key)
|
||||
if (db && isEqual(opts, storedOpts)) {
|
||||
return db
|
||||
}
|
||||
|
||||
const appId = getAppId()
|
||||
let toUseAppId
|
||||
|
||||
switch (key) {
|
||||
case ContextKeys.CURRENT_DB:
|
||||
toUseAppId = appId
|
||||
break
|
||||
case ContextKeys.PROD_DB:
|
||||
toUseAppId = getProdAppID(appId)
|
||||
break
|
||||
case ContextKeys.DEV_DB:
|
||||
toUseAppId = getDevelopmentAppID(appId)
|
||||
break
|
||||
}
|
||||
db = dangerousGetDB(toUseAppId, opts)
|
||||
try {
|
||||
cls.setOnContext(key, db)
|
||||
if (opts) {
|
||||
cls.setOnContext(dbOptsKey, opts)
|
||||
}
|
||||
} catch (err) {
|
||||
if (!env.isTest()) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
return db
|
||||
}
|
|
@ -11,7 +11,7 @@ export enum AutomationViewModes {
|
|||
}
|
||||
|
||||
export enum ViewNames {
|
||||
USER_BY_EMAIL = "by_email",
|
||||
USER_BY_EMAIL = "by_email2",
|
||||
BY_API_KEY = "by_api_key",
|
||||
USER_BY_BUILDERS = "by_builders",
|
||||
LINK = "by_link",
|
||||
|
@ -19,6 +19,13 @@ export enum ViewNames {
|
|||
AUTOMATION_LOGS = "automation_logs",
|
||||
}
|
||||
|
||||
export const DeprecatedViews = {
|
||||
[ViewNames.USER_BY_EMAIL]: [
|
||||
// removed due to inaccuracy in view doc filter logic
|
||||
"by_email",
|
||||
],
|
||||
}
|
||||
|
||||
export enum DocumentTypes {
|
||||
USER = "us",
|
||||
WORKSPACE = "workspace",
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
const pouch = require("./pouch")
|
||||
const env = require("../environment")
|
||||
|
||||
const openDbs = []
|
||||
let PouchDB
|
||||
let initialised = false
|
||||
const dbList = new Set()
|
||||
|
||||
if (env.MEMORY_LEAK_CHECK) {
|
||||
setInterval(() => {
|
||||
console.log("--- OPEN DBS ---")
|
||||
console.log(openDbs)
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
const put =
|
||||
dbPut =>
|
||||
async (doc, options = {}) => {
|
||||
|
@ -35,6 +43,9 @@ exports.dangerousGetDB = (dbName, opts) => {
|
|||
dbList.add(dbName)
|
||||
}
|
||||
const db = new PouchDB(dbName, opts)
|
||||
if (env.MEMORY_LEAK_CHECK) {
|
||||
openDbs.push(db.name)
|
||||
}
|
||||
const dbPut = db.put
|
||||
db.put = put(dbPut)
|
||||
return db
|
||||
|
@ -46,6 +57,9 @@ exports.closeDB = async db => {
|
|||
if (!db || env.isTest()) {
|
||||
return
|
||||
}
|
||||
if (env.MEMORY_LEAK_CHECK) {
|
||||
openDbs.splice(openDbs.indexOf(db.name), 1)
|
||||
}
|
||||
try {
|
||||
// specifically await so that if there is an error, it can be ignored
|
||||
return await db.close()
|
||||
|
|
|
@ -1,20 +1,42 @@
|
|||
const { DocumentTypes, ViewNames } = require("./utils")
|
||||
const {
|
||||
DocumentTypes,
|
||||
ViewNames,
|
||||
DeprecatedViews,
|
||||
SEPARATOR,
|
||||
} = require("./utils")
|
||||
const { getGlobalDB } = require("../tenancy")
|
||||
|
||||
const DESIGN_DB = "_design/database"
|
||||
|
||||
function DesignDoc() {
|
||||
return {
|
||||
_id: "_design/database",
|
||||
_id: DESIGN_DB,
|
||||
// view collation information, read before writing any complex views:
|
||||
// https://docs.couchdb.org/en/master/ddocs/views/collation.html#collation-specification
|
||||
views: {},
|
||||
}
|
||||
}
|
||||
|
||||
exports.createUserEmailView = async () => {
|
||||
async function removeDeprecated(db, viewName) {
|
||||
if (!DeprecatedViews[viewName]) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const designDoc = await db.get(DESIGN_DB)
|
||||
for (let deprecatedNames of DeprecatedViews[viewName]) {
|
||||
delete designDoc.views[deprecatedNames]
|
||||
}
|
||||
await db.put(designDoc)
|
||||
} catch (err) {
|
||||
// doesn't exist, ignore
|
||||
}
|
||||
}
|
||||
|
||||
exports.createNewUserEmailView = async () => {
|
||||
const db = getGlobalDB()
|
||||
let designDoc
|
||||
try {
|
||||
designDoc = await db.get("_design/database")
|
||||
designDoc = await db.get(DESIGN_DB)
|
||||
} catch (err) {
|
||||
// no design doc, make one
|
||||
designDoc = DesignDoc()
|
||||
|
@ -22,7 +44,7 @@ exports.createUserEmailView = async () => {
|
|||
const view = {
|
||||
// if using variables in a map function need to inject them before use
|
||||
map: `function(doc) {
|
||||
if (doc._id.startsWith("${DocumentTypes.USER}")) {
|
||||
if (doc._id.startsWith("${DocumentTypes.USER}${SEPARATOR}")) {
|
||||
emit(doc.email.toLowerCase(), doc._id)
|
||||
}
|
||||
}`,
|
||||
|
@ -81,7 +103,7 @@ exports.createUserBuildersView = async () => {
|
|||
|
||||
exports.queryGlobalView = async (viewName, params, db = null) => {
|
||||
const CreateFuncByName = {
|
||||
[ViewNames.USER_BY_EMAIL]: exports.createUserEmailView,
|
||||
[ViewNames.USER_BY_EMAIL]: exports.createNewUserEmailView,
|
||||
[ViewNames.BY_API_KEY]: exports.createApiKeyView,
|
||||
[ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView,
|
||||
}
|
||||
|
@ -98,6 +120,7 @@ exports.queryGlobalView = async (viewName, params, db = null) => {
|
|||
} catch (err) {
|
||||
if (err != null && err.name === "not_found") {
|
||||
const createFunc = CreateFuncByName[viewName]
|
||||
await removeDeprecated(db, viewName)
|
||||
await createFunc()
|
||||
return exports.queryGlobalView(viewName, params)
|
||||
} else {
|
||||
|
|
|
@ -54,6 +54,7 @@ const env = {
|
|||
DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE,
|
||||
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
|
||||
SERVICE: process.env.SERVICE || "budibase",
|
||||
MEMORY_LEAK_CHECK: process.env.MEMORY_LEAK_CHECK || false,
|
||||
DEPLOYMENT_ENVIRONMENT:
|
||||
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose",
|
||||
_set(key: any, value: any) {
|
||||
|
|
|
@ -2,7 +2,7 @@ import PostHog from "posthog-node"
|
|||
import { Event, Identity, Group, BaseEvent } from "@budibase/types"
|
||||
import { EventProcessor } from "./types"
|
||||
import env from "../../environment"
|
||||
import context from "../../context"
|
||||
import * as context from "../../context"
|
||||
const pkg = require("../../../package.json")
|
||||
|
||||
export default class PosthogProcessor implements EventProcessor {
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
getGlobalDBName,
|
||||
getTenantId,
|
||||
} from "../tenancy"
|
||||
import context from "../context"
|
||||
import * as context from "../context"
|
||||
import { DEFINITIONS } from "."
|
||||
import {
|
||||
Migration,
|
||||
|
|
|
@ -764,6 +764,11 @@
|
|||
"@types/koa-compose" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/lodash@4.14.180":
|
||||
version "4.14.180"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670"
|
||||
integrity sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g==
|
||||
|
||||
"@types/mime@^1":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.0.220-alpha.4",
|
||||
"version": "1.1.10-alpha.3",
|
||||
"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.220-alpha.4",
|
||||
"@budibase/string-templates": "^1.1.10-alpha.3",
|
||||
"@spectrum-css/actionbutton": "^1.0.1",
|
||||
"@spectrum-css/actiongroup": "^1.0.1",
|
||||
"@spectrum-css/avatar": "^3.0.2",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.0.220-alpha.4",
|
||||
"version": "1.1.10-alpha.3",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -69,10 +69,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.220-alpha.4",
|
||||
"@budibase/client": "^1.0.220-alpha.4",
|
||||
"@budibase/frontend-core": "^1.0.220-alpha.4",
|
||||
"@budibase/string-templates": "^1.0.220-alpha.4",
|
||||
"@budibase/bbui": "^1.1.10-alpha.3",
|
||||
"@budibase/client": "^1.1.10-alpha.3",
|
||||
"@budibase/frontend-core": "^1.1.10-alpha.3",
|
||||
"@budibase/string-templates": "^1.1.10-alpha.3",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -96,7 +96,7 @@
|
|||
onSelect(block)
|
||||
}}
|
||||
>
|
||||
<Icon name={blockComplete ? "ChevronUp" : "ChevronDown"} />
|
||||
<Icon hoverable name={blockComplete ? "ChevronUp" : "ChevronDown"} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -67,27 +67,20 @@
|
|||
{/if}
|
||||
|
||||
<div class="tabs">
|
||||
<Tabs quiet noPadding selected="Input">
|
||||
<Tabs noHorizPadding selected="Input">
|
||||
<Tab title="Input">
|
||||
<div style="padding: 10px 10px 10px 10px;">
|
||||
<TextArea
|
||||
minHeight="80px"
|
||||
disabled
|
||||
value={textArea(filteredResults?.[idx]?.inputs, "No input")}
|
||||
/>
|
||||
</div></Tab
|
||||
>
|
||||
</Tab>
|
||||
<Tab title="Output">
|
||||
<div style="padding: 10px 10px 10px 10px;">
|
||||
<TextArea
|
||||
minHeight="100px"
|
||||
disabled
|
||||
value={textArea(
|
||||
filteredResults?.[idx]?.outputs,
|
||||
"No output"
|
||||
)}
|
||||
value={textArea(filteredResults?.[idx]?.outputs, "No output")}
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
@ -113,6 +106,7 @@
|
|||
align-items: stretch;
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
padding: 0 var(--spacing-xl) var(--spacing-xl) var(--spacing-xl);
|
||||
}
|
||||
|
||||
.block {
|
||||
|
|
|
@ -69,7 +69,14 @@
|
|||
|
||||
{#if !hideIcon}
|
||||
<div class="icon-wrapper" class:highlight={updateAvailable}>
|
||||
<Icon name="Refresh" hoverable on:click={updateModal.show} />
|
||||
<Icon
|
||||
name="Refresh"
|
||||
hoverable
|
||||
on:click={updateModal.show}
|
||||
tooltip={updateAvailable
|
||||
? "An update is available"
|
||||
: "No updates are available"}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<Modal bind:this={updateModal}>
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
Button,
|
||||
Layout,
|
||||
DrawerContent,
|
||||
ActionMenu,
|
||||
MenuItem,
|
||||
ActionButton,
|
||||
Search,
|
||||
} from "@budibase/bbui"
|
||||
import { getAvailableActions } from "./index"
|
||||
import { generate } from "shortid"
|
||||
|
@ -22,8 +22,24 @@
|
|||
export let actions
|
||||
export let bindings = []
|
||||
|
||||
$: showAvailableActions = !actions?.length
|
||||
|
||||
let actionQuery
|
||||
$: parsedQuery =
|
||||
typeof actionQuery === "string" ? actionQuery.toLowerCase().trim() : ""
|
||||
|
||||
let selectedAction = actions?.length ? actions[0] : null
|
||||
|
||||
$: mappedActionTypes = actionTypes.reduce((acc, action) => {
|
||||
let parsedName = action.name.toLowerCase().trim()
|
||||
if (parsedQuery.length && parsedName.indexOf(parsedQuery) < 0) {
|
||||
return acc
|
||||
}
|
||||
acc[action.type] = acc[action.type] || []
|
||||
acc[action.type].push(action)
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
// These are ephemeral bindings which only exist while executing actions
|
||||
$: buttonContextBindings = getButtonContextBindings(
|
||||
$currentAsset,
|
||||
|
@ -61,7 +77,12 @@
|
|||
actions = actions
|
||||
}
|
||||
|
||||
const addAction = actionType => () => {
|
||||
const toggleActionList = () => {
|
||||
actionQuery = null
|
||||
showAvailableActions = !showAvailableActions
|
||||
}
|
||||
|
||||
const addAction = actionType => {
|
||||
const newAction = {
|
||||
parameters: {},
|
||||
[EVENT_TYPE_KEY]: actionType.name,
|
||||
|
@ -78,6 +99,11 @@
|
|||
selectedAction = action
|
||||
}
|
||||
|
||||
const onAddAction = actionType => {
|
||||
addAction(actionType)
|
||||
toggleActionList()
|
||||
}
|
||||
|
||||
function handleDndConsider(e) {
|
||||
actions = e.detail.items
|
||||
}
|
||||
|
@ -88,7 +114,39 @@
|
|||
|
||||
<DrawerContent>
|
||||
<Layout noPadding gap="S" slot="sidebar">
|
||||
{#if actions && actions.length > 0}
|
||||
{#if showAvailableActions || !actions?.length}
|
||||
<div class="actions-list">
|
||||
{#if actions?.length > 0}
|
||||
<div>
|
||||
<ActionButton
|
||||
secondary
|
||||
icon={"ArrowLeft"}
|
||||
on:click={toggleActionList}
|
||||
>
|
||||
Back
|
||||
</ActionButton>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="search-wrap">
|
||||
<Search placeholder="Search" bind:value={actionQuery} />
|
||||
</div>
|
||||
{#each Object.entries(mappedActionTypes) as [categoryId, category], idx}
|
||||
<div class="heading" class:top-entry={idx === 0}>{categoryId}</div>
|
||||
<ul>
|
||||
{#each category as actionType}
|
||||
<li on:click={onAddAction(actionType)}>
|
||||
<span class="action-name">{actionType.name}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if actions && actions.length > 0 && !showAvailableActions}
|
||||
<div>
|
||||
<Button secondary on:click={toggleActionList}>Add Action</Button>
|
||||
</div>
|
||||
<div
|
||||
class="actions"
|
||||
use:dndzone={{
|
||||
|
@ -120,17 +178,9 @@
|
|||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<ActionMenu>
|
||||
<Button slot="control" secondary>Add Action</Button>
|
||||
{#each actionTypes as actionType}
|
||||
<MenuItem on:click={addAction(actionType)}>
|
||||
{actionType.name}
|
||||
</MenuItem>
|
||||
{/each}
|
||||
</ActionMenu>
|
||||
</Layout>
|
||||
<Layout noPadding>
|
||||
{#if selectedActionComponent}
|
||||
{#if selectedActionComponent && !showAvailableActions}
|
||||
{#key selectedAction.id}
|
||||
<div class="selected-action-container">
|
||||
<svelte:component
|
||||
|
@ -152,13 +202,10 @@
|
|||
align-items: stretch;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.action-header {
|
||||
color: var(--spectrum-global-color-gray-700);
|
||||
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
background-color: var(--background);
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
|
@ -182,4 +229,55 @@
|
|||
.action-container.selected .action-header {
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
|
||||
.actions-list > * {
|
||||
padding-bottom: var(--spectrum-global-dimension-static-size-200);
|
||||
}
|
||||
|
||||
.actions-list .heading {
|
||||
padding-bottom: var(--spectrum-global-dimension-static-size-100);
|
||||
padding-top: var(--spectrum-global-dimension-static-size-50);
|
||||
}
|
||||
|
||||
.actions-list .heading.top-entry {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: var(--font-size-s);
|
||||
padding: var(--spacing-m);
|
||||
border-radius: 4px;
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
|
||||
border-color 130ms ease-in-out;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
li:not(:last-of-type) {
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
li :global(*) {
|
||||
transition: color 130ms ease-in-out;
|
||||
}
|
||||
li:hover {
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
background-color: var(--spectrum-global-color-gray-50);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-name {
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.heading {
|
||||
font-size: var(--font-size-s);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -69,9 +69,16 @@
|
|||
notifications.error("Error creating automation")
|
||||
}
|
||||
}
|
||||
|
||||
$: actionCount = value?.length
|
||||
$: actionText = `${actionCount || "No"} action${
|
||||
actionCount !== 1 ? "s" : ""
|
||||
} set`
|
||||
</script>
|
||||
|
||||
<div class="action-count">{actionText}</div>
|
||||
<ActionButton on:click={openDrawer}>Define actions</ActionButton>
|
||||
|
||||
<Drawer bind:this={drawer} title={"Actions"}>
|
||||
<svelte:fragment slot="description">
|
||||
Define what actions to run.
|
||||
|
@ -85,3 +92,10 @@
|
|||
{key}
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
<style>
|
||||
.action-count {
|
||||
padding-bottom: var(--spacing-s);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"actions": [
|
||||
{
|
||||
"name": "Save Row",
|
||||
"type": "data",
|
||||
"component": "SaveRow",
|
||||
"context": [
|
||||
{
|
||||
|
@ -12,6 +13,7 @@
|
|||
},
|
||||
{
|
||||
"name": "Duplicate Row",
|
||||
"type": "data",
|
||||
"component": "DuplicateRow",
|
||||
"context": [
|
||||
{
|
||||
|
@ -22,14 +24,17 @@
|
|||
},
|
||||
{
|
||||
"name": "Delete Row",
|
||||
"type": "data",
|
||||
"component": "DeleteRow"
|
||||
},
|
||||
{
|
||||
"name": "Navigate To",
|
||||
"type": "application",
|
||||
"component": "NavigateTo"
|
||||
},
|
||||
{
|
||||
"name": "Execute Query",
|
||||
"type": "data",
|
||||
"component": "ExecuteQuery",
|
||||
"context": [
|
||||
{
|
||||
|
@ -40,43 +45,53 @@
|
|||
},
|
||||
{
|
||||
"name": "Trigger Automation",
|
||||
"type": "application",
|
||||
"component": "TriggerAutomation"
|
||||
},
|
||||
{
|
||||
"name": "Update Field Value",
|
||||
"type": "form",
|
||||
"component": "UpdateFieldValue"
|
||||
},
|
||||
{
|
||||
"name": "Validate Form",
|
||||
"type": "form",
|
||||
"component": "ValidateForm"
|
||||
},
|
||||
{
|
||||
"name": "Change Form Step",
|
||||
"type": "form",
|
||||
"component": "ChangeFormStep"
|
||||
},
|
||||
{
|
||||
"name": "Clear Form",
|
||||
"type": "form",
|
||||
"component": "ClearForm"
|
||||
},
|
||||
{
|
||||
"name": "Log Out",
|
||||
"type": "application",
|
||||
"component": "LogOut"
|
||||
},
|
||||
{
|
||||
"name": "Close Screen Modal",
|
||||
"type": "application",
|
||||
"component": "CloseScreenModal"
|
||||
},
|
||||
{
|
||||
"name": "Refresh Data Provider",
|
||||
"type": "data",
|
||||
"component": "RefreshDataProvider"
|
||||
},
|
||||
{
|
||||
"name": "Update State",
|
||||
"type": "data",
|
||||
"component": "UpdateState",
|
||||
"dependsOnFeature": "state"
|
||||
},
|
||||
{
|
||||
"name": "Upload File to S3",
|
||||
"type": "data",
|
||||
"component": "S3Upload",
|
||||
"context": [
|
||||
{
|
||||
|
@ -87,10 +102,12 @@
|
|||
},
|
||||
{
|
||||
"name": "Export Data",
|
||||
"type": "data",
|
||||
"component": "ExportData"
|
||||
},
|
||||
{
|
||||
"name": "Continue if / Stop if",
|
||||
"type": "logic",
|
||||
"component": "ContinueIf",
|
||||
"dependsOnFeature": "continueIfAction"
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
export let otherSources
|
||||
export let showAllQueries
|
||||
export let bindings = []
|
||||
export let showDataProviders = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const arrayTypes = ["attachment", "array"]
|
||||
|
@ -258,7 +259,7 @@
|
|||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if dataProviders?.length}
|
||||
{#if showDataProviders && dataProviders?.length}
|
||||
<Divider size="S" />
|
||||
<div class="title">
|
||||
<Heading size="XS">Data Providers</Heading>
|
||||
|
|
|
@ -4,4 +4,10 @@
|
|||
const otherSources = [{ name: "Custom", label: "Custom" }]
|
||||
</script>
|
||||
|
||||
<DataSourceSelect on:change {...$$props} showAllQueries={true} {otherSources} />
|
||||
<DataSourceSelect
|
||||
on:change
|
||||
{...$$props}
|
||||
showAllQueries={true}
|
||||
showDataProviders={false}
|
||||
{otherSources}
|
||||
/>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<ActionButton noPadding size="S" icon="Close" quiet on:click={close} />
|
||||
</div>
|
||||
</div>
|
||||
<Layout paddingX="XL" gap="S">
|
||||
<Layout paddingY="XL" paddingX="XL" gap="S">
|
||||
<div class="icon">
|
||||
<Icon name="Clock" />
|
||||
<DateTimeRenderer value={history.createdAt} />
|
||||
|
@ -71,7 +71,6 @@
|
|||
}
|
||||
|
||||
.bottom {
|
||||
margin-top: var(--spacing-m);
|
||||
border-top: var(--border-light);
|
||||
padding-top: calc(var(--spacing-xl) * 2);
|
||||
padding-bottom: calc(var(--spacing-xl) * 2);
|
||||
|
|
|
@ -138,7 +138,7 @@
|
|||
</script>
|
||||
|
||||
<div class="root" class:panelOpen={showPanel}>
|
||||
<Layout paddingX="XL" gap="S" alignContent="start">
|
||||
<Layout noPadding gap="M" alignContent="start">
|
||||
<div class="search">
|
||||
{#if licensePlan.type === "free"}
|
||||
<div>
|
||||
|
@ -172,6 +172,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{#if runHistory}
|
||||
<div>
|
||||
<Table
|
||||
on:click={viewDetails}
|
||||
schema={runHistorySchema}
|
||||
|
@ -181,7 +182,18 @@
|
|||
data={runHistory}
|
||||
{customRenderers}
|
||||
placeholderText="No history found"
|
||||
border={false}
|
||||
/>
|
||||
<div class="pagination">
|
||||
<Pagination
|
||||
page={$pageInfo.pageNumber}
|
||||
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage}
|
||||
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage}
|
||||
goToPrevPage={pageInfo.prevPage}
|
||||
goToNextPage={pageInfo.nextPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
<div class="panel" class:panelShow={showPanel}>
|
||||
|
@ -194,26 +206,19 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pagination">
|
||||
<Pagination
|
||||
page={$pageInfo.pageNumber}
|
||||
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage}
|
||||
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage}
|
||||
goToPrevPage={pageInfo.prevPage}
|
||||
goToNextPage={pageInfo.nextPage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
height: 100%;
|
||||
padding: var(--spectrum-alias-grid-gutter-medium)
|
||||
var(--spectrum-alias-grid-gutter-large);
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
gap: var(--spacing-xl);
|
||||
width: 100%;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
@ -223,15 +228,15 @@
|
|||
}
|
||||
|
||||
.pagination {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
margin-left: var(--spacing-l);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
margin-top: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: none;
|
||||
background-color: var(--background);
|
||||
margin-top: calc(-1 * var(--spectrum-alias-grid-gutter-medium));
|
||||
}
|
||||
|
||||
.panelShow {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { roles, flags } from "stores/backend"
|
||||
import { Icon, Tabs, Tab, Heading, notifications } from "@budibase/bbui"
|
||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||
import DeployNavigation from "components/deploy/DeployNavigation.svelte"
|
||||
import { API } from "api"
|
||||
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
||||
|
@ -107,6 +108,7 @@
|
|||
</Tabs>
|
||||
</div>
|
||||
<div class="toprightnav">
|
||||
<VersionModal />
|
||||
<RevertModal />
|
||||
<Icon
|
||||
name="Visibility"
|
||||
|
|
|
@ -28,12 +28,15 @@
|
|||
}
|
||||
drawer.hide()
|
||||
}
|
||||
|
||||
$: conditionCount = componentInstance?._conditions?.length
|
||||
$: conditionText = `${conditionCount || "No"} condition${
|
||||
conditionCount !== 1 ? "s" : ""
|
||||
} set`
|
||||
</script>
|
||||
|
||||
<DetailSummary
|
||||
name={`Conditions${componentInstance?._conditions ? " *" : ""}`}
|
||||
collapsible={false}
|
||||
>
|
||||
<DetailSummary name={"Conditions"} collapsible={false}>
|
||||
<div class="conditionCount">{conditionText}</div>
|
||||
<div>
|
||||
<ActionButton on:click={openDrawer}>Configure conditions</ActionButton>
|
||||
</div>
|
||||
|
@ -45,3 +48,10 @@
|
|||
<Button cta slot="buttons" on:click={() => save()}>Save</Button>
|
||||
<ConditionalUIDrawer slot="body" bind:conditions={tempValue} {bindings} />
|
||||
</Drawer>
|
||||
|
||||
<style>
|
||||
.conditionCount {
|
||||
font-weight: 600;
|
||||
margin-top: -5px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -207,11 +207,6 @@
|
|||
<span class="overview-wrap">
|
||||
<Page wide noPadding>
|
||||
{#await promise}
|
||||
<span class="page-header">
|
||||
<ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}>
|
||||
Back
|
||||
</ActionButton>
|
||||
</span>
|
||||
<div class="loading">
|
||||
<ProgressCircle size="XL" />
|
||||
</div>
|
||||
|
@ -400,7 +395,7 @@
|
|||
line-height: 1em;
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
.tab-wrap :global(.spectrum-Tabs) {
|
||||
.tab-wrap :global(> .spectrum-Tabs) {
|
||||
padding-left: var(--spectrum-alias-grid-gutter-large);
|
||||
padding-right: var(--spectrum-alias-grid-gutter-large);
|
||||
}
|
||||
|
|
|
@ -58,16 +58,16 @@
|
|||
</Layout>
|
||||
</span>
|
||||
<span class="version-section">
|
||||
<Layout gap="XS" paddingY="XXL" paddingX="">
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="S">App version</Heading>
|
||||
<Divider />
|
||||
<Body>
|
||||
{#if updateAvailable}
|
||||
<p class="version-status">
|
||||
<Body>
|
||||
The app is currently using version
|
||||
<strong>{$store.version}</strong>
|
||||
but version <strong>{clientPackage.version}</strong> is available.
|
||||
</p>
|
||||
</Body>
|
||||
{:else}
|
||||
<p class="version-status">
|
||||
The app is currently using version
|
||||
|
|
|
@ -90,8 +90,8 @@ export function createQueriesStore() {
|
|||
// Assume all the fields are strings and create a basic schema from the
|
||||
// unique fields returned by the server
|
||||
const schema = {}
|
||||
for (let field of result.schemaFields) {
|
||||
schema[field] = "string"
|
||||
for (let [field, type] of Object.entries(result.schemaFields)) {
|
||||
schema[field] = type || "string"
|
||||
}
|
||||
return { ...result, schema, rows: result.rows || [] }
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "1.0.220-alpha.4",
|
||||
"version": "1.1.10-alpha.3",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "1.0.220-alpha.4",
|
||||
"version": "1.1.10-alpha.3",
|
||||
"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.220-alpha.4",
|
||||
"@budibase/frontend-core": "^1.0.220-alpha.4",
|
||||
"@budibase/string-templates": "^1.0.220-alpha.4",
|
||||
"@budibase/bbui": "^1.1.10-alpha.3",
|
||||
"@budibase/frontend-core": "^1.1.10-alpha.3",
|
||||
"@budibase/string-templates": "^1.1.10-alpha.3",
|
||||
"@spectrum-css/button": "^3.0.3",
|
||||
"@spectrum-css/card": "^3.0.3",
|
||||
"@spectrum-css/divider": "^1.0.3",
|
||||
|
|
|
@ -323,6 +323,9 @@
|
|||
position: relative;
|
||||
padding: 32px;
|
||||
}
|
||||
.main.size--max {
|
||||
padding: 0;
|
||||
}
|
||||
.layout--none .main {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -465,6 +468,9 @@
|
|||
.mobile:not(.layout--none) .main {
|
||||
padding: 16px;
|
||||
}
|
||||
.mobile .main.size--max {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Transform links into drawer */
|
||||
.mobile .links {
|
||||
|
|
|
@ -98,7 +98,7 @@
|
|||
})
|
||||
}
|
||||
})
|
||||
return enrichedColumns.slice(0, 3)
|
||||
return enrichedColumns.slice(0, 5)
|
||||
}
|
||||
|
||||
// Builds a full details page URL for the card title
|
||||
|
|
|
@ -89,7 +89,7 @@
|
|||
})
|
||||
}
|
||||
})
|
||||
return enrichedColumns.slice(0, 3)
|
||||
return enrichedColumns.slice(0, 5)
|
||||
}
|
||||
|
||||
// Load the datasource schema so we can determine column types
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@budibase/frontend-core",
|
||||
"version": "1.0.220-alpha.4",
|
||||
"version": "1.1.10-alpha.3",
|
||||
"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.220-alpha.4",
|
||||
"@budibase/bbui": "^1.1.10-alpha.3",
|
||||
"lodash": "^4.17.21",
|
||||
"svelte": "^3.46.2"
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "1.0.220-alpha.4",
|
||||
"version": "1.1.10-alpha.3",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/index.ts",
|
||||
"repository": {
|
||||
|
@ -77,10 +77,11 @@
|
|||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "10.0.3",
|
||||
"@budibase/backend-core": "^1.0.220-alpha.4",
|
||||
"@budibase/client": "^1.0.220-alpha.4",
|
||||
"@budibase/pro": "1.0.220-alpha.4",
|
||||
"@budibase/string-templates": "^1.0.220-alpha.4",
|
||||
"@budibase/backend-core": "^1.1.10-alpha.3",
|
||||
"@budibase/client": "^1.1.10-alpha.3",
|
||||
"@budibase/pro": "1.1.10-alpha.3",
|
||||
"@budibase/string-templates": "^1.1.10-alpha.3",
|
||||
"@budibase/types": "^1.1.10-alpha.3",
|
||||
"@bull-board/api": "3.7.0",
|
||||
"@bull-board/koa": "3.9.4",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
|
@ -151,7 +152,6 @@
|
|||
"@babel/core": "7.17.4",
|
||||
"@babel/preset-env": "7.16.11",
|
||||
"@budibase/standard-components": "^0.9.139",
|
||||
"@budibase/types": "^1.0.220-alpha.4",
|
||||
"@jest/test-sequencer": "24.9.0",
|
||||
"@types/apidoc": "0.50.0",
|
||||
"@types/bson": "4.2.0",
|
||||
|
|
|
@ -22,9 +22,6 @@ const {
|
|||
BUILTIN_ROLE_IDS,
|
||||
AccessController,
|
||||
} = require("@budibase/backend-core/roles")
|
||||
import { BASE_LAYOUTS } from "../../constants/layouts"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
const { processObject } = require("@budibase/string-templates")
|
||||
const { CacheKeys, bustCache } = require("@budibase/backend-core/cache")
|
||||
const {
|
||||
getAllApps,
|
||||
|
@ -45,13 +42,8 @@ const { getTenantId, isMultiTenant } = require("@budibase/backend-core/tenancy")
|
|||
import { syncGlobalUsers } from "./user"
|
||||
const { app: appCache } = require("@budibase/backend-core/cache")
|
||||
import { cleanupAutomations } from "../../automations/utils"
|
||||
import { context } from "@budibase/backend-core"
|
||||
import { checkAppMetadata } from "../../automations/logging"
|
||||
const {
|
||||
getAppDB,
|
||||
getProdAppDB,
|
||||
updateAppId,
|
||||
doInAppContext,
|
||||
} = require("@budibase/backend-core/context")
|
||||
import { getUniqueRows } from "../../utilities/usageQuota/rows"
|
||||
import { quotas } from "@budibase/pro"
|
||||
import { errors, events, migrations } from "@budibase/backend-core"
|
||||
|
@ -61,7 +53,7 @@ const URL_REGEX_SLASH = /\/|\\/g
|
|||
|
||||
// utility function, need to do away with this
|
||||
async function getLayouts() {
|
||||
const db = getAppDB()
|
||||
const db = context.getAppDB()
|
||||
return (
|
||||
await db.allDocs(
|
||||
getLayoutParams(null, {
|
||||
|
@ -72,7 +64,7 @@ async function getLayouts() {
|
|||
}
|
||||
|
||||
async function getScreens() {
|
||||
const db = getAppDB()
|
||||
const db = context.getAppDB()
|
||||
return (
|
||||
await db.allDocs(
|
||||
getScreenParams(null, {
|
||||
|
@ -135,9 +127,9 @@ async function createInstance(template: any) {
|
|||
const tenantId = isMultiTenant() ? getTenantId() : null
|
||||
const baseAppId = generateAppID(tenantId)
|
||||
const appId = generateDevAppID(baseAppId)
|
||||
await updateAppId(appId)
|
||||
await context.updateAppId(appId)
|
||||
|
||||
const db = getAppDB()
|
||||
const db = context.getAppDB()
|
||||
await db.put({
|
||||
_id: "_design/database",
|
||||
// view collation information, read before writing any complex views:
|
||||
|
@ -213,7 +205,7 @@ export const fetchAppDefinition = async (ctx: any) => {
|
|||
}
|
||||
|
||||
export const fetchAppPackage = async (ctx: any) => {
|
||||
const db = getAppDB()
|
||||
const db = context.getAppDB()
|
||||
const application = await db.get(DocumentTypes.APP_METADATA)
|
||||
const layouts = await getLayouts()
|
||||
let screens = await getScreens()
|
||||
|
@ -252,7 +244,7 @@ const performAppCreate = async (ctx: any) => {
|
|||
const instance = await createInstance(instanceConfig)
|
||||
const appId = instance._id
|
||||
|
||||
const db = getAppDB()
|
||||
const db = context.getAppDB()
|
||||
let _rev
|
||||
try {
|
||||
// if template there will be an existing doc
|
||||
|
@ -390,7 +382,7 @@ export const update = async (ctx: any) => {
|
|||
|
||||
export const updateClient = async (ctx: any) => {
|
||||
// Get current app version
|
||||
const db = getAppDB()
|
||||
const db = context.getAppDB()
|
||||
const application = await db.get(DocumentTypes.APP_METADATA)
|
||||
const currentVersion = application.version
|
||||
|
||||
|
@ -414,7 +406,7 @@ export const updateClient = async (ctx: any) => {
|
|||
|
||||
export const revertClient = async (ctx: any) => {
|
||||
// Check app can be reverted
|
||||
const db = getAppDB()
|
||||
const db = context.getAppDB()
|
||||
const application = await db.get(DocumentTypes.APP_METADATA)
|
||||
if (!application.revertableVersion) {
|
||||
ctx.throw(400, "There is no version to revert to")
|
||||
|
@ -446,7 +438,7 @@ const destroyApp = async (ctx: any) => {
|
|||
appId = getProdAppID(appId)
|
||||
}
|
||||
|
||||
const db = isUnpublish ? getProdAppDB() : getAppDB()
|
||||
const db = isUnpublish ? context.getProdAppDB() : context.getAppDB()
|
||||
const app = await db.get(DocumentTypes.APP_METADATA)
|
||||
const result = await db.destroy()
|
||||
|
||||
|
@ -514,7 +506,7 @@ export const sync = async (ctx: any, next: any) => {
|
|||
|
||||
try {
|
||||
// specific case, want to make sure setup is skipped
|
||||
const prodDb = getProdAppDB({ skip_setup: true })
|
||||
const prodDb = context.getProdAppDB({ skip_setup: true })
|
||||
const info = await prodDb.info()
|
||||
if (info.error) throw info.error
|
||||
} catch (err) {
|
||||
|
@ -556,8 +548,8 @@ export const sync = async (ctx: any, next: any) => {
|
|||
}
|
||||
|
||||
const updateAppPackage = async (appPackage: any, appId: any) => {
|
||||
return doInAppContext(appId, async () => {
|
||||
const db = getAppDB()
|
||||
return context.doInAppContext(appId, async () => {
|
||||
const db = context.getAppDB()
|
||||
const application = await db.get(DocumentTypes.APP_METADATA)
|
||||
|
||||
const newAppPackage = { ...application, ...appPackage }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { generateQueryID, getQueryParams, isProdAppID } from "../../../db/utils"
|
||||
import { BaseQueryVerbs } from "../../../constants"
|
||||
import { BaseQueryVerbs, FieldTypes } from "../../../constants"
|
||||
import { Thread, ThreadType } from "../../../threads"
|
||||
import { save as saveDatasource } from "../datasource"
|
||||
import { RestImporter } from "./import"
|
||||
|
@ -154,10 +154,37 @@ export async function preview(ctx: any) {
|
|||
},
|
||||
})
|
||||
const { rows, keys, info, extra } = await quotas.addQuery(runFn)
|
||||
const schemaFields: any = {}
|
||||
if (rows?.length > 0) {
|
||||
for (let key of [...new Set(keys)] as string[]) {
|
||||
const field = rows[0][key]
|
||||
let type = typeof field,
|
||||
fieldType = FieldTypes.STRING
|
||||
if (field)
|
||||
switch (type) {
|
||||
case "boolean":
|
||||
schemaFields[key] = FieldTypes.BOOLEAN
|
||||
break
|
||||
case "object":
|
||||
if (field instanceof Date) {
|
||||
fieldType = FieldTypes.DATETIME
|
||||
} else if (Array.isArray(field)) {
|
||||
fieldType = FieldTypes.ARRAY
|
||||
} else {
|
||||
fieldType = FieldTypes.JSON
|
||||
}
|
||||
break
|
||||
case "number":
|
||||
fieldType = FieldTypes.NUMBER
|
||||
break
|
||||
}
|
||||
schemaFields[key] = fieldType
|
||||
}
|
||||
}
|
||||
await events.query.previewed(datasource, query)
|
||||
ctx.body = {
|
||||
rows,
|
||||
schemaFields: [...new Set(keys)],
|
||||
schemaFields,
|
||||
info,
|
||||
extra,
|
||||
}
|
||||
|
|
|
@ -215,7 +215,10 @@ describe("/queries", () => {
|
|||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
// these responses come from the mock
|
||||
expect(res.body.schemaFields).toEqual(["a", "b"])
|
||||
expect(res.body.schemaFields).toEqual({
|
||||
"a": "string",
|
||||
"b": "number",
|
||||
})
|
||||
expect(res.body.rows.length).toEqual(1)
|
||||
expect(events.query.previewed).toBeCalledTimes(1)
|
||||
expect(events.query.previewed).toBeCalledWith(datasource, query)
|
||||
|
@ -289,7 +292,11 @@ describe("/queries", () => {
|
|||
queryString: "test={{ variable2 }}",
|
||||
})
|
||||
// these responses come from the mock
|
||||
expect(res.body.schemaFields).toEqual(["url", "opts", "value"])
|
||||
expect(res.body.schemaFields).toEqual({
|
||||
"opts": "json",
|
||||
"url": "string",
|
||||
"value": "string",
|
||||
})
|
||||
expect(res.body.rows[0].url).toEqual("http://www.google.com?test=1")
|
||||
})
|
||||
|
||||
|
@ -299,7 +306,11 @@ describe("/queries", () => {
|
|||
path: "www.google.com",
|
||||
queryString: "test={{ variable3 }}",
|
||||
})
|
||||
expect(res.body.schemaFields).toEqual(["url", "opts", "value"])
|
||||
expect(res.body.schemaFields).toEqual({
|
||||
"opts": "json",
|
||||
"url": "string",
|
||||
"value": "string"
|
||||
})
|
||||
expect(res.body.rows[0].url).toContain("doctype html")
|
||||
})
|
||||
|
||||
|
@ -318,7 +329,11 @@ describe("/queries", () => {
|
|||
path: "www.failonce.com",
|
||||
queryString: "test={{ variable3 }}",
|
||||
})
|
||||
expect(res.body.schemaFields).toEqual(["fails", "url", "opts"])
|
||||
expect(res.body.schemaFields).toEqual({
|
||||
"fails": "number",
|
||||
"opts": "json",
|
||||
"url": "string"
|
||||
})
|
||||
expect(res.body.rows[0].fails).toEqual(1)
|
||||
})
|
||||
|
||||
|
|
|
@ -46,26 +46,26 @@ describe("/rows", () => {
|
|||
|
||||
describe("save, load, update", () => {
|
||||
it("returns a success message when the row is created", async () => {
|
||||
const rowUsage = await getRowUsage()
|
||||
const queryUsage = await getQueryUsage()
|
||||
|
||||
const res = await request
|
||||
.post(`/api/${row.tableId}/rows`)
|
||||
.send(row)
|
||||
.set(config.defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
expect(res.res.statusMessage).toEqual(`${table.name} saved successfully`)
|
||||
expect(res.body.name).toEqual("Test Contact")
|
||||
expect(res.body._rev).toBeDefined()
|
||||
await assertRowUsage(rowUsage + 1)
|
||||
await assertQueryUsage(queryUsage + 1)
|
||||
// const rowUsage = await getRowUsage()
|
||||
// const queryUsage = await getQueryUsage()
|
||||
//
|
||||
// const res = await request
|
||||
// .post(`/api/${row.tableId}/rows`)
|
||||
// .send(row)
|
||||
// .set(config.defaultHeaders())
|
||||
// .expect('Content-Type', /json/)
|
||||
// .expect(200)
|
||||
// expect(res.res.statusMessage).toEqual(`${table.name} saved successfully`)
|
||||
// expect(res.body.name).toEqual("Test Contact")
|
||||
// expect(res.body._rev).toBeDefined()
|
||||
// await assertRowUsage(rowUsage + 1)
|
||||
// await assertQueryUsage(queryUsage + 1)
|
||||
})
|
||||
|
||||
it("updates a row successfully", async () => {
|
||||
const existing = await config.createRow()
|
||||
const rowUsage = await getRowUsage()
|
||||
const queryUsage = await getQueryUsage()
|
||||
// const rowUsage = await getRowUsage()
|
||||
// const queryUsage = await getQueryUsage()
|
||||
|
||||
const res = await request
|
||||
.post(`/api/${table._id}/rows`)
|
||||
|
@ -81,8 +81,8 @@ describe("/rows", () => {
|
|||
|
||||
expect(res.res.statusMessage).toEqual(`${table.name} updated successfully.`)
|
||||
expect(res.body.name).toEqual("Updated Name")
|
||||
await assertRowUsage(rowUsage)
|
||||
await assertQueryUsage(queryUsage + 1)
|
||||
// await assertRowUsage(rowUsage)
|
||||
// await assertQueryUsage(queryUsage + 1)
|
||||
})
|
||||
|
||||
it("should load a row", async () => {
|
||||
|
|
|
@ -20,7 +20,6 @@ import redis from "./utilities/redis"
|
|||
import * as migrations from "./migrations"
|
||||
import { events, installation, tenancy } from "@budibase/backend-core"
|
||||
import { createAdminUser, getChecklist } from "./utilities/workerRequests"
|
||||
import { tenantSucceeded } from "@budibase/backend-core/dist/src/events/publishers/backfill"
|
||||
|
||||
const app = new Koa()
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ const INTEGRATIONS = {
|
|||
}
|
||||
|
||||
// optionally add oracle integration if the oracle binary can be installed
|
||||
if (!(process.arch === "arm64" && process.platform === "darwin")) {
|
||||
if (process.arch && !process.arch.startsWith("arm")) {
|
||||
const oracle = require("./oracle")
|
||||
DEFINITIONS[SourceNames.ORACLE] = oracle.schema
|
||||
INTEGRATIONS[SourceNames.ORACLE] = oracle.integration
|
||||
|
|
|
@ -106,21 +106,31 @@ class TestConfiguration {
|
|||
|
||||
// UTILS
|
||||
|
||||
async _req(config, params, controlFunc) {
|
||||
async _req(body, params, controlFunc, opts = { prodApp: false }) {
|
||||
// create a fake request ctx
|
||||
const request = {}
|
||||
|
||||
// set the app id
|
||||
let appId
|
||||
if (opts.prodApp) {
|
||||
appId = this.prodAppId
|
||||
} else {
|
||||
appId = this.appId
|
||||
}
|
||||
request.appId = appId
|
||||
|
||||
// fake cookies, we don't need them
|
||||
request.cookies = { set: () => {}, get: () => {} }
|
||||
request.config = { jwtSecret: env.JWT_SECRET }
|
||||
request.appId = this.appId
|
||||
request.user = { appId: this.appId, tenantId: TENANT_ID }
|
||||
request.user = { appId, tenantId: TENANT_ID }
|
||||
request.query = {}
|
||||
request.request = {
|
||||
body: config,
|
||||
body,
|
||||
}
|
||||
return this.doInContext(this.appId, async () => {
|
||||
if (params) {
|
||||
request.params = params
|
||||
}
|
||||
return this.doInContext(appId, async () => {
|
||||
await controlFunc(request)
|
||||
return request.body
|
||||
})
|
||||
|
@ -323,7 +333,6 @@ class TestConfiguration {
|
|||
|
||||
// create production app
|
||||
this.prodApp = await this.deploy()
|
||||
this.prodAppId = this.prodApp.appId
|
||||
|
||||
this.allApps.push(this.prodApp)
|
||||
this.allApps.push(this.app)
|
||||
|
@ -334,11 +343,13 @@ class TestConfiguration {
|
|||
async deploy() {
|
||||
await this._req(null, null, controllers.deploy.deployApp)
|
||||
const prodAppId = this.getAppId().replace("_dev", "")
|
||||
this.prodAppId = prodAppId
|
||||
return context.doInAppContext(prodAppId, async () => {
|
||||
const appPackage = await this._req(
|
||||
null,
|
||||
{ appId: prodAppId },
|
||||
controllers.app.fetchAppPackage
|
||||
controllers.app.fetchAppPackage,
|
||||
{ prodApp: true }
|
||||
)
|
||||
return appPackage.application
|
||||
})
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/string-templates",
|
||||
"version": "1.0.220-alpha.4",
|
||||
"version": "1.1.10-alpha.3",
|
||||
"description": "Handlebars wrapper for Budibase templating.",
|
||||
"main": "src/index.cjs",
|
||||
"module": "dist/bundle.mjs",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/types",
|
||||
"version": "1.0.220-alpha.4",
|
||||
"version": "1.1.10-alpha.3",
|
||||
"description": "Budibase types",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
|
@ -4,6 +4,7 @@ import { IdentityType } from "./events/identification"
|
|||
export interface BaseContext {
|
||||
_id: string
|
||||
type: IdentityType
|
||||
tenantId?: string
|
||||
}
|
||||
|
||||
export interface AccountUserContext extends BaseContext {
|
||||
|
@ -13,6 +14,7 @@ export interface AccountUserContext extends BaseContext {
|
|||
|
||||
export interface UserContext extends BaseContext, User {
|
||||
_id: string
|
||||
tenantId: string
|
||||
account?: Account
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/worker",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "1.0.220-alpha.4",
|
||||
"version": "1.1.10-alpha.3",
|
||||
"description": "Budibase background service",
|
||||
"main": "src/index.ts",
|
||||
"repository": {
|
||||
|
@ -17,6 +17,7 @@
|
|||
"postbuild": "copyfiles -u 1 src/**/*.hbs dist/",
|
||||
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
||||
"run:docker": "node dist/index.js",
|
||||
"debug": "yarn build && node --expose-gc --inspect=9223 dist/index.js",
|
||||
"run:docker:cluster": "pm2-runtime start pm2.config.js",
|
||||
"build:docker": "docker build . -t worker-service --label version=$BUDIBASE_RELEASE_VERSION",
|
||||
"dev:stack:init": "node ./scripts/dev/manage.js init",
|
||||
|
@ -34,9 +35,10 @@
|
|||
"author": "Budibase",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@budibase/backend-core": "^1.0.220-alpha.4",
|
||||
"@budibase/pro": "1.0.220-alpha.4",
|
||||
"@budibase/string-templates": "^1.0.220-alpha.4",
|
||||
"@budibase/backend-core": "^1.1.10-alpha.3",
|
||||
"@budibase/pro": "1.1.10-alpha.3",
|
||||
"@budibase/string-templates": "^1.1.10-alpha.3",
|
||||
"@budibase/types": "^1.1.10-alpha.3",
|
||||
"@koa/router": "8.0.8",
|
||||
"@sentry/node": "6.17.7",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
|
@ -66,7 +68,6 @@
|
|||
"server-destroy": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@budibase/types": "^1.0.220-alpha.4",
|
||||
"@types/jest": "26.0.23",
|
||||
"@types/koa": "2.13.4",
|
||||
"@types/koa-router": "7.4.4",
|
||||
|
|
|
@ -75,7 +75,7 @@ function buildConfigSaveValidation() {
|
|||
{ is: Configs.OIDC, then: oidcValidation() }
|
||||
],
|
||||
}),
|
||||
}).required(),
|
||||
}).required().unknown(true),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -84,14 +84,14 @@ function buildUploadValidation() {
|
|||
return joiValidator.params(Joi.object({
|
||||
type: Joi.string().valid(...Object.values(Configs)).required(),
|
||||
name: Joi.string().required(),
|
||||
}).required())
|
||||
}).required().unknown(true))
|
||||
}
|
||||
|
||||
function buildConfigGetValidation() {
|
||||
// prettier-ignore
|
||||
return joiValidator.params(Joi.object({
|
||||
type: Joi.string().valid(...Object.values(Configs)).required()
|
||||
}).unknown(true).required())
|
||||
}).required().unknown(true))
|
||||
}
|
||||
|
||||
router
|
||||
|
|
|
@ -69,7 +69,7 @@ router
|
|||
)
|
||||
.get("/api/global/users/tenant/:id", controller.tenantUserLookup)
|
||||
// global endpoint but needs to come at end (blocks other endpoints otherwise)
|
||||
.get("/api/global/users/:id", adminOnly, controller.find)
|
||||
.get("/api/global/users/:id", builderOrAdmin, controller.find)
|
||||
// DEPRECATED - use new versions with self API
|
||||
.get("/api/global/users/self", selfController.getSelf)
|
||||
.post(
|
||||
|
|
|
@ -19,8 +19,14 @@ describe("/api/global/email", () => {
|
|||
async function sendRealEmail(purpose) {
|
||||
let response, text
|
||||
try {
|
||||
await config.saveEtherealSmtpConfig()
|
||||
await config.saveSettingsConfig()
|
||||
const timeout = () => new Promise((resolve, reject) =>
|
||||
setTimeout(() => reject({
|
||||
status: 301,
|
||||
errno: "ETIME"
|
||||
}), 20000)
|
||||
)
|
||||
await Promise.race([config.saveEtherealSmtpConfig(), timeout()])
|
||||
await Promise.race([config.saveSettingsConfig(), timeout()])
|
||||
const user = await config.getUser("test@test.com")
|
||||
const res = await request
|
||||
.post(`/api/global/email/send`)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue