Merge branch 'master' of github.com:Budibase/budibase into labday/sqs
This commit is contained in:
commit
d87c7a1c4a
|
@ -1,194 +0,0 @@
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
"README.md"
|
|
||||||
],
|
|
||||||
"imageSize": 100,
|
|
||||||
"commit": false,
|
|
||||||
"contributors": [
|
|
||||||
{
|
|
||||||
"login": "shogunpurple",
|
|
||||||
"name": "Martin McKeaveney",
|
|
||||||
"avatar_url": "https://avatars1.githubusercontent.com/u/11256663?v=4",
|
|
||||||
"profile": "http://martinmck.com",
|
|
||||||
"contributions": [
|
|
||||||
"code",
|
|
||||||
"doc",
|
|
||||||
"test",
|
|
||||||
"infra"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "mike12345567",
|
|
||||||
"name": "Michael Drury",
|
|
||||||
"avatar_url": "https://avatars2.githubusercontent.com/u/4407001?v=4",
|
|
||||||
"profile": "http://www.michaeldrury.co.uk/",
|
|
||||||
"contributions": [
|
|
||||||
"doc",
|
|
||||||
"code",
|
|
||||||
"test",
|
|
||||||
"infra"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "aptkingston",
|
|
||||||
"name": "Andrew Kingston",
|
|
||||||
"avatar_url": "https://avatars3.githubusercontent.com/u/9075550?v=4",
|
|
||||||
"profile": "https://github.com/aptkingston",
|
|
||||||
"contributions": [
|
|
||||||
"doc",
|
|
||||||
"code",
|
|
||||||
"test",
|
|
||||||
"design"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "mjashanks",
|
|
||||||
"name": "Michael Shanks",
|
|
||||||
"avatar_url": "https://avatars3.githubusercontent.com/u/3524181?v=4",
|
|
||||||
"profile": "https://budibase.com/",
|
|
||||||
"contributions": [
|
|
||||||
"doc",
|
|
||||||
"code",
|
|
||||||
"test"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "kevmodrome",
|
|
||||||
"name": "Kevin Åberg Kultalahti",
|
|
||||||
"avatar_url": "https://avatars3.githubusercontent.com/u/534488?v=4",
|
|
||||||
"profile": "https://github.com/kevmodrome",
|
|
||||||
"contributions": [
|
|
||||||
"doc",
|
|
||||||
"code",
|
|
||||||
"test"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "joebudi",
|
|
||||||
"name": "Joe",
|
|
||||||
"avatar_url": "https://avatars2.githubusercontent.com/u/49767913?v=4",
|
|
||||||
"profile": "https://www.budibase.com/",
|
|
||||||
"contributions": [
|
|
||||||
"doc",
|
|
||||||
"code",
|
|
||||||
"content",
|
|
||||||
"design"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Rory-Powell",
|
|
||||||
"name": "Rory Powell",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/8755148?v=4",
|
|
||||||
"profile": "https://github.com/Rory-Powell",
|
|
||||||
"contributions": [
|
|
||||||
"code",
|
|
||||||
"doc",
|
|
||||||
"test"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "PClmnt",
|
|
||||||
"name": "Peter Clement",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/5665926?v=4",
|
|
||||||
"profile": "https://github.com/PClmnt",
|
|
||||||
"contributions": [
|
|
||||||
"code",
|
|
||||||
"doc",
|
|
||||||
"test"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Conor-Mack",
|
|
||||||
"name": "Conor_Mack",
|
|
||||||
"avatar_url": "https://avatars1.githubusercontent.com/u/36074859?v=4",
|
|
||||||
"profile": "https://github.com/Conor-Mack",
|
|
||||||
"contributions": [
|
|
||||||
"code",
|
|
||||||
"test"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "pngwn",
|
|
||||||
"name": "pngwn",
|
|
||||||
"avatar_url": "https://avatars1.githubusercontent.com/u/12937446?v=4",
|
|
||||||
"profile": "https://github.com/pngwn",
|
|
||||||
"contributions": [
|
|
||||||
"code",
|
|
||||||
"test"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "HugoLd",
|
|
||||||
"name": "HugoLd",
|
|
||||||
"avatar_url": "https://avatars0.githubusercontent.com/u/26521848?v=4",
|
|
||||||
"profile": "https://github.com/HugoLd",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "victoriasloan",
|
|
||||||
"name": "victoriasloan",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/9913651?v=4",
|
|
||||||
"profile": "https://github.com/victoriasloan",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "yashank09",
|
|
||||||
"name": "yashank09",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/37672190?v=4",
|
|
||||||
"profile": "https://github.com/yashank09",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "SOVLOOKUP",
|
|
||||||
"name": "SOVLOOKUP",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/53158137?v=4",
|
|
||||||
"profile": "https://github.com/SOVLOOKUP",
|
|
||||||
"contributions": [
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "seoulaja",
|
|
||||||
"name": "seoulaja",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/15101654?v=4",
|
|
||||||
"profile": "https://github.com/seoulaja",
|
|
||||||
"contributions": [
|
|
||||||
"translation"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "mslourens",
|
|
||||||
"name": "Maurits Lourens",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/1907152?v=4",
|
|
||||||
"profile": "https://github.com/mslourens",
|
|
||||||
"contributions": [
|
|
||||||
"test",
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"login": "Rory-Powell",
|
|
||||||
"name": "Rory Powell",
|
|
||||||
"avatar_url": "https://avatars.githubusercontent.com/u/8755148?v=4",
|
|
||||||
"profile": "https://github.com/Rory-Powell",
|
|
||||||
"contributions": [
|
|
||||||
"infra",
|
|
||||||
"test",
|
|
||||||
"code"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"contributorsPerLine": 7,
|
|
||||||
"projectName": "budibase",
|
|
||||||
"projectOwner": "Budibase",
|
|
||||||
"repoType": "github",
|
|
||||||
"repoHost": "https://github.com",
|
|
||||||
"skipCi": true,
|
|
||||||
"commitConvention": "none"
|
|
||||||
}
|
|
|
@ -8,3 +8,6 @@ packages/backend-core/coverage
|
||||||
packages/server/client
|
packages/server/client
|
||||||
packages/builder/.routify
|
packages/builder/.routify
|
||||||
packages/sdk/sdk
|
packages/sdk/sdk
|
||||||
|
packages/account-portal/packages/server/build
|
||||||
|
packages/account-portal/packages/ui/.routify
|
||||||
|
packages/account-portal/packages/ui/build
|
|
@ -45,6 +45,16 @@
|
||||||
"no-prototype-builtins": "off",
|
"no-prototype-builtins": "off",
|
||||||
"local-rules/no-budibase-imports": "error"
|
"local-rules/no-budibase-imports": "error"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"packages/builder/**/*",
|
||||||
|
"packages/client/**/*",
|
||||||
|
"packages/frontend-core/**/*"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"no-console": ["error", { "allow": ["warn", "error", "debug"] } ]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
|
|
@ -1,139 +1,45 @@
|
||||||
# Budibase CI Pipelines
|
# Budibase CI Pipelines
|
||||||
|
|
||||||
Welcome to the budibase CI pipelines directory. This document details what each of the CI pipelines are for, and come common combinations.
|
Welcome to the Budibase CI pipelines directory. This document details what each of the CI pipelines are for, and come common combinations.
|
||||||
|
|
||||||
## All CI Pipelines
|
## All CI Pipelines
|
||||||
|
|
||||||
### Note
|
|
||||||
|
|
||||||
- When running workflow dispatch jobs, ensure you always run them off the `master` branch. It defaults to `develop`, so double check before running any jobs. The exception to this case is the `deploy-release` job which requires the develop branch.
|
|
||||||
|
|
||||||
### Standard CI Build Job (budibase_ci.yml)
|
### Standard CI Build Job (budibase_ci.yml)
|
||||||
|
|
||||||
Triggers:
|
Triggers:
|
||||||
|
|
||||||
- PR or push to develop
|
|
||||||
- PR or push to master
|
- PR or push to master
|
||||||
|
|
||||||
The standard CI Build job is what runs when you raise a PR to develop or master.
|
The standard CI Build job is what runs when you raise a PR to master.
|
||||||
|
|
||||||
- Installs all dependencies,
|
- Installs all dependencies,
|
||||||
- builds the project
|
- builds the project
|
||||||
- run the unit tests
|
- run the unit tests
|
||||||
- Generate test coverage metrics with codecov
|
- Generate test coverage metrics with codecov
|
||||||
- Run the integration tests
|
- Run the integration tests
|
||||||
|
- Check that the pro and account portal submodules are pointing to the lastest master head
|
||||||
|
|
||||||
### Release Develop Job (release-develop.yml)
|
### Release Job (tag-release.yml)
|
||||||
|
|
||||||
Triggers:
|
Triggers:
|
||||||
|
|
||||||
- Push to develop
|
- Manually triggered
|
||||||
|
|
||||||
The job responsible for building, tagging and pushing docker images out to the test and release environments.
|
This job is responsible for building and pushing all the production services, packages and images. This is done via [budibase-deploys](https://github.com/Budibase/budibase-deploys/actions/workflows/release.yml).
|
||||||
|
|
||||||
- Installs all dependencies
|
An input is required, indicating if the new version will be a `patch`, `minor` or `major` bump.
|
||||||
- builds the project
|
|
||||||
- run the unit tests
|
|
||||||
- publish the budibase JS packages under a prerelease tag to NPM
|
|
||||||
- build, tag and push docker images under the `develop` tag to docker hub
|
|
||||||
|
|
||||||
These images will then be pulled by the test and release environments, updating the latest automatically. Discord notifications are sent to the #infra channel when this occurs.
|
More documentation can be found in here: https://budibase.atlassian.net/wiki/spaces/DEVOPS/pages/347930625/Production+release
|
||||||
|
|
||||||
### Release Job (release.yml)
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
- Push to master
|
|
||||||
|
|
||||||
This job is responsible for building and pushing the latest code to NPM and docker hub, so that it can be deployed.
|
|
||||||
|
|
||||||
- Installs all dependencies
|
|
||||||
- builds the project
|
|
||||||
- run the unit tests
|
|
||||||
- publish the budibase JS packages under a release tag to NPM (always incremented by patch versions)
|
|
||||||
- build, tag and push docker images under the `v.x.x.x` (the tag of the NPM release) tag to docker hub
|
|
||||||
|
|
||||||
### Release Selfhost Job (release-selfhost.yml)
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
- Manual Workflow Dispatch Trigger
|
|
||||||
|
|
||||||
This job is responsible for delivering the latest version of budibase to those that are self-hosting.
|
|
||||||
|
|
||||||
This job relies on the release job to have run first, so the latest image is pushed to dockerhub. This job then will pull the latest version from `lerna.json` and try to find an image in dockerhub corresponding to that version. For example, if the version in `lerna.json` is `1.0.0`:
|
|
||||||
|
|
||||||
- Pull the images for all budibase services tagged `v1.0.0` from dockerhub
|
|
||||||
- Tag these images as `latest`
|
|
||||||
- Push them back to dockerhub. This now means anyone who pulls `latest` (self hosters using docker-compose) will get the latest version.
|
|
||||||
- Build and release the budibase helm chart for kubernetes users
|
|
||||||
- Perform a github release with the latest version. You can see previous releases here (https://github.com/Budibase/budibase/releases)
|
|
||||||
|
|
||||||
### Deploy Release (deploy-release.yml)
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
- Manual Workflow Dispatch Trigger
|
|
||||||
|
|
||||||
This job is responsible for deploying to our release, cloud kubernetes environment. You must run the release job first, to ensure that the latest images have been built and pushed to docker hub. After kicking off this job, the following will occur:
|
|
||||||
|
|
||||||
- Checks out the release branch
|
|
||||||
- Pulls the latest `values.yaml` from budibase infra, a private repo containing budibases infrastructure configuration
|
|
||||||
- Gets the latest budibase version from `lerna.json`, if it hasn't been specified in the workflow when you kicked it off
|
|
||||||
- Configures AWS Credentials
|
|
||||||
- Deploys the helm chart in the budibase repo to our preproduction EKS cluster, injecting the `values.yaml` we pulled from budibase-infra
|
|
||||||
- Fires off a discord webhook in the #infra channel to show that the deployment completely successfully.
|
|
||||||
|
|
||||||
### Deploy Preprod (deploy-preprod.yml)
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
- Manual Workflow Dispatch Trigger
|
|
||||||
|
|
||||||
This job is responsible for deploying to our preprod, cloud kubernetes environment. You must run the release job first, to ensure that the latest images have been built and pushed to docker hub. After kicking off this job, the following will occur:
|
|
||||||
|
|
||||||
- Checks out the master branch
|
|
||||||
- Pulls the latest `values.yaml` from budibase infra, a private repo containing budibases infrastructure configuration
|
|
||||||
- Gets the latest budibase version from `lerna.json`, if it hasn't been specified in the workflow when you kicked it off
|
|
||||||
- Configures AWS Credentials
|
|
||||||
- Deploys the helm chart in the budibase repo to our preprod EKS cluster, injecting the `values.yaml` we pulled from budibase-infra
|
|
||||||
- Fires off a discord webhook in the #infra channel to show that the deployment completely successfully.
|
|
||||||
|
|
||||||
### Deploy Production (deploy-cloud.yml)
|
|
||||||
|
|
||||||
Triggers:
|
|
||||||
|
|
||||||
- Manual Workflow Dispatch Trigger
|
|
||||||
|
|
||||||
This job is responsible for deploying to our production, cloud kubernetes environment. You must run the release job first, to ensure that the latest images have been built and pushed to docker hub. You can also manually enter a version number for this job, so you can perform rollbacks or upgrade to a specific version. After kicking off this job, the following will occur:
|
|
||||||
|
|
||||||
- Checks out the master branch
|
|
||||||
- Pulls the latest `values.yaml` from budibase infra, a private repo containing budibases infrastructure configuration
|
|
||||||
- Gets the latest budibase version from `lerna.json`, if it hasn't been specified in the workflow when you kicked it off
|
|
||||||
- Configures AWS Credentials
|
|
||||||
- Deploys the helm chart in the budibase repo to our production EKS cluster, injecting the `values.yaml` we pulled from budibase-infra
|
|
||||||
- Fires off a discord webhook in the #infra channel to show that the deployment completely successfully.
|
|
||||||
|
|
||||||
## Common Workflows
|
## Common Workflows
|
||||||
|
|
||||||
### Deploy Changes to Production (Release)
|
### Deploy Changes to Production (Release)
|
||||||
|
|
||||||
- Merge `develop` into `master`
|
- Merge your changes into `master`
|
||||||
- Wait for budibase CI job and release job to run
|
- Run `tag-release.yml`
|
||||||
- Run cloud deploy job
|
- Check the progress in [budibase-deploys](https://github.com/Budibase/budibase-deploys/actions/workflows/release.yml)
|
||||||
- Run release selfhost job
|
|
||||||
|
|
||||||
### Deploy Changes to Production (Hotfix)
|
|
||||||
|
|
||||||
- Branch off `master`
|
|
||||||
- Perform your hotfix
|
|
||||||
- Merge back into `master`
|
|
||||||
- Wait for budibase CI job and release job to run
|
|
||||||
- Run cloud deploy job
|
|
||||||
- Run release selfhost job
|
|
||||||
|
|
||||||
### Rollback A Bad Cloud Deployment
|
### Rollback A Bad Cloud Deployment
|
||||||
|
|
||||||
- Kick off cloud deploy job
|
Rollback documentation can be found in here.
|
||||||
- Ensure you are running off master
|
https://budibase.atlassian.net/wiki/spaces/DEVOPS/pages/347930625/Production+release#Rollback
|
||||||
- Enter the version number of the last known good version of budibase. For example `1.0.0`
|
|
||||||
|
|
|
@ -33,15 +33,15 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
cache: yarn
|
cache: yarn
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
|
@ -50,16 +50,16 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
cache: yarn
|
cache: yarn
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
|
|
||||||
|
@ -76,20 +76,32 @@ jobs:
|
||||||
yarn check:types
|
yarn check:types
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
helm-lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Use Node.js 20.x
|
||||||
|
uses: azure/setup-helm@v3
|
||||||
|
- run: cd charts/budibase && helm lint .
|
||||||
|
|
||||||
test-libraries:
|
test-libraries:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
cache: yarn
|
cache: yarn
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
- name: Test
|
- name: Test
|
||||||
|
@ -104,16 +116,16 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
cache: yarn
|
cache: yarn
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
- name: Test worker
|
- name: Test worker
|
||||||
|
@ -128,16 +140,16 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
cache: yarn
|
cache: yarn
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
- name: Test server
|
- name: Test server
|
||||||
|
@ -153,16 +165,16 @@ jobs:
|
||||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
cache: yarn
|
cache: yarn
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
- name: Test
|
- name: Test
|
||||||
|
@ -177,15 +189,15 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
cache: yarn
|
cache: yarn
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
- name: Build packages
|
- name: Build packages
|
||||||
|
@ -204,10 +216,10 @@ jobs:
|
||||||
|
|
||||||
check-pro-submodule:
|
check-pro-submodule:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase')
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
@ -237,7 +249,7 @@ jobs:
|
||||||
|
|
||||||
- name: Check submodule merged to base branch
|
- name: Check submodule merged to base branch
|
||||||
if: ${{ steps.get_pro_commits.outputs.base_commit != '' }}
|
if: ${{ steps.get_pro_commits.outputs.base_commit != '' }}
|
||||||
uses: actions/github-script@v4
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
|
@ -246,7 +258,57 @@ jobs:
|
||||||
|
|
||||||
if (submoduleCommit !== baseCommit) {
|
if (submoduleCommit !== baseCommit) {
|
||||||
console.error('Submodule commit does not match the latest commit on the "${{ steps.get_pro_commits.outputs.target_branch }}" branch.');
|
console.error('Submodule commit does not match the latest commit on the "${{ steps.get_pro_commits.outputs.target_branch }}" branch.');
|
||||||
console.error('Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/develop/docs/getting_started.md')
|
console.error('Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/master/docs/getting_started.md')
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.log('All good, the submodule had been merged and setup correctly!')
|
||||||
|
}
|
||||||
|
|
||||||
|
check-accountportal-submodule:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: inputs.run_as_oss != true && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase')
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo and submodules
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Check account portal commit
|
||||||
|
id: get_accountportal_commits
|
||||||
|
run: |
|
||||||
|
cd packages/account-portal
|
||||||
|
accountportal_commit=$(git rev-parse HEAD)
|
||||||
|
|
||||||
|
branch="${{ github.base_ref || github.ref_name }}"
|
||||||
|
echo "Running on branch '$branch' (base_ref=${{ github.base_ref }}, ref_name=${{ github.head_ref }})"
|
||||||
|
|
||||||
|
base_commit=$(git rev-parse origin/master)
|
||||||
|
|
||||||
|
if [[ ! -z $base_commit ]]; then
|
||||||
|
echo "target_branch=$branch"
|
||||||
|
echo "target_branch=$branch" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "accountportal_commit=$accountportal_commit"
|
||||||
|
echo "accountportal_commit=$accountportal_commit" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "base_commit=$base_commit"
|
||||||
|
echo "base_commit=$base_commit" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "Nothing to do - branch to branch merge."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check submodule merged to base branch
|
||||||
|
if: ${{ steps.get_accountportal_commits.outputs.base_commit != '' }}
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
const submoduleCommit = '${{ steps.get_accountportal_commits.outputs.accountportal_commit }}';
|
||||||
|
const baseCommit = '${{ steps.get_accountportal_commits.outputs.base_commit }}';
|
||||||
|
|
||||||
|
if (submoduleCommit !== baseCommit) {
|
||||||
|
console.error('Submodule commit does not match the latest commit on the "${{ steps.get_accountportal_commits.outputs.target_branch }}" branch.');
|
||||||
|
console.error('Refer to the account portal repo to merge your changes: https://github.com/Budibase/account-portal/blob/master/docs/index.md')
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
} else {
|
} else {
|
||||||
console.log('All good, the submodule had been merged and setup correctly!')
|
console.log('All good, the submodule had been merged and setup correctly!')
|
||||||
|
|
|
@ -2,9 +2,7 @@ name: close-featurebranch
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [closed]
|
types: [closed, unlabeled]
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
BRANCH:
|
BRANCH:
|
||||||
|
@ -14,9 +12,12 @@ on:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
|
if: |
|
||||||
|
(github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'feature-branch')) ||
|
||||||
|
github.event.label.name == 'feature-branch'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: passeidireto/trigger-external-workflow-action@main
|
- uses: passeidireto/trigger-external-workflow-action@main
|
||||||
env:
|
env:
|
||||||
PAYLOAD_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.BRANCH || github.head_ref }}
|
PAYLOAD_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.BRANCH || github.head_ref }}
|
||||||
|
|
|
@ -2,15 +2,22 @@ name: deploy-featurebranch
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
types: [
|
||||||
- master
|
labeled,
|
||||||
|
# default types below (https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request)
|
||||||
|
opened,
|
||||||
|
synchronize,
|
||||||
|
reopened,
|
||||||
|
]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
if: |
|
||||||
|
(github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase') &&
|
||||||
|
contains(github.event.pull_request.labels.*.name, 'feature-branch')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: passeidireto/trigger-external-workflow-action@main
|
- uses: passeidireto/trigger-external-workflow-action@main
|
||||||
env:
|
env:
|
||||||
PAYLOAD_BRANCH: ${{ github.head_ref }}
|
PAYLOAD_BRANCH: ${{ github.head_ref }}
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
name: Forced release
|
||||||
|
concurrency:
|
||||||
|
group: tag-release
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ensure-is-master-tag:
|
||||||
|
name: Ensure is a master tag
|
||||||
|
runs-on: qa-arc-runner-set
|
||||||
|
steps:
|
||||||
|
- name: Checkout monorepo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
fetch-tags: true
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Fail if ref is not a tag
|
||||||
|
run: |
|
||||||
|
if ! git show-ref -q --verify "refs/tags/${{ github.ref_name }}" 2>/dev/null; then
|
||||||
|
echo "'${{ github.ref_name }}' is not a valid tag."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
- name: Fail if tag is not in master
|
||||||
|
run: |
|
||||||
|
if ! git merge-base --is-ancestor ${{ github.ref_name }} origin/master; then
|
||||||
|
echo "Tag is not in master. Release can only execute tags that are present on the master branch"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
trigger-release:
|
||||||
|
needs: [ensure-is-master-tag]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: peter-evans/repository-dispatch@v2
|
||||||
|
with:
|
||||||
|
repository: budibase/budibase-deploys
|
||||||
|
event-type: release-prod
|
||||||
|
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
|
client-payload: |-
|
||||||
|
{
|
||||||
|
"TAG": "${{ github.ref_name }}"
|
||||||
|
}
|
|
@ -16,8 +16,8 @@ jobs:
|
||||||
days-before-pr-stale: 7
|
days-before-pr-stale: 7
|
||||||
stale-issue-label: stale
|
stale-issue-label: stale
|
||||||
exempt-pr-labels: pinned,security,roadmap
|
exempt-pr-labels: pinned,security,roadmap
|
||||||
|
|
||||||
days-before-pr-close: 7
|
days-before-pr-close: 7
|
||||||
|
days-before-issue-close: 30
|
||||||
|
|
||||||
- uses: actions/stale@v8
|
- uses: actions/stale@v8
|
||||||
with:
|
with:
|
||||||
|
@ -26,6 +26,7 @@ jobs:
|
||||||
days-before-stale: 30
|
days-before-stale: 30
|
||||||
only-issue-labels: bug,High priority
|
only-issue-labels: bug,High priority
|
||||||
stale-issue-label: warn
|
stale-issue-label: warn
|
||||||
|
days-before-close: 30
|
||||||
|
|
||||||
- uses: actions/stale@v8
|
- uses: actions/stale@v8
|
||||||
with:
|
with:
|
||||||
|
@ -34,6 +35,7 @@ jobs:
|
||||||
days-before-stale: 90
|
days-before-stale: 90
|
||||||
only-issue-labels: bug,Medium priority
|
only-issue-labels: bug,Medium priority
|
||||||
stale-issue-label: warn
|
stale-issue-label: warn
|
||||||
|
days-before-close: 30
|
||||||
|
|
||||||
- uses: actions/stale@v8
|
- uses: actions/stale@v8
|
||||||
with:
|
with:
|
||||||
|
@ -43,5 +45,4 @@ jobs:
|
||||||
stale-issue-label: stale
|
stale-issue-label: stale
|
||||||
only-issue-labels: bug
|
only-issue-labels: bug
|
||||||
stale-issue-message: "This issue has been automatically marked as stale because it has not had any activity for six months."
|
stale-issue-message: "This issue has been automatically marked as stale because it has not had any activity for six months."
|
||||||
|
|
||||||
days-before-close: 30
|
days-before-close: 30
|
||||||
|
|
|
@ -28,7 +28,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
echo "Ref is not master, you must run this job from master."
|
echo "Ref is not master, you must run this job from master."
|
||||||
exit 1
|
exit 1
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
@ -53,7 +53,7 @@ jobs:
|
||||||
needs: [tag-release]
|
needs: [tag-release]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: peter-evans/repository-dispatch@v2
|
- uses: peter-evans/repository-dispatch@v2
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
builder/*
|
|
||||||
.data/
|
.data/
|
||||||
.temp/
|
.temp/
|
||||||
packages/server/runtime_apps/
|
packages/server/runtime_apps/
|
||||||
|
@ -41,8 +40,11 @@ bower_components
|
||||||
build/Release
|
build/Release
|
||||||
|
|
||||||
# Dependency directories
|
# Dependency directories
|
||||||
/node_modules/
|
|
||||||
jspm_packages/
|
jspm_packages/
|
||||||
|
*.min.js
|
||||||
|
*.map
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
|
||||||
# TypeScript v1 declaration files
|
# TypeScript v1 declaration files
|
||||||
typings/
|
typings/
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
[submodule "packages/pro"]
|
[submodule "packages/pro"]
|
||||||
path = packages/pro
|
path = packages/pro
|
||||||
url = git@github.com:Budibase/budibase-pro.git
|
url = git@github.com:Budibase/budibase-pro.git
|
||||||
|
[submodule "packages/account-portal"]
|
||||||
|
path = packages/account-portal
|
||||||
|
url = git@github.com:Budibase/account-portal.git
|
||||||
|
|
|
@ -8,4 +8,7 @@ packages/worker/coverage
|
||||||
packages/backend-core/coverage
|
packages/backend-core/coverage
|
||||||
packages/builder/.routify
|
packages/builder/.routify
|
||||||
packages/sdk/sdk
|
packages/sdk/sdk
|
||||||
packages/pro/coverage
|
packages/pro/coverage
|
||||||
|
packages/account-portal/packages/ui/build
|
||||||
|
packages/account-portal/packages/ui/.routify
|
||||||
|
packages/account-portal/packages/server/build
|
|
@ -1,3 +1,3 @@
|
||||||
nodejs 18.17.0
|
nodejs 20.10.0
|
||||||
python 3.10.0
|
python 3.10.0
|
||||||
yarn 1.22.19
|
yarn 1.22.19
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": true
|
"source.fixAll": "explicit"
|
||||||
},
|
},
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"[json]": {
|
"[json]": {
|
||||||
|
|
49
README.md
49
README.md
|
@ -11,7 +11,7 @@
|
||||||
The low code platform you'll enjoy using
|
The low code platform you'll enjoy using
|
||||||
</h3>
|
</h3>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Budibase is an open source low-code platform, and the easiest way to build internal apps that improve productivity.
|
Budibase is an open-source low-code platform that saves engineers 100s of hours building forms, portals, and approval apps, securely.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 align="center">
|
<h3 align="center">
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="Budibase design ui" src="https://res.cloudinary.com/daog6scxm/image/upload/v1633524049/ui/design-ui-wide-mobile_gdaveq.jpg">
|
<img alt="Budibase design ui" src="https://res.cloudinary.com/daog6scxm/image/upload/v1680181644/ui/homepage-design-ui_sizp7b.png">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
### Build and ship real software
|
### Build and ship real software
|
||||||
Unlike other platforms, with Budibase you build and ship single page applications. Budibase applications have performance baked in and can be designed responsively, providing your users with a great experience.
|
Unlike other platforms, with Budibase you build and ship single page applications. Budibase applications have performance baked in and can be designed responsively, providing users with a great experience.
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
### Open source and extensible
|
### Open source and extensible
|
||||||
|
@ -65,40 +65,36 @@ Budibase is open-source - licensed as GPL v3. This should fill you with confiden
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
### Load data or start from scratch
|
### Load data or start from scratch
|
||||||
Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no datasources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
Budibase pulls data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/data_n1tlhf.png">
|
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1680281798/ui/data_klbuna.png">
|
||||||
</p>
|
</p>
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
### Design and build apps with powerful pre-made components
|
### Design and build apps with powerful pre-made components
|
||||||
|
|
||||||
Budibase comes out of the box with beautifully designed, powerful components which you can use like building blocks to build your UI. We also expose a lot of your favourite CSS styling options so you can go that extra creative mile. [Request new component](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
Budibase comes out of the box with beautifully designed, powerful components which you can use like building blocks to build your UI. We also expose many of your favourite CSS styling options so you can go that extra creative mile. [Request new component](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="Budibase design" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970243/Out%20of%20beta%20launch/design-like-a-pro_qhlfeu.gif">
|
<img alt="Budibase design" src="https://res.cloudinary.com/daog6scxm/image/upload/v1675437167/ui/form_2x_mbli8y.png">
|
||||||
</p>
|
</p>
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
### Automate processes, integrate with other tools, and connect to webhooks
|
### Automate processes, integrate with other tools and connect to webhooks
|
||||||
Save time by automating manual processes and workflows. From connecting to webhooks, to automating emails, simply tell Budibase what to do and let it work for you. You can easily [create new automations for Budibase here](https://github.com/Budibase/automations) or [Request new automation](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
Save time by automating manual processes and workflows. From connecting to webhooks to automating emails, simply tell Budibase what to do and let it work for you. You can easily [create new automations for Budibase here](https://github.com/Budibase/automations) or [Request new automation](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img alt="Budibase automations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970486/Out%20of%20beta%20launch/automation_riro7u.png">
|
|
||||||
</p>
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
### Integrate with your favorite tools
|
### Integrate with your favorite tools
|
||||||
Budibase integrates with a number of popular tools allowing you to build apps that perfectly fit your stack.
|
Budibase integrates with a number of popular tools allowing you to build apps that perfectly fit your stack.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="Budibase integrations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1636970242/Out%20of%20beta%20launch/integrations_kc7dqt.png">
|
<img alt="Budibase integrations" src="https://res.cloudinary.com/daog6scxm/image/upload/v1680195228/ui/automate_fg9z07.png">
|
||||||
</p>
|
</p>
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
### Admin paradise
|
### Deploy with confidence and security
|
||||||
Budibase is made to scale. With Budibase, you can self-host on your own infrastructure and globally manage users, onboarding, SMTP, apps, groups, theming and more. You can also provide users/groups with an app portal and disseminate user-management to the group manager.
|
Budibase is made to scale. With Budibase, you can self-host on your own infrastructure and globally manage users, onboarding, SMTP, apps, groups, theming and more. You can also provide users/groups with an app portal and disseminate user management to the group manager.
|
||||||
|
|
||||||
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
|
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
|
||||||
|
|
||||||
|
@ -119,17 +115,14 @@ As with anything that we build in Budibase, our new public API is simple to use,
|
||||||
#### Docs
|
#### Docs
|
||||||
You can learn more about the Budibase API at the following places:
|
You can learn more about the Budibase API at the following places:
|
||||||
|
|
||||||
- [General documentation](https://docs.budibase.com/docs/public-api) : Learn how to get your API key, how to use spec, and how to use with Postman
|
- [General documentation](https://docs.budibase.com/docs/public-api): Learn how to get your API key, how to use spec, and how to use Postman
|
||||||
- [Interactive API documentation](https://docs.budibase.com/reference/post_applications) : Learn how to interact with the API
|
- [Interactive API documentation](https://docs.budibase.com/reference/post_applications) : Learn how to interact with the API
|
||||||
|
|
||||||
#### Guides
|
<br /><br />
|
||||||
|
|
||||||
- [Build an app with Budibase and Next.js](https://budibase.com/blog/building-a-crud-app-with-budibase-and-next.js/)
|
|
||||||
|
|
||||||
## 🏁 Get started
|
## 🏁 Get started
|
||||||
|
|
||||||
Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean.
|
Deploy Budibase using Docker, Kubernetes, and Digital Ocean on your existing infrastructure. Or use Budibase Cloud if you don't need to self-host and would like to get started quickly.
|
||||||
Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly.
|
|
||||||
|
|
||||||
### [Get started with self-hosting Budibase](https://docs.budibase.com/docs/hosting-methods)
|
### [Get started with self-hosting Budibase](https://docs.budibase.com/docs/hosting-methods)
|
||||||
|
|
||||||
|
@ -162,7 +155,7 @@ If you have a question or would like to talk with other Budibase users and join
|
||||||
|
|
||||||
## ❗ Code of conduct
|
## ❗ Code of conduct
|
||||||
|
|
||||||
Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/docs/CODE_OF_CONDUCT.md). Please read it.
|
Budibase is dedicated to providing everyone a welcoming, diverse, and harassment-free experience. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/docs/CODE_OF_CONDUCT.md). Please read it.
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|
||||||
|
@ -171,16 +164,16 @@ Budibase is dedicated to providing a welcoming, diverse, and harrassment-free ex
|
||||||
|
|
||||||
## 🙌 Contributing to Budibase
|
## 🙌 Contributing to Budibase
|
||||||
|
|
||||||
From opening a bug report to creating a pull request: every contribution is appreciated and welcomed. If you're planning to implement a new feature or change the API please create an issue first. This way we can ensure your work is not in vain.
|
From opening a bug report to creating a pull request: every contribution is appreciated and welcomed. If you're planning to implement a new feature or change the API, please create an issue first. This way, we can ensure your work is not in vain.
|
||||||
Environment setup instructions are available for [Debian](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-DEBIAN.md) and [MacOSX](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-MACOSX.md)
|
Environment setup instructions are available [here](https://github.com/Budibase/budibase/tree/HEAD/docs/CONTRIBUTING.md).
|
||||||
|
|
||||||
### Not Sure Where to Start?
|
### Not Sure Where to Start?
|
||||||
A good place to start contributing, is the [First time issues project](https://github.com/Budibase/budibase/projects/22).
|
A good place to start contributing is the [First time issues project](https://github.com/Budibase/budibase/projects/22).
|
||||||
|
|
||||||
### How the repository is organized
|
### How the repository is organized
|
||||||
Budibase is a monorepo managed by lerna. Lerna manages the building and publishing of the budibase packages. At a high level, here are the packages that make up Budibase.
|
Budibase is a monorepo managed by lerna. Lerna manages the building and publishing of the budibase packages. At a high level, here are the packages that make up Budibase.
|
||||||
|
|
||||||
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contains code for the budibase builder client side svelte application.
|
- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contains code for the budibase builder client-side svelte application.
|
||||||
|
|
||||||
- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - A module that runs in the browser responsible for reading JSON definition and creating living, breathing web apps from it.
|
- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - A module that runs in the browser responsible for reading JSON definition and creating living, breathing web apps from it.
|
||||||
|
|
||||||
|
@ -193,7 +186,7 @@ For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase
|
||||||
|
|
||||||
## 📝 License
|
## 📝 License
|
||||||
|
|
||||||
Budibase is open-source, licensed as [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). The client and component libraries are licensed as [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - so the apps that you build can be licensed however you like.
|
Budibase is open-source, licensed as [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). The client and component libraries are licensed as [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - so the apps you build can be licensed however you like.
|
||||||
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
|
|
|
@ -157,6 +157,17 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml
|
||||||
| services.apps.replicaCount | int | `1` | The number of apps replicas to run. |
|
| services.apps.replicaCount | int | `1` | The number of apps replicas to run. |
|
||||||
| services.apps.resources | object | `{}` | The resources to use for apps pods. See <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/> for more information on how to set these. |
|
| services.apps.resources | object | `{}` | The resources to use for apps pods. See <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/> for more information on how to set these. |
|
||||||
| services.apps.startupProbe | object | HTTP health checks. | Startup probe configuration for apps pods. You shouldn't need to change this, but if you want to you can find more information here: <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/> |
|
| services.apps.startupProbe | object | HTTP health checks. | Startup probe configuration for apps pods. You shouldn't need to change this, but if you want to you can find more information here: <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/> |
|
||||||
|
| services.automationWorkers.autoscaling.enabled | bool | `false` | Whether to enable horizontal pod autoscaling for the apps service. |
|
||||||
|
| services.automationWorkers.autoscaling.maxReplicas | int | `10` | |
|
||||||
|
| services.automationWorkers.autoscaling.minReplicas | int | `1` | |
|
||||||
|
| services.automationWorkers.autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization percentage for the automation worker service. Note that for autoscaling to work, you will need to have metrics-server configured, and resources set for the automation worker pods. |
|
||||||
|
| services.automationWorkers.enabled | bool | `true` | Whether or not to enable the automation worker service. If you disable this, automations will be processed by the apps service. |
|
||||||
|
| services.automationWorkers.livenessProbe | object | HTTP health checks. | Liveness probe configuration for automation worker pods. You shouldn't need to change this, but if you want to you can find more information here: <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/> |
|
||||||
|
| services.automationWorkers.logLevel | string | `"info"` | The log level for the automation worker service. |
|
||||||
|
| services.automationWorkers.readinessProbe | object | HTTP health checks. | Readiness probe configuration for automation worker pods. You shouldn't need to change this, but if you want to you can find more information here: <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/> |
|
||||||
|
| services.automationWorkers.replicaCount | int | `1` | The number of automation worker replicas to run. |
|
||||||
|
| services.automationWorkers.resources | object | `{}` | The resources to use for automation worker pods. See <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/> for more information on how to set these. |
|
||||||
|
| services.automationWorkers.startupProbe | object | HTTP health checks. | Startup probe configuration for automation worker pods. You shouldn't need to change this, but if you want to you can find more information here: <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/> |
|
||||||
| services.couchdb.backup.enabled | bool | `false` | Whether or not to enable periodic CouchDB backups. This works by replicating to another CouchDB instance. |
|
| services.couchdb.backup.enabled | bool | `false` | Whether or not to enable periodic CouchDB backups. This works by replicating to another CouchDB instance. |
|
||||||
| services.couchdb.backup.interval | string | `""` | Backup interval in seconds |
|
| services.couchdb.backup.interval | string | `""` | Backup interval in seconds |
|
||||||
| services.couchdb.backup.resources | object | `{}` | The resources to use for CouchDB backup pods. See <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/> for more information on how to set these. |
|
| services.couchdb.backup.resources | object | `{}` | The resources to use for CouchDB backup pods. See <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/> for more information on how to set these. |
|
||||||
|
|
|
@ -192,7 +192,14 @@ spec:
|
||||||
- name: NODE_TLS_REJECT_UNAUTHORIZED
|
- name: NODE_TLS_REJECT_UNAUTHORIZED
|
||||||
value: {{ .Values.services.tlsRejectUnauthorized }}
|
value: {{ .Values.services.tlsRejectUnauthorized }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{- if .Values.services.automationWorkers.enabled }}
|
||||||
|
- name: APP_FEATURES
|
||||||
|
value: "api"
|
||||||
|
{{- end }}
|
||||||
|
{{- range .Values.services.apps.extraEnv }}
|
||||||
|
- name: {{ .name }}
|
||||||
|
value: {{ .value | quote }}
|
||||||
|
{{- end }}
|
||||||
image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
{{- if .Values.services.apps.startupProbe }}
|
{{- if .Values.services.apps.startupProbe }}
|
||||||
|
@ -220,6 +227,14 @@ spec:
|
||||||
resources:
|
resources:
|
||||||
{{- toYaml . | nindent 10 }}
|
{{- toYaml . | nindent 10 }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ if .Values.services.apps.command }}
|
||||||
|
command:
|
||||||
|
{{- toYaml .Values.services.apps.command | nindent 10 }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Values.services.apps.args }}
|
||||||
|
args:
|
||||||
|
{{- toYaml .Values.services.apps.args | nindent 10 }}
|
||||||
|
{{ end }}
|
||||||
{{- with .Values.affinity }}
|
{{- with .Values.affinity }}
|
||||||
affinity:
|
affinity:
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
|
@ -237,4 +252,10 @@ spec:
|
||||||
{{ end }}
|
{{ end }}
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
serviceAccountName: ""
|
serviceAccountName: ""
|
||||||
|
{{ if .Values.services.apps.ndots }}
|
||||||
|
dnsConfig:
|
||||||
|
options:
|
||||||
|
- name: ndots
|
||||||
|
value: {{ .Values.services.apps.ndots | quote }}
|
||||||
|
{{ end }}
|
||||||
status: {}
|
status: {}
|
||||||
|
|
|
@ -0,0 +1,262 @@
|
||||||
|
{{- if .Values.services.automationWorkers.enabled }}
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
{{ if .Values.services.automationWorkers.deploymentAnnotations }}
|
||||||
|
{{- toYaml .Values.services.automationWorkers.deploymentAnnotations | indent 4 -}}
|
||||||
|
{{ end }}
|
||||||
|
labels:
|
||||||
|
io.kompose.service: automation-worker-service
|
||||||
|
{{ if .Values.services.automationWorkers.deploymentLabels }}
|
||||||
|
{{- toYaml .Values.services.automationWorkers.deploymentLabels | indent 4 -}}
|
||||||
|
{{ end }}
|
||||||
|
name: automation-worker-service
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.services.automationWorkers.replicaCount }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
io.kompose.service: automation-worker-service
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
{{ if .Values.services.automationWorkers.templateAnnotations }}
|
||||||
|
{{- toYaml .Values.services.automationWorkers.templateAnnotations | indent 8 -}}
|
||||||
|
{{ end }}
|
||||||
|
labels:
|
||||||
|
io.kompose.service: automation-worker-service
|
||||||
|
{{ if .Values.services.automationWorkers.templateLabels }}
|
||||||
|
{{- toYaml .Values.services.automationWorkers.templateLabels | indent 8 -}}
|
||||||
|
{{ end }}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- env:
|
||||||
|
- name: BUDIBASE_ENVIRONMENT
|
||||||
|
value: {{ .Values.globals.budibaseEnv }}
|
||||||
|
- name: DEPLOYMENT_ENVIRONMENT
|
||||||
|
value: "kubernetes"
|
||||||
|
- name: COUCH_DB_URL
|
||||||
|
{{ if .Values.services.couchdb.url }}
|
||||||
|
value: {{ .Values.services.couchdb.url }}
|
||||||
|
{{ else }}
|
||||||
|
value: http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Values.services.couchdb.enabled }}
|
||||||
|
- name: COUCH_DB_USER
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ template "couchdb.fullname" . }}
|
||||||
|
key: adminUsername
|
||||||
|
- name: COUCH_DB_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ template "couchdb.fullname" . }}
|
||||||
|
key: adminPassword
|
||||||
|
{{ end }}
|
||||||
|
- name: ENABLE_ANALYTICS
|
||||||
|
value: {{ .Values.globals.enableAnalytics | quote }}
|
||||||
|
- name: API_ENCRYPTION_KEY
|
||||||
|
value: {{ .Values.globals.apiEncryptionKey | quote }}
|
||||||
|
- name: HTTP_LOGGING
|
||||||
|
value: {{ .Values.services.automationWorkers.httpLogging | quote }}
|
||||||
|
- name: INTERNAL_API_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ template "budibase.fullname" . }}
|
||||||
|
key: internalApiKey
|
||||||
|
- name: INTERNAL_API_KEY_FALLBACK
|
||||||
|
value: {{ .Values.globals.internalApiKeyFallback | quote }}
|
||||||
|
- name: JWT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ template "budibase.fullname" . }}
|
||||||
|
key: jwtSecret
|
||||||
|
- name: JWT_SECRET_FALLBACK
|
||||||
|
value: {{ .Values.globals.jwtSecretFallback | quote }}
|
||||||
|
{{ if .Values.services.objectStore.region }}
|
||||||
|
- name: AWS_REGION
|
||||||
|
value: {{ .Values.services.objectStore.region }}
|
||||||
|
{{ end }}
|
||||||
|
- name: MINIO_ENABLED
|
||||||
|
value: {{ .Values.services.objectStore.minio | quote }}
|
||||||
|
- name: MINIO_ACCESS_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ template "budibase.fullname" . }}
|
||||||
|
key: objectStoreAccess
|
||||||
|
- name: MINIO_SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ template "budibase.fullname" . }}
|
||||||
|
key: objectStoreSecret
|
||||||
|
- name: CLOUDFRONT_CDN
|
||||||
|
value: {{ .Values.services.objectStore.cloudfront.cdn | quote }}
|
||||||
|
- name: CLOUDFRONT_PUBLIC_KEY_ID
|
||||||
|
value: {{ .Values.services.objectStore.cloudfront.publicKeyId | quote }}
|
||||||
|
- name: CLOUDFRONT_PRIVATE_KEY_64
|
||||||
|
value: {{ .Values.services.objectStore.cloudfront.privateKey64 | quote }}
|
||||||
|
- name: MINIO_URL
|
||||||
|
value: {{ .Values.services.objectStore.url }}
|
||||||
|
- name: PLUGIN_BUCKET_NAME
|
||||||
|
value: {{ .Values.services.objectStore.pluginBucketName | quote }}
|
||||||
|
- name: APPS_BUCKET_NAME
|
||||||
|
value: {{ .Values.services.objectStore.appsBucketName | quote }}
|
||||||
|
- name: GLOBAL_BUCKET_NAME
|
||||||
|
value: {{ .Values.services.objectStore.globalBucketName | quote }}
|
||||||
|
- name: BACKUPS_BUCKET_NAME
|
||||||
|
value: {{ .Values.services.objectStore.backupsBucketName | quote }}
|
||||||
|
- name: PORT
|
||||||
|
value: {{ .Values.services.automationWorkers.port | quote }}
|
||||||
|
{{ if .Values.services.worker.publicApiRateLimitPerSecond }}
|
||||||
|
- name: API_REQ_LIMIT_PER_SEC
|
||||||
|
value: {{ .Values.globals.automationWorkers.publicApiRateLimitPerSecond | quote }}
|
||||||
|
{{ end }}
|
||||||
|
- name: MULTI_TENANCY
|
||||||
|
value: {{ .Values.globals.multiTenancy | quote }}
|
||||||
|
- name: OFFLINE_MODE
|
||||||
|
value: {{ .Values.globals.offlineMode | quote }}
|
||||||
|
- name: LOG_LEVEL
|
||||||
|
value: {{ .Values.services.automationWorkers.logLevel | quote }}
|
||||||
|
- name: REDIS_PASSWORD
|
||||||
|
value: {{ .Values.services.redis.password }}
|
||||||
|
- name: REDIS_URL
|
||||||
|
{{ if .Values.services.redis.url }}
|
||||||
|
value: {{ .Values.services.redis.url }}
|
||||||
|
{{ else }}
|
||||||
|
value: redis-service:{{ .Values.services.redis.port }}
|
||||||
|
{{ end }}
|
||||||
|
- name: SELF_HOSTED
|
||||||
|
value: {{ .Values.globals.selfHosted | quote }}
|
||||||
|
- name: POSTHOG_TOKEN
|
||||||
|
value: {{ .Values.globals.posthogToken | quote }}
|
||||||
|
- name: WORKER_URL
|
||||||
|
value: http://worker-service:{{ .Values.services.worker.port }}
|
||||||
|
- name: PLATFORM_URL
|
||||||
|
value: {{ .Values.globals.platformUrl | quote }}
|
||||||
|
- name: ACCOUNT_PORTAL_URL
|
||||||
|
value: {{ .Values.globals.accountPortalUrl | quote }}
|
||||||
|
- name: ACCOUNT_PORTAL_API_KEY
|
||||||
|
value: {{ .Values.globals.accountPortalApiKey | quote }}
|
||||||
|
- name: COOKIE_DOMAIN
|
||||||
|
value: {{ .Values.globals.cookieDomain | quote }}
|
||||||
|
- name: HTTP_MIGRATIONS
|
||||||
|
value: {{ .Values.globals.httpMigrations | quote }}
|
||||||
|
- name: GOOGLE_CLIENT_ID
|
||||||
|
value: {{ .Values.globals.google.clientId | quote }}
|
||||||
|
- name: GOOGLE_CLIENT_SECRET
|
||||||
|
value: {{ .Values.globals.google.secret | quote }}
|
||||||
|
- name: AUTOMATION_MAX_ITERATIONS
|
||||||
|
value: {{ .Values.globals.automationMaxIterations | quote }}
|
||||||
|
- name: TENANT_FEATURE_FLAGS
|
||||||
|
value: {{ .Values.globals.tenantFeatureFlags | quote }}
|
||||||
|
- name: ENCRYPTION_KEY
|
||||||
|
value: {{ .Values.globals.bbEncryptionKey | quote }}
|
||||||
|
{{ if .Values.globals.bbAdminUserEmail }}
|
||||||
|
- name: BB_ADMIN_USER_EMAIL
|
||||||
|
value: {{ .Values.globals.bbAdminUserEmail | quote }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Values.globals.bbAdminUserPassword }}
|
||||||
|
- name: BB_ADMIN_USER_PASSWORD
|
||||||
|
value: {{ .Values.globals.bbAdminUserPassword | quote }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Values.globals.pluginsDir }}
|
||||||
|
- name: PLUGINS_DIR
|
||||||
|
value: {{ .Values.globals.pluginsDir | quote }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Values.services.automationWorkers.nodeDebug }}
|
||||||
|
- name: NODE_DEBUG
|
||||||
|
value: {{ .Values.services.automationWorkers.nodeDebug | quote }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Values.globals.datadogApmEnabled }}
|
||||||
|
- name: DD_LOGS_INJECTION
|
||||||
|
value: {{ .Values.globals.datadogApmEnabled | quote }}
|
||||||
|
- name: DD_APM_ENABLED
|
||||||
|
value: {{ .Values.globals.datadogApmEnabled | quote }}
|
||||||
|
- name: DD_APM_DD_URL
|
||||||
|
value: https://trace.agent.datadoghq.eu
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Values.globals.globalAgentHttpProxy }}
|
||||||
|
- name: GLOBAL_AGENT_HTTP_PROXY
|
||||||
|
value: {{ .Values.globals.globalAgentHttpProxy | quote }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Values.globals.globalAgentHttpsProxy }}
|
||||||
|
- name: GLOBAL_AGENT_HTTPS_PROXY
|
||||||
|
value: {{ .Values.globals.globalAgentHttpsProxy | quote }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Values.globals.globalAgentNoProxy }}
|
||||||
|
- name: GLOBAL_AGENT_NO_PROXY
|
||||||
|
value: {{ .Values.globals.globalAgentNoProxy | quote }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Values.services.tlsRejectUnauthorized }}
|
||||||
|
- name: NODE_TLS_REJECT_UNAUTHORIZED
|
||||||
|
value: {{ .Values.services.tlsRejectUnauthorized }}
|
||||||
|
{{ end }}
|
||||||
|
- name: APP_FEATURES
|
||||||
|
value: "automations"
|
||||||
|
{{- range .Values.services.automationWorkers.extraEnv }}
|
||||||
|
- name: {{ .name }}
|
||||||
|
value: {{ .value | quote }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||||
|
imagePullPolicy: Always
|
||||||
|
{{- if .Values.services.automationWorkers.startupProbe }}
|
||||||
|
{{- with .Values.services.automationWorkers.startupProbe }}
|
||||||
|
startupProbe:
|
||||||
|
{{- toYaml . | nindent 10 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.services.automationWorkers.livenessProbe }}
|
||||||
|
{{- with .Values.services.automationWorkers.livenessProbe }}
|
||||||
|
livenessProbe:
|
||||||
|
{{- toYaml . | nindent 10 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.services.automationWorkers.readinessProbe }}
|
||||||
|
{{- with .Values.services.automationWorkers.readinessProbe }}
|
||||||
|
readinessProbe:
|
||||||
|
{{- toYaml . | nindent 10 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
name: bbautomationworker
|
||||||
|
ports:
|
||||||
|
- containerPort: {{ .Values.services.automationWorkers.port }}
|
||||||
|
{{ with .Values.services.automationWorkers.resources }}
|
||||||
|
resources:
|
||||||
|
{{- toYaml . | nindent 10 }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Values.services.automationWorkers.command }}
|
||||||
|
command:
|
||||||
|
{{- toYaml .Values.services.automationWorkers.command | nindent 10 }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Values.services.automationWorkers.args }}
|
||||||
|
args:
|
||||||
|
{{- toYaml .Values.services.automationWorkers.args | nindent 10 }}
|
||||||
|
{{ end }}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{ if .Values.schedulerName }}
|
||||||
|
schedulerName: {{ .Values.schedulerName | quote }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Values.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
|
||||||
|
{{ end }}
|
||||||
|
restartPolicy: Always
|
||||||
|
serviceAccountName: ""
|
||||||
|
{{ if .Values.services.automationWorkers.ndots }}
|
||||||
|
dnsConfig:
|
||||||
|
options:
|
||||||
|
- name: ndots
|
||||||
|
value: {{ .Values.services.automationWorkers.ndots | quote }}
|
||||||
|
{{ end }}
|
||||||
|
status: {}
|
||||||
|
{{- end }}
|
|
@ -0,0 +1,32 @@
|
||||||
|
{{- if .Values.services.automationWorkers.autoscaling.enabled }}
|
||||||
|
apiVersion: {{ ternary "autoscaling/v2" "autoscaling/v2beta2" (.Capabilities.APIVersions.Has "autoscaling/v2") }}
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: {{ include "budibase.fullname" . }}-apps
|
||||||
|
labels:
|
||||||
|
{{- include "budibase.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: automation-worker-service
|
||||||
|
minReplicas: {{ .Values.services.automationWorkers.autoscaling.minReplicas }}
|
||||||
|
maxReplicas: {{ .Values.services.automationWorkers.autoscaling.maxReplicas }}
|
||||||
|
metrics:
|
||||||
|
{{- if .Values.services.automationWorkers.autoscaling.targetCPUUtilizationPercentage }}
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: {{ .Values.services.automationWorkers.autoscaling.targetCPUUtilizationPercentage }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.services.automationWorkers.autoscaling.targetMemoryUtilizationPercentage }}
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: memory
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: {{ .Values.services.automationWorkers.autoscaling.targetMemoryUtilizationPercentage }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
|
@ -100,5 +100,19 @@ spec:
|
||||||
{{ end }}
|
{{ end }}
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
serviceAccountName: ""
|
serviceAccountName: ""
|
||||||
|
{{ if .Values.services.proxy.command }}
|
||||||
|
command:
|
||||||
|
{{- toYaml .Values.services.proxy.command | nindent 8 }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Values.services.proxy.args }}
|
||||||
|
args:
|
||||||
|
{{- toYaml .Values.services.proxy.args | nindent 8 }}
|
||||||
|
{{ end }}
|
||||||
volumes:
|
volumes:
|
||||||
|
{{ if .Values.services.proxy.ndots }}
|
||||||
|
dnsConfig:
|
||||||
|
options:
|
||||||
|
- name: ndots
|
||||||
|
value: {{ .Values.services.proxy.ndots | quote }}
|
||||||
|
{{ end }}
|
||||||
status: {}
|
status: {}
|
||||||
|
|
|
@ -182,6 +182,10 @@ spec:
|
||||||
- name: NODE_TLS_REJECT_UNAUTHORIZED
|
- name: NODE_TLS_REJECT_UNAUTHORIZED
|
||||||
value: {{ .Values.services.tlsRejectUnauthorized }}
|
value: {{ .Values.services.tlsRejectUnauthorized }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{- range .Values.services.worker.extraEnv }}
|
||||||
|
- name: {{ .name }}
|
||||||
|
value: {{ .value | quote }}
|
||||||
|
{{- end }}
|
||||||
image: budibase/worker:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
image: budibase/worker:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
{{- if .Values.services.worker.startupProbe }}
|
{{- if .Values.services.worker.startupProbe }}
|
||||||
|
@ -209,6 +213,14 @@ spec:
|
||||||
resources:
|
resources:
|
||||||
{{- toYaml . | nindent 10 }}
|
{{- toYaml . | nindent 10 }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ if .Values.services.worker.command }}
|
||||||
|
command:
|
||||||
|
{{- toYaml .Values.services.worker.command | nindent 10 }}
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Values.services.worker.args }}
|
||||||
|
args:
|
||||||
|
{{- toYaml .Values.services.worker.args | nindent 10 }}
|
||||||
|
{{ end }}
|
||||||
{{- with .Values.affinity }}
|
{{- with .Values.affinity }}
|
||||||
affinity:
|
affinity:
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
|
@ -226,4 +238,10 @@ spec:
|
||||||
{{ end }}
|
{{ end }}
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
serviceAccountName: ""
|
serviceAccountName: ""
|
||||||
|
{{ if .Values.services.worker.ndots }}
|
||||||
|
dnsConfig:
|
||||||
|
options:
|
||||||
|
- name: ndots
|
||||||
|
value: {{ .Values.services.worker.ndots | quote }}
|
||||||
|
{{ end }}
|
||||||
status: {}
|
status: {}
|
||||||
|
|
|
@ -220,6 +220,9 @@ services:
|
||||||
# <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/>
|
# <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/>
|
||||||
# for more information on how to set these.
|
# for more information on how to set these.
|
||||||
resources: {}
|
resources: {}
|
||||||
|
# -- Extra environment variables to set for apps pods. Takes a list of
|
||||||
|
# name=value pairs.
|
||||||
|
extraEnv: []
|
||||||
# -- Startup probe configuration for apps pods. You shouldn't need to
|
# -- Startup probe configuration for apps pods. You shouldn't need to
|
||||||
# change this, but if you want to you can find more information here:
|
# change this, but if you want to you can find more information here:
|
||||||
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
||||||
|
@ -272,6 +275,78 @@ services:
|
||||||
# and resources set for the apps pods.
|
# and resources set for the apps pods.
|
||||||
targetCPUUtilizationPercentage: 80
|
targetCPUUtilizationPercentage: 80
|
||||||
|
|
||||||
|
automationWorkers:
|
||||||
|
# -- Whether or not to enable the automation worker service. If you disable this,
|
||||||
|
# automations will be processed by the apps service.
|
||||||
|
enabled: true
|
||||||
|
# @ignore (you shouldn't need to change this)
|
||||||
|
port: 4002
|
||||||
|
# -- The number of automation worker replicas to run.
|
||||||
|
replicaCount: 1
|
||||||
|
# -- The log level for the automation worker service.
|
||||||
|
logLevel: info
|
||||||
|
# -- The resources to use for automation worker pods. See
|
||||||
|
# <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/>
|
||||||
|
# for more information on how to set these.
|
||||||
|
resources: {}
|
||||||
|
# -- Extra environment variables to set for automation worker pods. Takes a list of
|
||||||
|
# name=value pairs.
|
||||||
|
extraEnv: []
|
||||||
|
# -- Startup probe configuration for automation worker pods. You shouldn't
|
||||||
|
# need to change this, but if you want to you can find more information
|
||||||
|
# here:
|
||||||
|
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
||||||
|
# @default -- HTTP health checks.
|
||||||
|
startupProbe:
|
||||||
|
# @ignore
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 4002
|
||||||
|
scheme: HTTP
|
||||||
|
# @ignore
|
||||||
|
failureThreshold: 30
|
||||||
|
# @ignore
|
||||||
|
periodSeconds: 3
|
||||||
|
# -- Readiness probe configuration for automation worker pods. You shouldn't
|
||||||
|
# need to change this, but if you want to you can find more information
|
||||||
|
# here:
|
||||||
|
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
||||||
|
# @default -- HTTP health checks.
|
||||||
|
readinessProbe:
|
||||||
|
# @ignore
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 4002
|
||||||
|
scheme: HTTP
|
||||||
|
# @ignore
|
||||||
|
periodSeconds: 3
|
||||||
|
# @ignore
|
||||||
|
failureThreshold: 1
|
||||||
|
# -- Liveness probe configuration for automation worker pods. You shouldn't
|
||||||
|
# need to change this, but if you want to you can find more information
|
||||||
|
# here:
|
||||||
|
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
||||||
|
# @default -- HTTP health checks.
|
||||||
|
livenessProbe:
|
||||||
|
# @ignore
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 4002
|
||||||
|
scheme: HTTP
|
||||||
|
# @ignore
|
||||||
|
failureThreshold: 3
|
||||||
|
# @ignore
|
||||||
|
periodSeconds: 30
|
||||||
|
autoscaling:
|
||||||
|
# -- Whether to enable horizontal pod autoscaling for the apps service.
|
||||||
|
enabled: false
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 10
|
||||||
|
# -- Target CPU utilization percentage for the automation worker service.
|
||||||
|
# Note that for autoscaling to work, you will need to have metrics-server
|
||||||
|
# configured, and resources set for the automation worker pods.
|
||||||
|
targetCPUUtilizationPercentage: 80
|
||||||
|
|
||||||
worker:
|
worker:
|
||||||
# @ignore (you shouldn't need to change this)
|
# @ignore (you shouldn't need to change this)
|
||||||
port: 4003
|
port: 4003
|
||||||
|
@ -285,6 +360,9 @@ services:
|
||||||
# <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/>
|
# <https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/>
|
||||||
# for more information on how to set these.
|
# for more information on how to set these.
|
||||||
resources: {}
|
resources: {}
|
||||||
|
# -- Extra environment variables to set for worker pods. Takes a list of
|
||||||
|
# name=value pairs.
|
||||||
|
extraEnv: []
|
||||||
# -- Startup probe configuration for worker pods. You shouldn't need to
|
# -- Startup probe configuration for worker pods. You shouldn't need to
|
||||||
# change this, but if you want to you can find more information here:
|
# change this, but if you want to you can find more information here:
|
||||||
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
# <https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/>
|
||||||
|
|
|
@ -84,13 +84,13 @@ 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.
|
- 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 `develop` branch with some information about what has changed and why.
|
- Once your work is completed, please raise a PR against the `master` branch with some information about what has changed and why.
|
||||||
|
|
||||||
### Getting Started For Contributors
|
### Getting Started For Contributors
|
||||||
|
|
||||||
#### 1. Prerequisites
|
#### 1. Prerequisites
|
||||||
|
|
||||||
- NodeJS version `18.x.x`
|
- NodeJS version `20.x.x`
|
||||||
- Python version `3.x`
|
- Python version `3.x`
|
||||||
|
|
||||||
### Using asdf (recommended)
|
### Using asdf (recommended)
|
||||||
|
@ -246,7 +246,7 @@ From here - to develop a change in pro, you can follow the below flow:
|
||||||
cd packages/pro
|
cd packages/pro
|
||||||
# get the base branch you are working from (same as monorepo)
|
# get the base branch you are working from (same as monorepo)
|
||||||
git fetch
|
git fetch
|
||||||
git checkout <develop | master>
|
git checkout master
|
||||||
# create a branch, named the same as the branch in your monorepo
|
# create a branch, named the same as the branch in your monorepo
|
||||||
git checkout -b <some branch>
|
git checkout -b <some branch>
|
||||||
... make changes
|
... make changes
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
## Dev Environment on Debian 11
|
|
||||||
|
|
||||||
### Install NVM & Node 14
|
|
||||||
|
|
||||||
NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating
|
|
||||||
|
|
||||||
Install NVM
|
|
||||||
|
|
||||||
```
|
|
||||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
Install Node 14
|
|
||||||
|
|
||||||
```
|
|
||||||
nvm install 14
|
|
||||||
```
|
|
||||||
|
|
||||||
### Install npm requirements
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install -g yarn jest lerna
|
|
||||||
```
|
|
||||||
|
|
||||||
### Install Docker and Docker Compose
|
|
||||||
|
|
||||||
```
|
|
||||||
apt install docker.io
|
|
||||||
pip3 install docker-compose
|
|
||||||
```
|
|
||||||
|
|
||||||
### Clone the repo
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/Budibase/budibase.git
|
|
||||||
```
|
|
||||||
|
|
||||||
### Check Versions
|
|
||||||
|
|
||||||
This setup process was tested on Debian 11 (bullseye) with version numbers show below. Your mileage may vary using anything else.
|
|
||||||
|
|
||||||
- Docker: 20.10.5
|
|
||||||
- Docker-Compose: 1.29.2
|
|
||||||
- Node: v14.20.1
|
|
||||||
- Yarn: 1.22.19
|
|
||||||
- Lerna: 5.1.4
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
```
|
|
||||||
cd budibase
|
|
||||||
yarn setup
|
|
||||||
```
|
|
||||||
|
|
||||||
The yarn setup command runs several build steps i.e.
|
|
||||||
|
|
||||||
```
|
|
||||||
node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
|
|
||||||
|
|
||||||
The dev version will be available on port 10000 i.e.
|
|
||||||
|
|
||||||
http://127.0.0.1:10000/builder/admin
|
|
||||||
|
|
||||||
### File descriptor issues with Vite and Chrome in Linux
|
|
||||||
|
|
||||||
If your dev environment stalls forever, with some network requests stuck in flight, it's likely that Chrome is trying to open more file descriptors than your system allows.
|
|
||||||
To fix this, apply the following tweaks.
|
|
||||||
|
|
||||||
Debian based distros:
|
|
||||||
Add `* - nofile 65536` to `/etc/security/limits.conf`.
|
|
||||||
|
|
||||||
Arch:
|
|
||||||
Add `DefaultLimitNOFILE=65536` to `/etc/systemd/system.conf`.
|
|
|
@ -1,84 +0,0 @@
|
||||||
## Dev Environment on MAC OSX 12 (Monterey)
|
|
||||||
|
|
||||||
### Install Homebrew
|
|
||||||
|
|
||||||
Install instructions [here](https://brew.sh/)
|
|
||||||
|
|
||||||
| **NOTE**: If you are working on a M1 Apple Silicon which is running Z shell, you could need to add
|
|
||||||
`eval $(/opt/homebrew/bin/brew shellenv)` line to your `.zshrc`. This will make your zsh to find the apps you install
|
|
||||||
through brew.
|
|
||||||
|
|
||||||
### Install Node
|
|
||||||
|
|
||||||
Budibase requires a recent version of node 14:
|
|
||||||
|
|
||||||
```
|
|
||||||
brew install node npm
|
|
||||||
node -v
|
|
||||||
```
|
|
||||||
|
|
||||||
### Install npm requirements
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install -g yarn jest lerna
|
|
||||||
```
|
|
||||||
|
|
||||||
### Install Docker and Docker Compose
|
|
||||||
|
|
||||||
```
|
|
||||||
brew install docker docker-compose
|
|
||||||
```
|
|
||||||
|
|
||||||
### Clone the repo
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/Budibase/budibase.git
|
|
||||||
```
|
|
||||||
|
|
||||||
### Check Versions
|
|
||||||
|
|
||||||
This setup process was tested on Mac OSX 12 (Monterey) with version numbers shown below. Your mileage may vary using anything else.
|
|
||||||
|
|
||||||
- Docker: 20.10.14
|
|
||||||
- Docker-Compose: 2.6.0
|
|
||||||
- Node: 14.20.1
|
|
||||||
- Yarn: 1.22.19
|
|
||||||
- Lerna: 5.1.4
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
```
|
|
||||||
cd budibase
|
|
||||||
yarn setup
|
|
||||||
```
|
|
||||||
|
|
||||||
The yarn setup command runs several build steps i.e.
|
|
||||||
|
|
||||||
```
|
|
||||||
node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
|
|
||||||
|
|
||||||
The dev version will be available on port 10000 i.e.
|
|
||||||
|
|
||||||
http://127.0.0.1:10000/builder/admin
|
|
||||||
|
|
||||||
| **NOTE**: If you are working on a M1 Apple Silicon, you will need to uncomment `# platform: linux/amd64` line in
|
|
||||||
[hosting/docker-compose-dev.yaml](../hosting/docker-compose.dev.yaml)
|
|
||||||
|
|
||||||
### Troubleshootings
|
|
||||||
|
|
||||||
#### Yarn setup errors
|
|
||||||
|
|
||||||
If there are errors with the `yarn setup` command, you can try installing nvm and node 14. This is the same as the instructions for Debian 11.
|
|
||||||
|
|
||||||
#### Node 14.20.1 not supported for arm64
|
|
||||||
|
|
||||||
If you are working with M1 or M2 Mac and trying the Node installation via `nvm`, probably you will find the error `curl: (22) The requested URL returned error: 404`.
|
|
||||||
|
|
||||||
Version `v14.20.1` is not supported for arm64; in order to use it, you can switch the CPU architecture for this by the following command:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
arch -x86_64 zsh #Run this before nvm install
|
|
||||||
```
|
|
|
@ -1,92 +0,0 @@
|
||||||
## Dev Environment on Windows 10/11 (WSL2)
|
|
||||||
|
|
||||||
### Install WSL with Ubuntu LTS
|
|
||||||
|
|
||||||
Enable WSL 2 on Windows 10/11 for docker support.
|
|
||||||
|
|
||||||
```
|
|
||||||
wsl --set-default-version 2
|
|
||||||
```
|
|
||||||
|
|
||||||
Install Ubuntu LTS.
|
|
||||||
|
|
||||||
```
|
|
||||||
wsl --install Ubuntu
|
|
||||||
```
|
|
||||||
|
|
||||||
Or follow the instruction here:
|
|
||||||
https://learn.microsoft.com/en-us/windows/wsl/install
|
|
||||||
|
|
||||||
### Install Docker in windows
|
|
||||||
|
|
||||||
Download the installer from docker and install it.
|
|
||||||
|
|
||||||
Check this url for more detailed instructions:
|
|
||||||
https://docs.docker.com/desktop/install/windows-install/
|
|
||||||
|
|
||||||
You should follow the next steps from within the Ubuntu terminal.
|
|
||||||
|
|
||||||
### Install NVM & Node 14
|
|
||||||
|
|
||||||
NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating
|
|
||||||
|
|
||||||
Install NVM
|
|
||||||
|
|
||||||
```
|
|
||||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
Install Node 14
|
|
||||||
|
|
||||||
```
|
|
||||||
nvm install 14
|
|
||||||
```
|
|
||||||
|
|
||||||
### Install npm requirements
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install -g yarn jest lerna
|
|
||||||
```
|
|
||||||
|
|
||||||
### Clone the repo
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/Budibase/budibase.git
|
|
||||||
```
|
|
||||||
|
|
||||||
### Check Versions
|
|
||||||
|
|
||||||
This setup process was tested on Windows 11 with version numbers show below. Your mileage may vary using anything else.
|
|
||||||
|
|
||||||
- Docker: 20.10.7
|
|
||||||
- Docker-Compose: 2.10.2
|
|
||||||
- Node: v14.20.1
|
|
||||||
- Yarn: 1.22.19
|
|
||||||
- Lerna: 5.5.4
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
```
|
|
||||||
cd budibase
|
|
||||||
yarn setup
|
|
||||||
```
|
|
||||||
|
|
||||||
The yarn setup command runs several build steps i.e.
|
|
||||||
|
|
||||||
```
|
|
||||||
node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev
|
|
||||||
```
|
|
||||||
|
|
||||||
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
|
|
||||||
|
|
||||||
The dev version will be available on port 10000 i.e.
|
|
||||||
|
|
||||||
http://127.0.0.1:10000/builder/admin
|
|
||||||
|
|
||||||
### Working with the code
|
|
||||||
|
|
||||||
Here are the instructions to work on the application from within Visual Studio Code (in Windows) through the WSL. All the commands and files are within the Ubuntu system and it should run as if you were working on a Linux machine.
|
|
||||||
|
|
||||||
https://code.visualstudio.com/docs/remote/wsl
|
|
||||||
|
|
||||||
Note you will be able to run the application from within the WSL terminal and you will be able to access the application from the a browser in Windows.
|
|
|
@ -22,6 +22,6 @@
|
||||||
"@types/react": "17.0.39",
|
"@types/react": "17.0.39",
|
||||||
"eslint": "8.10.0",
|
"eslint": "8.10.0",
|
||||||
"eslint-config-next": "12.1.0",
|
"eslint-config-next": "12.1.0",
|
||||||
"typescript": "4.6.2"
|
"typescript": "5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,6 +79,7 @@ done
|
||||||
|
|
||||||
# CouchDB needs the `_users` and `_replicator` databases to exist before it will
|
# CouchDB needs the `_users` and `_replicator` databases to exist before it will
|
||||||
# function correctly, so we create them here.
|
# function correctly, so we create them here.
|
||||||
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_users
|
sleep infinity
|
||||||
curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_replicator
|
curl -X PUT -u "${COUCHDB_USER}:${COUCHDB_PASSWORD}" http://localhost:5984/_users
|
||||||
|
curl -X PUT -u "${COUCHDB_USER}:${COUCHDB_PASSWORD}" http://localhost:5984/_replicator
|
||||||
sleep infinity
|
sleep infinity
|
||||||
|
|
|
@ -26,7 +26,7 @@ services:
|
||||||
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
|
BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL}
|
||||||
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
|
BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD}
|
||||||
PLUGINS_DIR: ${PLUGINS_DIR}
|
PLUGINS_DIR: ${PLUGINS_DIR}
|
||||||
OFFLINE_MODE: ${OFFLINE_MODE}
|
OFFLINE_MODE: ${OFFLINE_MODE:-}
|
||||||
depends_on:
|
depends_on:
|
||||||
- worker-service
|
- worker-service
|
||||||
- redis-service
|
- redis-service
|
||||||
|
@ -53,7 +53,7 @@ services:
|
||||||
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
|
||||||
REDIS_URL: redis-service:6379
|
REDIS_URL: redis-service:6379
|
||||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
OFFLINE_MODE: ${OFFLINE_MODE}
|
OFFLINE_MODE: ${OFFLINE_MODE:-}
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis-service
|
- redis-service
|
||||||
- minio-service
|
- minio-service
|
||||||
|
@ -109,7 +109,7 @@ services:
|
||||||
redis-service:
|
redis-service:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
image: redis
|
image: redis
|
||||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
command: redis-server --requirepass "${REDIS_PASSWORD}"
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
|
|
||||||
|
|
|
@ -257,6 +257,7 @@ http {
|
||||||
|
|
||||||
access_log off;
|
access_log off;
|
||||||
allow 127.0.0.1;
|
allow 127.0.0.1;
|
||||||
|
allow 10.0.0.0/8;
|
||||||
deny all;
|
deny all;
|
||||||
|
|
||||||
location /nginx_status {
|
location /nginx_status {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:18-slim as build
|
FROM node:20-slim as build
|
||||||
|
|
||||||
# install node-gyp dependencies
|
# install node-gyp dependencies
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends g++ make python3 jq
|
RUN apt-get update && apt-get install -y --no-install-recommends g++ make python3 jq
|
||||||
|
@ -42,7 +42,7 @@ COPY packages/string-templates packages/string-templates
|
||||||
FROM budibase/couchdb as runner
|
FROM budibase/couchdb as runner
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ENV TARGETARCH $TARGETARCH
|
ENV TARGETARCH $TARGETARCH
|
||||||
ENV NODE_MAJOR 18
|
ENV NODE_MAJOR 20
|
||||||
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
|
#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service)
|
||||||
# e.g. docker build --build-arg TARGETBUILD=aas ....
|
# e.g. docker build --build-arg TARGETBUILD=aas ....
|
||||||
ARG TARGETBUILD=single
|
ARG TARGETBUILD=single
|
||||||
|
|
|
@ -7,7 +7,7 @@ declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONME
|
||||||
[[ -z "${BUDIBASE_ENVIRONMENT}" ]] && export BUDIBASE_ENVIRONMENT=PRODUCTION
|
[[ -z "${BUDIBASE_ENVIRONMENT}" ]] && export BUDIBASE_ENVIRONMENT=PRODUCTION
|
||||||
[[ -z "${CLUSTER_PORT}" ]] && export CLUSTER_PORT=80
|
[[ -z "${CLUSTER_PORT}" ]] && export CLUSTER_PORT=80
|
||||||
[[ -z "${DEPLOYMENT_ENVIRONMENT}" ]] && export DEPLOYMENT_ENVIRONMENT=docker
|
[[ -z "${DEPLOYMENT_ENVIRONMENT}" ]] && export DEPLOYMENT_ENVIRONMENT=docker
|
||||||
[[ -z "${MINIO_URL}" ]] && export MINIO_URL=http://127.0.0.1:9000
|
[[ -z "${MINIO_URL}" ]] && [[ -z "${USE_S3}" ]] && export MINIO_URL=http://127.0.0.1:9000
|
||||||
[[ -z "${NODE_ENV}" ]] && export NODE_ENV=production
|
[[ -z "${NODE_ENV}" ]] && export NODE_ENV=production
|
||||||
[[ -z "${POSTHOG_TOKEN}" ]] && export POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
[[ -z "${POSTHOG_TOKEN}" ]] && export POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
|
||||||
[[ -z "${TENANT_FEATURE_FLAGS}" ]] && export TENANT_FEATURE_FLAGS="*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR"
|
[[ -z "${TENANT_FEATURE_FLAGS}" ]] && export TENANT_FEATURE_FLAGS="*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR"
|
||||||
|
@ -77,7 +77,12 @@ mkdir -p ${DATA_DIR}/minio
|
||||||
chown -R couchdb:couchdb ${DATA_DIR}/couch
|
chown -R couchdb:couchdb ${DATA_DIR}/couch
|
||||||
redis-server --requirepass $REDIS_PASSWORD > /dev/stdout 2>&1 &
|
redis-server --requirepass $REDIS_PASSWORD > /dev/stdout 2>&1 &
|
||||||
/bbcouch-runner.sh &
|
/bbcouch-runner.sh &
|
||||||
/minio/minio server --console-address ":9001" ${DATA_DIR}/minio > /dev/stdout 2>&1 &
|
|
||||||
|
# only start minio if use s3 isn't passed
|
||||||
|
if [[ -z "${USE_S3}" ]]; then
|
||||||
|
/minio/minio server --console-address ":9001" ${DATA_DIR}/minio > /dev/stdout 2>&1 &
|
||||||
|
fi
|
||||||
|
|
||||||
/etc/init.d/nginx restart
|
/etc/init.d/nginx restart
|
||||||
if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then
|
if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then
|
||||||
# Add monthly cron job to renew certbot certificate
|
# Add monthly cron job to renew certbot certificate
|
||||||
|
|
|
@ -207,8 +207,7 @@ Desde comunicar un bug a solventar un error en el codigo, toda contribucion es a
|
||||||
implementar una nueva funcionalidad o un realizar un cambio en la API, por favor crea un [nuevo mensaje aqui](https://github.com/Budibase/budibase/issues),
|
implementar una nueva funcionalidad o un realizar un cambio en la API, por favor crea un [nuevo mensaje aqui](https://github.com/Budibase/budibase/issues),
|
||||||
de esta manera nos encargaremos que tu trabajo no sea en vano.
|
de esta manera nos encargaremos que tu trabajo no sea en vano.
|
||||||
|
|
||||||
Aqui tienes instrucciones de como configurar tu entorno Budibase para [Debian](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-DEBIAN.md)
|
Aqui tienes instrucciones de como configurar tu entorno Budibase para [aquí](https://github.com/Budibase/budibase/tree/HEAD/docs/CONTRIBUTING.md).
|
||||||
y [MacOSX](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-MACOSX.md)
|
|
||||||
|
|
||||||
### No estas seguro por donde empezar?
|
### No estas seguro por donde empezar?
|
||||||
Un buen lugar para empezar a contribuir con nosotros es [aqui](https://github.com/Budibase/budibase/projects/22).
|
Un buen lugar para empezar a contribuir con nosotros es [aqui](https://github.com/Budibase/budibase/projects/22).
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
{
|
{
|
||||||
"version": "2.13.36",
|
"version": "2.17.3",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*",
|
||||||
|
"!packages/account-portal",
|
||||||
|
"packages/account-portal/packages/*"
|
||||||
],
|
],
|
||||||
"useNx": true,
|
"useNx": true,
|
||||||
|
"concurrency": 20,
|
||||||
"command": {
|
"command": {
|
||||||
"publish": {
|
"publish": {
|
||||||
"ignoreChanges": [
|
"ignoreChanges": [
|
||||||
|
|
28
package.json
28
package.json
|
@ -6,13 +6,14 @@
|
||||||
"@babel/eslint-parser": "^7.22.5",
|
"@babel/eslint-parser": "^7.22.5",
|
||||||
"@babel/preset-env": "^7.22.5",
|
"@babel/preset-env": "^7.22.5",
|
||||||
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
||||||
"@typescript-eslint/parser": "6.7.2",
|
"@types/node": "20.10.0",
|
||||||
|
"@typescript-eslint/parser": "6.9.0",
|
||||||
"esbuild": "^0.18.17",
|
"esbuild": "^0.18.17",
|
||||||
"esbuild-node-externals": "^1.8.0",
|
"esbuild-node-externals": "^1.8.0",
|
||||||
"eslint": "^8.44.0",
|
"eslint": "^8.52.0",
|
||||||
"eslint-plugin-import": "^2.29.0",
|
"eslint-plugin-import": "^2.29.0",
|
||||||
"eslint-plugin-local-rules": "^2.0.0",
|
"eslint-plugin-local-rules": "^2.0.0",
|
||||||
"eslint-plugin-svelte": "^2.32.2",
|
"eslint-plugin-svelte": "^2.34.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"kill-port": "^1.6.1",
|
"kill-port": "^1.6.1",
|
||||||
"lerna": "7.1.1",
|
"lerna": "7.1.1",
|
||||||
|
@ -22,7 +23,7 @@
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
"prettier-plugin-svelte": "^2.3.0",
|
"prettier-plugin-svelte": "^2.3.0",
|
||||||
"svelte": "3.49.0",
|
"svelte": "3.49.0",
|
||||||
"svelte-eslint-parser": "^0.32.0",
|
"svelte-eslint-parser": "^0.33.1",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
|
@ -39,13 +40,16 @@
|
||||||
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
||||||
"nuke:packages": "yarn run restore",
|
"nuke:packages": "yarn run restore",
|
||||||
"nuke:docker": "lerna run --stream dev:stack:nuke",
|
"nuke:docker": "lerna run --stream dev:stack:nuke",
|
||||||
"clean": "lerna clean -y",
|
"clean": "lerna clean -y && echo Cleaning top level node modules 🧹 && rm -rf ./node_modules && echo Done! 🚀",
|
||||||
"kill-builder": "kill-port 3000",
|
"kill-builder": "kill-port 3000",
|
||||||
"kill-server": "kill-port 4001 4002",
|
"kill-server": "kill-port 4001 4002",
|
||||||
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
"kill-accountportal": "kill-port 3001 4003",
|
||||||
"dev": "yarn run kill-all && lerna run --parallel prebuild && lerna run --stream dev:builder",
|
"kill-all": "yarn run kill-builder && yarn run kill-server && yarn kill-accountportal",
|
||||||
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
"dev": "yarn run kill-all && lerna run --parallel prebuild && lerna run --stream dev --ignore=@budibase/account-portal-ui --ignore @budibase/account-portal-server",
|
||||||
"dev:server": "yarn run kill-server && lerna run --stream dev:builder --scope @budibase/worker --scope @budibase/server",
|
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up --ignore @budibase/account-portal-server && lerna run --stream dev --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker --ignore=@budibase/account-portal-ui --ignore @budibase/account-portal-server",
|
||||||
|
"dev:server": "yarn run kill-server && lerna run --stream dev --scope @budibase/worker --scope @budibase/server",
|
||||||
|
"dev:accountportal": "yarn kill-accountportal && lerna run dev --stream --scope @budibase/account-portal-ui --scope @budibase/account-portal-server",
|
||||||
|
"dev:all": "yarn run kill-all && lerna run --stream dev",
|
||||||
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
|
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
|
||||||
"dev:docker": "yarn build --scope @budibase/server --scope @budibase/worker && 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 --scope @budibase/server --scope @budibase/worker && 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",
|
||||||
|
@ -84,7 +88,9 @@
|
||||||
},
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*",
|
||||||
|
"!packages/account-portal",
|
||||||
|
"packages/account-portal/packages/*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
@ -94,7 +100,7 @@
|
||||||
"@budibase/types": "0.0.0"
|
"@budibase/types": "0.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=20.0.0 <21.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit cc12291732ee902dc832bc7d93cf2086ffdf0cff
|
|
@ -21,7 +21,7 @@
|
||||||
"test:watch": "jest --watchAll"
|
"test:watch": "jest --watchAll"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/nano": "10.1.3",
|
"@budibase/nano": "10.1.5",
|
||||||
"@budibase/pouchdb-replication-stream": "1.2.10",
|
"@budibase/pouchdb-replication-stream": "1.2.10",
|
||||||
"@budibase/shared-core": "0.0.0",
|
"@budibase/shared-core": "0.0.0",
|
||||||
"@budibase/types": "0.0.0",
|
"@budibase/types": "0.0.0",
|
||||||
|
@ -32,6 +32,7 @@
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"bull": "4.10.1",
|
"bull": "4.10.1",
|
||||||
"correlation-id": "4.0.0",
|
"correlation-id": "4.0.0",
|
||||||
|
"dd-trace": "5.0.0",
|
||||||
"dotenv": "16.0.1",
|
"dotenv": "16.0.1",
|
||||||
"ioredis": "5.3.2",
|
"ioredis": "5.3.2",
|
||||||
"joi": "17.6.0",
|
"joi": "17.6.0",
|
||||||
|
@ -64,7 +65,6 @@
|
||||||
"@types/cookies": "0.7.8",
|
"@types/cookies": "0.7.8",
|
||||||
"@types/jest": "29.5.5",
|
"@types/jest": "29.5.5",
|
||||||
"@types/lodash": "4.14.200",
|
"@types/lodash": "4.14.200",
|
||||||
"@types/node": "18.17.0",
|
|
||||||
"@types/node-fetch": "2.6.4",
|
"@types/node-fetch": "2.6.4",
|
||||||
"@types/pouchdb": "6.4.0",
|
"@types/pouchdb": "6.4.0",
|
||||||
"@types/redlock": "4.0.3",
|
"@types/redlock": "4.0.3",
|
||||||
|
@ -73,8 +73,8 @@
|
||||||
"@types/uuid": "8.3.4",
|
"@types/uuid": "8.3.4",
|
||||||
"chance": "1.1.8",
|
"chance": "1.1.8",
|
||||||
"ioredis-mock": "8.9.0",
|
"ioredis-mock": "8.9.0",
|
||||||
"jest": "29.6.2",
|
"jest": "29.7.0",
|
||||||
"jest-environment-node": "29.6.2",
|
"jest-environment-node": "29.7.0",
|
||||||
"jest-serial-runner": "1.2.1",
|
"jest-serial-runner": "1.2.1",
|
||||||
"pino-pretty": "10.0.0",
|
"pino-pretty": "10.0.0",
|
||||||
"pouchdb-adapter-memory": "7.2.2",
|
"pouchdb-adapter-memory": "7.2.2",
|
||||||
|
|
|
@ -18,14 +18,15 @@ export enum TTL {
|
||||||
ONE_DAY = 86400,
|
ONE_DAY = 86400,
|
||||||
}
|
}
|
||||||
|
|
||||||
function performExport(funcName: string) {
|
export const keys = (...args: Parameters<typeof GENERIC.keys>) =>
|
||||||
// @ts-ignore
|
GENERIC.keys(...args)
|
||||||
return (...args: any) => GENERIC[funcName](...args)
|
export const get = (...args: Parameters<typeof GENERIC.get>) =>
|
||||||
}
|
GENERIC.get(...args)
|
||||||
|
export const store = (...args: Parameters<typeof GENERIC.store>) =>
|
||||||
export const keys = performExport("keys")
|
GENERIC.store(...args)
|
||||||
export const get = performExport("get")
|
export const destroy = (...args: Parameters<typeof GENERIC.delete>) =>
|
||||||
export const store = performExport("store")
|
GENERIC.delete(...args)
|
||||||
export const destroy = performExport("delete")
|
export const withCache = (...args: Parameters<typeof GENERIC.withCache>) =>
|
||||||
export const withCache = performExport("withCache")
|
GENERIC.withCache(...args)
|
||||||
export const bustCache = performExport("bustCache")
|
export const bustCache = (...args: Parameters<typeof GENERIC.bustCache>) =>
|
||||||
|
GENERIC.bustCache(...args)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as redis from "../redis/init"
|
import * as redis from "../redis/init"
|
||||||
import * as utils from "../utils"
|
import * as utils from "../utils"
|
||||||
import { Duration, DurationType } from "../utils"
|
import { Duration } from "../utils"
|
||||||
|
|
||||||
const TTL_SECONDS = Duration.fromHours(1).toSeconds()
|
const TTL_SECONDS = Duration.fromHours(1).toSeconds()
|
||||||
|
|
||||||
|
@ -32,7 +32,18 @@ export async function getCode(code: string): Promise<PasswordReset> {
|
||||||
const client = await redis.getPasswordResetClient()
|
const client = await redis.getPasswordResetClient()
|
||||||
const value = (await client.get(code)) as PasswordReset | undefined
|
const value = (await client.get(code)) as PasswordReset | undefined
|
||||||
if (!value) {
|
if (!value) {
|
||||||
throw "Provided information is not valid, cannot reset password - please try again."
|
throw new Error(
|
||||||
|
"Provided information is not valid, cannot reset password - please try again."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a reset code this will invalidate it.
|
||||||
|
* @param code The code provided via the email link.
|
||||||
|
*/
|
||||||
|
export async function invalidateCode(code: string): Promise<void> {
|
||||||
|
const client = await redis.getPasswordResetClient()
|
||||||
|
await client.delete(code)
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import { DBTestConfiguration } from "../../../tests/extra"
|
import { DBTestConfiguration } from "../../../tests/extra"
|
||||||
import {
|
import { structures } from "../../../tests"
|
||||||
structures,
|
|
||||||
expectFunctionWasCalledTimesWith,
|
|
||||||
mocks,
|
|
||||||
} from "../../../tests"
|
|
||||||
import { Writethrough } from "../writethrough"
|
import { Writethrough } from "../writethrough"
|
||||||
import { getDB } from "../../db"
|
import { getDB } from "../../db"
|
||||||
|
import { Document } from "@budibase/types"
|
||||||
import tk from "timekeeper"
|
import tk from "timekeeper"
|
||||||
|
|
||||||
tk.freeze(Date.now())
|
tk.freeze(Date.now())
|
||||||
|
|
||||||
|
interface ValueDoc extends Document {
|
||||||
|
value: any
|
||||||
|
}
|
||||||
|
|
||||||
const DELAY = 5000
|
const DELAY = 5000
|
||||||
|
|
||||||
describe("writethrough", () => {
|
describe("writethrough", () => {
|
||||||
|
@ -117,7 +118,7 @@ describe("writethrough", () => {
|
||||||
describe("get", () => {
|
describe("get", () => {
|
||||||
it("should be able to retrieve", async () => {
|
it("should be able to retrieve", async () => {
|
||||||
await config.doInTenant(async () => {
|
await config.doInTenant(async () => {
|
||||||
const response = await writethrough.get(docId)
|
const response = await writethrough.get<ValueDoc>(docId)
|
||||||
expect(response.value).toBe(4)
|
expect(response.value).toBe(4)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,7 +7,7 @@ import * as locks from "../redis/redlockImpl"
|
||||||
const DEFAULT_WRITE_RATE_MS = 10000
|
const DEFAULT_WRITE_RATE_MS = 10000
|
||||||
let CACHE: BaseCache | null = null
|
let CACHE: BaseCache | null = null
|
||||||
|
|
||||||
interface CacheItem {
|
interface CacheItem<T extends Document> {
|
||||||
doc: any
|
doc: any
|
||||||
lastWrite: number
|
lastWrite: number
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,10 @@ function makeCacheKey(db: Database, key: string) {
|
||||||
return db.name + key
|
return db.name + key
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeCacheItem(doc: any, lastWrite: number | null = null): CacheItem {
|
function makeCacheItem<T extends Document>(
|
||||||
|
doc: T,
|
||||||
|
lastWrite: number | null = null
|
||||||
|
): CacheItem<T> {
|
||||||
return { doc, lastWrite: lastWrite || Date.now() }
|
return { doc, lastWrite: lastWrite || Date.now() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,7 +38,7 @@ async function put(
|
||||||
) {
|
) {
|
||||||
const cache = await getCache()
|
const cache = await getCache()
|
||||||
const key = doc._id
|
const key = doc._id
|
||||||
let cacheItem: CacheItem | undefined
|
let cacheItem: CacheItem<any> | undefined
|
||||||
if (key) {
|
if (key) {
|
||||||
cacheItem = await cache.get(makeCacheKey(db, key))
|
cacheItem = await cache.get(makeCacheKey(db, key))
|
||||||
}
|
}
|
||||||
|
@ -53,11 +56,8 @@ async function put(
|
||||||
const writeDb = async (toWrite: any) => {
|
const writeDb = async (toWrite: any) => {
|
||||||
// doc should contain the _id and _rev
|
// doc should contain the _id and _rev
|
||||||
const response = await db.put(toWrite, { force: true })
|
const response = await db.put(toWrite, { force: true })
|
||||||
output = {
|
output._id = response.id
|
||||||
...doc,
|
output._rev = response.rev
|
||||||
_id: response.id,
|
|
||||||
_rev: response.rev,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await writeDb(doc)
|
await writeDb(doc)
|
||||||
|
@ -84,12 +84,12 @@ async function put(
|
||||||
return { ok: true, id: output._id, rev: output._rev }
|
return { ok: true, id: output._id, rev: output._rev }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function get(db: Database, id: string): Promise<any> {
|
async function get<T extends Document>(db: Database, id: string): Promise<T> {
|
||||||
const cache = await getCache()
|
const cache = await getCache()
|
||||||
const cacheKey = makeCacheKey(db, id)
|
const cacheKey = makeCacheKey(db, id)
|
||||||
let cacheItem: CacheItem = await cache.get(cacheKey)
|
let cacheItem: CacheItem<T> = await cache.get(cacheKey)
|
||||||
if (!cacheItem) {
|
if (!cacheItem) {
|
||||||
const doc = await db.get(id)
|
const doc = await db.get<T>(id)
|
||||||
cacheItem = makeCacheItem(doc)
|
cacheItem = makeCacheItem(doc)
|
||||||
await cache.store(cacheKey, cacheItem)
|
await cache.store(cacheKey, cacheItem)
|
||||||
}
|
}
|
||||||
|
@ -123,8 +123,8 @@ export class Writethrough {
|
||||||
return put(this.db, doc, writeRateMs)
|
return put(this.db, doc, writeRateMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: string) {
|
async get<T extends Document>(id: string) {
|
||||||
return get(this.db, id)
|
return get<T>(this.db, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(docOrId: any, rev?: any) {
|
async remove(docOrId: any, rev?: any) {
|
||||||
|
|
|
@ -11,24 +11,7 @@ export enum Cookie {
|
||||||
OIDC_CONFIG = "budibase:oidc:config",
|
OIDC_CONFIG = "budibase:oidc:config",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Header {
|
export { Header } from "@budibase/shared-core"
|
||||||
API_KEY = "x-budibase-api-key",
|
|
||||||
LICENSE_KEY = "x-budibase-license-key",
|
|
||||||
API_VER = "x-budibase-api-version",
|
|
||||||
APP_ID = "x-budibase-app-id",
|
|
||||||
SESSION_ID = "x-budibase-session-id",
|
|
||||||
TYPE = "x-budibase-type",
|
|
||||||
PREVIEW_ROLE = "x-budibase-role",
|
|
||||||
TENANT_ID = "x-budibase-tenant-id",
|
|
||||||
VERIFICATION_CODE = "x-budibase-verification-code",
|
|
||||||
RETURN_VERIFICATION_CODE = "x-budibase-return-verification-code",
|
|
||||||
RESET_PASSWORD_CODE = "x-budibase-reset-password-code",
|
|
||||||
RETURN_RESET_PASSWORD_CODE = "x-budibase-return-reset-password-code",
|
|
||||||
TOKEN = "x-budibase-token",
|
|
||||||
CSRF_TOKEN = "x-csrf-token",
|
|
||||||
CORRELATION_ID = "x-budibase-correlation-id",
|
|
||||||
AUTHORIZATION = "authorization",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum GlobalRole {
|
export enum GlobalRole {
|
||||||
OWNER = "owner",
|
OWNER = "owner",
|
||||||
|
|
|
@ -134,7 +134,7 @@ export async function doInContext(appId: string, task: any): Promise<any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function doInTenant<T>(
|
export async function doInTenant<T>(
|
||||||
tenantId: string | null,
|
tenantId: string | undefined,
|
||||||
task: () => T
|
task: () => T
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
// make sure default always selected in single tenancy
|
// make sure default always selected in single tenancy
|
||||||
|
@ -335,3 +335,11 @@ export function isScim(): boolean {
|
||||||
const scimCall = context?.isScim
|
const scimCall = context?.isScim
|
||||||
return !!scimCall
|
return !!scimCall
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCurrentContext(): ContextMap | undefined {
|
||||||
|
try {
|
||||||
|
return Context.get()
|
||||||
|
} catch (e) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { IdentityContext } from "@budibase/types"
|
import { IdentityContext } from "@budibase/types"
|
||||||
|
import { ExecutionTimeTracker } from "../timers"
|
||||||
|
|
||||||
// keep this out of Budibase types, don't want to expose context info
|
// keep this out of Budibase types, don't want to expose context info
|
||||||
export type ContextMap = {
|
export type ContextMap = {
|
||||||
|
@ -9,4 +10,5 @@ export type ContextMap = {
|
||||||
isScim?: boolean
|
isScim?: boolean
|
||||||
automationId?: string
|
automationId?: string
|
||||||
isMigrating?: boolean
|
isMigrating?: boolean
|
||||||
|
jsExecutionTracker?: ExecutionTimeTracker
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,9 @@ import { getPouchDB } from "./pouchDB"
|
||||||
import { WriteStream, ReadStream } from "fs"
|
import { WriteStream, ReadStream } from "fs"
|
||||||
import { newid } from "../../docIds/newid"
|
import { newid } from "../../docIds/newid"
|
||||||
import { SQLITE_DESIGN_DOC_ID } from "../../constants"
|
import { SQLITE_DESIGN_DOC_ID } from "../../constants"
|
||||||
|
import { DDInstrumentedDatabase } from "../instrumentation"
|
||||||
|
|
||||||
|
const DATABASE_NOT_FOUND = "Database does not exist."
|
||||||
|
|
||||||
function buildNano(couchInfo: { url: string; cookie: string }) {
|
function buildNano(couchInfo: { url: string; cookie: string }) {
|
||||||
return Nano({
|
return Nano({
|
||||||
|
@ -31,15 +34,15 @@ function buildNano(couchInfo: { url: string; cookie: string }) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DBCall<T> = () => Promise<T>
|
||||||
|
|
||||||
export function DatabaseWithConnection(
|
export function DatabaseWithConnection(
|
||||||
dbName: string,
|
dbName: string,
|
||||||
connection: string,
|
connection: string,
|
||||||
opts?: DatabaseOpts
|
opts?: DatabaseOpts
|
||||||
) {
|
) {
|
||||||
if (!connection) {
|
const db = new DatabaseImpl(dbName, opts, connection)
|
||||||
throw new Error("Must provide connection details")
|
return new DDInstrumentedDatabase(db)
|
||||||
}
|
|
||||||
return new DatabaseImpl(dbName, opts, connection)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DatabaseImpl implements Database {
|
export class DatabaseImpl implements Database {
|
||||||
|
@ -80,7 +83,11 @@ export class DatabaseImpl implements Database {
|
||||||
return this.instanceNano || DatabaseImpl.nano
|
return this.instanceNano || DatabaseImpl.nano
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkSetup() {
|
private getDb() {
|
||||||
|
return this.nano().db.use(this.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkAndCreateDb() {
|
||||||
let shouldCreate = !this.pouchOpts?.skip_setup
|
let shouldCreate = !this.pouchOpts?.skip_setup
|
||||||
// check exists in a lightweight fashion
|
// check exists in a lightweight fashion
|
||||||
let exists = await this.exists()
|
let exists = await this.exists()
|
||||||
|
@ -97,14 +104,22 @@ export class DatabaseImpl implements Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.nano().db.use(this.name)
|
return this.getDb()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateOutput(fnc: any) {
|
// this function fetches the DB and handles if DB creation is needed
|
||||||
|
private async performCall<T>(
|
||||||
|
call: (db: Nano.DocumentScope<any>) => Promise<DBCall<T>> | DBCall<T>
|
||||||
|
): Promise<any> {
|
||||||
|
const db = this.getDb()
|
||||||
|
const fnc = await call(db)
|
||||||
try {
|
try {
|
||||||
return await fnc()
|
return await fnc()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.statusCode) {
|
if (err.statusCode === 404 && err.reason === DATABASE_NOT_FOUND) {
|
||||||
|
await this.checkAndCreateDb()
|
||||||
|
return await this.performCall(call)
|
||||||
|
} else if (err.statusCode) {
|
||||||
err.status = err.statusCode
|
err.status = err.statusCode
|
||||||
}
|
}
|
||||||
throw err
|
throw err
|
||||||
|
@ -112,11 +127,12 @@ export class DatabaseImpl implements Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
async get<T extends Document>(id?: string): Promise<T> {
|
async get<T extends Document>(id?: string): Promise<T> {
|
||||||
const db = await this.checkSetup()
|
return this.performCall(db => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw new Error("Unable to get doc without a valid _id.")
|
throw new Error("Unable to get doc without a valid _id.")
|
||||||
}
|
}
|
||||||
return this.updateOutput(() => db.get(id))
|
return () => db.get(id)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMultiple<T extends Document>(
|
async getMultiple<T extends Document>(
|
||||||
|
@ -149,22 +165,23 @@ export class DatabaseImpl implements Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(idOrDoc: string | Document, rev?: string) {
|
async remove(idOrDoc: string | Document, rev?: string) {
|
||||||
const db = await this.checkSetup()
|
return this.performCall(db => {
|
||||||
let _id: string
|
let _id: string
|
||||||
let _rev: string
|
let _rev: string
|
||||||
|
|
||||||
if (isDocument(idOrDoc)) {
|
if (isDocument(idOrDoc)) {
|
||||||
_id = idOrDoc._id!
|
_id = idOrDoc._id!
|
||||||
_rev = idOrDoc._rev!
|
_rev = idOrDoc._rev!
|
||||||
} else {
|
} else {
|
||||||
_id = idOrDoc
|
_id = idOrDoc
|
||||||
_rev = rev!
|
_rev = rev!
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_id || !_rev) {
|
if (!_id || !_rev) {
|
||||||
throw new Error("Unable to remove doc without a valid _id and _rev.")
|
throw new Error("Unable to remove doc without a valid _id and _rev.")
|
||||||
}
|
}
|
||||||
return this.updateOutput(() => db.destroy(_id, _rev))
|
return () => db.destroy(_id, _rev)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async post(document: AnyDocument, opts?: DatabasePutOpts) {
|
async post(document: AnyDocument, opts?: DatabasePutOpts) {
|
||||||
|
@ -178,36 +195,39 @@ export class DatabaseImpl implements Database {
|
||||||
if (!document._id) {
|
if (!document._id) {
|
||||||
throw new Error("Cannot store document without _id field.")
|
throw new Error("Cannot store document without _id field.")
|
||||||
}
|
}
|
||||||
const db = await this.checkSetup()
|
return this.performCall(async db => {
|
||||||
if (!document.createdAt) {
|
if (!document.createdAt) {
|
||||||
document.createdAt = new Date().toISOString()
|
document.createdAt = new Date().toISOString()
|
||||||
}
|
}
|
||||||
document.updatedAt = new Date().toISOString()
|
document.updatedAt = new Date().toISOString()
|
||||||
if (opts?.force && document._id) {
|
if (opts?.force && document._id) {
|
||||||
try {
|
try {
|
||||||
const existing = await this.get(document._id)
|
const existing = await this.get(document._id)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
document._rev = existing._rev
|
document._rev = existing._rev
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.status !== 404) {
|
if (err.status !== 404) {
|
||||||
throw err
|
throw err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return () => db.insert(document)
|
||||||
return this.updateOutput(() => db.insert(document))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkDocs(documents: AnyDocument[]) {
|
async bulkDocs(documents: AnyDocument[]) {
|
||||||
const db = await this.checkSetup()
|
return this.performCall(db => {
|
||||||
return this.updateOutput(() => db.bulk({ docs: documents }))
|
return () => db.bulk({ docs: documents })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async allDocs<T extends Document>(
|
async allDocs<T extends Document>(
|
||||||
params: DatabaseQueryOpts
|
params: DatabaseQueryOpts
|
||||||
): Promise<AllDocsResponse<T>> {
|
): Promise<AllDocsResponse<T>> {
|
||||||
const db = await this.checkSetup()
|
return this.performCall(db => {
|
||||||
return this.updateOutput(() => db.list(params))
|
return () => db.list(params)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async sql<T>(sql: string): Promise<T> {
|
async sql<T>(sql: string): Promise<T> {
|
||||||
|
@ -229,9 +249,10 @@ export class DatabaseImpl implements Database {
|
||||||
viewName: string,
|
viewName: string,
|
||||||
params: DatabaseQueryOpts
|
params: DatabaseQueryOpts
|
||||||
): Promise<AllDocsResponse<T>> {
|
): Promise<AllDocsResponse<T>> {
|
||||||
const db = await this.checkSetup()
|
return this.performCall(db => {
|
||||||
const [database, view] = viewName.split("/")
|
const [database, view] = viewName.split("/")
|
||||||
return this.updateOutput(() => db.view(database, view, params))
|
return () => db.view(database, view, params)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async destroy() {
|
async destroy() {
|
||||||
|
@ -248,8 +269,9 @@ export class DatabaseImpl implements Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
async compact() {
|
async compact() {
|
||||||
const db = await this.checkSetup()
|
return this.performCall(db => {
|
||||||
return this.updateOutput(() => db.compact())
|
return () => db.compact()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// All below functions are in-frequently called, just utilise PouchDB
|
// All below functions are in-frequently called, just utilise PouchDB
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { directCouchQuery, DatabaseImpl } from "./couch"
|
import { directCouchQuery, DatabaseImpl } from "./couch"
|
||||||
import { CouchFindOptions, Database, DatabaseOpts } from "@budibase/types"
|
import { CouchFindOptions, Database, DatabaseOpts } from "@budibase/types"
|
||||||
|
import { DDInstrumentedDatabase } from "./instrumentation"
|
||||||
|
|
||||||
export function getDB(dbName: string, opts?: DatabaseOpts): Database {
|
export function getDB(dbName: string, opts?: DatabaseOpts): Database {
|
||||||
return new DatabaseImpl(dbName, opts)
|
return new DDInstrumentedDatabase(new DatabaseImpl(dbName, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
// we have to use a callback for this so that we can close
|
// we have to use a callback for this so that we can close
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
import {
|
||||||
|
DocumentScope,
|
||||||
|
DocumentDestroyResponse,
|
||||||
|
DocumentInsertResponse,
|
||||||
|
DocumentBulkResponse,
|
||||||
|
OkResponse,
|
||||||
|
} from "@budibase/nano"
|
||||||
|
import {
|
||||||
|
AllDocsResponse,
|
||||||
|
AnyDocument,
|
||||||
|
Database,
|
||||||
|
DatabaseDumpOpts,
|
||||||
|
DatabasePutOpts,
|
||||||
|
DatabaseQueryOpts,
|
||||||
|
Document,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import tracer from "dd-trace"
|
||||||
|
import { Writable } from "stream"
|
||||||
|
|
||||||
|
export class DDInstrumentedDatabase implements Database {
|
||||||
|
constructor(private readonly db: Database) {}
|
||||||
|
|
||||||
|
get name(): string {
|
||||||
|
return this.db.name
|
||||||
|
}
|
||||||
|
|
||||||
|
exists(): Promise<boolean> {
|
||||||
|
return tracer.trace("db.exists", span => {
|
||||||
|
span?.addTags({ db_name: this.name })
|
||||||
|
return this.db.exists()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get<T extends Document>(id?: string | undefined): Promise<T> {
|
||||||
|
return tracer.trace("db.get", span => {
|
||||||
|
span?.addTags({ db_name: this.name, doc_id: id })
|
||||||
|
return this.db.get(id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getMultiple<T extends Document>(
|
||||||
|
ids: string[],
|
||||||
|
opts?: { allowMissing?: boolean | undefined } | undefined
|
||||||
|
): Promise<T[]> {
|
||||||
|
return tracer.trace("db.getMultiple", span => {
|
||||||
|
span?.addTags({
|
||||||
|
db_name: this.name,
|
||||||
|
num_docs: ids.length,
|
||||||
|
allow_missing: opts?.allowMissing,
|
||||||
|
})
|
||||||
|
return this.db.getMultiple(ids, opts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(
|
||||||
|
id: string | Document,
|
||||||
|
rev?: string | undefined
|
||||||
|
): Promise<DocumentDestroyResponse> {
|
||||||
|
return tracer.trace("db.remove", span => {
|
||||||
|
span?.addTags({ db_name: this.name, doc_id: id })
|
||||||
|
return this.db.remove(id, rev)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
put(
|
||||||
|
document: AnyDocument,
|
||||||
|
opts?: DatabasePutOpts | undefined
|
||||||
|
): Promise<DocumentInsertResponse> {
|
||||||
|
return tracer.trace("db.put", span => {
|
||||||
|
span?.addTags({ db_name: this.name, doc_id: document._id })
|
||||||
|
return this.db.put(document, opts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkDocs(documents: AnyDocument[]): Promise<DocumentBulkResponse[]> {
|
||||||
|
return tracer.trace("db.bulkDocs", span => {
|
||||||
|
span?.addTags({ db_name: this.name, num_docs: documents.length })
|
||||||
|
return this.db.bulkDocs(documents)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
allDocs<T extends Document>(
|
||||||
|
params: DatabaseQueryOpts
|
||||||
|
): Promise<AllDocsResponse<T>> {
|
||||||
|
return tracer.trace("db.allDocs", span => {
|
||||||
|
span?.addTags({ db_name: this.name })
|
||||||
|
return this.db.allDocs(params)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
query<T extends Document>(
|
||||||
|
viewName: string,
|
||||||
|
params: DatabaseQueryOpts
|
||||||
|
): Promise<AllDocsResponse<T>> {
|
||||||
|
return tracer.trace("db.query", span => {
|
||||||
|
span?.addTags({ db_name: this.name, view_name: viewName })
|
||||||
|
return this.db.query(viewName, params)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): Promise<void | OkResponse> {
|
||||||
|
return tracer.trace("db.destroy", span => {
|
||||||
|
span?.addTags({ db_name: this.name })
|
||||||
|
return this.db.destroy()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
compact(): Promise<void | OkResponse> {
|
||||||
|
return tracer.trace("db.compact", span => {
|
||||||
|
span?.addTags({ db_name: this.name })
|
||||||
|
return this.db.compact()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
dump(stream: Writable, opts?: DatabaseDumpOpts | undefined): Promise<any> {
|
||||||
|
return tracer.trace("db.dump", span => {
|
||||||
|
span?.addTags({ db_name: this.name })
|
||||||
|
return this.db.dump(stream, opts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
load(...args: any[]): Promise<any> {
|
||||||
|
return tracer.trace("db.load", span => {
|
||||||
|
span?.addTags({ db_name: this.name })
|
||||||
|
return this.db.load(...args)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
createIndex(...args: any[]): Promise<any> {
|
||||||
|
return tracer.trace("db.createIndex", span => {
|
||||||
|
span?.addTags({ db_name: this.name })
|
||||||
|
return this.db.createIndex(...args)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteIndex(...args: any[]): Promise<any> {
|
||||||
|
return tracer.trace("db.deleteIndex", span => {
|
||||||
|
span?.addTags({ db_name: this.name })
|
||||||
|
return this.db.deleteIndex(...args)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getIndexes(...args: any[]): Promise<any> {
|
||||||
|
return tracer.trace("db.getIndexes", span => {
|
||||||
|
span?.addTags({ db_name: this.name })
|
||||||
|
return this.db.getIndexes(...args)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -166,6 +166,8 @@ const environment = {
|
||||||
DISABLE_JWT_WARNING: process.env.DISABLE_JWT_WARNING,
|
DISABLE_JWT_WARNING: process.env.DISABLE_JWT_WARNING,
|
||||||
BLACKLIST_IPS: process.env.BLACKLIST_IPS,
|
BLACKLIST_IPS: process.env.BLACKLIST_IPS,
|
||||||
SERVICE_TYPE: "unknown",
|
SERVICE_TYPE: "unknown",
|
||||||
|
PASSWORD_MIN_LENGTH: process.env.PASSWORD_MIN_LENGTH,
|
||||||
|
PASSWORD_MAX_LENGTH: process.env.PASSWORD_MAX_LENGTH,
|
||||||
/**
|
/**
|
||||||
* Enable to allow an admin user to login using a password.
|
* Enable to allow an admin user to login using a password.
|
||||||
* This can be useful to prevent lockout when configuring SSO.
|
* This can be useful to prevent lockout when configuring SSO.
|
||||||
|
@ -177,6 +179,7 @@ const environment = {
|
||||||
...getPackageJsonFields(),
|
...getPackageJsonFields(),
|
||||||
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
||||||
OFFLINE_MODE: process.env.OFFLINE_MODE,
|
OFFLINE_MODE: process.env.OFFLINE_MODE,
|
||||||
|
SESSION_EXPIRY_SECONDS: process.env.SESSION_EXPIRY_SECONDS,
|
||||||
_set(key: any, value: any) {
|
_set(key: any, value: any) {
|
||||||
process.env[key] = value
|
process.env[key] = value
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
@ -2,6 +2,7 @@ export * as configs from "./configs"
|
||||||
export * as events from "./events"
|
export * as events from "./events"
|
||||||
export * as migrations from "./migrations"
|
export * as migrations from "./migrations"
|
||||||
export * as users from "./users"
|
export * as users from "./users"
|
||||||
|
export * as userUtils from "./users/utils"
|
||||||
export * as roles from "./security/roles"
|
export * as roles from "./security/roles"
|
||||||
export * as permissions from "./security/permissions"
|
export * as permissions from "./security/permissions"
|
||||||
export * as accounts from "./accounts"
|
export * as accounts from "./accounts"
|
||||||
|
@ -33,6 +34,7 @@ export * as docUpdates from "./docUpdates"
|
||||||
export * from "./utils/Duration"
|
export * from "./utils/Duration"
|
||||||
export { SearchParams } from "./db"
|
export { SearchParams } from "./db"
|
||||||
export * as docIds from "./docIds"
|
export * as docIds from "./docIds"
|
||||||
|
export * as security from "./security"
|
||||||
// Add context to tenancy for backwards compatibility
|
// Add context to tenancy for backwards compatibility
|
||||||
// only do this for external usages to prevent internal
|
// only do this for external usages to prevent internal
|
||||||
// circular dependencies
|
// circular dependencies
|
||||||
|
|
|
@ -5,6 +5,8 @@ import { IdentityType } from "@budibase/types"
|
||||||
import env from "../../environment"
|
import env from "../../environment"
|
||||||
import * as context from "../../context"
|
import * as context from "../../context"
|
||||||
import * as correlation from "../correlation"
|
import * as correlation from "../correlation"
|
||||||
|
import tracer from "dd-trace"
|
||||||
|
import { formats } from "dd-trace/ext"
|
||||||
|
|
||||||
import { localFileDestination } from "../system"
|
import { localFileDestination } from "../system"
|
||||||
|
|
||||||
|
@ -115,6 +117,11 @@ if (!env.DISABLE_PINO_LOGGER) {
|
||||||
correlationId: correlation.getId(),
|
correlationId: correlation.getId(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const span = tracer.scope().active()
|
||||||
|
if (span) {
|
||||||
|
tracer.inject(span.context(), formats.LOG, contextObject)
|
||||||
|
}
|
||||||
|
|
||||||
const mergingObject: any = {
|
const mergingObject: any = {
|
||||||
err: error,
|
err: error,
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
|
|
|
@ -15,6 +15,7 @@ import * as identity from "../context/identity"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { Ctx, EndpointMatcher, SessionCookie } from "@budibase/types"
|
import { Ctx, EndpointMatcher, SessionCookie } from "@budibase/types"
|
||||||
import { InvalidAPIKeyError, ErrorCode } from "../errors"
|
import { InvalidAPIKeyError, ErrorCode } from "../errors"
|
||||||
|
import tracer from "dd-trace"
|
||||||
|
|
||||||
const ONE_MINUTE = env.SESSION_UPDATE_PERIOD
|
const ONE_MINUTE = env.SESSION_UPDATE_PERIOD
|
||||||
? parseInt(env.SESSION_UPDATE_PERIOD)
|
? parseInt(env.SESSION_UPDATE_PERIOD)
|
||||||
|
@ -166,6 +167,16 @@ export default function (
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
authenticated = false
|
authenticated = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
tracer.setUser({
|
||||||
|
id: user?._id,
|
||||||
|
tenantId: user?.tenantId,
|
||||||
|
budibaseAccess: user?.budibaseAccess,
|
||||||
|
status: user?.status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// isAuthenticated is a function, so use a variable to be able to check authed state
|
// isAuthenticated is a function, so use a variable to be able to check authed state
|
||||||
finalise(ctx, { authenticated, user, internal, version, publicEndpoint })
|
finalise(ctx, { authenticated, user, internal, version, publicEndpoint })
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ const getCloudfrontSignParams = () => {
|
||||||
return {
|
return {
|
||||||
keypairId: env.CLOUDFRONT_PUBLIC_KEY_ID!,
|
keypairId: env.CLOUDFRONT_PUBLIC_KEY_ID!,
|
||||||
privateKeyString: getPrivateKey(),
|
privateKeyString: getPrivateKey(),
|
||||||
expireTime: new Date().getTime() + 1000 * 60 * 60, // 1 hour
|
expireTime: new Date().getTime() + 1000 * 60 * 60 * 24, // 1 day
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import tar from "tar-fs"
|
||||||
import zlib from "zlib"
|
import zlib from "zlib"
|
||||||
import { promisify } from "util"
|
import { promisify } from "util"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import fs from "fs"
|
import fs, { ReadStream } from "fs"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { budibaseTempDir } from "./utils"
|
import { budibaseTempDir } from "./utils"
|
||||||
import { v4 } from "uuid"
|
import { v4 } from "uuid"
|
||||||
|
@ -184,7 +184,7 @@ export async function upload({
|
||||||
export async function streamUpload(
|
export async function streamUpload(
|
||||||
bucketName: string,
|
bucketName: string,
|
||||||
filename: string,
|
filename: string,
|
||||||
stream: any,
|
stream: ReadStream | ReadableStream,
|
||||||
extra = {}
|
extra = {}
|
||||||
) {
|
) {
|
||||||
const objectStore = ObjectStore(bucketName)
|
const objectStore = ObjectStore(bucketName)
|
||||||
|
@ -260,12 +260,12 @@ export async function listAllObjects(bucketName: string, path: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a presigned url with a default TTL of 36 hours
|
* Generate a presigned url with a default TTL of 1 hour
|
||||||
*/
|
*/
|
||||||
export function getPresignedUrl(
|
export function getPresignedUrl(
|
||||||
bucketName: string,
|
bucketName: string,
|
||||||
key: string,
|
key: string,
|
||||||
durationSeconds: number = 129600
|
durationSeconds: number = 3600
|
||||||
) {
|
) {
|
||||||
const objectStore = ObjectStore(bucketName, { presigning: true })
|
const objectStore = ObjectStore(bucketName, { presigning: true })
|
||||||
const params = {
|
const params = {
|
||||||
|
|
|
@ -15,6 +15,7 @@ function newJob(queue: string, message: any) {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
queue: queue,
|
queue: queue,
|
||||||
data: message,
|
data: message,
|
||||||
|
opts: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,6 +69,10 @@ class InMemoryQueue {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async isReady() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// simply puts a message to the queue and emits to the queue for processing
|
// simply puts a message to the queue and emits to the queue for processing
|
||||||
/**
|
/**
|
||||||
* Simple function to replicate the add message functionality of Bull, putting
|
* Simple function to replicate the add message functionality of Bull, putting
|
||||||
|
|
|
@ -47,7 +47,7 @@ export function createQueue<T>(
|
||||||
cleanupInterval = timers.set(cleanup, CLEANUP_PERIOD_MS)
|
cleanupInterval = timers.set(cleanup, CLEANUP_PERIOD_MS)
|
||||||
// fire off an initial cleanup
|
// fire off an initial cleanup
|
||||||
cleanup().catch(err => {
|
cleanup().catch(err => {
|
||||||
console.error(`Unable to cleanup automation queue initially - ${err}`)
|
console.error(`Unable to cleanup ${jobQueue} initially - ${err}`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return queue
|
return queue
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
SelectableDatabase,
|
SelectableDatabase,
|
||||||
getRedisConnectionDetails,
|
getRedisConnectionDetails,
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
|
import { logAlert } from "../logging"
|
||||||
import * as timers from "../timers"
|
import * as timers from "../timers"
|
||||||
|
|
||||||
const RETRY_PERIOD_MS = 2000
|
const RETRY_PERIOD_MS = 2000
|
||||||
|
@ -39,21 +40,16 @@ function pickClient(selectDb: number): any {
|
||||||
return CLIENTS[selectDb]
|
return CLIENTS[selectDb]
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectionError(
|
function connectionError(timeout: NodeJS.Timeout, err: Error | string) {
|
||||||
selectDb: number,
|
|
||||||
timeout: NodeJS.Timeout,
|
|
||||||
err: Error | string
|
|
||||||
) {
|
|
||||||
// manually shut down, ignore errors
|
// manually shut down, ignore errors
|
||||||
if (CLOSED) {
|
if (CLOSED) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pickClient(selectDb).disconnect()
|
|
||||||
CLOSED = true
|
CLOSED = true
|
||||||
// always clear this on error
|
// always clear this on error
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
CONNECTED = false
|
CONNECTED = false
|
||||||
console.error("Redis connection failed - " + err)
|
logAlert("Redis connection failed", err)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
init()
|
init()
|
||||||
}, RETRY_PERIOD_MS)
|
}, RETRY_PERIOD_MS)
|
||||||
|
@ -79,11 +75,7 @@ function init(selectDb = DEFAULT_SELECT_DB) {
|
||||||
// start the timer - only allowed 5 seconds to connect
|
// start the timer - only allowed 5 seconds to connect
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
if (!CONNECTED) {
|
if (!CONNECTED) {
|
||||||
connectionError(
|
connectionError(timeout, "Did not successfully connect in timeout")
|
||||||
selectDb,
|
|
||||||
timeout,
|
|
||||||
"Did not successfully connect in timeout"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}, STARTUP_TIMEOUT_MS)
|
}, STARTUP_TIMEOUT_MS)
|
||||||
|
|
||||||
|
@ -106,12 +98,13 @@ function init(selectDb = DEFAULT_SELECT_DB) {
|
||||||
// allow the process to exit
|
// allow the process to exit
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
connectionError(selectDb, timeout, err)
|
connectionError(timeout, err)
|
||||||
})
|
})
|
||||||
client.on("error", (err: Error) => {
|
client.on("error", (err: Error) => {
|
||||||
connectionError(selectDb, timeout, err)
|
connectionError(timeout, err)
|
||||||
})
|
})
|
||||||
client.on("connect", () => {
|
client.on("connect", () => {
|
||||||
|
console.log(`Connected to Redis DB: ${selectDb}`)
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
CONNECTED = true
|
CONNECTED = true
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,7 +2,6 @@ import Redlock from "redlock"
|
||||||
import { getLockClient } from "./init"
|
import { getLockClient } from "./init"
|
||||||
import { LockOptions, LockType } from "@budibase/types"
|
import { LockOptions, LockType } from "@budibase/types"
|
||||||
import * as context from "../context"
|
import * as context from "../context"
|
||||||
import { logWarn } from "../logging"
|
|
||||||
import { utils } from "@budibase/shared-core"
|
import { utils } from "@budibase/shared-core"
|
||||||
import { Duration } from "../utils"
|
import { Duration } from "../utils"
|
||||||
|
|
||||||
|
@ -137,7 +136,6 @@ export async function doWithLock<T>(
|
||||||
const result = await task()
|
const result = await task()
|
||||||
return { executed: true, result }
|
return { executed: true, result }
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logWarn(`lock type: ${opts.type} error`, e)
|
|
||||||
// lock limit exceeded
|
// lock limit exceeded
|
||||||
if (e.name === "LockError") {
|
if (e.name === "LockError") {
|
||||||
if (opts.type === LockType.TRY_ONCE) {
|
if (opts.type === LockType.TRY_ONCE) {
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import env from "../environment"
|
||||||
|
|
||||||
|
export const PASSWORD_MIN_LENGTH = +(env.PASSWORD_MIN_LENGTH || 8)
|
||||||
|
export const PASSWORD_MAX_LENGTH = +(env.PASSWORD_MAX_LENGTH || 512)
|
||||||
|
|
||||||
|
export function validatePassword(
|
||||||
|
password: string
|
||||||
|
): { valid: true } | { valid: false; error: string } {
|
||||||
|
if (!password || password.length < PASSWORD_MIN_LENGTH) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `Password invalid. Minimum ${PASSWORD_MIN_LENGTH} characters.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length > PASSWORD_MAX_LENGTH) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `Password invalid. Maximum ${PASSWORD_MAX_LENGTH} characters.`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true }
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./auth"
|
|
@ -1,8 +1,8 @@
|
||||||
const redis = require("../redis/init")
|
import * as redis from "../redis/init"
|
||||||
const { v4: uuidv4 } = require("uuid")
|
import { v4 as uuidv4 } from "uuid"
|
||||||
const { logWarn } = require("../logging")
|
import { logWarn } from "../logging"
|
||||||
|
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
|
import { Duration } from "../utils"
|
||||||
import {
|
import {
|
||||||
Session,
|
Session,
|
||||||
ScannedSession,
|
ScannedSession,
|
||||||
|
@ -10,8 +10,10 @@ import {
|
||||||
CreateSession,
|
CreateSession,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
// a week in seconds
|
// a week expiry is the default
|
||||||
const EXPIRY_SECONDS = 86400 * 7
|
const EXPIRY_SECONDS = env.SESSION_EXPIRY_SECONDS
|
||||||
|
? parseInt(env.SESSION_EXPIRY_SECONDS)
|
||||||
|
: Duration.fromDays(7).toSeconds()
|
||||||
|
|
||||||
function makeSessionID(userId: string, sessionId: string) {
|
function makeSessionID(userId: string, sessionId: string) {
|
||||||
return `${userId}/${sessionId}`
|
return `${userId}/${sessionId}`
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { generator } from "../../../tests"
|
||||||
|
import { PASSWORD_MAX_LENGTH, validatePassword } from "../auth"
|
||||||
|
|
||||||
|
describe("auth", () => {
|
||||||
|
describe("validatePassword", () => {
|
||||||
|
it("a valid password returns successful", () => {
|
||||||
|
expect(validatePassword("password")).toEqual({ valid: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["undefined", undefined],
|
||||||
|
["null", null],
|
||||||
|
["empty", ""],
|
||||||
|
])("%s returns unsuccessful", (_, password) => {
|
||||||
|
expect(validatePassword(password as string)).toEqual({
|
||||||
|
valid: false,
|
||||||
|
error: "Password invalid. Minimum 8 characters.",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
generator.word({ length: PASSWORD_MAX_LENGTH }),
|
||||||
|
generator.paragraph().substring(0, PASSWORD_MAX_LENGTH),
|
||||||
|
])(`can use passwords up to 512 characters in length`, password => {
|
||||||
|
expect(validatePassword(password)).toEqual({
|
||||||
|
valid: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
generator.word({ length: PASSWORD_MAX_LENGTH + 1 }),
|
||||||
|
generator
|
||||||
|
.paragraph({ sentences: 50 })
|
||||||
|
.substring(0, PASSWORD_MAX_LENGTH + 1),
|
||||||
|
])(
|
||||||
|
`passwords cannot have more than ${PASSWORD_MAX_LENGTH} characters`,
|
||||||
|
password => {
|
||||||
|
expect(validatePassword(password)).toEqual({
|
||||||
|
valid: false,
|
||||||
|
error: "Password invalid. Maximum 512 characters.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
|
@ -39,7 +39,7 @@ const ALL_STRATEGIES = Object.values(TenantResolutionStrategy)
|
||||||
export const getTenantIDFromCtx = (
|
export const getTenantIDFromCtx = (
|
||||||
ctx: BBContext,
|
ctx: BBContext,
|
||||||
opts: GetTenantIdOptions
|
opts: GetTenantIdOptions
|
||||||
): string | null => {
|
): string | undefined => {
|
||||||
// exit early if not multi-tenant
|
// exit early if not multi-tenant
|
||||||
if (!isMultiTenant()) {
|
if (!isMultiTenant()) {
|
||||||
return DEFAULT_TENANT_ID
|
return DEFAULT_TENANT_ID
|
||||||
|
@ -144,5 +144,5 @@ export const getTenantIDFromCtx = (
|
||||||
ctx.throw(403, "Tenant id not set")
|
ctx.throw(403, "Tenant id not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
|
@ -157,12 +157,12 @@ describe("getTenantIDFromCtx", () => {
|
||||||
TenantResolutionStrategy.PATH,
|
TenantResolutionStrategy.PATH,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
expect(getTenantIDFromCtx(ctx, mockOpts)).toBeNull()
|
expect(getTenantIDFromCtx(ctx, mockOpts)).toBeUndefined()
|
||||||
expect(ctx.throw).toBeCalledTimes(1)
|
expect(ctx.throw).toBeCalledTimes(1)
|
||||||
expect(ctx.throw).toBeCalledWith(403, "Tenant id not set")
|
expect(ctx.throw).toBeCalledWith(403, "Tenant id not set")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns null if allowNoTenant is true", () => {
|
it("returns undefined if allowNoTenant is true", () => {
|
||||||
const ctx = createCtx({})
|
const ctx = createCtx({})
|
||||||
mockOpts = {
|
mockOpts = {
|
||||||
allowNoTenant: true,
|
allowNoTenant: true,
|
||||||
|
@ -172,7 +172,7 @@ describe("getTenantIDFromCtx", () => {
|
||||||
TenantResolutionStrategy.PATH,
|
TenantResolutionStrategy.PATH,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
expect(getTenantIDFromCtx(ctx, mockOpts)).toBeNull()
|
expect(getTenantIDFromCtx(ctx, mockOpts)).toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -20,3 +20,41 @@ export function cleanup() {
|
||||||
}
|
}
|
||||||
intervals = []
|
intervals = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ExecutionTimeoutError extends Error {
|
||||||
|
public readonly name = "ExecutionTimeoutError"
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExecutionTimeTracker {
|
||||||
|
static withLimit(limitMs: number) {
|
||||||
|
return new ExecutionTimeTracker(limitMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(readonly limitMs: number) {}
|
||||||
|
|
||||||
|
private totalTimeMs = 0
|
||||||
|
|
||||||
|
track<T>(f: () => T): T {
|
||||||
|
this.checkLimit()
|
||||||
|
const start = process.hrtime.bigint()
|
||||||
|
try {
|
||||||
|
return f()
|
||||||
|
} finally {
|
||||||
|
const end = process.hrtime.bigint()
|
||||||
|
this.totalTimeMs += Number(end - start) / 1e6
|
||||||
|
this.checkLimit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get elapsedMS() {
|
||||||
|
return this.totalTimeMs
|
||||||
|
}
|
||||||
|
|
||||||
|
checkLimit() {
|
||||||
|
if (this.totalTimeMs > this.limitMs) {
|
||||||
|
throw new ExecutionTimeoutError(
|
||||||
|
`Execution time limit of ${this.limitMs}ms exceeded: ${this.totalTimeMs}ms`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import env from "../environment"
|
||||||
import * as eventHelpers from "./events"
|
import * as eventHelpers from "./events"
|
||||||
import * as accountSdk from "../accounts"
|
import * as accountSdk from "../accounts"
|
||||||
import * as cache from "../cache"
|
import * as cache from "../cache"
|
||||||
import { doInTenant, getGlobalDB, getIdentity, getTenantId } from "../context"
|
import { getGlobalDB, getIdentity, getTenantId } from "../context"
|
||||||
import * as dbUtils from "../db"
|
import * as dbUtils from "../db"
|
||||||
import { EmailUnavailableError, HTTPError } from "../errors"
|
import { EmailUnavailableError, HTTPError } from "../errors"
|
||||||
import * as platform from "../platform"
|
import * as platform from "../platform"
|
||||||
|
@ -27,6 +27,7 @@ import {
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
import { searchExistingEmails } from "./lookup"
|
import { searchExistingEmails } from "./lookup"
|
||||||
import { hash } from "../utils"
|
import { hash } from "../utils"
|
||||||
|
import { validatePassword } from "../security"
|
||||||
|
|
||||||
type QuotaUpdateFn = (
|
type QuotaUpdateFn = (
|
||||||
change: number,
|
change: number,
|
||||||
|
@ -43,6 +44,12 @@ type GroupFns = {
|
||||||
getBulk: GroupGetFn
|
getBulk: GroupGetFn
|
||||||
getGroupBuilderAppIds: GroupBuildersFn
|
getGroupBuilderAppIds: GroupBuildersFn
|
||||||
}
|
}
|
||||||
|
type CreateAdminUserOpts = {
|
||||||
|
ssoId?: string
|
||||||
|
hashPassword?: boolean
|
||||||
|
requirePassword?: boolean
|
||||||
|
skipPasswordValidation?: boolean
|
||||||
|
}
|
||||||
type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn }
|
type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn }
|
||||||
|
|
||||||
const bulkDeleteProcessing = async (dbUser: User) => {
|
const bulkDeleteProcessing = async (dbUser: User) => {
|
||||||
|
@ -110,6 +117,14 @@ export class UserDB {
|
||||||
if (await UserDB.isPreventPasswordActions(user, account)) {
|
if (await UserDB.isPreventPasswordActions(user, account)) {
|
||||||
throw new HTTPError("Password change is disabled for this user", 400)
|
throw new HTTPError("Password change is disabled for this user", 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!opts.skipPasswordValidation) {
|
||||||
|
const passwordValidation = validatePassword(password)
|
||||||
|
if (!passwordValidation.valid) {
|
||||||
|
throw new HTTPError(passwordValidation.error, 400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
hashedPassword = opts.hashPassword ? await hash(password) : password
|
hashedPassword = opts.hashPassword ? await hash(password) : password
|
||||||
} else if (dbUser) {
|
} else if (dbUser) {
|
||||||
hashedPassword = dbUser.password
|
hashedPassword = dbUser.password
|
||||||
|
@ -236,7 +251,8 @@ export class UserDB {
|
||||||
}
|
}
|
||||||
|
|
||||||
const change = dbUser ? 0 : 1 // no change if there is existing user
|
const change = dbUser ? 0 : 1 // no change if there is existing user
|
||||||
const creatorsChange = isCreator(dbUser) !== isCreator(user) ? 1 : 0
|
const creatorsChange =
|
||||||
|
(await isCreator(dbUser)) !== (await isCreator(user)) ? 1 : 0
|
||||||
return UserDB.quotas.addUsers(change, creatorsChange, async () => {
|
return UserDB.quotas.addUsers(change, creatorsChange, async () => {
|
||||||
await validateUniqueUser(email, tenantId)
|
await validateUniqueUser(email, tenantId)
|
||||||
|
|
||||||
|
@ -320,7 +336,7 @@ export class UserDB {
|
||||||
}
|
}
|
||||||
newUser.userGroups = groups || []
|
newUser.userGroups = groups || []
|
||||||
newUsers.push(newUser)
|
newUsers.push(newUser)
|
||||||
if (isCreator(newUser)) {
|
if (await isCreator(newUser)) {
|
||||||
newCreators.push(newUser)
|
newCreators.push(newUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -417,12 +433,16 @@ export class UserDB {
|
||||||
_deleted: true,
|
_deleted: true,
|
||||||
}))
|
}))
|
||||||
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
|
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
|
||||||
const creatorsToDelete = usersToDelete.filter(isCreator)
|
|
||||||
|
const creatorsEval = await Promise.all(usersToDelete.map(isCreator))
|
||||||
|
const creatorsToDeleteCount = creatorsEval.filter(
|
||||||
|
creator => !!creator
|
||||||
|
).length
|
||||||
|
|
||||||
for (let user of usersToDelete) {
|
for (let user of usersToDelete) {
|
||||||
await bulkDeleteProcessing(user)
|
await bulkDeleteProcessing(user)
|
||||||
}
|
}
|
||||||
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDelete.length)
|
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount)
|
||||||
|
|
||||||
// Build Response
|
// Build Response
|
||||||
// index users by id
|
// index users by id
|
||||||
|
@ -471,7 +491,7 @@ export class UserDB {
|
||||||
|
|
||||||
await db.remove(userId, dbUser._rev)
|
await db.remove(userId, dbUser._rev)
|
||||||
|
|
||||||
const creatorsToDelete = isCreator(dbUser) ? 1 : 0
|
const creatorsToDelete = (await isCreator(dbUser)) ? 1 : 0
|
||||||
await UserDB.quotas.removeUsers(1, creatorsToDelete)
|
await UserDB.quotas.removeUsers(1, creatorsToDelete)
|
||||||
await eventHelpers.handleDeleteEvents(dbUser)
|
await eventHelpers.handleDeleteEvents(dbUser)
|
||||||
await cache.user.invalidateUser(userId)
|
await cache.user.invalidateUser(userId)
|
||||||
|
@ -482,7 +502,7 @@ export class UserDB {
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
opts?: { ssoId?: string; hashPassword?: boolean; requirePassword?: boolean }
|
opts?: CreateAdminUserOpts
|
||||||
) {
|
) {
|
||||||
const user: User = {
|
const user: User = {
|
||||||
email: email,
|
email: email,
|
||||||
|
@ -506,6 +526,7 @@ export class UserDB {
|
||||||
return await UserDB.save(user, {
|
return await UserDB.save(user, {
|
||||||
hashPassword: opts?.hashPassword,
|
hashPassword: opts?.hashPassword,
|
||||||
requirePassword: opts?.requirePassword,
|
requirePassword: opts?.requirePassword,
|
||||||
|
skipPasswordValidation: opts?.skipPasswordValidation,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { User, UserGroup } from "@budibase/types"
|
||||||
|
import { generator, structures } from "../../../tests"
|
||||||
|
import { DBTestConfiguration } from "../../../tests/extra"
|
||||||
|
import { getGlobalDB } from "../../context"
|
||||||
|
import { isCreator } from "../utils"
|
||||||
|
|
||||||
|
const config = new DBTestConfiguration()
|
||||||
|
|
||||||
|
describe("Users", () => {
|
||||||
|
it("User is a creator if it is configured as a global builder", async () => {
|
||||||
|
const user: User = structures.users.user({ builder: { global: true } })
|
||||||
|
expect(await isCreator(user)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("User is a creator if it is configured as a global admin", async () => {
|
||||||
|
const user: User = structures.users.user({ admin: { global: true } })
|
||||||
|
expect(await isCreator(user)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("User is a creator if it is configured with creator permission", async () => {
|
||||||
|
const user: User = structures.users.user({ builder: { creator: true } })
|
||||||
|
expect(await isCreator(user)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("User is a creator if it is a builder in some application", async () => {
|
||||||
|
const user: User = structures.users.user({ builder: { apps: ["app1"] } })
|
||||||
|
expect(await isCreator(user)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("User is a creator if it has CREATOR permission in some application", async () => {
|
||||||
|
const user: User = structures.users.user({ roles: { app1: "CREATOR" } })
|
||||||
|
expect(await isCreator(user)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("User is a creator if it has ADMIN permission in some application", async () => {
|
||||||
|
const user: User = structures.users.user({ roles: { app1: "ADMIN" } })
|
||||||
|
expect(await isCreator(user)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("User is a creator if it remains to a group with ADMIN permissions", async () => {
|
||||||
|
const usersInGroup = 10
|
||||||
|
const groupId = "gr_17abffe89e0b40268e755b952f101a59"
|
||||||
|
const group: UserGroup = {
|
||||||
|
...structures.userGroups.userGroup(),
|
||||||
|
...{ _id: groupId, roles: { app1: "ADMIN" } },
|
||||||
|
}
|
||||||
|
const users: User[] = []
|
||||||
|
for (const _ of Array.from({ length: usersInGroup })) {
|
||||||
|
const userId = `us_${generator.guid()}`
|
||||||
|
const user: User = structures.users.user({
|
||||||
|
_id: userId,
|
||||||
|
userGroups: [groupId],
|
||||||
|
})
|
||||||
|
users.push(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
await config.doInTenant(async () => {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
await db.put(group)
|
||||||
|
for (let user of users) {
|
||||||
|
await db.put(user)
|
||||||
|
const creator = await isCreator(user)
|
||||||
|
expect(creator).toBe(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -309,7 +309,8 @@ export async function getCreatorCount() {
|
||||||
let creators = 0
|
let creators = 0
|
||||||
async function iterate(startPage?: string) {
|
async function iterate(startPage?: string) {
|
||||||
const page = await paginatedUsers({ bookmark: startPage })
|
const page = await paginatedUsers({ bookmark: startPage })
|
||||||
creators += page.data.filter(isCreator).length
|
const creatorsEval = await Promise.all(page.data.map(isCreator))
|
||||||
|
creators += creatorsEval.filter(creator => !!creator).length
|
||||||
if (page.hasNextPage) {
|
if (page.hasNextPage) {
|
||||||
await iterate(page.nextPage)
|
await iterate(page.nextPage)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CloudAccount } from "@budibase/types"
|
import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types"
|
||||||
import * as accountSdk from "../accounts"
|
import * as accountSdk from "../accounts"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { getPlatformUser } from "./lookup"
|
import { getPlatformUser } from "./lookup"
|
||||||
|
@ -6,17 +6,48 @@ import { EmailUnavailableError } from "../errors"
|
||||||
import { getTenantId } from "../context"
|
import { getTenantId } from "../context"
|
||||||
import { sdk } from "@budibase/shared-core"
|
import { sdk } from "@budibase/shared-core"
|
||||||
import { getAccountByTenantId } from "../accounts"
|
import { getAccountByTenantId } from "../accounts"
|
||||||
|
import { BUILTIN_ROLE_IDS } from "../security/roles"
|
||||||
|
import * as context from "../context"
|
||||||
|
|
||||||
// extract from shared-core to make easily accessible from backend-core
|
// extract from shared-core to make easily accessible from backend-core
|
||||||
export const isBuilder = sdk.users.isBuilder
|
export const isBuilder = sdk.users.isBuilder
|
||||||
export const isAdmin = sdk.users.isAdmin
|
export const isAdmin = sdk.users.isAdmin
|
||||||
export const isCreator = sdk.users.isCreator
|
|
||||||
export const isGlobalBuilder = sdk.users.isGlobalBuilder
|
export const isGlobalBuilder = sdk.users.isGlobalBuilder
|
||||||
export const isAdminOrBuilder = sdk.users.isAdminOrBuilder
|
export const isAdminOrBuilder = sdk.users.isAdminOrBuilder
|
||||||
export const hasAdminPermissions = sdk.users.hasAdminPermissions
|
export const hasAdminPermissions = sdk.users.hasAdminPermissions
|
||||||
export const hasBuilderPermissions = sdk.users.hasBuilderPermissions
|
export const hasBuilderPermissions = sdk.users.hasBuilderPermissions
|
||||||
export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions
|
export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions
|
||||||
|
|
||||||
|
export async function isCreator(user?: User | ContextUser) {
|
||||||
|
const isCreatorByUserDefinition = sdk.users.isCreator(user)
|
||||||
|
if (!isCreatorByUserDefinition && user) {
|
||||||
|
return await isCreatorByGroupMembership(user)
|
||||||
|
}
|
||||||
|
return isCreatorByUserDefinition
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isCreatorByGroupMembership(user?: User | ContextUser) {
|
||||||
|
const userGroups = user?.userGroups || []
|
||||||
|
if (userGroups.length > 0) {
|
||||||
|
const db = context.getGlobalDB()
|
||||||
|
const groups: UserGroup[] = []
|
||||||
|
for (let groupId of userGroups) {
|
||||||
|
try {
|
||||||
|
const group = await db.get<UserGroup>(groupId)
|
||||||
|
groups.push(group)
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.error !== "not_found") {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups.some(group =>
|
||||||
|
Object.values(group.roles || {}).includes(BUILTIN_ROLE_IDS.ADMIN)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
export async function validateUniqueUser(email: string, tenantId: string) {
|
export async function validateUniqueUser(email: string, tenantId: string) {
|
||||||
// check budibase users in other tenants
|
// check budibase users in other tenants
|
||||||
if (env.MULTI_TENANCY) {
|
if (env.MULTI_TENANCY) {
|
||||||
|
|
|
@ -49,4 +49,8 @@ export class Duration {
|
||||||
static fromDays(duration: number) {
|
static fromDays(duration: number) {
|
||||||
return Duration.from(DurationType.DAYS, duration)
|
return Duration.from(DurationType.DAYS, duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromMilliseconds(duration: number) {
|
||||||
|
return Duration.from(DurationType.MILLISECONDS, duration)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,8 @@ export async function resolveAppUrl(ctx: Ctx) {
|
||||||
const appUrl = ctx.path.split("/")[2]
|
const appUrl = ctx.path.split("/")[2]
|
||||||
let possibleAppUrl = `/${appUrl.toLowerCase()}`
|
let possibleAppUrl = `/${appUrl.toLowerCase()}`
|
||||||
|
|
||||||
let tenantId: string | null = context.getTenantId()
|
let tenantId: string | undefined = context.getTenantId()
|
||||||
if (env.MULTI_TENANCY) {
|
if (!env.isDev() && env.MULTI_TENANCY) {
|
||||||
// always use the tenant id from the subdomain in multi tenancy
|
// always use the tenant id from the subdomain in multi tenancy
|
||||||
// this ensures the logged-in user tenant id doesn't overwrite
|
// this ensures the logged-in user tenant id doesn't overwrite
|
||||||
// e.g. in the case of viewing a public app while already logged-in to another tenant
|
// e.g. in the case of viewing a public app while already logged-in to another tenant
|
||||||
|
@ -41,7 +41,7 @@ export async function resolveAppUrl(ctx: Ctx) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// search prod apps for a url that matches
|
// search prod apps for an url that matches
|
||||||
const apps: App[] = await context.doInTenant(
|
const apps: App[] = await context.doInTenant(
|
||||||
tenantId,
|
tenantId,
|
||||||
() => getAllApps({ dev: false }) as Promise<App[]>
|
() => getAllApps({ dev: false }) as Promise<App[]>
|
||||||
|
@ -96,7 +96,7 @@ export async function getAppIdFromCtx(ctx: Ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// look in the path
|
// look in the path
|
||||||
const pathId = parseAppIdFromUrl(ctx.path)
|
const pathId = parseAppIdFromUrlPath(ctx.path)
|
||||||
if (!appId && pathId) {
|
if (!appId && pathId) {
|
||||||
appId = confirmAppId(pathId)
|
appId = confirmAppId(pathId)
|
||||||
}
|
}
|
||||||
|
@ -116,18 +116,21 @@ export async function getAppIdFromCtx(ctx: Ctx) {
|
||||||
// referer header is present from a builder redirect
|
// referer header is present from a builder redirect
|
||||||
const referer = ctx.request.headers.referer
|
const referer = ctx.request.headers.referer
|
||||||
if (!appId && referer?.includes(BUILDER_APP_PREFIX)) {
|
if (!appId && referer?.includes(BUILDER_APP_PREFIX)) {
|
||||||
const refererId = parseAppIdFromUrl(ctx.request.headers.referer)
|
const refererId = parseAppIdFromUrlPath(ctx.request.headers.referer)
|
||||||
appId = confirmAppId(refererId)
|
appId = confirmAppId(refererId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return appId
|
return appId
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseAppIdFromUrl(url?: string) {
|
function parseAppIdFromUrlPath(url?: string) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return url.split("/").find(subPath => subPath.startsWith(APP_PREFIX))
|
return url
|
||||||
|
.split("?")[0] // Remove any possible query string
|
||||||
|
.split("/")
|
||||||
|
.find(subPath => subPath.startsWith(APP_PREFIX))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const user = (userProps?: Partial<Omit<User, "userId">>): User => {
|
||||||
_id: userId,
|
_id: userId,
|
||||||
userId,
|
userId,
|
||||||
email: newEmail(),
|
email: newEmail(),
|
||||||
password: "test",
|
password: "password",
|
||||||
roles: { app_test: "admin" },
|
roles: { app_test: "admin" },
|
||||||
firstName: generator.first(),
|
firstName: generator.first(),
|
||||||
lastName: generator.last(),
|
lastName: generator.last(),
|
||||||
|
|
|
@ -130,5 +130,6 @@
|
||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,7 +18,6 @@ export default function positionDropdown(element, opts) {
|
||||||
useAnchorWidth,
|
useAnchorWidth,
|
||||||
offset = 5,
|
offset = 5,
|
||||||
customUpdate,
|
customUpdate,
|
||||||
offsetBelow,
|
|
||||||
} = opts
|
} = opts
|
||||||
if (!anchor) {
|
if (!anchor) {
|
||||||
return
|
return
|
||||||
|
@ -48,7 +47,7 @@ export default function positionDropdown(element, opts) {
|
||||||
styles.top = anchorBounds.top - elementBounds.height - offset
|
styles.top = anchorBounds.top - elementBounds.height - offset
|
||||||
styles.maxHeight = maxHeight || 240
|
styles.maxHeight = maxHeight || 240
|
||||||
} else {
|
} else {
|
||||||
styles.top = anchorBounds.bottom + (offsetBelow || offset)
|
styles.top = anchorBounds.bottom + offset
|
||||||
styles.maxHeight =
|
styles.maxHeight =
|
||||||
maxHeight || window.innerHeight - anchorBounds.bottom - 20
|
maxHeight || window.innerHeight - anchorBounds.bottom - 20
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@
|
||||||
var(--spacing-xl);
|
var(--spacing-xl);
|
||||||
}
|
}
|
||||||
.property-panel.no-title {
|
.property-panel.no-title {
|
||||||
padding: var(--spacing-xl);
|
padding-top: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.show {
|
.show {
|
||||||
|
|
|
@ -15,8 +15,6 @@
|
||||||
export let autoWidth = false
|
export let autoWidth = false
|
||||||
export let searchTerm = null
|
export let searchTerm = null
|
||||||
export let customPopoverHeight
|
export let customPopoverHeight
|
||||||
export let customPopoverOffsetBelow
|
|
||||||
export let customPopoverMaxHeight
|
|
||||||
export let open = false
|
export let open = false
|
||||||
export let loading
|
export let loading
|
||||||
|
|
||||||
|
@ -98,7 +96,5 @@
|
||||||
{sort}
|
{sort}
|
||||||
{autoWidth}
|
{autoWidth}
|
||||||
{customPopoverHeight}
|
{customPopoverHeight}
|
||||||
{customPopoverOffsetBelow}
|
|
||||||
{customPopoverMaxHeight}
|
|
||||||
{loading}
|
{loading}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -37,8 +37,6 @@
|
||||||
export let sort = false
|
export let sort = false
|
||||||
export let searchTerm = null
|
export let searchTerm = null
|
||||||
export let customPopoverHeight
|
export let customPopoverHeight
|
||||||
export let customPopoverOffsetBelow
|
|
||||||
export let customPopoverMaxHeight
|
|
||||||
export let align = "left"
|
export let align = "left"
|
||||||
export let footer = null
|
export let footer = null
|
||||||
export let customAnchor = null
|
export let customAnchor = null
|
||||||
|
@ -156,9 +154,7 @@
|
||||||
on:close={() => (open = false)}
|
on:close={() => (open = false)}
|
||||||
useAnchorWidth={!autoWidth}
|
useAnchorWidth={!autoWidth}
|
||||||
maxWidth={autoWidth ? 400 : null}
|
maxWidth={autoWidth ? 400 : null}
|
||||||
maxHeight={customPopoverMaxHeight}
|
|
||||||
customHeight={customPopoverHeight}
|
customHeight={customPopoverHeight}
|
||||||
offsetBelow={customPopoverOffsetBelow}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="popover-content"
|
class="popover-content"
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
export let getOptionIcon = () => null
|
export let getOptionIcon = () => null
|
||||||
export let getOptionColour = () => null
|
export let getOptionColour = () => null
|
||||||
export let getOptionSubtitle = () => null
|
export let getOptionSubtitle = () => null
|
||||||
|
export let compare = null
|
||||||
export let useOptionIconImage = false
|
export let useOptionIconImage = false
|
||||||
export let isOptionEnabled
|
export let isOptionEnabled
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
|
@ -23,8 +24,6 @@
|
||||||
export let footer = null
|
export let footer = null
|
||||||
export let open = false
|
export let open = false
|
||||||
export let tag = null
|
export let tag = null
|
||||||
export let customPopoverOffsetBelow
|
|
||||||
export let customPopoverMaxHeight
|
|
||||||
export let searchTerm = null
|
export let searchTerm = null
|
||||||
export let loading
|
export let loading
|
||||||
|
|
||||||
|
@ -34,13 +33,19 @@
|
||||||
$: fieldIcon = getFieldAttribute(getOptionIcon, value, options)
|
$: fieldIcon = getFieldAttribute(getOptionIcon, value, options)
|
||||||
$: fieldColour = getFieldAttribute(getOptionColour, value, options)
|
$: fieldColour = getFieldAttribute(getOptionColour, value, options)
|
||||||
|
|
||||||
|
function compareOptionAndValue(option, value) {
|
||||||
|
return typeof compare === "function"
|
||||||
|
? compare(option, value)
|
||||||
|
: option === value
|
||||||
|
}
|
||||||
|
|
||||||
const getFieldAttribute = (getAttribute, value, options) => {
|
const getFieldAttribute = (getAttribute, value, options) => {
|
||||||
// Wait for options to load if there is a value but no options
|
// Wait for options to load if there is a value but no options
|
||||||
if (!options?.length) {
|
if (!options?.length) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
const index = options.findIndex(
|
const index = options.findIndex((option, idx) =>
|
||||||
(option, idx) => getOptionValue(option, idx) === value
|
compareOptionAndValue(getOptionValue(option, idx), value)
|
||||||
)
|
)
|
||||||
return index !== -1 ? getAttribute(options[index], index) : null
|
return index !== -1 ? getAttribute(options[index], index) : null
|
||||||
}
|
}
|
||||||
|
@ -90,11 +95,9 @@
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
{sort}
|
{sort}
|
||||||
{tag}
|
{tag}
|
||||||
{customPopoverOffsetBelow}
|
|
||||||
{customPopoverMaxHeight}
|
|
||||||
isPlaceholder={value == null || value === ""}
|
isPlaceholder={value == null || value === ""}
|
||||||
placeholderOption={placeholder === false ? null : placeholder}
|
placeholderOption={placeholder === false ? null : placeholder}
|
||||||
isOptionSelected={option => option === value}
|
isOptionSelected={option => compareOptionAndValue(option, value)}
|
||||||
onSelectOption={selectOption}
|
onSelectOption={selectOption}
|
||||||
{loading}
|
{loading}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -51,15 +51,13 @@
|
||||||
margin-top: var(--spectrum-global-dimension-size-75);
|
margin-top: var(--spectrum-global-dimension-size-75);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.helpText :global(svg) {
|
.helpText :global(svg) {
|
||||||
width: 14px;
|
width: 13px;
|
||||||
color: var(--grey-5);
|
color: var(--spectrum-global-color-gray-600);
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.helpText span {
|
.helpText span {
|
||||||
color: var(--grey-7);
|
color: var(--spectrum-global-color-gray-800);
|
||||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
export let footer = null
|
export let footer = null
|
||||||
export let tag = null
|
export let tag = null
|
||||||
export let helpText = null
|
export let helpText = null
|
||||||
|
export let compare
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
value = e.detail
|
value = e.detail
|
||||||
|
@ -65,6 +66,7 @@
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
{customPopoverHeight}
|
{customPopoverHeight}
|
||||||
{tag}
|
{tag}
|
||||||
|
{compare}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
on:click
|
on:click
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
// Ensure the value is updated if the value prop changes outside the editor's
|
// Ensure the value is updated if the value prop changes outside the editor's
|
||||||
// control
|
// control
|
||||||
$: checkValue(value)
|
$: checkValue(value)
|
||||||
$: mde?.codemirror.on("change", debouncedUpdate)
|
$: mde?.codemirror.on("blur", update)
|
||||||
$: if (readonly || disabled) {
|
$: if (readonly || disabled) {
|
||||||
mde?.togglePreview()
|
mde?.togglePreview()
|
||||||
}
|
}
|
||||||
|
@ -30,21 +30,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const debounce = (fn, interval) => {
|
|
||||||
let timeout
|
|
||||||
return () => {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
timeout = setTimeout(fn, interval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
latestValue = mde.value()
|
latestValue = mde.value()
|
||||||
dispatch("change", latestValue)
|
dispatch("change", latestValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debounce the update function to avoid spamming it constantly
|
|
||||||
const debouncedUpdate = debounce(update, 250)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key height}
|
{#key height}
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirm() {
|
export async function confirm() {
|
||||||
loading = true
|
loading = true
|
||||||
if (!onConfirm || (await onConfirm()) !== keepOpen) {
|
if (!onConfirm || (await onConfirm()) !== keepOpen) {
|
||||||
hide()
|
hide()
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
export let useAnchorWidth = false
|
export let useAnchorWidth = false
|
||||||
export let dismissible = true
|
export let dismissible = true
|
||||||
export let offset = 5
|
export let offset = 5
|
||||||
export let offsetBelow
|
|
||||||
export let customHeight
|
export let customHeight
|
||||||
export let animate = true
|
export let animate = true
|
||||||
export let customZindex
|
export let customZindex
|
||||||
|
@ -89,7 +88,6 @@
|
||||||
maxWidth,
|
maxWidth,
|
||||||
useAnchorWidth,
|
useAnchorWidth,
|
||||||
offset,
|
offset,
|
||||||
offsetBelow,
|
|
||||||
customUpdate: handlePostionUpdate,
|
customUpdate: handlePostionUpdate,
|
||||||
}}
|
}}
|
||||||
use:clickOutside={{
|
use:clickOutside={{
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "routify -b && vite build --emptyOutDir",
|
"build": "routify -b && vite build --emptyOutDir",
|
||||||
"start": "routify -c rollup",
|
"start": "routify -c rollup",
|
||||||
"dev:builder": "routify -c dev:vite",
|
"dev": "routify -c dev:vite",
|
||||||
"dev:vite": "vite --host 0.0.0.0",
|
"dev:vite": "vite --host 0.0.0.0",
|
||||||
"rollup": "rollup -c -w",
|
"rollup": "rollup -c -w",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
|
@ -61,11 +61,12 @@
|
||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "^6.1.2",
|
||||||
"@codemirror/view": "^6.11.2",
|
"@codemirror/view": "^6.11.2",
|
||||||
"@fontsource/source-sans-pro": "^5.0.3",
|
"@fontsource/source-sans-pro": "^5.0.3",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
"@zerodevx/svelte-json-view": "^1.0.7",
|
||||||
"codemirror": "^5.59.0",
|
"codemirror": "^5.59.0",
|
||||||
"dayjs": "^1.10.8",
|
"dayjs": "^1.10.8",
|
||||||
"downloadjs": "1.4.7",
|
"downloadjs": "1.4.7",
|
||||||
|
@ -78,25 +79,24 @@
|
||||||
"svelte-dnd-action": "^0.9.8",
|
"svelte-dnd-action": "^0.9.8",
|
||||||
"svelte-loading-spinners": "^0.1.1",
|
"svelte-loading-spinners": "^0.1.1",
|
||||||
"svelte-portal": "1.0.0",
|
"svelte-portal": "1.0.0",
|
||||||
"yup": "0.29.2"
|
"yup": "^0.32.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.14",
|
|
||||||
"@babel/plugin-transform-runtime": "^7.13.10",
|
"@babel/plugin-transform-runtime": "^7.13.10",
|
||||||
"@babel/preset-env": "^7.13.12",
|
"@babel/preset-env": "^7.13.12",
|
||||||
"@rollup/plugin-replace": "^5.0.3",
|
"@rollup/plugin-replace": "^5.0.3",
|
||||||
"@roxi/routify": "2.18.12",
|
"@roxi/routify": "2.18.12",
|
||||||
"@sveltejs/vite-plugin-svelte": "1.0.1",
|
"@sveltejs/vite-plugin-svelte": "1.4.0",
|
||||||
"@testing-library/jest-dom": "5.17.0",
|
"@testing-library/jest-dom": "5.17.0",
|
||||||
"@testing-library/svelte": "^3.2.2",
|
"@testing-library/svelte": "^3.2.2",
|
||||||
"babel-jest": "29.6.2",
|
"babel-jest": "^29.6.2",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "29.6.2",
|
"jest": "29.7.0",
|
||||||
"jsdom": "^21.1.1",
|
"jsdom": "^21.1.1",
|
||||||
"ncp": "^2.0.0",
|
"ncp": "^2.0.0",
|
||||||
"svelte": "^3.48.0",
|
"svelte": "^3.49.0",
|
||||||
"svelte-jester": "^1.3.2",
|
"svelte-jester": "^1.3.2",
|
||||||
"vite": "^4.4.11",
|
"vite": "^4.5.0",
|
||||||
"vite-plugin-static-copy": "^0.17.0",
|
"vite-plugin-static-copy": "^0.17.0",
|
||||||
"vitest": "^0.29.2"
|
"vitest": "^0.29.2"
|
||||||
},
|
},
|
||||||
|
@ -115,7 +115,7 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dev:builder": {
|
"dev": {
|
||||||
"dependsOn": [
|
"dependsOn": [
|
||||||
{
|
{
|
||||||
"projects": [
|
"projects": [
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
} from "@budibase/frontend-core"
|
} from "@budibase/frontend-core"
|
||||||
import { store } from "./builderStore"
|
import { store } from "./builderStore"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { auth } from "./stores/portal"
|
import { auth, navigation } from "./stores/portal"
|
||||||
|
|
||||||
export const API = createAPIClient({
|
export const API = createAPIClient({
|
||||||
attachHeaders: headers => {
|
attachHeaders: headers => {
|
||||||
|
@ -45,4 +45,15 @@ export const API = createAPIClient({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onMigrationDetected: appId => {
|
||||||
|
const updatingUrl = `/builder/app/updating/${appId}`
|
||||||
|
|
||||||
|
if (window.location.pathname === updatingUrl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
get(navigation).goto(
|
||||||
|
`${updatingUrl}?returnUrl=${encodeURIComponent(window.location.pathname)}`
|
||||||
|
)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -92,7 +92,14 @@ export const findAllMatchingComponents = (rootComponent, selector) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the closes parent component which matches certain criteria
|
* Recurses through the component tree and finds all components.
|
||||||
|
*/
|
||||||
|
export const findAllComponents = rootComponent => {
|
||||||
|
return findAllMatchingComponents(rootComponent, () => true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the closest parent component which matches certain criteria
|
||||||
*/
|
*/
|
||||||
export const findClosestMatchingComponent = (
|
export const findClosestMatchingComponent = (
|
||||||
rootComponent,
|
rootComponent,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import {
|
import {
|
||||||
|
findAllComponents,
|
||||||
findAllMatchingComponents,
|
findAllMatchingComponents,
|
||||||
findComponent,
|
findComponent,
|
||||||
findComponentPath,
|
findComponentPath,
|
||||||
|
@ -102,6 +103,9 @@ export const getAuthBindings = () => {
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all bindings for environment variables
|
||||||
|
*/
|
||||||
export const getEnvironmentBindings = () => {
|
export const getEnvironmentBindings = () => {
|
||||||
let envVars = get(environment).variables
|
let envVars = get(environment).variables
|
||||||
return envVars.map(variable => {
|
return envVars.map(variable => {
|
||||||
|
@ -130,26 +134,22 @@ export const toBindingsArray = (valueMap, prefix, category) => {
|
||||||
if (!binding) {
|
if (!binding) {
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = {
|
let config = {
|
||||||
type: "context",
|
type: "context",
|
||||||
runtimeBinding: binding,
|
runtimeBinding: binding,
|
||||||
readableBinding: `${prefix}.${binding}`,
|
readableBinding: `${prefix}.${binding}`,
|
||||||
icon: "Brackets",
|
icon: "Brackets",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category) {
|
if (category) {
|
||||||
config.category = category
|
config.category = category
|
||||||
}
|
}
|
||||||
|
|
||||||
acc.push(config)
|
acc.push(config)
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility - coverting a map of readable bindings to runtime
|
* Utility to covert a map of readable bindings to runtime
|
||||||
*/
|
*/
|
||||||
export const readableToRuntimeMap = (bindings, ctx) => {
|
export const readableToRuntimeMap = (bindings, ctx) => {
|
||||||
if (!bindings || !ctx) {
|
if (!bindings || !ctx) {
|
||||||
|
@ -162,7 +162,7 @@ export const readableToRuntimeMap = (bindings, ctx) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility - coverting a map of runtime bindings to readable
|
* Utility to covert a map of runtime bindings to readable bindings
|
||||||
*/
|
*/
|
||||||
export const runtimeToReadableMap = (bindings, ctx) => {
|
export const runtimeToReadableMap = (bindings, ctx) => {
|
||||||
if (!bindings || !ctx) {
|
if (!bindings || !ctx) {
|
||||||
|
@ -188,15 +188,23 @@ export const getComponentBindableProperties = (asset, componentId) => {
|
||||||
if (!def?.context) {
|
if (!def?.context) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||||
|
|
||||||
// Get the bindings for the component
|
// Get the bindings for the component
|
||||||
return getProviderContextBindings(asset, component)
|
const componentContext = {
|
||||||
|
component,
|
||||||
|
definition: def,
|
||||||
|
contexts,
|
||||||
|
}
|
||||||
|
return generateComponentContextBindings(asset, componentContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all data provider components above a component.
|
* Gets all component contexts available to a certain component. This handles
|
||||||
|
* both global and local bindings, taking into account a component's position
|
||||||
|
* in the component tree.
|
||||||
*/
|
*/
|
||||||
export const getContextProviderComponents = (
|
export const getComponentContexts = (
|
||||||
asset,
|
asset,
|
||||||
componentId,
|
componentId,
|
||||||
type,
|
type,
|
||||||
|
@ -205,30 +213,55 @@ export const getContextProviderComponents = (
|
||||||
if (!asset || !componentId) {
|
if (!asset || !componentId) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
let map = {}
|
||||||
|
|
||||||
// Get the component tree leading up to this component, ignoring the component
|
// Processes all contexts exposed by a component
|
||||||
// itself
|
const processContexts = scope => component => {
|
||||||
const path = findComponentPath(asset.props, componentId)
|
|
||||||
if (!options?.includeSelf) {
|
|
||||||
path.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by only data provider components
|
|
||||||
return path.filter(component => {
|
|
||||||
const def = store.actions.components.getDefinition(component._component)
|
const def = store.actions.components.getDefinition(component._component)
|
||||||
if (!def?.context) {
|
if (!def?.context) {
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
|
if (!map[component._id]) {
|
||||||
// If no type specified, return anything that exposes context
|
map[component._id] = {
|
||||||
if (!type) {
|
component,
|
||||||
return true
|
definition: def,
|
||||||
|
contexts: [],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise only match components with the specific context type
|
|
||||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||||
return contexts.find(context => context.type === type) != null
|
contexts.forEach(context => {
|
||||||
})
|
// Ensure type matches
|
||||||
|
if (type && context.type !== type) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Ensure scope matches
|
||||||
|
let contextScope = context.scope || "global"
|
||||||
|
if (contextScope !== scope) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Ensure the context is compatible with the component's current settings
|
||||||
|
if (!isContextCompatibleWithComponent(context, component)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
map[component._id].contexts.push(context)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process all global contexts
|
||||||
|
const allComponents = findAllComponents(asset.props)
|
||||||
|
allComponents.forEach(processContexts("global"))
|
||||||
|
|
||||||
|
// Process all local contexts
|
||||||
|
const localComponents = findComponentPath(asset.props, componentId)
|
||||||
|
localComponents.forEach(processContexts("local"))
|
||||||
|
|
||||||
|
// Exclude self if required
|
||||||
|
if (!options?.includeSelf) {
|
||||||
|
delete map[componentId]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only return components which provide at least 1 matching context
|
||||||
|
return Object.values(map).filter(x => x.contexts.length > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -240,20 +273,19 @@ export const getActionProviders = (
|
||||||
actionType,
|
actionType,
|
||||||
options = { includeSelf: false }
|
options = { includeSelf: false }
|
||||||
) => {
|
) => {
|
||||||
if (!asset || !componentId) {
|
if (!asset) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the component tree leading up to this component, ignoring the component
|
// Get all components
|
||||||
// itself
|
const components = findAllComponents(asset.props)
|
||||||
const path = findComponentPath(asset.props, componentId)
|
|
||||||
if (!options?.includeSelf) {
|
|
||||||
path.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find matching contexts and generate bindings
|
// Find matching contexts and generate bindings
|
||||||
let providers = []
|
let providers = []
|
||||||
path.forEach(component => {
|
components.forEach(component => {
|
||||||
|
if (!options?.includeSelf && component._id === componentId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const def = store.actions.components.getDefinition(component._component)
|
const def = store.actions.components.getDefinition(component._component)
|
||||||
const actions = (def?.actions || []).map(action => {
|
const actions = (def?.actions || []).map(action => {
|
||||||
return typeof action === "string" ? { type: action } : action
|
return typeof action === "string" ? { type: action } : action
|
||||||
|
@ -317,142 +349,131 @@ export const getDatasourceForProvider = (asset, component) => {
|
||||||
* Gets all bindable data properties from component data contexts.
|
* Gets all bindable data properties from component data contexts.
|
||||||
*/
|
*/
|
||||||
const getContextBindings = (asset, componentId) => {
|
const getContextBindings = (asset, componentId) => {
|
||||||
// Extract any components which provide data contexts
|
// Get all available contexts for this component
|
||||||
const dataProviders = getContextProviderComponents(asset, componentId)
|
const componentContexts = getComponentContexts(asset, componentId)
|
||||||
|
|
||||||
// Generate bindings for all matching components
|
// Generate bindings for each context
|
||||||
return getProviderContextBindings(asset, dataProviders)
|
return componentContexts
|
||||||
|
.map(componentContext => {
|
||||||
|
return generateComponentContextBindings(asset, componentContext)
|
||||||
|
})
|
||||||
|
.flat()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the context bindings exposed by a set of data provider components.
|
* Generates a set of bindings for a given component context
|
||||||
*/
|
*/
|
||||||
const getProviderContextBindings = (asset, dataProviders) => {
|
const generateComponentContextBindings = (asset, componentContext) => {
|
||||||
if (!asset || !dataProviders) {
|
const { component, definition, contexts } = componentContext
|
||||||
|
if (!component || !definition || !contexts?.length) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure providers is an array
|
|
||||||
if (!Array.isArray(dataProviders)) {
|
|
||||||
dataProviders = [dataProviders]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create bindings for each data provider
|
// Create bindings for each data provider
|
||||||
let bindings = []
|
let bindings = []
|
||||||
dataProviders.forEach(component => {
|
contexts.forEach(context => {
|
||||||
const def = store.actions.components.getDefinition(component._component)
|
if (!context?.type) {
|
||||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Create bindings for each context block provided by this data provider
|
let schema
|
||||||
contexts.forEach(context => {
|
let table
|
||||||
if (!context?.type) {
|
let readablePrefix
|
||||||
|
let runtimeSuffix = context.suffix
|
||||||
|
|
||||||
|
if (context.type === "form") {
|
||||||
|
// Forms do not need table schemas
|
||||||
|
// Their schemas are built from their component field names
|
||||||
|
schema = buildFormSchema(component, asset)
|
||||||
|
readablePrefix = "Fields"
|
||||||
|
} else if (context.type === "static") {
|
||||||
|
// Static contexts are fully defined by the components
|
||||||
|
schema = {}
|
||||||
|
const values = context.values || []
|
||||||
|
values.forEach(value => {
|
||||||
|
schema[value.key] = {
|
||||||
|
name: value.label,
|
||||||
|
type: value.type || "string",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (context.type === "schema") {
|
||||||
|
// Schema contexts are generated dynamically depending on their data
|
||||||
|
const datasource = getDatasourceForProvider(asset, component)
|
||||||
|
if (!datasource) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const info = getSchemaForDatasource(asset, datasource)
|
||||||
|
schema = info.schema
|
||||||
|
table = info.table
|
||||||
|
|
||||||
let schema
|
// Determine what to prefix bindings with
|
||||||
let table
|
if (datasource.type === "jsonarray") {
|
||||||
let readablePrefix
|
// For JSON arrays, use the array name as the readable prefix
|
||||||
let runtimeSuffix = context.suffix
|
const split = datasource.label.split(".")
|
||||||
|
readablePrefix = split[split.length - 1]
|
||||||
if (context.type === "form") {
|
} else if (datasource.type === "viewV2") {
|
||||||
// Forms do not need table schemas
|
// For views, use the view name
|
||||||
// Their schemas are built from their component field names
|
const view = Object.values(table?.views || {}).find(
|
||||||
schema = buildFormSchema(component, asset)
|
view => view.id === datasource.id
|
||||||
readablePrefix = "Fields"
|
|
||||||
} else if (context.type === "static") {
|
|
||||||
// Static contexts are fully defined by the components
|
|
||||||
schema = {}
|
|
||||||
const values = context.values || []
|
|
||||||
values.forEach(value => {
|
|
||||||
schema[value.key] = {
|
|
||||||
name: value.label,
|
|
||||||
type: value.type || "string",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else if (context.type === "schema") {
|
|
||||||
// Schema contexts are generated dynamically depending on their data
|
|
||||||
const datasource = getDatasourceForProvider(asset, component)
|
|
||||||
if (!datasource) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const info = getSchemaForDatasource(asset, datasource)
|
|
||||||
schema = info.schema
|
|
||||||
table = info.table
|
|
||||||
|
|
||||||
// Determine what to prefix bindings with
|
|
||||||
if (datasource.type === "jsonarray") {
|
|
||||||
// For JSON arrays, use the array name as the readable prefix
|
|
||||||
const split = datasource.label.split(".")
|
|
||||||
readablePrefix = split[split.length - 1]
|
|
||||||
} else if (datasource.type === "viewV2") {
|
|
||||||
// For views, use the view name
|
|
||||||
const view = Object.values(table?.views || {}).find(
|
|
||||||
view => view.id === datasource.id
|
|
||||||
)
|
|
||||||
readablePrefix = view?.name
|
|
||||||
} else {
|
|
||||||
// Otherwise use the table name
|
|
||||||
readablePrefix = info.table?.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!schema) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = Object.keys(schema).sort()
|
|
||||||
|
|
||||||
// Generate safe unique runtime prefix
|
|
||||||
let providerId = component._id
|
|
||||||
if (runtimeSuffix) {
|
|
||||||
providerId += `-${runtimeSuffix}`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!filterCategoryByContext(component, context)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const safeComponentId = makePropSafe(providerId)
|
|
||||||
|
|
||||||
// Create bindable properties for each schema field
|
|
||||||
keys.forEach(key => {
|
|
||||||
const fieldSchema = schema[key]
|
|
||||||
|
|
||||||
// Make safe runtime binding
|
|
||||||
const safeKey = key.split(".").map(makePropSafe).join(".")
|
|
||||||
const runtimeBinding = `${safeComponentId}.${safeKey}`
|
|
||||||
|
|
||||||
// Optionally use a prefix with readable bindings
|
|
||||||
let readableBinding = component._instanceName
|
|
||||||
if (readablePrefix) {
|
|
||||||
readableBinding += `.${readablePrefix}`
|
|
||||||
}
|
|
||||||
readableBinding += `.${fieldSchema.name || key}`
|
|
||||||
|
|
||||||
const bindingCategory = getComponentBindingCategory(
|
|
||||||
component,
|
|
||||||
context,
|
|
||||||
def
|
|
||||||
)
|
)
|
||||||
|
readablePrefix = view?.name
|
||||||
|
} else {
|
||||||
|
// Otherwise use the table name
|
||||||
|
readablePrefix = info.table?.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!schema) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Create the binding object
|
const keys = Object.keys(schema).sort()
|
||||||
bindings.push({
|
|
||||||
type: "context",
|
// Generate safe unique runtime prefix
|
||||||
runtimeBinding,
|
let providerId = component._id
|
||||||
readableBinding,
|
if (runtimeSuffix) {
|
||||||
// Field schema and provider are required to construct relationship
|
providerId += `-${runtimeSuffix}`
|
||||||
// datasource options, based on bindable properties
|
}
|
||||||
fieldSchema,
|
const safeComponentId = makePropSafe(providerId)
|
||||||
providerId,
|
|
||||||
// Table ID is used by JSON fields to know what table the field is in
|
// Create bindable properties for each schema field
|
||||||
tableId: table?._id,
|
keys.forEach(key => {
|
||||||
component: component._component,
|
const fieldSchema = schema[key]
|
||||||
category: bindingCategory.category,
|
|
||||||
icon: bindingCategory.icon,
|
// Make safe runtime binding
|
||||||
display: {
|
const safeKey = key.split(".").map(makePropSafe).join(".")
|
||||||
name: fieldSchema.name || key,
|
const runtimeBinding = `${safeComponentId}.${safeKey}`
|
||||||
type: fieldSchema.type,
|
|
||||||
},
|
// Optionally use a prefix with readable bindings
|
||||||
})
|
let readableBinding = component._instanceName
|
||||||
|
if (readablePrefix) {
|
||||||
|
readableBinding += `.${readablePrefix}`
|
||||||
|
}
|
||||||
|
readableBinding += `.${fieldSchema.name || key}`
|
||||||
|
|
||||||
|
// Determine which category this binding belongs in
|
||||||
|
const bindingCategory = getComponentBindingCategory(
|
||||||
|
component,
|
||||||
|
context,
|
||||||
|
definition
|
||||||
|
)
|
||||||
|
// Create the binding object
|
||||||
|
bindings.push({
|
||||||
|
type: "context",
|
||||||
|
runtimeBinding,
|
||||||
|
readableBinding: `${readableBinding}`,
|
||||||
|
// Field schema and provider are required to construct relationship
|
||||||
|
// datasource options, based on bindable properties
|
||||||
|
fieldSchema,
|
||||||
|
providerId,
|
||||||
|
// Table ID is used by JSON fields to know what table the field is in
|
||||||
|
tableId: table?._id,
|
||||||
|
component: component._component,
|
||||||
|
category: bindingCategory.category,
|
||||||
|
icon: bindingCategory.icon,
|
||||||
|
display: {
|
||||||
|
name: `${fieldSchema.name || key}`,
|
||||||
|
type: fieldSchema.type,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -460,34 +481,48 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exclude a data context based on the component settings
|
/**
|
||||||
const filterCategoryByContext = (component, context) => {
|
* Checks if a certain data context is compatible with a certain instance of a
|
||||||
const { _component } = component
|
* configured component.
|
||||||
|
*/
|
||||||
|
const isContextCompatibleWithComponent = (context, component) => {
|
||||||
|
if (!component) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const { _component, actionType } = component
|
||||||
|
const { type } = context
|
||||||
|
|
||||||
|
// Certain types of form blocks only allow certain contexts
|
||||||
if (_component.endsWith("formblock")) {
|
if (_component.endsWith("formblock")) {
|
||||||
if (
|
if (
|
||||||
(component.actionType == "Create" && context.type === "schema") ||
|
(actionType === "Create" && type === "schema") ||
|
||||||
(component.actionType == "View" && context.type === "form")
|
(actionType === "View" && type === "form")
|
||||||
) {
|
) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow the context by default
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enrich binding category information for certain components
|
||||||
const getComponentBindingCategory = (component, context, def) => {
|
const getComponentBindingCategory = (component, context, def) => {
|
||||||
|
// Default category to component name
|
||||||
let icon = def.icon
|
let icon = def.icon
|
||||||
let category = component._instanceName
|
let category = component._instanceName
|
||||||
|
|
||||||
|
// Form block edge case
|
||||||
if (component._component.endsWith("formblock")) {
|
if (component._component.endsWith("formblock")) {
|
||||||
let contextCategorySuffix = {
|
if (context.type === "form") {
|
||||||
form: "Fields",
|
category = `${component._instanceName} - Fields`
|
||||||
schema: "Row",
|
icon = "Form"
|
||||||
|
} else if (context.type === "schema") {
|
||||||
|
category = `${component._instanceName} - Row`
|
||||||
|
icon = "Data"
|
||||||
}
|
}
|
||||||
category = `${component._instanceName} - ${
|
|
||||||
contextCategorySuffix[context.type]
|
|
||||||
}`
|
|
||||||
icon = context.type === "form" ? "Form" : "Data"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
icon,
|
icon,
|
||||||
category,
|
category,
|
||||||
|
@ -495,7 +530,7 @@ const getComponentBindingCategory = (component, context, def) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all bindable properties from the logged in user.
|
* Gets all bindable properties from the logged-in user.
|
||||||
*/
|
*/
|
||||||
export const getUserBindings = () => {
|
export const getUserBindings = () => {
|
||||||
let bindings = []
|
let bindings = []
|
||||||
|
@ -565,6 +600,7 @@ const getDeviceBindings = () => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all selected rows bindings for tables in the current asset.
|
* Gets all selected rows bindings for tables in the current asset.
|
||||||
|
* TODO: remove in future because we don't need a separate store for this
|
||||||
*/
|
*/
|
||||||
const getSelectedRowsBindings = asset => {
|
const getSelectedRowsBindings = asset => {
|
||||||
let bindings = []
|
let bindings = []
|
||||||
|
@ -607,6 +643,9 @@ const getSelectedRowsBindings = asset => {
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a state binding for a certain key name
|
||||||
|
*/
|
||||||
export const makeStateBinding = key => {
|
export const makeStateBinding = key => {
|
||||||
return {
|
return {
|
||||||
type: "context",
|
type: "context",
|
||||||
|
@ -661,6 +700,9 @@ const getUrlBindings = asset => {
|
||||||
return urlParamBindings.concat([queryParamsBinding])
|
return urlParamBindings.concat([queryParamsBinding])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates all bindings for role IDs
|
||||||
|
*/
|
||||||
const getRoleBindings = () => {
|
const getRoleBindings = () => {
|
||||||
return (get(rolesStore) || []).map(role => {
|
return (get(rolesStore) || []).map(role => {
|
||||||
return {
|
return {
|
||||||
|
@ -1034,11 +1076,48 @@ export const getAllStateVariables = () => {
|
||||||
getAllAssets().forEach(asset => {
|
getAllAssets().forEach(asset => {
|
||||||
findAllMatchingComponents(asset.props, component => {
|
findAllMatchingComponents(asset.props, component => {
|
||||||
const settings = getComponentSettings(component._component)
|
const settings = getComponentSettings(component._component)
|
||||||
settings
|
|
||||||
.filter(setting => setting.type === "event")
|
const parseEventSettings = (settings, comp) => {
|
||||||
.forEach(setting => {
|
settings
|
||||||
eventSettings.push(component[setting.key])
|
.filter(setting => setting.type === "event")
|
||||||
})
|
.forEach(setting => {
|
||||||
|
eventSettings.push(comp[setting.key])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseComponentSettings = (settings, component) => {
|
||||||
|
// Parse the nested button configurations
|
||||||
|
settings
|
||||||
|
.filter(setting => setting.type === "buttonConfiguration")
|
||||||
|
.forEach(setting => {
|
||||||
|
const buttonConfig = component[setting.key]
|
||||||
|
|
||||||
|
if (Array.isArray(buttonConfig)) {
|
||||||
|
buttonConfig.forEach(button => {
|
||||||
|
const nestedSettings = getComponentSettings(button._component)
|
||||||
|
parseEventSettings(nestedSettings, button)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
parseEventSettings(settings, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the base component settings
|
||||||
|
parseComponentSettings(settings, component)
|
||||||
|
|
||||||
|
// Parse step configuration
|
||||||
|
const stepSetting = settings.find(
|
||||||
|
setting => setting.type === "stepConfiguration"
|
||||||
|
)
|
||||||
|
const steps = stepSetting ? component[stepSetting.key] : []
|
||||||
|
const stepDefinition = getComponentSettings(
|
||||||
|
"@budibase/standard-components/multistepformblockstep"
|
||||||
|
)
|
||||||
|
|
||||||
|
steps.forEach(step => {
|
||||||
|
parseComponentSettings(stepDefinition, step)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { findComponent, findComponentPath } from "./componentUtils"
|
||||||
import { RoleUtils } from "@budibase/frontend-core"
|
import { RoleUtils } from "@budibase/frontend-core"
|
||||||
import { createHistoryStore } from "builderStore/store/history"
|
import { createHistoryStore } from "builderStore/store/history"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import { getHoverStore } from "./store/hover"
|
||||||
|
|
||||||
export const store = getFrontendStore()
|
export const store = getFrontendStore()
|
||||||
export const automationStore = getAutomationStore()
|
export const automationStore = getAutomationStore()
|
||||||
|
@ -16,6 +17,7 @@ export const themeStore = getThemeStore()
|
||||||
export const temporalStore = getTemporalStore()
|
export const temporalStore = getTemporalStore()
|
||||||
export const userStore = getUserStore()
|
export const userStore = getUserStore()
|
||||||
export const deploymentStore = getDeploymentStore()
|
export const deploymentStore = getDeploymentStore()
|
||||||
|
export const hoverStore = getHoverStore()
|
||||||
|
|
||||||
// Setup history for screens
|
// Setup history for screens
|
||||||
export const screenHistoryStore = createHistoryStore({
|
export const screenHistoryStore = createHistoryStore({
|
||||||
|
|
|
@ -85,7 +85,6 @@ const INITIAL_FRONTEND_STATE = {
|
||||||
selectedScreenId: null,
|
selectedScreenId: null,
|
||||||
selectedComponentId: null,
|
selectedComponentId: null,
|
||||||
selectedLayoutId: null,
|
selectedLayoutId: null,
|
||||||
hoverComponentId: null,
|
|
||||||
|
|
||||||
// Client state
|
// Client state
|
||||||
selectedComponentInstance: null,
|
selectedComponentInstance: null,
|
||||||
|
@ -159,6 +158,7 @@ export const getFrontendStore = () => {
|
||||||
...INITIAL_FRONTEND_STATE.features,
|
...INITIAL_FRONTEND_STATE.features,
|
||||||
...application.features,
|
...application.features,
|
||||||
},
|
},
|
||||||
|
automations: application.automations || {},
|
||||||
icon: application.icon || {},
|
icon: application.icon || {},
|
||||||
initialised: true,
|
initialised: true,
|
||||||
}))
|
}))
|
||||||
|
@ -610,12 +610,12 @@ export const getFrontendStore = () => {
|
||||||
// Use default config if the 'buttons' prop has never been initialised
|
// Use default config if the 'buttons' prop has never been initialised
|
||||||
if (!("buttons" in enrichedComponent)) {
|
if (!("buttons" in enrichedComponent)) {
|
||||||
enrichedComponent["buttons"] =
|
enrichedComponent["buttons"] =
|
||||||
Utils.buildDynamicButtonConfig(enrichedComponent)
|
Utils.buildFormBlockButtonConfig(enrichedComponent)
|
||||||
migrated = true
|
migrated = true
|
||||||
} else if (enrichedComponent["buttons"] == null) {
|
} else if (enrichedComponent["buttons"] == null) {
|
||||||
// Ignore legacy config if 'buttons' has been reset by 'resetOn'
|
// Ignore legacy config if 'buttons' has been reset by 'resetOn'
|
||||||
const { _id, actionType, dataSource } = enrichedComponent
|
const { _id, actionType, dataSource } = enrichedComponent
|
||||||
enrichedComponent["buttons"] = Utils.buildDynamicButtonConfig({
|
enrichedComponent["buttons"] = Utils.buildFormBlockButtonConfig({
|
||||||
_id,
|
_id,
|
||||||
actionType,
|
actionType,
|
||||||
dataSource,
|
dataSource,
|
||||||
|
@ -707,10 +707,9 @@ export const getFrontendStore = () => {
|
||||||
else {
|
else {
|
||||||
if (setting.type === "dataProvider") {
|
if (setting.type === "dataProvider") {
|
||||||
// Validate data provider exists, or else clear it
|
// Validate data provider exists, or else clear it
|
||||||
const treeId = parent?._id || component._id
|
const providers = findAllMatchingComponents(
|
||||||
const path = findComponentPath(screen?.props, treeId)
|
screen?.props,
|
||||||
const providers = path.filter(component =>
|
component => component._component?.endsWith("/dataprovider")
|
||||||
component._component?.endsWith("/dataprovider")
|
|
||||||
)
|
)
|
||||||
// Validate non-empty values
|
// Validate non-empty values
|
||||||
const valid = providers?.some(dp => value.includes?.(dp._id))
|
const valid = providers?.some(dp => value.includes?.(dp._id))
|
||||||
|
@ -732,6 +731,16 @@ export const getFrontendStore = () => {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find all existing components of this type so that we can give this
|
||||||
|
// component a unique name
|
||||||
|
const screen = get(selectedScreen).props
|
||||||
|
const otherComponents = findAllMatchingComponents(
|
||||||
|
screen,
|
||||||
|
x => x._component === definition.component && x._id !== screen._id
|
||||||
|
)
|
||||||
|
let name = definition.friendlyName || definition.name
|
||||||
|
name = `${name} ${otherComponents.length + 1}`
|
||||||
|
|
||||||
// Generate basic component structure
|
// Generate basic component structure
|
||||||
let instance = {
|
let instance = {
|
||||||
_id: Helpers.uuid(),
|
_id: Helpers.uuid(),
|
||||||
|
@ -741,7 +750,7 @@ export const getFrontendStore = () => {
|
||||||
hover: {},
|
hover: {},
|
||||||
active: {},
|
active: {},
|
||||||
},
|
},
|
||||||
_instanceName: `New ${definition.friendlyName || definition.name}`,
|
_instanceName: name,
|
||||||
...presetProps,
|
...presetProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1289,15 +1298,14 @@ export const getFrontendStore = () => {
|
||||||
const settings = getComponentSettings(component._component)
|
const settings = getComponentSettings(component._component)
|
||||||
const updatedSetting = settings.find(setting => setting.key === name)
|
const updatedSetting = settings.find(setting => setting.key === name)
|
||||||
|
|
||||||
// Can be a single string or array of strings
|
// Reset dependent fields
|
||||||
const resetFields = settings.filter(setting => {
|
settings.forEach(setting => {
|
||||||
return (
|
const needsReset =
|
||||||
name === setting.resetOn ||
|
name === setting.resetOn ||
|
||||||
(Array.isArray(setting.resetOn) && setting.resetOn.includes(name))
|
(Array.isArray(setting.resetOn) && setting.resetOn.includes(name))
|
||||||
)
|
if (needsReset) {
|
||||||
})
|
component[setting.key] = setting.defaultValue || null
|
||||||
resetFields?.forEach(setting => {
|
}
|
||||||
component[setting.key] = null
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { get, writable } from "svelte/store"
|
||||||
|
import { store as builder } from "builderStore"
|
||||||
|
|
||||||
|
export const getHoverStore = () => {
|
||||||
|
const initialValue = {
|
||||||
|
componentId: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = writable(initialValue)
|
||||||
|
|
||||||
|
const update = (componentId, notifyClient = true) => {
|
||||||
|
if (componentId === get(store).componentId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
store.update(state => {
|
||||||
|
state.componentId = componentId
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
if (notifyClient) {
|
||||||
|
builder.actions.preview.sendEvent("hover-component", componentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions: { update },
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ export const createBuilderWebsocket = appId => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
socket.on("connect_error", err => {
|
socket.on("connect_error", err => {
|
||||||
console.log("Failed to connect to builder websocket:", err.message)
|
console.error("Failed to connect to builder websocket:", err.message)
|
||||||
})
|
})
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
userStore.actions.reset()
|
userStore.actions.reset()
|
||||||
|
|
|
@ -19,10 +19,15 @@
|
||||||
export let lastStep
|
export let lastStep
|
||||||
|
|
||||||
let syncAutomationsEnabled = $licensing.syncAutomationsEnabled
|
let syncAutomationsEnabled = $licensing.syncAutomationsEnabled
|
||||||
|
let triggerAutomationRunEnabled = $licensing.triggerAutomationRunEnabled
|
||||||
let collectBlockAllowedSteps = [TriggerStepID.APP, TriggerStepID.WEBHOOK]
|
let collectBlockAllowedSteps = [TriggerStepID.APP, TriggerStepID.WEBHOOK]
|
||||||
let selectedAction
|
let selectedAction
|
||||||
let actionVal
|
let actionVal
|
||||||
let actions = Object.entries($automationStore.blockDefinitions.ACTION)
|
let actions = Object.entries($automationStore.blockDefinitions.ACTION)
|
||||||
|
let lockedFeatures = [
|
||||||
|
ActionStepID.COLLECT,
|
||||||
|
ActionStepID.TRIGGER_AUTOMATION_RUN,
|
||||||
|
]
|
||||||
|
|
||||||
$: collectBlockExists = checkForCollectStep($selectedAutomation)
|
$: collectBlockExists = checkForCollectStep($selectedAutomation)
|
||||||
|
|
||||||
|
@ -36,6 +41,10 @@
|
||||||
disabled: !lastStep || !syncAutomationsEnabled || collectBlockExists,
|
disabled: !lastStep || !syncAutomationsEnabled || collectBlockExists,
|
||||||
message: collectDisabledMessage(),
|
message: collectDisabledMessage(),
|
||||||
},
|
},
|
||||||
|
TRIGGER_AUTOMATION_RUN: {
|
||||||
|
disabled: !triggerAutomationRunEnabled,
|
||||||
|
message: "Please upgrade to a paid plan",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,10 +158,10 @@
|
||||||
<div class="item-body">
|
<div class="item-body">
|
||||||
<Icon name={action.icon} />
|
<Icon name={action.icon} />
|
||||||
<Body size="XS">{action.name}</Body>
|
<Body size="XS">{action.name}</Body>
|
||||||
{#if isDisabled && !syncAutomationsEnabled && action.stepId === ActionStepID.COLLECT}
|
{#if isDisabled && !syncAutomationsEnabled && !triggerAutomationRunEnabled && lockedFeatures.includes(action.stepId)}
|
||||||
<div class="tag-color">
|
<div class="tag-color">
|
||||||
<Tags>
|
<Tags>
|
||||||
<Tag icon="LockClosed">Business</Tag>
|
<Tag icon="LockClosed">Premium</Tag>
|
||||||
</Tags>
|
</Tags>
|
||||||
</div>
|
</div>
|
||||||
{:else if isDisabled}
|
{:else if isDisabled}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, Divider, Tabs, Tab, TextArea, Label } from "@budibase/bbui"
|
import { Icon, Divider, Tabs, Tab, Label } from "@budibase/bbui"
|
||||||
import FlowItemHeader from "./FlowChart/FlowItemHeader.svelte"
|
import FlowItemHeader from "./FlowChart/FlowItemHeader.svelte"
|
||||||
import { ActionStepID } from "constants/backend/automations"
|
import { ActionStepID } from "constants/backend/automations"
|
||||||
|
import { JsonView } from "@zerodevx/svelte-json-view"
|
||||||
|
|
||||||
export let automation
|
export let automation
|
||||||
export let testResults
|
export let testResults
|
||||||
|
@ -14,13 +15,6 @@
|
||||||
return results?.steps.filter(x => x.stepId !== ActionStepID.LOOP || [])
|
return results?.steps.filter(x => x.stepId !== ActionStepID.LOOP || [])
|
||||||
}
|
}
|
||||||
|
|
||||||
function textArea(results, message) {
|
|
||||||
if (!results) {
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
return JSON.stringify(results, null, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
$: filteredResults = prepTestResults(testResults)
|
$: filteredResults = prepTestResults(testResults)
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
|
@ -71,26 +65,34 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<Tabs noHorizPadding selected="Input">
|
<Tabs quiet noHorizPadding selected="Input">
|
||||||
<Tab title="Input">
|
<Tab title="Input">
|
||||||
<TextArea
|
<div class="wrap">
|
||||||
minHeight="160px"
|
{#if filteredResults?.[idx]?.inputs}
|
||||||
disabled
|
<JsonView depth={2} json={filteredResults?.[idx]?.inputs} />
|
||||||
value={textArea(filteredResults?.[idx]?.inputs, "No input")}
|
{:else}
|
||||||
/>
|
No input
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Output">
|
<Tab title="Output">
|
||||||
<TextArea
|
<div class="wrap">
|
||||||
minHeight="160px"
|
{#if filteredResults?.[idx]?.outputs}
|
||||||
disabled
|
<JsonView
|
||||||
value={textArea(filteredResults?.[idx]?.outputs, "No output")}
|
depth={2}
|
||||||
/>
|
json={filteredResults?.[idx]?.outputs}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
No input
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if blocks.length - 1 !== idx}
|
{#if blocks.length - 1 !== idx}
|
||||||
<div class="separator" />
|
<div class="separator" />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -104,6 +106,33 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
font-family: monospace;
|
||||||
|
background-color: var(
|
||||||
|
--spectrum-textfield-m-background-color,
|
||||||
|
var(--spectrum-global-color-gray-50)
|
||||||
|
);
|
||||||
|
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||||
|
font-size: 12px;
|
||||||
|
max-height: 160px; /* Adjusted max-height */
|
||||||
|
height: 160px;
|
||||||
|
--jsonPaddingLeft: 20px;
|
||||||
|
--jsonborderleft: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
overflow: overlay;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-left: var(--spacing-s);
|
||||||
|
padding-top: var(--spacing-xl);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap::-webkit-scrollbar {
|
||||||
|
width: 7px; /* width of the scrollbar */
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap::-webkit-scrollbar-track {
|
||||||
|
background: transparent; /* transparent track */
|
||||||
|
}
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -1,44 +1,117 @@
|
||||||
<script>
|
<script>
|
||||||
import AutomationList from "./AutomationList.svelte"
|
|
||||||
import CreateAutomationModal from "./CreateAutomationModal.svelte"
|
import CreateAutomationModal from "./CreateAutomationModal.svelte"
|
||||||
import { Modal, Icon } from "@budibase/bbui"
|
import { Modal, notifications, Layout } from "@budibase/bbui"
|
||||||
import Panel from "components/design/Panel.svelte"
|
import NavHeader from "components/common/NavHeader.svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import {
|
||||||
|
automationStore,
|
||||||
|
selectedAutomation,
|
||||||
|
userSelectedResourceMap,
|
||||||
|
} from "builderStore"
|
||||||
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
|
import EditAutomationPopover from "./EditAutomationPopover.svelte"
|
||||||
|
|
||||||
export let modal
|
export let modal
|
||||||
export let webhookModal
|
export let webhookModal
|
||||||
|
let searchString
|
||||||
|
|
||||||
|
$: selectedAutomationId = $selectedAutomation?._id
|
||||||
|
|
||||||
|
$: filteredAutomations = $automationStore.automations
|
||||||
|
.filter(automation => {
|
||||||
|
return (
|
||||||
|
!searchString ||
|
||||||
|
automation.name.toLowerCase().includes(searchString.toLowerCase())
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const lowerA = a.name.toLowerCase()
|
||||||
|
const lowerB = b.name.toLowerCase()
|
||||||
|
return lowerA > lowerB ? 1 : -1
|
||||||
|
})
|
||||||
|
|
||||||
|
$: showNoResults = searchString && !filteredAutomations.length
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
await automationStore.actions.fetch()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error getting automations list")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function selectAutomation(id) {
|
||||||
|
automationStore.actions.select(id)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel title="Automations" borderRight noHeaderBorder titleCSS={false}>
|
<div class="side-bar">
|
||||||
<span class="panel-title-content" slot="panel-title-content">
|
<div class="side-bar-controls">
|
||||||
<div class="header">
|
<NavHeader
|
||||||
<div>Automations</div>
|
title="Automations"
|
||||||
<div on:click={modal.show} class="add-automation-button">
|
placeholder="Search for automation"
|
||||||
<Icon name="Add" />
|
bind:value={searchString}
|
||||||
</div>
|
onAdd={() => modal.show()}
|
||||||
</div>
|
/>
|
||||||
</span>
|
</div>
|
||||||
<AutomationList />
|
<div class="side-bar-nav">
|
||||||
</Panel>
|
{#each filteredAutomations as automation}
|
||||||
|
<NavItem
|
||||||
|
text={automation.name}
|
||||||
|
selected={automation._id === selectedAutomationId}
|
||||||
|
on:click={() => selectAutomation(automation._id)}
|
||||||
|
selectedBy={$userSelectedResourceMap[automation._id]}
|
||||||
|
>
|
||||||
|
<EditAutomationPopover {automation} />
|
||||||
|
</NavItem>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if showNoResults}
|
||||||
|
<Layout paddingY="none" paddingX="L">
|
||||||
|
<div class="no-results">
|
||||||
|
There aren't any automations matching that name
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<CreateAutomationModal {webhookModal} />
|
<CreateAutomationModal {webhookModal} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.header {
|
.side-bar {
|
||||||
|
flex: 0 0 260px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
border-right: var(--border-light);
|
||||||
|
background: var(--spectrum-global-color-gray-100);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: margin-left 300ms ease-out;
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.side-bar {
|
||||||
|
margin-left: -262px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-bar-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: var(--spacing-l);
|
||||||
gap: var(--spacing-m);
|
padding: 0 var(--spacing-l);
|
||||||
|
}
|
||||||
|
.side-bar-nav {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-automation-button {
|
.no-results {
|
||||||
margin-left: 130px;
|
color: var(--spectrum-global-color-gray-600);
|
||||||
color: var(--grey-7);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-automation-button:hover {
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
import CodeEditorModal from "./CodeEditorModal.svelte"
|
import CodeEditorModal from "./CodeEditorModal.svelte"
|
||||||
import QuerySelector from "./QuerySelector.svelte"
|
import QuerySelector from "./QuerySelector.svelte"
|
||||||
import QueryParamSelector from "./QueryParamSelector.svelte"
|
import QueryParamSelector from "./QueryParamSelector.svelte"
|
||||||
|
import AutomationSelector from "./AutomationSelector.svelte"
|
||||||
import CronBuilder from "./CronBuilder.svelte"
|
import CronBuilder from "./CronBuilder.svelte"
|
||||||
import Editor from "components/integration/QueryEditor.svelte"
|
import Editor from "components/integration/QueryEditor.svelte"
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||||
|
@ -51,7 +52,6 @@
|
||||||
export let testData
|
export let testData
|
||||||
export let schemaProperties
|
export let schemaProperties
|
||||||
export let isTestModal = false
|
export let isTestModal = false
|
||||||
|
|
||||||
let webhookModal
|
let webhookModal
|
||||||
let drawer
|
let drawer
|
||||||
let fillWidth = true
|
let fillWidth = true
|
||||||
|
@ -101,7 +101,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChange = Utils.sequential(async (e, key) => {
|
const onChange = Utils.sequential(async (e, key) => {
|
||||||
// We need to cache the schema as part of the definition because it is
|
// We need to cache the schema as part of the definition because it is
|
||||||
// used in the server to detect relationships. It would be far better to
|
// used in the server to detect relationships. It would be far better to
|
||||||
|
@ -145,6 +144,7 @@
|
||||||
if (!block || !automation) {
|
if (!block || !automation) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find previous steps to the selected one
|
// Find previous steps to the selected one
|
||||||
let allSteps = [...automation.steps]
|
let allSteps = [...automation.steps]
|
||||||
|
|
||||||
|
@ -156,22 +156,98 @@
|
||||||
// Extract all outputs from all previous steps as available bindingsx§x
|
// Extract all outputs from all previous steps as available bindingsx§x
|
||||||
let bindings = []
|
let bindings = []
|
||||||
let loopBlockCount = 0
|
let loopBlockCount = 0
|
||||||
|
const addBinding = (name, value, icon, idx, isLoopBlock, bindingName) => {
|
||||||
|
if (!name) return
|
||||||
|
const runtimeBinding = determineRuntimeBinding(name, idx, isLoopBlock)
|
||||||
|
const categoryName = determineCategoryName(idx, isLoopBlock, bindingName)
|
||||||
|
|
||||||
|
bindings.push(
|
||||||
|
createBindingObject(
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
icon,
|
||||||
|
idx,
|
||||||
|
loopBlockCount,
|
||||||
|
isLoopBlock,
|
||||||
|
runtimeBinding,
|
||||||
|
categoryName,
|
||||||
|
bindingName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const determineRuntimeBinding = (name, idx, isLoopBlock) => {
|
||||||
|
let runtimeName
|
||||||
|
|
||||||
|
/* Begin special cases for generating custom schemas based on triggers */
|
||||||
|
if (idx === 0 && automation.trigger?.event === "app:trigger") {
|
||||||
|
return `trigger.fields.${name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
idx === 0 &&
|
||||||
|
(automation.trigger?.event === "row:update" ||
|
||||||
|
automation.trigger?.event === "row:save")
|
||||||
|
) {
|
||||||
|
if (name !== "id" && name !== "revision") return `trigger.row.${name}`
|
||||||
|
}
|
||||||
|
/* End special cases for generating custom schemas based on triggers */
|
||||||
|
|
||||||
|
if (isLoopBlock) {
|
||||||
|
runtimeName = `loop.${name}`
|
||||||
|
} else if (block.name.startsWith("JS")) {
|
||||||
|
runtimeName = `steps[${idx - loopBlockCount}].${name}`
|
||||||
|
} else {
|
||||||
|
runtimeName = `steps.${idx - loopBlockCount}.${name}`
|
||||||
|
}
|
||||||
|
return idx === 0 ? `trigger.${name}` : runtimeName
|
||||||
|
}
|
||||||
|
|
||||||
|
const determineCategoryName = (idx, isLoopBlock, bindingName) => {
|
||||||
|
if (idx === 0) return "Trigger outputs"
|
||||||
|
if (isLoopBlock) return "Loop Outputs"
|
||||||
|
return bindingName
|
||||||
|
? `${bindingName} outputs`
|
||||||
|
: `Step ${idx - loopBlockCount} outputs`
|
||||||
|
}
|
||||||
|
|
||||||
|
const createBindingObject = (
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
icon,
|
||||||
|
idx,
|
||||||
|
loopBlockCount,
|
||||||
|
isLoopBlock,
|
||||||
|
runtimeBinding,
|
||||||
|
categoryName,
|
||||||
|
bindingName
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
readableBinding: bindingName
|
||||||
|
? `${bindingName}.${name}`
|
||||||
|
: runtimeBinding,
|
||||||
|
runtimeBinding,
|
||||||
|
type: value.type,
|
||||||
|
description: value.description,
|
||||||
|
icon,
|
||||||
|
category: categoryName,
|
||||||
|
display: {
|
||||||
|
type: value.type,
|
||||||
|
name,
|
||||||
|
rank: isLoopBlock ? idx + 1 : idx - loopBlockCount,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (let idx = 0; idx < blockIdx; idx++) {
|
for (let idx = 0; idx < blockIdx; idx++) {
|
||||||
let wasLoopBlock = allSteps[idx - 1]?.stepId === ActionStepID.LOOP
|
let wasLoopBlock = allSteps[idx - 1]?.stepId === ActionStepID.LOOP
|
||||||
let isLoopBlock =
|
let isLoopBlock =
|
||||||
allSteps[idx]?.stepId === ActionStepID.LOOP &&
|
allSteps[idx]?.stepId === ActionStepID.LOOP &&
|
||||||
allSteps.find(x => x.blockToLoop === block.id)
|
allSteps.some(x => x.blockToLoop === block.id)
|
||||||
|
let schema = cloneDeep(allSteps[idx]?.schema?.outputs?.properties) ?? {}
|
||||||
|
let bindingName =
|
||||||
|
automation.stepNames?.[allSteps[idx - loopBlockCount].id]
|
||||||
|
|
||||||
// If the previous block was a loop block, decrement the index so the following
|
|
||||||
// steps are in the correct order
|
|
||||||
if (wasLoopBlock) {
|
|
||||||
loopBlockCount++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
let schema = allSteps[idx]?.schema?.outputs?.properties ?? {}
|
|
||||||
|
|
||||||
// If its a Loop Block, we need to add this custom schema
|
|
||||||
if (isLoopBlock) {
|
if (isLoopBlock) {
|
||||||
schema = {
|
schema = {
|
||||||
currentItem: {
|
currentItem: {
|
||||||
|
@ -180,54 +256,44 @@
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const outputs = Object.entries(schema)
|
|
||||||
let bindingIcon = ""
|
if (idx === 0 && automation.trigger?.event === "app:trigger") {
|
||||||
let bindingRank = 0
|
schema = Object.fromEntries(
|
||||||
if (idx === 0) {
|
Object.keys(automation.trigger.inputs.fields || []).map(key => [
|
||||||
bindingIcon = automation.trigger.icon
|
key,
|
||||||
} else if (isLoopBlock) {
|
{ type: automation.trigger.inputs.fields[key] },
|
||||||
bindingIcon = "Reuse"
|
])
|
||||||
bindingRank = idx + 1
|
)
|
||||||
} else {
|
|
||||||
bindingIcon = allSteps[idx].icon
|
|
||||||
bindingRank = idx - loopBlockCount
|
|
||||||
}
|
}
|
||||||
let bindingName =
|
if (
|
||||||
automation.stepNames?.[allSteps[idx - loopBlockCount].id]
|
(idx === 0 && automation.trigger.event === "row:update") ||
|
||||||
bindings = bindings.concat(
|
(idx === 0 && automation.trigger.event === "row:save")
|
||||||
outputs.map(([name, value]) => {
|
) {
|
||||||
let runtimeName = isLoopBlock
|
let table = $tables.list.find(
|
||||||
? `loop.${name}`
|
table => table._id === automation.trigger.inputs.tableId
|
||||||
: block.name.startsWith("JS")
|
)
|
||||||
? `steps[${idx - loopBlockCount}].${name}`
|
// We want to generate our own schema for the bindings from the table schema itself
|
||||||
: `steps.${idx - loopBlockCount}.${name}`
|
for (const key in table?.schema) {
|
||||||
const runtime = idx === 0 ? `trigger.${name}` : runtimeName
|
schema[key] = {
|
||||||
|
type: table.schema[key].type,
|
||||||
let categoryName
|
|
||||||
if (idx === 0) {
|
|
||||||
categoryName = "Trigger outputs"
|
|
||||||
} else if (isLoopBlock) {
|
|
||||||
categoryName = "Loop Outputs"
|
|
||||||
} else if (bindingName) {
|
|
||||||
categoryName = `${bindingName} outputs`
|
|
||||||
} else {
|
|
||||||
categoryName = `Step ${idx - loopBlockCount} outputs`
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// remove the original binding
|
||||||
|
delete schema.row
|
||||||
|
}
|
||||||
|
let icon =
|
||||||
|
idx === 0
|
||||||
|
? automation.trigger.icon
|
||||||
|
: isLoopBlock
|
||||||
|
? "Reuse"
|
||||||
|
: allSteps[idx].icon
|
||||||
|
|
||||||
return {
|
if (wasLoopBlock) {
|
||||||
readableBinding: bindingName ? `${bindingName}.${name}` : runtime,
|
loopBlockCount++
|
||||||
runtimeBinding: runtime,
|
continue
|
||||||
type: value.type,
|
}
|
||||||
description: value.description,
|
Object.entries(schema).forEach(([name, value]) =>
|
||||||
icon: bindingIcon,
|
addBinding(name, value, icon, idx, isLoopBlock, bindingName)
|
||||||
category: categoryName,
|
|
||||||
display: {
|
|
||||||
type: value.type,
|
|
||||||
name: name,
|
|
||||||
rank: bindingRank,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,10 +311,8 @@
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return bindings
|
return bindings
|
||||||
}
|
}
|
||||||
|
|
||||||
function lookForFilters(properties) {
|
function lookForFilters(properties) {
|
||||||
if (!properties) {
|
if (!properties) {
|
||||||
return []
|
return []
|
||||||
|
@ -286,7 +350,8 @@
|
||||||
value.customType !== "code" &&
|
value.customType !== "code" &&
|
||||||
value.customType !== "queryParams" &&
|
value.customType !== "queryParams" &&
|
||||||
value.customType !== "cron" &&
|
value.customType !== "cron" &&
|
||||||
value.customType !== "triggerSchema"
|
value.customType !== "triggerSchema" &&
|
||||||
|
value.customType !== "automationFields"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -421,6 +486,12 @@
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
/>
|
/>
|
||||||
|
{:else if value.customType === "automationFields"}
|
||||||
|
<AutomationSelector
|
||||||
|
on:change={e => onChange(e, key)}
|
||||||
|
value={inputData[key]}
|
||||||
|
{bindings}
|
||||||
|
/>
|
||||||
{:else if value.customType === "queryParams"}
|
{:else if value.customType === "queryParams"}
|
||||||
<QueryParamSelector
|
<QueryParamSelector
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
<script>
|
||||||
|
import { Select, Label } from "@budibase/bbui"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
|
import { TriggerStepID } from "constants/backend/automations"
|
||||||
|
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||||
|
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
export let value
|
||||||
|
export let bindings = []
|
||||||
|
const onChangeAutomation = e => {
|
||||||
|
value.automationId = e.detail
|
||||||
|
dispatch("change", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange = (e, field) => {
|
||||||
|
value[field] = e.detail
|
||||||
|
dispatch("change", value)
|
||||||
|
}
|
||||||
|
$: if (value?.automationId == null) value = { automationId: "" }
|
||||||
|
|
||||||
|
$: automationFields =
|
||||||
|
$automationStore.automations.find(
|
||||||
|
automation => automation._id === value?.automationId
|
||||||
|
)?.definition?.trigger?.inputs?.fields || []
|
||||||
|
|
||||||
|
$: filteredAutomations = $automationStore.automations.filter(
|
||||||
|
automation =>
|
||||||
|
automation.definition.trigger.stepId === TriggerStepID.APP &&
|
||||||
|
automation._id !== $selectedAutomation._id
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="schema-field">
|
||||||
|
<Label>Automation</Label>
|
||||||
|
<div class="field-width">
|
||||||
|
<Select
|
||||||
|
on:change={onChangeAutomation}
|
||||||
|
value={value.automationId}
|
||||||
|
options={filteredAutomations}
|
||||||
|
getOptionValue={automation => automation._id}
|
||||||
|
getOptionLabel={automation => automation.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if Object.keys(automationFields)}
|
||||||
|
{#each Object.keys(automationFields) as field}
|
||||||
|
<div class="schema-field">
|
||||||
|
<Label>{field}</Label>
|
||||||
|
<div class="field-width">
|
||||||
|
<DrawerBindableInput
|
||||||
|
panel={AutomationBindingPanel}
|
||||||
|
extraThin
|
||||||
|
value={value[field]}
|
||||||
|
on:change={e => onChange(e, field)}
|
||||||
|
type="string"
|
||||||
|
{bindings}
|
||||||
|
fillWidth={true}
|
||||||
|
updateOnChange={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.field-width {
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-field {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schema-field :global(label) {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -41,7 +41,7 @@
|
||||||
{ label: "False", value: "false" },
|
{ label: "False", value: "false" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{:else if schema.type === "array"}
|
{:else if schemaHasOptions(schema) && schema.type === "array"}
|
||||||
<Multiselect
|
<Multiselect
|
||||||
bind:value={value[field]}
|
bind:value={value[field]}
|
||||||
options={schema.constraints.inclusion}
|
options={schema.constraints.inclusion}
|
||||||
|
@ -69,7 +69,15 @@
|
||||||
on:change={e => onChange(e, field)}
|
on:change={e => onChange(e, field)}
|
||||||
useLabel={false}
|
useLabel={false}
|
||||||
/>
|
/>
|
||||||
{:else if schema.type === "string" || schema.type === "number"}
|
{:else if schema.type === "bb_reference"}
|
||||||
|
<LinkedRowSelector
|
||||||
|
linkedRows={value[field]}
|
||||||
|
{schema}
|
||||||
|
linkedTableId={"ta_users"}
|
||||||
|
on:change={e => onChange(e, field)}
|
||||||
|
useLabel={false}
|
||||||
|
/>
|
||||||
|
{:else if ["string", "number", "bigint", "barcodeqr", "array"].includes(schema.type)}
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
<script>
|
<script>
|
||||||
import { ActionButton, notifications } from "@budibase/bbui"
|
import { ActionButton, notifications } from "@budibase/bbui"
|
||||||
import CreateEditRelationshipModal from "../../Datasources/CreateEditRelationshipModal.svelte"
|
import CreateEditRelationshipModal from "../../Datasources/CreateEditRelationshipModal.svelte"
|
||||||
import { datasources } from "../../../../stores/backend"
|
import {
|
||||||
|
datasources,
|
||||||
|
tables as tablesStore,
|
||||||
|
} from "../../../../stores/backend"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let table
|
export let table
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: datasource = findDatasource(table?._id)
|
$: datasource = findDatasource(table?._id)
|
||||||
$: tables = datasource?.plus ? Object.values(datasource?.entities || {}) : []
|
$: tables = datasource?.plus
|
||||||
|
? $tablesStore.list.filter(tbl => tbl.sourceId === datasource._id)
|
||||||
|
: []
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
|
@ -28,7 +33,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const onError = err => {
|
const onError = err => {
|
||||||
notifications.error(`Error saving relationship info: ${err}`)
|
if (err.err) {
|
||||||
|
err = err.err
|
||||||
|
}
|
||||||
|
notifications.error(
|
||||||
|
`Error saving relationship info: ${err?.message || JSON.stringify(err)}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,7 @@
|
||||||
let relationshipTableIdSecondary = null
|
let relationshipTableIdSecondary = null
|
||||||
|
|
||||||
let table = $tables.selected
|
let table = $tables.selected
|
||||||
|
|
||||||
let confirmDeleteDialog
|
let confirmDeleteDialog
|
||||||
let savingColumn
|
let savingColumn
|
||||||
let deleteColName
|
let deleteColName
|
||||||
|
@ -171,22 +172,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!savingColumn && !originalName) {
|
|
||||||
let highestNumber = 0
|
|
||||||
Object.keys(table.schema).forEach(columnName => {
|
|
||||||
const columnNumber = extractColumnNumber(columnName)
|
|
||||||
if (columnNumber > highestNumber) {
|
|
||||||
highestNumber = columnNumber
|
|
||||||
}
|
|
||||||
return highestNumber
|
|
||||||
})
|
|
||||||
|
|
||||||
if (highestNumber >= 1) {
|
|
||||||
editableColumn.name = `Column 0${highestNumber + 1}`
|
|
||||||
} else {
|
|
||||||
editableColumn.name = "Column 01"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!savingColumn) {
|
if (!savingColumn) {
|
||||||
editableColumn.fieldId = makeFieldId(
|
editableColumn.fieldId = makeFieldId(
|
||||||
|
@ -388,11 +373,6 @@
|
||||||
deleteColName = ""
|
deleteColName = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractColumnNumber(columnName) {
|
|
||||||
const match = columnName.match(/Column (\d+)/)
|
|
||||||
return match ? parseInt(match[1]) : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAllowedTypes() {
|
function getAllowedTypes() {
|
||||||
if (
|
if (
|
||||||
originalName &&
|
originalName &&
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue