1
0
Fork 0
mirror of synced 2024-07-30 10:36:28 +12:00

Merge remote-tracking branch 'origin/develop' into feature/app-settings-section

This commit is contained in:
Dean 2023-06-08 21:26:03 +01:00
commit d80f53e37a
30 changed files with 541 additions and 378 deletions

View file

@ -1,45 +1,53 @@
name: Budibase Deploy Production
on:
workflow_dispatch:
workflow_dispatch:
inputs:
version:
description: Budibase release version. For example - 1.0.0
required: false
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Fail if not a tag
run: |
if [[ $GITHUB_REF != refs/tags/* ]]; then
echo "Workflow Dispatch can only be run on tags"
exit 1
fi
# - name: Fail if not a tag
# run: |
# if [[ $GITHUB_REF != refs/tags/* ]]; then
# echo "Workflow Dispatch can only be run on tags"
# exit 1
# fi
- uses: actions/checkout@v2
with:
fetch-depth: 0
# with:
# fetch-depth: 0
- name: Fail if tag is not in master
run: |
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
exit 1
fi
# - name: Fail if tag is not in master
# run: |
# if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
# echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
# exit 1
# fi
- name: Pull values.yaml from budibase-infra
run: |
run: |
curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \
-H 'Accept: application/vnd.github.v3.raw' \
-o values.production.yaml \
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.yaml
wc -l values.production.yaml
- name: Get the latest budibase release version
id: version
run: |
release_version=$(cat lerna.json | jq -r '.version')
run: |
if [ -z "${{ github.event.inputs.version }}" ]; then
release_version=$(cat lerna.json | jq -r '.version')
else
release_version=${{ github.event.inputs.version }}
fi
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
@ -66,4 +74,3 @@ jobs:
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
content: "Production Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Cloud."
embed-title: ${{ env.RELEASE_VERSION }}

View file

@ -48,19 +48,8 @@ jobs:
- run: yarn install --frozen-lockfile
- name: Update versions
run: |
version=$(cat lerna.json \
| grep version \
| head -1 \
| awk -F: '{gsub(/"/,"",$2);gsub(/[[:space:]]*/,"",$2); print $2}' \
| sed 's/[",]//g')
echo "Setting version $version"
yarn lerna exec "yarn version --no-git-tag-version --new-version=$version"
echo "Updating dependencies"
node scripts/syncLocalDependencies.js $version
echo "Syncing yarn workspace"
yarn
- run: yarn build --configuration=production
run: ./scripts/updateVersions.sh
- run: yarn build
- run: yarn build:sdk
- name: Publish budibase packages to NPM

View file

@ -41,20 +41,9 @@ jobs:
- run: yarn install --frozen-lockfile
- name: Update versions
run: |
version=$(cat lerna.json \
| grep version \
| head -1 \
| awk -F: '{gsub(/"/,"",$2);gsub(/[[:space:]]*/,"",$2); print $2}' \
| sed 's/[",]//g')
echo "Setting version $version"
yarn lerna exec "yarn version --no-git-tag-version --new-version=$version"
echo "Updating dependencies"
node scripts/syncLocalDependencies.js $version
echo "Syncing yarn workspace"
yarn
run: ./scripts/updateVersions.sh
- run: yarn lint
- run: yarn build --configuration=production
- run: yarn build
- run: yarn build:sdk
- name: Publish budibase packages to NPM

View file

@ -1,7 +1,7 @@
name: Budibase Release Selfhost
on:
workflow_dispatch:
workflow_dispatch:
jobs:
release:
@ -16,8 +16,10 @@ jobs:
fi
- uses: actions/checkout@v2
with:
fetch_depth: 0
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
fetch-depth: 0
- name: Fail if tag is not in master
run: |
@ -38,7 +40,7 @@ jobs:
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Tag and release Docker images (Self Host)
run: |
run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
release_tag=v${{ env.RELEASE_VERSION }}
@ -52,7 +54,7 @@ jobs:
docker tag budibase/apps:$release_tag budibase/apps:$SELFHOST_TAG
docker tag budibase/worker:$release_tag budibase/worker:$SELFHOST_TAG
docker tag budibase/proxy:$release_tag budibase/proxy:$SELFHOST_TAG
# Push images
docker push budibase/apps:$SELFHOST_TAG
docker push budibase/worker:$SELFHOST_TAG
@ -74,15 +76,15 @@ jobs:
yarn
yarn specs
popd
- name: Setup Helm
- name: Setup Helm
uses: azure/setup-helm@v1
id: helm-install
# due to helm repo index issue: https://github.com/helm/helm/issues/7363
# we need to create new package in a different dir, merge the index and move the package back
- name: Build and release helm chart
run: |
run: |
git config user.name "Budibase Helm Bot"
git config user.email "<>"
git reset --hard

View file

@ -5,7 +5,7 @@ on:
env:
CI: true
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
REGISTRY_URL: registry.hub.docker.com
jobs:
build:
@ -24,6 +24,8 @@ jobs:
- name: "Checkout"
uses: actions/checkout@v2
with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
fetch-depth: 0
- name: Fail if tag is not in master
@ -32,7 +34,7 @@ jobs:
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
exit 1
fi
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
@ -44,10 +46,12 @@ jobs:
uses: docker/setup-buildx-action@v1
- name: Run Yarn
run: yarn
- name: Run Yarn Bootstrap
run: yarn bootstrap
- name: Update versions
run: ./scripts/updateVersions.sh
- name: Runt Yarn Lint
run: yarn lint
- name: Update versions
run: ./scripts/updateVersions.sh
- name: Run Yarn Build
run: yarn build:docker:pre
- name: Login to Docker Hub

View file

@ -1,4 +1,4 @@
FROM node:16-slim as build
FROM node:14-slim as build
# install node-gyp dependencies
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python
@ -11,12 +11,16 @@ RUN chmod +x /cleanup.sh
# build server
WORKDIR /app
ADD packages/server .
RUN yarn install --frozen-lockfile --production=true && /cleanup.sh
COPY yarn.lock .
RUN yarn install --production=true
RUN /cleanup.sh
# build worker
WORKDIR /worker
ADD packages/worker .
RUN yarn install --frozen-lockfile --production=true && /cleanup.sh
COPY yarn.lock .
RUN yarn install --production=true
RUN /cleanup.sh
FROM budibase/couchdb
ARG TARGETARCH

View file

@ -17,6 +17,7 @@ declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONME
[[ -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
[[ -z "${SERVER_TOP_LEVEL_PATH}" ]] && export SERVER_TOP_LEVEL_PATH=/app
# export CUSTOM_DOMAIN=budi001.custom.com
# Azure App Service customisations

View file

@ -1,5 +1,5 @@
{
"version": "2.6.28-alpha.0",
"version": "2.7.7-alpha.2",
"npmClient": "yarn",
"packages": [
"packages/backend-core",

View file

@ -52,7 +52,7 @@
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --scope @budibase/worker --scope @budibase/server",
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
"dev:docker": "yarn build && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
"dev:docker": "yarn build:docker:pre && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
"test": "lerna run --stream test --stream",
"lint:eslint": "eslint packages && eslint qa-core",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
@ -62,7 +62,7 @@
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"build:specs": "lerna run --stream specs",
"build:docker": "lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
"build:docker:pre": "lerna run --stream build && lerna run --stream predocker",
"build:docker:pre": "yarn build && lerna run --stream predocker",
"build:docker:proxy": "docker build hosting/proxy -t proxy-service",
"build:docker:selfhost": "lerna run --stream build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -",
"build:docker:develop": "node scripts/pinVersions && lerna run --stream build:docker && yarn build:docker:proxy && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",

View file

@ -1,10 +1,11 @@
import * as google from "../sso/google"
import { Cookie } from "../../../constants"
import { clearCookie, getCookie } from "../../../utils"
import * as configs from "../../../configs"
import { BBContext, SSOProfile } from "@budibase/types"
import * as cache from "../../../cache"
import * as utils from "../../../utils"
import { UserCtx, SSOProfile } from "@budibase/types"
import { ssoSaveUserNoOp } from "../sso/sso"
import { cache, utils } from "../../../"
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
type Passport = {
@ -22,7 +23,7 @@ async function fetchGoogleCreds() {
export async function preAuth(
passport: Passport,
ctx: BBContext,
ctx: UserCtx,
next: Function
) {
// get the relevant config
@ -49,7 +50,7 @@ export async function preAuth(
export async function postAuth(
passport: Passport,
ctx: BBContext,
ctx: UserCtx,
next: Function
) {
// get the relevant config
@ -57,7 +58,7 @@ export async function postAuth(
const platformUrl = await configs.getPlatformUrl({ tenantAware: false })
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback`
const authStateCookie = getCookie(ctx, Cookie.DatasourceAuth)
const authStateCookie = utils.getCookie(ctx, Cookie.DatasourceAuth)
return passport.authenticate(
new GoogleStrategy(
@ -72,7 +73,7 @@ export async function postAuth(
_profile: SSOProfile,
done: Function
) => {
clearCookie(ctx, Cookie.DatasourceAuth)
utils.clearCookie(ctx, Cookie.DatasourceAuth)
done(null, { accessToken, refreshToken })
}
),

View file

@ -13,6 +13,8 @@
Modal,
notifications,
Icon,
Checkbox,
DatePicker,
} from "@budibase/bbui"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import { automationStore, selectedAutomation } from "builderStore"
@ -306,6 +308,11 @@
drawer.hide()
}
function canShowField(key, value) {
const dependsOn = value.dependsOn
return !dependsOn || !!inputData[dependsOn]
}
onMount(async () => {
try {
await environment.loadVariables()
@ -317,210 +324,233 @@
<div class="fields">
{#each deprecatedSchemaProperties as [key, value]}
<div class="block-field">
{#if key !== "fields"}
<Label
tooltip={value.title === "Binding / Value"
? "If using the String input type, please use a comma or newline separated string"
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
>
{/if}
{#if value.type === "string" && value.enum}
<Select
on:change={e => onChange(e, key)}
value={inputData[key]}
placeholder={false}
options={value.enum}
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
/>
{:else if value.type === "json"}
<Editor
editorHeight="250"
editorWidth="448"
mode="json"
value={inputData[key]?.value}
on:change={e => {
/**
* TODO - Remove after November 2023
* *******************************
* Code added to provide backwards compatibility between Values 1,2,3,4,5
* and the new JSON body.
*/
delete inputData.value1
delete inputData.value2
delete inputData.value3
delete inputData.value4
delete inputData.value5
/***********************/
onChange(e, key)
}}
/>
{:else if value.customType === "column"}
<Select
on:change={e => onChange(e, key)}
value={inputData[key]}
options={Object.keys(table?.schema || {})}
/>
{:else if value.customType === "filters"}
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
<Drawer bind:this={drawer} {fillWidth} title="Filtering">
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
Save
</Button>
<FilterDrawer
slot="body"
{filters}
{bindings}
{schemaFields}
datasource={{ type: "table", tableId }}
panel={AutomationBindingPanel}
fillWidth
on:change={e => (tempFilters = e.detail)}
/>
</Drawer>
{:else if value.customType === "password"}
<Input
type="password"
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "email"}
{#if isTestModal}
<ModalBindableInput
title={value.title}
value={inputData[key]}
panel={AutomationBindingPanel}
type="email"
on:change={e => onChange(e, key)}
{bindings}
fillWidth
updateOnChange={false}
/>
{:else}
<DrawerBindableInput
fillWidth
title={value.title}
panel={AutomationBindingPanel}
type="email"
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
allowJS={false}
updateOnChange={false}
drawerLeft="260px"
/>
{#if canShowField(key, value)}
<div class="block-field">
{#if key !== "fields" && value.type !== "boolean"}
<Label
tooltip={value.title === "Binding / Value"
? "If using the String input type, please use a comma or newline separated string"
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
>
{/if}
{:else if value.customType === "query"}
<QuerySelector
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "cron"}
<CronBuilder on:change={e => onChange(e, key)} value={inputData[key]} />
{:else if value.customType === "queryParams"}
<QueryParamSelector
on:change={e => onChange(e, key)}
value={inputData[key]}
{bindings}
/>
{:else if value.customType === "table"}
<TableSelector
{isTrigger}
value={inputData[key]}
on:change={e => onChange(e, key)}
/>
{:else if value.customType === "row"}
<RowSelector
{block}
value={inputData[key]}
meta={inputData["meta"] || {}}
on:change={e => {
if (e.detail?.key) {
onChange(e, e.detail.key)
} else {
onChange(e, key)
}
}}
{bindings}
{isTestModal}
{isUpdateRow}
/>
{:else if value.customType === "webhookUrl"}
<WebhookDisplay
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "fields"}
<FieldSelector
{block}
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
{isTestModal}
/>
{:else if value.customType === "triggerSchema"}
<SchemaSetup on:change={e => onChange(e, key)} value={inputData[key]} />
{:else if value.customType === "code"}
<CodeEditorModal>
<CodeEditor
value={inputData[key]}
on:change={e => {
// need to pass without the value inside
onChange({ detail: e.detail }, key)
inputData[key] = e.detail
}}
completions={[
jsAutocomplete([
...bindingsToCompletions(bindings, EditorModes.JS),
]),
]}
mode={EditorModes.JS}
height={500}
/>
<div class="messaging">
<Icon name="FlashOn" />
<div class="messaging-wrap">
<div>Add available bindings by typing <strong>$</strong></div>
</div>
</div>
</CodeEditorModal>
{:else if value.customType === "loopOption"}
<Select
on:change={e => onChange(e, key)}
autoWidth
value={inputData[key]}
options={["Array", "String"]}
defaultValue={"Array"}
/>
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
{#if isTestModal}
<ModalBindableInput
title={value.title}
value={inputData[key]}
panel={AutomationBindingPanel}
type={value.customType}
{#if value.type === "string" && value.enum && canShowField(key)}
<Select
on:change={e => onChange(e, key)}
{bindings}
updateOnChange={false}
value={inputData[key]}
placeholder={false}
options={value.enum}
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
/>
{:else}
<div class="test">
{:else if value.type === "json"}
<Editor
editorHeight="250"
editorWidth="448"
mode="json"
value={inputData[key]?.value}
on:change={e => {
/**
* TODO - Remove after November 2023
* *******************************
* Code added to provide backwards compatibility between Values 1,2,3,4,5
* and the new JSON body.
*/
delete inputData.value1
delete inputData.value2
delete inputData.value3
delete inputData.value4
delete inputData.value5
/***********************/
onChange(e, key)
}}
/>
{:else if value.type === "boolean"}
<div style="margin-top: 10px">
<Checkbox
text={value.title}
value={inputData[key]}
on:change={e => onChange(e, key)}
/>
</div>
{:else if value.type === "date"}
<DatePicker
value={inputData[key]}
on:change={e => onChange(e, key)}
/>
{:else if value.customType === "column"}
<Select
on:change={e => onChange(e, key)}
value={inputData[key]}
options={Object.keys(table?.schema || {})}
/>
{:else if value.customType === "filters"}
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
<Drawer bind:this={drawer} {fillWidth} title="Filtering">
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
Save
</Button>
<FilterDrawer
slot="body"
{filters}
{bindings}
{schemaFields}
datasource={{ type: "table", tableId }}
panel={AutomationBindingPanel}
fillWidth
on:change={e => (tempFilters = e.detail)}
/>
</Drawer>
{:else if value.customType === "password"}
<Input
type="password"
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "email"}
{#if isTestModal}
<ModalBindableInput
title={value.title}
value={inputData[key]}
panel={AutomationBindingPanel}
type="email"
on:change={e => onChange(e, key)}
{bindings}
fillWidth
updateOnChange={false}
/>
{:else}
<DrawerBindableInput
fillWidth={true}
fillWidth
title={value.title}
panel={AutomationBindingPanel}
type={value.customType}
type="email"
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
allowJS={false}
updateOnChange={false}
placeholder={value.customType === "queryLimit" ? queryLimit : ""}
drawerLeft="260px"
/>
</div>
{/if}
{:else if value.customType === "query"}
<QuerySelector
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "cron"}
<CronBuilder
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "queryParams"}
<QueryParamSelector
on:change={e => onChange(e, key)}
value={inputData[key]}
{bindings}
/>
{:else if value.customType === "table"}
<TableSelector
{isTrigger}
value={inputData[key]}
on:change={e => onChange(e, key)}
/>
{:else if value.customType === "row"}
<RowSelector
{block}
value={inputData[key]}
meta={inputData["meta"] || {}}
on:change={e => {
if (e.detail?.key) {
onChange(e, e.detail.key)
} else {
onChange(e, key)
}
}}
{bindings}
{isTestModal}
{isUpdateRow}
/>
{:else if value.customType === "webhookUrl"}
<WebhookDisplay
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "fields"}
<FieldSelector
{block}
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
{isTestModal}
/>
{:else if value.customType === "triggerSchema"}
<SchemaSetup
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "code"}
<CodeEditorModal>
<CodeEditor
value={inputData[key]}
on:change={e => {
// need to pass without the value inside
onChange({ detail: e.detail }, key)
inputData[key] = e.detail
}}
completions={[
jsAutocomplete([
...bindingsToCompletions(bindings, EditorModes.JS),
]),
]}
mode={EditorModes.JS}
height={500}
/>
<div class="messaging">
<Icon name="FlashOn" />
<div class="messaging-wrap">
<div>Add available bindings by typing <strong>$</strong></div>
</div>
</div>
</CodeEditorModal>
{:else if value.customType === "loopOption"}
<Select
on:change={e => onChange(e, key)}
autoWidth
value={inputData[key]}
options={["Array", "String"]}
defaultValue={"Array"}
/>
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
{#if isTestModal}
<ModalBindableInput
title={value.title}
value={inputData[key]}
panel={AutomationBindingPanel}
type={value.customType}
on:change={e => onChange(e, key)}
{bindings}
updateOnChange={false}
/>
{:else}
<div class="test">
<DrawerBindableInput
fillWidth={true}
title={value.title}
panel={AutomationBindingPanel}
type={value.customType}
value={inputData[key]}
on:change={e => onChange(e, key)}
{bindings}
updateOnChange={false}
placeholder={value.customType === "queryLimit"
? queryLimit
: ""}
drawerLeft="260px"
/>
</div>
{/if}
{/if}
{/if}
</div>
</div>
{/if}
{/each}
</div>
<Modal bind:this={webhookModal} width="30%">

View file

@ -14,7 +14,7 @@ ENV SERVICE=app-service
ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
ENV TOP_LEVEL_PATH=/
# handle node-gyp
RUN apt-get update \
@ -27,7 +27,8 @@ COPY scripts/integrations/oracle/ scripts/integrations/oracle/
RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh
COPY package.json .
RUN yarn install --frozen-lockfile --production=true
COPY dist/yarn.lock .
RUN yarn install --production=true
# Remove unneeded data from file system to reduce image size
RUN yarn cache clean && apt-get remove -y --purge --auto-remove g++ make python \
&& rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp

View file

@ -18,7 +18,7 @@
"test": "bash scripts/test.sh",
"test:memory": "jest --maxWorkers=2 --logHeapUsage --forceExit",
"test:watch": "jest --watch",
"predocker": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client && yarn build --configuration=production",
"predocker": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client && yarn build && cp ../../yarn.lock ./dist/",
"build:docker": "yarn predocker && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION",
"build:docs": "node ./scripts/docs/generate.js open",
"run:docker": "node dist/index.js",

View file

@ -29,7 +29,7 @@ import {
} from "@budibase/types"
import sdk from "../../sdk"
import { builderSocket } from "../../websockets"
import { setupCreationAuth as googleSetupCreationAuth } from "src/integrations/googlesheets"
import { setupCreationAuth as googleSetupCreationAuth } from "../../integrations/googlesheets"
function getErrorTables(errors: any, errorType: string) {
return Object.entries(errors)

View file

@ -1,6 +1,6 @@
require("svelte/register")
import { resolve, join } from "../../../utilities/centralPath"
import { join } from "../../../utilities/centralPath"
const uuid = require("uuid")
import { ObjectStoreBuckets } from "../../../constants"
import { processString } from "@budibase/string-templates"
@ -49,7 +49,7 @@ export const toggleBetaUiFeature = async function (ctx: any) {
return
}
let builderPath = resolve(TOP_LEVEL_PATH, "new_design_ui")
let builderPath = join(TOP_LEVEL_PATH, "new_design_ui")
// // download it from S3
if (!fs.existsSync(builderPath)) {
@ -67,7 +67,7 @@ export const toggleBetaUiFeature = async function (ctx: any) {
}
export const serveBuilder = async function (ctx: any) {
const builderPath = resolve(TOP_LEVEL_PATH, "builder")
const builderPath = join(TOP_LEVEL_PATH, "builder")
await send(ctx, ctx.file, { root: builderPath })
}

View file

@ -208,6 +208,7 @@ export async function save(ctx: UserCtx) {
let tableToSave: TableRequest = {
type: "table",
_id: buildExternalTableId(datasourceId, inputs.name),
sourceId: datasourceId,
...inputs,
}

View file

@ -48,6 +48,35 @@ export const definition: AutomationStepSchema = {
type: AutomationIOType.STRING,
title: "HTML Contents",
},
addInvite: {
type: AutomationIOType.BOOLEAN,
title: "Add calendar invite",
},
startTime: {
type: AutomationIOType.DATE,
title: "Start Time",
dependsOn: "addInvite",
},
endTime: {
type: AutomationIOType.DATE,
title: "End Time",
dependsOn: "addInvite",
},
summary: {
type: AutomationIOType.STRING,
title: "Meeting Summary",
dependsOn: "addInvite",
},
location: {
type: AutomationIOType.STRING,
title: "Location",
dependsOn: "addInvite",
},
url: {
type: AutomationIOType.STRING,
title: "URL",
dependsOn: "addInvite",
},
},
required: ["to", "from", "subject", "contents"],
},
@ -68,21 +97,43 @@ export const definition: AutomationStepSchema = {
}
export async function run({ inputs }: AutomationStepInput) {
let { to, from, subject, contents, cc, bcc } = inputs
let {
to,
from,
subject,
contents,
cc,
bcc,
addInvite,
startTime,
endTime,
summary,
location,
url,
} = inputs
if (!contents) {
contents = "<h1>No content</h1>"
}
to = to || undefined
try {
let response = await sendSmtpEmail(
let response = await sendSmtpEmail({
to,
from,
subject,
contents,
cc,
bcc,
true
)
automation: true,
invite: addInvite
? {
startTime,
endTime,
summary,
location,
url,
}
: undefined,
})
return {
success: true,
response,

View file

@ -1,71 +0,0 @@
function generateResponse(to, from) {
return {
"success": true,
"response": {
"accepted": [
to
],
"envelope": {
"from": from,
"to": [
to
]
},
"message": `Email sent to ${to}.`
}
}
}
const mockFetch = jest.fn(() => ({
headers: {
raw: () => {
return { "content-type": ["application/json"] }
},
get: () => ["application/json"],
},
json: jest.fn(() => response),
status: 200,
text: jest.fn(),
}))
jest.mock("node-fetch", () => mockFetch)
const setup = require("./utilities")
describe("test the outgoing webhook action", () => {
let inputs
let config = setup.getConfig()
beforeAll(async () => {
await config.init()
})
afterAll(setup.afterAll)
it("should be able to run the action", async () => {
inputs = {
to: "user1@test.com",
from: "admin@test.com",
subject: "hello",
contents: "testing",
}
let resp = generateResponse(inputs.to, inputs.from)
mockFetch.mockImplementationOnce(() => ({
headers: {
raw: () => {
return { "content-type": ["application/json"] }
},
get: () => ["application/json"],
},
json: jest.fn(() => resp),
status: 200,
text: jest.fn(),
}))
const res = await setup.runStep(setup.actions.SEND_EMAIL_SMTP.stepId, inputs)
expect(res.response).toEqual(resp)
expect(res.success).toEqual(true)
})
})

View file

@ -0,0 +1,74 @@
import * as workerRequests from "../../utilities/workerRequests"
jest.mock("../../utilities/workerRequests", () => ({
sendSmtpEmail: jest.fn(),
}))
function generateResponse(to: string, from: string) {
return {
success: true,
response: {
accepted: [to],
envelope: {
from: from,
to: [to],
},
message: `Email sent to ${to}.`,
},
}
}
const setup = require("./utilities")
describe("test the outgoing webhook action", () => {
let inputs
let config = setup.getConfig()
beforeAll(async () => {
await config.init()
})
afterAll(setup.afterAll)
it("should be able to run the action", async () => {
jest
.spyOn(workerRequests, "sendSmtpEmail")
.mockImplementationOnce(async () =>
generateResponse("user1@test.com", "admin@test.com")
)
const invite = {
startTime: new Date(),
endTime: new Date(),
summary: "summary",
location: "location",
url: "url",
}
inputs = {
to: "user1@test.com",
from: "admin@test.com",
subject: "hello",
contents: "testing",
cc: "cc",
bcc: "bcc",
addInvite: true,
...invite,
}
let resp = generateResponse(inputs.to, inputs.from)
const res = await setup.runStep(
setup.actions.SEND_EMAIL_SMTP.stepId,
inputs
)
expect(res.response).toEqual(resp)
expect(res.success).toEqual(true)
expect(workerRequests.sendSmtpEmail).toHaveBeenCalledTimes(1)
expect(workerRequests.sendSmtpEmail).toHaveBeenCalledWith({
to: "user1@test.com",
from: "admin@test.com",
subject: "hello",
contents: "testing",
cc: "cc",
bcc: "bcc",
invite,
automation: true,
})
})
})

View file

@ -98,7 +98,8 @@ const environment = {
isInThread: () => {
return process.env.FORKED_PROCESS
},
TOP_LEVEL_PATH: process.env.TOP_LEVEL_PATH,
TOP_LEVEL_PATH:
process.env.TOP_LEVEL_PATH || process.env.SERVER_TOP_LEVEL_PATH,
}
// threading can cause memory issues with node-ts in development

View file

@ -1,14 +1,14 @@
import { PathLike } from "fs"
import fs from "fs"
import { budibaseTempDir } from "../budibaseDir"
import { join } from "path"
import { resolve, join } from "path"
import env from "../../environment"
import tar from "tar"
import environment from "../../environment"
const uuid = require("uuid/v4")
export const TOP_LEVEL_PATH =
environment.TOP_LEVEL_PATH || join(__dirname, "..", "..", "..")
environment.TOP_LEVEL_PATH || resolve(join(__dirname, "..", "..", ".."))
/**
* Upon first startup of instance there may not be everything we need in tmp directory, set it up.

View file

@ -9,7 +9,7 @@ import {
env as coreEnv,
} from "@budibase/backend-core"
import { updateAppRole } from "./global"
import { BBContext, User } from "@budibase/types"
import { BBContext, User, EmailInvite } from "@budibase/types"
export function request(ctx?: BBContext, request?: any) {
if (!request.headers) {
@ -65,15 +65,25 @@ async function checkResponse(
}
// have to pass in the tenant ID as this could be coming from an automation
export async function sendSmtpEmail(
to: string,
from: string,
subject: string,
contents: string,
cc: string,
bcc: string,
export async function sendSmtpEmail({
to,
from,
subject,
contents,
cc,
bcc,
automation,
invite,
}: {
to: string
from: string
subject: string
contents: string
cc: string
bcc: string
automation: boolean
) {
invite?: EmailInvite
}) {
// tenant ID will be set in header
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + `/api/global/email/send`),
@ -88,6 +98,7 @@ export async function sendSmtpEmail(
bcc,
purpose: "custom",
automation,
invite,
},
})
)

View file

@ -1,5 +1,6 @@
import { Document } from "../document"
import { EventEmitter } from "events"
import { User } from "../global"
export enum AutomationIOType {
OBJECT = "object",
@ -8,6 +9,7 @@ export enum AutomationIOType {
NUMBER = "number",
ARRAY = "array",
JSON = "json",
DATE = "date",
}
export enum AutomationCustomIOType {
@ -66,6 +68,33 @@ export enum AutomationActionStepId {
integromat = "integromat",
}
export interface EmailInvite {
startTime: Date
endTime: Date
summary: string
location?: string
url?: string
}
export interface SendEmailOpts {
// workspaceId If finer grain controls being used then this will lookup config for workspace.
workspaceId?: string
// user If sending to an existing user the object can be provided, this is used in the context.
user: User
// from If sending from an address that is not what is configured in the SMTP config.
from?: string
// contents If sending a custom email then can supply contents which will be added to it.
contents?: string
// subject A custom subject can be specified if the config one is not desired.
subject?: string
// info Pass in a structure of information to be stored alongside the invitation.
info?: any
cc?: boolean
bcc?: boolean
automation?: boolean
invite?: EmailInvite
}
export const AutomationStepIdArray = [
...Object.values(AutomationActionStepId),
...Object.values(AutomationTriggerStepId),
@ -90,6 +119,7 @@ interface BaseIOStructure {
customType?: AutomationCustomIOType
title?: string
description?: string
dependsOn?: string
enum?: string[]
pretty?: string[]
properties?: {

View file

@ -13,7 +13,8 @@ RUN yarn global add pm2
COPY package.json .
RUN yarn install --frozen-lockfile --production=true
COPY dist/yarn.lock .
RUN yarn install --production=true
# Remove unneeded data from file system to reduce image size
RUN apk del .gyp \
&& yarn cache clean

View file

@ -19,7 +19,7 @@
"run:docker": "node dist/index.js",
"debug": "yarn build && node --expose-gc --inspect=9223 dist/index.js",
"run:docker:cluster": "pm2-runtime start pm2.config.js",
"predocker": "yarn build --configuration=production",
"predocker": "yarn build && cp ../../yarn.lock ./dist/",
"build:docker": "yarn predocker && docker build . -t worker-service --label version=$BUDIBASE_RELEASE_VERSION",
"dev:stack:init": "node ./scripts/dev/manage.js init",
"dev:builder": "npm run dev:stack:init && rimraf dist/ && nodemon",
@ -53,6 +53,7 @@
"elastic-apm-node": "3.38.0",
"global-agent": "3.0.0",
"got": "11.8.3",
"ical-generator": "4.1.0",
"joi": "17.6.0",
"koa": "2.13.4",
"koa-body": "4.2.0",

View file

@ -14,6 +14,7 @@ export async function sendEmail(ctx: BBContext) {
cc,
bcc,
automation,
invite,
} = ctx.request.body
let user
if (userId) {
@ -29,6 +30,7 @@ export async function sendEmail(ctx: BBContext) {
cc,
bcc,
automation,
invite,
})
ctx.body = {
...response,

View file

@ -4,28 +4,11 @@ import { getTemplateByPurpose, EmailTemplates } from "../constants/templates"
import { getSettingsTemplateContext } from "./templates"
import { processString } from "@budibase/string-templates"
import { getResetPasswordCode, getInviteCode } from "./redis"
import { User, SMTPInnerConfig } from "@budibase/types"
import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types"
import { configs } from "@budibase/backend-core"
import ical from "ical-generator"
const nodemailer = require("nodemailer")
type SendEmailOpts = {
// workspaceId If finer grain controls being used then this will lookup config for workspace.
workspaceId?: string
// user If sending to an existing user the object can be provided, this is used in the context.
user: User
// from If sending from an address that is not what is configured in the SMTP config.
from?: string
// contents If sending a custom email then can supply contents which will be added to it.
contents?: string
// subject A custom subject can be specified if the config one is not desired.
subject?: string
// info Pass in a structure of information to be stored alongside the invitation.
info?: any
cc?: boolean
bcc?: boolean
automation?: boolean
}
const TEST_MODE = env.ENABLE_EMAIL_TEST_MODE && env.isDev()
const TYPE = TemplateType.EMAIL
@ -200,6 +183,26 @@ export async function sendEmail(
context
)
}
if (opts?.invite) {
const calendar = ical({
name: "Invite",
})
calendar.createEvent({
start: opts.invite.startTime,
end: opts.invite.endTime,
summary: opts.invite.summary,
location: opts.invite.location,
url: opts.invite.url,
})
message = {
...message,
icalEvent: {
method: "request",
content: calendar.toString(),
},
}
}
const response = await transport.sendMail(message)
if (TEST_MODE) {
console.log("Test email URL: " + nodemailer.getTestMessageUrl(response))

7
scripts/resetVersions.sh Executable file
View file

@ -0,0 +1,7 @@
#!/bin/bash
echo "Resetting package versions"
yarn lerna exec "yarn version --no-git-tag-version --new-version=0.0.0"
echo "Updating dependencies"
node scripts/syncLocalDependencies.js "0.0.0"
git checkout package.json
echo "Package versions reset!"

12
scripts/updateVersions.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/bash
version=$(cat lerna.json \
| grep version \
| head -1 \
| awk -F: '{gsub(/"/,"",$2);gsub(/[[:space:]]*/,"",$2); print $2}' \
| sed 's/[",]//g')
echo "Setting version $version"
yarn lerna exec "yarn version --no-git-tag-version --new-version=$version"
echo "Updating dependencies"
node scripts/syncLocalDependencies.js $version
echo "Syncing yarn workspace"
yarn

View file

@ -13668,6 +13668,13 @@ husky@^8.0.3:
resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184"
integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==
ical-generator@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/ical-generator/-/ical-generator-4.1.0.tgz#2a336c951864c5583a2aa715d16f2edcdfd2d90b"
integrity sha512-5GrFDJ8SAOj8cB9P1uEZIfKrNxSZ1R2eOQfZePL+CtdWh4RwNXWe8b0goajz+Hu37vcipG3RVldoa2j57Y20IA==
dependencies:
uuid-random "^1.3.2"
iconv-lite@0.4.24, iconv-lite@^0.4.15, iconv-lite@^0.4.24, iconv-lite@^0.4.5:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@ -25279,6 +25286,11 @@ utils-merge@1.0.1, utils-merge@1.x.x:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
uuid-random@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/uuid-random/-/uuid-random-1.3.2.tgz#96715edbaef4e84b1dcf5024b00d16f30220e2d0"
integrity sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ==
uuid@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"