Browse Source

Merge pull request #2988 from Budibase/develop

Develop -> Master
pull/3003/head
Martin McKeaveney 5 years ago
committed by GitHub
parent
commit
012bbf236d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 118
      .github/CONTRIBUTING.md
  2. 57
      .github/workflows/release-selfhost.yml
  3. 33
      .github/workflows/release.yml
  4. 2
      lerna.json
  5. 18
      package.json
  6. 2
      packages/auth/package.json
  7. 5
      packages/auth/src/middleware/authenticated.js
  8. 15
      packages/auth/src/security/sessions.js
  9. 28
      packages/auth/src/utils.js
  10. 2
      packages/bbui/package.json
  11. 5
      packages/bbui/src/Table/CellRenderer.svelte
  12. 28
      packages/bbui/src/Table/InternalRenderer.svelte
  13. 41
      packages/builder/cypress/integration/addMultiOptionDatatype.spec.js
  14. 35
      packages/builder/cypress/integration/addRadioButtons.spec.js
  15. 7
      packages/builder/cypress/integration/createTable.spec.js
  16. 8
      packages/builder/cypress/integration/createView.spec.js
  17. 61
      packages/builder/cypress/integration/customThemingProperties.spec.js
  18. 88
      packages/builder/cypress/support/commands.js
  19. 8
      packages/builder/package.json
  20. 2
      packages/builder/src/builderStore/store/automation/Automation.js
  21. 8
      packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte
  22. 2
      packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte
  23. 17
      packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte
  24. 27
      packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
  25. 21
      packages/builder/src/components/automation/SetupPanel/RowSelector.svelte
  26. 2
      packages/builder/src/components/automation/Shared/CreateWebhookModal.svelte
  27. 22
      packages/builder/src/components/backend/DataTable/DataTable.svelte
  28. 10
      packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
  29. 19
      packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte
  30. 2
      packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte
  31. 2
      packages/builder/src/components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte
  32. 6
      packages/builder/src/components/integration/index.svelte
  33. 2
      packages/cli/package.json
  34. 6
      packages/client/package.json
  35. 2
      packages/server/nodemon.json
  36. 22
      packages/server/package.json
  37. 8
      packages/server/scripts/account.js
  38. 2
      packages/server/src/automations/steps/discord.js
  39. 7
      packages/server/src/automations/tests/automation.spec.js
  40. 1
      packages/server/src/automations/triggerInfo/app.js
  41. 22
      packages/server/src/automations/triggers.js
  42. 2
      packages/server/src/definitions/datasource.ts
  43. 19
      packages/server/src/integrations/postgres.ts
  44. 14
      packages/server/src/integrations/rest.ts
  45. 70
      packages/server/src/integrations/tests/rest.spec.js
  46. 2
      packages/string-templates/package.json
  47. 3
      packages/worker/nodemon.json
  48. 22
      packages/worker/package.json
  49. 8
      packages/worker/scripts/account.js
  50. 4
      packages/worker/src/api/controllers/global/auth.js
  51. 28
      packages/worker/src/api/controllers/global/users.js

118
.github/CONTRIBUTING.md

