diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index b644a6bdb9..3e4b2221d2 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -18,6 +18,8 @@ env: BRANCH: ${{ github.event.pull_request.head.ref }} BASE_BRANCH: ${{ github.event.pull_request.base.ref}} PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + NX_BASE_BRANCH: origin/${{ github.base_ref }} + USE_NX_AFFECTED: ${{ github.event_name == 'pull_request' && github.base_ref != 'master'}} jobs: lint: @@ -25,20 +27,20 @@ jobs: steps: - name: Checkout repo and submodules uses: actions/checkout@v3 - if: github.repository == github.event.pull_request.head.repo.full_name + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase' with: submodules: true token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} - name: Checkout repo only uses: actions/checkout@v3 - if: github.repository != github.event.pull_request.head.repo.full_name + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase' - - name: Use Node.js 14.x + - name: Use Node.js 18.x uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.x cache: "yarn" - - run: yarn + - run: yarn --frozen-lockfile - run: yarn lint build: @@ -46,45 +48,66 @@ jobs: steps: - name: Checkout repo and submodules uses: actions/checkout@v3 - if: github.repository == github.event.pull_request.head.repo.full_name + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase' with: submodules: true token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} + fetch-depth: 0 - name: Checkout repo only uses: actions/checkout@v3 - if: github.repository != github.event.pull_request.head.repo.full_name + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase' + with: + fetch-depth: 0 - - name: Use Node.js 14.x + - name: Use Node.js 18.x uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.x cache: "yarn" - - run: yarn + - run: yarn --frozen-lockfile + # Run build all the projects - - run: yarn build + - name: Build + run: | + yarn build # Check the types of the projects built via esbuild - - run: yarn check:types + - name: Check types + run: | + if ${{ env.USE_NX_AFFECTED }}; then + yarn check:types --since=${{ env.NX_BASE_BRANCH }} + else + yarn check:types + fi test-libraries: runs-on: ubuntu-latest steps: - name: Checkout repo and submodules uses: actions/checkout@v3 - if: github.repository == github.event.pull_request.head.repo.full_name + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase' with: submodules: true token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} + fetch-depth: 0 - name: Checkout repo only uses: actions/checkout@v3 - if: github.repository != github.event.pull_request.head.repo.full_name + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase' + with: + fetch-depth: 0 - - name: Use Node.js 14.x + - name: Use Node.js 18.x uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.x cache: "yarn" - - run: yarn - - run: yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro + - run: yarn --frozen-lockfile + - name: Test + run: | + if ${{ env.USE_NX_AFFECTED }}; then + yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro --since=${{ env.NX_BASE_BRANCH }} + else + yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro + fi - uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos @@ -96,21 +119,31 @@ jobs: steps: - name: Checkout repo and submodules uses: actions/checkout@v3 - if: github.repository == github.event.pull_request.head.repo.full_name + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase' with: submodules: true token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} + fetch-depth: 0 - name: Checkout repo only uses: actions/checkout@v3 - if: github.repository != github.event.pull_request.head.repo.full_name + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase' + with: + fetch-depth: 0 - - name: Use Node.js 14.x + - name: Use Node.js 18.x uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.x cache: "yarn" - - run: yarn - - run: yarn test --scope=@budibase/worker --scope=@budibase/server + - run: yarn --frozen-lockfile + - name: Test worker and server + run: | + if ${{ env.USE_NX_AFFECTED }}; then + yarn test --scope=@budibase/worker --scope=@budibase/server --since=${{ env.NX_BASE_BRANCH }} + else + yarn test --scope=@budibase/worker --scope=@budibase/server + fi + - uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos @@ -119,60 +152,67 @@ jobs: test-pro: runs-on: ubuntu-latest - if: github.repository == github.event.pull_request.head.repo.full_name + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase' steps: - name: Checkout repo and submodules uses: actions/checkout@v3 with: submodules: true token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} + fetch-depth: 0 - - name: Use Node.js 14.x + - name: Use Node.js 18.x uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.x cache: "yarn" - - run: yarn - - run: yarn test --scope=@budibase/pro + - run: yarn --frozen-lockfile + - name: Test + run: | + if ${{ env.USE_NX_AFFECTED }}; then + yarn test --scope=@budibase/pro --since=${{ env.NX_BASE_BRANCH }} + else + yarn test --scope=@budibase/pro + fi integration-test: runs-on: ubuntu-latest steps: - name: Checkout repo and submodules uses: actions/checkout@v3 - if: github.repository == github.event.pull_request.head.repo.full_name + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase' with: submodules: true token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} - name: Checkout repo only uses: actions/checkout@v3 - if: github.repository != github.event.pull_request.head.repo.full_name + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase' - - name: Use Node.js 14.x + - name: Use Node.js 18.x uses: actions/setup-node@v3 with: - node-version: 14.x + node-version: 18.x cache: "yarn" - - run: yarn - - run: yarn build + - run: yarn --frozen-lockfile + - name: Build packages + run: yarn build --scope @budibase/server --scope @budibase/worker --scope @budibase/client --scope @budibase/backend-core - name: Run tests run: | cd qa-core yarn setup - yarn test:ci + yarn serve:test:self:ci env: BB_ADMIN_USER_EMAIL: admin BB_ADMIN_USER_PASSWORD: admin check-pro-submodule: runs-on: ubuntu-latest - if: github.repository == github.event.pull_request.head.repo.full_name + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase' steps: - name: Checkout repo and submodules uses: actions/checkout@v3 with: submodules: true - fetch-depth: 0 token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} - name: Check pro commit @@ -190,6 +230,8 @@ jobs: base_commit=$(git rev-parse origin/develop) fi + echo "target_branch=$branch" + echo "target_branch=$branch" >> "$GITHUB_OUTPUT" echo "pro_commit=$pro_commit" echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT" echo "base_commit=$base_commit" @@ -204,7 +246,7 @@ jobs: const baseCommit = '${{ steps.get_pro_commits.outputs.base_commit }}'; if (submoduleCommit !== baseCommit) { - console.error('Submodule commit does not match the latest commit on the develop 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') process.exit(1); } else { diff --git a/.github/workflows/check_unreleased_changes.yml b/.github/workflows/check_unreleased_changes.yml new file mode 100644 index 0000000000..d558330545 --- /dev/null +++ b/.github/workflows/check_unreleased_changes.yml @@ -0,0 +1,29 @@ +name: check_unreleased_changes + +on: + pull_request: + branches: + - master + +jobs: + check_unreleased: + runs-on: ubuntu-latest + steps: + - name: Check for unreleased changes + env: + REPO: "Budibase/budibase" + TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + RELEASE_TIMESTAMP=$(curl -s -H "Authorization: token $TOKEN" \ + "https://api.github.com/repos/$REPO/releases/latest" | \ + jq -r .published_at) + COMMIT_TIMESTAMP=$(curl -s -H "Authorization: token $TOKEN" \ + "https://api.github.com/repos/$REPO/commits/master" | \ + jq -r .commit.committer.date) + RELEASE_SECONDS=$(date --date="$RELEASE_TIMESTAMP" "+%s") + COMMIT_SECONDS=$(date --date="$COMMIT_TIMESTAMP" "+%s") + if (( COMMIT_SECONDS > RELEASE_SECONDS )); then + echo "There are unreleased changes. Please release these changes before merging." + exit 1 + fi + echo "No unreleased changes detected." diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index a7bf041eb5..bd727b7865 100644 --- a/.github/workflows/release-develop.yml +++ b/.github/workflows/release-develop.yml @@ -6,7 +6,7 @@ concurrency: on: push: tags: - - v*-alpha.* + - "*-alpha.*" workflow_dispatch: env: @@ -44,7 +44,7 @@ jobs: - uses: actions/setup-node@v1 with: - node-version: 14.x + node-version: 18.x - run: yarn install --frozen-lockfile - name: Update versions diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml index c0e012bb27..1dae4ae578 100644 --- a/.github/workflows/release-master.yml +++ b/.github/workflows/release-master.yml @@ -6,9 +6,9 @@ concurrency: on: push: tags: - - "v[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+" # Exclude all pre-releases - - "!v*[0-9]+.[0-9]+.[0-9]+-*" + - "!*[0-9]+.[0-9]+.[0-9]+-*" env: # Posthog token used by ui at build time @@ -60,9 +60,9 @@ jobs: - name: "Get Current tag" id: currenttag run: | - version=v$(./scripts/getCurrentVersion.sh) - echo 'Using tag $version' - echo "::set-output name=tag::$resversionult" + version=$(./scripts/getCurrentVersion.sh) + echo "Using tag $version" + echo "version=$version" >> "$GITHUB_OUTPUT" - name: Build/release Docker images run: | @@ -71,7 +71,7 @@ jobs: env: DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} - BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.tag }} + BUDIBASE_RELEASE_VERSION: ${{ steps.currenttag.outputs.version }} release-helm-chart: needs: [release-images] diff --git a/.github/workflows/release-singleimage.yml b/.github/workflows/release-singleimage.yml index 92f21bd649..b879b519d2 100644 --- a/.github/workflows/release-singleimage.yml +++ b/.github/workflows/release-singleimage.yml @@ -15,6 +15,13 @@ jobs: matrix: node-version: [14.x] steps: + - name: Maximize build space + uses: easimon/maximize-build-space@master + with: + root-reserve-mb: 35000 + swap-size-mb: 1024 + remove-android: 'true' + remove-dotnet: 'true' - name: Fail if not a tag run: | if [[ $GITHUB_REF != refs/tags/* ]]; then diff --git a/.gitignore b/.gitignore index b3dc8af0d4..22a7313e66 100644 --- a/.gitignore +++ b/.gitignore @@ -101,8 +101,6 @@ packages/builder/cypress.env.json packages/builder/cypress/reports stats.html -# TypeScript cache -*.tsbuildinfo # plugins budibase-component diff --git a/.nvmrc b/.nvmrc index 835d07c442..7950a44576 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v14.20.1 +v18.17.0 diff --git a/.tool-versions b/.tool-versions index da92e03885..a909d60941 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,3 @@ -nodejs 14.21.3 -python 3.10.0 \ No newline at end of file +nodejs 18.17.0 +python 3.10.0 +yarn 1.22.19 diff --git a/.vscode/launch.json b/.vscode/launch.json index 8cb49d5825..cfd8d7b155 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,42 +1,31 @@ + { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Budibase Server", - "type": "node", - "request": "launch", - "runtimeArgs": [ - "--nolazy", - "-r", - "ts-node/register/transpile-only" - ], - "args": [ - "${workspaceFolder}/packages/server/src/index.ts" - ], - "cwd": "${workspaceFolder}/packages/server" - }, - { - "name": "Budibase Worker", - "type": "node", - "request": "launch", - "runtimeArgs": [ - "--nolazy", - "-r", - "ts-node/register/transpile-only" - ], - "args": [ - "${workspaceFolder}/packages/worker/src/index.ts" - ], - "cwd": "${workspaceFolder}/packages/worker" - }, - ], - "compounds": [ - { - "name": "Start Budibase", - "configurations": ["Budibase Server", "Budibase Worker"] - } - ] -} \ No newline at end of file + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Budibase Server", + "type": "node", + "request": "launch", + "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"], + "args": ["${workspaceFolder}/packages/server/src/index.ts"], + "cwd": "${workspaceFolder}/packages/server" + }, + { + "name": "Budibase Worker", + "type": "node", + "request": "launch", + "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"], + "args": ["${workspaceFolder}/packages/worker/src/index.ts"], + "cwd": "${workspaceFolder}/packages/worker" + } + ], + "compounds": [ + { + "name": "Start Budibase", + "configurations": ["Budibase Server", "Budibase Worker"] + } + ] +} diff --git a/charts/budibase/templates/app-service-deployment.yaml b/charts/budibase/templates/app-service-deployment.yaml index 2b2589406a..3243509094 100644 --- a/charts/budibase/templates/app-service-deployment.yaml +++ b/charts/budibase/templates/app-service-deployment.yaml @@ -120,6 +120,8 @@ spec: {{ end }} - name: MULTI_TENANCY value: {{ .Values.globals.multiTenancy | quote }} + - name: OFFLINE_MODE + value: {{ .Values.globals.offlineMode | quote }} - name: LOG_LEVEL value: {{ .Values.services.apps.logLevel | quote }} - name: REDIS_PASSWORD @@ -201,25 +203,24 @@ spec: image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }} imagePullPolicy: Always + {{- if .Values.services.apps.startupProbe }} + {{- with .Values.services.apps.startupProbe }} + startupProbe: + {{- toYaml . | nindent 10 }} + {{- end }} + {{- end }} + {{- if .Values.services.apps.livenessProbe }} + {{- with .Values.services.apps.livenessProbe }} livenessProbe: - httpGet: - path: /health - port: {{ .Values.services.apps.port }} - initialDelaySeconds: 10 - periodSeconds: 5 - successThreshold: 1 - failureThreshold: 3 - timeoutSeconds: 3 + {{- toYaml . | nindent 10 }} + {{- end }} + {{- end }} + {{- if .Values.services.apps.readinessProbe }} + {{- with .Values.services.apps.readinessProbe }} readinessProbe: - httpGet: - path: /health - port: {{ .Values.services.apps.port }} - initialDelaySeconds: 5 - periodSeconds: 5 - successThreshold: 1 - failureThreshold: 3 - timeoutSeconds: 3 - + {{- toYaml . | nindent 10 }} + {{- end }} + {{- end }} name: bbapps ports: - containerPort: {{ .Values.services.apps.port }} diff --git a/charts/budibase/templates/proxy-service-deployment.yaml b/charts/budibase/templates/proxy-service-deployment.yaml index e4825ea5d0..53bba6232d 100644 --- a/charts/budibase/templates/proxy-service-deployment.yaml +++ b/charts/budibase/templates/proxy-service-deployment.yaml @@ -40,6 +40,24 @@ spec: - image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }} imagePullPolicy: Always name: proxy-service + {{- if .Values.services.proxy.startupProbe }} + {{- with .Values.services.proxy.startupProbe }} + startupProbe: + {{- toYaml . | nindent 10 }} + {{- end }} + {{- end }} + {{- if .Values.services.proxy.livenessProbe }} + {{- with .Values.services.proxy.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 10 }} + {{- end }} + {{- end }} + {{- if .Values.services.proxy.readinessProbe }} + {{- with .Values.services.proxy.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 10 }} + {{- end }} + {{- end }} ports: - containerPort: {{ .Values.services.proxy.port }} env: diff --git a/charts/budibase/templates/worker-service-deployment.yaml b/charts/budibase/templates/worker-service-deployment.yaml index 5fed80b355..7621aa23ef 100644 --- a/charts/budibase/templates/worker-service-deployment.yaml +++ b/charts/budibase/templates/worker-service-deployment.yaml @@ -116,6 +116,8 @@ spec: value: {{ .Values.services.worker.port | quote }} - name: MULTI_TENANCY value: {{ .Values.globals.multiTenancy | quote }} + - name: OFFLINE_MODE + value: {{ .Values.globals.offlineMode | quote }} - name: LOG_LEVEL value: {{ .Values.services.worker.logLevel | quote }} - name: REDIS_PASSWORD @@ -190,24 +192,24 @@ spec: {{ end }} image: budibase/worker:{{ .Values.globals.appVersion | default .Chart.AppVersion }} imagePullPolicy: Always + {{- if .Values.services.worker.startupProbe }} + {{- with .Values.services.worker.startupProbe }} + startupProbe: + {{- toYaml . | nindent 10 }} + {{- end }} + {{- end }} + {{- if .Values.services.worker.livenessProbe }} + {{- with .Values.services.worker.livenessProbe }} livenessProbe: - httpGet: - path: /health - port: {{ .Values.services.worker.port }} - initialDelaySeconds: 10 - periodSeconds: 5 - successThreshold: 1 - failureThreshold: 3 - timeoutSeconds: 3 + {{- toYaml . | nindent 10 }} + {{- end }} + {{- end }} + {{- if .Values.services.worker.readinessProbe }} + {{- with .Values.services.worker.readinessProbe }} readinessProbe: - httpGet: - path: /health - port: {{ .Values.services.worker.port }} - initialDelaySeconds: 5 - periodSeconds: 5 - successThreshold: 1 - failureThreshold: 3 - timeoutSeconds: 3 + {{- toYaml . | nindent 10 }} + {{- end }} + {{- end }} name: bbworker ports: - containerPort: {{ .Values.services.worker.port }} diff --git a/charts/budibase/values.yaml b/charts/budibase/values.yaml index 74e4c52654..e5f1eabb53 100644 --- a/charts/budibase/values.yaml +++ b/charts/budibase/values.yaml @@ -82,6 +82,7 @@ globals: posthogToken: "phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU" selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs + offlineMode: "0" # set to 1 to enable offline mode accountPortalUrl: "" accountPortalApiKey: "" cookieDomain: "" @@ -119,15 +120,36 @@ services: port: 10000 replicaCount: 1 upstreams: - apps: 'http://app-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.apps.port }}' - worker: 'http://worker-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.worker.port }}' - minio: 'http://minio-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.objectStore.port }}' - couchdb: 'http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }}' + apps: "http://app-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.apps.port }}" + worker: "http://worker-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.worker.port }}" + minio: "http://minio-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.objectStore.port }}" + couchdb: "http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }}" resources: {} -# annotations: -# co.elastic.logs/module: nginx -# co.elastic.logs/fileset.stdout: access -# co.elastic.logs/fileset.stderr: error + startupProbe: + httpGet: + path: /health + port: 10000 + scheme: HTTP + failureThreshold: 30 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /health + port: 10000 + scheme: HTTP + periodSeconds: 3 + failureThreshold: 1 + livenessProbe: + httpGet: + path: /health + port: 10000 + scheme: HTTP + failureThreshold: 3 + periodSeconds: 5 + # annotations: + # co.elastic.logs/module: nginx + # co.elastic.logs/fileset.stdout: access + # co.elastic.logs/fileset.stderr: error apps: port: 4002 @@ -135,23 +157,65 @@ services: logLevel: info httpLogging: 1 resources: {} -# nodeDebug: "" # set the value of NODE_DEBUG -# annotations: -# co.elastic.logs/multiline.type: pattern -# co.elastic.logs/multiline.pattern: '^[[:space:]]' -# co.elastic.logs/multiline.negate: false -# co.elastic.logs/multiline.match: after + startupProbe: + httpGet: + path: /health + port: 4002 + scheme: HTTP + failureThreshold: 30 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /health + port: 4002 + scheme: HTTP + periodSeconds: 3 + failureThreshold: 1 + livenessProbe: + httpGet: + path: /health + port: 4002 + scheme: HTTP + failureThreshold: 3 + periodSeconds: 5 + # nodeDebug: "" # set the value of NODE_DEBUG + # annotations: + # co.elastic.logs/multiline.type: pattern + # co.elastic.logs/multiline.pattern: '^[[:space:]]' + # co.elastic.logs/multiline.negate: false + # co.elastic.logs/multiline.match: after worker: port: 4003 replicaCount: 1 logLevel: info httpLogging: 1 resources: {} -# annotations: -# co.elastic.logs/multiline.type: pattern -# co.elastic.logs/multiline.pattern: '^[[:space:]]' -# co.elastic.logs/multiline.negate: false -# co.elastic.logs/multiline.match: after + startupProbe: + httpGet: + path: /health + port: 4003 + scheme: HTTP + failureThreshold: 30 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /health + port: 4003 + scheme: HTTP + periodSeconds: 3 + failureThreshold: 1 + livenessProbe: + httpGet: + path: /health + port: 4003 + scheme: HTTP + failureThreshold: 3 + periodSeconds: 5 + # annotations: + # co.elastic.logs/multiline.type: pattern + # co.elastic.logs/multiline.pattern: '^[[:space:]]' + # co.elastic.logs/multiline.negate: false + # co.elastic.logs/multiline.match: after couchdb: enabled: true @@ -344,14 +408,12 @@ couchdb: ## Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes # FOR COUCHDB livenessProbe: - enabled: true failureThreshold: 3 initialDelaySeconds: 0 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 1 readinessProbe: - enabled: true failureThreshold: 3 initialDelaySeconds: 0 periodSeconds: 10 diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index ac35929be1..70f198a84c 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -90,7 +90,7 @@ Component libraries are collections of components as well as the definition of t #### 1. Prerequisites -- NodeJS version `14.x.x` +- NodeJS version `18.x.x` - Python version `3.x` ### Using asdf (recommended) @@ -231,18 +231,33 @@ An overview of the CI pipelines can be found [here](../.github/workflows/README. ### Pro -@budibase/pro is the closed source package that supports licensed features in budibase. By default the package will be pulled from NPM and will not normally need to be touched in local development. If you require to update code inside the pro package it can be cloned to the same root level as budibase, e.g. +@budibase/pro is the closed source package that supports licensed features in budibase. By default the package will be pulled from NPM and will not normally need to be touched in local development. If you need to make an update to pro and have access to the repo, then you can update your submodule within the mono-repo by running `git submodule update --init` - from here you can use normal submodule flow to develop a change within pro. + +Once you have updated to use the pro submodule, it will be linked into all of your local dependencies by NX as with all other monorepo packages. If you have been using the NPM version of `@budibase/pro` then you may need to run a `git reset --hard` to fix all of the pro versions back to `0.0.0` to be monorepo aware. + +From here - to develop a change in pro, you can follow the below flow: ``` -. -|_ budibase -|_ budibase-pro +# enter the pro submodule +cd packages/pro +# get the base branch you are working from (same as monorepo) +git fetch +git checkout +# create a branch, named the same as the branch in your monorepo +git checkout -b +... make changes +# commit the changes you've made, with a message for pro +git commit +# within the monorepo, add the pro reference to your branch, commit it with a message like "Update pro ref" +cd ../.. +git add packages/pro +git commit ``` +From here, you will have created a branch in the pro repository and commited the reference to your branch on the monorepo. When you eventually PR this work back into the mainline branch, you will need to first merge your pro PR to the pro mainline, then go into your PR in the monorepo and update the reference again to the new mainline. + Note that only budibase maintainers will be able to access the pro repo. -By default, NX will make sure that dependencies are replaced with local source aware version. This is achieved using the `yarn link` command. To see specifically how dependencies are linked see [scripts/link-dependencies.sh](../scripts/link-dependencies.sh). The same link script is used to link dependencies to account-portal in local dev. - ### Troubleshooting Sometimes, things go wrong. This can be due to incompatible updates on the budibase platform. To clear down your development environment and start again follow **Step 6. Cleanup**, then proceed from **Step 3. Install and Build** in the setup guide above to create a fresh Budibase installation. diff --git a/hosting/couchdb/Dockerfile b/hosting/couchdb/Dockerfile index 70b4413859..ce77002052 100644 --- a/hosting/couchdb/Dockerfile +++ b/hosting/couchdb/Dockerfile @@ -5,11 +5,11 @@ ENV COUCHDB_PASSWORD admin EXPOSE 5984 RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common wget unzip curl && \ - wget -qO - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add - && \ + wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo apt-key add - && \ apt-add-repository 'deb http://security.debian.org/debian-security bullseye-security/updates main' && \ apt-add-repository 'deb http://archive.debian.org/debian stretch-backports main' && \ - apt-add-repository --yes https://adoptopenjdk.jfrog.io/adoptopenjdk/deb/ && \ - apt-get update && apt-get install -y --no-install-recommends adoptopenjdk-8-hotspot && \ + apt-add-repository 'deb https://packages.adoptium.net/artifactory/deb bullseye main' && \ + apt-get update && apt-get install -y --no-install-recommends temurin-8-jdk && \ rm -rf /var/lib/apt/lists/ # setup clouseau diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml index bad34a20ea..b3887c15fa 100644 --- a/hosting/docker-compose.yaml +++ b/hosting/docker-compose.yaml @@ -27,6 +27,7 @@ services: BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL} BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD} PLUGINS_DIR: ${PLUGINS_DIR} + OFFLINE_MODE: ${OFFLINE_MODE} depends_on: - worker-service - redis-service @@ -54,6 +55,7 @@ services: INTERNAL_API_KEY: ${INTERNAL_API_KEY} REDIS_URL: redis-service:6379 REDIS_PASSWORD: ${REDIS_PASSWORD} + OFFLINE_MODE: ${OFFLINE_MODE} depends_on: - redis-service - minio-service diff --git a/hosting/single/Dockerfile b/hosting/single/Dockerfile index e43e5ad10c..9fdf2449d1 100644 --- a/hosting/single/Dockerfile +++ b/hosting/single/Dockerfile @@ -1,7 +1,7 @@ -FROM node:14-slim as build +FROM node:18-slim as build # install node-gyp dependencies -RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python +RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python3 # add pin script WORKDIR / diff --git a/jestTestcontainersConfigGenerator.js b/jestTestcontainersConfigGenerator.js index 4b94cf5016..4b8afe327d 100644 --- a/jestTestcontainersConfigGenerator.js +++ b/jestTestcontainersConfigGenerator.js @@ -1,9 +1,26 @@ module.exports = () => { return { - dockerCompose: { - composeFilePath: "../../hosting", - composeFile: "docker-compose.test.yaml", - startupTimeout: 10000, - }, + couchdb: { + image: "budibase/couchdb", + ports: [5984], + env: { + COUCHDB_PASSWORD: "budibase", + COUCHDB_USER: "budibase", + }, + wait: { + type: "ports", + timeout: 10000, + } + } } } + +// module.exports = () => { +// return { +// dockerCompose: { +// composeFilePath: "../../hosting", +// composeFile: "docker-compose.test.yaml", +// startupTimeout: 10000, +// }, +// } +// } diff --git a/lerna.json b/lerna.json index 498d8a91b5..a5b5a4a968 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.8.16-alpha.0", + "version": "2.9.30-alpha.9", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/nx.json b/nx.json index 3df61886c2..8176bae82c 100644 --- a/nx.json +++ b/nx.json @@ -3,24 +3,10 @@ "default": { "runner": "nx-cloud", "options": { - "cacheableOperations": [ - "build", - "test" - ], - "accessToken": "YWNiYzc5NTEtMzMzZC00NDhjLTgyNjktZTllMjI1MzM4OGQxfHJlYWQtd3JpdGU=" + "cacheableOperations": ["build", "test", "check:types"], + "accessToken": "MmM4OGYxNzItMDBlYy00ZmE3LTk4MTYtNmJhYWMyZjBjZTUyfHJlYWQ=" } } }, - "targetDefaults": { - "dev:builder": { - "dependsOn": [ - { - "projects": [ - "@budibase/string-templates" - ], - "target": "build" - } - ] - } - } + "targetDefaults": {} } diff --git a/package.json b/package.json index 3afe279e00..4e4befb5f2 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "@nx/js": "16.4.3", "@rollup/plugin-json": "^4.0.2", "@typescript-eslint/parser": "5.45.0", - "esbuild": "^0.17.18", - "esbuild-node-externals": "^1.7.0", + "esbuild": "^0.18.17", + "esbuild-node-externals": "^1.8.0", "eslint": "^8.44.0", "eslint-plugin-cypress": "^2.11.3", "husky": "^8.0.3", @@ -34,9 +34,9 @@ "preinstall": "node scripts/syncProPackage.js", "setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev", "bootstrap": "./scripts/link-dependencies.sh && echo '***BOOTSTRAP ONLY REQUIRED FOR USE WITH ACCOUNT PORTAL***'", - "build": "yarn nx run-many -t=build", + "build": "lerna run build --stream", "build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput", - "check:types": "lerna run check:types --skip-nx-cache", + "check:types": "lerna run check:types", "backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap", "backend:build": "./scripts/scopeBackend.sh 'lerna run --stream build'", "build:sdk": "lerna run --stream build:sdk", @@ -51,7 +51,7 @@ "kill-builder": "kill-port 3000", "kill-server": "kill-port 4001 4002", "kill-all": "yarn run kill-builder && yarn run kill-server", - "dev": "yarn run kill-all && lerna run --stream dev:builder --stream", + "dev": "yarn run kill-all && lerna run --stream dev:builder", "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:server": "yarn run kill-server && lerna run --stream dev:builder --scope @budibase/worker --scope @budibase/server", "dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built", @@ -108,5 +108,8 @@ "@budibase/string-templates": "0.0.0", "@budibase/types": "0.0.0" }, + "engines": { + "node": ">=18.0.0 <19.0.0" + }, "dependencies": {} } diff --git a/packages/backend-core/.npmignore b/packages/backend-core/.npmignore new file mode 100644 index 0000000000..fb547825eb --- /dev/null +++ b/packages/backend-core/.npmignore @@ -0,0 +1,4 @@ +* +!dist/**/* +dist/tsconfig.build.tsbuildinfo +!package.json \ No newline at end of file diff --git a/packages/backend-core/jest.config.ts b/packages/backend-core/jest.config.ts index 8d64b24a2f..3f1065ead2 100644 --- a/packages/backend-core/jest.config.ts +++ b/packages/backend-core/jest.config.ts @@ -1,8 +1,6 @@ import { Config } from "@jest/types" -const preset = require("ts-jest/jest-preset") const baseConfig: Config.InitialProjectOptions = { - ...preset, preset: "@trendyol/jest-testcontainers", setupFiles: ["./tests/jestEnv.ts"], setupFilesAfterEnv: ["./tests/jestSetup.ts"], @@ -11,6 +9,7 @@ const baseConfig: Config.InitialProjectOptions = { }, moduleNameMapper: { "@budibase/types": "/../types/src", + "@budibase/shared-core": ["/../shared-core/src"], }, } diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 7f3c064c92..1d6aa5b9fd 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -2,11 +2,11 @@ "name": "@budibase/backend-core", "version": "0.0.0", "description": "Budibase backend core libraries used in server and worker", - "main": "dist/src/index.js", + "main": "dist/index.js", "types": "dist/src/index.d.ts", "exports": { - ".": "./dist/src/index.js", - "./tests": "./dist/tests/index.js", + ".": "./dist/index.js", + "./tests": "./dist/tests.js", "./*": "./dist/*.js" }, "author": "Budibase", @@ -14,16 +14,17 @@ "scripts": { "prebuild": "rimraf dist/", "prepack": "cp package.json dist", - "build": "tsc -p tsconfig.build.json", + "build": "node ./scripts/build.js && tsc -p tsconfig.build.json --emitDeclarationOnly --paths null", "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", + "check:types": "tsc -p tsconfig.json --noEmit --paths null", "test": "bash scripts/test.sh", "test:watch": "jest --watchAll" }, "dependencies": { "@budibase/nano": "10.1.2", "@budibase/pouchdb-replication-stream": "1.2.10", + "@budibase/shared-core": "0.0.0", "@budibase/types": "0.0.0", - "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", "aws-sdk": "2.1030.0", @@ -58,12 +59,13 @@ "uuid": "8.3.2" }, "devDependencies": { - "@jest/test-sequencer": "29.5.0", - "@swc/core": "^1.3.25", - "@swc/jest": "^0.2.24", + "@jest/test-sequencer": "29.6.2", + "@shopify/jest-koa-mocks": "5.1.1", + "@swc/core": "1.3.71", + "@swc/jest": "0.2.27", "@trendyol/jest-testcontainers": "^2.1.1", "@types/chance": "1.1.3", - "@types/jest": "29.5.0", + "@types/jest": "29.5.3", "@types/koa": "2.13.4", "@types/lodash": "4.14.180", "@types/node": "14.18.20", @@ -75,15 +77,14 @@ "@types/uuid": "8.3.4", "chance": "1.1.8", "ioredis-mock": "8.7.0", - "jest": "29.5.0", - "jest-environment-node": "29.5.0", - "jest-serial-runner": "^1.2.1", + "jest": "29.6.2", + "jest-environment-node": "29.6.2", + "jest-serial-runner": "1.2.1", "koa": "2.13.4", "nodemon": "2.0.16", "pino-pretty": "10.0.0", "pouchdb-adapter-memory": "7.2.2", "timekeeper": "2.2.0", - "ts-jest": "29.0.5", "ts-node": "10.8.1", "tsconfig-paths": "4.0.0", "typescript": "4.7.3" @@ -94,6 +95,7 @@ "dependsOn": [ { "projects": [ + "@budibase/shared-core", "@budibase/types" ], "target": "build" @@ -101,6 +103,5 @@ ] } } - }, - "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" + } } diff --git a/packages/backend-core/plugins.ts b/packages/backend-core/plugins.ts deleted file mode 100644 index 33354eaf64..0000000000 --- a/packages/backend-core/plugins.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./src/plugin" diff --git a/packages/backend-core/scripts/build.js b/packages/backend-core/scripts/build.js new file mode 100644 index 0000000000..bd00cbc7ff --- /dev/null +++ b/packages/backend-core/scripts/build.js @@ -0,0 +1,6 @@ +#!/usr/bin/node +const coreBuild = require("../../../scripts/build") + +coreBuild("./src/plugin/index.ts", "./dist/plugins.js") +coreBuild("./src/index.ts", "./dist/index.js") +coreBuild("./tests/index.ts", "./dist/tests.js") diff --git a/packages/backend-core/scripts/test.sh b/packages/backend-core/scripts/test.sh index 3d8240e65a..7d19ec96cc 100644 --- a/packages/backend-core/scripts/test.sh +++ b/packages/backend-core/scripts/test.sh @@ -8,6 +8,6 @@ then jest --coverage --runInBand --forceExit else # --maxWorkers performs better in development - echo "jest --coverage --forceExit" - jest --coverage --forceExit + echo "jest --coverage --detectOpenHandles" + jest --coverage --detectOpenHandles fi \ No newline at end of file diff --git a/packages/backend-core/src/cache/appMetadata.ts b/packages/backend-core/src/cache/appMetadata.ts index 5b66c356d3..0c320ec776 100644 --- a/packages/backend-core/src/cache/appMetadata.ts +++ b/packages/backend-core/src/cache/appMetadata.ts @@ -2,9 +2,14 @@ import { getAppClient } from "../redis/init" import { doWithDB, DocumentType } from "../db" import { Database, App } from "@budibase/types" -const AppState = { - INVALID: "invalid", +export enum AppState { + INVALID = "invalid", } + +export interface DeletedApp { + state: AppState +} + const EXPIRY_SECONDS = 3600 /** @@ -31,7 +36,7 @@ function isInvalid(metadata?: { state: string }) { * @param {string} appId the id of the app to get metadata from. * @returns {object} the app metadata. */ -export async function getAppMetadata(appId: string) { +export async function getAppMetadata(appId: string): Promise { const client = await getAppClient() // try cache let metadata = await client.get(appId) @@ -61,11 +66,8 @@ export async function getAppMetadata(appId: string) { } await client.store(appId, metadata, expiry) } - // we've stored in the cache an object to tell us that it is currently invalid - if (isInvalid(metadata)) { - throw { status: 404, message: "No app metadata found" } - } - return metadata as App + + return metadata } /** diff --git a/packages/backend-core/src/cache/tests/writethrough.spec.ts b/packages/backend-core/src/cache/tests/writethrough.spec.ts index 92b073ed64..97d3ece7a6 100644 --- a/packages/backend-core/src/cache/tests/writethrough.spec.ts +++ b/packages/backend-core/src/cache/tests/writethrough.spec.ts @@ -36,7 +36,7 @@ describe("writethrough", () => { _id: docId, value: 1, }) - const output = await db.get(response.id) + const output = await db.get(response.id) current = output expect(output.value).toBe(1) }) @@ -45,7 +45,7 @@ describe("writethrough", () => { it("second put shouldn't update DB", async () => { await config.doInTenant(async () => { const response = await writethrough.put({ ...current, value: 2 }) - const output = await db.get(response.id) + const output = await db.get(response.id) expect(current._rev).toBe(output._rev) expect(output.value).toBe(1) }) @@ -55,7 +55,7 @@ describe("writethrough", () => { await config.doInTenant(async () => { tk.freeze(Date.now() + DELAY + 1) const response = await writethrough.put({ ...current, value: 3 }) - const output = await db.get(response.id) + const output = await db.get(response.id) expect(response.rev).not.toBe(current._rev) expect(output.value).toBe(3) @@ -79,7 +79,7 @@ describe("writethrough", () => { expect.arrayContaining([current._rev, current._rev, newRev]) ) - const output = await db.get(current._id) + const output = await db.get(current._id) expect(output.value).toBe(4) expect(output._rev).toBe(newRev) @@ -107,7 +107,7 @@ describe("writethrough", () => { }) expect(res.ok).toBe(true) - const output = await db.get(id) + const output = await db.get(id) expect(output.value).toBe(3) expect(output._rev).toBe(res.rev) }) @@ -130,8 +130,8 @@ describe("writethrough", () => { const resp2 = await writethrough2.put({ _id: "db1", value: "second" }) expect(resp1.rev).toBeDefined() expect(resp2.rev).toBeDefined() - expect((await db.get("db1")).value).toBe("first") - expect((await db2.get("db1")).value).toBe("second") + expect((await db.get("db1")).value).toBe("first") + expect((await db2.get("db1")).value).toBe("second") }) }) }) diff --git a/packages/backend-core/src/constants/db.ts b/packages/backend-core/src/constants/db.ts index be49b9f261..83f8298f54 100644 --- a/packages/backend-core/src/constants/db.ts +++ b/packages/backend-core/src/constants/db.ts @@ -1,5 +1,5 @@ -export const SEPARATOR = "_" -export const UNICODE_MAX = "\ufff0" +import { prefixed, DocumentType } from "@budibase/types" +export { SEPARATOR, UNICODE_MAX, DocumentType } from "@budibase/types" /** * Can be used to create a few different forms of querying a view. @@ -14,8 +14,6 @@ export enum ViewName { USER_BY_APP = "by_app", USER_BY_EMAIL = "by_email2", BY_API_KEY = "by_api_key", - /** @deprecated - could be deleted */ - USER_BY_BUILDERS = "by_builders", LINK = "by_link", ROUTING = "screen_routes", AUTOMATION_LOGS = "automation_logs", @@ -36,42 +34,6 @@ export enum InternalTable { USER_METADATA = "ta_users", } -export enum DocumentType { - USER = "us", - GROUP = "gr", - WORKSPACE = "workspace", - CONFIG = "config", - TEMPLATE = "template", - APP = "app", - DEV = "dev", - APP_DEV = "app_dev", - APP_METADATA = "app_metadata", - ROLE = "role", - MIGRATIONS = "migrations", - DEV_INFO = "devinfo", - AUTOMATION_LOG = "log_au", - ACCOUNT_METADATA = "acc_metadata", - PLUGIN = "plg", - DATASOURCE = "datasource", - DATASOURCE_PLUS = "datasource_plus", - APP_BACKUP = "backup", - TABLE = "ta", - ROW = "ro", - AUTOMATION = "au", - LINK = "li", - WEBHOOK = "wh", - INSTANCE = "inst", - LAYOUT = "layout", - SCREEN = "screen", - QUERY = "query", - DEPLOYMENTS = "deployments", - METADATA = "metadata", - MEM_VIEW = "view", - USER_FLAG = "flag", - AUTOMATION_METADATA = "meta_au", - AUDIT_LOG = "al", -} - export const StaticDatabases = { GLOBAL: { name: "global-db", @@ -95,7 +57,7 @@ export const StaticDatabases = { }, } -export const APP_PREFIX = DocumentType.APP + SEPARATOR -export const APP_DEV = DocumentType.APP_DEV + SEPARATOR +export const APP_PREFIX = prefixed(DocumentType.APP) +export const APP_DEV = prefixed(DocumentType.APP_DEV) export const APP_DEV_PREFIX = APP_DEV export const BUDIBASE_DATASOURCE_TYPE = "budibase" diff --git a/packages/backend-core/src/constants/misc.ts b/packages/backend-core/src/constants/misc.ts index ba2533cf4a..0c68798164 100644 --- a/packages/backend-core/src/constants/misc.ts +++ b/packages/backend-core/src/constants/misc.ts @@ -20,6 +20,8 @@ export enum Header { 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", TOKEN = "x-budibase-token", CSRF_TOKEN = "x-csrf-token", CORRELATION_ID = "x-budibase-correlation-id", diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/db/constants.ts new file mode 100644 index 0000000000..aea485e3e3 --- /dev/null +++ b/packages/backend-core/src/db/constants.ts @@ -0,0 +1,10 @@ +export const CONSTANT_INTERNAL_ROW_COLS = [ + "_id", + "_rev", + "type", + "createdAt", + "updatedAt", + "tableId", +] as const + +export const CONSTANT_EXTERNAL_ROW_COLS = ["_id", "_rev", "tableId"] as const diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index 29ca4123f5..89f76769b3 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -8,6 +8,7 @@ import { DatabasePutOpts, DatabaseCreateIndexOpts, DatabaseDeleteIndexOpts, + DocExistsResponse, Document, isDocument, } from "@budibase/types" @@ -120,6 +121,19 @@ export class DatabaseImpl implements Database { return this.updateOutput(() => db.get(id)) } + async docExists(docId: string): Promise { + const db = await this.checkSetup() + let _rev, exists + try { + const { etag } = await db.head(docId) + _rev = etag + exists = true + } catch (err) { + exists = false + } + return { _rev, exists } + } + async remove(idOrDoc: string | Document, rev?: string) { const db = await this.checkSetup() let _id: string diff --git a/packages/backend-core/src/db/couch/index.ts b/packages/backend-core/src/db/couch/index.ts index c731d20d6c..932efed3f7 100644 --- a/packages/backend-core/src/db/couch/index.ts +++ b/packages/backend-core/src/db/couch/index.ts @@ -2,3 +2,4 @@ export * from "./connections" export * from "./DatabaseImpl" export * from "./utils" export { init, getPouch, getPouchDB, closePouchDB } from "./pouchDB" +export * from "../constants" diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts index a491451a62..7451d581b5 100644 --- a/packages/backend-core/src/db/lucene.ts +++ b/packages/backend-core/src/db/lucene.ts @@ -1,7 +1,6 @@ import fetch from "node-fetch" import { getCouchInfo } from "./couch" -import { SearchFilters, Row } from "@budibase/types" -import { createUserIndex } from "./searchIndexes/searchIndexes" +import { SearchFilters, Row, EmptyFilterOption } from "@budibase/types" const QUERY_START_REGEX = /\d[0-9]*:/g @@ -65,6 +64,7 @@ export class QueryBuilder { this.#index = index this.#query = { allOr: false, + onEmptyFilter: EmptyFilterOption.RETURN_ALL, string: {}, fuzzy: {}, range: {}, @@ -218,6 +218,10 @@ export class QueryBuilder { this.#query.allOr = true } + setOnEmptyFilter(value: EmptyFilterOption) { + this.#query.onEmptyFilter = value + } + handleSpaces(input: string) { if (this.#noEscaping) { return input @@ -289,8 +293,9 @@ export class QueryBuilder { const builder = this let allOr = this.#query && this.#query.allOr let query = allOr ? "" : "*:*" + let allFiltersEmpty = true const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true } - let tableId + let tableId: string = "" if (this.#query.equal!.tableId) { tableId = this.#query.equal!.tableId delete this.#query.equal!.tableId @@ -305,7 +310,7 @@ export class QueryBuilder { } const contains = (key: string, value: any, mode = "AND") => { - if (Array.isArray(value) && value.length === 0) { + if (!value || (Array.isArray(value) && value.length === 0)) { return null } if (!Array.isArray(value)) { @@ -384,6 +389,12 @@ export class QueryBuilder { built += ` ${mode} ` } built += expression + if ( + (typeof value !== "string" && value != null) || + (typeof value === "string" && value !== tableId && value !== "") + ) { + allFiltersEmpty = false + } } if (opts?.returnBuilt) { return built @@ -463,6 +474,13 @@ export class QueryBuilder { allOr = false build({ tableId }, equal) } + if (allFiltersEmpty) { + if (this.#query.onEmptyFilter === EmptyFilterOption.RETURN_NONE) { + return "" + } else if (this.#query?.allOr) { + return query.replace("()", "(*:*)") + } + } return query } diff --git a/packages/backend-core/src/db/tests/lucene.spec.ts b/packages/backend-core/src/db/tests/lucene.spec.ts index a82828d8f2..7716661d88 100644 --- a/packages/backend-core/src/db/tests/lucene.spec.ts +++ b/packages/backend-core/src/db/tests/lucene.spec.ts @@ -1,6 +1,6 @@ import { newid } from "../../docIds/newid" import { getDB } from "../db" -import { Database } from "@budibase/types" +import { Database, EmptyFilterOption } from "@budibase/types" import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene" const INDEX_NAME = "main" @@ -156,6 +156,76 @@ describe("lucene", () => { expect(resp.rows.length).toBe(2) }) + describe("empty filters behaviour", () => { + it("should return all rows by default", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addEqual("property", "") + builder.addEqual("number", null) + builder.addString("property", "") + builder.addFuzzy("property", "") + builder.addNotEqual("number", undefined) + builder.addOneOf("number", null) + builder.addContains("array", undefined) + builder.addNotContains("array", null) + builder.addContainsAny("array", null) + + const resp = await builder.run() + expect(resp.rows.length).toBe(3) + }) + + it("should return all rows when onEmptyFilter is ALL", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.setOnEmptyFilter(EmptyFilterOption.RETURN_ALL) + builder.setAllOr() + builder.addEqual("property", "") + builder.addEqual("number", null) + builder.addString("property", "") + builder.addFuzzy("property", "") + builder.addNotEqual("number", undefined) + builder.addOneOf("number", null) + builder.addContains("array", undefined) + builder.addNotContains("array", null) + builder.addContainsAny("array", null) + + const resp = await builder.run() + expect(resp.rows.length).toBe(3) + }) + + it("should return no rows when onEmptyFilter is NONE", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.setOnEmptyFilter(EmptyFilterOption.RETURN_NONE) + builder.addEqual("property", "") + builder.addEqual("number", null) + builder.addString("property", "") + builder.addFuzzy("property", "") + builder.addNotEqual("number", undefined) + builder.addOneOf("number", null) + builder.addContains("array", undefined) + builder.addNotContains("array", null) + builder.addContainsAny("array", null) + + const resp = await builder.run() + expect(resp.rows.length).toBe(0) + }) + + it("should return all matching rows when onEmptyFilter is NONE, but a filter value is provided", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.setOnEmptyFilter(EmptyFilterOption.RETURN_NONE) + builder.addEqual("property", "") + builder.addEqual("number", 1) + builder.addString("property", "") + builder.addFuzzy("property", "") + builder.addNotEqual("number", undefined) + builder.addOneOf("number", null) + builder.addContains("array", undefined) + builder.addNotContains("array", null) + builder.addContainsAny("array", null) + + const resp = await builder.run() + expect(resp.rows.length).toBe(1) + }) + }) + describe("skip", () => { const skipDbName = `db-${newid()}` let docs: { diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index 6034296996..4ebf8392b5 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -2,7 +2,7 @@ import env from "../environment" import { DEFAULT_TENANT_ID, SEPARATOR, DocumentType } from "../constants" import { getTenantId, getGlobalDBName } from "../context" import { doWithDB, directCouchAllDbs } from "./db" -import { getAppMetadata } from "../cache/appMetadata" +import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata" import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions" import { App, Database } from "@budibase/types" import { getStartEndKeyURL } from "../docIds" @@ -101,7 +101,9 @@ export async function getAllApps({ const response = await Promise.allSettled(appPromises) const apps = response .filter( - (result: any) => result.status === "fulfilled" && result.value != null + (result: any) => + result.status === "fulfilled" && + result.value?.state !== AppState.INVALID ) .map(({ value }: any) => value) if (!all) { @@ -126,7 +128,11 @@ export async function getAppsByIDs(appIds: string[]) { ) // have to list the apps which exist, some may have been deleted return settled - .filter(promise => promise.status === "fulfilled") + .filter( + promise => + promise.status === "fulfilled" && + (promise.value as DeletedApp).state !== AppState.INVALID + ) .map(promise => (promise as PromiseFulfilledResult).value) } diff --git a/packages/backend-core/src/db/views.ts b/packages/backend-core/src/db/views.ts index fddb1ab34b..7f5ef29a0a 100644 --- a/packages/backend-core/src/db/views.ts +++ b/packages/backend-core/src/db/views.ts @@ -105,16 +105,6 @@ export const createApiKeyView = async () => { await createView(db, viewJs, ViewName.BY_API_KEY) } -export const createUserBuildersView = async () => { - const db = getGlobalDB() - const viewJs = `function(doc) { - if (doc.builder && doc.builder.global === true) { - emit(doc._id, doc._id) - } - }` - await createView(db, viewJs, ViewName.USER_BY_BUILDERS) -} - export interface QueryViewOptions { arrayResponse?: boolean } @@ -223,7 +213,6 @@ export const queryPlatformView = async ( const CreateFuncByName: any = { [ViewName.USER_BY_EMAIL]: createNewUserEmailView, [ViewName.BY_API_KEY]: createApiKeyView, - [ViewName.USER_BY_BUILDERS]: createUserBuildersView, [ViewName.USER_BY_APP]: createUserAppView, } diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 5076b7569b..05fcbffd46 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -1,4 +1,5 @@ import { existsSync, readFileSync } from "fs" +import { ServiceType } from "@budibase/types" function isTest() { return isCypress() || isJest() @@ -83,10 +84,20 @@ function getPackageJsonFields(): { } } +function isWorker() { + return environment.SERVICE_TYPE === ServiceType.WORKER +} + +function isApps() { + return environment.SERVICE_TYPE === ServiceType.APPS +} + const environment = { isTest, isJest, isDev, + isWorker, + isApps, isProd: () => { return !isDev() }, @@ -153,6 +164,7 @@ const environment = { SMTP_FROM_ADDRESS: process.env.SMTP_FROM_ADDRESS, DISABLE_JWT_WARNING: process.env.DISABLE_JWT_WARNING, BLACKLIST_IPS: process.env.BLACKLIST_IPS, + SERVICE_TYPE: "unknown", /** * Enable to allow an admin user to login using a password. * This can be useful to prevent lockout when configuring SSO. @@ -163,6 +175,7 @@ const environment = { : false, ...getPackageJsonFields(), DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER, + OFFLINE_MODE: process.env.OFFLINE_MODE, _set(key: any, value: any) { process.env[key] = value // @ts-ignore diff --git a/packages/backend-core/src/errors/errors.ts b/packages/backend-core/src/errors/errors.ts index 4e1f1abbb5..7d55d25e89 100644 --- a/packages/backend-core/src/errors/errors.ts +++ b/packages/backend-core/src/errors/errors.ts @@ -55,6 +55,18 @@ export class HTTPError extends BudibaseError { } } +export class NotFoundError extends HTTPError { + constructor(message: string) { + super(message, 404) + } +} + +export class BadRequestError extends HTTPError { + constructor(message: string) { + super(message, 400) + } +} + // LICENSING export class UsageLimitError extends HTTPError { diff --git a/packages/backend-core/src/events/identification.ts b/packages/backend-core/src/events/identification.ts index 5eb11d1354..c7bc1c817b 100644 --- a/packages/backend-core/src/events/identification.ts +++ b/packages/backend-core/src/events/identification.ts @@ -21,6 +21,7 @@ import { processors } from "./processors" import { newid } from "../utils" import * as installation from "../installation" import * as configs from "../configs" +import * as users from "../users" import { withCache, TTL, CacheKey } from "../cache/generic" /** @@ -164,8 +165,8 @@ const identifyUser = async ( const id = user._id as string const tenantId = await getEventTenantId(user.tenantId) const type = IdentityType.USER - let builder = user.builder?.global || false - let admin = user.admin?.global || false + let builder = users.hasBuilderPermissions(user) + let admin = users.hasAdminPermissions(user) let providerType if (isSSOUser(user)) { providerType = user.providerType @@ -264,7 +265,7 @@ const getEventTenantId = async (tenantId: string): Promise => { } } -const getUniqueTenantId = async (tenantId: string): Promise => { +export const getUniqueTenantId = async (tenantId: string): Promise => { // make sure this tenantId always matches the tenantId in context return context.doInTenant(tenantId, () => { return withCache(CacheKey.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => { diff --git a/packages/backend-core/src/featureFlags/index.ts b/packages/backend-core/src/features/index.ts similarity index 98% rename from packages/backend-core/src/featureFlags/index.ts rename to packages/backend-core/src/features/index.ts index 877cd60e1a..8f5c903e05 100644 --- a/packages/backend-core/src/featureFlags/index.ts +++ b/packages/backend-core/src/features/index.ts @@ -1,5 +1,6 @@ import env from "../environment" import * as context from "../context" +export * from "./installation" /** * Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant. diff --git a/packages/backend-core/src/features/installation.ts b/packages/backend-core/src/features/installation.ts new file mode 100644 index 0000000000..defc8bf987 --- /dev/null +++ b/packages/backend-core/src/features/installation.ts @@ -0,0 +1,17 @@ +export function processFeatureEnvVar( + fullList: string[], + featureList?: string +) { + let list + if (!featureList) { + list = fullList + } else { + list = featureList.split(",") + } + for (let feature of list) { + if (!fullList.includes(feature)) { + throw new Error(`Feature: ${feature} is not an allowed option`) + } + } + return list as unknown as T[] +} diff --git a/packages/backend-core/src/featureFlags/tests/featureFlags.spec.ts b/packages/backend-core/src/features/tests/featureFlags.spec.ts similarity index 100% rename from packages/backend-core/src/featureFlags/tests/featureFlags.spec.ts rename to packages/backend-core/src/features/tests/featureFlags.spec.ts diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index 7b98674788..ffffd8240a 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -6,7 +6,8 @@ export * as roles from "./security/roles" export * as permissions from "./security/permissions" export * as accounts from "./accounts" export * as installation from "./installation" -export * as featureFlags from "./featureFlags" +export * as featureFlags from "./features" +export * as features from "./features/installation" export * as sessions from "./security/sessions" export * as platform from "./platform" export * as auth from "./auth" diff --git a/packages/backend-core/src/logging/index.ts b/packages/backend-core/src/logging/index.ts index f7e1c4fa41..0824fa681b 100644 --- a/packages/backend-core/src/logging/index.ts +++ b/packages/backend-core/src/logging/index.ts @@ -2,6 +2,3 @@ export * as correlation from "./correlation/correlation" export { logger } from "./pino/logger" export * from "./alerts" export * as system from "./system" - -// turn off or on context logging i.e. tenantId, appId etc -export let LOG_CONTEXT = true diff --git a/packages/backend-core/src/logging/pino/logger.ts b/packages/backend-core/src/logging/pino/logger.ts index 0b130068f3..7c444a3a59 100644 --- a/packages/backend-core/src/logging/pino/logger.ts +++ b/packages/backend-core/src/logging/pino/logger.ts @@ -2,11 +2,9 @@ import pino, { LoggerOptions } from "pino" import pinoPretty from "pino-pretty" import { IdentityType } from "@budibase/types" - import env from "../../environment" import * as context from "../../context" import * as correlation from "../correlation" -import { LOG_CONTEXT } from "../index" import { localFileDestination } from "../system" @@ -14,29 +12,44 @@ import { localFileDestination } from "../system" let pinoInstance: pino.Logger | undefined if (!env.DISABLE_PINO_LOGGER) { + const level = env.LOG_LEVEL const pinoOptions: LoggerOptions = { - level: env.LOG_LEVEL, + level, formatters: { - level: label => { - return { level: label.toUpperCase() } + level: level => { + return { level: level.toUpperCase() } }, bindings: () => { - return { - service: env.SERVICE_NAME, + if (env.SELF_HOSTED) { + // "service" is being injected in datadog using the pod names, + // so we should leave it blank to allow the default behaviour if it's not running self-hosted + return { + service: env.SERVICE_NAME, + } + } else { + return {} } }, }, timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`, } - const destinations: pino.DestinationStream[] = [] + const destinations: pino.StreamEntry[] = [] - if (env.isDev()) { - destinations.push(pinoPretty({ singleLine: true })) - } + destinations.push( + env.isDev() + ? { + stream: pinoPretty({ singleLine: true }), + level: level as pino.Level, + } + : { stream: process.stdout, level: level as pino.Level } + ) if (env.SELF_HOSTED) { - destinations.push(localFileDestination()) + destinations.push({ + stream: localFileDestination(), + level: level as pino.Level, + }) } pinoInstance = destinations.length @@ -93,15 +106,13 @@ if (!env.DISABLE_PINO_LOGGER) { let contextObject = {} - if (LOG_CONTEXT) { - contextObject = { - tenantId: getTenantId(), - appId: getAppId(), - automationId: getAutomationId(), - identityId: identity?._id, - identityType: identity?.type, - correlationId: correlation.getId(), - } + contextObject = { + tenantId: getTenantId(), + appId: getAppId(), + automationId: getAutomationId(), + identityId: identity?._id, + identityType: identity?.type, + correlationId: correlation.getId(), } const mergingObject: any = { diff --git a/packages/backend-core/src/middleware/adminOnly.ts b/packages/backend-core/src/middleware/adminOnly.ts index dbe1e3a501..6b2ee87c01 100644 --- a/packages/backend-core/src/middleware/adminOnly.ts +++ b/packages/backend-core/src/middleware/adminOnly.ts @@ -1,10 +1,8 @@ -import { BBContext } from "@budibase/types" +import { UserCtx } from "@budibase/types" +import { isAdmin } from "../users" -export default async (ctx: BBContext, next: any) => { - if ( - !ctx.internal && - (!ctx.user || !ctx.user.admin || !ctx.user.admin.global) - ) { +export default async (ctx: UserCtx, next: any) => { + if (!ctx.internal && !isAdmin(ctx.user)) { ctx.throw(403, "Admin user only endpoint.") } return next() diff --git a/packages/backend-core/src/middleware/builderOnly.ts b/packages/backend-core/src/middleware/builderOnly.ts index a00fd63a22..8c1c54a44c 100644 --- a/packages/backend-core/src/middleware/builderOnly.ts +++ b/packages/backend-core/src/middleware/builderOnly.ts @@ -1,10 +1,19 @@ -import { BBContext } from "@budibase/types" +import { UserCtx } from "@budibase/types" +import { isBuilder, hasBuilderPermissions } from "../users" +import { getAppId } from "../context" +import env from "../environment" -export default async (ctx: BBContext, next: any) => { - if ( - !ctx.internal && - (!ctx.user || !ctx.user.builder || !ctx.user.builder.global) - ) { +export default async (ctx: UserCtx, next: any) => { + const appId = getAppId() + const builderFn = env.isWorker() + ? hasBuilderPermissions + : env.isApps() + ? isBuilder + : undefined + if (!builderFn) { + throw new Error("Service name unknown - middleware inactive.") + } + if (!ctx.internal && !builderFn(ctx.user, appId)) { ctx.throw(403, "Builder user only endpoint.") } return next() diff --git a/packages/backend-core/src/middleware/builderOrAdmin.ts b/packages/backend-core/src/middleware/builderOrAdmin.ts index 26bb3a1bda..c03e856233 100644 --- a/packages/backend-core/src/middleware/builderOrAdmin.ts +++ b/packages/backend-core/src/middleware/builderOrAdmin.ts @@ -1,12 +1,20 @@ -import { BBContext } from "@budibase/types" +import { UserCtx } from "@budibase/types" +import { isBuilder, isAdmin, hasBuilderPermissions } from "../users" +import { getAppId } from "../context" +import env from "../environment" -export default async (ctx: BBContext, next: any) => { - if ( - !ctx.internal && - (!ctx.user || !ctx.user.builder || !ctx.user.builder.global) && - (!ctx.user || !ctx.user.admin || !ctx.user.admin.global) - ) { - ctx.throw(403, "Builder user only endpoint.") +export default async (ctx: UserCtx, next: any) => { + const appId = getAppId() + const builderFn = env.isWorker() + ? hasBuilderPermissions + : env.isApps() + ? isBuilder + : undefined + if (!builderFn) { + throw new Error("Service name unknown - middleware inactive.") + } + if (!ctx.internal && !builderFn(ctx.user, appId) && !isAdmin(ctx.user)) { + ctx.throw(403, "Admin/Builder user only endpoint.") } return next() } diff --git a/packages/backend-core/src/middleware/tests/builder.spec.ts b/packages/backend-core/src/middleware/tests/builder.spec.ts new file mode 100644 index 0000000000..d350eff4f6 --- /dev/null +++ b/packages/backend-core/src/middleware/tests/builder.spec.ts @@ -0,0 +1,180 @@ +import adminOnly from "../adminOnly" +import builderOnly from "../builderOnly" +import builderOrAdmin from "../builderOrAdmin" +import { structures } from "../../../tests" +import { ContextUser, ServiceType } from "@budibase/types" +import { doInAppContext } from "../../context" +import env from "../../environment" +env._set("SERVICE_TYPE", ServiceType.APPS) + +const appId = "app_aaa" +const basicUser = structures.users.user() +const adminUser = structures.users.adminUser() +const adminOnlyUser = structures.users.adminOnlyUser() +const builderUser = structures.users.builderUser() +const appBuilderUser = structures.users.appBuilderUser(appId) + +function buildUserCtx(user: ContextUser) { + return { + internal: false, + user, + throw: jest.fn(), + } as any +} + +function passed(throwFn: jest.Func, nextFn: jest.Func) { + expect(throwFn).not.toBeCalled() + expect(nextFn).toBeCalled() +} + +function threw(throwFn: jest.Func) { + // cant check next, the throw function doesn't actually throw - so it still continues + expect(throwFn).toBeCalled() +} + +describe("adminOnly middleware", () => { + it("should allow admin user", () => { + const ctx = buildUserCtx(adminUser), + next = jest.fn() + adminOnly(ctx, next) + passed(ctx.throw, next) + }) + + it("should not allow basic user", () => { + const ctx = buildUserCtx(basicUser), + next = jest.fn() + adminOnly(ctx, next) + threw(ctx.throw) + }) + + it("should not allow builder user", () => { + const ctx = buildUserCtx(builderUser), + next = jest.fn() + adminOnly(ctx, next) + threw(ctx.throw) + }) +}) + +describe("builderOnly middleware", () => { + it("should allow builder user", () => { + const ctx = buildUserCtx(builderUser), + next = jest.fn() + builderOnly(ctx, next) + passed(ctx.throw, next) + }) + + it("should allow app builder user", () => { + const ctx = buildUserCtx(appBuilderUser), + next = jest.fn() + doInAppContext(appId, () => { + builderOnly(ctx, next) + }) + passed(ctx.throw, next) + }) + + it("should allow admin and builder user", () => { + const ctx = buildUserCtx(adminUser), + next = jest.fn() + builderOnly(ctx, next) + passed(ctx.throw, next) + }) + + it("should not allow admin user", () => { + const ctx = buildUserCtx(adminOnlyUser), + next = jest.fn() + builderOnly(ctx, next) + threw(ctx.throw) + }) + + it("should not allow app builder user to different app", () => { + const ctx = buildUserCtx(appBuilderUser), + next = jest.fn() + doInAppContext("app_bbb", () => { + builderOnly(ctx, next) + }) + threw(ctx.throw) + }) + + it("should not allow basic user", () => { + const ctx = buildUserCtx(basicUser), + next = jest.fn() + builderOnly(ctx, next) + threw(ctx.throw) + }) +}) + +describe("builderOrAdmin middleware", () => { + it("should allow builder user", () => { + const ctx = buildUserCtx(builderUser), + next = jest.fn() + builderOrAdmin(ctx, next) + passed(ctx.throw, next) + }) + + it("should allow builder and admin user", () => { + const ctx = buildUserCtx(adminUser), + next = jest.fn() + builderOrAdmin(ctx, next) + passed(ctx.throw, next) + }) + + it("should allow admin user", () => { + const ctx = buildUserCtx(adminOnlyUser), + next = jest.fn() + builderOrAdmin(ctx, next) + passed(ctx.throw, next) + }) + + it("should allow app builder user", () => { + const ctx = buildUserCtx(appBuilderUser), + next = jest.fn() + doInAppContext(appId, () => { + builderOrAdmin(ctx, next) + }) + passed(ctx.throw, next) + }) + + it("should not allow basic user", () => { + const ctx = buildUserCtx(basicUser), + next = jest.fn() + builderOrAdmin(ctx, next) + threw(ctx.throw) + }) +}) + +describe("check service difference", () => { + it("should not allow without app ID in apps", () => { + env._set("SERVICE_TYPE", ServiceType.APPS) + const appId = "app_a" + const ctx = buildUserCtx({ + ...basicUser, + builder: { + apps: [appId], + }, + }) + const next = jest.fn() + doInAppContext(appId, () => { + builderOnly(ctx, next) + }) + passed(ctx.throw, next) + doInAppContext("app_b", () => { + builderOnly(ctx, next) + }) + threw(ctx.throw) + }) + + it("should allow without app ID in worker", () => { + env._set("SERVICE_TYPE", ServiceType.WORKER) + const ctx = buildUserCtx({ + ...basicUser, + builder: { + apps: ["app_a"], + }, + }) + const next = jest.fn() + doInAppContext("app_b", () => { + builderOnly(ctx, next) + }) + passed(ctx.throw, next) + }) +}) diff --git a/packages/backend-core/src/security/permissions.ts b/packages/backend-core/src/security/permissions.ts index 6cacc12dd6..aa0b20a30c 100644 --- a/packages/backend-core/src/security/permissions.ts +++ b/packages/backend-core/src/security/permissions.ts @@ -1,29 +1,12 @@ -const { flatten } = require("lodash") -const { cloneDeep } = require("lodash/fp") +import { PermissionType, PermissionLevel } from "@budibase/types" +export { PermissionType, PermissionLevel } from "@budibase/types" +import flatten from "lodash/flatten" +import cloneDeep from "lodash/fp/cloneDeep" export type RoleHierarchy = { permissionId: string }[] -export enum PermissionLevel { - READ = "read", - WRITE = "write", - EXECUTE = "execute", - ADMIN = "admin", -} - -// these are the global types, that govern the underlying default behaviour -export enum PermissionType { - APP = "app", - TABLE = "table", - USER = "user", - AUTOMATION = "automation", - WEBHOOK = "webhook", - BUILDER = "builder", - VIEW = "view", - QUERY = "query", -} - export class Permission { type: PermissionType level: PermissionLevel @@ -95,7 +78,6 @@ export const BUILTIN_PERMISSIONS = { permissions: [ new Permission(PermissionType.QUERY, PermissionLevel.READ), new Permission(PermissionType.TABLE, PermissionLevel.READ), - new Permission(PermissionType.VIEW, PermissionLevel.READ), ], }, WRITE: { @@ -104,7 +86,6 @@ export const BUILTIN_PERMISSIONS = { permissions: [ new Permission(PermissionType.QUERY, PermissionLevel.WRITE), new Permission(PermissionType.TABLE, PermissionLevel.WRITE), - new Permission(PermissionType.VIEW, PermissionLevel.READ), new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), ], }, @@ -115,7 +96,6 @@ export const BUILTIN_PERMISSIONS = { new Permission(PermissionType.TABLE, PermissionLevel.WRITE), new Permission(PermissionType.USER, PermissionLevel.READ), new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE), - new Permission(PermissionType.VIEW, PermissionLevel.READ), new Permission(PermissionType.WEBHOOK, PermissionLevel.READ), ], }, @@ -126,7 +106,6 @@ export const BUILTIN_PERMISSIONS = { new Permission(PermissionType.TABLE, PermissionLevel.ADMIN), new Permission(PermissionType.USER, PermissionLevel.ADMIN), new Permission(PermissionType.AUTOMATION, PermissionLevel.ADMIN), - new Permission(PermissionType.VIEW, PermissionLevel.ADMIN), new Permission(PermissionType.WEBHOOK, PermissionLevel.READ), new Permission(PermissionType.QUERY, PermissionLevel.ADMIN), ], @@ -173,3 +152,4 @@ export function isPermissionLevelHigherThanRead(level: PermissionLevel) { // utility as a lot of things need simply the builder permission export const BUILDER = PermissionType.BUILDER +export const GLOBAL_BUILDER = PermissionType.GLOBAL_BUILDER diff --git a/packages/backend-core/src/security/roles.ts b/packages/backend-core/src/security/roles.ts index cf5c6bc406..081193b433 100644 --- a/packages/backend-core/src/security/roles.ts +++ b/packages/backend-core/src/security/roles.ts @@ -3,7 +3,7 @@ import { prefixRoleID, getRoleParams, DocumentType, SEPARATOR } from "../db" import { getAppDB } from "../context" import { doWithDB } from "../db" import { Screen, Role as RoleDoc } from "@budibase/types" -const { cloneDeep } = require("lodash/fp") +import cloneDeep from "lodash/fp/cloneDeep" export const BUILTIN_ROLE_IDS = { ADMIN: "ADMIN", diff --git a/packages/backend-core/src/security/tests/permissions.spec.ts b/packages/backend-core/src/security/tests/permissions.spec.ts index caf8bb29a6..39348646fb 100644 --- a/packages/backend-core/src/security/tests/permissions.spec.ts +++ b/packages/backend-core/src/security/tests/permissions.spec.ts @@ -1,4 +1,4 @@ -import { cloneDeep } from "lodash" +import cloneDeep from "lodash/cloneDeep" import * as permissions from "../permissions" import { BUILTIN_ROLE_IDS } from "../roles" diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts new file mode 100644 index 0000000000..14140cba81 --- /dev/null +++ b/packages/backend-core/src/users/db.ts @@ -0,0 +1,468 @@ +import env from "../environment" +import * as eventHelpers from "./events" +import * as accounts from "../accounts" +import * as accountSdk from "../accounts" +import * as cache from "../cache" +import { getGlobalDB, getIdentity, getTenantId } from "../context" +import * as dbUtils from "../db" +import { EmailUnavailableError, HTTPError } from "../errors" +import * as platform from "../platform" +import * as sessions from "../security/sessions" +import * as usersCore from "./users" +import { + Account, + AllDocsResponse, + BulkUserCreated, + BulkUserDeleted, + isSSOAccount, + isSSOUser, + RowResponse, + SaveUserOpts, + User, + UserStatus, +} from "@budibase/types" +import { + getAccountHolderFromUserIds, + isAdmin, + validateUniqueUser, +} from "./utils" +import { searchExistingEmails } from "./lookup" +import { hash } from "../utils" + +type QuotaUpdateFn = (change: number, cb?: () => Promise) => Promise +type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise +type FeatureFn = () => Promise +type QuotaFns = { addUsers: QuotaUpdateFn; removeUsers: QuotaUpdateFn } +type GroupFns = { addUsers: GroupUpdateFn } +type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn } + +const bulkDeleteProcessing = async (dbUser: User) => { + const userId = dbUser._id as string + await platform.users.removeUser(dbUser) + await eventHelpers.handleDeleteEvents(dbUser) + await cache.user.invalidateUser(userId) + await sessions.invalidateSessions(userId, { reason: "bulk-deletion" }) +} + +export class UserDB { + static quotas: QuotaFns + static groups: GroupFns + static features: FeatureFns + + static init(quotaFns: QuotaFns, groupFns: GroupFns, featureFns: FeatureFns) { + UserDB.quotas = quotaFns + UserDB.groups = groupFns + UserDB.features = featureFns + } + + static async isPreventPasswordActions(user: User, account?: Account) { + // when in maintenance mode we allow sso users with the admin role + // to perform any password action - this prevents lockout + if (env.ENABLE_SSO_MAINTENANCE_MODE && isAdmin(user)) { + return false + } + + // SSO is enforced for all users + if (await UserDB.features.isSSOEnforced()) { + return true + } + + // Check local sso + if (isSSOUser(user)) { + return true + } + + // Check account sso + if (!account) { + account = await accountSdk.getAccountByTenantId(getTenantId()) + } + return !!(account && account.email === user.email && isSSOAccount(account)) + } + + static async buildUser( + user: User, + opts: SaveUserOpts = { + hashPassword: true, + requirePassword: true, + }, + tenantId: string, + dbUser?: any, + account?: Account + ): Promise { + let { password, _id } = user + + // don't require a password if the db user doesn't already have one + if (dbUser && !dbUser.password) { + opts.requirePassword = false + } + + let hashedPassword + if (password) { + if (await UserDB.isPreventPasswordActions(user, account)) { + throw new HTTPError("Password change is disabled for this user", 400) + } + hashedPassword = opts.hashPassword ? await hash(password) : password + } else if (dbUser) { + hashedPassword = dbUser.password + } + + // passwords are never required if sso is enforced + const requirePasswords = + opts.requirePassword && !(await UserDB.features.isSSOEnforced()) + if (!hashedPassword && requirePasswords) { + throw "Password must be specified." + } + + _id = _id || dbUtils.generateGlobalUserID() + + const fullUser = { + createdAt: Date.now(), + ...dbUser, + ...user, + _id, + password: hashedPassword, + tenantId, + } + // make sure the roles object is always present + if (!fullUser.roles) { + fullUser.roles = {} + } + // add the active status to a user if its not provided + if (fullUser.status == null) { + fullUser.status = UserStatus.ACTIVE + } + + return fullUser + } + + static async allUsers() { + const db = getGlobalDB() + const response = await db.allDocs( + dbUtils.getGlobalUserParams(null, { + include_docs: true, + }) + ) + return response.rows.map((row: any) => row.doc) + } + + static async countUsersByApp(appId: string) { + let response: any = await usersCore.searchGlobalUsersByApp(appId, {}) + return { + userCount: response.length, + } + } + + static async getUsersByAppAccess(appId?: string) { + const opts: any = { + include_docs: true, + limit: 50, + } + let response: User[] = await usersCore.searchGlobalUsersByAppAccess( + appId, + opts + ) + return response + } + + static async getUserByEmail(email: string) { + return usersCore.getGlobalUserByEmail(email) + } + + /** + * Gets a user by ID from the global database, based on the current tenancy. + */ + static async getUser(userId: string) { + const user = await usersCore.getById(userId) + if (user) { + delete user.password + } + return user + } + + static async bulkGet(userIds: string[]) { + return await usersCore.bulkGetGlobalUsersById(userIds) + } + + static async bulkUpdate(users: User[]) { + return await usersCore.bulkUpdateGlobalUsers(users) + } + + static async save(user: User, opts: SaveUserOpts = {}): Promise { + // default booleans to true + if (opts.hashPassword == null) { + opts.hashPassword = true + } + if (opts.requirePassword == null) { + opts.requirePassword = true + } + const tenantId = getTenantId() + const db = getGlobalDB() + + let { email, _id, userGroups = [], roles } = user + + if (!email && !_id) { + throw new Error("_id or email is required") + } + + if ( + user.builder?.apps?.length && + !(await UserDB.features.isAppBuildersEnabled()) + ) { + throw new Error("Unable to update app builders, please check license") + } + + let dbUser: User | undefined + if (_id) { + // try to get existing user from db + try { + dbUser = (await db.get(_id)) as User + if (email && dbUser.email !== email) { + throw "Email address cannot be changed" + } + email = dbUser.email + } catch (e: any) { + if (e.status === 404) { + // do nothing, save this new user with the id specified - required for SSO auth + } else { + throw e + } + } + } + + if (!dbUser && email) { + // no id was specified - load from email instead + dbUser = await usersCore.getGlobalUserByEmail(email) + if (dbUser && dbUser._id !== _id) { + throw new EmailUnavailableError(email) + } + } + + const change = dbUser ? 0 : 1 // no change if there is existing user + return UserDB.quotas.addUsers(change, async () => { + await validateUniqueUser(email, tenantId) + + let builtUser = await UserDB.buildUser(user, opts, tenantId, dbUser) + // don't allow a user to update its own roles/perms + if (opts.currentUserId && opts.currentUserId === dbUser?._id) { + builtUser = usersCore.cleanseUserObject(builtUser, dbUser) as User + } + + if (!dbUser && roles?.length) { + builtUser.roles = { ...roles } + } + + // make sure we set the _id field for a new user + // Also if this is a new user, associate groups with them + let groupPromises = [] + if (!_id) { + _id = builtUser._id! + + if (userGroups.length > 0) { + for (let groupId of userGroups) { + groupPromises.push(UserDB.groups.addUsers(groupId, [_id!])) + } + } + } + + try { + // save the user to db + let response = await db.put(builtUser) + builtUser._rev = response.rev + + await eventHelpers.handleSaveEvents(builtUser, dbUser) + await platform.users.addUser(tenantId, builtUser._id!, builtUser.email) + await cache.user.invalidateUser(response.id) + + await Promise.all(groupPromises) + + // finally returned the saved user from the db + return db.get(builtUser._id!) + } catch (err: any) { + if (err.status === 409) { + throw "User exists already" + } else { + throw err + } + } + }) + } + + static async bulkCreate( + newUsersRequested: User[], + groups: string[] + ): Promise { + const tenantId = getTenantId() + + let usersToSave: any[] = [] + let newUsers: any[] = [] + + const emails = newUsersRequested.map((user: User) => user.email) + const existingEmails = await searchExistingEmails(emails) + const unsuccessful: { email: string; reason: string }[] = [] + + for (const newUser of newUsersRequested) { + if ( + newUsers.find( + (x: User) => x.email.toLowerCase() === newUser.email.toLowerCase() + ) || + existingEmails.includes(newUser.email.toLowerCase()) + ) { + unsuccessful.push({ + email: newUser.email, + reason: `Unavailable`, + }) + continue + } + newUser.userGroups = groups + newUsers.push(newUser) + } + + const account = await accountSdk.getAccountByTenantId(tenantId) + return UserDB.quotas.addUsers(newUsers.length, async () => { + // create the promises array that will be called by bulkDocs + newUsers.forEach((user: any) => { + usersToSave.push( + UserDB.buildUser( + user, + { + hashPassword: true, + requirePassword: user.requirePassword, + }, + tenantId, + undefined, // no dbUser + account + ) + ) + }) + + const usersToBulkSave = await Promise.all(usersToSave) + await usersCore.bulkUpdateGlobalUsers(usersToBulkSave) + + // Post-processing of bulk added users, e.g. events and cache operations + for (const user of usersToBulkSave) { + // TODO: Refactor to bulk insert users into the info db + // instead of relying on looping tenant creation + await platform.users.addUser(tenantId, user._id, user.email) + await eventHelpers.handleSaveEvents(user, undefined) + } + + const saved = usersToBulkSave.map(user => { + return { + _id: user._id, + email: user.email, + } + }) + + // now update the groups + if (Array.isArray(saved) && groups) { + const groupPromises = [] + const createdUserIds = saved.map(user => user._id) + for (let groupId of groups) { + groupPromises.push(UserDB.groups.addUsers(groupId, createdUserIds)) + } + await Promise.all(groupPromises) + } + + return { + successful: saved, + unsuccessful, + } + }) + } + + static async bulkDelete(userIds: string[]): Promise { + const db = getGlobalDB() + + const response: BulkUserDeleted = { + successful: [], + unsuccessful: [], + } + + // remove the account holder from the delete request if present + const account = await getAccountHolderFromUserIds(userIds) + if (account) { + userIds = userIds.filter(u => u !== account.budibaseUserId) + // mark user as unsuccessful + response.unsuccessful.push({ + _id: account.budibaseUserId, + email: account.email, + reason: "Account holder cannot be deleted", + }) + } + + // Get users and delete + const allDocsResponse: AllDocsResponse = await db.allDocs({ + include_docs: true, + keys: userIds, + }) + const usersToDelete: User[] = allDocsResponse.rows.map( + (user: RowResponse) => { + return user.doc + } + ) + + // Delete from DB + const toDelete = usersToDelete.map(user => ({ + ...user, + _deleted: true, + })) + const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete) + + await UserDB.quotas.removeUsers(toDelete.length) + for (let user of usersToDelete) { + await bulkDeleteProcessing(user) + } + + // Build Response + // index users by id + const userIndex: { [key: string]: User } = {} + usersToDelete.reduce((prev, current) => { + prev[current._id!] = current + return prev + }, userIndex) + + // add the successful and unsuccessful users to response + dbResponse.forEach(item => { + const email = userIndex[item.id].email + if (item.ok) { + response.successful.push({ _id: item.id, email }) + } else { + response.unsuccessful.push({ + _id: item.id, + email, + reason: "Database error", + }) + } + }) + + return response + } + + static async destroy(id: string) { + const db = getGlobalDB() + const dbUser = (await db.get(id)) as User + const userId = dbUser._id as string + + if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { + // root account holder can't be deleted from inside budibase + const email = dbUser.email + const account = await accounts.getAccount(email) + if (account) { + if (dbUser.userId === getIdentity()!._id) { + throw new HTTPError('Please visit "Account" to delete this user', 400) + } else { + throw new HTTPError("Account holder cannot be deleted", 400) + } + } + } + + await platform.users.removeUser(dbUser) + + await db.remove(userId, dbUser._rev) + + await UserDB.quotas.removeUsers(1) + await eventHelpers.handleDeleteEvents(dbUser) + await cache.user.invalidateUser(userId) + await sessions.invalidateSessions(userId, { reason: "deletion" }) + } +} diff --git a/packages/worker/src/sdk/users/events.ts b/packages/backend-core/src/users/events.ts similarity index 86% rename from packages/worker/src/sdk/users/events.ts rename to packages/backend-core/src/users/events.ts index 7d86182a3c..f170c9ffe9 100644 --- a/packages/worker/src/sdk/users/events.ts +++ b/packages/backend-core/src/users/events.ts @@ -1,15 +1,18 @@ -import env from "../../environment" -import { events, accounts, tenancy } from "@budibase/backend-core" +import env from "../environment" +import * as events from "../events" +import * as accounts from "../accounts" +import { getTenantId } from "../context" import { User, UserRoles, CloudAccount } from "@budibase/types" +import { hasBuilderPermissions, hasAdminPermissions } from "./utils" export const handleDeleteEvents = async (user: any) => { await events.user.deleted(user) - if (isBuilder(user)) { + if (hasBuilderPermissions(user)) { await events.user.permissionBuilderRemoved(user) } - if (isAdmin(user)) { + if (hasAdminPermissions(user)) { await events.user.permissionAdminRemoved(user) } } @@ -55,7 +58,7 @@ export const handleSaveEvents = async ( user: User, existingUser: User | undefined ) => { - const tenantId = tenancy.getTenantId() + const tenantId = getTenantId() let tenantAccount: CloudAccount | undefined if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { tenantAccount = await accounts.getAccountByTenantId(tenantId) @@ -103,23 +106,20 @@ export const handleSaveEvents = async ( await handleAppRoleEvents(user, existingUser) } -const isBuilder = (user: any) => user.builder && user.builder.global -const isAdmin = (user: any) => user.admin && user.admin.global - export const isAddingBuilder = (user: any, existingUser: any) => { - return isAddingPermission(user, existingUser, isBuilder) + return isAddingPermission(user, existingUser, hasBuilderPermissions) } export const isRemovingBuilder = (user: any, existingUser: any) => { - return isRemovingPermission(user, existingUser, isBuilder) + return isRemovingPermission(user, existingUser, hasBuilderPermissions) } const isAddingAdmin = (user: any, existingUser: any) => { - return isAddingPermission(user, existingUser, isAdmin) + return isAddingPermission(user, existingUser, hasAdminPermissions) } const isRemovingAdmin = (user: any, existingUser: any) => { - return isRemovingPermission(user, existingUser, isAdmin) + return isRemovingPermission(user, existingUser, hasAdminPermissions) } const isOnboardingComplete = (user: any, existingUser: any) => { diff --git a/packages/backend-core/src/users/index.ts b/packages/backend-core/src/users/index.ts new file mode 100644 index 0000000000..c11d2a2c62 --- /dev/null +++ b/packages/backend-core/src/users/index.ts @@ -0,0 +1,4 @@ +export * from "./users" +export * from "./utils" +export * from "./lookup" +export { UserDB } from "./db" diff --git a/packages/backend-core/src/users/lookup.ts b/packages/backend-core/src/users/lookup.ts new file mode 100644 index 0000000000..17d0e91d88 --- /dev/null +++ b/packages/backend-core/src/users/lookup.ts @@ -0,0 +1,102 @@ +import { + AccountMetadata, + PlatformUser, + PlatformUserByEmail, + User, +} from "@budibase/types" +import * as dbUtils from "../db" +import { ViewName } from "../constants" + +/** + * Apply a system-wide search on emails: + * - in tenant + * - cross tenant + * - accounts + * return an array of emails that match the supplied emails. + */ +export async function searchExistingEmails(emails: string[]) { + let matchedEmails: string[] = [] + + const existingTenantUsers = await getExistingTenantUsers(emails) + matchedEmails.push(...existingTenantUsers.map(user => user.email)) + + const existingPlatformUsers = await getExistingPlatformUsers(emails) + matchedEmails.push(...existingPlatformUsers.map(user => user._id!)) + + const existingAccounts = await getExistingAccounts(emails) + matchedEmails.push(...existingAccounts.map(account => account.email)) + + return [...new Set(matchedEmails.map(email => email.toLowerCase()))] +} + +// lookup, could be email or userId, either will return a doc +export async function getPlatformUser( + identifier: string +): Promise { + // use the view here and allow to find anyone regardless of casing + // Use lowercase to ensure email login is case insensitive + return (await dbUtils.queryPlatformView(ViewName.PLATFORM_USERS_LOWERCASE, { + keys: [identifier.toLowerCase()], + include_docs: true, + })) as PlatformUser +} + +export async function getExistingTenantUsers( + emails: string[] +): Promise { + const lcEmails = emails.map(email => email.toLowerCase()) + const params = { + keys: lcEmails, + include_docs: true, + } + + const opts = { + arrayResponse: true, + } + + return (await dbUtils.queryGlobalView( + ViewName.USER_BY_EMAIL, + params, + undefined, + opts + )) as User[] +} + +export async function getExistingPlatformUsers( + emails: string[] +): Promise { + const lcEmails = emails.map(email => email.toLowerCase()) + const params = { + keys: lcEmails, + include_docs: true, + } + + const opts = { + arrayResponse: true, + } + return (await dbUtils.queryPlatformView( + ViewName.PLATFORM_USERS_LOWERCASE, + params, + opts + )) as PlatformUserByEmail[] +} + +export async function getExistingAccounts( + emails: string[] +): Promise { + const lcEmails = emails.map(email => email.toLowerCase()) + const params = { + keys: lcEmails, + include_docs: true, + } + + const opts = { + arrayResponse: true, + } + + return (await dbUtils.queryPlatformView( + ViewName.ACCOUNT_BY_EMAIL, + params, + opts + )) as AccountMetadata[] +} diff --git a/packages/backend-core/src/users.ts b/packages/backend-core/src/users/users.ts similarity index 88% rename from packages/backend-core/src/users.ts rename to packages/backend-core/src/users/users.ts index b49058f546..a7e1389920 100644 --- a/packages/backend-core/src/users.ts +++ b/packages/backend-core/src/users/users.ts @@ -11,10 +11,16 @@ import { SEPARATOR, UNICODE_MAX, ViewName, -} from "./db" -import { BulkDocsResponse, SearchUsersRequest, User } from "@budibase/types" -import { getGlobalDB } from "./context" -import * as context from "./context" +} from "../db" +import { + BulkDocsResponse, + SearchUsersRequest, + User, + ContextUser, +} from "@budibase/types" +import { getGlobalDB } from "../context" +import * as context from "../context" +import { user as userCache } from "../cache" type GetOpts = { cleanup?: boolean } @@ -178,7 +184,7 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => { * Performs a starts with search on the global email view. */ export const searchGlobalUsersByEmail = async ( - email: string, + email: string | unknown, opts: any, getOpts?: GetOpts ) => { @@ -248,3 +254,23 @@ export async function getUserCount() { }) return response.total_rows } + +// used to remove the builder/admin permissions, for processing the +// user as an app user (they may have some specific role/group +export function removePortalUserPermissions(user: User | ContextUser) { + delete user.admin + delete user.builder + return user +} + +export function cleanseUserObject(user: User | ContextUser, base?: User) { + delete user.admin + delete user.builder + delete user.roles + if (base) { + user.admin = base.admin + user.builder = base.builder + user.roles = base.roles + } + return user +} diff --git a/packages/backend-core/src/users/utils.ts b/packages/backend-core/src/users/utils.ts new file mode 100644 index 0000000000..af0e8e10c7 --- /dev/null +++ b/packages/backend-core/src/users/utils.ts @@ -0,0 +1,55 @@ +import { CloudAccount } from "@budibase/types" +import * as accountSdk from "../accounts" +import env from "../environment" +import { getPlatformUser } from "./lookup" +import { EmailUnavailableError } from "../errors" +import { getTenantId } from "../context" +import { sdk } from "@budibase/shared-core" +import { getAccountByTenantId } from "../accounts" + +// extract from shared-core to make easily accessible from backend-core +export const isBuilder = sdk.users.isBuilder +export const isAdmin = sdk.users.isAdmin +export const isGlobalBuilder = sdk.users.isGlobalBuilder +export const isAdminOrBuilder = sdk.users.isAdminOrBuilder +export const hasAdminPermissions = sdk.users.hasAdminPermissions +export const hasBuilderPermissions = sdk.users.hasBuilderPermissions +export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions + +export async function validateUniqueUser(email: string, tenantId: string) { + // check budibase users in other tenants + if (env.MULTI_TENANCY) { + const tenantUser = await getPlatformUser(email) + if (tenantUser != null && tenantUser.tenantId !== tenantId) { + throw new EmailUnavailableError(email) + } + } + + // check root account users in account portal + if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { + const account = await accountSdk.getAccount(email) + if (account && account.verified && account.tenantId !== tenantId) { + throw new EmailUnavailableError(email) + } + } +} + +/** + * For the given user id's, return the account holder if it is in the ids. + */ +export async function getAccountHolderFromUserIds( + userIds: string[] +): Promise { + if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { + const tenantId = getTenantId() + const account = await getAccountByTenantId(tenantId) + if (!account) { + throw new Error(`Account not found for tenantId=${tenantId}`) + } + + const budibaseUserId = account.budibaseUserId + if (userIds.includes(budibaseUserId)) { + return account + } + } +} diff --git a/packages/backend-core/tests/core/utilities/jestUtils.ts b/packages/backend-core/tests/core/utilities/jestUtils.ts index d84eac548c..4a3da8db8c 100644 --- a/packages/backend-core/tests/core/utilities/jestUtils.ts +++ b/packages/backend-core/tests/core/utilities/jestUtils.ts @@ -1,3 +1,5 @@ +import { db } from "../../../src" + export function expectFunctionWasCalledTimesWith( jestFunction: any, times: number, @@ -7,3 +9,22 @@ export function expectFunctionWasCalledTimesWith( jestFunction.mock.calls.filter((call: any) => call[0] === argument).length ).toBe(times) } + +export const expectAnyInternalColsAttributes: { + [K in (typeof db.CONSTANT_INTERNAL_ROW_COLS)[number]]: any +} = { + tableId: expect.anything(), + type: expect.anything(), + _id: expect.anything(), + _rev: expect.anything(), + createdAt: expect.anything(), + updatedAt: expect.anything(), +} + +export const expectAnyExternalColsAttributes: { + [K in (typeof db.CONSTANT_EXTERNAL_ROW_COLS)[number]]: any +} = { + tableId: expect.anything(), + _id: expect.anything(), + _rev: expect.anything(), +} diff --git a/packages/backend-core/tests/core/utilities/mocks/events.ts b/packages/backend-core/tests/core/utilities/mocks/events.ts index 81de1f8175..fef730768a 100644 --- a/packages/backend-core/tests/core/utilities/mocks/events.ts +++ b/packages/backend-core/tests/core/utilities/mocks/events.ts @@ -1,5 +1,3 @@ -import * as events from "../../../../src/events" - beforeAll(async () => { const processors = await import("../../../../src/events/processors") const events = await import("../../../../src/events") diff --git a/packages/backend-core/tests/core/utilities/mocks/licenses.ts b/packages/backend-core/tests/core/utilities/mocks/licenses.ts index 4272e78eb8..14a1f1f4d3 100644 --- a/packages/backend-core/tests/core/utilities/mocks/licenses.ts +++ b/packages/backend-core/tests/core/utilities/mocks/licenses.ts @@ -1,5 +1,5 @@ import { Feature, License, Quotas } from "@budibase/types" -import _ from "lodash" +import cloneDeep from "lodash/cloneDeep" let CLOUD_FREE_LICENSE: License let UNLIMITED_LICENSE: License @@ -58,7 +58,7 @@ export const useCloudFree = () => { // FEATURES const useFeature = (feature: Feature) => { - const license = _.cloneDeep(UNLIMITED_LICENSE) + const license = cloneDeep(UNLIMITED_LICENSE) const opts: UseLicenseOpts = { features: [feature], } @@ -86,6 +86,10 @@ export const useAuditLogs = () => { return useFeature(Feature.AUDIT_LOGS) } +export const usePublicApiUserRoles = () => { + return useFeature(Feature.USER_ROLE_PUBLIC_API) +} + export const useScimIntegration = () => { return useFeature(Feature.SCIM) } @@ -94,10 +98,14 @@ export const useSyncAutomations = () => { return useFeature(Feature.SYNC_AUTOMATIONS) } +export const useAppBuilders = () => { + return useFeature(Feature.APP_BUILDERS) +} + // QUOTAS export const setAutomationLogsQuota = (value: number) => { - const license = _.cloneDeep(UNLIMITED_LICENSE) + const license = cloneDeep(UNLIMITED_LICENSE) license.quotas.constant.automationLogRetentionDays.value = value return useLicense(license) } diff --git a/packages/backend-core/tests/core/utilities/structures/accounts.ts b/packages/backend-core/tests/core/utilities/structures/accounts.ts index 807153cd09..67e4411ea3 100644 --- a/packages/backend-core/tests/core/utilities/structures/accounts.ts +++ b/packages/backend-core/tests/core/utilities/structures/accounts.ts @@ -11,9 +11,9 @@ import { CreateAccount, CreatePassswordAccount, } from "@budibase/types" -import _ from "lodash" +import sample from "lodash/sample" -export const account = (): Account => { +export const account = (partial: Partial = {}): Account => { return { accountId: uuid(), tenantId: generator.word(), @@ -29,6 +29,7 @@ export const account = (): Account => { size: "10+", profession: "Software Engineer", quotaUsage: quotas.usage(), + ...partial, } } @@ -45,13 +46,11 @@ export const cloudAccount = (): CloudAccount => { } function providerType(): AccountSSOProviderType { - return _.sample( - Object.values(AccountSSOProviderType) - ) as AccountSSOProviderType + return sample(Object.values(AccountSSOProviderType)) as AccountSSOProviderType } function provider(): AccountSSOProvider { - return _.sample(Object.values(AccountSSOProvider)) as AccountSSOProvider + return sample(Object.values(AccountSSOProvider)) as AccountSSOProvider } export function ssoAccount(account: Account = cloudAccount()): SSOAccount { diff --git a/packages/backend-core/tests/core/utilities/structures/db.ts b/packages/backend-core/tests/core/utilities/structures/db.ts index 31a52dce8b..87325573eb 100644 --- a/packages/backend-core/tests/core/utilities/structures/db.ts +++ b/packages/backend-core/tests/core/utilities/structures/db.ts @@ -1,4 +1,4 @@ -import { structures } from ".." +import { generator } from "./generator" import { newid } from "../../../../src/docIds/newid" export function id() { @@ -6,7 +6,7 @@ export function id() { } export function rev() { - return `${structures.generator.character({ + return `${generator.character({ numeric: true, - })}-${structures.uuid().replace(/-/, "")}` + })}-${generator.guid().replace(/-/, "")}` } diff --git a/packages/backend-core/tests/core/utilities/structures/documents/index.ts b/packages/backend-core/tests/core/utilities/structures/documents/index.ts new file mode 100644 index 0000000000..c3bfba3597 --- /dev/null +++ b/packages/backend-core/tests/core/utilities/structures/documents/index.ts @@ -0,0 +1 @@ +export * from "./platform" diff --git a/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts b/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts new file mode 100644 index 0000000000..98a6314999 --- /dev/null +++ b/packages/backend-core/tests/core/utilities/structures/documents/platform/index.ts @@ -0,0 +1 @@ +export * as installation from "./installation" diff --git a/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts b/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts new file mode 100644 index 0000000000..711c6cf14f --- /dev/null +++ b/packages/backend-core/tests/core/utilities/structures/documents/platform/installation.ts @@ -0,0 +1,12 @@ +import { generator } from "../../generator" +import { Installation } from "@budibase/types" +import * as db from "../../db" + +export function install(): Installation { + return { + _id: "install", + _rev: db.rev(), + installId: generator.guid(), + version: generator.string(), + } +} diff --git a/packages/backend-core/tests/core/utilities/structures/index.ts b/packages/backend-core/tests/core/utilities/structures/index.ts index 2c094f43a7..1a49e912fc 100644 --- a/packages/backend-core/tests/core/utilities/structures/index.ts +++ b/packages/backend-core/tests/core/utilities/structures/index.ts @@ -2,6 +2,7 @@ export * from "./common" export * as accounts from "./accounts" export * as apps from "./apps" export * as db from "./db" +export * as docs from "./documents" export * as koa from "./koa" export * as licenses from "./licenses" export * as plugins from "./plugins" diff --git a/packages/backend-core/tests/core/utilities/structures/licenses.ts b/packages/backend-core/tests/core/utilities/structures/licenses.ts index 22e73f2871..5cce84edfd 100644 --- a/packages/backend-core/tests/core/utilities/structures/licenses.ts +++ b/packages/backend-core/tests/core/utilities/structures/licenses.ts @@ -3,6 +3,8 @@ import { Customer, Feature, License, + OfflineIdentifier, + OfflineLicense, PlanModel, PlanType, PriceDuration, @@ -11,6 +13,7 @@ import { Quotas, Subscription, } from "@budibase/types" +import { generator } from "./generator" export function price(): PurchasedPrice { return { @@ -127,15 +130,15 @@ export function subscription(): Subscription { } } -export const license = ( - opts: { - quotas?: Quotas - plan?: PurchasedPlan - planType?: PlanType - features?: Feature[] - billing?: Billing - } = {} -): License => { +interface GenerateLicenseOpts { + quotas?: Quotas + plan?: PurchasedPlan + planType?: PlanType + features?: Feature[] + billing?: Billing +} + +export const license = (opts: GenerateLicenseOpts = {}): License => { return { features: opts.features || [], quotas: opts.quotas || quotas(), @@ -143,3 +146,22 @@ export const license = ( billing: opts.billing || billing(), } } + +export function offlineLicense(opts: GenerateLicenseOpts = {}): OfflineLicense { + const base = license(opts) + return { + ...base, + expireAt: new Date().toISOString(), + identifier: offlineIdentifier(), + } +} + +export function offlineIdentifier( + installId: string = generator.guid(), + tenantId: string = generator.guid() +): OfflineIdentifier { + return { + installId, + tenantId, + } +} diff --git a/packages/backend-core/tests/core/utilities/structures/scim.ts b/packages/backend-core/tests/core/utilities/structures/scim.ts index 741cff165e..80f41c605d 100644 --- a/packages/backend-core/tests/core/utilities/structures/scim.ts +++ b/packages/backend-core/tests/core/utilities/structures/scim.ts @@ -1,7 +1,6 @@ import { ScimCreateGroupRequest, ScimCreateUserRequest } from "@budibase/types" import { uuid } from "./common" import { generator } from "./generator" -import _ from "lodash" interface CreateUserRequestFields { externalId: string @@ -20,10 +19,10 @@ export function createUserRequest(userData?: Partial) { username: generator.name(), } - const { externalId, email, firstName, lastName, username } = _.assign( - defaultValues, - userData - ) + const { externalId, email, firstName, lastName, username } = { + ...defaultValues, + ...userData, + } let user: ScimCreateUserRequest = { schemas: [ diff --git a/packages/backend-core/tests/core/utilities/structures/sso.ts b/packages/backend-core/tests/core/utilities/structures/sso.ts index 9da9c82223..4d13635f09 100644 --- a/packages/backend-core/tests/core/utilities/structures/sso.ts +++ b/packages/backend-core/tests/core/utilities/structures/sso.ts @@ -15,7 +15,7 @@ import { generator } from "./generator" import { email, uuid } from "./common" import * as shared from "./shared" import { user } from "./shared" -import _ from "lodash" +import sample from "lodash/sample" export function OAuth(): OAuth2 { return { @@ -47,7 +47,7 @@ export function authDetails(userDoc?: User): SSOAuthDetails { } export function providerType(): SSOProviderType { - return _.sample(Object.values(SSOProviderType)) as SSOProviderType + return sample(Object.values(SSOProviderType)) as SSOProviderType } export function ssoProfile(user?: User): SSOProfile { diff --git a/packages/backend-core/tests/core/utilities/structures/users.ts b/packages/backend-core/tests/core/utilities/structures/users.ts index 7a6b4f0d80..0a4f2e8b54 100644 --- a/packages/backend-core/tests/core/utilities/structures/users.ts +++ b/packages/backend-core/tests/core/utilities/structures/users.ts @@ -1,5 +1,6 @@ import { AdminUser, + AdminOnlyUser, BuilderUser, SSOAuthDetails, SSOUser, @@ -21,6 +22,15 @@ export const adminUser = (userProps?: any): AdminUser => { } } +export const adminOnlyUser = (userProps?: any): AdminOnlyUser => { + return { + ...user(userProps), + admin: { + global: true, + }, + } +} + export const builderUser = (userProps?: any): BuilderUser => { return { ...user(userProps), @@ -30,6 +40,15 @@ export const builderUser = (userProps?: any): BuilderUser => { } } +export const appBuilderUser = (appId: string, userProps?: any): BuilderUser => { + return { + ...user(userProps), + builder: { + apps: [appId], + }, + } +} + export function ssoUser( opts: { user?: any; details?: SSOAuthDetails } = {} ): SSOUser { diff --git a/packages/backend-core/tests/core/utilities/testContainerUtils.ts b/packages/backend-core/tests/core/utilities/testContainerUtils.ts index f6c702f7ef..7da6cbc777 100644 --- a/packages/backend-core/tests/core/utilities/testContainerUtils.ts +++ b/packages/backend-core/tests/core/utilities/testContainerUtils.ts @@ -32,8 +32,8 @@ function getTestContainerSettings( ): string | null { const entry = Object.entries(global).find( ([k]) => - k.includes(`_${serverName.toUpperCase()}`) && - k.includes(`_${key.toUpperCase()}__`) + k.includes(`${serverName.toUpperCase()}`) && + k.includes(`${key.toUpperCase()}`) ) if (!entry) { return null @@ -67,27 +67,14 @@ function getContainerInfo(containerName: string, port: number) { } function getCouchConfig() { - return getContainerInfo("couchdb-service", 5984) -} - -function getMinioConfig() { - return getContainerInfo("minio-service", 9000) -} - -function getRedisConfig() { - return getContainerInfo("redis-service", 6379) + return getContainerInfo("couchdb", 5984) } export function setupEnv(...envs: any[]) { - const couch = getCouchConfig(), - minio = getCouchConfig(), - redis = getRedisConfig() + const couch = getCouchConfig() const configs = [ { key: "COUCH_DB_PORT", value: couch.port }, { key: "COUCH_DB_URL", value: couch.url }, - { key: "MINIO_PORT", value: minio.port }, - { key: "MINIO_URL", value: minio.url }, - { key: "REDIS_URL", value: redis.url }, ] for (const config of configs.filter(x => !!x.value)) { diff --git a/packages/backend-core/tsconfig.build.json b/packages/backend-core/tsconfig.build.json index bfbed31e23..c714f4d942 100644 --- a/packages/backend-core/tsconfig.build.json +++ b/packages/backend-core/tsconfig.build.json @@ -12,7 +12,11 @@ "declaration": true, "types": ["node", "jest"], "outDir": "dist", - "skipLibCheck": true + "skipLibCheck": true, + "paths": { + "@budibase/types": ["../types/src"], + "@budibase/shared-core": ["../shared-core/src"] + } }, "include": ["**/*.js", "**/*.ts"], "exclude": [ diff --git a/packages/backend-core/tsconfig.json b/packages/backend-core/tsconfig.json index 2b1419b051..33e37179d7 100644 --- a/packages/backend-core/tsconfig.json +++ b/packages/backend-core/tsconfig.json @@ -1,12 +1,4 @@ { "extends": "./tsconfig.build.json", - "compilerOptions": { - "composite": true, - "baseUrl": ".", - "paths": { - "@budibase/types": ["../types/src"] - } - }, - "exclude": ["node_modules", "dist"] } diff --git a/packages/bbui/package.json b/packages/bbui/package.json index b03c83d71b..0b87960ab5 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -85,7 +85,8 @@ "dayjs": "^1.10.4", "easymde": "^2.16.1", "svelte-flatpickr": "3.2.3", - "svelte-portal": "^1.0.0" + "svelte-portal": "^1.0.0", + "svelte-dnd-action": "^0.9.8" }, "resolutions": { "loader-utils": "1.4.1" @@ -96,13 +97,13 @@ "dependsOn": [ { "projects": [ - "@budibase/string-templates" + "@budibase/string-templates", + "@budibase/shared-core" ], "target": "build" } ] } } - }, - "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" + } } diff --git a/packages/bbui/src/Actions/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.js index d73b3b4d6a..4a5ef890bf 100644 --- a/packages/bbui/src/Actions/position_dropdown.js +++ b/packages/bbui/src/Actions/position_dropdown.js @@ -63,7 +63,7 @@ export default function positionDropdown(element, opts) { // Apply styles Object.entries(styles).forEach(([style, value]) => { - if (value) { + if (value != null) { element.style[style] = `${value.toFixed(0)}px` } else { element.style[style] = null diff --git a/packages/bbui/src/Drawer/Drawer.svelte b/packages/bbui/src/Drawer/Drawer.svelte index 782c3c6ed2..4ff4df854b 100644 --- a/packages/bbui/src/Drawer/Drawer.svelte +++ b/packages/bbui/src/Drawer/Drawer.svelte @@ -30,6 +30,7 @@ setContext("drawer-actions", { hide, show, + headless, }) const easeInOutQuad = x => { diff --git a/packages/bbui/src/Form/Core/CheckboxGroup.svelte b/packages/bbui/src/Form/Core/CheckboxGroup.svelte index 640d5d99cd..2b8a1e438a 100644 --- a/packages/bbui/src/Form/Core/CheckboxGroup.svelte +++ b/packages/bbui/src/Form/Core/CheckboxGroup.svelte @@ -12,23 +12,24 @@ export let getOptionValue = option => option const dispatch = createEventDispatcher() + const onChange = e => { - let tempValue = value - let isChecked = e.target.checked - if (!tempValue.includes(e.target.value) && isChecked) { - tempValue.push(e.target.value) + const optionValue = e.target.value + if (e.target.checked && !value.includes(optionValue)) { + dispatch("change", [...value, optionValue]) + } else { + dispatch( + "change", + value.filter(x => x !== optionValue) + ) } - value = tempValue - dispatch( - "change", - tempValue.filter(val => val !== e.target.value || isChecked) - ) }
{#if options && Array.isArray(options)} {#each options as option} + {@const optionValue = getOptionValue(option)}
{#if tooltip && showTooltip}
- +
{/if}
@@ -80,15 +80,14 @@ position: absolute; pointer-events: none; left: 50%; - top: calc(100% + 4px); - width: 100vw; - max-width: 150px; + bottom: calc(100% + 4px); transform: translateX(-50%); text-align: center; + z-index: 1; } .spectrum-Icon--sizeXS { - width: 10px; - height: 10px; + width: var(--spectrum-global-dimension-size-150); + height: var(--spectrum-global-dimension-size-150); } diff --git a/packages/bbui/src/IconPicker/IconPicker.svelte b/packages/bbui/src/IconPicker/IconPicker.svelte index 2b42da61b1..b3cc72daa3 100644 --- a/packages/bbui/src/IconPicker/IconPicker.svelte +++ b/packages/bbui/src/IconPicker/IconPicker.svelte @@ -1,5 +1,4 @@ + +
+
+ {#each options as option, idx (option.id)} +
+
+ +
+
+
openColorPickerPopover(idx, e.target)} + > + +
+ {#each colorsArray as color} +
handleColorChange(option.name, color, idx)} + style="--color:{color};" + class="circle circle-hover" + /> + {/each} +
+ +
+
+
+ handleNameChange(option.name, idx, e.target.value)} + value={option.name} + placeholder="Option name" + /> +
+
+ +
+
+ {/each} +
+
+ +
Add option
+
+
+ + diff --git a/packages/bbui/src/Popover/Popover.svelte b/packages/bbui/src/Popover/Popover.svelte index 3b0ac9a74f..1259f2704f 100644 --- a/packages/bbui/src/Popover/Popover.svelte +++ b/packages/bbui/src/Popover/Popover.svelte @@ -21,6 +21,7 @@ export let offset = 5 export let customHeight export let animate = true + export let customZindex $: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum" @@ -85,8 +86,9 @@ }} on:keydown={handleEscape} class="spectrum-Popover is-open" + class:customZindex role="presentation" - style="height: {customHeight}" + style="height: {customHeight}; --customZindex: {customZindex};" transition:fly|local={{ y: -20, duration: animate ? 200 : 0 }} > @@ -100,4 +102,8 @@ border-color: var(--spectrum-global-color-gray-300); overflow: auto; } + + .customZindex { + z-index: var(--customZindex) !important; + } diff --git a/packages/bbui/src/index.js b/packages/bbui/src/index.js index 97762d2b3a..cda6b5acbf 100644 --- a/packages/bbui/src/index.js +++ b/packages/bbui/src/index.js @@ -84,7 +84,7 @@ export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte export { default as Slider } from "./Form/Slider.svelte" export { default as Accordion } from "./Accordion/Accordion.svelte" export { default as File } from "./Form/File.svelte" - +export { default as OptionSelectDnD } from "./OptionSelectDnD/OptionSelectDnD.svelte" // Renderers export { default as BoldRenderer } from "./Table/BoldRenderer.svelte" export { default as CodeRenderer } from "./Table/CodeRenderer.svelte" diff --git a/packages/builder/package.json b/packages/builder/package.json index 646bb144df..db8ef86086 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -101,14 +101,14 @@ "@rollup/plugin-replace": "^2.4.2", "@roxi/routify": "2.18.5", "@sveltejs/vite-plugin-svelte": "1.0.1", - "@testing-library/jest-dom": "^5.11.10", + "@testing-library/jest-dom": "5.17.0", "@testing-library/svelte": "^3.2.2", - "babel-jest": "^26.6.3", + "babel-jest": "29.6.2", "cypress": "^9.3.1", "cypress-multi-reporters": "^1.6.0", "cypress-terminal-report": "^1.4.1", "identity-obj-proxy": "^3.0.0", - "jest": "^26.6.3", + "jest": "29.6.2", "jsdom": "^21.1.1", "mochawesome": "^7.1.3", "mochawesome-merge": "^4.2.1", @@ -133,8 +133,17 @@ "dependsOn": [ { "projects": [ - "@budibase/string-templates", - "@budibase/shared-core" + "@budibase/string-templates" + ], + "target": "build" + } + ] + }, + "dev:builder": { + "dependsOn": [ + { + "projects": [ + "@budibase/string-templates" ], "target": "build" } @@ -144,7 +153,6 @@ "dependsOn": [ { "projects": [ - "@budibase/shared-core", "@budibase/string-templates" ], "target": "build" @@ -152,6 +160,5 @@ ] } } - }, - "gitHead": "115189f72a850bfb52b65ec61d932531bf327072" + } } diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index e9c8643bce..bbe116721a 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -491,6 +491,7 @@ const getSelectedRowsBindings = asset => { readableBinding: `${table._instanceName}.Selected rows`, category: "Selected rows", icon: "ViewRow", + display: { name: table._instanceName }, })) ) @@ -506,6 +507,7 @@ const getSelectedRowsBindings = asset => { )}.${makePropSafe("selectedRows")}`, readableBinding: `${block._instanceName}.Selected rows`, category: "Selected rows", + display: { name: block._instanceName }, })) ) } diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index 2ca8057b48..eecccab7a6 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -4,7 +4,7 @@ import { getTemporalStore } from "./store/temporal" import { getThemeStore } from "./store/theme" import { getUserStore } from "./store/users" import { getDeploymentStore } from "./store/deployments" -import { derived } from "svelte/store" +import { derived, writable } from "svelte/store" import { findComponent, findComponentPath } from "./componentUtils" import { RoleUtils } from "@budibase/frontend-core" import { createHistoryStore } from "builderStore/store/history" @@ -61,6 +61,12 @@ export const selectedLayout = derived(store, $store => { export const selectedComponent = derived( [store, selectedScreen], ([$store, $selectedScreen]) => { + if ( + $selectedScreen && + $store.selectedComponentId?.startsWith(`${$selectedScreen._id}-`) + ) { + return $selectedScreen?.props + } if (!$selectedScreen || !$store.selectedComponentId) { return null } @@ -141,3 +147,5 @@ export const userSelectedResourceMap = derived(userStore, $userStore => { export const isOnlyUser = derived(userStore, $userStore => { return $userStore.length < 2 }) + +export const screensHeight = writable("210px") diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index f312a58e97..8f4f6cc2c4 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -225,7 +225,6 @@ export const getFrontendStore = () => { // Select new screen store.update(state => { state.selectedScreenId = screen._id - state.selectedComponentId = screen.props?._id return state }) }, @@ -769,9 +768,13 @@ export const getFrontendStore = () => { else { await store.actions.screens.patch(screen => { // Find the selected component + let selectedComponentId = state.selectedComponentId + if (selectedComponentId.startsWith(`${screen._id}-`)) { + selectedComponentId = screen?.props._id + } const currentComponent = findComponent( screen.props, - state.selectedComponentId + selectedComponentId ) if (!currentComponent) { return false @@ -994,12 +997,20 @@ export const getFrontendStore = () => { const componentId = state.selectedComponentId const screen = get(selectedScreen) const parent = findComponentParent(screen.props, componentId) - - // Check we aren't right at the top of the tree const index = parent?._children.findIndex(x => x._id === componentId) - if (!parent || componentId === screen.props._id) { + + // Check for screen and navigation component edge cases + const screenComponentId = `${screen._id}-screen` + const navComponentId = `${screen._id}-navigation` + if (componentId === screenComponentId) { return null } + if (componentId === navComponentId) { + return screenComponentId + } + if (parent._id === screen.props._id && index === 0) { + return navComponentId + } // If we have siblings above us, choose the sibling or a descendant if (index > 0) { @@ -1021,12 +1032,20 @@ export const getFrontendStore = () => { return parent._id }, getNext: () => { + const state = get(store) const component = get(selectedComponent) const componentId = component?._id const screen = get(selectedScreen) const parent = findComponentParent(screen.props, componentId) const index = parent?._children.findIndex(x => x._id === componentId) + // Check for screen and navigation component edge cases + const screenComponentId = `${screen._id}-screen` + const navComponentId = `${screen._id}-navigation` + if (state.selectedComponentId === screenComponentId) { + return navComponentId + } + // If we have children, select first child if (component._children?.length) { return component._children[0]._id diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index cece075860..6b57fe3d18 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -108,7 +108,10 @@ /****************************************************/ const getInputData = (testData, blockInputs) => { - let newInputData = testData || blockInputs + // Test data is not cloned for reactivity + let newInputData = testData || cloneDeep(blockInputs) + + // Ensures the app action fields are populated if (block.event === "app:trigger" && !newInputData?.fields) { newInputData = cloneDeep(blockInputs) } diff --git a/packages/builder/src/components/automation/SetupPanel/QueryParamSelector.svelte b/packages/builder/src/components/automation/SetupPanel/QueryParamSelector.svelte index 9c47178b0e..250d235266 100644 --- a/packages/builder/src/components/automation/SetupPanel/QueryParamSelector.svelte +++ b/packages/builder/src/components/automation/SetupPanel/QueryParamSelector.svelte @@ -50,6 +50,7 @@ type="string" {bindings} fillWidth={true} + updateOnChange={false} /> {/each}
diff --git a/packages/builder/src/components/backend/DataTable/DataTable.svelte b/packages/builder/src/components/backend/DataTable/DataTable.svelte index 33db9b60e3..38eb87aa73 100644 --- a/packages/builder/src/components/backend/DataTable/DataTable.svelte +++ b/packages/builder/src/components/backend/DataTable/DataTable.svelte @@ -1,5 +1,5 @@ - - - + + {#if mounted} + + {/if} {:else if editableColumn.type === "options"} - {:else if editableColumn.type === "longform"}
- +
+ + + + +
+
{:else if editableColumn.type === "array"} - {:else if editableColumn.type === "datetime" && !editableColumn.autocolumn} - - +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
{#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"}
- +
+ + + + +
{/if} {:else if editableColumn.type === "number" && !editableColumn.autocolumn} - - +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
{:else if editableColumn.type === "link"} option.label} - getOptionValue={option => option.value} - tooltip="Dynamic formula are calculated when retrieved, but cannot be filtered or sorted by, +
+
+ +
+
+ + + diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte index 01964aed75..c18ba313e0 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditUser.svelte @@ -95,9 +95,9 @@ {#if !creating}
A user's email, role, first and last names cannot be changed from within - the app builder. Please go to the user portal to do this. + the app builder. Please go to the + user portal + to do this.
{/if} - import { getContext, onMount } from "svelte" - import { Modal } from "@budibase/bbui" + import { getContext } from "svelte" import CreateEditColumn from "components/backend/DataTable/modals/CreateEditColumn.svelte" - const { rows, subscribe } = getContext("grid") - - let modal - - onMount(() => subscribe("add-column", modal.show)) + const { rows } = getContext("grid") - - - + diff --git a/packages/builder/src/components/backend/DataTable/modals/grid/GridEditColumnModal.svelte b/packages/builder/src/components/backend/DataTable/modals/grid/GridEditColumnModal.svelte index 3297037265..020c58d19f 100644 --- a/packages/builder/src/components/backend/DataTable/modals/grid/GridEditColumnModal.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/grid/GridEditColumnModal.svelte @@ -1,24 +1,19 @@ - - - + diff --git a/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte b/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte index 53fcf56e7f..36c6a32801 100644 --- a/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte +++ b/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte @@ -1,5 +1,5 @@
@@ -119,10 +127,8 @@ on:change={handleFile} />
{/each} @@ -167,7 +176,7 @@ {#if parameters.type === "specific"} - + (parameters.number = e.detail)} + /> {/if}
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte index 109eb9a956..e4a5f171ff 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/DeleteRow.svelte @@ -1,5 +1,5 @@
- - Please specify one or more rows to delete. +
+ + + {/if} +
diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ExecuteQuery.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ExecuteQuery.svelte index ba72fd2ed7..a085d86507 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ExecuteQuery.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/ExecuteQuery.svelte @@ -73,9 +73,12 @@ {#if query?.parameters?.length > 0}
{ + parameters.queryParams = { ...v.detail } + }} /> { - tmpQueryParams = value.queryParams + tmpQueryParams = { ...value.queryParams } drawer.show() } const getQueryValue = queries => { - value = queries.find(q => q._id === value._id) || value - return value + return queries.find(q => q._id === value._id) || value } const saveQueryParams = () => { @@ -176,7 +175,10 @@ {#if getQueryParams(value).length > 0} { + tmpQueryParams = { ...v.detail } + }} queryBindings={getQueryParams(value)} bind:bindings /> diff --git a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte index 4b1ab0d68a..ef8699824e 100644 --- a/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte +++ b/packages/builder/src/components/design/settings/controls/FilterEditor/FilterDrawer.svelte @@ -17,7 +17,7 @@ import { generate } from "shortid" import { LuceneUtils, Constants } from "@budibase/frontend-core" import { getFields } from "helpers/searchFields" - import { createEventDispatcher } from "svelte" + import { createEventDispatcher, onMount } from "svelte" export let schemaFields export let filters = [] @@ -35,22 +35,28 @@ { value: "and", label: "Match all filters" }, { value: "or", label: "Match any filter" }, ] + const onEmptyOptions = [ + { value: "all", label: "Return all table rows" }, + { value: "none", label: "Return no rows" }, + ] let rawFilters let matchAny = false + let onEmptyFilter = "all" $: parseFilters(filters) - $: dispatch("change", enrichFilters(rawFilters, matchAny)) + $: dispatch("change", enrichFilters(rawFilters, matchAny, onEmptyFilter)) $: enrichedSchemaFields = getFields(schemaFields || [], { allowLinks: true }) $: fieldOptions = enrichedSchemaFields.map(field => field.name) || [] $: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"] - // Remove field key prefixes and determine whether to use the "match all" - // or "match any" behaviour + // Remove field key prefixes and determine which behaviours to use const parseFilters = filters => { matchAny = filters?.find(filter => filter.operator === "allOr") != null + onEmptyFilter = + filters?.find(filter => filter.onEmptyFilter)?.onEmptyFilter ?? "all" rawFilters = (filters || []) - .filter(filter => filter.operator !== "allOr") + .filter(filter => filter.operator !== "allOr" && !filter.onEmptyFilter) .map(filter => { const { field } = filter let newFilter = { ...filter } @@ -64,9 +70,18 @@ }) } + onMount(() => { + parseFilters(filters) + rawFilters.forEach(filter => { + filter.type = + schemaFields.find(field => field.name === filter.field)?.type || + filter.type + }) + }) + // Add field key prefixes and a special metadata filter object to indicate - // whether to use the "match all" or "match any" behaviour - const enrichFilters = (rawFilters, matchAny) => { + // how to handle filter behaviour + const enrichFilters = (rawFilters, matchAny, onEmptyFilter) => { let count = 1 return rawFilters .filter(filter => filter.field) @@ -75,6 +90,7 @@ field: `${count++}:${filter.field}`, })) .concat(matchAny ? [{ operator: "allOr" }] : []) + .concat([{ onEmptyFilter }]) } const addFilter = () => { @@ -186,6 +202,17 @@ on:change={e => (matchAny = e.detail === "or")} placeholder={null} /> + {#if datasource?.type === "table"} + update("navWidth", e.detail)} + /> + {/if} +
+ +
+ update("hideLogo", !e.detail)} + /> + {#if !$store.navigation.hideLogo} +
+ +
+ update("logoUrl", e.detail)} + updateOnChange={false} + /> + {/if} +
+ +
+ update("hideTitle", !e.detail)} + /> + {#if !$store.navigation.hideTitle} +
+ +
+ update("title", e.detail)} + updateOnChange={false} + /> + {/if} +
+ +
+ update("navBackground", e.detail)} + /> +
+ +
+ update("navTextColor", e.detail)} + /> +
+
+ {/if} + + + diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenSettingsPanel/GeneralPane.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPane.svelte similarity index 75% rename from packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenSettingsPanel/GeneralPane.svelte rename to packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPane.svelte index 2ed0552252..e6200272be 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenSettingsPanel/GeneralPane.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/GeneralPane.svelte @@ -1,22 +1,10 @@ - - - + + {#if $selectedScreen.layoutId} {/if} - - {#each screenSettings as setting (setting.key)} - setScreenSetting(setting, val)} - props={{ ...setting.props, error: errors[setting.key] }} - {bindings} - /> - {/each} - - + {#each screenSettings as setting (setting.key)} + setScreenSetting(setting, val)} + props={{ ...setting.props, error: errors[setting.key] }} + {bindings} + /> + {/each} - - - + diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenSettingsPanel/ThemePane.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/ThemePane.svelte similarity index 100% rename from packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenSettingsPanel/ThemePane.svelte rename to packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/ThemePane.svelte diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenSettingsPanel/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/index.svelte similarity index 100% rename from packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenSettingsPanel/index.svelte rename to packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/index.svelte diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenSettingsPanel/theme/AppThemeSelect.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/theme/AppThemeSelect.svelte similarity index 100% rename from packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenSettingsPanel/theme/AppThemeSelect.svelte rename to packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/theme/AppThemeSelect.svelte diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenSettingsPanel/theme/ButtonRoundnessSelect.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/theme/ButtonRoundnessSelect.svelte similarity index 100% rename from packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenSettingsPanel/theme/ButtonRoundnessSelect.svelte rename to packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Screen/theme/ButtonRoundnessSelect.svelte diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_layout.svelte new file mode 100644 index 0000000000..03a0aad9ba --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/_layout.svelte @@ -0,0 +1,52 @@ + + +{#if routeComponentId === `${$store.selectedScreenId}-screen`} + +{:else if routeComponentId === `${$store.selectedScreenId}-navigation`} + +{:else} + +{/if} + diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/index.svelte new file mode 100644 index 0000000000..0ff63d1ead --- /dev/null +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/index.svelte @@ -0,0 +1 @@ + diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/NewComponentPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte similarity index 98% rename from packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/NewComponentPanel.svelte rename to packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte index 7dca5b792b..248bbc8141 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/NewComponentPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/NewComponentPanel.svelte @@ -31,6 +31,10 @@ $: orderMap = createComponentOrderMap(componentList) const getAllowedComponents = (allComponents, screen, component) => { + // Default to using the root screen container if no component specified + if (!component) { + component = screen.props + } const path = findComponentPath(screen?.props, component?._id) if (!path?.length) { return [] diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/componentStructure.json b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json similarity index 91% rename from packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/componentStructure.json rename to packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json index de0270f6be..ee0f99a074 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/componentStructure.json +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/_components/componentStructure.json @@ -75,6 +75,14 @@ { "name": "Chart", "icon": "GraphBarVertical", - "children": ["bar", "line", "area", "candlestick", "pie", "donut"] + "children": [ + "bar", + "line", + "area", + "candlestick", + "pie", + "donut", + "histogram" + ] } ] diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/index.svelte similarity index 100% rename from packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/index.svelte rename to packages/builder/src/pages/builder/app/[application]/design/[screenId]/[componentId]/new/index.svelte diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte index 785b221239..09f97302fd 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_components/AppPanel.svelte @@ -1,32 +1,16 @@
- +
+ Screens +
+
+ +
+
+ +
+
+
+ {#if filteredScreens?.length} + {#each filteredScreens as screen (screen._id)} + store.actions.screens.select(screen._id)} + rightAlignIcon + showTooltip + selectedBy={$userSelectedResourceMap[screen._id]} + > + +
+ +
+
+ {/each} + {:else} + +
+ There aren't any screens matching that route +
+
+ {/if} +
+ +
screensHeight.set("210px")} + /> +
+ + diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_fallback.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_fallback.svelte deleted file mode 100644 index 00165e4ee9..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_fallback.svelte +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_layout.svelte index 72d959c18b..0e630b4f39 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/_layout.svelte @@ -1,14 +1,10 @@ -
-
- - $goto("./screens")} - /> - $goto("./components")} - /> - $goto("./navigation")} - /> - {#if $store.layouts?.length} - $goto("./layouts")} - /> - {/if} - -
- -
- {#if $selectedScreen} - +{#if $selectedScreen} +
+
+ - {/if} + +
-
+{/if} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentListPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentListPanel.svelte deleted file mode 100644 index 9513753d76..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentListPanel.svelte +++ /dev/null @@ -1,90 +0,0 @@ - - - -
- -
- -
    -
  • - { - $store.selectedComponentId = $selectedScreen?.props._id - }} - id={`component-${$selectedScreen?.props._id}`} - selectedBy={$userSelectedResourceMap[$selectedScreen?.props._id]} - > - - - - - - {#if $dndStore.dragging && $dndStore.valid} - - {#if $dndStore.dropPosition !== DropPosition.INSIDE} - - {/if} - {/if} -
  • -
-
-
- - - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_layout.svelte deleted file mode 100644 index 860258c940..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_layout.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - - - - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/index.svelte deleted file mode 100644 index 9b5d05fe57..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/index.svelte +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/index.svelte deleted file mode 100644 index f8c4cc0868..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/index.svelte +++ /dev/null @@ -1,18 +0,0 @@ - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/index.svelte index f5e3806bd6..c4ed7d949c 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/index.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/index.svelte @@ -1,5 +1,6 @@ diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/_components/LayoutDropdownMenu.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/_components/LayoutDropdownMenu.svelte deleted file mode 100644 index cba68f899d..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/_components/LayoutDropdownMenu.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - - -
- -
- Delete -
- - - - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/_components/LayoutListPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/_components/LayoutListPanel.svelte deleted file mode 100644 index cc895317fd..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/_components/LayoutListPanel.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - - -
- {#each $store.layouts as layout (layout._id)} - store.actions.layouts.select(layout._id)} - > - - - {/each} -
-
- - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/_components/LayoutSettingsPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/_components/LayoutSettingsPanel.svelte deleted file mode 100644 index bfc2f94f43..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/_components/LayoutSettingsPanel.svelte +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - Custom layouts are being deprecated. They will be removed in a future - release. - - - You can save the content of this layout by pressing the button below. - - - This will copy all components inside your layout, which you can then paste - into a screen. - - - - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/_layout.svelte deleted file mode 100644 index c82fefab3e..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/_layout.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/index.svelte deleted file mode 100644 index 4d39403bc3..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/[layoutId]/index.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/_layout.svelte deleted file mode 100644 index 1333c6afe3..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/_layout.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/index.svelte deleted file mode 100644 index 09d45f8fde..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/layouts/index.svelte +++ /dev/null @@ -1,12 +0,0 @@ - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/navigation/_components/NavigationInfoPanel/CustomizePane.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/navigation/_components/NavigationInfoPanel/CustomizePane.svelte deleted file mode 100644 index 05108b1736..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/navigation/_components/NavigationInfoPanel/CustomizePane.svelte +++ /dev/null @@ -1,176 +0,0 @@ - - - -
-
- - CHANGES WILL APPLY TO ALL SCREENS -
- - Your navigation is configured for all the screens within your app. - -
- -
-
- -
- - update("navigation", "Top")} - /> - update("navigation", "Left")} - /> - - - {#if $store.navigation.navigation === "Top"} -
- -
- update("sticky", e.detail)} - /> -
- -
- update("logoUrl", e.detail)} - updateOnChange={false} - /> - {/if} -
- -
- update("hideTitle", !e.detail)} - /> - {#if !$store.navigation.hideTitle} -
- -
- update("title", e.detail)} - updateOnChange={false} - /> - {/if} -
- -
- update("navBackground", e.detail)} - /> -
- -
- update("navTextColor", e.detail)} - /> -
-
- - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/navigation/_components/NavigationInfoPanel/SettingsPane.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/navigation/_components/NavigationInfoPanel/SettingsPane.svelte deleted file mode 100644 index d3ce720994..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/navigation/_components/NavigationInfoPanel/SettingsPane.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - - -
- - Show nav on this screen -
-
- - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/navigation/_components/NavigationInfoPanel/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/navigation/_components/NavigationInfoPanel/index.svelte deleted file mode 100644 index 6ce5405f93..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/navigation/_components/NavigationInfoPanel/index.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/navigation/_components/NavigationSettingsPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/navigation/_components/NavigationSettingsPanel.svelte deleted file mode 100644 index c6d43984b2..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/navigation/_components/NavigationSettingsPanel.svelte +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - - update("navigation", "Top")} - /> - update("navigation", "Left")} - /> - - - {#if $store.navigation.navigation === "Top"} - update("sticky", e.detail)} - /> - update("logoUrl", e.detail)} - placeholder="Add logo URL" - updateOnChange={false} - /> - {/if} - - - update("hideTitle", !e.detail)} - /> - {#if !$store.navigation.hideTitle} - update("title", e.detail)} - placeholder="Add title" - updateOnChange={false} - /> - {/if} - - - - update("navBackground", e.detail)} - /> - - - - update("navTextColor", e.detail)} - /> - - - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/navigation/index.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/navigation/index.svelte deleted file mode 100644 index 2331d8b285..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/navigation/index.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenListPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenListPanel.svelte deleted file mode 100644 index 6362af3073..0000000000 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenListPanel.svelte +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - (searchString = e.detail)} - /> - + Installation identifier + Share this with support@budibase.com to obtain your offline license + + +
+ +
+
+ + + License + Upload your license to activate your plan + + +
+
-
- - - {#if licenseInfo?.licenseKey} - - {/if} - - + {#if licenseKey} + + {/if} + + + {/if} - Plan - + Plan + You are currently on the {license.plan.type} plan +
+ If you purchase or update your plan on the account + portal, click the refresh button to sync those changes +
{processStringSync("Updated {{ duration time 'millisecond' }} ago", { time: @@ -169,4 +294,7 @@ grid-gap: var(--spacing-l); align-items: center; } + .identifier-input { + width: 300px; + } diff --git a/packages/builder/src/pages/builder/portal/account/usage.svelte b/packages/builder/src/pages/builder/portal/account/usage.svelte index 8dc02c6c80..cbe84973ef 100644 --- a/packages/builder/src/pages/builder/portal/account/usage.svelte +++ b/packages/builder/src/pages/builder/portal/account/usage.svelte @@ -14,6 +14,7 @@ import { Constants } from "@budibase/frontend-core" import { DashCard, Usage } from "components/usage" import { PlanModel } from "constants" + import { sdk } from "@budibase/shared-core" let staticUsage = [] let monthlyUsage = [] @@ -51,7 +52,8 @@ $: accountPortalAccess = $auth?.user?.accountPortalAccess $: quotaReset = quotaUsage?.quotaReset $: canManagePlan = - ($admin.cloud && accountPortalAccess) || (!$admin.cloud && $auth.isAdmin) + ($admin.cloud && accountPortalAccess) || + (!$admin.cloud && sdk.users.isAdmin($auth.user)) $: showButton = !usesInvoicing && accountPortalAccess diff --git a/packages/builder/src/pages/builder/portal/apps/_layout.svelte b/packages/builder/src/pages/builder/portal/apps/_layout.svelte index 4818946771..bf0bca0df4 100644 --- a/packages/builder/src/pages/builder/portal/apps/_layout.svelte +++ b/packages/builder/src/pages/builder/portal/apps/_layout.svelte @@ -1,11 +1,19 @@ -{#if $apps.length} - - - {#each Object.keys(automationErrors || {}) as appId} - goToAutomationError(appId)} - type="error" - icon="Alert" - actionMessage={errorCount(automationErrors[appId]) > 1 - ? "View errors" - : "View error"} - on:dismiss={async () => { - await automationStore.actions.clearLogErrors({ appId }) - await apps.load() - }} - message={automationErrorMessage(appId)} - /> - {/each} -
-
- - {welcomeHeader} - - Manage your apps and get a head start with templates - - -
+ + + {#each Object.keys(automationErrors || {}) as appId} + goToAutomationError(appId)} + type="error" + icon="Alert" + actionMessage={errorCount(automationErrors[appId]) > 1 + ? "View errors" + : "View error"} + on:dismiss={async () => { + await automationStore.actions.clearLogErrors({ appId }) + await apps.load() + }} + message={automationErrorMessage(appId)} + /> + {/each} +
+
+ + {welcomeHeader} + + Below you'll find the list of apps that you have access to + +
+
- {#if enrichedApps.length} - -
+ {#if enrichedApps.length} + +
+ {#if $auth.user && sdk.users.isGlobalBuilder($auth.user)}
- {#if enrichedApps.length > 1} -
- + +
+ {/if}
- {/if} -
- -{/if} + +
+ {#each filteredApps as app (app.appId)} + + {/each} +
+ + {:else} +
+ spaceman + You haven't been given access to any apps yet +
+ {/if} + + {#if creatingFromTemplate} +
+ +

Creating your Budibase app from your selected template...

+ +
+ {/if} + + -{#if $auth.isAdmin && mounted} +{#if sdk.users.isAdmin($auth.user) && mounted}
@@ -400,7 +401,7 @@ on:click={() => { if (isCloud && $auth?.user?.accountPortalAccess) { window.open($admin.accountPortalUrl + "/portal/upgrade", "_blank") - } else if ($auth.isAdmin) { + } else if (sdk.users.isAdmin($auth.user)) { $goto("/builder/portal/account/upgrade") } }} diff --git a/packages/builder/src/pages/builder/portal/settings/diagnostics.svelte b/packages/builder/src/pages/builder/portal/settings/diagnostics.svelte index bba0b4ea29..3442d39a68 100644 --- a/packages/builder/src/pages/builder/portal/settings/diagnostics.svelte +++ b/packages/builder/src/pages/builder/portal/settings/diagnostics.svelte @@ -13,6 +13,7 @@ import { redirect } from "@roxi/routify" import { API } from "api" import { onMount } from "svelte" + import { sdk } from "@budibase/shared-core" let diagnosticInfo = "" @@ -46,7 +47,7 @@ }) -{#if $auth.isAdmin && diagnosticInfo} +{#if sdk.users.isAdmin($auth.user) && diagnosticInfo} Diagnostics diff --git a/packages/builder/src/pages/builder/portal/settings/organisation.svelte b/packages/builder/src/pages/builder/portal/settings/organisation.svelte index d2af329cb9..c107a7339e 100644 --- a/packages/builder/src/pages/builder/portal/settings/organisation.svelte +++ b/packages/builder/src/pages/builder/portal/settings/organisation.svelte @@ -13,10 +13,11 @@ import { auth, organisation, admin } from "stores/portal" import { writable } from "svelte/store" import { redirect } from "@roxi/routify" + import { sdk } from "@budibase/shared-core" // Only admins allowed here $: { - if (!$auth.isAdmin) { + if (!sdk.users.isAdmin($auth.user)) { $redirect("../../portal") } } @@ -50,7 +51,7 @@ } -{#if $auth.isAdmin} +{#if sdk.users.isAdmin($auth.user)} Organisation diff --git a/packages/builder/src/pages/builder/portal/settings/version.svelte b/packages/builder/src/pages/builder/portal/settings/version.svelte index c9b2f618e3..fb9144c730 100644 --- a/packages/builder/src/pages/builder/portal/settings/version.svelte +++ b/packages/builder/src/pages/builder/portal/settings/version.svelte @@ -14,6 +14,7 @@ import { API } from "api" import { auth, admin } from "stores/portal" import { redirect } from "@roxi/routify" + import { sdk } from "@budibase/shared-core" let version let loaded = false @@ -25,7 +26,7 @@ // Only admins allowed here $: { - if (!$auth.isAdmin || $admin.cloud) { + if (!sdk.users.isAdmin($auth.user) || $admin.cloud) { $redirect("../../portal") } } @@ -89,7 +90,7 @@ }) -{#if $auth.isAdmin} +{#if sdk.users.isAdmin($auth.user)} Version diff --git a/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte b/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte index 3482b9a88f..6a4364c414 100644 --- a/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte +++ b/packages/builder/src/pages/builder/portal/users/groups/[groupId].svelte @@ -20,6 +20,7 @@ import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte" import GroupIcon from "./_components/GroupIcon.svelte" import GroupUsers from "./_components/GroupUsers.svelte" + import { sdk } from "@budibase/shared-core" export let groupId @@ -46,7 +47,7 @@ let editModal, deleteModal $: scimEnabled = $features.isScimEnabled - $: readonly = !$auth.isAdmin || scimEnabled + $: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled $: group = $groups.find(x => x._id === groupId) $: groupApps = $apps .filter(app => diff --git a/packages/builder/src/pages/builder/portal/users/groups/_components/EditUserPicker.svelte b/packages/builder/src/pages/builder/portal/users/groups/_components/EditUserPicker.svelte index a71634b262..da4b12f7f9 100644 --- a/packages/builder/src/pages/builder/portal/users/groups/_components/EditUserPicker.svelte +++ b/packages/builder/src/pages/builder/portal/users/groups/_components/EditUserPicker.svelte @@ -3,6 +3,7 @@ import UserGroupPicker from "components/settings/UserGroupPicker.svelte" import { createPaginationStore } from "helpers/pagination" import { auth, groups, users } from "stores/portal" + import { sdk } from "@budibase/shared-core" export let groupId export let onUsersUpdated @@ -13,7 +14,7 @@ let prevSearch = undefined let pageInfo = createPaginationStore() - $: readonly = !$auth.isAdmin + $: readonly = !sdk.users.isAdmin($auth.user) $: page = $pageInfo.page $: searchUsers(page, searchTerm) $: group = $groups.find(x => x._id === groupId) diff --git a/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte b/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte index 3159425b06..cd57f31877 100644 --- a/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte +++ b/packages/builder/src/pages/builder/portal/users/groups/_components/GroupUsers.svelte @@ -9,6 +9,7 @@ import { setContext } from "svelte" import ScimBanner from "../../_components/SCIMBanner.svelte" import RemoveUserTableRenderer from "../_components/RemoveUserTableRenderer.svelte" + import { sdk } from "@budibase/shared-core" export let groupId @@ -49,7 +50,7 @@ ] $: scimEnabled = $features.isScimEnabled - $: readonly = !$auth.isAdmin || scimEnabled + $: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled const removeUser = async id => { await groups.actions.removeUser(groupId, id) diff --git a/packages/builder/src/pages/builder/portal/users/groups/_components/RemoveUserTableRenderer.svelte b/packages/builder/src/pages/builder/portal/users/groups/_components/RemoveUserTableRenderer.svelte index 1efd60a50d..4d71c71ae6 100644 --- a/packages/builder/src/pages/builder/portal/users/groups/_components/RemoveUserTableRenderer.svelte +++ b/packages/builder/src/pages/builder/portal/users/groups/_components/RemoveUserTableRenderer.svelte @@ -2,6 +2,7 @@ import { ActionButton } from "@budibase/bbui" import { getContext } from "svelte" import { auth } from "stores/portal" + import { sdk } from "@budibase/shared-core" export let value @@ -13,6 +14,10 @@ } - + Remove diff --git a/packages/builder/src/pages/builder/portal/users/groups/index.svelte b/packages/builder/src/pages/builder/portal/users/groups/index.svelte index f01d306df2..a82da5cf34 100644 --- a/packages/builder/src/pages/builder/portal/users/groups/index.svelte +++ b/packages/builder/src/pages/builder/portal/users/groups/index.svelte @@ -22,6 +22,7 @@ import GroupNameTableRenderer from "./_components/GroupNameTableRenderer.svelte" import { goto } from "@roxi/routify" import ScimBanner from "../_components/SCIMBanner.svelte" + import { sdk } from "@budibase/shared-core" const DefaultGroup = { name: "", @@ -40,7 +41,7 @@ { column: "roles", component: GroupAppsTableRenderer }, ] - $: readonly = !$auth.isAdmin + $: readonly = !sdk.users.isAdmin($auth.user) $: schema = { name: { displayName: "Group", width: "2fr", minWidth: "200px" }, users: { sortable: false, width: "1fr" }, diff --git a/packages/builder/src/pages/builder/portal/users/users/[userId].svelte b/packages/builder/src/pages/builder/portal/users/users/[userId].svelte index ec556abf72..904dd7d430 100644 --- a/packages/builder/src/pages/builder/portal/users/users/[userId].svelte +++ b/packages/builder/src/pages/builder/portal/users/users/[userId].svelte @@ -31,6 +31,7 @@ import AppNameTableRenderer from "./_components/AppNameTableRenderer.svelte" import AppRoleTableRenderer from "./_components/AppRoleTableRenderer.svelte" import ScimBanner from "../_components/SCIMBanner.svelte" + import { sdk } from "@budibase/shared-core" export let userId @@ -87,8 +88,8 @@ $: scimEnabled = $features.isScimEnabled $: isSSO = !!user?.provider - $: readonly = !$auth.isAdmin || scimEnabled - $: privileged = user?.admin?.global || user?.builder?.global + $: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled + $: privileged = sdk.users.isAdminOrBuilder(user) $: nameLabel = getNameLabel(user) $: filteredGroups = getFilteredGroups($groups, searchTerm) $: availableApps = getAvailableApps($apps, privileged, user?.roles) @@ -97,9 +98,9 @@ return y._id === userId }) }) - $: globalRole = user?.admin?.global + $: globalRole = sdk.users.isAdmin(user) ? "admin" - : user?.builder?.global + : sdk.users.isBuilder(user) ? "developer" : "appUser" @@ -285,7 +286,7 @@