Merge branch 'develop' of github.com:Budibase/budibase into grid-block

This commit is contained in:
Andrew Kingston 2023-06-09 11:54:27 +01:00
commit 1377ee090a
28 changed files with 504 additions and 348 deletions

View File

@ -48,19 +48,8 @@ jobs:
- run: yarn install --frozen-lockfile - run: yarn install --frozen-lockfile
- name: Update versions - name: Update versions
run: | run: ./scripts/updateVersions.sh
version=$(cat lerna.json \ - run: yarn build
| 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: yarn build:sdk - run: yarn build:sdk
- name: Publish budibase packages to NPM - name: Publish budibase packages to NPM

View File

@ -41,20 +41,9 @@ jobs:
- run: yarn install --frozen-lockfile - run: yarn install --frozen-lockfile
- name: Update versions - name: Update versions
run: | run: ./scripts/updateVersions.sh
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 lint - run: yarn lint
- run: yarn build --configuration=production - run: yarn build
- run: yarn build:sdk - run: yarn build:sdk
- name: Publish budibase packages to NPM - name: Publish budibase packages to NPM

View File

@ -17,6 +17,8 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
fetch-depth: 0 fetch-depth: 0
- name: Fail if tag is not in master - name: Fail if tag is not in master

View File

@ -5,7 +5,7 @@ on:
env: env:
CI: true CI: true
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }} PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
REGISTRY_URL: registry.hub.docker.com REGISTRY_URL: registry.hub.docker.com
jobs: jobs:
build: build:
@ -24,6 +24,8 @@ jobs:
- name: "Checkout" - name: "Checkout"
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
submodules: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
fetch-depth: 0 fetch-depth: 0
- name: Fail if tag is not in master - 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" echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
exit 1 exit 1
fi fi
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
@ -44,10 +46,12 @@ jobs:
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
- name: Run Yarn - name: Run Yarn
run: yarn run: yarn
- name: Run Yarn Bootstrap - name: Update versions
run: yarn bootstrap run: ./scripts/updateVersions.sh
- name: Runt Yarn Lint - name: Runt Yarn Lint
run: yarn lint run: yarn lint
- name: Update versions
run: ./scripts/updateVersions.sh
- name: Run Yarn Build - name: Run Yarn Build
run: yarn build:docker:pre run: yarn build:docker:pre
- name: Login to Docker Hub - 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 # 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 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 # build server
WORKDIR /app WORKDIR /app
ADD packages/server . 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 # build worker
WORKDIR /worker WORKDIR /worker
ADD packages/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 FROM budibase/couchdb
ARG TARGETARCH 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_PORT}" ]] && export WORKER_PORT=4002
[[ -z "${WORKER_URL}" ]] && export WORKER_URL=http://localhost:4002 [[ -z "${WORKER_URL}" ]] && export WORKER_URL=http://localhost:4002
[[ -z "${APPS_URL}" ]] && export APPS_URL=http://localhost:4001 [[ -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 # export CUSTOM_DOMAIN=budi001.custom.com
# Azure App Service customisations # Azure App Service customisations

View File

