diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml
index e940e6fa10..42a0c0a273 100644
--- a/.github/workflows/budibase_ci.yml
+++ b/.github/workflows/budibase_ci.yml
@@ -59,3 +59,9 @@ jobs:
with:
install: false
command: yarn test:e2e:ci
+
+ - name: QA Core Integration Tests
+ run: |
+ cd qa-core
+ yarn
+ yarn api:test:ci
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 32c6faf980..e1d3e6db0e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -63,6 +63,7 @@ typings/
# dotenv environment variables file
.env
+!qa-core/.env
!hosting/.env
hosting/.generated-nginx.dev.conf
hosting/proxy/.generated-nginx.prod.conf
diff --git a/.prettierignore b/.prettierignore
index bbeff65da7..ad36a86b99 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -8,4 +8,4 @@ packages/server/client
packages/server/src/definitions/openapi.ts
packages/builder/.routify
packages/builder/cypress/support/queryLevelTransformerFunction.js
-packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
+packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
\ No newline at end of file
diff --git a/README.md b/README.md
index 1dec1737da..bd38610566 100644
--- a/README.md
+++ b/README.md
@@ -65,7 +65,7 @@ Budibase is open-source - licensed as GPL v3. This should fill you with confiden
### Load data or start from scratch
-Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
+Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no datasources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
diff --git a/examples/nextjs-api-sales/definitions/openapi.ts b/examples/nextjs-api-sales/definitions/openapi.ts
index 4f4ad45fc6..7f7f6befec 100644
--- a/examples/nextjs-api-sales/definitions/openapi.ts
+++ b/examples/nextjs-api-sales/definitions/openapi.ts
@@ -348,7 +348,7 @@ export interface paths {
}
}
responses: {
- /** Returns the created table, including the ID which has been generated for it. This can be internal or external data sources. */
+ /** Returns the created table, including the ID which has been generated for it. This can be internal or external datasources. */
200: {
content: {
"application/json": components["schemas"]["tableOutput"]
@@ -959,7 +959,7 @@ export interface components {
query: {
/** @description The ID of the query. */
_id: string
- /** @description The ID of the data source the query belongs to. */
+ /** @description The ID of the datasource the query belongs to. */
datasourceId?: string
/** @description The bindings which are required to perform this query. */
parameters?: string[]
@@ -983,7 +983,7 @@ export interface components {
data: {
/** @description The ID of the query. */
_id: string
- /** @description The ID of the data source the query belongs to. */
+ /** @description The ID of the datasource the query belongs to. */
datasourceId?: string
/** @description The bindings which are required to perform this query. */
parameters?: string[]
diff --git a/hosting/scripts/build-target-paths.sh b/hosting/scripts/build-target-paths.sh
index fce768e2ee..c974d9a304 100644
--- a/hosting/scripts/build-target-paths.sh
+++ b/hosting/scripts/build-target-paths.sh
@@ -9,7 +9,11 @@ if [[ "${TARGETBUILD}" = "aas" ]]; then
chown -R couchdb:couchdb $DATA_DIR/couch/
apt update
apt-get install -y openssh-server
- sed -i "s/#Port 22/Port 2222/" /etc/ssh/sshd_config
+ echo "root:Docker!" | chpasswd
+ mkdir -p /tmp
+ chmod +x /tmp/ssh_setup.sh \
+ && (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null)
+ cp /etc/sshd_config /etc/ssh/sshd_config
/etc/init.d/ssh restart
sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini
diff --git a/hosting/single/Dockerfile b/hosting/single/Dockerfile
index 476a6e5e94..f34290f627 100644
--- a/hosting/single/Dockerfile
+++ b/hosting/single/Dockerfile
@@ -29,23 +29,8 @@ ENV TARGETBUILD $TARGETBUILD
COPY --from=build /app /app
COPY --from=build /worker /worker
-ENV \
- APP_PORT=4001 \
- ARCHITECTURE=amd \
- BUDIBASE_ENVIRONMENT=PRODUCTION \
- CLUSTER_PORT=80 \
- # CUSTOM_DOMAIN=budi001.custom.com \
- DATA_DIR=/data \
- DEPLOYMENT_ENVIRONMENT=docker \
- MINIO_URL=http://localhost:9000 \
- POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU \
- REDIS_URL=localhost:6379 \
- SELF_HOSTED=1 \
- TARGETBUILD=$TARGETBUILD \
- WORKER_PORT=4002 \
- WORKER_URL=http://localhost:4002 \
- APPS_URL=http://localhost:4001
-
+# ENV CUSTOM_DOMAIN=budi001.custom.com \
+# See runner.sh for Env Vars
# These secret env variables are generated by the runner at startup
# their values can be overriden by the user, they will be written
# to the .env file in the /data directory for use later on
@@ -117,6 +102,8 @@ RUN chmod +x ./build-target-paths.sh
# Script below sets the path for storing data based on $DATA_DIR
# For Azure App Service install SSH & point data locations to /home
+ADD hosting/single/ssh/sshd_config /etc/
+ADD hosting/single/ssh/ssh_setup.sh /tmp
RUN /build-target-paths.sh
# cleanup cache
@@ -124,6 +111,8 @@ RUN yarn cache clean -f
EXPOSE 80
EXPOSE 443
+# Expose port 2222 for SSH on Azure App Service build
+EXPOSE 2222
VOLUME /data
# setup letsencrypt certificate
diff --git a/hosting/single/runner.sh b/hosting/single/runner.sh
index 77015d75ee..cf82e6701b 100644
--- a/hosting/single/runner.sh
+++ b/hosting/single/runner.sh
@@ -1,6 +1,21 @@
#!/bin/bash
declare -a ENV_VARS=("COUCHDB_USER" "COUCHDB_PASSWORD" "DATA_DIR" "MINIO_ACCESS_KEY" "MINIO_SECRET_KEY" "INTERNAL_API_KEY" "JWT_SECRET" "REDIS_PASSWORD")
-
+declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONMENT" "CLUSTER_PORT" "DEPLOYMENT_ENVIRONMENT" "MINIO_URL" "NODE_ENV" "POSTHOG_TOKEN" "REDIS_URL" "SELF_HOSTED" "WORKER_PORT" "WORKER_URL")
+# Check the env vars set in Dockerfile have come through, AAS seems to drop them
+[[ -z "${APP_PORT}" ]] && export APP_PORT=4001
+[[ -z "${ARCHITECTURE}" ]] && export ARCHITECTURE=amd
+[[ -z "${BUDIBASE_ENVIRONMENT}" ]] && export BUDIBASE_ENVIRONMENT=PRODUCTION
+[[ -z "${CLUSTER_PORT}" ]] && export CLUSTER_PORT=80
+[[ -z "${DEPLOYMENT_ENVIRONMENT}" ]] && export DEPLOYMENT_ENVIRONMENT=docker
+[[ -z "${MINIO_URL}" ]] && export MINIO_URL=http://localhost:9000
+[[ -z "${NODE_ENV}" ]] && export NODE_ENV=production
+[[ -z "${POSTHOG_TOKEN}" ]] && export POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
+[[ -z "${REDIS_URL}" ]] && export REDIS_URL=localhost:6379
+[[ -z "${SELF_HOSTED}" ]] && export SELF_HOSTED=1
+[[ -z "${WORKER_PORT}" ]] && export WORKER_PORT=4002
+[[ -z "${WORKER_URL}" ]] && export WORKER_URL=http://localhost:4002
+[[ -z "${APPS_URL}" ]] && export APPS_URL=http://localhost:4001
+# export CUSTOM_DOMAIN=budi001.custom.com
# Azure App Service customisations
if [[ "${TARGETBUILD}" = "aas" ]]; then
DATA_DIR=/home
@@ -10,9 +25,10 @@ else
fi
if [ -f "${DATA_DIR}/.env" ]; then
- export $(cat ${DATA_DIR}/.env | xargs)
+ # Read in the .env file and export the variables
+ for LINE in $(cat ${DATA_DIR}/.env); do export $LINE; done
fi
-# first randomise any unset environment variables
+# randomise any unset environment variables
for ENV_VAR in "${ENV_VARS[@]}"
do
temp=$(eval "echo \$$ENV_VAR")
@@ -30,11 +46,18 @@ if [ ! -f "${DATA_DIR}/.env" ]; then
temp=$(eval "echo \$$ENV_VAR")
echo "$ENV_VAR=$temp" >> ${DATA_DIR}/.env
done
+ for ENV_VAR in "${DOCKER_VARS[@]}"
+ do
+ temp=$(eval "echo \$$ENV_VAR")
+ echo "$ENV_VAR=$temp" >> ${DATA_DIR}/.env
+ done
echo "COUCH_DB_URL=${COUCH_DB_URL}" >> ${DATA_DIR}/.env
fi
-export COUCH_DB_URL=http://$COUCHDB_USER:$COUCHDB_PASSWORD@localhost:5984
-
+# Read in the .env file and export the variables
+for LINE in $(cat ${DATA_DIR}/.env); do export $LINE; done
+ln -s ${DATA_DIR}/.env /app/.env
+ln -s ${DATA_DIR}/.env /worker/.env
# make these directories in runner, incase of mount
mkdir -p ${DATA_DIR}/couch/{dbs,views}
mkdir -p ${DATA_DIR}/minio
diff --git a/hosting/single/ssh/ssh_setup.sh b/hosting/single/ssh/ssh_setup.sh
new file mode 100644
index 0000000000..0af0b6d7ad
--- /dev/null
+++ b/hosting/single/ssh/ssh_setup.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+ssh-keygen -A
+
+#prepare run dir
+if [ ! -d "/var/run/sshd" ]; then
+ mkdir -p /var/run/sshd
+fi
\ No newline at end of file
diff --git a/hosting/single/ssh/sshd_config b/hosting/single/ssh/sshd_config
new file mode 100644
index 0000000000..7eb5df953a
--- /dev/null
+++ b/hosting/single/ssh/sshd_config
@@ -0,0 +1,12 @@
+Port 2222
+ListenAddress 0.0.0.0
+LoginGraceTime 180
+X11Forwarding yes
+Ciphers aes128-cbc,3des-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr
+MACs hmac-sha1,hmac-sha1-96
+StrictModes yes
+SyslogFacility DAEMON
+PasswordAuthentication yes
+PermitEmptyPasswords no
+PermitRootLogin yes
+Subsystem sftp internal-sftp
diff --git a/lerna.json b/lerna.json
index 6b8eaa45b2..87fc470b92 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "1.3.15-alpha.7",
+ "version": "1.3.19-alpha.6",
"npmClient": "yarn",
"packages": [
"packages/*"
diff --git a/package.json b/package.json
index 4c24e0025b..71acd886d3 100644
--- a/package.json
+++ b/package.json
@@ -45,8 +45,8 @@
"lint:eslint": "eslint packages",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
"lint": "yarn run lint:eslint && yarn run lint:prettier",
- "lint:fix:eslint": "eslint --fix packages",
- "lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
+ "lint:fix:eslint": "eslint --fix packages qa-core",
+ "lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"",
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"test:e2e": "lerna run cy:test --stream",
"test:e2e:ci": "lerna run cy:ci --stream",
diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json
index 627cd16f8c..92ac2acbd1 100644
--- a/packages/backend-core/package.json
+++ b/packages/backend-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
- "version": "1.3.15-alpha.7",
+ "version": "1.3.19-alpha.6",
"description": "Budibase backend core libraries used in server and worker",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
@@ -20,7 +20,7 @@
"test:watch": "jest --watchAll"
},
"dependencies": {
- "@budibase/types": "1.3.15-alpha.7",
+ "@budibase/types": "1.3.19-alpha.6",
"@shopify/jest-koa-mocks": "5.0.1",
"@techpass/passport-openidconnect": "0.3.2",
"aws-sdk": "2.1030.0",
diff --git a/packages/backend-core/src/constants.js b/packages/backend-core/src/constants.js
index 172e66e603..44c271a4f8 100644
--- a/packages/backend-core/src/constants.js
+++ b/packages/backend-core/src/constants.js
@@ -7,6 +7,7 @@ exports.Cookies = {
CurrentApp: "budibase:currentapp",
Auth: "budibase:auth",
Init: "budibase:init",
+ ACCOUNT_RETURN_URL: "budibase:account:returnurl",
DatasourceAuth: "budibase:datasourceauth",
OIDC_CONFIG: "budibase:oidc:config",
}
diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/db/constants.ts
index fd464ba5fb..2c2c29cee2 100644
--- a/packages/backend-core/src/db/constants.ts
+++ b/packages/backend-core/src/db/constants.ts
@@ -19,6 +19,7 @@ export enum ViewName {
ROUTING = "screen_routes",
AUTOMATION_LOGS = "automation_logs",
ACCOUNT_BY_EMAIL = "account_by_email",
+ PLATFORM_USERS_LOWERCASE = "platform_users_lowercase",
}
export const DeprecatedViews = {
diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts
index 4926a60150..c93c7b5662 100644
--- a/packages/backend-core/src/db/utils.ts
+++ b/packages/backend-core/src/db/utils.ts
@@ -2,7 +2,8 @@ import { newid } from "../hashing"
import { DEFAULT_TENANT_ID, Configs } from "../constants"
import env from "../environment"
import { SEPARATOR, DocumentType, UNICODE_MAX, ViewName } from "./constants"
-import { getTenantId, getGlobalDBName, getGlobalDB } from "../tenancy"
+import { getTenantId, getGlobalDB } from "../context"
+import { getGlobalDBName } from "../tenancy/utils"
import fetch from "node-fetch"
import { doWithDB, allDbs } from "./index"
import { getCouchInfo } from "./pouch"
diff --git a/packages/backend-core/src/db/views.js b/packages/backend-core/src/db/views.js
deleted file mode 100644
index b2562bdc71..0000000000
--- a/packages/backend-core/src/db/views.js
+++ /dev/null
@@ -1,203 +0,0 @@
-const {
- DocumentType,
- ViewName,
- DeprecatedViews,
- SEPARATOR,
-} = require("./utils")
-const { getGlobalDB } = require("../tenancy")
-const { StaticDatabases } = require("./constants")
-const { doWithDB } = require("./")
-
-const DESIGN_DB = "_design/database"
-
-function DesignDoc() {
- return {
- _id: DESIGN_DB,
- // view collation information, read before writing any complex views:
- // https://docs.couchdb.org/en/master/ddocs/views/collation.html#collation-specification
- views: {},
- }
-}
-
-async function removeDeprecated(db, viewName) {
- if (!DeprecatedViews[viewName]) {
- return
- }
- try {
- const designDoc = await db.get(DESIGN_DB)
- for (let deprecatedNames of DeprecatedViews[viewName]) {
- delete designDoc.views[deprecatedNames]
- }
- await db.put(designDoc)
- } catch (err) {
- // doesn't exist, ignore
- }
-}
-
-exports.createNewUserEmailView = async () => {
- const db = getGlobalDB()
- let designDoc
- try {
- designDoc = await db.get(DESIGN_DB)
- } catch (err) {
- // no design doc, make one
- designDoc = DesignDoc()
- }
- const view = {
- // if using variables in a map function need to inject them before use
- map: `function(doc) {
- if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}")) {
- emit(doc.email.toLowerCase(), doc._id)
- }
- }`,
- }
- designDoc.views = {
- ...designDoc.views,
- [ViewName.USER_BY_EMAIL]: view,
- }
- await db.put(designDoc)
-}
-
-exports.createAccountEmailView = async () => {
- await doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => {
- let designDoc
- try {
- designDoc = await db.get(DESIGN_DB)
- } catch (err) {
- // no design doc, make one
- designDoc = DesignDoc()
- }
- const view = {
- // if using variables in a map function need to inject them before use
- map: `function(doc) {
- if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) {
- emit(doc.email.toLowerCase(), doc._id)
- }
- }`,
- }
- designDoc.views = {
- ...designDoc.views,
- [ViewName.ACCOUNT_BY_EMAIL]: view,
- }
- await db.put(designDoc)
- })
-}
-
-exports.createUserAppView = async () => {
- const db = getGlobalDB()
- let designDoc
- try {
- designDoc = await db.get("_design/database")
- } catch (err) {
- // no design doc, make one
- designDoc = DesignDoc()
- }
- const view = {
- // if using variables in a map function need to inject them before use
- map: `function(doc) {
- if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}") && doc.roles) {
- for (let prodAppId of Object.keys(doc.roles)) {
- let emitted = prodAppId + "${SEPARATOR}" + doc._id
- emit(emitted, null)
- }
- }
- }`,
- }
- designDoc.views = {
- ...designDoc.views,
- [ViewName.USER_BY_APP]: view,
- }
- await db.put(designDoc)
-}
-
-exports.createApiKeyView = async () => {
- const db = getGlobalDB()
- let designDoc
- try {
- designDoc = await db.get("_design/database")
- } catch (err) {
- designDoc = DesignDoc()
- }
- const view = {
- map: `function(doc) {
- if (doc._id.startsWith("${DocumentType.DEV_INFO}") && doc.apiKey) {
- emit(doc.apiKey, doc.userId)
- }
- }`,
- }
- designDoc.views = {
- ...designDoc.views,
- [ViewName.BY_API_KEY]: view,
- }
- await db.put(designDoc)
-}
-
-exports.createUserBuildersView = async () => {
- const db = getGlobalDB()
- let designDoc
- try {
- designDoc = await db.get("_design/database")
- } catch (err) {
- // no design doc, make one
- designDoc = DesignDoc()
- }
- const view = {
- map: `function(doc) {
- if (doc.builder && doc.builder.global === true) {
- emit(doc._id, doc._id)
- }
- }`,
- }
- designDoc.views = {
- ...designDoc.views,
- [ViewName.USER_BY_BUILDERS]: view,
- }
- await db.put(designDoc)
-}
-
-exports.queryView = async (viewName, params, db, CreateFuncByName) => {
- try {
- let response = (await db.query(`database/${viewName}`, params)).rows
- response = response.map(resp =>
- params.include_docs ? resp.doc : resp.value
- )
- if (params.arrayResponse) {
- return response
- } else {
- return response.length <= 1 ? response[0] : response
- }
- } catch (err) {
- if (err != null && err.name === "not_found") {
- const createFunc = CreateFuncByName[viewName]
- await removeDeprecated(db, viewName)
- await createFunc()
- return exports.queryView(viewName, params, db, CreateFuncByName)
- } else {
- throw err
- }
- }
-}
-
-exports.queryPlatformView = async (viewName, params) => {
- const CreateFuncByName = {
- [ViewName.ACCOUNT_BY_EMAIL]: exports.createAccountEmailView,
- }
-
- return doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => {
- return exports.queryView(viewName, params, db, CreateFuncByName)
- })
-}
-
-exports.queryGlobalView = async (viewName, params, db = null) => {
- const CreateFuncByName = {
- [ViewName.USER_BY_EMAIL]: exports.createNewUserEmailView,
- [ViewName.BY_API_KEY]: exports.createApiKeyView,
- [ViewName.USER_BY_BUILDERS]: exports.createUserBuildersView,
- [ViewName.USER_BY_APP]: exports.createUserAppView,
- }
- // can pass DB in if working with something specific
- if (!db) {
- db = getGlobalDB()
- }
- return exports.queryView(viewName, params, db, CreateFuncByName)
-}
diff --git a/packages/backend-core/src/db/views.ts b/packages/backend-core/src/db/views.ts
new file mode 100644
index 0000000000..c337d26eaa
--- /dev/null
+++ b/packages/backend-core/src/db/views.ts
@@ -0,0 +1,261 @@
+import { DocumentType, ViewName, DeprecatedViews, SEPARATOR } from "./utils"
+import { getGlobalDB } from "../context"
+import PouchDB from "pouchdb"
+import { StaticDatabases } from "./constants"
+import { doWithDB } from "./"
+
+const DESIGN_DB = "_design/database"
+
+function DesignDoc() {
+ return {
+ _id: DESIGN_DB,
+ // view collation information, read before writing any complex views:
+ // https://docs.couchdb.org/en/master/ddocs/views/collation.html#collation-specification
+ views: {},
+ }
+}
+
+interface DesignDocument {
+ views: any
+}
+
+async function removeDeprecated(db: PouchDB.Database, viewName: ViewName) {
+ // @ts-ignore
+ if (!DeprecatedViews[viewName]) {
+ return
+ }
+ try {
+ const designDoc = await db.get(DESIGN_DB)
+ // @ts-ignore
+ for (let deprecatedNames of DeprecatedViews[viewName]) {
+ delete designDoc.views[deprecatedNames]
+ }
+ await db.put(designDoc)
+ } catch (err) {
+ // doesn't exist, ignore
+ }
+}
+
+export const createNewUserEmailView = async () => {
+ const db = getGlobalDB()
+ let designDoc
+ try {
+ designDoc = await db.get(DESIGN_DB)
+ } catch (err) {
+ // no design doc, make one
+ designDoc = DesignDoc()
+ }
+ const view = {
+ // if using variables in a map function need to inject them before use
+ map: `function(doc) {
+ if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}")) {
+ emit(doc.email.toLowerCase(), doc._id)
+ }
+ }`,
+ }
+ designDoc.views = {
+ ...designDoc.views,
+ [ViewName.USER_BY_EMAIL]: view,
+ }
+ await db.put(designDoc)
+}
+
+export const createAccountEmailView = async () => {
+ await doWithDB(
+ StaticDatabases.PLATFORM_INFO.name,
+ async (db: PouchDB.Database) => {
+ let designDoc
+ try {
+ designDoc = await db.get(DESIGN_DB)
+ } catch (err) {
+ // no design doc, make one
+ designDoc = DesignDoc()
+ }
+ const view = {
+ // if using variables in a map function need to inject them before use
+ map: `function(doc) {
+ if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) {
+ emit(doc.email.toLowerCase(), doc._id)
+ }
+ }`,
+ }
+ designDoc.views = {
+ ...designDoc.views,
+ [ViewName.ACCOUNT_BY_EMAIL]: view,
+ }
+ await db.put(designDoc)
+ }
+ )
+}
+
+export const createUserAppView = async () => {
+ const db = getGlobalDB() as PouchDB.Database
+ let designDoc
+ try {
+ designDoc = await db.get("_design/database")
+ } catch (err) {
+ // no design doc, make one
+ designDoc = DesignDoc()
+ }
+ const view = {
+ // if using variables in a map function need to inject them before use
+ map: `function(doc) {
+ if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}") && doc.roles) {
+ for (let prodAppId of Object.keys(doc.roles)) {
+ let emitted = prodAppId + "${SEPARATOR}" + doc._id
+ emit(emitted, null)
+ }
+ }
+ }`,
+ }
+ designDoc.views = {
+ ...designDoc.views,
+ [ViewName.USER_BY_APP]: view,
+ }
+ await db.put(designDoc)
+}
+
+export const createApiKeyView = async () => {
+ const db = getGlobalDB()
+ let designDoc
+ try {
+ designDoc = await db.get("_design/database")
+ } catch (err) {
+ designDoc = DesignDoc()
+ }
+ const view = {
+ map: `function(doc) {
+ if (doc._id.startsWith("${DocumentType.DEV_INFO}") && doc.apiKey) {
+ emit(doc.apiKey, doc.userId)
+ }
+ }`,
+ }
+ designDoc.views = {
+ ...designDoc.views,
+ [ViewName.BY_API_KEY]: view,
+ }
+ await db.put(designDoc)
+}
+
+export const createUserBuildersView = async () => {
+ const db = getGlobalDB()
+ let designDoc
+ try {
+ designDoc = await db.get("_design/database")
+ } catch (err) {
+ // no design doc, make one
+ designDoc = DesignDoc()
+ }
+ const view = {
+ map: `function(doc) {
+ if (doc.builder && doc.builder.global === true) {
+ emit(doc._id, doc._id)
+ }
+ }`,
+ }
+ designDoc.views = {
+ ...designDoc.views,
+ [ViewName.USER_BY_BUILDERS]: view,
+ }
+ await db.put(designDoc)
+}
+
+export const createPlatformUserView = async () => {
+ await doWithDB(
+ StaticDatabases.PLATFORM_INFO.name,
+ async (db: PouchDB.Database) => {
+ let designDoc
+ try {
+ designDoc = await db.get(DESIGN_DB)
+ } catch (err) {
+ // no design doc, make one
+ designDoc = DesignDoc()
+ }
+ const view = {
+ // if using variables in a map function need to inject them before use
+ map: `function(doc) {
+ if (doc.tenantId) {
+ emit(doc._id.toLowerCase(), doc._id)
+ }
+ }`,
+ }
+ designDoc.views = {
+ ...designDoc.views,
+ [ViewName.PLATFORM_USERS_LOWERCASE]: view,
+ }
+ await db.put(designDoc)
+ }
+ )
+}
+
+export interface QueryViewOptions {
+ arrayResponse?: boolean
+}
+
+export const queryView = async (
+ viewName: ViewName,
+ params: PouchDB.Query.Options,
+ db: PouchDB.Database,
+ CreateFuncByName: any,
+ opts?: QueryViewOptions
+): Promise => {
+ try {
+ let response = await db.query(`database/${viewName}`, params)
+ const rows = response.rows
+ const docs = rows.map(row => (params.include_docs ? row.doc : row.value))
+
+ // if arrayResponse has been requested, always return array regardless of length
+ if (opts?.arrayResponse) {
+ return docs
+ } else {
+ // return the single document if there is only one
+ return docs.length <= 1 ? docs[0] : docs
+ }
+ } catch (err: any) {
+ if (err != null && err.name === "not_found") {
+ const createFunc = CreateFuncByName[viewName]
+ await removeDeprecated(db, viewName)
+ await createFunc()
+ return queryView(viewName, params, db, CreateFuncByName, opts)
+ } else {
+ throw err
+ }
+ }
+}
+
+export const queryPlatformView = async (
+ viewName: ViewName,
+ params: PouchDB.Query.Options,
+ opts?: QueryViewOptions
+): Promise => {
+ const CreateFuncByName = {
+ [ViewName.ACCOUNT_BY_EMAIL]: createAccountEmailView,
+ [ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView,
+ }
+
+ return doWithDB(
+ StaticDatabases.PLATFORM_INFO.name,
+ async (db: PouchDB.Database) => {
+ return queryView(viewName, params, db, CreateFuncByName, opts)
+ }
+ )
+}
+
+export const queryGlobalView = async (
+ viewName: ViewName,
+ params: PouchDB.Query.Options,
+ db?: PouchDB.Database,
+ opts?: QueryViewOptions
+): Promise => {
+ const CreateFuncByName = {
+ [ViewName.USER_BY_EMAIL]: createNewUserEmailView,
+ [ViewName.BY_API_KEY]: createApiKeyView,
+ [ViewName.USER_BY_BUILDERS]: createUserBuildersView,
+ [ViewName.USER_BY_APP]: createUserAppView,
+ }
+ // can pass DB in if working with something specific
+ if (!db) {
+ db = getGlobalDB() as PouchDB.Database
+ }
+ return queryView(viewName, params, db, CreateFuncByName, opts)
+}
diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts
index b979635fcc..be1e1eacfc 100644
--- a/packages/backend-core/src/environment.ts
+++ b/packages/backend-core/src/environment.ts
@@ -37,7 +37,7 @@ const env = {
MULTI_TENANCY: process.env.MULTI_TENANCY,
ACCOUNT_PORTAL_URL:
process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app",
- ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY,
+ ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY || "",
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""),
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
diff --git a/packages/backend-core/src/errors/base.js b/packages/backend-core/src/errors/base.js
deleted file mode 100644
index 7cb0c0fc23..0000000000
--- a/packages/backend-core/src/errors/base.js
+++ /dev/null
@@ -1,11 +0,0 @@
-class BudibaseError extends Error {
- constructor(message, code, type) {
- super(message)
- this.code = code
- this.type = type
- }
-}
-
-module.exports = {
- BudibaseError,
-}
diff --git a/packages/backend-core/src/errors/base.ts b/packages/backend-core/src/errors/base.ts
new file mode 100644
index 0000000000..801dcf168d
--- /dev/null
+++ b/packages/backend-core/src/errors/base.ts
@@ -0,0 +1,10 @@
+export class BudibaseError extends Error {
+ code: string
+ type: string
+
+ constructor(message: string, code: string, type: string) {
+ super(message)
+ this.code = code
+ this.type = type
+ }
+}
diff --git a/packages/backend-core/src/errors/generic.js b/packages/backend-core/src/errors/generic.js
deleted file mode 100644
index 5c7661f035..0000000000
--- a/packages/backend-core/src/errors/generic.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const { BudibaseError } = require("./base")
-
-class GenericError extends BudibaseError {
- constructor(message, code, type) {
- super(message, code, type ? type : "generic")
- }
-}
-
-module.exports = {
- GenericError,
-}
diff --git a/packages/backend-core/src/errors/generic.ts b/packages/backend-core/src/errors/generic.ts
new file mode 100644
index 0000000000..71b3352438
--- /dev/null
+++ b/packages/backend-core/src/errors/generic.ts
@@ -0,0 +1,7 @@
+import { BudibaseError } from "./base"
+
+export class GenericError extends BudibaseError {
+ constructor(message: string, code: string, type: string) {
+ super(message, code, type ? type : "generic")
+ }
+}
diff --git a/packages/backend-core/src/errors/http.js b/packages/backend-core/src/errors/http.js
deleted file mode 100644
index 8e7cab4638..0000000000
--- a/packages/backend-core/src/errors/http.js
+++ /dev/null
@@ -1,12 +0,0 @@
-const { GenericError } = require("./generic")
-
-class HTTPError extends GenericError {
- constructor(message, httpStatus, code = "http", type = "generic") {
- super(message, code, type)
- this.status = httpStatus
- }
-}
-
-module.exports = {
- HTTPError,
-}
diff --git a/packages/backend-core/src/errors/http.ts b/packages/backend-core/src/errors/http.ts
new file mode 100644
index 0000000000..182e009f58
--- /dev/null
+++ b/packages/backend-core/src/errors/http.ts
@@ -0,0 +1,15 @@
+import { GenericError } from "./generic"
+
+export class HTTPError extends GenericError {
+ status: number
+
+ constructor(
+ message: string,
+ httpStatus: number,
+ code = "http",
+ type = "generic"
+ ) {
+ super(message, code, type)
+ this.status = httpStatus
+ }
+}
diff --git a/packages/backend-core/src/errors/index.js b/packages/backend-core/src/errors/index.ts
similarity index 65%
rename from packages/backend-core/src/errors/index.js
rename to packages/backend-core/src/errors/index.ts
index 31ffd739a0..be6657093d 100644
--- a/packages/backend-core/src/errors/index.js
+++ b/packages/backend-core/src/errors/index.ts
@@ -1,5 +1,6 @@
-const http = require("./http")
-const licensing = require("./licensing")
+import { HTTPError } from "./http"
+import { UsageLimitError, FeatureDisabledError } from "./licensing"
+import * as licensing from "./licensing"
const codes = {
...licensing.codes,
@@ -11,7 +12,7 @@ const context = {
...licensing.context,
}
-const getPublicError = err => {
+const getPublicError = (err: any) => {
let error
if (err.code || err.type) {
// add generic error information
@@ -32,13 +33,15 @@ const getPublicError = err => {
return error
}
-module.exports = {
+const pkg = {
codes,
types,
errors: {
- UsageLimitError: licensing.UsageLimitError,
- FeatureDisabledError: licensing.FeatureDisabledError,
- HTTPError: http.HTTPError,
+ UsageLimitError,
+ FeatureDisabledError,
+ HTTPError,
},
getPublicError,
}
+
+export = pkg
diff --git a/packages/backend-core/src/errors/licensing.js b/packages/backend-core/src/errors/licensing.js
deleted file mode 100644
index 85d207ac35..0000000000
--- a/packages/backend-core/src/errors/licensing.js
+++ /dev/null
@@ -1,43 +0,0 @@
-const { HTTPError } = require("./http")
-
-const type = "license_error"
-
-const codes = {
- USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
- FEATURE_DISABLED: "feature_disabled",
-}
-
-const context = {
- [codes.USAGE_LIMIT_EXCEEDED]: err => {
- return {
- limitName: err.limitName,
- }
- },
- [codes.FEATURE_DISABLED]: err => {
- return {
- featureName: err.featureName,
- }
- },
-}
-
-class UsageLimitError extends HTTPError {
- constructor(message, limitName) {
- super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type)
- this.limitName = limitName
- }
-}
-
-class FeatureDisabledError extends HTTPError {
- constructor(message, featureName) {
- super(message, 400, codes.FEATURE_DISABLED, type)
- this.featureName = featureName
- }
-}
-
-module.exports = {
- type,
- codes,
- context,
- UsageLimitError,
- FeatureDisabledError,
-}
diff --git a/packages/backend-core/src/errors/licensing.ts b/packages/backend-core/src/errors/licensing.ts
new file mode 100644
index 0000000000..7ffcefa167
--- /dev/null
+++ b/packages/backend-core/src/errors/licensing.ts
@@ -0,0 +1,39 @@
+import { HTTPError } from "./http"
+
+export const type = "license_error"
+
+export const codes = {
+ USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
+ FEATURE_DISABLED: "feature_disabled",
+}
+
+export const context = {
+ [codes.USAGE_LIMIT_EXCEEDED]: (err: any) => {
+ return {
+ limitName: err.limitName,
+ }
+ },
+ [codes.FEATURE_DISABLED]: (err: any) => {
+ return {
+ featureName: err.featureName,
+ }
+ },
+}
+
+export class UsageLimitError extends HTTPError {
+ limitName: string
+
+ constructor(message: string, limitName: string) {
+ super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type)
+ this.limitName = limitName
+ }
+}
+
+export class FeatureDisabledError extends HTTPError {
+ featureName: string
+
+ constructor(message: string, featureName: string) {
+ super(message, 400, codes.FEATURE_DISABLED, type)
+ this.featureName = featureName
+ }
+}
diff --git a/packages/backend-core/src/events/publishers/datasource.ts b/packages/backend-core/src/events/publishers/datasource.ts
index 3cd68033fc..d3ea7402f9 100644
--- a/packages/backend-core/src/events/publishers/datasource.ts
+++ b/packages/backend-core/src/events/publishers/datasource.ts
@@ -5,8 +5,15 @@ import {
DatasourceCreatedEvent,
DatasourceUpdatedEvent,
DatasourceDeletedEvent,
+ SourceName,
} from "@budibase/types"
+function isCustom(datasource: Datasource) {
+ const sources = Object.values(SourceName)
+ // if not in the base source list, then it must be custom
+ return !sources.includes(datasource.source)
+}
+
export async function created(
datasource: Datasource,
timestamp?: string | number
@@ -14,6 +21,7 @@ export async function created(
const properties: DatasourceCreatedEvent = {
datasourceId: datasource._id as string,
source: datasource.source,
+ custom: isCustom(datasource),
}
await publishEvent(Event.DATASOURCE_CREATED, properties, timestamp)
}
@@ -22,6 +30,7 @@ export async function updated(datasource: Datasource) {
const properties: DatasourceUpdatedEvent = {
datasourceId: datasource._id as string,
source: datasource.source,
+ custom: isCustom(datasource),
}
await publishEvent(Event.DATASOURCE_UPDATED, properties)
}
@@ -30,6 +39,7 @@ export async function deleted(datasource: Datasource) {
const properties: DatasourceDeletedEvent = {
datasourceId: datasource._id as string,
source: datasource.source,
+ custom: isCustom(datasource),
}
await publishEvent(Event.DATASOURCE_DELETED, properties)
}
diff --git a/packages/backend-core/src/events/publishers/index.ts b/packages/backend-core/src/events/publishers/index.ts
index 57fd0bf8e2..6fe42c4bda 100644
--- a/packages/backend-core/src/events/publishers/index.ts
+++ b/packages/backend-core/src/events/publishers/index.ts
@@ -18,3 +18,4 @@ export * as view from "./view"
export * as installation from "./installation"
export * as backfill from "./backfill"
export * as group from "./group"
+export * as plugin from "./plugin"
diff --git a/packages/backend-core/src/events/publishers/plugin.ts b/packages/backend-core/src/events/publishers/plugin.ts
new file mode 100644
index 0000000000..4e4d87cf56
--- /dev/null
+++ b/packages/backend-core/src/events/publishers/plugin.ts
@@ -0,0 +1,41 @@
+import { publishEvent } from "../events"
+import {
+ Event,
+ Plugin,
+ PluginDeletedEvent,
+ PluginImportedEvent,
+ PluginInitEvent,
+} from "@budibase/types"
+
+export async function init(plugin: Plugin) {
+ const properties: PluginInitEvent = {
+ type: plugin.schema.type,
+ name: plugin.name,
+ description: plugin.description,
+ version: plugin.version,
+ }
+ await publishEvent(Event.PLUGIN_INIT, properties)
+}
+
+export async function imported(plugin: Plugin) {
+ const properties: PluginImportedEvent = {
+ pluginId: plugin._id as string,
+ type: plugin.schema.type,
+ source: plugin.source,
+ name: plugin.name,
+ description: plugin.description,
+ version: plugin.version,
+ }
+ await publishEvent(Event.PLUGIN_IMPORTED, properties)
+}
+
+export async function deleted(plugin: Plugin) {
+ const properties: PluginDeletedEvent = {
+ pluginId: plugin._id as string,
+ type: plugin.schema.type,
+ name: plugin.name,
+ description: plugin.description,
+ version: plugin.version,
+ }
+ await publishEvent(Event.PLUGIN_DELETED, properties)
+}
diff --git a/packages/backend-core/src/featureFlags/index.js b/packages/backend-core/src/featureFlags/index.js
index 103ac4df59..b328839fda 100644
--- a/packages/backend-core/src/featureFlags/index.js
+++ b/packages/backend-core/src/featureFlags/index.js
@@ -31,20 +31,26 @@ const TENANT_FEATURE_FLAGS = getFeatureFlags()
exports.isEnabled = featureFlag => {
const tenantId = tenancy.getTenantId()
-
- return (
- TENANT_FEATURE_FLAGS &&
- TENANT_FEATURE_FLAGS[tenantId] &&
- TENANT_FEATURE_FLAGS[tenantId].includes(featureFlag)
- )
+ const flags = exports.getTenantFeatureFlags(tenantId)
+ return flags.includes(featureFlag)
}
exports.getTenantFeatureFlags = tenantId => {
- if (TENANT_FEATURE_FLAGS && TENANT_FEATURE_FLAGS[tenantId]) {
- return TENANT_FEATURE_FLAGS[tenantId]
+ const flags = []
+
+ if (TENANT_FEATURE_FLAGS) {
+ const globalFlags = TENANT_FEATURE_FLAGS["*"]
+ const tenantFlags = TENANT_FEATURE_FLAGS[tenantId]
+
+ if (globalFlags) {
+ flags.push(...globalFlags)
+ }
+ if (tenantFlags) {
+ flags.push(...tenantFlags)
+ }
}
- return []
+ return flags
}
exports.FeatureFlag = {
diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts
index d9dbe58264..2c234bd4b8 100644
--- a/packages/backend-core/src/index.ts
+++ b/packages/backend-core/src/index.ts
@@ -1,5 +1,4 @@
import errors from "./errors"
-
const errorClasses = errors.errors
import * as events from "./events"
import * as migrations from "./migrations"
@@ -15,7 +14,7 @@ import deprovisioning from "./context/deprovision"
import auth from "./auth"
import constants from "./constants"
import * as dbConstants from "./db/constants"
-import logging from "./logging"
+import * as logging from "./logging"
import pino from "./pino"
import * as middleware from "./middleware"
import plugins from "./plugin"
diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts
index 062070785d..a3c6b67cde 100644
--- a/packages/backend-core/src/middleware/authenticated.ts
+++ b/packages/backend-core/src/middleware/authenticated.ts
@@ -106,6 +106,7 @@ export = (
user = await getUser(userId, session.tenantId)
}
user.csrfToken = session.csrfToken
+
if (session?.lastAccessedAt < timeMinusOneMinute()) {
// make sure we denote that the session is still in use
await updateSessionTTL(session)
diff --git a/packages/backend-core/src/migrations/definitions.ts b/packages/backend-core/src/migrations/definitions.ts
index 34ec0f0cad..0eea946be8 100644
--- a/packages/backend-core/src/migrations/definitions.ts
+++ b/packages/backend-core/src/migrations/definitions.ts
@@ -17,14 +17,6 @@ export const DEFINITIONS: MigrationDefinition[] = [
type: MigrationType.APP,
name: MigrationName.APP_URLS,
},
- {
- type: MigrationType.GLOBAL,
- name: MigrationName.DEVELOPER_QUOTA,
- },
- {
- type: MigrationType.GLOBAL,
- name: MigrationName.PUBLISHED_APP_QUOTA,
- },
{
type: MigrationType.APP,
name: MigrationName.EVENT_APP_BACKFILL,
diff --git a/packages/backend-core/src/security/sessions.ts b/packages/backend-core/src/security/sessions.ts
index f621b99dc2..33230afc60 100644
--- a/packages/backend-core/src/security/sessions.ts
+++ b/packages/backend-core/src/security/sessions.ts
@@ -2,28 +2,12 @@ const redis = require("../redis/init")
const { v4: uuidv4 } = require("uuid")
const { logWarn } = require("../logging")
const env = require("../environment")
-
-interface CreateSession {
- sessionId: string
- tenantId: string
- csrfToken?: string
-}
-
-interface Session extends CreateSession {
- userId: string
- lastAccessedAt: string
- createdAt: string
- // make optional attributes required
- csrfToken: string
-}
-
-interface SessionKey {
- key: string
-}
-
-interface ScannedSession {
- value: Session
-}
+import {
+ Session,
+ ScannedSession,
+ SessionKey,
+ CreateSession,
+} from "@budibase/types"
// a week in seconds
const EXPIRY_SECONDS = 86400 * 7
diff --git a/packages/backend-core/src/tenancy/index.ts b/packages/backend-core/src/tenancy/index.ts
index e0006abab2..d1ccbbf001 100644
--- a/packages/backend-core/src/tenancy/index.ts
+++ b/packages/backend-core/src/tenancy/index.ts
@@ -1,9 +1,11 @@
import * as context from "../context"
import * as tenancy from "./tenancy"
+import * as utils from "./utils"
const pkg = {
...context,
...tenancy,
+ ...utils,
}
export = pkg
diff --git a/packages/backend-core/src/tenancy/tenancy.ts b/packages/backend-core/src/tenancy/tenancy.ts
index 041f694d34..1c71935eb0 100644
--- a/packages/backend-core/src/tenancy/tenancy.ts
+++ b/packages/backend-core/src/tenancy/tenancy.ts
@@ -1,6 +1,7 @@
import { doWithDB } from "../db"
-import { StaticDatabases } from "../db/constants"
-import { baseGlobalDBName } from "./utils"
+import { queryPlatformView } from "../db/views"
+import { StaticDatabases, ViewName } from "../db/constants"
+import { getGlobalDBName } from "./utils"
import {
getTenantId,
DEFAULT_TENANT_ID,
@@ -8,6 +9,7 @@ import {
getTenantIDFromAppID,
} from "../context"
import env from "../environment"
+import { PlatformUser, PlatformUserByEmail } from "@budibase/types"
const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
@@ -87,15 +89,6 @@ export const tryAddTenant = async (
})
}
-export const getGlobalDBName = (tenantId?: string) => {
- // tenant ID can be set externally, for example user API where
- // new tenants are being created, this may be the case
- if (!tenantId) {
- tenantId = getTenantId()
- }
- return baseGlobalDBName(tenantId)
-}
-
export const doWithGlobalDB = (tenantId: string, cb: any) => {
return doWithDB(getGlobalDBName(tenantId), cb)
}
@@ -116,14 +109,16 @@ export const lookupTenantId = async (userId: string) => {
}
// lookup, could be email or userId, either will return a doc
-export const getTenantUser = async (identifier: string) => {
- return doWithDB(PLATFORM_INFO_DB, async (db: any) => {
- try {
- return await db.get(identifier)
- } catch (err) {
- return null
- }
- })
+export const getTenantUser = async (
+ identifier: string
+): Promise => {
+ // use the view here and allow to find anyone regardless of casing
+ // Use lowercase to ensure email login is case insensitive
+ const response = queryPlatformView(ViewName.PLATFORM_USERS_LOWERCASE, {
+ keys: [identifier.toLowerCase()],
+ include_docs: true,
+ }) as Promise
+ return response
}
export const isUserInAppTenant = (appId: string, user: any) => {
diff --git a/packages/backend-core/src/tenancy/utils.js b/packages/backend-core/src/tenancy/utils.js
deleted file mode 100644
index 70a965ddb7..0000000000
--- a/packages/backend-core/src/tenancy/utils.js
+++ /dev/null
@@ -1,12 +0,0 @@
-const { DEFAULT_TENANT_ID } = require("../constants")
-const { StaticDatabases, SEPARATOR } = require("../db/constants")
-
-exports.baseGlobalDBName = tenantId => {
- let dbName
- if (!tenantId || tenantId === DEFAULT_TENANT_ID) {
- dbName = StaticDatabases.GLOBAL.name
- } else {
- dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}`
- }
- return dbName
-}
diff --git a/packages/backend-core/src/tenancy/utils.ts b/packages/backend-core/src/tenancy/utils.ts
new file mode 100644
index 0000000000..f99f1e30af
--- /dev/null
+++ b/packages/backend-core/src/tenancy/utils.ts
@@ -0,0 +1,22 @@
+import { DEFAULT_TENANT_ID } from "../constants"
+import { StaticDatabases, SEPARATOR } from "../db/constants"
+import { getTenantId } from "../context"
+
+export const getGlobalDBName = (tenantId?: string) => {
+ // tenant ID can be set externally, for example user API where
+ // new tenants are being created, this may be the case
+ if (!tenantId) {
+ tenantId = getTenantId()
+ }
+ return baseGlobalDBName(tenantId)
+}
+
+export const baseGlobalDBName = (tenantId: string | undefined | null) => {
+ let dbName
+ if (!tenantId || tenantId === DEFAULT_TENANT_ID) {
+ dbName = StaticDatabases.GLOBAL.name
+ } else {
+ dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}`
+ }
+ return dbName
+}
diff --git a/packages/backend-core/src/users.js b/packages/backend-core/src/users.ts
similarity index 60%
rename from packages/backend-core/src/users.js
rename to packages/backend-core/src/users.ts
index 81bf28bb46..0793eeb1d9 100644
--- a/packages/backend-core/src/users.js
+++ b/packages/backend-core/src/users.ts
@@ -1,29 +1,39 @@
-const {
+import {
ViewName,
getUsersByAppParams,
getProdAppID,
generateAppUserID,
-} = require("./db/utils")
-const { queryGlobalView } = require("./db/views")
-const { UNICODE_MAX } = require("./db/constants")
+} from "./db/utils"
+import { queryGlobalView } from "./db/views"
+import { UNICODE_MAX } from "./db/constants"
+import { User } from "@budibase/types"
/**
* Given an email address this will use a view to search through
* all the users to find one with this email address.
* @param {string} email the email to lookup the user by.
*/
-exports.getGlobalUserByEmail = async email => {
+export const getGlobalUserByEmail = async (
+ email: String
+): Promise => {
if (email == null) {
throw "Must supply an email address to view"
}
- return await queryGlobalView(ViewName.USER_BY_EMAIL, {
+ const response = await queryGlobalView(ViewName.USER_BY_EMAIL, {
key: email.toLowerCase(),
include_docs: true,
})
+
+ if (Array.isArray(response)) {
+ // shouldn't be able to happen, but need to handle just in case
+ throw new Error(`Multiple users found with email address: ${email}`)
+ }
+
+ return response
}
-exports.searchGlobalUsersByApp = async (appId, opts) => {
+export const searchGlobalUsersByApp = async (appId: any, opts: any) => {
if (typeof appId !== "string") {
throw new Error("Must provide a string based app ID")
}
@@ -38,24 +48,24 @@ exports.searchGlobalUsersByApp = async (appId, opts) => {
return Array.isArray(response) ? response : [response]
}
-exports.getGlobalUserByAppPage = (appId, user) => {
+export const getGlobalUserByAppPage = (appId: string, user: User) => {
if (!user) {
return
}
- return generateAppUserID(getProdAppID(appId), user._id)
+ return generateAppUserID(getProdAppID(appId), user._id!)
}
/**
* Performs a starts with search on the global email view.
*/
-exports.searchGlobalUsersByEmail = async (email, opts) => {
+export const searchGlobalUsersByEmail = async (email: string, opts: any) => {
if (typeof email !== "string") {
throw new Error("Must provide a string to search by")
}
const lcEmail = email.toLowerCase()
// handle if passing up startkey for pagination
const startkey = opts && opts.startkey ? opts.startkey : lcEmail
- let response = await queryGlobalView(ViewName.USER_BY_EMAIL, {
+ let response = await queryGlobalView(ViewName.USER_BY_EMAIL, {
...opts,
startkey,
endkey: `${lcEmail}${UNICODE_MAX}`,
diff --git a/packages/backend-core/src/utils.js b/packages/backend-core/src/utils.js
index 0587267e9a..6b59c7cb72 100644
--- a/packages/backend-core/src/utils.js
+++ b/packages/backend-core/src/utils.js
@@ -42,6 +42,18 @@ async function resolveAppUrl(ctx) {
return app && app.appId ? app.appId : undefined
}
+exports.isServingApp = ctx => {
+ // dev app
+ if (ctx.path.startsWith(`/${APP_PREFIX}`)) {
+ return true
+ }
+ // prod app
+ if (ctx.path.startsWith(PROD_APP_PREFIX)) {
+ return true
+ }
+ return false
+}
+
/**
* Given a request tries to find the appId, which can be located in various places
* @param {object} ctx The main request body to look through.
diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock
index 22c17a9444..2e62aea734 100644
--- a/packages/backend-core/yarn.lock
+++ b/packages/backend-core/yarn.lock
@@ -1377,6 +1377,11 @@ bcrypt@5.0.1:
"@mapbox/node-pre-gyp" "^1.0.0"
node-addon-api "^3.1.0"
+bcryptjs@2.4.3:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
+ integrity sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==
+
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
diff --git a/packages/bbui/package.json b/packages/bbui/package.json
index da52ae0084..d19fdaecdd 100644
--- a/packages/bbui/package.json
+++ b/packages/bbui/package.json
@@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
- "version": "1.3.15-alpha.7",
+ "version": "1.3.19-alpha.6",
"license": "MPL-2.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",
@@ -38,7 +38,7 @@
],
"dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
- "@budibase/string-templates": "1.3.15-alpha.7",
+ "@budibase/string-templates": "1.3.19-alpha.6",
"@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2",
diff --git a/packages/bbui/src/Banner/BannerDisplay.svelte b/packages/bbui/src/Banner/BannerDisplay.svelte
index aad742b1bd..9ea2eaf2ec 100644
--- a/packages/bbui/src/Banner/BannerDisplay.svelte
+++ b/packages/bbui/src/Banner/BannerDisplay.svelte
@@ -4,22 +4,32 @@
import { banner } from "../Stores/banner"
import Banner from "./Banner.svelte"
import { fly } from "svelte/transition"
+ import TooltipWrapper from "../Tooltip/TooltipWrapper.svelte"
- {#if $banner.message}
+ {#each $banner.messages as message}
@@ -54,7 +55,6 @@
transform: scale(0.75);
}
.icon-small {
- margin-top: -2px;
- margin-bottom: -5px;
+ margin-bottom: -2px;
}
diff --git a/packages/bbui/src/index.js b/packages/bbui/src/index.js
index b45f3e9ed6..ead226bdc3 100644
--- a/packages/bbui/src/index.js
+++ b/packages/bbui/src/index.js
@@ -34,6 +34,7 @@ export { default as Layout } from "./Layout/Layout.svelte"
export { default as Page } from "./Layout/Page.svelte"
export { default as Link } from "./Link/Link.svelte"
export { default as Tooltip } from "./Tooltip/Tooltip.svelte"
+export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte"
export { default as Menu } from "./Menu/Menu.svelte"
export { default as MenuSection } from "./Menu/Section.svelte"
export { default as MenuSeparator } from "./Menu/Separator.svelte"
@@ -94,7 +95,7 @@ export { default as clickOutside } from "./Actions/click_outside"
// Stores
export { notifications, createNotificationStore } from "./Stores/notifications"
-export { banner } from "./Stores/banner"
+export { banner, BANNER_TYPES } from "./Stores/banner"
// Helpers
export * as Helpers from "./helpers"
diff --git a/packages/builder/cypress/integration/autoScreensUI.spec.js b/packages/builder/cypress/integration/autoScreensUI.spec.js
index 7a5dbef5a5..0253675c5b 100644
--- a/packages/builder/cypress/integration/autoScreensUI.spec.js
+++ b/packages/builder/cypress/integration/autoScreensUI.spec.js
@@ -82,10 +82,10 @@ filterTests(['smoke', 'all'], () => {
})
if (Cypress.env("TEST_ENV")) {
- it("should generate data source screens", () => {
- // Using MySQL data source for testing this
+ it("should generate datasource screens", () => {
+ // Using MySQL datasource for testing this
const datasource = "MySQL"
- // Select & configure MySQL data source
+ // Select & configure MySQL datasource
cy.selectExternalDatasource(datasource)
cy.addDatasourceConfig(datasource)
// Create Autogenerated screens from a MySQL table - MySQL contains books table
diff --git a/packages/builder/cypress/integration/datasources/mySql.spec.js b/packages/builder/cypress/integration/datasources/mySql.spec.js
index 654705a24e..33aa72f0bb 100644
--- a/packages/builder/cypress/integration/datasources/mySql.spec.js
+++ b/packages/builder/cypress/integration/datasources/mySql.spec.js
@@ -11,8 +11,8 @@ filterTests(["all"], () => {
const queryName = "Cypress Test Query"
const queryRename = "CT Query Rename"
- it("Should add MySQL data source without configuration", () => {
- // Select MySQL data source
+ it("Should add MySQL datasource without configuration", () => {
+ // Select MySQL datasource
cy.selectExternalDatasource(datasource)
// Attempt to fetch tables without applying configuration
cy.intercept("**/datasources").as("datasource")
@@ -35,8 +35,8 @@ filterTests(["all"], () => {
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
})
- it("should add MySQL data source and fetch tables", () => {
- // Add & configure MySQL data source
+ it("should add MySQL datasource and fetch tables", () => {
+ // Add & configure MySQL datasource
cy.selectExternalDatasource(datasource)
cy.intercept("**/datasources").as("datasource")
cy.addDatasourceConfig(datasource)
@@ -52,7 +52,7 @@ filterTests(["all"], () => {
})
it("should check table fetching error", () => {
- // MySQL test data source contains tables without primary keys
+ // MySQL test datasource contains tables without primary keys
cy.get(".spectrum-InLineAlert")
.should("contain", "Error fetching tables")
.and("contain", "No primary key constraint found")
diff --git a/packages/builder/cypress/integration/datasources/oracle.spec.js b/packages/builder/cypress/integration/datasources/oracle.spec.js
index 5d92d6b217..ae1ca5cd75 100644
--- a/packages/builder/cypress/integration/datasources/oracle.spec.js
+++ b/packages/builder/cypress/integration/datasources/oracle.spec.js
@@ -11,8 +11,8 @@ filterTests(["all"], () => {
const queryName = "Cypress Test Query"
const queryRename = "CT Query Rename"
- it("Should add Oracle data source and skip table fetch", () => {
- // Select Oracle data source
+ it("Should add Oracle datasource and skip table fetch", () => {
+ // Select Oracle datasource
cy.selectExternalDatasource(datasource)
// Skip table fetch - no config added
cy.get(".spectrum-Button")
@@ -23,7 +23,7 @@ filterTests(["all"], () => {
cy.get(".spectrum-Textfield-input", { timeout: 500 })
.eq(1)
.should("have.value", "localhost")
- // Add another Oracle data source, configure & skip table fetch
+ // Add another Oracle datasource, configure & skip table fetch
cy.selectExternalDatasource(datasource)
cy.addDatasourceConfig(datasource, true)
// Confirm config and no tables
@@ -33,8 +33,8 @@ filterTests(["all"], () => {
cy.get(".spectrum-Body").eq(2).should("contain", "No tables found.")
})
- it("Should add Oracle data source and fetch tables without configuration", () => {
- // Select Oracle data source
+ it("Should add Oracle datasource and fetch tables without configuration", () => {
+ // Select Oracle datasource
cy.selectExternalDatasource(datasource)
// Attempt to fetch tables without applying configuration
cy.intercept("**/datasources").as("datasource")
@@ -49,8 +49,8 @@ filterTests(["all"], () => {
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
})
- xit("should add Oracle data source and fetch tables", () => {
- // Add & configure Oracle data source
+ xit("should add Oracle datasource and fetch tables", () => {
+ // Add & configure Oracle datasource
cy.selectExternalDatasource(datasource)
cy.intercept("**/datasources").as("datasource")
cy.addDatasourceConfig(datasource)
diff --git a/packages/builder/cypress/integration/datasources/postgreSql.spec.js b/packages/builder/cypress/integration/datasources/postgreSql.spec.js
index 622c3ade73..8ef574566e 100644
--- a/packages/builder/cypress/integration/datasources/postgreSql.spec.js
+++ b/packages/builder/cypress/integration/datasources/postgreSql.spec.js
@@ -11,8 +11,8 @@ filterTests(["all"], () => {
const queryName = "Cypress Test Query"
const queryRename = "CT Query Rename"
- xit("Should add PostgreSQL data source without configuration", () => {
- // Select PostgreSQL data source
+ xit("Should add PostgreSQL datasource without configuration", () => {
+ // Select PostgreSQL datasource
cy.selectExternalDatasource(datasource)
// Attempt to fetch tables without applying configuration
cy.intercept("**/datasources").as("datasource")
@@ -27,8 +27,8 @@ filterTests(["all"], () => {
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true })
})
- it("should add PostgreSQL data source and fetch tables", () => {
- // Add & configure PostgreSQL data source
+ it("should add PostgreSQL datasource and fetch tables", () => {
+ // Add & configure PostgreSQL datasource
cy.selectExternalDatasource(datasource)
cy.intercept("**/datasources").as("datasource")
cy.addDatasourceConfig(datasource)
diff --git a/packages/builder/cypress/integration/datasources/rest.spec.js b/packages/builder/cypress/integration/datasources/rest.spec.js
index 7a145049e2..ec9864a47d 100644
--- a/packages/builder/cypress/integration/datasources/rest.spec.js
+++ b/packages/builder/cypress/integration/datasources/rest.spec.js
@@ -10,8 +10,8 @@ filterTests(["smoke", "all"], () => {
const datasource = "REST"
const restUrl = "https://api.openbrewerydb.org/breweries"
- it("Should add REST data source with incorrect API", () => {
- // Select REST data source
+ it("Should add REST datasource with incorrect API", () => {
+ // Select REST datasource
cy.selectExternalDatasource(datasource)
// Enter incorrect api & attempt to send query
cy.get(".query-buttons", { timeout: 1000 }).contains("Add query").click({ force: true })
diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js
index a07a22188f..53accfbbe4 100644
--- a/packages/builder/cypress/support/commands.js
+++ b/packages/builder/cypress/support/commands.js
@@ -763,7 +763,7 @@ Cypress.Commands.add("navigateToDataSection", () => {
})
Cypress.Commands.add("navigateToAutogeneratedModal", () => {
- // Screen name must already exist within data source
+ // Screen name must already exist within datasource
cy.contains("Design").click()
cy.get(".spectrum-Button").contains("Add screen").click({ force: true })
cy.get(".spectrum-Modal").within(() => {
@@ -779,7 +779,7 @@ Cypress.Commands.add("navigateToAutogeneratedModal", () => {
Cypress.Commands.add("selectExternalDatasource", datasourceName => {
// Navigates to Data Section
cy.navigateToDataSection()
- // Open Data Source modal
+ // Open Datasource modal
cy.get(".nav").within(() => {
cy.get(".add-button").click()
})
diff --git a/packages/builder/package.json b/packages/builder/package.json
index 33ad9c46a8..bd7cb3e881 100644
--- a/packages/builder/package.json
+++ b/packages/builder/package.json
@@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
- "version": "1.3.15-alpha.7",
+ "version": "1.3.19-alpha.6",
"license": "GPL-3.0",
"private": true,
"scripts": {
@@ -69,10 +69,10 @@
}
},
"dependencies": {
- "@budibase/bbui": "1.3.15-alpha.7",
- "@budibase/client": "1.3.15-alpha.7",
- "@budibase/frontend-core": "1.3.15-alpha.7",
- "@budibase/string-templates": "1.3.15-alpha.7",
+ "@budibase/bbui": "1.3.19-alpha.6",
+ "@budibase/client": "1.3.19-alpha.6",
+ "@budibase/frontend-core": "1.3.19-alpha.6",
+ "@budibase/string-templates": "1.3.19-alpha.6",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",
diff --git a/packages/builder/src/App.svelte b/packages/builder/src/App.svelte
index 0fb0fe59d5..4d193df104 100644
--- a/packages/builder/src/App.svelte
+++ b/packages/builder/src/App.svelte
@@ -4,6 +4,7 @@
import { NotificationDisplay, BannerDisplay } from "@budibase/bbui"
import { parse, stringify } from "qs"
import HelpIcon from "components/common/HelpIcon.svelte"
+ import LicensingOverlays from "components/portal/licensing/LicensingOverlays.svelte"
const queryHandler = { parse, stringify }
@@ -12,6 +13,9 @@
+
+
+
diff --git a/packages/builder/src/builderStore/datasource.js b/packages/builder/src/builderStore/datasource.js
index 84edcdd6ad..e12b318e1c 100644
--- a/packages/builder/src/builderStore/datasource.js
+++ b/packages/builder/src/builderStore/datasource.js
@@ -27,7 +27,7 @@ export async function saveDatasource(config, skipFetch = false) {
// Create datasource
const resp = await datasources.save(datasource, !skipFetch && datasource.plus)
- // update the tables incase data source plus
+ // update the tables incase datasource plus
await tables.fetch()
await datasources.select(resp._id)
return resp
diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js
index 35c4587874..69bca7eac3 100644
--- a/packages/builder/src/builderStore/index.js
+++ b/packages/builder/src/builderStore/index.js
@@ -1,5 +1,6 @@
import { getFrontendStore } from "./store/frontend"
import { getAutomationStore } from "./store/automation"
+import { getTemporalStore } from "./store/temporal"
import { getThemeStore } from "./store/theme"
import { derived } from "svelte/store"
import { findComponent, findComponentPath } from "./componentUtils"
@@ -8,6 +9,7 @@ import { RoleUtils } from "@budibase/frontend-core"
export const store = getFrontendStore()
export const automationStore = getAutomationStore()
export const themeStore = getThemeStore()
+export const temporalStore = getTemporalStore()
export const selectedScreen = derived(store, $store => {
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
diff --git a/packages/builder/src/builderStore/store/temporal.js b/packages/builder/src/builderStore/store/temporal.js
new file mode 100644
index 0000000000..ca70fd6293
--- /dev/null
+++ b/packages/builder/src/builderStore/store/temporal.js
@@ -0,0 +1,43 @@
+import { createLocalStorageStore } from "@budibase/frontend-core"
+import { get } from "svelte/store"
+
+export const getTemporalStore = () => {
+ const initialValue = {}
+
+ const localStorageKey = `bb-temporal`
+ const store = createLocalStorageStore(localStorageKey, initialValue)
+
+ const setExpiring = (key, data, duration) => {
+ const updated = {
+ ...data,
+ expiry: Date.now() + duration * 1000,
+ }
+
+ store.update(state => ({
+ ...state,
+ [key]: updated,
+ }))
+ }
+
+ const getExpiring = key => {
+ const entry = get(store)[key]
+ if (!entry) {
+ return
+ }
+ const currentExpiry = entry.expiry
+ if (currentExpiry < Date.now()) {
+ store.update(state => {
+ delete state[key]
+ return state
+ })
+ return null
+ } else {
+ return entry
+ }
+ }
+
+ return {
+ subscribe: store.subscribe,
+ actions: { setExpiring, getExpiring },
+ }
+}
diff --git a/packages/builder/src/components/automation/SetupPanel/QueryParamSelector.svelte b/packages/builder/src/components/automation/SetupPanel/QueryParamSelector.svelte
index 0b6a48ff9d..9c47178b0e 100644
--- a/packages/builder/src/components/automation/SetupPanel/QueryParamSelector.svelte
+++ b/packages/builder/src/components/automation/SetupPanel/QueryParamSelector.svelte
@@ -50,7 +50,6 @@
type="string"
{bindings}
fillWidth={true}
- allowJS={false}
/>
{/each}
diff --git a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte
index 6235e52916..19946a2386 100644
--- a/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte
+++ b/packages/builder/src/components/backend/DatasourceNavigator/DatasourceNavigator.svelte
@@ -32,8 +32,8 @@
: []
$: openDataSource = enrichedDataSources.find(x => x.open)
$: {
- // Ensure the open data source is always included in the list of open
- // data sources
+ // Ensure the open datasource is always included in the list of open
+ // datasources
if (openDataSource) {
openNode(openDataSource)
}
@@ -79,7 +79,7 @@
})
const containsActiveEntity = datasource => {
- // If we're view a query then the data source ID is in the URL
+ // If we're view a query then the datasource ID is in the URL
if ($params.selectedDatasource === datasource._id) {
return true
}
diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte
index 1c9d3c76a8..40ef294339 100644
--- a/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte
+++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte
@@ -126,7 +126,7 @@
- Connect to an external data source
+ Connect to an external datasource
{#each Object.entries(integrations).filter(([key, val]) => key !== IntegrationTypes.INTERNAL && !val.custom) as [integrationType, schema]}
0}
- Custom data source
+ Custom datasource