Merge remote-tracking branch 'origin/develop' into feat/user-groups-tab

This commit is contained in:
Peter Clement 2022-07-19 11:23:31 +01:00
commit ca41e88045
106 changed files with 5004 additions and 15655 deletions

View File

@ -1,11 +1,8 @@
name: Deploy Budibase Single Container Image to DockerHub name: Deploy Budibase Single Container Image to DockerHub
on: on:
push: workflow_dispatch:
branches:
- "omnibus-action"
- "develop"
- "master"
- "main"
env: env:
BASE_BRANCH: ${{ github.event.pull_request.base.ref}} BASE_BRANCH: ${{ github.event.pull_request.base.ref}}
BRANCH: ${{ github.event.pull_request.head.ref }} BRANCH: ${{ github.event.pull_request.head.ref }}
@ -40,7 +37,7 @@ jobs:
- name: Runt Yarn Lint - name: Runt Yarn Lint
run: yarn lint run: yarn lint
- name: Run Yarn Build - name: Run Yarn Build
run: yarn build run: yarn build:docker:pre
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
@ -60,3 +57,12 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }} tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }}
file: ./hosting/single/Dockerfile file: ./hosting/single/Dockerfile
- name: Tag and release Budibase Azure App Service docker image
uses: docker/build-push-action@v2
with:
context: .
push: true
platforms: linux/amd64
build-args: TARGETBUILD=aas
tags: budibase/budibase-aas,budibase/budibase-aas:v${{ env.RELEASE_VERSION }}
file: ./hosting/single/Dockerfile

View File

@ -68,7 +68,7 @@ jobs:
- name: Publish budibase packages to NPM - name: Publish budibase packages to NPM
env: env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
RELEASE_VERSION_TYPE: ${{ github.event.inputs.version }} RELEASE_VERSION_TYPE: ${{ github.event.inputs.versioning }}
run: | run: |
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default # 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" git config --global user.name "Budibase Release Bot"

View File