@ -1,5 +1,5 @@
{ {
"version": "2.7.1-alpha.1", "version": "2.7.7-alpha.2",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/backend-core", "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: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: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: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", "test": "lerna run --stream test --stream",
"lint:eslint": "eslint packages && eslint qa-core", "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}\"", "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", "lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
"build:specs": "lerna run --stream specs", "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": "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: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: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 -", "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 * as google from "../sso/google"
import { Cookie } from "../../../constants" import { Cookie } from "../../../constants"
import { clearCookie, getCookie } from "../../../utils"
import * as configs from "../../../configs" 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 { ssoSaveUserNoOp } from "../sso/sso"
import { cache, utils } from "../../../"
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
type Passport = { type Passport = {
@ -22,7 +23,7 @@ async function fetchGoogleCreds() {
export async function preAuth( export async function preAuth(
passport: Passport, passport: Passport,
ctx: BBContext, ctx: UserCtx,
next: Function next: Function
) { ) {
// get the relevant config // get the relevant config
@ -49,7 +50,7 @@ export async function preAuth(
export async function postAuth( export async function postAuth(
passport: Passport, passport: Passport,
ctx: BBContext, ctx: UserCtx,
next: Function next: Function
) { ) {
// get the relevant config // get the relevant config
@ -57,7 +58,7 @@ export async function postAuth(
const platformUrl = await configs.getPlatformUrl({ tenantAware: false }) const platformUrl = await configs.getPlatformUrl({ tenantAware: false })
let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback` 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( return passport.authenticate(
new GoogleStrategy( new GoogleStrategy(
@ -72,7 +73,7 @@ export async function postAuth(
_profile: SSOProfile, _profile: SSOProfile,
done: Function done: Function
) => { ) => {
clearCookie(ctx, Cookie.DatasourceAuth) utils.clearCookie(ctx, Cookie.DatasourceAuth)
done(null, { accessToken, refreshToken }) done(null, { accessToken, refreshToken })
} }
), ),

View File

@ -13,6 +13,8 @@
Modal, Modal,
notifications, notifications,
Icon, Icon,
Checkbox,
DatePicker,
} from "@budibase/bbui" } from "@budibase/bbui"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import { automationStore, selectedAutomation } from "builderStore" import { automationStore, selectedAutomation } from "builderStore"
@ -306,6 +308,11 @@
drawer.hide() drawer.hide()
} }
function canShowField(key, value) {
const dependsOn = value.dependsOn
return !dependsOn || !!inputData[dependsOn]
}
onMount(async () => { onMount(async () => {
try { try {
await environment.loadVariables() await environment.loadVariables()
@ -317,210 +324,233 @@
<div class="fields"> <div class="fields">
{#each deprecatedSchemaProperties as [key, value]} {#each deprecatedSchemaProperties as [key, value]}
<div class="block-field"> {#if canShowField(key, value)}
{#if key !== "fields"} <div class="block-field">
<Label {#if key !== "fields" && value.type !== "boolean"}
tooltip={value.title === "Binding / Value" <Label
? "If using the String input type, please use a comma or newline separated string" tooltip={value.title === "Binding / Value"
: null}>{value.title || (key === "row" ? "Table" : key)}</Label ? "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} {/if}
{:else if value.customType === "query"} {#if value.type === "string" && value.enum && canShowField(key)}
<QuerySelector <Select
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)} on:change={e => onChange(e, key)}
{bindings} value={inputData[key]}
updateOnChange={false} placeholder={false}
options={value.enum}
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
/> />
{:else} {:else if value.type === "json"}
<div class="test"> <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 <DrawerBindableInput
fillWidth={true} fillWidth
title={value.title} title={value.title}
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
type={value.customType} type="email"
value={inputData[key]} value={inputData[key]}
on:change={e => onChange(e, key)} on:change={e => onChange(e, key)}
{bindings} {bindings}
allowJS={false}
updateOnChange={false} updateOnChange={false}
placeholder={value.customType === "queryLimit" ? queryLimit : ""}
drawerLeft="260px" 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}
{/if} </div>
</div> {/if}
{/each} {/each}
</div> </div>
<Modal bind:this={webhookModal} width="30%"> <Modal bind:this={webhookModal} width="30%">

View File

@ -14,7 +14,7 @@ ENV SERVICE=app-service
ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
ENV TOP_LEVEL_PATH=/
# handle node-gyp # handle node-gyp
RUN apt-get update \ 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 RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh
COPY package.json . 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 # Remove unneeded data from file system to reduce image size
RUN yarn cache clean && apt-get remove -y --purge --auto-remove g++ make python \ 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 && 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": "bash scripts/test.sh",
"test:memory": "jest --maxWorkers=2 --logHeapUsage --forceExit", "test:memory": "jest --maxWorkers=2 --logHeapUsage --forceExit",
"test:watch": "jest --watch", "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:docker": "yarn predocker && docker build . -t app-service --label version=$BUDIBASE_RELEASE_VERSION",
"build:docs": "node ./scripts/docs/generate.js open", "build:docs": "node ./scripts/docs/generate.js open",
"run:docker": "node dist/index.js", "run:docker": "node dist/index.js",

View File

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

View File

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

View File

@ -48,6 +48,35 @@ export const definition: AutomationStepSchema = {
type: AutomationIOType.STRING, type: AutomationIOType.STRING,
title: "HTML Contents", 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"], required: ["to", "from", "subject", "contents"],
}, },
@ -68,21 +97,43 @@ export const definition: AutomationStepSchema = {
} }
export async function run({ inputs }: AutomationStepInput) { 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) { if (!contents) {
contents = "<h1>No content</h1>" contents = "<h1>No content</h1>"
} }
to = to || undefined to = to || undefined
try { try {
let response = await sendSmtpEmail( let response = await sendSmtpEmail({
to, to,
from, from,
subject, subject,
contents, contents,
cc, cc,
bcc, bcc,
true automation: true,
) invite: addInvite
? {
startTime,
endTime,
summary,
location,
url,
}
: undefined,
})
return { return {
success: true, success: true,
response, 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: () => { isInThread: () => {
return process.env.FORKED_PROCESS 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 // threading can cause memory issues with node-ts in development

View File

@ -1,14 +1,14 @@
import { PathLike } from "fs" import { PathLike } from "fs"
import fs from "fs" import fs from "fs"
import { budibaseTempDir } from "../budibaseDir" import { budibaseTempDir } from "../budibaseDir"
import { join } from "path" import { resolve, join } from "path"
import env from "../../environment" import env from "../../environment"
import tar from "tar" import tar from "tar"
import environment from "../../environment" import environment from "../../environment"
const uuid = require("uuid/v4") const uuid = require("uuid/v4")
export const TOP_LEVEL_PATH = 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. * 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, env as coreEnv,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { updateAppRole } from "./global" import { updateAppRole } from "./global"
import { BBContext, User } from "@budibase/types" import { BBContext, User, EmailInvite } from "@budibase/types"
export function request(ctx?: BBContext, request?: any) { export function request(ctx?: BBContext, request?: any) {
if (!request.headers) { 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 // have to pass in the tenant ID as this could be coming from an automation
export async function sendSmtpEmail( export async function sendSmtpEmail({
to: string, to,
from: string, from,
subject: string, subject,
contents: string, contents,
cc: string, cc,
bcc: string, bcc,
automation,
invite,
}: {
to: string
from: string
subject: string
contents: string
cc: string
bcc: string
automation: boolean automation: boolean
) { invite?: EmailInvite
}) {
// tenant ID will be set in header // tenant ID will be set in header
const response = await fetch( const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + `/api/global/email/send`), checkSlashesInUrl(env.WORKER_URL + `/api/global/email/send`),
@ -88,6 +98,7 @@ export async function sendSmtpEmail(
bcc, bcc,
purpose: "custom", purpose: "custom",
automation, automation,
invite,
}, },
}) })
) )

View File

@ -1,5 +1,6 @@
import { Document } from "../document" import { Document } from "../document"
import { EventEmitter } from "events" import { EventEmitter } from "events"
import { User } from "../global"
export enum AutomationIOType { export enum AutomationIOType {
OBJECT = "object", OBJECT = "object",
@ -8,6 +9,7 @@ export enum AutomationIOType {
NUMBER = "number", NUMBER = "number",
ARRAY = "array", ARRAY = "array",
JSON = "json", JSON = "json",
DATE = "date",
} }
export enum AutomationCustomIOType { export enum AutomationCustomIOType {
@ -66,6 +68,33 @@ export enum AutomationActionStepId {
integromat = "integromat", 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 = [ export const AutomationStepIdArray = [
...Object.values(AutomationActionStepId), ...Object.values(AutomationActionStepId),
...Object.values(AutomationTriggerStepId), ...Object.values(AutomationTriggerStepId),
@ -90,6 +119,7 @@ interface BaseIOStructure {
customType?: AutomationCustomIOType customType?: AutomationCustomIOType
title?: string title?: string
description?: string description?: string
dependsOn?: string
enum?: string[] enum?: string[]
pretty?: string[] pretty?: string[]
properties?: { properties?: {

View File

@ -13,7 +13,8 @@ RUN yarn global add pm2
COPY package.json . 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 # Remove unneeded data from file system to reduce image size
RUN apk del .gyp \ RUN apk del .gyp \
&& yarn cache clean && yarn cache clean

View File

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

View File

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

View File

@ -4,28 +4,11 @@ import { getTemplateByPurpose, EmailTemplates } from "../constants/templates"
import { getSettingsTemplateContext } from "./templates" import { getSettingsTemplateContext } from "./templates"
import { processString } from "@budibase/string-templates" import { processString } from "@budibase/string-templates"
import { getResetPasswordCode, getInviteCode } from "./redis" 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 { configs } from "@budibase/backend-core"
import ical from "ical-generator"
const nodemailer = require("nodemailer") 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 TEST_MODE = env.ENABLE_EMAIL_TEST_MODE && env.isDev()
const TYPE = TemplateType.EMAIL const TYPE = TemplateType.EMAIL
@ -200,6 +183,26 @@ export async function sendEmail(
context 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) const response = await transport.sendMail(message)
if (TEST_MODE) { if (TEST_MODE) {
console.log("Test email URL: " + nodemailer.getTestMessageUrl(response)) 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" resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184"
integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== 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: iconv-lite@0.4.24, iconv-lite@^0.4.15, iconv-lite@^0.4.24, iconv-lite@^0.4.5:
version "0.4.24" version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 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" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== 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: uuid@3.3.2:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"