diff --git a/hosting/nginx.prod.conf.hbs b/hosting/nginx.prod.conf.hbs index 15d7c6823e..6f0f1b420d 100644 --- a/hosting/nginx.prod.conf.hbs +++ b/hosting/nginx.prod.conf.hbs @@ -124,12 +124,21 @@ http { } location /api/backups/ { + # calls to export apps are limited + limit_req zone=ratelimit burst=20 nodelay; + + # 1800s timeout for app export requests proxy_read_timeout 1800s; proxy_connect_timeout 1800s; proxy_send_timeout 1800s; - proxy_pass http://app-service; + proxy_http_version 1.1; - proxy_set_header Connection ""; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_pass http://$apps:4002; } location /api/ { diff --git a/hosting/single/nginx/nginx-default-site.conf b/hosting/single/nginx/nginx-default-site.conf index bd89e21251..9a5ec91c1f 100644 --- a/hosting/single/nginx/nginx-default-site.conf +++ b/hosting/single/nginx/nginx-default-site.conf @@ -43,6 +43,24 @@ server { rewrite ^/worker/(.*)$ /$1 break; } + location /api/backups/ { + # calls to export apps are limited + limit_req zone=ratelimit burst=20 nodelay; + + # 1800s timeout for app export requests + proxy_read_timeout 1800s; + proxy_connect_timeout 1800s; + proxy_send_timeout 1800s; + + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_pass http://127.0.0.1:4001; + } + location /api/ { # calls to the API are rate limited with bursting limit_req zone=ratelimit burst=20 nodelay; diff --git a/hosting/single/runner.sh b/hosting/single/runner.sh index a95c21a98f..ea825131db 100644 --- a/hosting/single/runner.sh +++ b/hosting/single/runner.sh @@ -27,12 +27,14 @@ if [[ "${TARGETBUILD}" = "aas" ]]; then else DATA_DIR=${DATA_DIR:-/data} fi - +mkdir -p ${DATA_DIR} # Mount NFS or GCP Filestore if env vars exist for it -if [[ -z ${FILESHARE_IP} && -z ${FILESHARE_NAME} ]]; then +if [[ ! -z ${FILESHARE_IP} && ! -z ${FILESHARE_NAME} ]]; then + echo "Mounting NFS share" + apt update && apt install -y nfs-common nfs-kernel-server echo "Mount file share ${FILESHARE_IP}:/${FILESHARE_NAME} to ${DATA_DIR}" mount -o nolock ${FILESHARE_IP}:/${FILESHARE_NAME} ${DATA_DIR} - echo "Mounting completed." + echo "Mounting result: $?" fi if [ -f "${DATA_DIR}/.env" ]; then @@ -74,9 +76,9 @@ mkdir -p ${DATA_DIR}/couch/{dbs,views} mkdir -p ${DATA_DIR}/minio mkdir -p ${DATA_DIR}/search chown -R couchdb:couchdb ${DATA_DIR}/couch -redis-server --requirepass $REDIS_PASSWORD & -/opt/clouseau/bin/clouseau & -/minio/minio server ${DATA_DIR}/minio & +redis-server --requirepass $REDIS_PASSWORD > /dev/stdout 2>&1 & +/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 & +/minio/minio server ${DATA_DIR}/minio > /dev/stdout 2>&1 & /docker-entrypoint.sh /opt/couchdb/bin/couchdb & /etc/init.d/nginx restart if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then @@ -85,16 +87,18 @@ if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then chmod +x /etc/cron.d/certificate-renew # Request the certbot certificate /app/letsencrypt/certificate-request.sh ${CUSTOM_DOMAIN} + /etc/init.d/nginx restart fi -/etc/init.d/nginx restart pushd app -pm2 start --name app "yarn run:docker" +pm2 start -l /dev/stdout --name app "yarn run:docker" popd pushd worker -pm2 start --name worker "yarn run:docker" +pm2 start -l /dev/stdout --name worker "yarn run:docker" popd sleep 10 +echo "curl to couchdb endpoints" curl -X PUT ${COUCH_DB_URL}/_users curl -X PUT ${COUCH_DB_URL}/_replicator +echo "end of runner.sh, sleeping ..." sleep infinity diff --git a/lerna.json b/lerna.json index 5988f03a85..bba5d0d806 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.1.22-alpha.9", + "version": "2.1.31", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 252c7b628e..ed19230933 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.1.22-alpha.9", + "version": "2.1.31", "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": "2.1.22-alpha.9", + "@budibase/types": "^2.1.31", "@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/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index d4b7918ff1..de06b4e8ee 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -6,12 +6,15 @@ import { DatabaseOpts, DatabaseQueryOpts, DatabasePutOpts, + DatabaseCreateIndexOpts, + DatabaseDeleteIndexOpts, Document, isDocument, } from "@budibase/types" import { getCouchInfo } from "./connections" import { directCouchCall } from "./utils" import { getPouchDB } from "./pouchDB" +import { WriteStream, ReadStream } from "fs" export class DatabaseImpl implements Database { public readonly name: string @@ -159,34 +162,32 @@ export class DatabaseImpl implements Database { return this.updateOutput(() => db.compact()) } - private doWithPouchDB(func: string) { - const dbName = this.name - return async (args: any[]) => { - const pouch = getPouchDB(dbName) - // @ts-ignore - return pouch[func](...args) - } - } - // All below functions are in-frequently called, just utilise PouchDB // for them as it implements them better than we can - async dump(...args: any[]) { - return this.doWithPouchDB("dump")(args) + async dump(stream: WriteStream, opts?: { filter?: any }) { + const pouch = getPouchDB(this.name) + // @ts-ignore + return pouch.dump(stream, opts) } - async load(...args: any[]) { - return this.doWithPouchDB("load")(args) + async load(stream: ReadStream) { + const pouch = getPouchDB(this.name) + // @ts-ignore + return pouch.load(stream) } - async createIndex(...args: any[]) { - return this.doWithPouchDB("createIndex")(args) + async createIndex(opts: DatabaseCreateIndexOpts) { + const pouch = getPouchDB(this.name) + return pouch.createIndex(opts) } - async deleteIndex(...args: any[]) { - return this.doWithPouchDB("createIndex")(args) + async deleteIndex(opts: DatabaseDeleteIndexOpts) { + const pouch = getPouchDB(this.name) + return pouch.deleteIndex(opts) } - async getIndexes(...args: any[]) { - return this.doWithPouchDB("createIndex")(args) + async getIndexes() { + const pouch = getPouchDB(this.name) + return pouch.getIndexes() } } diff --git a/packages/backend-core/src/events/publishers/backup.ts b/packages/backend-core/src/events/publishers/backup.ts index 0fc81da259..4a68364016 100644 --- a/packages/backend-core/src/events/publishers/backup.ts +++ b/packages/backend-core/src/events/publishers/backup.ts @@ -1,12 +1,34 @@ -import { AppBackup, AppBackupRestoreEvent, Event } from "@budibase/types" +import { + AppBackup, + AppBackupRestoreEvent, + AppBackupTriggeredEvent, + AppBackupTrigger, + AppBackupType, + Event, +} from "@budibase/types" import { publishEvent } from "../events" export async function appBackupRestored(backup: AppBackup) { const properties: AppBackupRestoreEvent = { appId: backup.appId, - backupName: backup.name!, + restoreId: backup._id!, backupCreatedAt: backup.timestamp, } await publishEvent(Event.APP_BACKUP_RESTORED, properties) } + +export async function appBackupTriggered( + appId: string, + backupId: string, + type: AppBackupType, + trigger: AppBackupTrigger +) { + const properties: AppBackupTriggeredEvent = { + appId: appId, + backupId, + type, + trigger, + } + await publishEvent(Event.APP_BACKUP_TRIGGERED, properties) +} diff --git a/packages/backend-core/src/plugin/utils.js b/packages/backend-core/src/plugin/utils.js index 60a40f3a76..b943747483 100644 --- a/packages/backend-core/src/plugin/utils.js +++ b/packages/backend-core/src/plugin/utils.js @@ -51,6 +51,7 @@ function validateDatasource(schema) { const queryValidator = joi .object({ type: joi.string().allow(...Object.values(QueryType)), + readable: joi.boolean(), fields: joi.object().pattern(joi.string(), fieldValidator), }) .required() diff --git a/packages/bbui/package.json b/packages/bbui/package.json index c398e9bf1e..b01cd7106b 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": "2.1.22-alpha.9", + "version": "2.1.31", "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": "2.1.22-alpha.9", + "@budibase/string-templates": "^2.1.31", "@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", @@ -85,5 +85,8 @@ "svelte-flatpickr": "^3.2.3", "svelte-portal": "^1.0.0" }, + "resolutions": { + "loader-utils": "1.4.1" + }, "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" } diff --git a/packages/builder/package.json b/packages/builder/package.json index c9992b0579..cd8f82cb1f 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.1.22-alpha.9", + "version": "2.1.31", "license": "GPL-3.0", "private": true, "scripts": { @@ -71,10 +71,10 @@ } }, "dependencies": { - "@budibase/bbui": "2.1.22-alpha.9", - "@budibase/client": "2.1.22-alpha.9", - "@budibase/frontend-core": "2.1.22-alpha.9", - "@budibase/string-templates": "2.1.22-alpha.9", + "@budibase/bbui": "^2.1.31", + "@budibase/client": "^2.1.31", + "@budibase/frontend-core": "^2.1.31", + "@budibase/string-templates": "^2.1.31", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index dce6df6d0d..960283842d 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -304,6 +304,8 @@ const newError = {} if (!external && fieldInfo.name?.startsWith("_")) { newError.name = `Column name cannot start with an underscore.` + } else if (fieldInfo.name && !fieldInfo.name.match(/^[_a-zA-Z0-9\s]*$/g)) { + newError.name = `Illegal character; must be alpha-numeric.` } else if (PROHIBITED_COLUMN_NAMES.some(name => fieldInfo.name === name)) { newError.name = `${PROHIBITED_COLUMN_NAMES.join( ", " diff --git a/packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte b/packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte index a9eff7f957..b125e18b31 100644 --- a/packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte @@ -97,7 +97,7 @@ } function fieldOptions(field) { - return schema[field]?.type === "options" + return schema[field]?.type === "options" || schema[field]?.type === "array" ? schema[field]?.constraints.inclusion : [true, false] } diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte index ef7c81233b..1d18fa3a92 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte @@ -23,14 +23,18 @@ export let bindings = [] export let nested - $: showAvailableActions = !actions?.length - let actionQuery - $: parsedQuery = - typeof actionQuery === "string" ? actionQuery.toLowerCase().trim() : "" - let selectedAction = actions?.length ? actions[0] : null + $: { + // Ensure parameters object is never null + if (selectedAction && !selectedAction.parameters) { + selectedAction.parameters = {} + } + } + $: parsedQuery = + typeof actionQuery === "string" ? actionQuery.toLowerCase().trim() : "" + $: showAvailableActions = !actions?.length $: mappedActionTypes = actionTypes.reduce((acc, action) => { let parsedName = action.name.toLowerCase().trim() if (parsedQuery.length && parsedName.indexOf(parsedQuery) < 0) { @@ -40,7 +44,6 @@ acc[action.type].push(action) return acc }, {}) - // These are ephemeral bindings which only exist while executing actions $: eventContexBindings = getEventContextBindings( $currentAsset, @@ -50,9 +53,8 @@ selectedAction?.id ) $: allBindings = eventContexBindings.concat(bindings) - - // Assign a unique ID to each action $: { + // Ensure each action has a unique ID if (actions) { actions.forEach(action => { if (!action.id) { @@ -61,13 +63,11 @@ }) } } - $: selectedActionComponent = selectedAction && actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_KEY])?.component - - // Select the first action if we delete an action $: { + // Select the first action if we delete an action if (selectedAction && !actions?.includes(selectedAction)) { selectedAction = actions?.[0] } diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/CloseScreenModal.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/CloseScreenModal.svelte index 5f3b3ef639..12d4bad05a 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/CloseScreenModal.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/CloseScreenModal.svelte @@ -1,14 +1,16 @@ -Navigate To screen, or leave blank. -
+ + You can optionally navigate to another screen after closing the screen + modal. +