@ -135,13 +135,18 @@ You can learn more about the Budibase API at the following places:
## 🏁 Get started ## 🏁 Get started
<a href="https://docs.budibase.com/docs/hosting-methods"><img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" /></a>
Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean. Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean.
Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly. Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly.
### [Get started with self-hosting Budibase](https://docs.budibase.com/docs/hosting-methods) ### [Get started with self-hosting Budibase](https://docs.budibase.com/docs/hosting-methods)
- [Docker - single ARM compatible image](https://docs.budibase.com/docs/docker)
- [Docker Compose](https://docs.budibase.com/docs/docker-compose)
- [Kubernetes](https://docs.budibase.com/docs/kubernetes-k8s)
- [Digital Ocean](https://docs.budibase.com/docs/digitalocean)
- [Portainer](https://docs.budibase.com/docs/portainer)
### [Get started with Budibase Cloud](https://budibase.com) ### [Get started with Budibase Cloud](https://budibase.com)

View File

@ -151,6 +151,10 @@ spec:
tolerations: tolerations:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
{{ if .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
{{ end }}
restartPolicy: Always restartPolicy: Always
serviceAccountName: "" serviceAccountName: ""
status: {} status: {}

View File

@ -68,6 +68,10 @@ spec:
tolerations: tolerations:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
{{ if .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
{{ end }}
restartPolicy: Always restartPolicy: Always
serviceAccountName: "" serviceAccountName: ""
volumes: volumes:
@ -75,4 +79,4 @@ spec:
persistentVolumeClaim: persistentVolumeClaim:
claimName: minio-data claimName: minio-data
status: {} status: {}
{{- end }} {{- end }}

View File

@ -40,6 +40,10 @@ spec:
tolerations: tolerations:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
{{ if .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
{{ end }}
restartPolicy: Always restartPolicy: Always
serviceAccountName: "" serviceAccountName: ""
volumes: volumes:

View File

@ -47,6 +47,10 @@ spec:
tolerations: tolerations:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
{{ if .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
{{ end }}
restartPolicy: Always restartPolicy: Always
serviceAccountName: "" serviceAccountName: ""
volumes: volumes:
@ -54,4 +58,4 @@ spec:
persistentVolumeClaim: persistentVolumeClaim:
claimName: redis-data claimName: redis-data
status: {} status: {}
{{- end }} {{- end }}

View File

@ -145,6 +145,10 @@ spec:
tolerations: tolerations:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
{{ if .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
{{ end }}
restartPolicy: Always restartPolicy: Always
serviceAccountName: "" serviceAccountName: ""
status: {} status: {}

View File

@ -11,10 +11,11 @@ services:
- minio_data:/data - minio_data:/data
ports: ports:
- "${MINIO_PORT}:9000" - "${MINIO_PORT}:9000"
- "9001:9001"
environment: environment:
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
command: server /data command: server /data --console-address ":9001"
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s interval: 30s

View File

@ -63,7 +63,7 @@ services:
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
MINIO_BROWSER: "off" MINIO_BROWSER: "off"
command: server /data command: server /data --console-address ":9001"
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s interval: 30s

View File

@ -3,15 +3,15 @@
echo ${TARGETBUILD} > /buildtarget.txt echo ${TARGETBUILD} > /buildtarget.txt
if [[ "${TARGETBUILD}" = "aas" ]]; then if [[ "${TARGETBUILD}" = "aas" ]]; then
# Azure AppService uses /home for persisent data & SSH on port 2222 # Azure AppService uses /home for persisent data & SSH on port 2222
mkdir -p /home/budibase/{minio,couchdb} mkdir -p /home/{search,minio,couch}
mkdir -p /home/budibase/couchdb/data mkdir -p /home/couch/{dbs,views}
chown -R couchdb:couchdb /home/budibase/couchdb/ chown -R couchdb:couchdb /home/couch/
apt update apt update
apt-get install -y openssh-server apt-get install -y openssh-server
sed -i 's#dir=/opt/couchdb/data/search#dir=/home/budibase/couchdb/data/search#' /opt/clouseau/clouseau.ini sed -i 's#dir=/opt/couchdb/data/search#dir=/home/search#' /opt/clouseau/clouseau.ini
sed -i 's#/minio/minio server /minio &#/minio/minio server /home/budibase/minio &#' /runner.sh sed -i 's#/minio/minio server /minio &#/minio/minio server /home/minio &#' /runner.sh
sed -i 's#database_dir = ./data#database_dir = /home/budibase/couchdb/data#' /opt/couchdb/etc/default.ini sed -i 's#database_dir = ./data#database_dir = /home/couch/dbs#' /opt/couchdb/etc/default.ini
sed -i 's#view_index_dir = ./data#view_index_dir = /home/budibase/couchdb/data#' /opt/couchdb/etc/default.ini sed -i 's#view_index_dir = ./data#view_index_dir = /home/couch/views#' /opt/couchdb/etc/default.ini
sed -i "s/#Port 22/Port 2222/" /etc/ssh/sshd_config sed -i "s/#Port 22/Port 2222/" /etc/ssh/sshd_config
/etc/init.d/ssh restart /etc/init.d/ssh restart
fi fi

View File

@ -122,8 +122,7 @@ RUN yarn cache clean -f
EXPOSE 80 EXPOSE 80
EXPOSE 443 EXPOSE 443
VOLUME /opt/couchdb/data VOLUME /data
VOLUME /minio
# setup letsencrypt certificate # setup letsencrypt certificate
RUN apt-get install -y certbot python3-certbot-nginx RUN apt-get install -y certbot python3-certbot-nginx

View File

@ -24,8 +24,8 @@ if [ ! -f "/data/.env" ]; then
fi fi
# make these directories in runner, incase of mount # make these directories in runner, incase of mount
mkdir -p /data/couch/dbs /data/couch/views mkdir -p /data/couch/{dbs,views} /home/couch/{dbs,views}
chown couchdb:couchdb /data/couch /data/couch/dbs /data/couch/views chown -R couchdb:couchdb /data/couch /home/couch
redis-server --requirepass $REDIS_PASSWORD & redis-server --requirepass $REDIS_PASSWORD &
/opt/clouseau/bin/clouseau & /opt/clouseau/bin/clouseau &
/minio/minio server /data/minio & /minio/minio server /data/minio &

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.220-alpha.4", "version": "1.1.15-alpha.2",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -54,6 +54,7 @@
"test:e2e:ci:notify": "lerna run cy:ci:notify", "test:e2e:ci:notify": "lerna run cy:ci:notify",
"build:specs": "lerna run specs", "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": "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": "docker build hosting/proxy -t proxy-service",
"build:docker:proxy:compose": "node scripts/proxy/generateProxyConfig compose && npm run build:docker:proxy", "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", "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: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: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: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", "build:docs": "lerna run build:docs",
"release:helm": "node scripts/releaseHelmChart", "release:helm": "node scripts/releaseHelmChart",
"env:multi:enable": "lerna run env:multi:enable", "env:multi:enable": "lerna run env:multi:enable",
@ -84,4 +85,4 @@
"install:pro": "bash scripts/pro/install.sh", "install:pro": "bash scripts/pro/install.sh",
"dep:clean": "yarn clean && yarn bootstrap" "dep:clean": "yarn clean && yarn bootstrap"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "1.0.220-alpha.4", "version": "1.1.15-alpha.2",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js", "main": "dist/src/index.js",
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
@ -20,7 +20,7 @@
"test:watch": "jest --watchAll" "test:watch": "jest --watchAll"
}, },
"dependencies": { "dependencies": {
"@budibase/types": "^1.0.220-alpha.4", "@budibase/types": "^1.1.15-alpha.2",
"@techpass/passport-openidconnect": "0.3.2", "@techpass/passport-openidconnect": "0.3.2",
"aws-sdk": "2.1030.0", "aws-sdk": "2.1030.0",
"bcrypt": "5.0.1", "bcrypt": "5.0.1",
@ -59,10 +59,10 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@budibase/types": "^1.0.219",
"@shopify/jest-koa-mocks": "3.1.5", "@shopify/jest-koa-mocks": "3.1.5",
"@types/jest": "27.5.1", "@types/jest": "27.5.1",
"@types/koa": "2.0.52", "@types/koa": "2.0.52",
"@types/lodash": "4.14.180",
"@types/node": "14.18.20", "@types/node": "14.18.20",
"@types/node-fetch": "2.6.1", "@types/node-fetch": "2.6.1",
"@types/pouchdb": "6.4.0", "@types/pouchdb": "6.4.0",

View File

@ -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",
}

View File

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

View File

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

View File

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

View File

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

View File

@ -11,8 +11,8 @@ export enum AutomationViewModes {
} }
export enum ViewNames { export enum ViewNames {
USER_BY_EMAIL = "by_email",
USER_BY_APP = "by_app", USER_BY_APP = "by_app",
USER_BY_EMAIL = "by_email2",
BY_API_KEY = "by_api_key", BY_API_KEY = "by_api_key",
USER_BY_BUILDERS = "by_builders", USER_BY_BUILDERS = "by_builders",
LINK = "by_link", LINK = "by_link",
@ -20,6 +20,13 @@ export enum ViewNames {
AUTOMATION_LOGS = "automation_logs", 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 { export enum DocumentTypes {
USER = "us", USER = "us",
GROUP = "gr", GROUP = "gr",

View File

@ -1,10 +1,18 @@
const pouch = require("./pouch") const pouch = require("./pouch")
const env = require("../environment") const env = require("../environment")
const openDbs = []
let PouchDB let PouchDB
let initialised = false let initialised = false
const dbList = new Set() const dbList = new Set()
if (env.MEMORY_LEAK_CHECK) {
setInterval(() => {
console.log("--- OPEN DBS ---")
console.log(openDbs)
}, 5000)
}
const put = const put =
dbPut => dbPut =>
async (doc, options = {}) => { async (doc, options = {}) => {
@ -35,6 +43,9 @@ exports.dangerousGetDB = (dbName, opts) => {
dbList.add(dbName) dbList.add(dbName)
} }
const db = new PouchDB(dbName, opts) const db = new PouchDB(dbName, opts)
if (env.MEMORY_LEAK_CHECK) {
openDbs.push(db.name)
}
const dbPut = db.put const dbPut = db.put
db.put = put(dbPut) db.put = put(dbPut)
return db return db
@ -46,6 +57,9 @@ exports.closeDB = async db => {
if (!db || env.isTest()) { if (!db || env.isTest()) {
return return
} }
if (env.MEMORY_LEAK_CHECK) {
openDbs.splice(openDbs.indexOf(db.name), 1)
}
try { try {
// specifically await so that if there is an error, it can be ignored // specifically await so that if there is an error, it can be ignored
return await db.close() return await db.close()

View File

@ -102,6 +102,13 @@ exports.getPouch = (opts = {}) => {
} }
} }
if (opts.onDisk) {
POUCH_DB_DEFAULTS = {
prefix: undefined,
adapter: "leveldb",
}
}
if (opts.replication) { if (opts.replication) {
const replicationStream = require("pouchdb-replication-stream") const replicationStream = require("pouchdb-replication-stream")
PouchDB.plugin(replicationStream.plugin) PouchDB.plugin(replicationStream.plugin)

View File

@ -1,20 +1,42 @@
const { DocumentTypes, ViewNames, SEPARATOR } = require("./constants") const {
DocumentTypes,
ViewNames,
DeprecatedViews,
SEPARATOR,
} = require("./utils")
const { getGlobalDB } = require("../tenancy") const { getGlobalDB } = require("../tenancy")
const DESIGN_DB = "_design/database"
function DesignDoc() { function DesignDoc() {
return { return {
_id: "_design/database", _id: DESIGN_DB,
// view collation information, read before writing any complex views: // view collation information, read before writing any complex views:
// https://docs.couchdb.org/en/master/ddocs/views/collation.html#collation-specification // https://docs.couchdb.org/en/master/ddocs/views/collation.html#collation-specification
views: {}, 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() const db = getGlobalDB()
let designDoc let designDoc
try { try {
designDoc = await db.get("_design/database") designDoc = await db.get(DESIGN_DB)
} catch (err) { } catch (err) {
// no design doc, make one // no design doc, make one
designDoc = DesignDoc() designDoc = DesignDoc()
@ -22,7 +44,7 @@ exports.createUserEmailView = async () => {
const view = { const view = {
// if using variables in a map function need to inject them before use // if using variables in a map function need to inject them before use
map: `function(doc) { map: `function(doc) {
if (doc._id.startsWith("${DocumentTypes.USER}")) { if (doc._id.startsWith("${DocumentTypes.USER}${SEPARATOR}")) {
emit(doc.email.toLowerCase(), doc._id) emit(doc.email.toLowerCase(), doc._id)
} }
}`, }`,
@ -108,7 +130,7 @@ exports.createUserBuildersView = async () => {
exports.queryGlobalView = async (viewName, params, db = null) => { exports.queryGlobalView = async (viewName, params, db = null) => {
const CreateFuncByName = { const CreateFuncByName = {
[ViewNames.USER_BY_EMAIL]: exports.createUserEmailView, [ViewNames.USER_BY_EMAIL]: exports.createNewUserEmailView,
[ViewNames.BY_API_KEY]: exports.createApiKeyView, [ViewNames.BY_API_KEY]: exports.createApiKeyView,
[ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView, [ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView,
[ViewNames.USER_BY_APP]: exports.createUserAppView, [ViewNames.USER_BY_APP]: exports.createUserAppView,
@ -126,6 +148,7 @@ exports.queryGlobalView = async (viewName, params, db = null) => {
} catch (err) { } catch (err) {
if (err != null && err.name === "not_found") { if (err != null && err.name === "not_found") {
const createFunc = CreateFuncByName[viewName] const createFunc = CreateFuncByName[viewName]
await removeDeprecated(db, viewName)
await createFunc() await createFunc()
return exports.queryGlobalView(viewName, params) return exports.queryGlobalView(viewName, params)
} else { } else {

View File

@ -54,6 +54,7 @@ const env = {
DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE, DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE,
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE, DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
SERVICE: process.env.SERVICE || "budibase", SERVICE: process.env.SERVICE || "budibase",
MEMORY_LEAK_CHECK: process.env.MEMORY_LEAK_CHECK || false,
DEPLOYMENT_ENVIRONMENT: DEPLOYMENT_ENVIRONMENT:
process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose", process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose",
_set(key: any, value: any) { _set(key: any, value: any) {

View File

@ -2,7 +2,7 @@ import PostHog from "posthog-node"
import { Event, Identity, Group, BaseEvent } from "@budibase/types" import { Event, Identity, Group, BaseEvent } from "@budibase/types"
import { EventProcessor } from "./types" import { EventProcessor } from "./types"
import env from "../../environment" import env from "../../environment"
import context from "../../context" import * as context from "../../context"
const pkg = require("../../../package.json") const pkg = require("../../../package.json")
export default class PosthogProcessor implements EventProcessor { export default class PosthogProcessor implements EventProcessor {

View File

@ -9,7 +9,7 @@ import {
getGlobalDBName, getGlobalDBName,
getTenantId, getTenantId,
} from "../tenancy" } from "../tenancy"
import context from "../context" import * as context from "../context"
import { DEFINITIONS } from "." import { DEFINITIONS } from "."
import { import {
Migration, Migration,

View File

@ -75,9 +75,11 @@ export const ObjectStore = (bucket: any) => {
s3ForcePathStyle: true, s3ForcePathStyle: true,
signatureVersion: "v4", signatureVersion: "v4",
apiVersion: "2006-03-01", apiVersion: "2006-03-01",
params: { }
if (bucket) {
config.params = {
Bucket: sanitizeBucket(bucket), Bucket: sanitizeBucket(bucket),
}, }
} }
if (env.MINIO_URL) { if (env.MINIO_URL) {
config.endpoint = env.MINIO_URL config.endpoint = env.MINIO_URL
@ -292,6 +294,7 @@ export const uploadDirectory = async (
} }
} }
await Promise.all(uploads) await Promise.all(uploads)
return files
} }
exports.downloadTarballDirect = async (url: string, path: string) => { exports.downloadTarballDirect = async (url: string, path: string) => {

View File

@ -764,6 +764,11 @@
"@types/koa-compose" "*" "@types/koa-compose" "*"
"@types/node" "*" "@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": "@types/mime@^1":
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"

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.220-alpha.4", "version": "1.1.15-alpha.2",
"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.220-alpha.4", "@budibase/string-templates": "^1.1.15-alpha.2",
"@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",

File diff suppressed because it is too large Load Diff

View File

@ -100,24 +100,18 @@ filterTests(['smoke', 'all'], () => {
}) })
it("should create the first application from scratch, using the users first name as the default app name", () => { it("should create the first application from scratch, using the users first name as the default app name", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.updateUserInformation("Ted", "Userman") cy.updateUserInformation("Ted", "Userman")
cy.createApp("", false) cy.createApp("", false)
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.applicationInAppTable("Teds app") cy.applicationInAppTable("Teds app")
cy.deleteApp("Teds app") cy.deleteApp("Teds app")
//Accomodate names that end in 'S' // Accomodate names that end in 'S'
cy.updateUserInformation("Chris", "Userman") cy.updateUserInformation("Chris", "Userman")
cy.createApp("", false) cy.createApp("", false)
cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.applicationInAppTable("Chris app") cy.applicationInAppTable("Chris app")
cy.deleteApp("Chris app") cy.deleteApp("Chris app")

View File

@ -4,7 +4,7 @@ Cypress.on("uncaught:exception", () => {
// ACCOUNTS & USERS // ACCOUNTS & USERS
Cypress.Commands.add("login", (email, password) => { Cypress.Commands.add("login", (email, password) => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.wait(2000) cy.wait(2000)
cy.url().then(url => { cy.url().then(url => {
if (url.includes("builder/admin")) { if (url.includes("builder/admin")) {
@ -139,7 +139,9 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => {
cy.get(`[data-cy="create-app-btn"]`, { timeout: 5000 }).click({ force: true }) cy.get(`[data-cy="create-app-btn"]`, { timeout: 5000 }).click({ force: true })
// If apps already exist // If apps already exist
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`, {
timeout: 5000,
})
.its("body") .its("body")
.then(val => { .then(val => {
if (val.length > 0) { if (val.length > 0) {
@ -223,9 +225,11 @@ Cypress.Commands.add("deleteApp", name => {
}) })
Cypress.Commands.add("deleteAllApps", () => { Cypress.Commands.add("deleteAllApps", () => {
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
cy.wait(500) cy.wait(500)
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`, {
timeout: 5000,
})
.its("body") .its("body")
.then(val => { .then(val => {
for (let i = 0; i < val.length; i++) { for (let i = 0; i < val.length; i++) {
@ -377,7 +381,7 @@ Cypress.Commands.add("searchForApplication", appName => {
// Assumes there are no others // Assumes there are no others
Cypress.Commands.add("applicationInAppTable", appName => { Cypress.Commands.add("applicationInAppTable", appName => {
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 }) cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 })
cy.get(".appTable", { timeout: 2000 }).within(() => { cy.get(".appTable", { timeout: 5000 }).within(() => {
cy.get(".title").contains(appName).should("exist") cy.get(".title").contains(appName).should("exist")
}) })
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "1.0.220-alpha.4", "version": "1.1.15-alpha.2",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -69,10 +69,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.220-alpha.4", "@budibase/bbui": "^1.1.15-alpha.2",
"@budibase/client": "^1.0.220-alpha.4", "@budibase/client": "^1.1.15-alpha.2",
"@budibase/frontend-core": "^1.0.220-alpha.4", "@budibase/frontend-core": "^1.1.15-alpha.2",
"@budibase/string-templates": "^1.0.220-alpha.4", "@budibase/string-templates": "^1.1.15-alpha.2",
"@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",
@ -113,7 +113,7 @@
"rollup": "^2.44.0", "rollup": "^2.44.0",
"rollup-plugin-copy": "^3.4.0", "rollup-plugin-copy": "^3.4.0",
"start-server-and-test": "^1.12.1", "start-server-and-test": "^1.12.1",
"svelte": "^3.48.0", "svelte": "^3.49.0",
"svelte-jester": "^1.3.2", "svelte-jester": "^1.3.2",
"ts-node": "^10.4.0", "ts-node": "^10.4.0",
"tsconfig-paths": "4.0.0", "tsconfig-paths": "4.0.0",

View File

@ -52,8 +52,8 @@ export default class IntercomClient {
* @param {Object} user - user to identify * @param {Object} user - user to identify
* @returns Intercom global object * @returns Intercom global object
*/ */
show(user = {}) { show(user = {}, enabled) {
if (!this.initialised || !user?.admin) return if (!this.initialised || !enabled) return
return window.Intercom("boot", { return window.Intercom("boot", {
app_id: this.token, app_id: this.token,

View File

@ -12,6 +12,7 @@
notifications, notifications,
Modal, Modal,
} from "@budibase/bbui" } from "@budibase/bbui"
import { ActionStepID } from "constants/backend/automations"
export let automation export let automation
let testDataModal let testDataModal
@ -82,7 +83,7 @@
in:fly|local={{ x: 500, duration: 500 }} in:fly|local={{ x: 500, duration: 500 }}
out:fly|local={{ x: 500, duration: 500 }} out:fly|local={{ x: 500, duration: 500 }}
> >
{#if block.stepId !== "LOOP"} {#if block.stepId !== ActionStepID.LOOP}
<FlowItem {testDataModal} {block} /> <FlowItem {testDataModal} {block} />
{/if} {/if}
</div> </div>

View File

@ -10,11 +10,15 @@
Select, Select,
ActionButton, ActionButton,
notifications, notifications,
Label,
} from "@budibase/bbui" } from "@budibase/bbui"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte" import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import ActionModal from "./ActionModal.svelte" import ActionModal from "./ActionModal.svelte"
import FlowItemHeader from "./FlowItemHeader.svelte" import FlowItemHeader from "./FlowItemHeader.svelte"
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
import { ActionStepID, TriggerStepID } from "constants/backend/automations"
import { permissions } from "stores/backend"
export let block export let block
export let testDataModal export let testDataModal
@ -23,9 +27,12 @@
let actionModal let actionModal
let blockComplete let blockComplete
let showLooping = false let showLooping = false
let role
$: automationId = $automationStore.selectedAutomation?.automation._id
$: showBindingPicker = $: showBindingPicker =
block.stepId === "CREATE_ROW" || block.stepId === "UPDATE_ROW" block.stepId === ActionStepID.CREATE_ROW ||
block.stepId === ActionStepID.UPDATE_ROW
$: isTrigger = block.type === "TRIGGER" $: isTrigger = block.type === "TRIGGER"
@ -45,6 +52,32 @@
x => x.blockToLoop === block.id x => x.blockToLoop === block.id
) )
$: setPermissions(role)
$: getPermissions(automationId)
async function setPermissions(role) {
if (!role || !automationId) {
return
}
await permissions.save({
level: "execute",
role,
resource: automationId,
})
}
async function getPermissions(automationId) {
if (!automationId) {
return
}
const perms = await permissions.forResource(automationId)
if (!perms["execute"]) {
role = "BASIC"
} else {
role = perms["execute"]
}
}
async function removeLooping() { async function removeLooping() {
loopingSelected = false loopingSelected = false
let loopBlock = let loopBlock =
@ -205,6 +238,10 @@
</div> </div>
{/if} {/if}
{#if block.stepId === TriggerStepID.APP}
<Label>Role</Label>
<RoleSelect bind:value={role} />
{/if}
<AutomationBlockSetup <AutomationBlockSetup
schemaProperties={Object.entries(block.schema.inputs.properties)} schemaProperties={Object.entries(block.schema.inputs.properties)}
{block} {block}

View File

@ -96,7 +96,7 @@
onSelect(block) onSelect(block)
}} }}
> >
<Icon name={blockComplete ? "ChevronUp" : "ChevronDown"} /> <Icon hoverable name={blockComplete ? "ChevronUp" : "ChevronDown"} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,6 +1,7 @@
<script> <script>
import { Icon, Divider, Tabs, Tab, TextArea, Label } from "@budibase/bbui" import { Icon, Divider, Tabs, Tab, TextArea, Label } from "@budibase/bbui"
import FlowItemHeader from "./FlowChart/FlowItemHeader.svelte" import FlowItemHeader from "./FlowChart/FlowItemHeader.svelte"
import { ActionStepID } from "constants/backend/automations"
export let automation export let automation
export let testResults export let testResults
@ -10,7 +11,7 @@
let blocks let blocks
function prepTestResults(results) { function prepTestResults(results) {
return results?.steps.filter(x => x.stepId !== "LOOP" || []) return results?.steps.filter(x => x.stepId !== ActionStepID.LOOP || [])
} }
function textArea(results, message) { function textArea(results, message) {
@ -30,7 +31,7 @@
} }
blocks = blocks blocks = blocks
.concat(automation.definition.steps || []) .concat(automation.definition.steps || [])
.filter(x => x.stepId !== "LOOP") .filter(x => x.stepId !== ActionStepID.LOOP)
} else if (filteredResults) { } else if (filteredResults) {
blocks = filteredResults || [] blocks = filteredResults || []
// make sure there is an ID for each block being displayed // make sure there is an ID for each block being displayed
@ -45,7 +46,7 @@
<div class="container"> <div class="container">
{#each blocks as block, idx} {#each blocks as block, idx}
<div class="block" style={width ? `width: ${width}` : ""}> <div class="block" style={width ? `width: ${width}` : ""}>
{#if block.stepId !== "LOOP"} {#if block.stepId !== ActionStepID.LOOP}
<FlowItemHeader <FlowItemHeader
showTestStatus={true} showTestStatus={true}
bind:showParameters bind:showParameters
@ -67,27 +68,20 @@
{/if} {/if}
<div class="tabs"> <div class="tabs">
<Tabs quiet noPadding selected="Input"> <Tabs noHorizPadding selected="Input">
<Tab title="Input"> <Tab title="Input">
<div style="padding: 10px 10px 10px 10px;"> <TextArea
<TextArea minHeight="80px"
minHeight="80px" disabled
disabled value={textArea(filteredResults?.[idx]?.inputs, "No input")}
value={textArea(filteredResults?.[idx]?.inputs, "No input")} />
/> </Tab>
</div></Tab
>
<Tab title="Output"> <Tab title="Output">
<div style="padding: 10px 10px 10px 10px;"> <TextArea
<TextArea minHeight="100px"
minHeight="100px" disabled
disabled value={textArea(filteredResults?.[idx]?.outputs, "No output")}
value={textArea( />
filteredResults?.[idx]?.outputs,
"No output"
)}
/>
</div>
</Tab> </Tab>
</Tabs> </Tabs>
</div> </div>
@ -113,6 +107,7 @@
align-items: stretch; align-items: stretch;
position: relative; position: relative;
flex: 1 1 auto; flex: 1 1 auto;
padding: 0 var(--spacing-xl) var(--spacing-xl) var(--spacing-xl);
} }
.block { .block {

View File

@ -2,6 +2,7 @@
import { Icon, Divider } from "@budibase/bbui" import { Icon, Divider } from "@budibase/bbui"
import TestDisplay from "./TestDisplay.svelte" import TestDisplay from "./TestDisplay.svelte"
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import { ActionStepID } from "constants/backend/automations"
export let automation export let automation
export let testResults export let testResults
@ -16,7 +17,7 @@
} }
blocks = blocks blocks = blocks
.concat(automation.definition.steps || []) .concat(automation.definition.steps || [])
.filter(x => x.stepId !== "LOOP") .filter(x => x.stepId !== ActionStepID.LOOP)
} else if (testResults) { } else if (testResults) {
blocks = testResults.steps || [] blocks = testResults.steps || []
} }

View File

@ -11,6 +11,7 @@
Body, Body,
Icon, Icon,
} from "@budibase/bbui" } from "@budibase/bbui"
import { TriggerStepID } from "constants/backend/automations"
let name let name
let selectedTrigger let selectedTrigger
@ -35,7 +36,7 @@
) )
automationStore.actions.addBlockToAutomation(newBlock) automationStore.actions.addBlockToAutomation(newBlock)
if (triggerVal.stepId === "WEBHOOK") { if (triggerVal.stepId === TriggerStepID.WEBHOOK) {
webhookModal.show webhookModal.show
} }

View File

@ -30,6 +30,7 @@
import { LuceneUtils } from "@budibase/frontend-core" import { LuceneUtils } from "@budibase/frontend-core"
import { getSchemaForTable } from "builderStore/dataBinding" import { getSchemaForTable } from "builderStore/dataBinding"
import { Utils } from "@budibase/frontend-core" import { Utils } from "@budibase/frontend-core"
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
export let block export let block
export let testData export let testData
@ -54,12 +55,13 @@
$: schema = getSchemaForTable(tableId, { searchableSchema: true }).schema $: schema = getSchemaForTable(tableId, { searchableSchema: true }).schema
$: schemaFields = Object.values(schema || {}) $: schemaFields = Object.values(schema || {})
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000" $: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
$: isTrigger = block?.type === "TRIGGER"
const onChange = Utils.sequential(async (e, key) => { const onChange = Utils.sequential(async (e, key) => {
try { try {
if (isTestModal) { if (isTestModal) {
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents // Special case for webhook, as it requires a body, but the schema already brings back the body's contents
if (stepId === "WEBHOOK") { if (stepId === TriggerStepID.WEBHOOK) {
automationStore.actions.addTestDataToAutomation({ automationStore.actions.addTestDataToAutomation({
body: { body: {
[key]: e.detail, [key]: e.detail,
@ -100,9 +102,9 @@
// Extract all outputs from all previous steps as available bindins // Extract all outputs from all previous steps as available bindins
let bindings = [] let bindings = []
for (let idx = 0; idx < blockIdx; idx++) { for (let idx = 0; idx < blockIdx; idx++) {
let wasLoopBlock = allSteps[idx]?.stepId === "LOOP" let wasLoopBlock = allSteps[idx]?.stepId === ActionStepID.LOOP
let isLoopBlock = let isLoopBlock =
allSteps[idx]?.stepId === "LOOP" && allSteps[idx]?.stepId === ActionStepID.LOOP &&
allSteps.find(x => x.blockToLoop === block.id) allSteps.find(x => x.blockToLoop === block.id)
// If the previous block was a loop block, decerement the index so the following // If the previous block was a loop block, decerement the index so the following
@ -261,6 +263,7 @@
/> />
{:else if value.customType === "table"} {:else if value.customType === "table"}
<TableSelector <TableSelector
{isTrigger}
value={inputData[key]} value={inputData[key]}
on:change={e => onChange(e, key)} on:change={e => onChange(e, key)}
/> />
@ -343,7 +346,7 @@
<CreateWebhookModal /> <CreateWebhookModal />
</Modal> </Modal>
{#if stepId === "WEBHOOK"} {#if stepId === TriggerStepID.WEBHOOK}
<Button secondary on:click={() => webhookModal.show()}>Set Up Webhook</Button> <Button secondary on:click={() => webhookModal.show()}>Set Up Webhook</Button>
{/if} {/if}

View File

@ -1,5 +1,5 @@
<script> <script>
import { Button, Select, Input } from "@budibase/bbui" import { Button, Select, Input, Label } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -9,6 +9,7 @@
dispatch("change", e.detail) dispatch("change", e.detail)
} }
let touched = false
let presets = false let presets = false
const CRON_EXPRESSIONS = [ const CRON_EXPRESSIONS = [
@ -36,8 +37,10 @@
</script> </script>
<div class="block-field"> <div class="block-field">
<Input on:change={onChange} {value} /> <Input on:change={onChange} {value} on:blur={() => (touched = true)} />
{#if touched && !value}
<Label><div class="error">Please specify a CRON expression</div></Label>
{/if}
<div class="presets"> <div class="presets">
<Button on:click={() => (presets = !presets)} <Button on:click={() => (presets = !presets)}
>{presets ? "Hide" : "Show"} Presets</Button >{presets ? "Hide" : "Show"} Presets</Button
@ -62,4 +65,8 @@
.block-field { .block-field {
padding-top: var(--spacing-s); padding-top: var(--spacing-s);
} }
.error {
padding-top: var(--spacing-xs);
color: var(--spectrum-global-color-red-500);
}
</style> </style>

View File

@ -2,10 +2,16 @@
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { Select } from "@budibase/bbui" import { Select } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { TableNames } from "constants"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let value export let value
export let isTrigger
$: filteredTables = $tables.list.filter(table => {
return !isTrigger || table._id !== TableNames.USERS
})
const onChange = e => { const onChange = e => {
value = e.detail value = e.detail
@ -16,7 +22,7 @@
<Select <Select
on:change={onChange} on:change={onChange}
bind:value bind:value
options={$tables.list} options={filteredTables}
getOptionLabel={table => table.name} getOptionLabel={table => table.name}
getOptionValue={table => table._id} getOptionValue={table => table._id}
/> />

View File

@ -3,6 +3,7 @@
import { ModalContent } from "@budibase/bbui" import { ModalContent } from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import WebhookDisplay from "../automation/Shared/WebhookDisplay.svelte" import WebhookDisplay from "../automation/Shared/WebhookDisplay.svelte"
import { TriggerStepID } from "constants/backend/automations"
let webhookUrls = [] let webhookUrls = []
@ -11,7 +12,7 @@
onMount(() => { onMount(() => {
webhookUrls = automations.map(automation => { webhookUrls = automations.map(automation => {
const trigger = automation.definition.trigger const trigger = automation.definition.trigger
if (trigger?.stepId === "WEBHOOK" && trigger.inputs) { if (trigger?.stepId === TriggerStepID.WEBHOOK && trigger.inputs) {
return { return {
type: "Automation", type: "Automation",
name: automation.name, name: automation.name,

View File

@ -69,7 +69,14 @@
{#if !hideIcon} {#if !hideIcon}
<div class="icon-wrapper" class:highlight={updateAvailable}> <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> </div>
{/if} {/if}
<Modal bind:this={updateModal}> <Modal bind:this={updateModal}>

View File

@ -6,8 +6,8 @@
Button, Button,
Layout, Layout,
DrawerContent, DrawerContent,
ActionMenu, ActionButton,
MenuItem, Search,
} from "@budibase/bbui" } from "@budibase/bbui"
import { getAvailableActions } from "./index" import { getAvailableActions } from "./index"
import { generate } from "shortid" import { generate } from "shortid"
@ -22,8 +22,24 @@
export let actions export let actions
export let bindings = [] export let bindings = []
$: showAvailableActions = !actions?.length
let actionQuery
$: parsedQuery =
typeof actionQuery === "string" ? actionQuery.toLowerCase().trim() : ""
let selectedAction = actions?.length ? actions[0] : null 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 // These are ephemeral bindings which only exist while executing actions
$: buttonContextBindings = getButtonContextBindings( $: buttonContextBindings = getButtonContextBindings(
$currentAsset, $currentAsset,
@ -61,7 +77,12 @@
actions = actions actions = actions
} }
const addAction = actionType => () => { const toggleActionList = () => {
actionQuery = null
showAvailableActions = !showAvailableActions
}
const addAction = actionType => {
const newAction = { const newAction = {
parameters: {}, parameters: {},
[EVENT_TYPE_KEY]: actionType.name, [EVENT_TYPE_KEY]: actionType.name,
@ -78,6 +99,11 @@
selectedAction = action selectedAction = action
} }
const onAddAction = actionType => {
addAction(actionType)
toggleActionList()
}
function handleDndConsider(e) { function handleDndConsider(e) {
actions = e.detail.items actions = e.detail.items
} }
@ -88,7 +114,39 @@
<DrawerContent> <DrawerContent>
<Layout noPadding gap="S" slot="sidebar"> <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 <div
class="actions" class="actions"
use:dndzone={{ use:dndzone={{
@ -120,17 +178,9 @@
{/each} {/each}
</div> </div>
{/if} {/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>
<Layout noPadding> <Layout noPadding>
{#if selectedActionComponent} {#if selectedActionComponent && !showAvailableActions}
{#key selectedAction.id} {#key selectedAction.id}
<div class="selected-action-container"> <div class="selected-action-container">
<svelte:component <svelte:component
@ -152,13 +202,10 @@
align-items: stretch; align-items: stretch;
gap: var(--spacing-s); gap: var(--spacing-s);
} }
.action-header { .action-header {
color: var(--spectrum-global-color-gray-700); color: var(--spectrum-global-color-gray-700);
flex: 1 1 auto; flex: 1 1 auto;
} }
.action-container { .action-container {
background-color: var(--background); background-color: var(--background);
padding: var(--spacing-s) var(--spacing-m); padding: var(--spacing-s) var(--spacing-m);
@ -182,4 +229,55 @@
.action-container.selected .action-header { .action-container.selected .action-header {
color: var(--spectrum-global-color-gray-900); 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> </style>

View File

@ -69,9 +69,16 @@
notifications.error("Error creating automation") notifications.error("Error creating automation")
} }
} }
$: actionCount = value?.length
$: actionText = `${actionCount || "No"} action${
actionCount !== 1 ? "s" : ""
} set`
</script> </script>
<div class="action-count">{actionText}</div>
<ActionButton on:click={openDrawer}>Define actions</ActionButton> <ActionButton on:click={openDrawer}>Define actions</ActionButton>
<Drawer bind:this={drawer} title={"Actions"}> <Drawer bind:this={drawer} title={"Actions"}>
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Define what actions to run. Define what actions to run.
@ -85,3 +92,10 @@
{key} {key}
/> />
</Drawer> </Drawer>
<style>
.action-count {
padding-bottom: var(--spacing-s);
font-weight: 600;
}
</style>

View File

@ -2,6 +2,7 @@
import { Select, Label, Input, Checkbox } from "@budibase/bbui" import { Select, Label, Input, Checkbox } from "@budibase/bbui"
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
import { TriggerStepID } from "constants/backend/automations"
export let parameters = {} export let parameters = {}
export let bindings = [] export let bindings = []
@ -16,7 +17,7 @@
: AUTOMATION_STATUS.NEW : AUTOMATION_STATUS.NEW
$: automations = $automationStore.automations $: automations = $automationStore.automations
.filter(a => a.definition.trigger?.stepId === "APP") .filter(a => a.definition.trigger?.stepId === TriggerStepID.APP)
.map(automation => { .map(automation => {
const schema = Object.entries( const schema = Object.entries(
automation.definition.trigger.inputs.fields || {} automation.definition.trigger.inputs.fields || {}

View File

@ -2,6 +2,7 @@
"actions": [ "actions": [
{ {
"name": "Save Row", "name": "Save Row",
"type": "data",
"component": "SaveRow", "component": "SaveRow",
"context": [ "context": [
{ {
@ -12,6 +13,7 @@
}, },
{ {
"name": "Duplicate Row", "name": "Duplicate Row",
"type": "data",
"component": "DuplicateRow", "component": "DuplicateRow",
"context": [ "context": [
{ {
@ -22,14 +24,17 @@
}, },
{ {
"name": "Delete Row", "name": "Delete Row",
"type": "data",
"component": "DeleteRow" "component": "DeleteRow"
}, },
{ {
"name": "Navigate To", "name": "Navigate To",
"type": "application",
"component": "NavigateTo" "component": "NavigateTo"
}, },
{ {
"name": "Execute Query", "name": "Execute Query",
"type": "data",
"component": "ExecuteQuery", "component": "ExecuteQuery",
"context": [ "context": [
{ {
@ -40,43 +45,53 @@
}, },
{ {
"name": "Trigger Automation", "name": "Trigger Automation",
"type": "application",
"component": "TriggerAutomation" "component": "TriggerAutomation"
}, },
{ {
"name": "Update Field Value", "name": "Update Field Value",
"type": "form",
"component": "UpdateFieldValue" "component": "UpdateFieldValue"
}, },
{ {
"name": "Validate Form", "name": "Validate Form",
"type": "form",
"component": "ValidateForm" "component": "ValidateForm"
}, },
{ {
"name": "Change Form Step", "name": "Change Form Step",
"type": "form",
"component": "ChangeFormStep" "component": "ChangeFormStep"
}, },
{ {
"name": "Clear Form", "name": "Clear Form",
"type": "form",
"component": "ClearForm" "component": "ClearForm"
}, },
{ {
"name": "Log Out", "name": "Log Out",
"type": "application",
"component": "LogOut" "component": "LogOut"
}, },
{ {
"name": "Close Screen Modal", "name": "Close Screen Modal",
"type": "application",
"component": "CloseScreenModal" "component": "CloseScreenModal"
}, },
{ {
"name": "Refresh Data Provider", "name": "Refresh Data Provider",
"type": "data",
"component": "RefreshDataProvider" "component": "RefreshDataProvider"
}, },
{ {
"name": "Update State", "name": "Update State",
"type": "data",
"component": "UpdateState", "component": "UpdateState",
"dependsOnFeature": "state" "dependsOnFeature": "state"
}, },
{ {
"name": "Upload File to S3", "name": "Upload File to S3",
"type": "data",
"component": "S3Upload", "component": "S3Upload",
"context": [ "context": [
{ {
@ -87,12 +102,14 @@
}, },
{ {
"name": "Export Data", "name": "Export Data",
"type": "data",
"component": "ExportData" "component": "ExportData"
}, },
{ {
"name": "Continue if / Stop if", "name": "Continue if / Stop if",
"type": "logic",
"component": "ContinueIf", "component": "ContinueIf",
"dependsOnFeature": "continueIfAction" "dependsOnFeature": "continueIfAction"
} }
] ]
} }

View File

@ -25,6 +25,7 @@
export let otherSources export let otherSources
export let showAllQueries export let showAllQueries
export let bindings = [] export let bindings = []
export let showDataProviders = true
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const arrayTypes = ["attachment", "array"] const arrayTypes = ["attachment", "array"]
@ -258,7 +259,7 @@
{/each} {/each}
</ul> </ul>
{/if} {/if}
{#if dataProviders?.length} {#if showDataProviders && dataProviders?.length}
<Divider size="S" /> <Divider size="S" />
<div class="title"> <div class="title">
<Heading size="XS">Data Providers</Heading> <Heading size="XS">Data Providers</Heading>

View File

@ -4,4 +4,10 @@
const otherSources = [{ name: "Custom", label: "Custom" }] const otherSources = [{ name: "Custom", label: "Custom" }]
</script> </script>
<DataSourceSelect on:change {...$$props} showAllQueries={true} {otherSources} /> <DataSourceSelect
on:change
{...$$props}
showAllQueries={true}
showDataProviders={false}
{otherSources}
/>

View File

@ -23,7 +23,7 @@
<ActionButton noPadding size="S" icon="Close" quiet on:click={close} /> <ActionButton noPadding size="S" icon="Close" quiet on:click={close} />
</div> </div>
</div> </div>
<Layout paddingX="XL" gap="S"> <Layout paddingY="XL" paddingX="XL" gap="S">
<div class="icon"> <div class="icon">
<Icon name="Clock" /> <Icon name="Clock" />
<DateTimeRenderer value={history.createdAt} /> <DateTimeRenderer value={history.createdAt} />
@ -71,7 +71,6 @@
} }
.bottom { .bottom {
margin-top: var(--spacing-m);
border-top: var(--border-light); border-top: var(--border-light);
padding-top: calc(var(--spacing-xl) * 2); padding-top: calc(var(--spacing-xl) * 2);
padding-bottom: calc(var(--spacing-xl) * 2); padding-bottom: calc(var(--spacing-xl) * 2);

View File

@ -119,7 +119,7 @@
</script> </script>
<div class="root" class:panelOpen={showPanel}> <div class="root" class:panelOpen={showPanel}>
<Layout paddingX="XL" gap="S" alignContent="start"> <Layout noPadding gap="M" alignContent="start">
<div class="search"> <div class="search">
<div class="select"> <div class="select">
<Select <Select
@ -147,16 +147,28 @@
</div> </div>
</div> </div>
{#if runHistory} {#if runHistory}
<Table <div>
on:click={viewDetails} <Table
schema={runHistorySchema} on:click={viewDetails}
allowSelectRows={false} schema={runHistorySchema}
allowEditColumns={false} allowSelectRows={false}
allowEditRows={false} allowEditColumns={false}
data={runHistory} allowEditRows={false}
{customRenderers} data={runHistory}
placeholderText="No history found" {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} {/if}
</Layout> </Layout>
<div class="panel" class:panelShow={showPanel}> <div class="panel" class:panelShow={showPanel}>
@ -169,26 +181,19 @@
/> />
</div> </div>
</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> <style>
.root { .root {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
height: 100%; height: 100%;
padding: var(--spectrum-alias-grid-gutter-medium)
var(--spectrum-alias-grid-gutter-large);
} }
.search { .search {
display: flex; display: flex;
gap: var(--spacing-l); gap: var(--spacing-xl);
width: 100%; width: 100%;
align-items: flex-end; align-items: flex-end;
} }
@ -198,15 +203,15 @@
} }
.pagination { .pagination {
position: absolute; display: flex;
bottom: 0; flex-direction: row;
margin-bottom: var(--spacing-xl); justify-content: flex-end;
margin-left: var(--spacing-l); margin-top: var(--spacing-xl);
} }
.panel { .panel {
display: none; display: none;
background-color: var(--background); margin-top: calc(-1 * var(--spectrum-alias-grid-gutter-medium));
} }
.panelShow { .panelShow {

View File

@ -0,0 +1,28 @@
export const TriggerStepID = {
ROW_SAVED: "ROW_SAVED",
ROW_UPDATED: "ROW_UPDATED",
ROW_DELETED: "ROW_DELETED",
WEBHOOK: "WEBHOOK",
APP: "APP",
CRON: "CRON",
}
export const ActionStepID = {
SEND_EMAIL_SMTP: "SEND_EMAIL_SMTP",
CREATE_ROW: "CREATE_ROW",
UPDATE_ROW: "UPDATE_ROW",
DELETE_ROW: "DELETE_ROW",
OUTGOING_WEBHOOK: "OUTGOING_WEBHOOK",
EXECUTE_SCRIPT: "EXECUTE_SCRIPT",
EXECUTE_QUERY: "EXECUTE_QUERY",
SERVER_LOG: "SERVER_LOG",
DELAY: "DELAY",
FILTER: "FILTER",
QUERY_ROWS: "QUERY_ROWS",
LOOP: "LOOP",
// these used to be lowercase step IDs, maintain for backwards compat
discord: "discord",
slack: "slack",
zapier: "zapier",
integromat: "integromat",
}

View File

@ -3,6 +3,7 @@
import { roles, flags } from "stores/backend" import { roles, flags } from "stores/backend"
import { Icon, Tabs, Tab, Heading, notifications } from "@budibase/bbui" import { Icon, Tabs, Tab, Heading, notifications } from "@budibase/bbui"
import RevertModal from "components/deploy/RevertModal.svelte" import RevertModal from "components/deploy/RevertModal.svelte"
import VersionModal from "components/deploy/VersionModal.svelte"
import DeployNavigation from "components/deploy/DeployNavigation.svelte" import DeployNavigation from "components/deploy/DeployNavigation.svelte"
import { API } from "api" import { API } from "api"
import { isActive, goto, layout, redirect } from "@roxi/routify" import { isActive, goto, layout, redirect } from "@roxi/routify"
@ -107,6 +108,7 @@
</Tabs> </Tabs>
</div> </div>
<div class="toprightnav"> <div class="toprightnav">
<VersionModal />
<RevertModal /> <RevertModal />
<Icon <Icon
name="Visibility" name="Visibility"

View File

@ -28,12 +28,15 @@
} }
drawer.hide() drawer.hide()
} }
$: conditionCount = componentInstance?._conditions?.length
$: conditionText = `${conditionCount || "No"} condition${
conditionCount !== 1 ? "s" : ""
} set`
</script> </script>
<DetailSummary <DetailSummary name={"Conditions"} collapsible={false}>
name={`Conditions${componentInstance?._conditions ? " *" : ""}`} <div class="conditionCount">{conditionText}</div>
collapsible={false}
>
<div> <div>
<ActionButton on:click={openDrawer}>Configure conditions</ActionButton> <ActionButton on:click={openDrawer}>Configure conditions</ActionButton>
</div> </div>
@ -45,3 +48,10 @@
<Button cta slot="buttons" on:click={() => save()}>Save</Button> <Button cta slot="buttons" on:click={() => save()}>Save</Button>
<ConditionalUIDrawer slot="body" bind:conditions={tempValue} {bindings} /> <ConditionalUIDrawer slot="body" bind:conditions={tempValue} {bindings} />
</Drawer> </Drawer>
<style>
.conditionCount {
font-weight: 600;
margin-top: -5px;
}
</style>

View File

@ -208,11 +208,6 @@
<span class="overview-wrap"> <span class="overview-wrap">
<Page wide noPadding> <Page wide noPadding>
{#await promise} {#await promise}
<span class="page-header">
<ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}>
Back
</ActionButton>
</span>
<div class="loading"> <div class="loading">
<ProgressCircle size="XL" /> <ProgressCircle size="XL" />
</div> </div>
@ -404,7 +399,7 @@
line-height: 1em; line-height: 1em;
margin-bottom: var(--spacing-s); margin-bottom: var(--spacing-s);
} }
.tab-wrap :global(.spectrum-Tabs) { .tab-wrap :global(> .spectrum-Tabs) {
padding-left: var(--spectrum-alias-grid-gutter-large); padding-left: var(--spectrum-alias-grid-gutter-large);
padding-right: var(--spectrum-alias-grid-gutter-large); padding-right: var(--spectrum-alias-grid-gutter-large);
} }

View File

@ -58,16 +58,16 @@
</Layout> </Layout>
</span> </span>
<span class="version-section"> <span class="version-section">
<Layout gap="XS" paddingY="XXL" paddingX=""> <Layout gap="XS" noPadding>
<Heading size="S">App version</Heading> <Heading size="S">App version</Heading>
<Divider /> <Divider />
<Body> <Body>
{#if updateAvailable} {#if updateAvailable}
<p class="version-status"> <Body>
The app is currently using version The app is currently using version
<strong>{$store.version}</strong> <strong>{$store.version}</strong>
but version <strong>{clientPackage.version}</strong> is available. but version <strong>{clientPackage.version}</strong> is available.
</p> </Body>
{:else} {:else}
<p class="version-status"> <p class="version-status">
The app is currently using version The app is currently using version

View File

@ -90,8 +90,8 @@ export function createQueriesStore() {
// Assume all the fields are strings and create a basic schema from the // Assume all the fields are strings and create a basic schema from the
// unique fields returned by the server // unique fields returned by the server
const schema = {} const schema = {}
for (let field of result.schemaFields) { for (let [field, type] of Object.entries(result.schemaFields)) {
schema[field] = "string" schema[field] = type || "string"
} }
return { ...result, schema, rows: result.rows || [] } return { ...result, schema, rows: result.rows || [] }
}, },

View File

@ -58,17 +58,20 @@ export function createAuthStore() {
.activate() .activate()
.then(() => { .then(() => {
analytics.identify(user._id) analytics.identify(user._id)
analytics.showChat({ analytics.showChat(
email: user.email, {
created_at: (user.createdAt || Date.now()) / 1000, email: user.email,
name: user.account?.name, created_at: (user.createdAt || Date.now()) / 1000,
user_id: user._id, name: user.account?.name,
tenant: user.tenantId, user_id: user._id,
admin: user?.admin?.global, tenant: user.tenantId,
builder: user?.builder?.global, admin: user?.admin?.global,
"Company size": user.account?.size, builder: user?.builder?.global,
"Job role": user.account?.profession, "Company size": user.account?.size,
}) "Job role": user.account?.profession,
},
!!user?.account
)
}) })
.catch(() => { .catch(() => {
// This request may fail due to browser extensions blocking requests // This request may fail due to browser extensions blocking requests

View File

@ -5732,10 +5732,10 @@ svelte-portal@0.1.0:
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-0.1.0.tgz#cc2821cc84b05ed5814e0218dcdfcbebc53c1742" resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-0.1.0.tgz#cc2821cc84b05ed5814e0218dcdfcbebc53c1742"
integrity sha512-kef+ksXVKun224mRxat+DdO4C+cGHla+fEcZfnBAvoZocwiaceOfhf5azHYOPXSSB1igWVFTEOF3CDENPnuWxg== integrity sha512-kef+ksXVKun224mRxat+DdO4C+cGHla+fEcZfnBAvoZocwiaceOfhf5azHYOPXSSB1igWVFTEOF3CDENPnuWxg==
svelte@^3.48.0: svelte@^3.49.0:
version "3.48.0" version "3.49.0"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.48.0.tgz#f98c866d45e155bad8e1e88f15f9c03cd28753d3" resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.49.0.tgz#5baee3c672306de1070c3b7888fc2204e36a4029"
integrity sha512-fN2YRm/bGumvjUpu6yI3BpvZnpIm9I6A7HR4oUNYd7ggYyIwSA/BX7DJ+UXXffLp6XNcUijyLvttbPVCYa/3xQ== integrity sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==
symbol-tree@^3.2.4: symbol-tree@^3.2.4:
version "3.2.4" version "3.2.4"

View File

@ -4,3 +4,4 @@ nginx.conf
build/ build/
docker-error.log docker-error.log
envoy.yaml envoy.yaml
*.tar.gz

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "1.0.220-alpha.4", "version": "1.1.15-alpha.2",
"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": {
@ -9,28 +9,43 @@
"author": "Budibase", "author": "Budibase",
"license": "GPL-3.0", "license": "GPL-3.0",
"scripts": { "scripts": {
"build": "pkg . --out-path build" "prebuild": "rm -rf prebuilds 2> /dev/null && cp -r node_modules/leveldown/prebuilds prebuilds",
"build": "yarn prebuild && renamer --find .node --replace .fake 'prebuilds/**' && pkg . --out-path build && yarn postbuild",
"postbuild": "rm -rf prebuilds 2> /dev/null"
}, },
"pkg": { "pkg": {
"targets": [ "targets": [
"node14-linux", "node16-linux",
"node14-win", "node16-win",
"node14-macos" "node16-macos"
],
"assets": [
"node_modules/@budibase/backend-core/dist/**/*",
"prebuilds/**/*"
], ],
"outputPath": "build" "outputPath": "build"
}, },
"dependencies": { "dependencies": {
"axios": "^0.21.1", "@budibase/backend-core": "^1.1.15-alpha.1",
"chalk": "^4.1.0", "axios": "0.21.1",
"commander": "^7.1.0", "chalk": "4.1.0",
"docker-compose": "^0.23.6", "cli-progress": "3.11.2",
"inquirer": "^8.0.0", "commander": "7.1.0",
"lookpath": "^1.1.0", "docker-compose": "0.23.6",
"pkg": "^5.3.0", "dotenv": "16.0.1",
"inquirer": "8.0.0",
"lookpath": "1.1.0",
"node-fetch": "2",
"pkg": "5.7.0",
"posthog-node": "1.0.7", "posthog-node": "1.0.7",
"randomstring": "^1.1.5" "pouchdb": "7.3.0",
"pouchdb-replication-stream": "1.2.9",
"randomstring": "1.1.5",
"tar": "6.1.11"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^7.20.0" "copyfiles": "^2.4.1",
"eslint": "^7.20.0",
"renamer": "^4.0.0"
} }
} }

View File

@ -0,0 +1,121 @@
const Command = require("../structures/Command")
const { CommandWords } = require("../constants")
const fs = require("fs")
const { join } = require("path")
const { getAllDbs } = require("../core/db")
const tar = require("tar")
const { progressBar } = require("../utils")
const {
TEMP_DIR,
COUCH_DIR,
MINIO_DIR,
getConfig,
replication,
getPouches,
} = require("./utils")
const { exportObjects, importObjects } = require("./objectStore")
async function exportBackup(opts) {
const envFile = opts.env || undefined
let filename = opts["export"] || opts
if (typeof filename !== "string") {
filename = `backup-${new Date().toISOString()}.tar.gz`
}
const config = await getConfig(envFile)
const dbList = await getAllDbs(config["COUCH_DB_URL"])
const { Remote, Local } = getPouches(config)
if (fs.existsSync(TEMP_DIR)) {
fs.rmSync(TEMP_DIR, { recursive: true })
}
const couchDir = join(TEMP_DIR, COUCH_DIR)
fs.mkdirSync(TEMP_DIR)
fs.mkdirSync(couchDir)
console.log("CouchDB Export")
const bar = progressBar(dbList.length)
let count = 0
for (let db of dbList) {
bar.update(++count)
const remote = new Remote(db)
const local = new Local(join(TEMP_DIR, COUCH_DIR, db))
await replication(remote, local)
}
bar.stop()
console.log("S3 Export")
await exportObjects()
tar.create(
{
sync: true,
gzip: true,
file: filename,
cwd: join(TEMP_DIR),
},
[COUCH_DIR, MINIO_DIR]
)
fs.rmSync(TEMP_DIR, { recursive: true })
console.log(`Generated export file - ${filename}`)
}
async function importBackup(opts) {
const envFile = opts.env || undefined
const filename = opts["import"] || opts
const config = await getConfig(envFile)
if (!filename || !fs.existsSync(filename)) {
console.error("Cannot import without specifying a valid file to import")
process.exit(-1)
}
if (fs.existsSync(TEMP_DIR)) {
fs.rmSync(TEMP_DIR, { recursive: true })
}
fs.mkdirSync(TEMP_DIR)
tar.extract({
sync: true,
cwd: join(TEMP_DIR),
file: filename,
})
const { Remote, Local } = getPouches(config)
const dbList = fs.readdirSync(join(TEMP_DIR, COUCH_DIR))
console.log("CouchDB Import")
const bar = progressBar(dbList.length)
let count = 0
for (let db of dbList) {
bar.update(++count)
const remote = new Remote(db)
const local = new Local(join(TEMP_DIR, COUCH_DIR, db))
await replication(local, remote)
}
bar.stop()
console.log("MinIO Import")
await importObjects()
console.log("Import complete")
fs.rmSync(TEMP_DIR, { recursive: true })
}
async function pickOne(opts) {
if (opts["import"]) {
return importBackup(opts)
} else if (opts["export"]) {
return exportBackup(opts)
}
}
const command = new Command(`${CommandWords.BACKUPS}`)
.addHelp(
"Allows building backups of Budibase, as well as importing a backup to a new instance."
)
.addSubOption(
"--export [filename]",
"Export a backup from an existing Budibase installation.",
exportBackup
)
.addSubOption(
"--import [filename]",
"Import a backup to a new Budibase installation.",
importBackup
)
.addSubOption(
"--env [envFile]",
"Provide an environment variable file to configure the CLI.",
pickOne
)
exports.command = command

View File

@ -0,0 +1,63 @@
const {
ObjectStoreBuckets,
ObjectStore,
retrieve,
uploadDirectory,
makeSureBucketExists,
} = require("@budibase/backend-core/objectStore")
const fs = require("fs")
const { join } = require("path")
const { TEMP_DIR, MINIO_DIR } = require("./utils")
const { progressBar } = require("../utils")
const bucketList = Object.values(ObjectStoreBuckets)
exports.exportObjects = async () => {
const path = join(TEMP_DIR, MINIO_DIR)
fs.mkdirSync(path)
let fullList = []
for (let bucket of bucketList) {
const client = ObjectStore(bucket)
try {
await client.headBucket().promise()
} catch (err) {
continue
}
const list = await client.listObjectsV2().promise()
fullList = fullList.concat(list.Contents.map(el => ({ ...el, bucket })))
}
const bar = progressBar(fullList.length)
let count = 0
for (let object of fullList) {
const filename = object.Key
const data = await retrieve(object.bucket, filename)
const possiblePath = filename.split("/")
if (possiblePath.length > 1) {
const dirs = possiblePath.slice(0, possiblePath.length - 1)
fs.mkdirSync(join(path, object.bucket, ...dirs), { recursive: true })
}
fs.writeFileSync(join(path, object.bucket, ...possiblePath), data)
bar.update(++count)
}
bar.stop()
}
exports.importObjects = async () => {
const path = join(TEMP_DIR, MINIO_DIR)
const buckets = fs.readdirSync(path)
let total = 0
buckets.forEach(bucket => {
const files = fs.readdirSync(join(path, bucket))
total += files.length
})
const bar = progressBar(total)
let count = 0
for (let bucket of buckets) {
const client = ObjectStore(bucket)
await makeSureBucketExists(client, bucket)
const files = await uploadDirectory(bucket, join(path, bucket), "/")
count += files.length
bar.update(count)
}
bar.stop()
}

View File

@ -0,0 +1,88 @@
const dotenv = require("dotenv")
const fs = require("fs")
const { string } = require("../questions")
const { getPouch } = require("../core/db")
exports.DEFAULT_COUCH = "http://budibase:budibase@localhost:10000/db/"
exports.DEFAULT_MINIO = "http://localhost:10000/"
exports.TEMP_DIR = ".temp"
exports.COUCH_DIR = "couchdb"
exports.MINIO_DIR = "minio"
const REQUIRED = [
{ value: "MAIN_PORT", default: "10000" },
{ value: "COUCH_DB_URL", default: exports.DEFAULT_COUCH },
{ value: "MINIO_URL", default: exports.DEFAULT_MINIO },
{ value: "MINIO_ACCESS_KEY" },
{ value: "MINIO_SECRET_KEY" },
]
exports.checkURLs = config => {
const mainPort = config["MAIN_PORT"],
username = config["COUCH_DB_USER"],
password = config["COUCH_DB_PASSWORD"]
if (!config["COUCH_DB_URL"] && mainPort && username && password) {
config[
"COUCH_DB_URL"
] = `http://${username}:${password}@localhost:${mainPort}/db/`
}
if (!config["MINIO_URL"]) {
config["MINIO_URL"] = exports.DEFAULT_MINIO
}
return config
}
exports.askQuestions = async () => {
console.log(
"*** NOTE: use a .env file to load these parameters repeatedly ***"
)
let config = {}
for (let property of REQUIRED) {
config[property.value] = await string(property.value, property.default)
}
return config
}
exports.loadEnvironment = path => {
if (!fs.existsSync(path)) {
throw "Unable to file specified .env file"
}
const env = fs.readFileSync(path, "utf8")
const config = exports.checkURLs(dotenv.parse(env))
for (let required of REQUIRED) {
if (!config[required.value]) {
throw `Cannot find "${required.value}" property in .env file`
}
}
return config
}
// true is the default value passed by commander
exports.getConfig = async (envFile = true) => {
let config
if (envFile !== true) {
config = exports.loadEnvironment(envFile)
} else {
config = await exports.askQuestions()
}
return config
}
exports.replication = (from, to) => {
return new Promise((resolve, reject) => {
from.replicate
.to(to)
.on("complete", () => {
resolve()
})
.on("error", err => {
reject(err)
})
})
}
exports.getPouches = config => {
const Remote = getPouch(config["COUCH_DB_URL"])
const Local = getPouch()
return { Remote, Local }
}

View File

@ -1,4 +1,5 @@
exports.CommandWords = { exports.CommandWords = {
BACKUPS: "backups",
HOSTING: "hosting", HOSTING: "hosting",
ANALYTICS: "analytics", ANALYTICS: "analytics",
HELP: "help", HELP: "help",

View File

@ -0,0 +1,38 @@
const PouchDB = require("pouchdb")
const { checkSlashesInUrl } = require("../utils")
const fetch = require("node-fetch")
/**
* Fully qualified URL including username and password, or nothing for local
*/
exports.getPouch = (url = undefined) => {
let POUCH_DB_DEFAULTS = {}
if (!url) {
POUCH_DB_DEFAULTS = {
prefix: undefined,
adapter: "leveldb",
}
} else {
POUCH_DB_DEFAULTS = {
prefix: url,
}
}
const replicationStream = require("pouchdb-replication-stream")
PouchDB.plugin(replicationStream.plugin)
PouchDB.adapter("writableStream", replicationStream.adapters.writableStream)
return PouchDB.defaults(POUCH_DB_DEFAULTS)
}
exports.getAllDbs = async url => {
const response = await fetch(
checkSlashesInUrl(encodeURI(`${url}/_all_dbs`)),
{
method: "GET",
}
)
if (response.status === 200) {
return await response.json()
} else {
throw "Cannot connect to CouchDB instance"
}
}

View File

@ -1,4 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
require("./prebuilds")
const { getCommands } = require("./options") const { getCommands } = require("./options")
const { Command } = require("commander") const { Command } = require("commander")
const { getHelpDescription } = require("./utils") const { getHelpDescription } = require("./utils")

View File

@ -1,6 +1,7 @@
const analytics = require("./analytics") const analytics = require("./analytics")
const hosting = require("./hosting") const hosting = require("./hosting")
const backups = require("./backups")
exports.getCommands = () => { exports.getCommands = () => {
return [hosting.command, analytics.command] return [hosting.command, analytics.command, backups.command]
} }

View File

@ -0,0 +1,34 @@
const os = require("os")
const { join } = require("path")
const fs = require("fs")
const PREBUILDS = "prebuilds"
const ARCH = `${os.platform()}-${os.arch()}`
const PREBUILD_DIR = join(process.execPath, "..", PREBUILDS, ARCH)
checkForBinaries()
function checkForBinaries() {
const readDir = join(__filename, "..", "..", PREBUILDS, ARCH)
if (fs.existsSync(PREBUILD_DIR) || !fs.existsSync(readDir)) {
return
}
const natives = fs.readdirSync(readDir)
if (fs.existsSync(readDir)) {
fs.mkdirSync(PREBUILD_DIR, { recursive: true })
for (let native of natives) {
const filename = `${native.split(".fake")[0]}.node`
fs.cpSync(join(readDir, native), join(PREBUILD_DIR, filename))
}
}
}
function cleanup() {
if (fs.existsSync(PREBUILD_DIR)) {
fs.rmSync(PREBUILD_DIR, { recursive: true })
}
}
const events = ["exit", "SIGINT", "SIGUSR1", "SIGUSR2", "uncaughtException"]
events.forEach(event => {
process.on(event, cleanup)
})

View File

@ -39,8 +39,10 @@ class Command {
let executed = false let executed = false
for (let opt of thisCmd.opts) { for (let opt of thisCmd.opts) {
const lookup = opt.command.split(" ")[0].replace("--", "") const lookup = opt.command.split(" ")[0].replace("--", "")
if (options[lookup]) { if (!executed && options[lookup]) {
await opt.func(options[lookup]) const input =
Object.keys(options).length > 1 ? options : options[lookup]
await opt.func(input)
executed = true executed = true
} }
} }

View File

@ -2,6 +2,7 @@ const chalk = require("chalk")
const fs = require("fs") const fs = require("fs")
const axios = require("axios") const axios = require("axios")
const path = require("path") const path = require("path")
const progress = require("cli-progress")
exports.downloadFile = async (url, filePath) => { exports.downloadFile = async (url, filePath) => {
filePath = path.resolve(filePath) filePath = path.resolve(filePath)
@ -56,3 +57,13 @@ exports.parseEnv = env => {
} }
return result return result
} }
exports.progressBar = total => {
const bar = new progress.SingleBar({}, progress.Presets.shades_classic)
bar.start(total, 0)
return bar
}
exports.checkSlashesInUrl = url => {
return url.replace(/(https?:\/\/)|(\/)+/g, "$1$2")
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.0.220-alpha.4", "version": "1.1.15-alpha.2",
"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.220-alpha.4", "@budibase/bbui": "^1.1.15-alpha.2",
"@budibase/frontend-core": "^1.0.220-alpha.4", "@budibase/frontend-core": "^1.1.15-alpha.2",
"@budibase/string-templates": "^1.0.220-alpha.4", "@budibase/string-templates": "^1.1.15-alpha.2",
"@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",
@ -39,7 +39,7 @@
"sanitize-html": "^2.7.0", "sanitize-html": "^2.7.0",
"screenfull": "^6.0.1", "screenfull": "^6.0.1",
"shortid": "^2.2.15", "shortid": "^2.2.15",
"svelte": "^3.38.2", "svelte": "^3.49.0",
"svelte-apexcharts": "^1.0.2", "svelte-apexcharts": "^1.0.2",
"svelte-flatpickr": "^3.1.0", "svelte-flatpickr": "^3.1.0",
"svelte-spa-router": "^3.0.5" "svelte-spa-router": "^3.0.5"

View File

@ -323,6 +323,9 @@
position: relative; position: relative;
padding: 32px; padding: 32px;
} }
.main.size--max {
padding: 0;
}
.layout--none .main { .layout--none .main {
padding: 0; padding: 0;
} }
@ -465,6 +468,9 @@
.mobile:not(.layout--none) .main { .mobile:not(.layout--none) .main {
padding: 16px; padding: 16px;
} }
.mobile .main.size--max {
padding: 0;
}
/* Transform links into drawer */ /* Transform links into drawer */
.mobile .links { .mobile .links {

View File

@ -98,7 +98,7 @@
}) })
} }
}) })
return enrichedColumns.slice(0, 3) return enrichedColumns.slice(0, 5)
} }
// Builds a full details page URL for the card title // Builds a full details page URL for the card title

View File

@ -89,7 +89,7 @@
}) })
} }
}) })
return enrichedColumns.slice(0, 3) return enrichedColumns.slice(0, 5)
} }
// Load the datasource schema so we can determine column types // Load the datasource schema so we can determine column types

View File

@ -1446,10 +1446,10 @@ svelte-spa-router@^3.0.5:
dependencies: dependencies:
regexparam "2.0.0" regexparam "2.0.0"
svelte@^3.38.2: svelte@^3.49.0:
version "3.46.4" version "3.49.0"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.46.4.tgz#0c46bc4a3e20a2617a1b7dc43a722f9d6c084a38" resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.49.0.tgz#5baee3c672306de1070c3b7888fc2204e36a4029"
integrity sha512-qKJzw6DpA33CIa+C/rGp4AUdSfii0DOTCzj/2YpSKKayw5WGSS624Et9L1nU1k2OVRS9vaENQXp2CVZNU+xvIg== integrity sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==
svg.draggable.js@^2.2.2: svg.draggable.js@^2.2.2:
version "2.2.2" version "2.2.2"
@ -1536,7 +1536,7 @@ timsort@^0.3.0:
util-deprecate@^1.0.2: util-deprecate@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
wrap-ansi@^7.0.0: wrap-ansi@^7.0.0:
version "7.0.0" version "7.0.0"

View File

@ -1,12 +1,12 @@
{ {
"name": "@budibase/frontend-core", "name": "@budibase/frontend-core",
"version": "1.0.220-alpha.4", "version": "1.1.15-alpha.2",
"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.220-alpha.4", "@budibase/bbui": "^1.1.15-alpha.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"svelte": "^3.46.2" "svelte": "^3.46.2"
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.220-alpha.4", "version": "1.1.15-alpha.2",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -77,10 +77,11 @@
"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.220-alpha.4", "@budibase/backend-core": "^1.1.15-alpha.2",
"@budibase/client": "^1.0.220-alpha.4", "@budibase/client": "^1.1.15-alpha.2",
"@budibase/pro": "1.0.220-alpha.4", "@budibase/pro": "1.1.15-alpha.2",
"@budibase/string-templates": "^1.0.220-alpha.4", "@budibase/string-templates": "^1.1.15-alpha.2",
"@budibase/types": "^1.1.15-alpha.2",
"@bull-board/api": "3.7.0", "@bull-board/api": "3.7.0",
"@bull-board/koa": "3.9.4", "@bull-board/koa": "3.9.4",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",
@ -136,7 +137,7 @@
"redis": "4", "redis": "4",
"server-destroy": "1.0.1", "server-destroy": "1.0.1",
"snowflake-promise": "^4.5.0", "snowflake-promise": "^4.5.0",
"svelte": "3.44.1", "svelte": "3.49.0",
"swagger-parser": "10.0.3", "swagger-parser": "10.0.3",
"to-json-schema": "0.2.5", "to-json-schema": "0.2.5",
"uuid": "3.3.2", "uuid": "3.3.2",
@ -151,7 +152,6 @@
"@babel/core": "7.17.4", "@babel/core": "7.17.4",
"@babel/preset-env": "7.16.11", "@babel/preset-env": "7.16.11",
"@budibase/standard-components": "^0.9.139", "@budibase/standard-components": "^0.9.139",
"@budibase/types": "^1.0.220-alpha.4",
"@jest/test-sequencer": "24.9.0", "@jest/test-sequencer": "24.9.0",
"@types/apidoc": "0.50.0", "@types/apidoc": "0.50.0",
"@types/bson": "4.2.0", "@types/bson": "4.2.0",

View File

@ -22,9 +22,6 @@ const {
BUILTIN_ROLE_IDS, BUILTIN_ROLE_IDS,
AccessController, AccessController,
} = require("@budibase/backend-core/roles") } = 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 { CacheKeys, bustCache } = require("@budibase/backend-core/cache")
const { const {
getAllApps, getAllApps,
@ -45,13 +42,8 @@ const { getTenantId, isMultiTenant } = require("@budibase/backend-core/tenancy")
import { syncGlobalUsers } from "./user" import { syncGlobalUsers } from "./user"
const { app: appCache } = require("@budibase/backend-core/cache") const { app: appCache } = require("@budibase/backend-core/cache")
import { cleanupAutomations } from "../../automations/utils" import { cleanupAutomations } from "../../automations/utils"
import { context } from "@budibase/backend-core"
import { checkAppMetadata } from "../../automations/logging" import { checkAppMetadata } from "../../automations/logging"
const {
getAppDB,
getProdAppDB,
updateAppId,
doInAppContext,
} = require("@budibase/backend-core/context")
import { getUniqueRows } from "../../utilities/usageQuota/rows" import { getUniqueRows } from "../../utilities/usageQuota/rows"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { errors, events, migrations } from "@budibase/backend-core" 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 // utility function, need to do away with this
async function getLayouts() { async function getLayouts() {
const db = getAppDB() const db = context.getAppDB()
return ( return (
await db.allDocs( await db.allDocs(
getLayoutParams(null, { getLayoutParams(null, {
@ -72,7 +64,7 @@ async function getLayouts() {
} }
async function getScreens() { async function getScreens() {
const db = getAppDB() const db = context.getAppDB()
return ( return (
await db.allDocs( await db.allDocs(
getScreenParams(null, { getScreenParams(null, {
@ -135,9 +127,9 @@ async function createInstance(template: any) {
const tenantId = isMultiTenant() ? getTenantId() : null const tenantId = isMultiTenant() ? getTenantId() : null
const baseAppId = generateAppID(tenantId) const baseAppId = generateAppID(tenantId)
const appId = generateDevAppID(baseAppId) const appId = generateDevAppID(baseAppId)
await updateAppId(appId) await context.updateAppId(appId)
const db = getAppDB() const db = context.getAppDB()
await db.put({ await db.put({
_id: "_design/database", _id: "_design/database",
// view collation information, read before writing any complex views: // 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) => { export const fetchAppPackage = async (ctx: any) => {
const db = getAppDB() const db = context.getAppDB()
const application = await db.get(DocumentTypes.APP_METADATA) const application = await db.get(DocumentTypes.APP_METADATA)
const layouts = await getLayouts() const layouts = await getLayouts()
let screens = await getScreens() let screens = await getScreens()
@ -252,7 +244,7 @@ const performAppCreate = async (ctx: any) => {
const instance = await createInstance(instanceConfig) const instance = await createInstance(instanceConfig)
const appId = instance._id const appId = instance._id
const db = getAppDB() const db = context.getAppDB()
let _rev let _rev
try { try {
// if template there will be an existing doc // if template there will be an existing doc
@ -390,7 +382,7 @@ export const update = async (ctx: any) => {
export const updateClient = async (ctx: any) => { export const updateClient = async (ctx: any) => {
// Get current app version // Get current app version
const db = getAppDB() const db = context.getAppDB()
const application = await db.get(DocumentTypes.APP_METADATA) const application = await db.get(DocumentTypes.APP_METADATA)
const currentVersion = application.version const currentVersion = application.version
@ -414,7 +406,7 @@ export const updateClient = async (ctx: any) => {
export const revertClient = async (ctx: any) => { export const revertClient = async (ctx: any) => {
// Check app can be reverted // Check app can be reverted
const db = getAppDB() const db = context.getAppDB()
const application = await db.get(DocumentTypes.APP_METADATA) const application = await db.get(DocumentTypes.APP_METADATA)
if (!application.revertableVersion) { if (!application.revertableVersion) {
ctx.throw(400, "There is no version to revert to") ctx.throw(400, "There is no version to revert to")
@ -446,7 +438,7 @@ const destroyApp = async (ctx: any) => {
appId = getProdAppID(appId) appId = getProdAppID(appId)
} }
const db = isUnpublish ? getProdAppDB() : getAppDB() const db = isUnpublish ? context.getProdAppDB() : context.getAppDB()
const app = await db.get(DocumentTypes.APP_METADATA) const app = await db.get(DocumentTypes.APP_METADATA)
const result = await db.destroy() const result = await db.destroy()
@ -514,7 +506,7 @@ export const sync = async (ctx: any, next: any) => {
try { try {
// specific case, want to make sure setup is skipped // 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() const info = await prodDb.info()
if (info.error) throw info.error if (info.error) throw info.error
} catch (err) { } catch (err) {
@ -556,8 +548,8 @@ export const sync = async (ctx: any, next: any) => {
} }
const updateAppPackage = async (appPackage: any, appId: any) => { const updateAppPackage = async (appPackage: any, appId: any) => {
return doInAppContext(appId, async () => { return context.doInAppContext(appId, async () => {
const db = getAppDB() const db = context.getAppDB()
const application = await db.get(DocumentTypes.APP_METADATA) const application = await db.get(DocumentTypes.APP_METADATA)
const newAppPackage = { ...application, ...appPackage } const newAppPackage = { ...application, ...appPackage }

View File

@ -1,5 +1,5 @@
import { generateQueryID, getQueryParams, isProdAppID } from "../../../db/utils" import { generateQueryID, getQueryParams, isProdAppID } from "../../../db/utils"
import { BaseQueryVerbs } from "../../../constants" import { BaseQueryVerbs, FieldTypes } from "../../../constants"
import { Thread, ThreadType } from "../../../threads" import { Thread, ThreadType } from "../../../threads"
import { save as saveDatasource } from "../datasource" import { save as saveDatasource } from "../datasource"
import { RestImporter } from "./import" import { RestImporter } from "./import"
@ -154,10 +154,37 @@ export async function preview(ctx: any) {
}, },
}) })
const { rows, keys, info, extra } = await quotas.addQuery(runFn) 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) await events.query.previewed(datasource, query)
ctx.body = { ctx.body = {
rows, rows,
schemaFields: [...new Set(keys)], schemaFields,
info, info,
extra, extra,
} }

View File

@ -37,9 +37,10 @@ describe("/permission", () => {
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
expect(res.body).toBeDefined() expect(res.body).toBeDefined()
expect(res.body.length).toEqual(2) expect(res.body.length).toEqual(3)
expect(res.body).toContain("read") expect(res.body).toContain("read")
expect(res.body).toContain("write") expect(res.body).toContain("write")
expect(res.body).toContain("execute")
}) })
}) })

View File

@ -215,7 +215,10 @@ describe("/queries", () => {
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200) .expect(200)
// these responses come from the mock // 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(res.body.rows.length).toEqual(1)
expect(events.query.previewed).toBeCalledTimes(1) expect(events.query.previewed).toBeCalledTimes(1)
expect(events.query.previewed).toBeCalledWith(datasource, query) expect(events.query.previewed).toBeCalledWith(datasource, query)
@ -289,7 +292,11 @@ describe("/queries", () => {
queryString: "test={{ variable2 }}", queryString: "test={{ variable2 }}",
}) })
// these responses come from the mock // 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") expect(res.body.rows[0].url).toEqual("http://www.google.com?test=1")
}) })
@ -299,7 +306,11 @@ describe("/queries", () => {
path: "www.google.com", path: "www.google.com",
queryString: "test={{ variable3 }}", 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") expect(res.body.rows[0].url).toContain("doctype html")
}) })
@ -318,7 +329,11 @@ describe("/queries", () => {
path: "www.failonce.com", path: "www.failonce.com",
queryString: "test={{ variable3 }}", 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) expect(res.body.rows[0].fails).toEqual(1)
}) })

View File

@ -46,26 +46,26 @@ describe("/rows", () => {
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 rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage() // 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)
.set(config.defaultHeaders()) // .set(config.defaultHeaders())
.expect('Content-Type', /json/) // .expect('Content-Type', /json/)
.expect(200) // .expect(200)
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 assertRowUsage(rowUsage + 1)
await assertQueryUsage(queryUsage + 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 rowUsage = await getRowUsage()
const queryUsage = await getQueryUsage() // const queryUsage = await getQueryUsage()
const res = await request const res = await request
.post(`/api/${table._id}/rows`) .post(`/api/${table._id}/rows`)
@ -78,11 +78,11 @@ describe("/rows", () => {
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
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 assertRowUsage(rowUsage)
await assertQueryUsage(queryUsage + 1) // await assertQueryUsage(queryUsage + 1)
}) })
it("should load a row", async () => { it("should load a row", async () => {

View File

@ -20,7 +20,6 @@ import redis from "./utilities/redis"
import * as migrations from "./migrations" import * as migrations from "./migrations"
import { events, installation, tenancy } from "@budibase/backend-core" import { events, installation, tenancy } from "@budibase/backend-core"
import { createAdminUser, getChecklist } from "./utilities/workerRequests" import { createAdminUser, getChecklist } from "./utilities/workerRequests"
import { tenantSucceeded } from "@budibase/backend-core/dist/src/events/publishers/backfill"
const app = new Koa() const app = new Koa()

View File

@ -97,7 +97,7 @@ export async function enableCronTrigger(appId: any, automation: any) {
) )
} }
// need to create cron job // need to create cron job
if (isCronTrigger(automation)) { if (isCronTrigger(automation) && trigger?.inputs.cron) {
// make a job id rather than letting Bull decide, makes it easier to handle on way out // make a job id rather than letting Bull decide, makes it easier to handle on way out
const jobId = `${appId}_cron_${newid()}` const jobId = `${appId}_cron_${newid()}`
const job: any = await queue.add( const job: any = await queue.add(

View File

@ -53,7 +53,7 @@ const INTEGRATIONS = {
} }
// optionally add oracle integration if the oracle binary can be installed // 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") const oracle = require("./oracle")
DEFINITIONS[SourceNames.ORACLE] = oracle.schema DEFINITIONS[SourceNames.ORACLE] = oracle.schema
INTEGRATIONS[SourceNames.ORACLE] = oracle.integration INTEGRATIONS[SourceNames.ORACLE] = oracle.integration

View File

@ -4,15 +4,13 @@ const { getGlobalDB, doInTenant } = require("@budibase/backend-core/tenancy")
// mock email view creation // mock email view creation
const coreDb = require("@budibase/backend-core/db") const coreDb = require("@budibase/backend-core/db")
const createUserEmailView = jest.fn() const createNewUserEmailView = jest.fn()
coreDb.createUserEmailView = createUserEmailView coreDb.createNewUserEmailView = createNewUserEmailView
const migration = require("../userEmailViewCasing") const migration = require("../userEmailViewCasing")
describe("run", () => { describe("run", () => {
doInTenant(TENANT_ID, () => {
let config = new TestConfig(false) let config = new TestConfig(false)
const globalDb = getGlobalDB()
beforeEach(async () => { beforeEach(async () => {
await config.init() await config.init()
@ -21,8 +19,10 @@ describe("run", () => {
afterAll(config.end) afterAll(config.end)
it("runs successfully", async () => { it("runs successfully", async () => {
await migration.run(globalDb) await doInTenant(TENANT_ID, async () => {
expect(createUserEmailView).toHaveBeenCalledTimes(1) const globalDb = getGlobalDB()
await migration.run(globalDb)
expect(createNewUserEmailView).toHaveBeenCalledTimes(1)
})
}) })
})
}) })

View File

@ -1,4 +1,4 @@
const { createUserEmailView } = require("@budibase/backend-core/db") const { createNewUserEmailView } = require("@budibase/backend-core/db")
/** /**
* Date: * Date:
@ -9,5 +9,5 @@ const { createUserEmailView } = require("@budibase/backend-core/db")
*/ */
export const run = async (db: any) => { export const run = async (db: any) => {
await createUserEmailView(db) await createNewUserEmailView(db)
} }

View File

@ -106,21 +106,31 @@ class TestConfiguration {
// UTILS // UTILS
async _req(config, params, controlFunc) { async _req(body, params, controlFunc, opts = { prodApp: false }) {
// create a fake request ctx
const request = {} 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 // fake cookies, we don't need them
request.cookies = { set: () => {}, get: () => {} } request.cookies = { set: () => {}, get: () => {} }
request.config = { jwtSecret: env.JWT_SECRET } request.config = { jwtSecret: env.JWT_SECRET }
request.appId = this.appId request.user = { appId, tenantId: TENANT_ID }
request.user = { appId: this.appId, tenantId: TENANT_ID }
request.query = {} request.query = {}
request.request = { request.request = {
body: config, body,
} }
return this.doInContext(this.appId, async () => { if (params) {
if (params) { request.params = params
request.params = params }
} return this.doInContext(appId, async () => {
await controlFunc(request) await controlFunc(request)
return request.body return request.body
}) })
@ -323,7 +333,6 @@ class TestConfiguration {
// create production app // create production app
this.prodApp = await this.deploy() this.prodApp = await this.deploy()
this.prodAppId = this.prodApp.appId
this.allApps.push(this.prodApp) this.allApps.push(this.prodApp)
this.allApps.push(this.app) this.allApps.push(this.app)
@ -334,11 +343,13 @@ class TestConfiguration {
async deploy() { async deploy() {
await this._req(null, null, controllers.deploy.deployApp) await this._req(null, null, controllers.deploy.deployApp)
const prodAppId = this.getAppId().replace("_dev", "") const prodAppId = this.getAppId().replace("_dev", "")
this.prodAppId = prodAppId
return context.doInAppContext(prodAppId, async () => { return context.doInAppContext(prodAppId, async () => {
const appPackage = await this._req( const appPackage = await this._req(
null, null,
{ appId: prodAppId }, { appId: prodAppId },
controllers.app.fetchAppPackage controllers.app.fetchAppPackage,
{ prodApp: true }
) )
return appPackage.application return appPackage.application
}) })

View File

@ -13,6 +13,7 @@ const { DocumentTypes } = require("../db/utils")
const CURRENTLY_SUPPORTED_LEVELS = [ const CURRENTLY_SUPPORTED_LEVELS = [
PermissionLevels.WRITE, PermissionLevels.WRITE,
PermissionLevels.READ, PermissionLevels.READ,
PermissionLevels.EXECUTE,
] ]
exports.getPermissionType = resourceId => { exports.getPermissionType = resourceId => {

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "1.0.220-alpha.4", "version": "1.1.15-alpha.2",
"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

@ -1,6 +1,6 @@
{ {
"name": "@budibase/types", "name": "@budibase/types",
"version": "1.0.220-alpha.4", "version": "1.1.15-alpha.2",
"description": "Budibase types", "description": "Budibase types",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@ -4,6 +4,7 @@ import { IdentityType } from "./events/identification"
export interface BaseContext { export interface BaseContext {
_id: string _id: string
type: IdentityType type: IdentityType
tenantId?: string
} }
export interface AccountUserContext extends BaseContext { export interface AccountUserContext extends BaseContext {
@ -13,6 +14,7 @@ export interface AccountUserContext extends BaseContext {
export interface UserContext extends BaseContext, User { export interface UserContext extends BaseContext, User {
_id: string _id: string
tenantId: string
account?: Account account?: Account
} }

Some files were not shown because too many files have changed in this diff Show More