@ -2,7 +2,7 @@
From opening a bug report to creating a pull request: every contribution is appreciated and welcome. If you're planning to implement a new feature or change the api please create an issue first. This way we can ensure that your precious work is not in vain.
### Not Sure Where to Start?
## Not Sure Where to Start?
Budibase is a low-code web application builder that creates svelte based web applications.
@ -14,6 +14,8 @@ Budibase is a monorepo managed by [lerna](https://github.com/lerna/lerna). Lerna
- **packages/server** - The budibase server. This [Koa](https://koajs.com/) app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system.
- **packages/worker** - This [Koa](https://koajs.com/) app is responsible for providing global apis for managing your budibase installation. Authentication, Users, Email, Org and Auth configs are all provided by the worker.
## Contributor License Agreement (CLA)
In order to accept your pull request, we need you to submit a CLA. You only need to do this once. If you are submitting a pull request for the first time, just submit a Pull Request and our CLA Bot will give you instructions on how to sign the CLA before merging your Pull Request.
@ -62,8 +64,6 @@ A component is the basic frontend building block of a budibase app.
Component libraries are collections of components as well as the definition of their props contained in a file called `components.json`.
## Contributing to Budibase
* Please maintain the existing code style.
@ -74,31 +74,30 @@ Component libraries are collections of components as well as the definition of t
* If the project diverges from your branch, please rebase instead of merging. This makes the commit graph easier to read.
* Once your work is completed, please raise a PR against the main branch with some information about what has changed and why.
* Once your work is completed, please raise a PR against the `develop` branch with some information about what has changed and why.
### Getting Started For Contributors
### 1. Prerequisites
#### 1. Prerequisites
*yarn -* `npm install -g yarn`
*jest* - `npm install -g jest`
### 2. Clone this repository
#### 2. Clone this repository
`git clone https://github.com/Budibase/budibase.git`
then `cd ` into your local copy.
### 3. Install and Build
#### 3. Install and Build
To develop the Budibase platform you'll need [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) installed.
#### Quick method
##### Quick method
`yarn setup` will check that all necessary components are installed and setup the repo for usage.
#### Manual method
##### Manual method
The following commands can be executed to manually get Budibase up and running (assuming Docker/Docker Compose has been installed).
@ -108,15 +107,7 @@ The following commands can be executed to manually get Budibase up and running (
`yarn build` will build all budibase packages.
### 4. Initialising Budibase and Creating a Budibase App
Starting up the budibase electron app should initialise budibase for you. A Budibase apps folder will have been created in `~/.budibase`.
This is a blank apps folder, so you will need to create yourself an app, you can do this by clicking "Create New App" from the budibase builder.
This will create a new budibase application in the `~/.budibase/<your-app-uuid>` directory, and NPM install the component libraries for that application. Let's start building your app with the budibase builder!
### 4. Running
#### 4. Running
To run the budibase server and builder in dev mode (i.e. with live reloading):
@ -126,7 +117,7 @@ To run the budibase server and builder in dev mode (i.e. with live reloading):
This will enable watch mode for both the builder app, server, client library and any component libraries.
### 5. Debugging using VS Code
#### 5. Debugging using VS Code
To debug the budibase server and worker a VS Code launch configuration has been provided.
@ -135,75 +126,90 @@ Alternatively to start both components simultaneously select `Start Budibase`.
In addition to the above, the remaining budibase components may be ran in dev mode using: `yarn dev:noserver`.
### 6. Cleanup
#### 6. Cleanup
If you wish to delete all the apps created in development and reset the environment then run the following:
1. `yarn nuke:docker` will wipe all the Budibase services
2. `yarn dev` will restart all the services
## Data Storage
When you are running locally, budibase stores data on disk using [PouchDB](https://pouchdb.com/), as well as some JSON on local files. After setting up budibase, you can find all of this data in the `~/.budibase` directory.
### Backend
A client can have one or more budibase applications. Budibase applications are stored in `~/.budibase/<app-uuid>`. Files used by your budibase application when running are stored in the `public` directory. Everything else is dev files used for the development of your apps in the builder.
#### Frontend
For the backend we run [Redis](https://redis.io/), [CouchDB](https://couchdb.apache.org/), [MinIO](https://min.io/) and [Envoy](https://www.envoyproxy.io/) in Docker compose. This means that to develop Budibase you will need Docker and Docker compose installed. The backend services are then ran separately as Node services with nodemon so that they can be debugged outside of Docker.
To see the current individual JSON definitions for your pages and screens used by the builder, have a look at `~/.budibase/<app-uuid>/pages`.
### Data Storage
For your actual running application (not in dev), the frontend tree structure of the application (known as `clientFrontendDefinition`) is stored as JSON on disk. This is what the budibase client library reads to create your app at runtime. This can be found at `~/.budibase/<app-uuid>/public/clientFrontendDefinition.js`
When you are running locally, budibase stores data on disk using docker volumes. The volumes and the types of data associated with each are:
The HTML and CSS for your apps runtime pages, as well as the budibase client library JS is stored at:
- `redis_data`
- Sessions, email tokens
- `couchdb3_data`
- Global and app databases
- `minio_data`
- App manifest, budibase client, static assets
- `~/.budibase/<app-uuid>/public/main`
- `~/.budibase/<app-uuid>/public/unauthenticated`
### Devlopment Modes
#### Backend
A combination of environment variables controls the mode that budibase runs in.
Yarn commands can be used to mimic the different modes that budibase can be ran in
For the backend we run [Redis](https://redis.io/), [CouchDB](https://couchdb.apache.org/), [MinIO](https://min.io/) and [Envoy](https://www.envoyproxy.io/) in Docker compose. This means that to develop Budibase you will need Docker and Docker compose installed. The backend services are then ran separately as Node services with nodemon so that they can be debugged outside of Docker.
#### Self Hosted
The default mode. A single tenant installation with no usage restrictions.
### Publishing Budibase to NPM
To enable this mode, use:
```
yarn mode:self
```
#### Testing In Electron
#### Cloud
The cloud mode, with account portal turned off.
At budibase, we pride ourselves on giving our users a fast, native and slick local development experience. As a result, we use the electron to provide a native GUI for the budibase builder. In order to release budibase out into the wild, you should test your changes in a packaged electron application. To do this, first build budibase from the root directory.
To enable this mode, use:
```
yarn build
yarn mode:cloud
```
#### Cloud & Account
The cloud mode, with account portal turned on. This is a replica of the mode that runs at https://budibase.app
Now everything is built, you can package up your electron application.
To enable this mode, use:
```
cd packages/server
yarn build:electron
yarn mode:account
```
### CI
Your new electron application will be stored in `packages/server/dist/<operating-system>`. Open up the executable and make sure everything is working smoothly.
#### PR Job
After your pr is submitted a github action (can be found at `.github/workflows/budibase_ci.yml`) will run to perform some checks against the changes such as linting, build and test.
#### Publishing to NPM
The job will run when changes are pushed to or targetted at `master` and `develop`
#### Release Develop
Once you are happy that your changes work in electron, you can publish all the latest versions of the monorepo packages by running:
To test changes before a release, a prerelease action (can be found at `.github/workflows/release-develop.yml`) will run to build and release develop versions of npm packages and docker images. On each subsequent commit to develop a new alpha version of npm packages will be created and released.
```
yarn publishnpm
```
For example:
from your root directory.
- `feature1` -> `develop` = `v0.9.160-alpha.1`
- `feature2` -> `develop` = `v0.9.160-alpha.0`
#### CI Release
The job will run when changes are pushed to `develop`
#### Release Job
After NPM has successfully published the budibase packages, a new tag will be pushed to master. This will kick off a github action (can be found at `.github/workflows/release.yml`) this will build and package the electron application for every OS (Windows, Mac, Linux). The binaries will be stored under the new tag on the [budibase releases page](https://github.com/Budibase/budibase/releases).
To release changes a release job (can be found at `.github/workflows/release.yml`) will run to create final versions of npm packages and docker images.
### Troubleshooting
Following the example above:
Sometimes, things go wrong. This can be due to incompatible updates on the budibase platform. To clear down your development environment and start again:
- `develop` -> `master` = `v0.9.160`
```
rm -rf ~/.budibase
```
Follow from **Step 3. Install and Build** in the setup guide above. You should have a fresh Budibase installation.
The job will run when changes are pushed to `master`
#### Release Self Host Job
To release the self hosted version of docker images, an additional job (can be found at `.github/workflows/release-selfhost.yml`) must be ran manually. This will releaae docker images to docker hub under the tag `latest` to be picked up by self hosted installations.
### Troubleshooting
Sometimes, things go wrong. This can be due to incompatible updates on the budibase platform. To clear down your development environment and start again follow **Step 6. Cleanup**, then proceed from **Step 3. Install and Build** in the setup guide above. You should have a fresh Budibase installation.
### Running tests
#### End-to-end Tests

57
.github/workflows/release-selfhost.yml

@ -0,0 +1,57 @@
name: Budibase Release
on:
workflow_dispatch:
env:
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12.x
- run: yarn
- run: yarn bootstrap
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: 'Get Previous tag'
id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
- name: Build/release Docker images (Self Host)
if: ${{ github.event.inputs.release_self_host == 'Y' }}
run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
yarn build
yarn build:docker:production
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}
- uses: azure/setup-helm@v1
id: install
# So, we need to inject the values into this
- run: yarn release:helm
- name: Run chart-releaser
uses: helm/chart-releaser-action@v1.1.0
with:
charts_dir: docs
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

33
.github/workflows/release.yml

@ -4,12 +4,6 @@ on:
push:
branches:
- master
workflow_dispatch:
inputs:
release_self_host:
description: 'Release to self hosters? (Y/N)'
required: true
default: 'N'
env:
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
@ -54,7 +48,6 @@ jobs:
uses: "WyriHaximus/github-action-get-previous-tag@v1"
- name: Build/release Docker images
if: ${{ github.event.inputs.release_self_host != 'Y' }}
run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
yarn build
@ -62,28 +55,4 @@ jobs:
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}
- name: Build/release Docker images (Self Host)
if: ${{ github.event.inputs.release_self_host == 'Y' }}
run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
yarn build
yarn build:docker:production
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }}
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}
- uses: azure/setup-helm@v1
id: install
# So, we need to inject the values into this
- run: yarn release:helm
- name: Run chart-releaser
uses: helm/chart-releaser-action@v1.1.0
with:
charts_dir: docs
env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }}

2
lerna.json

@ -1,5 +1,5 @@
{
"version": "0.9.164",
"version": "0.9.165-alpha.1",
"npmClient": "yarn",
"packages": [
"packages/*"

18
package.json

@ -32,6 +32,7 @@
"kill-port": "kill-port 4001",
"dev": "yarn run kill-port && lerna link && lerna run --parallel dev:builder --concurrency 1",
"dev:noserver": "lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "lerna run --parallel dev:builder --concurrency 1 --scope @budibase/worker --scope @budibase/server",
"test": "lerna run test",
"lint:eslint": "eslint packages",
"lint:prettier": "prettier --check \"packages/**/*.{js,svelte}\"",
@ -46,12 +47,17 @@
"build:docker:production": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION release && cd -",
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
"release:helm": "./scripts/release_helm_chart.sh",
"multi:enable": "lerna run multi:enable",
"multi:disable": "lerna run multi:disable",
"selfhost:enable": "lerna run selfhost:enable",
"selfhost:disable": "lerna run selfhost:disable",
"localdomain:enable": "lerna run localdomain:enable",
"localdomain:disable": "lerna run localdomain:disable",
"env:multi:enable": "lerna run env:multi:enable",
"env:multi:disable": "lerna run env:multi:disable",
"env:selfhost:enable": "lerna run env:selfhost:enable",
"env:selfhost:disable": "lerna run env:selfhost:disable",
"env:localdomain:enable": "lerna run env:localdomain:enable",
"env:localdomain:disable": "lerna run env:localdomain:disable",
"env:account:enable": "lerna run env:account:enable",
"env:account:disable": "lerna run env:account:disable",
"mode:self": "yarn env:selfhost:enable && yarn env:multi:disable && yarn env:account:disable",
"mode:cloud": "yarn env:selfhost:disable && yarn env:multi:enable && yarn env:account:disable",
"mode:account": "yarn mode:cloud && yarn env:account:enable",
"postinstall": "husky install"
}
}

2
packages/auth/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/auth",
"version": "0.9.164",
"version": "0.9.165-alpha.1",
"description": "Authentication middlewares for budibase builder and apps",
"main": "src/index.js",
"author": "Budibase",

5
packages/auth/src/middleware/authenticated.js

@ -42,8 +42,9 @@ module.exports = (
internal = false
if (authCookie) {
let error = null
const sessionId = authCookie.sessionId,
userId = authCookie.userId
const sessionId = authCookie.sessionId
const userId = authCookie.userId
const session = await getSession(userId, sessionId)
if (!session) {
error = "No session found"

15
packages/auth/src/security/sessions.js

@ -24,17 +24,24 @@ exports.createASession = async (userId, session) => {
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
}
exports.invalidateSessions = async (userId, sessionId = null) => {
exports.invalidateSessions = async (userId, sessionIds = null) => {
let sessions = []
if (sessionId) {
sessions.push({ key: makeSessionID(userId, sessionId) })
} else {
// If no sessionIds, get all the sessions for the user
if (!sessionIds) {
sessions = await getSessionsForUser(userId)
sessions.forEach(
session =>
(session.key = makeSessionID(session.userId, session.sessionId))
)
} else {
// use the passed array of sessionIds
sessions = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
sessions = sessions.map(sessionId => ({
key: makeSessionID(userId, sessionId),
}))
}
const client = await redis.getSessionClient()
const promises = []
for (let session of sessions) {

28
packages/auth/src/utils.js

@ -7,7 +7,7 @@ const {
const jwt = require("jsonwebtoken")
const { options } = require("./middleware/passport/jwt")
const { createUserEmailView } = require("./db/views")
const { Headers, UserStatus } = require("./constants")
const { Headers, UserStatus, Cookies } = require("./constants")
const {
getGlobalDB,
updateTenantId,
@ -19,6 +19,7 @@ const accounts = require("./cloud/accounts")
const { hash } = require("./hashing")
const userCache = require("./cache/user")
const env = require("./environment")
const { getUserSessions, invalidateSessions } = require("./security/sessions")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
@ -235,3 +236,28 @@ exports.saveUser = async (
}
}
}
/**
* Logs a user out from budibase. Re-used across account portal and builder.
*/
exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => {
if (!ctx) throw new Error("Koa context must be supplied to logout.")
const currentSession = this.getCookie(ctx, Cookies.Auth)
let sessions = await getUserSessions(userId)
if (keepActiveSession) {
sessions = sessions.filter(
session => session.sessionId !== currentSession.sessionId
)
} else {
// clear cookies
this.clearCookie(ctx, Cookies.Auth)
this.clearCookie(ctx, Cookies.CurrentApp)
}
await invalidateSessions(
userId,
sessions.map(({ sessionId }) => sessionId)
)
}

2
packages/bbui/package.json

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "0.9.164",
"version": "0.9.165-alpha.1",
"license": "AGPL-3.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",

5
packages/bbui/src/Table/CellRenderer.svelte

@ -5,11 +5,14 @@
import RelationshipRenderer from "./RelationshipRenderer.svelte"
import AttachmentRenderer from "./AttachmentRenderer.svelte"
import ArrayRenderer from "./ArrayRenderer.svelte"
import InternalRenderer from "./InternalRenderer.svelte"
export let row
export let schema
export let value
export let customRenderers = []
let renderer
const typeMap = {
boolean: BooleanRenderer,
datetime: DateTimeRenderer,
@ -20,7 +23,9 @@
number: StringRenderer,
longform: StringRenderer,
array: ArrayRenderer,
internal: InternalRenderer,
}
$: type = schema?.type ?? "string"
$: customRenderer = customRenderers?.find(x => x.column === schema?.name)
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer

28
packages/bbui/src/Table/InternalRenderer.svelte

@ -0,0 +1,28 @@
<script>
import Icon from "../Icon/Icon.svelte"
import { notifications } from "../Stores/notifications"
export let value
const onClick = e => {
e.stopPropagation()
copyToClipboard(value)
}
function copyToClipboard(value) {
navigator.clipboard.writeText(value).then(() => {
notifications.success("Copied")
})
}
</script>
<div on:click|stopPropagation={onClick}>
<Icon size="S" name="Copy" />
</div>
<style>
div {
overflow: hidden;
text-overflow: ellipsis;
width: 150px;
}
</style>

41
packages/builder/cypress/integration/addMultiOptionDatatype.spec.js

@ -0,0 +1,41 @@
context("Add Multi-Option Datatype", () => {
before(() => {
cy.login()
cy.createTestApp()
})
it("should create a new table, with data", () => {
cy.createTable("Multi Data")
cy.addColumn("Multi Data", "Test Data", "Multi-select", "1\n2\n3\n4\n5")
cy.addRowMultiValue(["1", "2", "3", "4", "5"])
})
it ("should add form with multi select picker, containing 5 options", () => {
cy.navigateToFrontend()
cy.wait(500)
// Add data provider
cy.get(`[data-cy="category-Data Provider"]`).click()
cy.get('[data-cy="dataSource-prop-control"]').click()
cy.get(".dropdown").contains("Multi Data").click()
cy.wait(500)
// Add Form with schema to match table
cy.addComponent("Form", "Form")
cy.get('[data-cy="dataSource-prop-control"').click()
cy.get(".dropdown").contains("Multi Data").click()
cy.wait(500)
// Add multi-select picker to form
cy.addComponent("Form", "Multi-select Picker").then((componentId) => {
cy.get('[data-cy="field-prop-control"]').type("Test Data").type('{enter}')
cy.wait(1000)
cy.getComponent(componentId).contains("Choose some options").click()
// Check picker has 5 items
cy.getComponent(componentId).find('li').should('have.length', 5)
// Select all items
for (let i = 1; i < 6; i++) {
cy.getComponent(componentId).find('li').contains(i).click()
}
// Check items have been selected
cy.getComponent(componentId).find('.spectrum-Picker-label').contains("(5)")
})
})
})

35
packages/builder/cypress/integration/addRadioButtons.spec.js

@ -0,0 +1,35 @@
context("Add Radio Buttons", () => {
before(() => {
cy.login()
cy.createTestApp()
})
it("should add Radio Buttons options picker on form, add data, and confirm", () => {
cy.navigateToFrontend()
cy.addComponent("Form", "Form")
cy.addComponent("Form", "Options Picker").then((componentId) => {
// Provide field setting
cy.get(`[data-cy="field-prop-control"]`).type("1")
// Open dropdown and select Radio buttons
cy.get(`[data-cy="optionsType-prop-control"]`).click().then(() => {
cy.get('.spectrum-Popover').contains('Radio buttons')
.wait(500)
.click()
})
const radioButtonsTotal = 3
// Add values and confirm total
addRadioButtonData(radioButtonsTotal)
cy.getComponent(componentId).find('[type="radio"]')
.should('have.length', radioButtonsTotal)
})
})
const addRadioButtonData = (totalRadioButtons) => {
cy.get(`[data-cy="optionsSource-prop-control"]`).click().then(() => {
cy.get('.spectrum-Popover').contains('Custom')
.wait(500)
.click()
})
cy.addCustomSourceOptions(totalRadioButtons)
}
})

7
packages/builder/cypress/integration/createTable.spec.js

@ -31,15 +31,16 @@ context("Create a Table", () => {
cy.contains("nameupdated ").should("contain", "nameupdated")
})
/*
it("edits a row", () => {
cy.contains("button", "Edit").click({ force: true })
cy.wait(1000)
cy.get(".spectrum-Modal input").clear()
cy.get(".spectrum-Modal input").type("RoverUpdated")
cy.get(".spectrum-Modal input").type("Updated")
cy.contains("Save").click()
cy.contains("RoverUpdated").should("have.text", "RoverUpdated")
cy.contains("Updated").should("have.text", "Updated")
})
*/
it("deletes a row", () => {
cy.get(".spectrum-Checkbox-input").check({ force: true })
cy.contains("Delete 1 row(s)").click()

8
packages/builder/cypress/integration/createView.spec.js

@ -62,7 +62,7 @@ context("Create a View", () => {
cy.get(".spectrum-Picker-label").eq(1).click()
cy.contains("age").click({ force: true })
cy.contains("Save").click()
cy.get(".spectrum-Button").contains("Save").click({ force: true })
})
cy.wait(1000)
@ -123,7 +123,7 @@ context("Create a View", () => {
cy.contains(".nav-item", "Test View")
.find(".actions .icon")
.click({ force: true })
cy.contains("Edit").click()
cy.get(".spectrum-Menu-itemLabel").contains("Edit").click()
cy.get(".modal-inner-wrapper").within(() => {
cy.get("input").type(" Updated")
cy.contains("Save").click()
@ -138,8 +138,8 @@ context("Create a View", () => {
.click({ force: true })
cy.contains("Delete").click()
cy.contains("Delete View").click()
cy.wait(1000)
cy.contains("TestView Updated").should("not.be.visible")
cy.wait(500)
cy.contains("TestView Updated").should("not.exist")
})
})

61
packages/builder/cypress/integration/customThemingProperties.spec.js

@ -0,0 +1,61 @@
context("Custom Theming Properties", () => {
before(() => {
cy.login()
cy.createTestApp()
cy.navigateToFrontend()
})
// Default Values
// Button roundness = Large
// Accent colour = Blue 600
// Accent colour (hover) = Blue 500
// Navigation bar background colour = Gray 100
// Navigation bar text colour = Gray 800
it("should reset the color property values", () => {
// Open Theme modal and change colours
cy.get(".spectrum-ActionButton-label").contains("Theme").click()
cy.get(".spectrum-Picker").contains("Large").click()
.parents()
.get(".spectrum-Menu-itemLabel").contains("None").click()
changeThemeColors()
// Reset colours
cy.get(".spectrum-Button-label").contains("Reset").click({force: true})
// Check values have reset
checkThemeColorDefaults()
})
const changeThemeColors = () => {
// Changes the theme colours
cy.get(".spectrum-FieldLabel").contains("Accent color")
.parent().find(".container.svelte-z3cm5a").click()
.find('[title="Red 400"]').click()
cy.get(".spectrum-FieldLabel").contains("Accent color (hover)")
.parent().find(".container.svelte-z3cm5a").click()
.find('[title="Orange 400"]').click()
cy.get(".spectrum-FieldLabel").contains("Navigation bar background color")
.parent().find(".container.svelte-z3cm5a").click()
.find('[title="Yellow 400"]').click()
cy.get(".spectrum-FieldLabel").contains("Navigation bar text color")
.parent().find(".container.svelte-z3cm5a").click()
.find('[title="Green 400"]').click()
}
const checkThemeColorDefaults = () => {
cy.get(".spectrum-FieldLabel").contains("Accent color")
.parent().find(".container.svelte-z3cm5a").click()
.get('[title="Blue 600"]').children().find('[aria-label="Checkmark"]')
cy.get(".spectrum-Dialog-grid").click()
cy.get(".spectrum-FieldLabel").contains("Accent color (hover)")
.parent().find(".container.svelte-z3cm5a").click()
.get('[title="Blue 500"]').children().find('[aria-label="Checkmark"]')
cy.get(".spectrum-Dialog-grid").click()
cy.get(".spectrum-FieldLabel").contains("Navigation bar background color")
.parent().find(".container.svelte-z3cm5a").click()
.get('[title="Gray 100"]').children().find('[aria-label="Checkmark"]')
cy.get(".spectrum-Dialog-grid").click()
cy.get(".spectrum-FieldLabel").contains("Navigation bar text color")
.parent().find(".container.svelte-z3cm5a").click()
.get('[title="Gray 800"]').children().find('[aria-label="Checkmark"]')
}
})

88
packages/builder/cypress/support/commands.js

@ -5,9 +5,13 @@
// ***********************************************
//
Cypress.on("uncaught:exception", () => {
return false
})
Cypress.Commands.add("login", () => {
cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
cy.wait(500)
cy.wait(2000)
cy.url().then(url => {
if (url.includes("builder/admin")) {
// create admin user
@ -22,6 +26,7 @@ Cypress.Commands.add("login", () => {
cy.get("input").first().type("test@test.com")
cy.get('input[type="password"]').type("test")
cy.get("button").first().click()
cy.wait(1000)
})
}
})
@ -100,24 +105,32 @@ Cypress.Commands.add("createTable", tableName => {
cy.contains(tableName).should("be.visible")
})
Cypress.Commands.add("addColumn", (tableName, columnName, type) => {
// Select Table
cy.selectTable(tableName)
cy.contains(".nav-item", tableName).click()
cy.contains("Create column").click()
Cypress.Commands.add(
"addColumn",
(tableName, columnName, type, multiOptions = null) => {
// Select Table
cy.selectTable(tableName)
cy.contains(".nav-item", tableName).click()
cy.contains("Create column").click()
// Configure column
cy.get(".spectrum-Modal").within(() => {
cy.get("input").first().type(columnName).blur()
// Configure column
cy.get(".spectrum-Modal").within(() => {
cy.get("input").first().type(columnName).blur()
// Unset table display column
cy.contains("display column").click({ force: true })
cy.get(".spectrum-Picker-label").click()
cy.contains(type).click()
// Unset table display column
cy.contains("display column").click({ force: true })
cy.get(".spectrum-Picker-label").click()
cy.contains(type).click()
cy.contains("Save Column").click()
})
})
// Add options for Multi-select Type
if (multiOptions !== null) {
cy.get(".spectrum-Textfield-input").eq(1).type(multiOptions)
}
cy.contains("Save Column").click()
})
}
)
Cypress.Commands.add("addRow", values => {
cy.contains("Create row").click()
@ -129,6 +142,21 @@ Cypress.Commands.add("addRow", values => {
})
})
Cypress.Commands.add("addRowMultiValue", values => {
cy.contains("Create row").click()
cy.get(".spectrum-Form-itemField")
.click()
.then(() => {
cy.get(".spectrum-Popover").within(() => {
for (let i = 0; i < values.length; i++) {
cy.get(".spectrum-Menu-item").eq(i).click()
}
})
cy.get(".spectrum-Dialog-grid").click("top")
cy.get(".spectrum-ButtonGroup").contains("Create").click()
})
})
Cypress.Commands.add("createUser", email => {
// quick hacky recorded way to create a user
cy.contains("Users").click()
@ -147,7 +175,9 @@ Cypress.Commands.add("addComponent", (category, component) => {
if (category) {
cy.get(`[data-cy="category-${category}"]`).click()
}
cy.get(`[data-cy="component-${component}"]`).click()
if (component) {
cy.get(`[data-cy="component-${component}"]`).click()
}
cy.wait(1000)
cy.location().then(loc => {
const params = loc.pathname.split("/")
@ -169,8 +199,11 @@ Cypress.Commands.add("getComponent", componentId => {
})
Cypress.Commands.add("navigateToFrontend", () => {
// Clicks on Design tab and then the Home nav item
cy.wait(1000)
cy.contains("Design").click()
cy.get(".spectrum-Search").type("/")
cy.get(".nav-item").contains("Home").click()
})
Cypress.Commands.add("createScreen", (screenName, route) => {
@ -193,3 +226,24 @@ Cypress.Commands.add("selectTable", tableName => {
cy.expandBudibaseConnection()
cy.contains(".nav-item", tableName).click()
})
Cypress.Commands.add("addCustomSourceOptions", totalOptions => {
cy.get(".spectrum-ActionButton")
.contains("Define Options")
.click()
.then(() => {
for (let i = 0; i < totalOptions; i++) {
// Add radio button options
cy.get(".spectrum-Button")
.contains("Add Option")
.click({ force: true })
.then(() => {
cy.wait(500)
cy.get("[placeholder='Label']").eq(i).type(i)
cy.get("[placeholder='Value']").eq(i).type(i)
})
}
// Save options
cy.get(".spectrum-Button").contains("Save").click({ force: true })
})
})

8
packages/builder/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "0.9.164",
"version": "0.9.165-alpha.1",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@ -65,10 +65,10 @@
}
},
"dependencies": {
"@budibase/bbui": "^0.9.164",
"@budibase/client": "^0.9.164",
"@budibase/bbui": "^0.9.165-alpha.1",
"@budibase/client": "^0.9.165-alpha.1",
"@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^0.9.164",
"@budibase/string-templates": "^0.9.165-alpha.1",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",

2
packages/builder/src/builderStore/store/automation/Automation.js

@ -14,7 +14,7 @@ export default class Automation {
}
addTestData(data) {
this.automation.testData = data
this.automation.testData = { ...this.automation.testData, ...data }
}
addBlock(block, idx) {

8
packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte

@ -4,10 +4,11 @@
import { externalActions } from "./ExternalActions"
export let blockIdx
export let blockComplete
let selectedAction
let actionVal
let actions = Object.entries($automationStore.blockDefinitions.ACTION)
export let blockComplete
const external = actions.reduce((acc, elm) => {
const [k, v] = elm
@ -36,10 +37,9 @@
actionVal.stepId,
actionVal
)
automationStore.actions.addBlockToAutomation(newBlock)
automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation,
blockIdx
$automationStore.selectedAutomation?.automation
)
}
</script>

2
packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte

@ -151,7 +151,7 @@
>
{/if}
<Button
disabled={disableAddButton ? true : !hasCompletedInputs}
disabled={!hasCompletedInputs}
on:click={() => {
setupToggled = false
actionModal.show()

17
packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte

@ -5,20 +5,24 @@
import { cloneDeep } from "lodash/fp"
let failedParse = null
let trigger = {}
let schemaProperties = {}
// clone the trigger so we're not mutating the reference
let trigger = cloneDeep(
$: trigger = cloneDeep(
$automationStore.selectedAutomation.automation.definition.trigger
)
let schemaProperties = Object.entries(trigger.schema.outputs.properties || {})
// get the outputs so we can define the fields
$: schemaProperties = Object.entries(trigger?.schema?.outputs?.properties)
if (!$automationStore.selectedAutomation.automation.testData) {
$automationStore.selectedAutomation.automation.testData = {}
}
// get the outputs so we can define the fields
// check to see if there is existing test data in the store
$: testData = $automationStore.selectedAutomation.automation.testData
$: testData = $automationStore.selectedAutomation.automation.testData || {}
// Check the schema to see if required fields have been entered
$: isError = !trigger.schema.outputs.required.every(
required => testData[required]
@ -41,7 +45,6 @@
showConfirmButton={true}
disabled={isError}
onConfirm={() => {
automationStore.actions.addTestDataToAutomation(testData)
automationStore.actions.test(
$automationStore.selectedAutomation?.automation,
testData
@ -53,7 +56,7 @@
><Tab icon="Form" title="Form">
<div class="tab-content-padding">
<AutomationBlockSetup
bind:testData
{testData}
{schemaProperties}
isTestModal
block={trigger}

27
packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte

@ -9,7 +9,10 @@
Label,
ActionButton,
Drawer,
Modal,
} from "@budibase/bbui"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import { automationStore } from "builderStore"
import { tables } from "stores/backend"
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
@ -27,13 +30,14 @@
import { buildLuceneQuery } from "helpers/lucene"
export let block
export let webhookModal
export let testData
export let schemaProperties
export let isTestModal = false
let webhookModal
let drawer
let tempFilters = lookForFilters(schemaProperties) || []
let fillWidth = true
$: stepId = block.stepId
$: bindings = getAvailableBindings(
block || $automationStore.selectedBlock,
@ -50,6 +54,18 @@
const onChange = debounce(
async function (e, key) {
if (isTestModal) {
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
if (stepId === "WEBHOOK") {
automationStore.actions.addTestDataToAutomation({
body: {
[key]: e.detail,
...$automationStore.selectedAutomation.automation.testData.body,
},
})
}
automationStore.actions.addTestDataToAutomation({
[key]: e.detail,
})
testData[key] = e.detail
} else {
block.inputs[key] = e.detail
@ -205,7 +221,10 @@
{bindings}
/>
{:else if value.customType === "webhookUrl"}
<WebhookDisplay value={inputData[key]} />
<WebhookDisplay
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "triggerSchema"}
<SchemaSetup on:change={e => onChange(e, key)} value={inputData[key]} />
{:else if value.customType === "code"}
@ -247,6 +266,10 @@
</div>
{/each}
</div>
<Modal bind:this={webhookModal} width="30%">
<CreateWebhookModal />
</Modal>
{#if stepId === "WEBHOOK"}
<Button secondary on:click={() => webhookModal.show()}>Set Up Webhook</Button>
{/if}

21
packages/builder/src/components/automation/SetupPanel/RowSelector.svelte

@ -5,16 +5,29 @@
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import { createEventDispatcher } from "svelte"
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
import { automationStore } from "builderStore"
const dispatch = createEventDispatcher()
export let value
export let bindings
$: table = $tables.list.find(table => table._id === value?.tableId)
$: schemaFields = Object.entries(table?.schema ?? {})
let table
let schemaFields
$: {
table = $tables.list.find(table => table._id === value?.tableId)
schemaFields = Object.entries(table?.schema ?? {})
// surface the schema so the user can see it in the json
schemaFields.map(([, schema]) => {
if (!schema.autocolumn && !value[schema.name]) {
value[schema.name] = ""
}
})
}
const onChangeTable = e => {
value = { tableId: e.detail }
value["tableId"] = e.detail
dispatch("change", value)
}
@ -69,6 +82,8 @@
label={field}
options={schema.constraints.inclusion}
/>
{:else if schema.type === "link"}
<LinkedRowSelector bind:linkedRows={value[field]} {schema} />
{:else if schema.type === "string" || schema.type === "number"}
{#if $automationStore.selectedAutomation.automation.testData}
<ModalBindableInput

2
packages/builder/src/components/automation/Shared/CreateWebhookModal.svelte

@ -1,7 +1,6 @@
<script>
import { Icon } from "@budibase/bbui"
import { automationStore } from "builderStore"
import { database } from "stores/backend"
import WebhookDisplay from "./WebhookDisplay.svelte"
import { ModalContent } from "@budibase/bbui"
import { onMount, onDestroy } from "svelte"
@ -12,7 +11,6 @@
let schemaURL
let propCount = 0
$: instanceId = $database._id
$: automation = $automationStore.selectedAutomation?.automation
onMount(async () => {

22
packages/builder/src/components/backend/DataTable/DataTable.svelte

@ -16,11 +16,29 @@
import { Pagination } from "@budibase/bbui"
let hideAutocolumns = true
let schema
$: isUsersTable = $tables.selected?._id === TableNames.USERS
$: schema = $tables.selected?.schema
$: type = $tables.selected?.type
$: isInternal = type !== "external"
$: {
schema = $tables.selected?.schema
// Manually add these as we don't want them to be 'real' auto-columns
schema._id = {
type: "internal",
editable: false,
displayName: "ID",
autocolumn: true,
}
if (isInternal) {
schema._rev = {
type: "internal",
editable: false,
displayName: "Revision",
autocolumn: true,
}
}
}
$: id = $tables.selected?._id
$: search = searchTable(id)
$: columnOptions = Object.keys($search.schema || {})

10
packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte

@ -32,6 +32,7 @@
const FORMULA_TYPE = FIELDS.FORMULA.type
const LINK_TYPE = FIELDS.LINK.type
const dispatch = createEventDispatcher()
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
const { hide } = getContext(Context.Modal)
let fieldDefinitions = cloneDeep(FIELDS)
@ -66,7 +67,11 @@
(field.type === LINK_TYPE && !field.tableId) ||
Object.keys($tables.draft?.schema ?? {}).some(
key => key !== originalName && key === field.name
)
) ||
columnNameInvalid
$: columnNameInvalid = PROHIBITED_COLUMN_NAMES.some(
name => field.name === name
)
// used to select what different options can be displayed for column type
$: canBeSearched =
@ -200,6 +205,9 @@
label="Name"
bind:value={field.name}
disabled={uneditable || (linkEditDisabled && field.type === LINK_TYPE)}
error={columnNameInvalid
? `${PROHIBITED_COLUMN_NAMES.join(", ")} are not allowed as column names`
: ""}
/>
<Select

19
packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte

@ -1,10 +1,18 @@
<script>
import { Label, Input, Layout, Toggle, Button } from "@budibase/bbui"
import {
Label,
Input,
Layout,
Toggle,
Button,
TextArea,
} from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import { capitalise } from "helpers"
export let integration
export let schema
let addButton
</script>
@ -29,6 +37,15 @@
<Label>{capitalise(configKey)}</Label>
<Toggle text="" bind:value={integration[configKey]} />
</div>
{:else if schema[configKey].type === "longForm"}
<div class="form-row">
<Label>{capitalise(configKey)}</Label>
<TextArea
type={schema[configKey].type}
on:change
bind:value={integration[configKey]}
/>
</div>
{:else}
<div class="form-row">
<Label>{capitalise(configKey)}</Label>

2
packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte

@ -60,7 +60,7 @@
</Modal>
<Modal bind:this={externalDatasourceModal}>
<DatasourceConfigModal {integration} />
<DatasourceConfigModal {integration} {modal} />
</Modal>
<Modal bind:this={modal}>

2
packages/builder/src/components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte

@ -64,7 +64,7 @@
? "Fetch tables from database"
: "Save and continue to query"}
cancelText="Back"
size="M"
size="L"
>
<Layout noPadding>
<Body size="XS"

6
packages/builder/src/components/integration/index.svelte

@ -17,9 +17,9 @@
$: urlDisplay =
schema.urlDisplay &&
`${datasource.config.url}${query.fields.path ?? ""}${
query.fields.queryString ?? ""
}`
`${datasource.config.url}${
query.fields.path ? "/" + query.fields.path : ""
}${query.fields.queryString ? "?" + query.fields.queryString : ""}`
function updateQuery({ detail }) {
query.fields[schema.type] = detail.value

2
packages/cli/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/cli",
"version": "0.9.164",
"version": "0.9.165-alpha.1",
"description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js",
"bin": {

6
packages/client/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "0.9.164",
"version": "0.9.165-alpha.1",
"license": "MPL-2.0",
"module": "dist/budibase-client.js",
"main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw"
},
"dependencies": {
"@budibase/bbui": "^0.9.164",
"@budibase/bbui": "^0.9.165-alpha.1",
"@budibase/standard-components": "^0.9.139",
"@budibase/string-templates": "^0.9.164",
"@budibase/string-templates": "^0.9.165-alpha.1",
"regexparam": "^1.3.0",
"shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5"

2
packages/server/nodemon.json

@ -1,5 +1,5 @@
{
"watch": ["src"],
"watch": ["src", "../auth"],
"ext": "js,ts,json",
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js"],
"exec": "ts-node src/index.ts"

22
packages/server/package.json

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "0.9.164",
"version": "0.9.165-alpha.1",
"description": "Budibase Web Server",
"main": "src/index.js",
"repository": {
@ -24,12 +24,14 @@
"lint": "eslint --fix src/",
"lint:fix": "yarn run format && yarn run lint",
"initialise": "node scripts/initialise.js",
"multi:enable": "node scripts/multiTenancy.js enable",
"multi:disable": "node scripts/multiTenancy.js disable",
"selfhost:enable": "node scripts/selfhost.js enable",
"selfhost:disable": "node scripts/selfhost.js disable",
"localdomain:enable": "node scripts/localdomain.js enable",
"localdomain:disable": "node scripts/localdomain.js disable"
"env:multi:enable": "node scripts/multiTenancy.js enable",
"env:multi:disable": "node scripts/multiTenancy.js disable",
"env:selfhost:enable": "node scripts/selfhost.js enable",
"env:selfhost:disable": "node scripts/selfhost.js disable",
"env:localdomain:enable": "node scripts/localdomain.js enable",
"env:localdomain:disable": "node scripts/localdomain.js disable",
"env:account:enable": "node scripts/account.js enable",
"env:account:disable": "node scripts/account.js disable"
},
"jest": {
"preset": "ts-jest",
@ -66,9 +68,9 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/auth": "^0.9.164",
"@budibase/client": "^0.9.164",
"@budibase/string-templates": "^0.9.164",
"@budibase/auth": "^0.9.165-alpha.1",
"@budibase/client": "^0.9.165-alpha.1",
"@budibase/string-templates": "^0.9.165-alpha.1",
"@elastic/elasticsearch": "7.10.0",
"@koa/router": "8.0.0",
"@sendgrid/mail": "7.1.1",

8
packages/server/scripts/account.js

@ -0,0 +1,8 @@
#!/usr/bin/env node
const updateDotEnv = require("update-dotenv")
const arg = process.argv.slice(2)[0]
updateDotEnv({
DISABLE_ACCOUNT_PORTAL: arg === "enable" ? "" : "1",
}).then(() => console.log("Updated server!"))

2
packages/server/src/automations/steps/discord.js

@ -77,7 +77,7 @@ exports.run = async function ({ inputs }) {
const { status, message } = await getFetchResponse(response)
return {
httpStatus: status,
success: status === 200,
success: status === 200 || status === 204,
response: message,
}
}

7
packages/server/src/automations/tests/automation.spec.js

@ -1,5 +1,12 @@
jest.mock("../../utilities/usageQuota")
jest.mock("../thread")
jest.mock("../../utilities/redis", () => ({
init: jest.fn(),
checkTestFlag: () => {
return false
},
}))
jest.spyOn(global.console, "error")
require("../../environment")

1
packages/server/src/automations/triggerInfo/app.js

@ -22,6 +22,7 @@ exports.definition = {
fields: {
type: "object",
description: "Fields submitted from the app frontend",
customType: "triggerSchema",
},
},
required: ["fields"],

22
packages/server/src/automations/triggers.js

@ -81,16 +81,20 @@ exports.externalTrigger = async function (
params,
{ getResponses } = {}
) {
if (automation.definition != null && automation.definition.trigger != null) {
if (automation.definition.trigger.stepId === "APP") {
// values are likely to be submitted as strings, so we shall convert to correct type
const coercedFields = {}
const fields = automation.definition.trigger.inputs.fields
for (let key of Object.keys(fields)) {
coercedFields[key] = coerce(params.fields[key], fields[key])
}
params.fields = coercedFields
if (
automation.definition != null &&
automation.definition.trigger != null &&
automation.definition.trigger.stepId === definitions.APP.stepId &&
automation.definition.trigger.stepId === "APP" &&
!checkTestFlag(automation._id)
) {
// values are likely to be submitted as strings, so we shall convert to correct type
const coercedFields = {}
const fields = automation.definition.trigger.inputs.fields
for (let key of Object.keys(fields)) {
coercedFields[key] = coerce(params.fields[key], fields[key])
}
params.fields = coercedFields
}
const data = { automation, event: params }
if (getResponses) {

2
packages/server/src/definitions/datasource.ts

@ -20,12 +20,14 @@ export enum QueryTypes {
export enum DatasourceFieldTypes {
STRING = "string",
LONGFORM = "longForm",
BOOLEAN = "boolean",
NUMBER = "number",
PASSWORD = "password",
LIST = "list",
OBJECT = "object",
JSON = "json",
FILE = "file",
}
export enum SourceNames {

19
packages/server/src/integrations/postgres.ts

@ -28,6 +28,8 @@ module PostgresModule {
user: string
password: string
ssl?: boolean
ca?: string
rejectUnauthorized?: boolean
}
const SCHEMA: Integration = {
@ -67,6 +69,16 @@ module PostgresModule {
default: false,
required: false,
},
rejectUnauthorized: {
type: DatasourceFieldTypes.BOOLEAN,
default: false,
required: false,
},
ca: {
type: DatasourceFieldTypes.LONGFORM,
default: false,
required: false,
},
},
query: {
create: {
@ -144,7 +156,12 @@ module PostgresModule {
let newConfig = {
...this.config,
ssl: this.config.ssl ? { rejectUnauthorized: true } : undefined,
ssl: this.config.ssl
? {
rejectUnauthorized: this.config.rejectUnauthorized,
ca: this.config.ca,
}
: undefined,
}
if (!this.pool) {
this.pool = new Pool(newConfig)

14
packages/server/src/integrations/rest.ts

@ -152,13 +152,17 @@ module RestModule {
}
}
getUrl(path: string, queryString: string): string {
return `${this.config.url}/${path}?${queryString}`
}
async create({ path = "", queryString = "", headers = {}, json = {} }) {
this.headers = {
...this.config.defaultHeaders,
...headers,
}
const response = await fetch(this.config.url + path + queryString, {
const response = await fetch(this.getUrl(path, queryString), {
method: "POST",
headers: this.headers,
body: JSON.stringify(json),
@ -173,7 +177,7 @@ module RestModule {
...headers,
}
const response = await fetch(this.config.url + path + queryString, {
const response = await fetch(this.getUrl(path, queryString), {
headers: this.headers,
})
@ -186,7 +190,7 @@ module RestModule {
...headers,
}
const response = await fetch(this.config.url + path + queryString, {
const response = await fetch(this.getUrl(path, queryString), {
method: "POST",
headers: this.headers,
body: JSON.stringify(json),
@ -201,7 +205,7 @@ module RestModule {
...headers,
}
const response = await fetch(this.config.url + path + queryString, {
const response = await fetch(this.getUrl(path, queryString), {
method: "PATCH",
headers: this.headers,
body: JSON.stringify(json),
@ -216,7 +220,7 @@ module RestModule {
...headers,
}
const response = await fetch(this.config.url + path + queryString, {
const response = await fetch(this.getUrl(path, queryString), {
method: "DELETE",
headers: this.headers,
})

70
packages/server/src/integrations/tests/rest.spec.js

@ -1,98 +1,100 @@
jest.mock("node-fetch", () => jest.fn(() => ({ json: jest.fn(), text: jest.fn() })))
jest.mock("node-fetch", () =>
jest.fn(() => ({ json: jest.fn(), text: jest.fn() }))
)
const fetch = require("node-fetch")
const RestIntegration = require("../rest")
class TestConfiguration {
constructor(config = {}) {
this.integration = new RestIntegration.integration(config)
this.integration = new RestIntegration.integration(config)
}
}
describe("REST Integration", () => {
const BASE_URL = "https://myapi.com"
let config
let config
beforeEach(() => {
config = new TestConfiguration({
url: BASE_URL
url: BASE_URL,
})
})
it("calls the create method with the correct params", async () => {
const query = {
path: "/api",
queryString: "?test=1",
path: "api",
queryString: "test=1",
headers: {
Accept: "application/json"
Accept: "application/json",
},
json: {
name: "test"
}
name: "test",
},
}
const response = await config.integration.create(query)
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
method: "POST",
body: "{\"name\":\"test\"}",
body: '{"name":"test"}',
headers: {
Accept: "application/json"
}
Accept: "application/json",
},
})
})
it("calls the read method with the correct params", async () => {
const query = {
path: "/api",
queryString: "?test=1",
path: "api",
queryString: "test=1",
headers: {
Accept: "text/html"
}
Accept: "text/html",
},
}
const response = await config.integration.read(query)
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
headers: {
Accept: "text/html"
}
Accept: "text/html",
},
})
})
it("calls the update method with the correct params", async () => {
const query = {
path: "/api",
queryString: "?test=1",
path: "api",
queryString: "test=1",
headers: {
Accept: "application/json"
Accept: "application/json",
},
json: {
name: "test"
}
name: "test",
},
}
const response = await config.integration.update(query)
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
method: "POST",
body: "{\"name\":\"test\"}",
body: '{"name":"test"}',
headers: {
Accept: "application/json"
}
Accept: "application/json",
},
})
})
it("calls the delete method with the correct params", async () => {
const query = {
path: "/api",
queryString: "?test=1",
path: "api",
queryString: "test=1",
headers: {
Accept: "application/json"
Accept: "application/json",
},
json: {
name: "test"
}
name: "test",
},
}
const response = await config.integration.delete(query)
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
method: "DELETE",
headers: {
Accept: "application/json"
}
Accept: "application/json",
},
})
})
})
})

2
packages/string-templates/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
"version": "0.9.164",
"version": "0.9.165-alpha.1",
"description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs",
"module": "dist/bundle.mjs",

3
packages/worker/nodemon.json

@ -0,0 +1,3 @@
{
"watch": ["src", "../auth"]
}

22
packages/worker/package.json

@ -1,7 +1,7 @@
{
"name": "@budibase/worker",
"email": "hi@budibase.com",
"version": "0.9.164",
"version": "0.9.165-alpha.1",
"description": "Budibase background service",
"main": "src/index.js",
"repository": {
@ -15,20 +15,22 @@
"run:docker": "node src/index.js",
"build:docker": "docker build . -t worker-service",
"dev:stack:init": "node ./scripts/dev/manage.js init",
"dev:builder": "npm run dev:stack:init && nodemon src/index.js",
"dev:builder": "npm run dev:stack:init && nodemon",
"test": "jest --runInBand",
"multi:enable": "node scripts/multiTenancy.js enable",
"multi:disable": "node scripts/multiTenancy.js disable",
"selfhost:enable": "node scripts/selfhost.js enable",
"selfhost:disable": "node scripts/selfhost.js disable",
"localdomain:enable": "node scripts/localdomain.js enable",
"localdomain:disable": "node scripts/localdomain.js disable"
"env:multi:enable": "node scripts/multiTenancy.js enable",
"env:multi:disable": "node scripts/multiTenancy.js disable",
"env:selfhost:enable": "node scripts/selfhost.js enable",
"env:selfhost:disable": "node scripts/selfhost.js disable",
"env:localdomain:enable": "node scripts/localdomain.js enable",
"env:localdomain:disable": "node scripts/localdomain.js disable",
"env:account:enable": "node scripts/account.js enable",
"env:account:disable": "node scripts/account.js disable"
},
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/auth": "^0.9.164",
"@budibase/string-templates": "^0.9.164",
"@budibase/auth": "^0.9.165-alpha.1",
"@budibase/string-templates": "^0.9.165-alpha.1",
"@koa/router": "^8.0.0",
"@techpass/passport-openidconnect": "^0.3.0",
"aws-sdk": "^2.811.0",

8
packages/worker/scripts/account.js

@ -0,0 +1,8 @@
#!/usr/bin/env node
const updateDotEnv = require("update-dotenv")
const arg = process.argv.slice(2)[0]
updateDotEnv({
DISABLE_ACCOUNT_PORTAL: arg === "enable" ? "" : "1",
}).then(() => console.log("Updated worker!"))

4
packages/worker/src/api/controllers/global/auth.js

@ -14,6 +14,7 @@ const {
isMultiTenant,
} = require("@budibase/auth/tenancy")
const env = require("../../../environment")
const { platformLogout } = require("../../../../../auth/src/utils")
function googleCallbackUrl(config) {
// incase there is a callback URL from before
@ -121,8 +122,7 @@ exports.resetUpdate = async ctx => {
}
exports.logout = async ctx => {
clearCookie(ctx, Cookies.Auth)
clearCookie(ctx, Cookies.CurrentApp)
await platformLogout({ ctx, userId: ctx.user._id })
ctx.body = { message: "User logged out." }
}

28
packages/worker/src/api/controllers/global/users.js

@ -3,7 +3,8 @@ const {
StaticDatabases,
generateNewUsageQuotaDoc,
} = require("@budibase/auth/db")
const { hash, getGlobalUserByEmail, saveUser } = require("@budibase/auth").utils
const { hash, getGlobalUserByEmail, saveUser, platformLogout } =
require("@budibase/auth").utils
const { EmailTemplatePurpose } = require("../../../constants")
const { checkInviteCode } = require("../../../utilities/redis")
const { sendEmail } = require("../../../utilities/email")
@ -111,14 +112,16 @@ exports.destroy = async ctx => {
const db = getGlobalDB()
const dbUser = await db.get(ctx.params.id)
// root account holder can't be deleted from inside budibase
const email = dbUser.email
const account = await accounts.getAccount(email)
if (account) {
if (email === ctx.user.email) {
ctx.throw(400, 'Please visit "Account" to delete this user')
} else {
ctx.throw(400, "Account holder cannot be deleted")
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
// root account holder can't be deleted from inside budibase
const email = dbUser.email
const account = await accounts.getAccount(email)
if (account) {
if (email === ctx.user.email) {
ctx.throw(400, 'Please visit "Account" to delete this user')
} else {
ctx.throw(400, "Account holder cannot be deleted")
}
}
}
@ -171,7 +174,14 @@ exports.updateSelf = async ctx => {
const db = getGlobalDB()
const user = await db.get(ctx.user._id)
if (ctx.request.body.password) {
// changing password
ctx.request.body.password = await hash(ctx.request.body.password)
// Log all other sessions out apart from the current one
await platformLogout({
ctx,
userId: ctx.user._id,
keepActiveSession: true,
})
}
// don't allow sending up an ID/Rev, always use the existing one
delete ctx.request.body._id

Loading…
Cancel
Save