Merge remote-tracking branch 'origin/develop' into feat/user-groups-tab
This commit is contained in:
commit
ca41e88045
106 changed files with 5004 additions and 15655 deletions
20
.github/workflows/deploy-single-image.yml
vendored
20
.github/workflows/deploy-single-image.yml
vendored
|
@ -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
|
||||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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: {}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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: {}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 &
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.220-alpha.4",
|
"version": "1.1.15-alpha.2",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
17
packages/backend-core/src/context/constants.ts
Normal file
17
packages/backend-core/src/context/constants.ts
Normal 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",
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
247
packages/backend-core/src/context/index.ts
Normal file
247
packages/backend-core/src/context/index.ts
Normal 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)
|
||||||
|
}
|
148
packages/backend-core/src/context/tests/index.spec.ts
Normal file
148
packages/backend-core/src/context/tests/index.spec.ts
Normal 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")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
113
packages/backend-core/src/context/utils.ts
Normal file
113
packages/backend-core/src/context/utils.ts
Normal 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
|
||||||
|
}
|
|
@ -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",
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 || []
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 || {}
|
||||||
|
|
|
@ -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,10 +102,12 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
28
packages/builder/src/constants/backend/automations.js
Normal file
28
packages/builder/src/constants/backend/automations.js
Normal 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",
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 || [] }
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
1
packages/cli/.gitignore
vendored
1
packages/cli/.gitignore
vendored
|
@ -4,3 +4,4 @@ nginx.conf
|
||||||
build/
|
build/
|
||||||
docker-error.log
|
docker-error.log
|
||||||
envoy.yaml
|
envoy.yaml
|
||||||
|
*.tar.gz
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
121
packages/cli/src/backups/index.js
Normal file
121
packages/cli/src/backups/index.js
Normal 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
|
63
packages/cli/src/backups/objectStore.js
Normal file
63
packages/cli/src/backups/objectStore.js
Normal 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()
|
||||||
|
}
|
88
packages/cli/src/backups/utils.js
Normal file
88
packages/cli/src/backups/utils.js
Normal 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 }
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
exports.CommandWords = {
|
exports.CommandWords = {
|
||||||
|
BACKUPS: "backups",
|
||||||
HOSTING: "hosting",
|
HOSTING: "hosting",
|
||||||
ANALYTICS: "analytics",
|
ANALYTICS: "analytics",
|
||||||
HELP: "help",
|
HELP: "help",
|
||||||
|
|
38
packages/cli/src/core/db.js
Normal file
38
packages/cli/src/core/db.js
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
|
|
@ -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]
|
||||||
}
|
}
|
||||||
|
|
34
packages/cli/src/prebuilds.js
Normal file
34
packages/cli/src/prebuilds.js
Normal 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)
|
||||||
|
})
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
@ -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"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
@ -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",
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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`)
|
||||||
|
@ -81,8 +81,8 @@ describe("/rows", () => {
|
||||||
|
|
||||||
expect(res.res.statusMessage).toEqual(`${table.name} updated successfully.`)
|
expect(res.res.statusMessage).toEqual(`${table.name} updated successfully.`)
|
||||||
expect(res.body.name).toEqual("Updated Name")
|
expect(res.body.name).toEqual("Updated Name")
|
||||||
await assertRowUsage(rowUsage)
|
// await assertRowUsage(rowUsage)
|
||||||
await assertQueryUsage(queryUsage + 1)
|
// await assertQueryUsage(queryUsage + 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should load a row", async () => {
|
it("should load a row", async () => {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue