diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..3ba13e0cec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/epic.md b/.github/ISSUE_TEMPLATE/epic.md deleted file mode 100644 index b8cf652125..0000000000 --- a/.github/ISSUE_TEMPLATE/epic.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: Epic -about: Plan a new project -title: '' -labels: epic -assignees: '' - ---- - -## Description -Brief summary of what this Epic is, whether it's a larger project, goal, or user story. Describe the job to be done, which persona this Epic is mainly for, or if more multiple, break it down by user and job story. - -## Spec -Link to confluence spec - -## Teams and Stakeholders -Describe who needs to be kept up-to-date about this Epic, included in discussions, or updated along the way. Stakeholders can be both in Product/Engineering, as well as other teams like Customer Success who might want to keep customers updated on the Epic project. - - -## Workflow -- [ ] Spec Created and pasted above -- [ ] Product Review -- [ ] Designs created -- [ ] Individual Tasks created and assigned to Epic diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index c07f9b2c28..c64adb010f 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -10,8 +10,7 @@ on: pull_request: branches: - master - - develop - - release + - develop workflow_dispatch: env: @@ -20,9 +19,67 @@ env: PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }} jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + - run: yarn + - run: yarn lint + build: runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + - name: Install Pro + run: yarn install:pro $BRANCH $BASE_BRANCH + - run: yarn + - run: yarn bootstrap + - run: yarn build + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + - name: Install Pro + run: yarn install:pro $BRANCH $BASE_BRANCH + - run: yarn + - run: yarn bootstrap + - run: yarn test + - uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos + files: ./packages/server/coverage/clover.xml,./packages/worker/coverage/clover.xml,./packages/backend-core/coverage/clover.xml + name: codecov-umbrella + verbose: true + + test-pro: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + - name: Install Pro + run: yarn install:pro $BRANCH $BASE_BRANCH + - run: yarn + - run: yarn bootstrap + - run: yarn test:pro + + integration-test: + runs-on: ubuntu-latest services: couchdb: image: ibmcom/couchdb3 @@ -31,39 +88,18 @@ jobs: COUCHDB_USER: budibase ports: - 4567:5984 - - strategy: - matrix: - node-version: [14.x] - steps: - - uses: actions/checkout@v2 - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - - name: Install Pro - run: yarn install:pro $BRANCH $BASE_BRANCH - - - run: yarn - - run: yarn bootstrap - - run: yarn lint - - run: yarn build - - run: yarn test - env: - CI: true - name: Budibase CI - - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos - files: ./packages/server/coverage/clover.xml,./packages/worker/coverage/clover.xml,./packages/backend-core/coverage/clover.xml - name: codecov-umbrella - verbose: true - - - name: QA Core Integration Tests - run: | - cd qa-core - yarn - yarn api:test:ci \ No newline at end of file + - uses: actions/checkout@v2 + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + - name: Install Pro + run: yarn install:pro $BRANCH $BASE_BRANCH + - run: yarn + - run: yarn bootstrap + - run: yarn build + - run: | + cd qa-core + yarn + yarn api:test:ci diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index 193f736b5f..e986179cfc 100644 --- a/.github/workflows/release-develop.yml +++ b/.github/workflows/release-develop.yml @@ -45,10 +45,9 @@ jobs: - run: yarn - run: yarn bootstrap - - run: yarn lint - run: yarn build - run: yarn build:sdk - - run: yarn test +# - run: yarn test - name: Publish budibase packages to NPM env: @@ -69,83 +68,6 @@ jobs: DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} - deploy-to-release-env: - needs: [release-images] - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Get the current budibase release version - id: version - run: | - release_version=$(cat lerna.json | jq -r '.version') - echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV - - - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: eu-west-1 - - - name: Pull values.yaml from budibase-infra - run: | - curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \ - -H 'Accept: application/vnd.github.v3.raw' \ - -o values.release.yaml \ - -L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-release/values.yaml - wc -l values.release.yaml - - - name: Deploy to Release Environment - uses: budibase/helm@v1.8.0 - with: - release: budibase-release - namespace: budibase - chart: charts/budibase - token: ${{ github.token }} - helm: helm3 - values: | - globals: - appVersion: develop - ingress: - enabled: true - nginx: true - value-files: >- - [ - "values.release.yaml" - ] - env: - KUBECONFIG_FILE: '${{ secrets.RELEASE_KUBECONFIG }}' - - - name: Re roll app-service - uses: actions-hub/kubectl@master - env: - KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} - with: - args: rollout restart deployment app-service -n budibase - - - name: Re roll proxy-service - uses: actions-hub/kubectl@master - env: - KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} - with: - args: rollout restart deployment proxy-service -n budibase - - - name: Re roll worker-service - uses: actions-hub/kubectl@master - env: - KUBE_CONFIG: ${{ secrets.RELEASE_KUBECONFIG_BASE64 }} - with: - args: rollout restart deployment worker-service -n budibase - - - name: Discord Webhook Action - uses: tsickert/discord-webhook@v4.0.0 - with: - webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }} - content: "Release Env Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Release Env." - embed-title: ${{ env.RELEASE_VERSION }} - release-helm-chart: needs: [release-images] runs-on: ubuntu-latest @@ -194,5 +116,5 @@ jobs: PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }} with: repository: budibase/budibase-deploys - event: deploy-budibase-develop-to-qa + event: budicloud-qa-deploy github_pat: ${{ secrets.GH_ACCESS_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/smoke_test.yaml b/.github/workflows/smoke_test.yaml index 29c7f5f85a..3fd61cd9c5 100644 --- a/.github/workflows/smoke_test.yaml +++ b/.github/workflows/smoke_test.yaml @@ -7,7 +7,7 @@ on: jobs: nightly: - runs-on: ubuntu-latest + runs-on: [self-hosted, qa] steps: - uses: actions/checkout@v2 @@ -15,30 +15,17 @@ jobs: uses: actions/setup-node@v1 with: node-version: 14.x - - run: yarn - - run: yarn bootstrap - - run: yarn build - - name: Pull from budibase-infra + - name: QA Core Integration Tests run: | - curl -H "Authorization: token ${{ secrets.GH_PERSONAL_TOKEN }}" \ - -H 'Accept: application/vnd.github.v3.raw' \ - -o - -L - wc -l - - - uses: actions/upload-artifact@v3 - with: - name: Test Reports - path: + cd qa-core + yarn + yarn api:test:ci + env: + BUDIBASE_HOST: budicloud.qa.budibase.net + BUDIBASE_ACCOUNTS_URL: https://account-portal.budicloud.qa.budibase.net - # TODO: enable once running in QA test env - # - name: Configure AWS Credentials - # uses: aws-actions/configure-aws-credentials@v1 - # with: - # aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - # aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - # aws-region: eu-west-1 - - # - name: Upload test results HTML - # uses: aws-actions/configure-aws-credentials@v1 - # run: aws s3 cp packages/builder/cypress/reports/testReport.html s3://{{ secrets.BUDI_QA_REPORTS_BUCKET_NAME }}/$GITHUB_RUN_ID/index.html + - name: Cypress Discord Notify + run: yarn test:notify + env: + WEBHOOK_URL: ${{ secrets.BUDI_QA_WEBHOOK }} + GITHUB_RUN_URL: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit index 3b614330e0..6700f51282 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,2 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" - -yarn run lint diff --git a/.tool-versions b/.tool-versions index 8a1af3c071..6ee8cc60be 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ nodejs 14.19.3 -python 3.11.1 \ No newline at end of file +python 3.10.0 \ No newline at end of file diff --git a/charts/budibase/templates/app-service-deployment.yaml b/charts/budibase/templates/app-service-deployment.yaml index 9ac8a1e7c6..6b0a0338d6 100644 --- a/charts/budibase/templates/app-service-deployment.yaml +++ b/charts/budibase/templates/app-service-deployment.yaml @@ -4,9 +4,15 @@ metadata: annotations: kompose.cmd: kompose convert kompose.version: 1.21.0 (992df58d8) +{{ if .Values.services.apps.deploymentAnnotations }} +{{- toYaml .Values.services.apps.deploymentAnnotations | indent 4 -}} +{{ end }} creationTimestamp: null labels: io.kompose.service: app-service +{{ if .Values.services.apps.deploymentLabels }} +{{- toYaml .Values.services.apps.deploymentLabels | indent 4 -}} +{{ end }} name: app-service spec: replicas: {{ .Values.services.apps.replicaCount }} @@ -20,12 +26,15 @@ spec: annotations: kompose.cmd: kompose convert kompose.version: 1.21.0 (992df58d8) -{{ if .Values.services.apps.annotations }} -{{- toYaml .Values.services.apps.annotations | indent 8 -}} +{{ if .Values.services.apps.templateAnnotations }} +{{- toYaml .Values.services.apps.templateAnnotations | indent 8 -}} {{ end }} creationTimestamp: null labels: io.kompose.service: app-service +{{ if .Values.services.apps.templateLabels }} +{{- toYaml .Values.services.apps.templateLabels | indent 8 -}} +{{ end }} spec: containers: - env: @@ -157,6 +166,14 @@ spec: - name: NODE_DEBUG value: {{ .Values.services.apps.nodeDebug | quote }} {{ end }} + {{ if .Values.globals.datadogApmEnabled }} + - name: DD_LOGS_INJECTION + value: {{ .Values.globals.datadogApmEnabled | quote }} + - name: DD_APM_ENABLED + value: {{ .Values.globals.datadogApmEnabled | quote }} + - name: DD_APM_DD_URL + value: https://trace.agent.datadoghq.eu + {{ end }} {{ if .Values.globals.elasticApmEnabled }} - name: ELASTIC_APM_ENABLED value: {{ .Values.globals.elasticApmEnabled | quote }} diff --git a/charts/budibase/templates/proxy-service-deployment.yaml b/charts/budibase/templates/proxy-service-deployment.yaml index dfddee8ba0..0dea38fcbd 100644 --- a/charts/budibase/templates/proxy-service-deployment.yaml +++ b/charts/budibase/templates/proxy-service-deployment.yaml @@ -4,9 +4,15 @@ metadata: annotations: kompose.cmd: kompose convert kompose.version: 1.21.0 (992df58d8) +{{ if .Values.services.proxy.deploymentAnnotations }} +{{- toYaml .Values.services.proxy.deploymentAnnotations | indent 4 -}} +{{ end }} creationTimestamp: null labels: app.kubernetes.io/name: budibase-proxy +{{ if .Values.services.proxy.deploymentLabels }} +{{- toYaml .Values.services.proxy.deploymentLabels | indent 4 -}} +{{ end }} name: proxy-service spec: replicas: {{ .Values.services.proxy.replicaCount }} @@ -20,12 +26,15 @@ spec: annotations: kompose.cmd: kompose convert kompose.version: 1.21.0 (992df58d8) -{{ if .Values.services.proxy.annotations }} -{{- toYaml .Values.services.proxy.annotations | indent 8 -}} +{{ if .Values.services.proxy.templateAnnotations }} +{{- toYaml .Values.services.proxy.templateAnnotations | indent 8 -}} {{ end }} creationTimestamp: null labels: app.kubernetes.io/name: budibase-proxy +{{ if .Values.services.proxy.templateLabels }} +{{- toYaml .Values.services.proxy.templateLabels | indent 8 -}} +{{ end }} spec: containers: - image: budibase/proxy:{{ .Values.globals.appVersion }} diff --git a/charts/budibase/templates/worker-service-deployment.yaml b/charts/budibase/templates/worker-service-deployment.yaml index a16f839ea7..f4305fbb00 100644 --- a/charts/budibase/templates/worker-service-deployment.yaml +++ b/charts/budibase/templates/worker-service-deployment.yaml @@ -4,13 +4,18 @@ metadata: annotations: kompose.cmd: kompose convert kompose.version: 1.21.0 (992df58d8) +{{ if .Values.services.worker.deploymentAnnotations }} +{{- toYaml .Values.services.worker.deploymentAnnotations | indent 4 -}} +{{ end }} creationTimestamp: null labels: io.kompose.service: worker-service +{{ if .Values.services.worker.deploymentLabels }} +{{- toYaml .Values.services.worker.deploymentLabels | indent 4 -}} +{{ end }} name: worker-service spec: replicas: {{ .Values.services.worker.replicaCount }} - selector: matchLabels: io.kompose.service: worker-service @@ -21,12 +26,15 @@ spec: annotations: kompose.cmd: kompose convert kompose.version: 1.21.0 (992df58d8) -{{ if .Values.services.worker.annotations }} -{{- toYaml .Values.services.worker.annotations | indent 8 -}} +{{ if .Values.services.worker.templateAnnotations }} +{{- toYaml .Values.services.worker.templateAnnotations | indent 8 -}} {{ end }} creationTimestamp: null labels: io.kompose.service: worker-service +{{ if .Values.services.worker.templateLabels }} +{{- toYaml .Values.services.worker.templateLabels | indent 8 -}} +{{ end }} spec: containers: - env: @@ -148,6 +156,14 @@ spec: value: {{ .Values.globals.tenantFeatureFlags | quote }} - name: ENCRYPTION_KEY value: {{ .Values.globals.bbEncryptionKey | quote }} + {{ if .Values.globals.datadogApmEnabled }} + - name: DD_LOGS_INJECTION + value: {{ .Values.globals.datadogApmEnabled | quote }} + - name: DD_APM_ENABLED + value: {{ .Values.globals.datadogApmEnabled | quote }} + - name: DD_APM_DD_URL + value: https://trace.agent.datadoghq.eu + {{ end }} {{ if .Values.globals.elasticApmEnabled }} - name: ELASTIC_APM_ENABLED value: {{ .Values.globals.elasticApmEnabled | quote }} diff --git a/hosting/.env b/hosting/.env index c5638a266f..07b506a6b2 100644 --- a/hosting/.env +++ b/hosting/.env @@ -19,10 +19,11 @@ COUCH_DB_PORT=4005 REDIS_PORT=6379 WATCHTOWER_PORT=6161 BUDIBASE_ENVIRONMENT=PRODUCTION +SQL_MAX_ROWS= # An admin user can be automatically created initially if these are set BB_ADMIN_USER_EMAIL= BB_ADMIN_USER_PASSWORD= # A path that is watched for plugin bundles. Any bundles found are imported automatically/ -PLUGINS_DIR= \ No newline at end of file +PLUGINS_DIR= diff --git a/hosting/couchdb/Dockerfile b/hosting/couchdb/Dockerfile new file mode 100644 index 0000000000..11fab7129f --- /dev/null +++ b/hosting/couchdb/Dockerfile @@ -0,0 +1,32 @@ +FROM couchdb:3.2.1 + +ENV COUCHDB_USER admin +ENV COUCHDB_PASSWORD admin +EXPOSE 5984 + +RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common wget unzip curl && \ + apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main' && \ + apt-get update && apt-get install -y --no-install-recommends openjdk-8-jre && \ + rm -rf /var/lib/apt/lists/ + +# setup clouseau +WORKDIR / +RUN wget https://github.com/cloudant-labs/clouseau/releases/download/2.21.0/clouseau-2.21.0-dist.zip && \ + unzip clouseau-2.21.0-dist.zip && \ + mv clouseau-2.21.0 /opt/clouseau && \ + rm clouseau-2.21.0-dist.zip + +WORKDIR /opt/clouseau +RUN mkdir ./bin +ADD clouseau/clouseau ./bin/ +ADD clouseau/log4j.properties clouseau/clouseau.ini ./ + +# setup CouchDB +WORKDIR /opt/couchdb +ADD couch/vm.args couch/local.ini ./etc/ + +WORKDIR / +ADD build-target-paths.sh . +ADD runner.sh ./bbcouch-runner.sh +RUN chmod +x ./bbcouch-runner.sh /opt/clouseau/bin/clouseau ./build-target-paths.sh +CMD ["./bbcouch-runner.sh"] diff --git a/hosting/couchdb/build-target-paths.sh b/hosting/couchdb/build-target-paths.sh new file mode 100644 index 0000000000..67e1765ca8 --- /dev/null +++ b/hosting/couchdb/build-target-paths.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +echo ${TARGETBUILD} > /buildtarget.txt +if [[ "${TARGETBUILD}" = "aas" ]]; then + # Azure AppService uses /home for persisent data & SSH on port 2222 + DATA_DIR=/home + WEBSITES_ENABLE_APP_SERVICE_STORAGE=true + mkdir -p $DATA_DIR/{search,minio,couch} + mkdir -p $DATA_DIR/couch/{dbs,views} + chown -R couchdb:couchdb $DATA_DIR/couch/ + apt update + apt-get install -y openssh-server + echo "root:Docker!" | chpasswd + mkdir -p /tmp + chmod +x /tmp/ssh_setup.sh \ + && (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null) + cp /etc/sshd_config /etc/ssh/sshd_config + /etc/init.d/ssh restart + sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini + sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini +else + sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini + sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini +fi \ No newline at end of file diff --git a/hosting/single/clouseau/clouseau b/hosting/couchdb/clouseau/clouseau similarity index 100% rename from hosting/single/clouseau/clouseau rename to hosting/couchdb/clouseau/clouseau diff --git a/hosting/single/clouseau/clouseau.ini b/hosting/couchdb/clouseau/clouseau.ini similarity index 100% rename from hosting/single/clouseau/clouseau.ini rename to hosting/couchdb/clouseau/clouseau.ini diff --git a/hosting/single/clouseau/log4j.properties b/hosting/couchdb/clouseau/log4j.properties similarity index 100% rename from hosting/single/clouseau/log4j.properties rename to hosting/couchdb/clouseau/log4j.properties diff --git a/hosting/single/couch/local.ini b/hosting/couchdb/couch/local.ini similarity index 100% rename from hosting/single/couch/local.ini rename to hosting/couchdb/couch/local.ini diff --git a/hosting/single/couch/vm.args b/hosting/couchdb/couch/vm.args similarity index 100% rename from hosting/single/couch/vm.args rename to hosting/couchdb/couch/vm.args diff --git a/hosting/couchdb/runner.sh b/hosting/couchdb/runner.sh new file mode 100644 index 0000000000..4102d2a751 --- /dev/null +++ b/hosting/couchdb/runner.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +DATA_DIR=${DATA_DIR:-/data} +mkdir -p ${DATA_DIR} +mkdir -p ${DATA_DIR}/couch/{dbs,views} +mkdir -p ${DATA_DIR}/search +chown -R couchdb:couchdb ${DATA_DIR}/couch +/build-target-paths.sh +/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 & +/docker-entrypoint.sh /opt/couchdb/bin/couchdb & +sleep 10 +curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_users +curl -X PUT http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984/_replicator +sleep infinity \ No newline at end of file diff --git a/hosting/dependencies/Dockerfile b/hosting/dependencies/Dockerfile new file mode 100644 index 0000000000..c4872213f1 --- /dev/null +++ b/hosting/dependencies/Dockerfile @@ -0,0 +1,23 @@ +FROM budibase/couchdb + +ENV DATA_DIR /data +RUN mkdir /data + +RUN apt-get update && \ + apt-get install -y --no-install-recommends redis-server + +WORKDIR /minio +ADD scripts/install-minio.sh ./install.sh +RUN chmod +x install.sh && ./install.sh + +WORKDIR / + +ADD dependencies/runner.sh . +RUN chmod +x ./runner.sh + +EXPOSE 5984 +EXPOSE 9000 +EXPOSE 9001 +EXPOSE 6379 + +CMD ["./runner.sh"] diff --git a/hosting/dependencies/README.md b/hosting/dependencies/README.md new file mode 100644 index 0000000000..8586b31948 --- /dev/null +++ b/hosting/dependencies/README.md @@ -0,0 +1,57 @@ +# Docker Image for Running Budibase Tests + +## Overview +This image contains the basic setup for running + +## Usage + +- Build the Image +- Run the Container + + +### Build the Image +The guidance below is based on building the Budibase single image on Debian 11 and AlmaLinux 8. If you use another distro or OS you will need to amend the commands to suit. +#### Install Node +Budibase requires a more recent version of node (14+) than is available in the base Debian repos so: + +``` +curl -sL https://deb.nodesource.com/setup_16.x | sudo bash - +apt install -y nodejs +node -v +``` +Install yarn and lerna: +``` +npm install -g yarn jest lerna +``` +#### Install Docker + +``` +apt install -y docker.io +``` + +Check the versions of each installed version. This process was tested with the version numbers below so YMMV using anything else: + +- Docker: 20.10.5 +- node: 16.15.1 +- yarn: 1.22.19 +- lerna: 5.1.4 + +#### Get the Code +Clone the Budibase repo +``` +git clone https://github.com/Budibase/budibase.git +cd budibase +``` +#### Setup Node +Node setup: +``` +node ./hosting/scripts/setup.js +yarn +yarn bootstrap +yarn build +``` +#### Build Image +The following yarn command does some prep and then runs the docker build command: +``` +yarn build:docker:dependencies +``` diff --git a/hosting/dependencies/runner.sh b/hosting/dependencies/runner.sh new file mode 100644 index 0000000000..d7aef15432 --- /dev/null +++ b/hosting/dependencies/runner.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +redis-server --requirepass $REDIS_PASSWORD > /dev/stdout 2>&1 & +/bbcouch-runner.sh & +/minio/minio server ${DATA_DIR}/minio --console-address ":9001" > /dev/stdout 2>&1 & + +echo "Budibase dependencies started..." +sleep infinity \ No newline at end of file diff --git a/hosting/docker-compose.test.yaml b/hosting/docker-compose.test.yaml new file mode 100644 index 0000000000..f059173d2d --- /dev/null +++ b/hosting/docker-compose.test.yaml @@ -0,0 +1,47 @@ +version: "3" + +# optional ports are specified throughout for more advanced use cases. + +services: + minio-service: + restart: on-failure + # Last version that supports the "fs" backend + image: minio/minio:RELEASE.2022-10-24T18-35-07Z + ports: + - "9000" + - "9001" + environment: + MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} + MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} + command: server /data --console-address ":9001" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 30s + timeout: 20s + retries: 3 + + couchdb-service: + # platform: linux/amd64 + restart: on-failure + image: budibase/couchdb + environment: + - COUCHDB_PASSWORD=${COUCH_DB_PASSWORD} + - COUCHDB_USER=${COUCH_DB_USER} + ports: + - "5984" + - "4369" + - "9100" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5984/_up"] + interval: 30s + timeout: 20s + retries: 3 + + redis-service: + restart: on-failure + image: redis + command: redis-server --requirepass ${REDIS_PASSWORD} + ports: + - "6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] \ No newline at end of file diff --git a/hosting/proxy/nginx.prod.conf b/hosting/proxy/nginx.prod.conf index 21b337deae..4d8b3466bf 100644 --- a/hosting/proxy/nginx.prod.conf +++ b/hosting/proxy/nginx.prod.conf @@ -55,7 +55,7 @@ http { set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com"; set $csp_object "object-src 'none'"; set $csp_base_uri "base-uri 'self'"; - set $csp_connect "connect-src 'self' https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.s3.amazonaws.com https://*.s3.us-east-2.amazonaws.com https://*.s3.us-east-1.amazonaws.com https://*.s3.us-west-1.amazonaws.com https://*.s3.us-west-2.amazonaws.com https://*.s3.af-south-1.amazonaws.com https://*.s3.ap-east-1.amazonaws.com https://*.s3.ap-southeast-3.amazonaws.com https://*.s3.ap-south-1.amazonaws.com https://*.s3.ap-northeast-3.amazonaws.com https://*.s3.ap-northeast-2.amazonaws.com https://*.s3.ap-southeast-1.amazonaws.com https://*.s3.ap-southeast-2.amazonaws.com https://*.s3.ap-northeast-1.amazonaws.com https://*.s3.ca-central-1.amazonaws.com https://*.s3.cn-north-1.amazonaws.com https://*.s3.cn-northwest-1.amazonaws.com https://*.s3.eu-central-1.amazonaws.com https://*.s3.eu-west-1.amazonaws.com https://*.s3.eu-west-2.amazonaws.com https://*.s3.eu-south-1.amazonaws.com https://*.s3.eu-west-3.amazonaws.com https://*.s3.eu-north-1.amazonaws.com https://*.s3.sa-east-1.amazonaws.com https://*.s3.me-south-1.amazonaws.com https://*.s3.us-gov-east-1.amazonaws.com https://*.s3.us-gov-west-1.amazonaws.com https://api.github.com"; + set $csp_connect "connect-src 'self' https://*.budibase.net https://api-iam.intercom.io https://api-iam.intercom.io https://api-ping.intercom.io https://app.posthog.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io https://nexus-websocket-a.intercom.io https://nexus-websocket-b.intercom.io https://uploads.intercomcdn.com https://uploads.intercomusercontent.com https://*.s3.*.amazonaws.com https://s3.*.amazonaws.com https://api.github.com"; set $csp_font "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com https://rsms.me https://maxcdn.bootstrapcdn.com https://js.intercomcdn.com https://fonts.intercomcdn.com"; set $csp_frame "frame-src 'self' https:"; set $csp_img "img-src http: https: data: blob:"; diff --git a/hosting/scripts/install-minio.sh b/hosting/scripts/install-minio.sh new file mode 100755 index 0000000000..8297593599 --- /dev/null +++ b/hosting/scripts/install-minio.sh @@ -0,0 +1,10 @@ +#!/bin/bash +if [[ $TARGETARCH == arm* ]] ; +then + echo "INSTALLING ARM64 MINIO" + wget https://dl.min.io/server/minio/release/linux-arm64/minio +else + echo "INSTALLING AMD64 MINIO" + wget https://dl.min.io/server/minio/release/linux-amd64/minio +fi +chmod +x minio diff --git a/hosting/scripts/linux/release-couch.sh b/hosting/scripts/linux/release-couch.sh new file mode 100755 index 0000000000..d5585d0c65 --- /dev/null +++ b/hosting/scripts/linux/release-couch.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +tag=$1 + +if [[ ! "$tag" ]]; then + echo "No tag present. You must pass a tag to this script" + exit 1 +fi + +echo "Tagging images with tag: $tag" + +docker tag budibase-couchdb budibase/couchdb:$tag + +docker push --all-tags budibase/couchdb + diff --git a/hosting/single/Dockerfile b/hosting/single/Dockerfile index 5127db9897..2c6c06aa6e 100644 --- a/hosting/single/Dockerfile +++ b/hosting/single/Dockerfile @@ -18,7 +18,7 @@ WORKDIR /worker ADD packages/worker . RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh -FROM couchdb:3.2.1 +FROM budibase/couchdb ARG TARGETARCH ENV TARGETARCH $TARGETARCH #TARGETBUILD can be set to single (for single docker image) or aas (for azure app service) @@ -29,23 +29,9 @@ ENV TARGETBUILD $TARGETBUILD COPY --from=build /app /app COPY --from=build /worker /worker -# ENV CUSTOM_DOMAIN=budi001.custom.com \ -# See runner.sh for Env Vars -# These secret env variables are generated by the runner at startup -# their values can be overriden by the user, they will be written -# to the .env file in the /data directory for use later on -# REDIS_PASSWORD=budibase \ -# COUCHDB_PASSWORD=budibase \ -# COUCHDB_USER=budibase \ -# COUCH_DB_URL=http://budibase:budibase@localhost:5984 \ -# INTERNAL_API_KEY=budibase \ -# JWT_SECRET=testsecret \ -# MINIO_ACCESS_KEY=budibase \ -# MINIO_SECRET_KEY=budibase \ - # install base dependencies RUN apt-get update && \ - apt-get install -y software-properties-common wget nginx uuid-runtime && \ + apt-get install -y --no-install-recommends software-properties-common nginx uuid-runtime redis-server && \ apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main' && \ apt-get update @@ -53,7 +39,7 @@ RUN apt-get update && \ WORKDIR /nodejs RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh && \ bash /tmp/nodesource_setup.sh && \ - apt-get install -y libaio1 nodejs nginx openjdk-8-jdk redis-server unzip && \ + apt-get install -y --no-install-recommends libaio1 nodejs && \ npm install --global yarn pm2 # setup nginx @@ -69,23 +55,6 @@ RUN mkdir -p scripts/integrations/oracle ADD packages/server/scripts/integrations/oracle scripts/integrations/oracle RUN /bin/bash -e ./scripts/integrations/oracle/instantclient/linux/install.sh -# setup clouseau -WORKDIR / -RUN wget https://github.com/cloudant-labs/clouseau/releases/download/2.21.0/clouseau-2.21.0-dist.zip && \ - unzip clouseau-2.21.0-dist.zip && \ - mv clouseau-2.21.0 /opt/clouseau && \ - rm clouseau-2.21.0-dist.zip - -WORKDIR /opt/clouseau -RUN mkdir ./bin -ADD hosting/single/clouseau/clouseau ./bin/ -ADD hosting/single/clouseau/log4j.properties hosting/single/clouseau/clouseau.ini ./ -RUN chmod +x ./bin/clouseau - -# setup CouchDB -WORKDIR /opt/couchdb -ADD hosting/single/couch/vm.args hosting/single/couch/local.ini ./etc/ - # setup minio WORKDIR /minio ADD scripts/install-minio.sh ./install.sh @@ -98,9 +67,6 @@ RUN chmod +x ./runner.sh ADD hosting/single/healthcheck.sh . RUN chmod +x ./healthcheck.sh -ADD hosting/scripts/build-target-paths.sh . -RUN chmod +x ./build-target-paths.sh - # Script below sets the path for storing data based on $DATA_DIR # For Azure App Service install SSH & point data locations to /home ADD hosting/single/ssh/sshd_config /etc/ diff --git a/hosting/single/runner.sh b/hosting/single/runner.sh index 3a28cd6e4f..0bd377cd7f 100644 --- a/hosting/single/runner.sh +++ b/hosting/single/runner.sh @@ -72,14 +72,11 @@ for LINE in $(cat ${DATA_DIR}/.env); do export $LINE; done ln -s ${DATA_DIR}/.env /app/.env ln -s ${DATA_DIR}/.env /worker/.env # make these directories in runner, incase of mount -mkdir -p ${DATA_DIR}/couch/{dbs,views} mkdir -p ${DATA_DIR}/minio -mkdir -p ${DATA_DIR}/search chown -R couchdb:couchdb ${DATA_DIR}/couch redis-server --requirepass $REDIS_PASSWORD > /dev/stdout 2>&1 & -/opt/clouseau/bin/clouseau > /dev/stdout 2>&1 & +/bbcouch-runner.sh & /minio/minio server --console-address ":9001" ${DATA_DIR}/minio > /dev/stdout 2>&1 & -/docker-entrypoint.sh /opt/couchdb/bin/couchdb & /etc/init.d/nginx restart if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then # Add monthly cron job to renew certbot certificate @@ -90,15 +87,14 @@ if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then /etc/init.d/nginx restart fi +# wait for backend services to start +sleep 10 + pushd app pm2 start -l /dev/stdout --name app "yarn run:docker" popd pushd worker pm2 start -l /dev/stdout --name worker "yarn run:docker" popd -sleep 10 -echo "curl to couchdb endpoints" -curl -X PUT ${COUCH_DB_URL}/_users -curl -X PUT ${COUCH_DB_URL}/_replicator echo "end of runner.sh, sleeping ..." sleep infinity diff --git a/jestTestcontainersConfigGenerator.js b/jestTestcontainersConfigGenerator.js new file mode 100644 index 0000000000..4b94cf5016 --- /dev/null +++ b/jestTestcontainersConfigGenerator.js @@ -0,0 +1,9 @@ +module.exports = () => { + return { + dockerCompose: { + composeFilePath: "../../hosting", + composeFile: "docker-compose.test.yaml", + startupTimeout: 10000, + }, + } +} diff --git a/lerna.json b/lerna.json index 1aee9f2bad..67b7904a53 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.3.20", + "version": "2.3.21-alpha.1", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index 6290930269..815e470916 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "devDependencies": { "@rollup/plugin-json": "^4.0.2", + "@types/supertest": "^2.0.12", "@typescript-eslint/parser": "5.45.0", "babel-eslint": "^10.0.3", "eslint": "^7.28.0", @@ -12,7 +13,7 @@ "js-yaml": "^4.1.0", "kill-port": "^1.6.1", "lerna": "3.14.1", - "madge": "^5.0.1", + "madge": "^6.0.0", "prettier": "^2.3.1", "prettier-plugin-svelte": "^2.3.0", "rimraf": "^3.0.2", @@ -43,7 +44,7 @@ "dev": "yarn run kill-all && lerna link && lerna run --parallel dev:builder --concurrency 1", "dev:noserver": "yarn run kill-builder && lerna link && lerna run dev:stack:up && lerna run --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker", "dev:server": "yarn run kill-server && lerna run --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server", - "test": "lerna run test && yarn test:pro", + "test": "lerna run test", "test:pro": "bash scripts/pro/test.sh", "lint:eslint": "eslint packages && eslint qa-core", "lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"", @@ -62,6 +63,9 @@ "build:docker:single:multiarch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/single/Dockerfile -t budibase:latest .", "build:docker:single:image": "docker build -f hosting/single/Dockerfile -t budibase:latest .", "build:docker:single": "npm run build:docker:pre && npm run build:docker:single:image", + "build:docker:dependencies": "docker build -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest ./hosting", + "publish:docker:couch": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/couchdb/Dockerfile -t budibase/couchdb:latest -t budibase/couchdb:v3.2.1 --push ./hosting/couchdb", + "publish:docker:dependencies": "docker buildx build --platform linux/arm64,linux/amd64 -f hosting/dependencies/Dockerfile -t budibase/dependencies:latest -t budibase/dependencies:v3.2.1 --push ./hosting", "build:docs": "lerna run build:docs", "release:helm": "node scripts/releaseHelmChart", "env:multi:enable": "lerna run env:multi:enable", @@ -80,4 +84,4 @@ "install:pro": "bash scripts/pro/install.sh", "dep:clean": "yarn clean && yarn bootstrap" } -} \ No newline at end of file +} diff --git a/packages/backend-core/jest-testcontainers-config.js b/packages/backend-core/jest-testcontainers-config.js new file mode 100644 index 0000000000..8ac0f0cd9d --- /dev/null +++ b/packages/backend-core/jest-testcontainers-config.js @@ -0,0 +1,8 @@ +const { join } = require("path") +require("dotenv").config({ + path: join(__dirname, "..", "..", "hosting", ".env"), +}) + +const jestTestcontainersConfigGenerator = require("../../jestTestcontainersConfigGenerator") + +module.exports = jestTestcontainersConfigGenerator() diff --git a/packages/backend-core/jest.config.ts b/packages/backend-core/jest.config.ts index e5993f6596..1e69797e71 100644 --- a/packages/backend-core/jest.config.ts +++ b/packages/backend-core/jest.config.ts @@ -1,24 +1,34 @@ import { Config } from "@jest/types" +const preset = require("ts-jest/jest-preset") -const config: Config.InitialOptions = { - preset: "ts-jest", - testEnvironment: "node", - setupFiles: ["./tests/jestSetup.ts"], - collectCoverageFrom: ["src/**/*.{js,ts}"], - coverageReporters: ["lcov", "json", "clover"], +const baseConfig: Config.InitialProjectOptions = { + ...preset, + preset: "@trendyol/jest-testcontainers", + setupFiles: ["./tests/jestEnv.ts"], + setupFilesAfterEnv: ["./tests/jestSetup.ts"], transform: { "^.+\\.ts?$": "@swc/jest", }, + moduleNameMapper: { + "@budibase/types": "/../types/src", + }, } -if (!process.env.CI) { - // use sources when not in CI - config.moduleNameMapper = { - "@budibase/types": "/../types/src", - "^axios.*$": "/node_modules/axios/lib/axios.js", - } -} else { - console.log("Running tests with compiled dependency sources") +const config: Config.InitialOptions = { + projects: [ + { + ...baseConfig, + displayName: "sequential test", + testMatch: ["/**/*.seq.spec.[jt]s"], + runner: "jest-serial-runner", + }, + { + ...baseConfig, + testMatch: ["/**/!(*.seq).spec.[jt]s"], + }, + ], + collectCoverageFrom: ["src/**/*.{js,ts}"], + coverageReporters: ["lcov", "json", "clover"], } export default config diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 8e8ec24808..421304809c 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.3.20", + "version": "2.3.21-alpha.1", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -18,12 +18,13 @@ "build:pro": "../../scripts/pro/build.sh", "postbuild": "yarn run build:pro", "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", - "test": "jest --coverage --maxWorkers=2", + "test": "bash scripts/test.sh", "test:watch": "jest --watchAll" }, "dependencies": { - "@budibase/nano": "10.1.1", - "@budibase/types": "^2.3.20", + "@budibase/nano": "10.1.2", + "@budibase/pouchdb-replication-stream": "1.2.10", + "@budibase/types": "2.3.21-alpha.1", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", @@ -48,7 +49,6 @@ "posthog-node": "1.3.0", "pouchdb": "7.3.0", "pouchdb-find": "7.2.2", - "pouchdb-replication-stream": "1.2.9", "redlock": "4.2.0", "sanitize-s3-objectkey": "0.0.1", "semver": "7.3.7", @@ -59,9 +59,10 @@ "devDependencies": { "@swc/core": "^1.3.25", "@swc/jest": "^0.2.24", + "@trendyol/jest-testcontainers": "^2.1.1", "@types/chance": "1.1.3", "@types/ioredis": "4.28.0", - "@types/jest": "27.5.1", + "@types/jest": "28.1.1", "@types/koa": "2.13.4", "@types/koa-pino-logger": "3.0.0", "@types/lodash": "4.14.180", @@ -76,6 +77,7 @@ "chance": "1.1.8", "ioredis-mock": "5.8.0", "jest": "28.1.1", + "jest-serial-runner": "^1.2.1", "koa": "2.13.4", "nodemon": "2.0.16", "pouchdb-adapter-memory": "7.2.2", diff --git a/packages/backend-core/scripts/test.sh b/packages/backend-core/scripts/test.sh new file mode 100644 index 0000000000..4bf1900984 --- /dev/null +++ b/packages/backend-core/scripts/test.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +if [[ -n $CI ]] +then + # --runInBand performs better in ci where resources are limited + echo "jest --coverage --runInBand" + jest --coverage --runInBand +else + # --maxWorkers performs better in development + echo "jest --coverage" + jest --coverage +fi \ No newline at end of file diff --git a/packages/backend-core/src/cloud/accounts.ts b/packages/backend-core/src/accounts/accounts.ts similarity index 69% rename from packages/backend-core/src/cloud/accounts.ts rename to packages/backend-core/src/accounts/accounts.ts index 90fa7ab824..a16d0f1074 100644 --- a/packages/backend-core/src/cloud/accounts.ts +++ b/packages/backend-core/src/accounts/accounts.ts @@ -1,13 +1,24 @@ import API from "./api" import env from "../environment" import { Header } from "../constants" -import { CloudAccount } from "@budibase/types" +import { CloudAccount, HealthStatusResponse } from "@budibase/types" const api = new API(env.ACCOUNT_PORTAL_URL) +/** + * This client is intended to be used in a cloud hosted deploy only. + * Rather than relying on each consumer to perform the necessary environmental checks + * we use the following check to exit early with a undefined response which should be + * handled by the caller. + */ +const EXIT_EARLY = env.SELF_HOSTED || env.DISABLE_ACCOUNT_PORTAL + export const getAccount = async ( email: string ): Promise => { + if (EXIT_EARLY) { + return + } const payload = { email, } @@ -29,6 +40,9 @@ export const getAccount = async ( export const getAccountByTenantId = async ( tenantId: string ): Promise => { + if (EXIT_EARLY) { + return + } const payload = { tenantId, } @@ -47,7 +61,12 @@ export const getAccountByTenantId = async ( return json[0] } -export const getStatus = async () => { +export const getStatus = async (): Promise< + HealthStatusResponse | undefined +> => { + if (EXIT_EARLY) { + return + } const response = await api.get(`/api/status`, { headers: { [Header.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, diff --git a/packages/backend-core/src/cloud/api.ts b/packages/backend-core/src/accounts/api.ts similarity index 100% rename from packages/backend-core/src/cloud/api.ts rename to packages/backend-core/src/accounts/api.ts diff --git a/packages/backend-core/src/accounts/index.ts b/packages/backend-core/src/accounts/index.ts new file mode 100644 index 0000000000..f2ae03040e --- /dev/null +++ b/packages/backend-core/src/accounts/index.ts @@ -0,0 +1 @@ +export * from "./accounts" diff --git a/packages/backend-core/src/auth/auth.ts b/packages/backend-core/src/auth/auth.ts index 75e425bd0f..7e6fe4bcee 100644 --- a/packages/backend-core/src/auth/auth.ts +++ b/packages/backend-core/src/auth/auth.ts @@ -1,30 +1,36 @@ const _passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy const JwtStrategy = require("passport-jwt").Strategy -import { getGlobalDB } from "../tenancy" -const refresh = require("passport-oauth2-refresh") -import { Config } from "../constants" -import { getScopedConfig } from "../db" +import { getGlobalDB } from "../context" +import { Cookie } from "../constants" +import { getSessionsForUser, invalidateSessions } from "../security/sessions" import { + authenticated, + csrf, + google, jwt as jwtPassport, local, - authenticated, - auditLog, - tenancy, - authError, - ssoCallbackUrl, - csrf, - internalApi, - adminOnly, - builderOnly, - builderOrAdmin, - joiValidator, oidc, - google, + tenancy, } from "../middleware" +import * as userCache from "../cache/user" import { invalidateUser } from "../cache/user" -import { User } from "@budibase/types" +import { + ConfigType, + GoogleInnerConfig, + OIDCInnerConfig, + PlatformLogoutOpts, + SSOProviderType, + User, +} from "@budibase/types" import { logAlert } from "../logging" +import * as events from "../events" +import * as configs from "../configs" +import { clearCookie, getCookie } from "../utils" +import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso" +import env from "../environment" + +const refresh = require("passport-oauth2-refresh") export { auditLog, authError, @@ -47,7 +53,7 @@ export const jwt = require("jsonwebtoken") _passport.use(new LocalStrategy(local.options, local.authenticate)) if (jwtPassport.options.secretOrKey) { _passport.use(new JwtStrategy(jwtPassport.options, jwtPassport.authenticate)) -} else { +} else if (!env.DISABLE_JWT_WARNING) { logAlert("No JWT Secret supplied, cannot configure JWT strategy") } @@ -66,11 +72,10 @@ _passport.deserializeUser(async (user: User, done: any) => { }) async function refreshOIDCAccessToken( - db: any, - chosenConfig: any, + chosenConfig: OIDCInnerConfig, refreshToken: string -) { - const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig) +): Promise { + const callbackUrl = await oidc.getCallbackUrl() let enrichedConfig: any let strategy: any @@ -79,7 +84,7 @@ async function refreshOIDCAccessToken( if (!enrichedConfig) { throw new Error("OIDC Config contents invalid") } - strategy = await oidc.strategyFactory(enrichedConfig) + strategy = await oidc.strategyFactory(enrichedConfig, ssoSaveUserNoOp) } catch (err) { console.error(err) throw new Error("Could not refresh OAuth Token") @@ -93,7 +98,7 @@ async function refreshOIDCAccessToken( return new Promise(resolve => { refresh.requestNewAccessToken( - Config.OIDC, + ConfigType.OIDC, refreshToken, (err: any, accessToken: string, refreshToken: any, params: any) => { resolve({ err, accessToken, refreshToken, params }) @@ -103,15 +108,18 @@ async function refreshOIDCAccessToken( } async function refreshGoogleAccessToken( - db: any, - config: any, + config: GoogleInnerConfig, refreshToken: any -) { - let callbackUrl = await google.getCallbackUrl(db, config) +): Promise { + let callbackUrl = await google.getCallbackUrl(config) let strategy try { - strategy = await google.strategyFactory(config, callbackUrl) + strategy = await google.strategyFactory( + config, + callbackUrl, + ssoSaveUserNoOp + ) } catch (err: any) { console.error(err) throw new Error( @@ -123,7 +131,7 @@ async function refreshGoogleAccessToken( return new Promise(resolve => { refresh.requestNewAccessToken( - Config.GOOGLE, + ConfigType.GOOGLE, refreshToken, (err: any, accessToken: string, refreshToken: string, params: any) => { resolve({ err, accessToken, refreshToken, params }) @@ -132,43 +140,41 @@ async function refreshGoogleAccessToken( }) } -export async function refreshOAuthToken( - refreshToken: string, - configType: string, - configId: string -) { - const db = getGlobalDB() - - const config = await getScopedConfig(db, { - type: configType, - group: {}, - }) - - let chosenConfig = {} - let refreshResponse - if (configType === Config.OIDC) { - // configId - retrieved from cookie. - chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0] - if (!chosenConfig) { - throw new Error("Invalid OIDC configuration") - } - refreshResponse = await refreshOIDCAccessToken( - db, - chosenConfig, - refreshToken - ) - } else { - chosenConfig = config - refreshResponse = await refreshGoogleAccessToken( - db, - chosenConfig, - refreshToken - ) +interface RefreshResponse { + err?: { + data?: string } - - return refreshResponse + accessToken?: string + refreshToken?: string + params?: any } +export async function refreshOAuthToken( + refreshToken: string, + providerType: SSOProviderType, + configId?: string +): Promise { + switch (providerType) { + case SSOProviderType.OIDC: + if (!configId) { + return { err: { data: "OIDC config id not provided" } } + } + const oidcConfig = await configs.getOIDCConfigById(configId) + if (!oidcConfig) { + return { err: { data: "OIDC configuration not found" } } + } + return refreshOIDCAccessToken(oidcConfig, refreshToken) + case SSOProviderType.GOOGLE: + let googleConfig = await configs.getGoogleConfig() + if (!googleConfig) { + return { err: { data: "Google configuration not found" } } + } + return refreshGoogleAccessToken(googleConfig, refreshToken) + } +} + +// TODO: Refactor to use user save function instead to prevent the need for +// manually saving and invalidating on callback export async function updateUserOAuth(userId: string, oAuthConfig: any) { const details = { accessToken: oAuthConfig.accessToken, @@ -196,3 +202,32 @@ export async function updateUserOAuth(userId: string, oAuthConfig: any) { console.error("Could not update OAuth details for current user", e) } } + +/** + * Logs a user out from budibase. Re-used across account portal and builder. + */ +export async function platformLogout(opts: PlatformLogoutOpts) { + const ctx = opts.ctx + const userId = opts.userId + const keepActiveSession = opts.keepActiveSession + + if (!ctx) throw new Error("Koa context must be supplied to logout.") + + const currentSession = getCookie(ctx, Cookie.Auth) + let sessions = await getSessionsForUser(userId) + + if (keepActiveSession) { + sessions = sessions.filter( + session => session.sessionId !== currentSession.sessionId + ) + } else { + // clear cookies + clearCookie(ctx, Cookie.Auth) + clearCookie(ctx, Cookie.CurrentApp) + } + + const sessionIds = sessions.map(({ sessionId }) => sessionId) + await invalidateSessions(userId, { sessionIds, reason: "logout" }) + await events.auth.logout(ctx.user?.email) + await userCache.invalidateUser(userId) +} diff --git a/packages/backend-core/src/auth/tests/auth.spec.ts b/packages/backend-core/src/auth/tests/auth.spec.ts new file mode 100644 index 0000000000..307f6a63c8 --- /dev/null +++ b/packages/backend-core/src/auth/tests/auth.spec.ts @@ -0,0 +1,13 @@ +import { structures, testEnv } from "../../../tests" +import * as auth from "../auth" +import * as events from "../../events" + +describe("platformLogout", () => { + it("should call platform logout", async () => { + await testEnv.withTenant(async () => { + const ctx = structures.koa.newContext() + await auth.platformLogout({ ctx, userId: "test" }) + expect(events.auth.logout).toBeCalledTimes(1) + }) + }) +}) diff --git a/packages/backend-core/src/cache/appMetadata.ts b/packages/backend-core/src/cache/appMetadata.ts index d24c4a3140..5b66c356d3 100644 --- a/packages/backend-core/src/cache/appMetadata.ts +++ b/packages/backend-core/src/cache/appMetadata.ts @@ -1,6 +1,6 @@ import { getAppClient } from "../redis/init" import { doWithDB, DocumentType } from "../db" -import { Database } from "@budibase/types" +import { Database, App } from "@budibase/types" const AppState = { INVALID: "invalid", @@ -65,7 +65,7 @@ export async function getAppMetadata(appId: string) { if (isInvalid(metadata)) { throw { status: 404, message: "No app metadata found" } } - return metadata + return metadata as App } /** diff --git a/packages/backend-core/src/cache/tests/writethrough.spec.js b/packages/backend-core/src/cache/tests/writethrough.spec.js deleted file mode 100644 index 716d3f9c23..0000000000 --- a/packages/backend-core/src/cache/tests/writethrough.spec.js +++ /dev/null @@ -1,59 +0,0 @@ -require("../../../tests") -const { Writethrough } = require("../writethrough") -const { getDB } = require("../../db") -const tk = require("timekeeper") - -const START_DATE = Date.now() -tk.freeze(START_DATE) - -const DELAY = 5000 - -const db = getDB("test") -const db2 = getDB("test2") -const writethrough = new Writethrough(db, DELAY), writethrough2 = new Writethrough(db2, DELAY) - -describe("writethrough", () => { - describe("put", () => { - let first - it("should be able to store, will go to DB", async () => { - const response = await writethrough.put({ _id: "test", value: 1 }) - const output = await db.get(response.id) - first = output - expect(output.value).toBe(1) - }) - - it("second put shouldn't update DB", async () => { - const response = await writethrough.put({ ...first, value: 2 }) - const output = await db.get(response.id) - expect(first._rev).toBe(output._rev) - expect(output.value).toBe(1) - }) - - it("should put it again after delay period", async () => { - tk.freeze(START_DATE + DELAY + 1) - const response = await writethrough.put({ ...first, value: 3 }) - const output = await db.get(response.id) - expect(response.rev).not.toBe(first._rev) - expect(output.value).toBe(3) - }) - }) - - describe("get", () => { - it("should be able to retrieve", async () => { - const response = await writethrough.get("test") - expect(response.value).toBe(3) - }) - }) - - describe("same doc, different databases (tenancy)", () => { - it("should be able to two different databases", async () => { - const resp1 = await writethrough.put({ _id: "db1", value: "first" }) - 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") - }) - }) -}) - diff --git a/packages/backend-core/src/cache/tests/writethrough.spec.ts b/packages/backend-core/src/cache/tests/writethrough.spec.ts new file mode 100644 index 0000000000..d346788121 --- /dev/null +++ b/packages/backend-core/src/cache/tests/writethrough.spec.ts @@ -0,0 +1,73 @@ +import { structures, DBTestConfiguration } from "../../../tests" +import { Writethrough } from "../writethrough" +import { getDB } from "../../db" +import tk from "timekeeper" + +const START_DATE = Date.now() +tk.freeze(START_DATE) + +const DELAY = 5000 + +describe("writethrough", () => { + const config = new DBTestConfiguration() + + const db = getDB(structures.db.id()) + const db2 = getDB(structures.db.id()) + + const writethrough = new Writethrough(db, DELAY) + const writethrough2 = new Writethrough(db2, DELAY) + + describe("put", () => { + let first: any + + it("should be able to store, will go to DB", async () => { + await config.doInTenant(async () => { + const response = await writethrough.put({ _id: "test", value: 1 }) + const output = await db.get(response.id) + first = output + expect(output.value).toBe(1) + }) + }) + + it("second put shouldn't update DB", async () => { + await config.doInTenant(async () => { + const response = await writethrough.put({ ...first, value: 2 }) + const output = await db.get(response.id) + expect(first._rev).toBe(output._rev) + expect(output.value).toBe(1) + }) + }) + + it("should put it again after delay period", async () => { + await config.doInTenant(async () => { + tk.freeze(START_DATE + DELAY + 1) + const response = await writethrough.put({ ...first, value: 3 }) + const output = await db.get(response.id) + expect(response.rev).not.toBe(first._rev) + expect(output.value).toBe(3) + }) + }) + }) + + describe("get", () => { + it("should be able to retrieve", async () => { + await config.doInTenant(async () => { + const response = await writethrough.get("test") + expect(response.value).toBe(3) + }) + }) + }) + + describe("same doc, different databases (tenancy)", () => { + it("should be able to two different databases", async () => { + await config.doInTenant(async () => { + const resp1 = await writethrough.put({ _id: "db1", value: "first" }) + 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") + }) + }) + }) +}) diff --git a/packages/backend-core/src/cache/user.ts b/packages/backend-core/src/cache/user.ts index a128465cd6..b514c3af9b 100644 --- a/packages/backend-core/src/cache/user.ts +++ b/packages/backend-core/src/cache/user.ts @@ -1,8 +1,9 @@ import * as redis from "../redis/init" -import { getTenantId, lookupTenantId, doWithGlobalDB } from "../tenancy" +import * as tenancy from "../tenancy" +import * as context from "../context" +import * as platform from "../platform" import env from "../environment" -import * as accounts from "../cloud/accounts" -import { Database } from "@budibase/types" +import * as accounts from "../accounts" const EXPIRY_SECONDS = 3600 @@ -10,7 +11,8 @@ const EXPIRY_SECONDS = 3600 * The default populate user function */ async function populateFromDB(userId: string, tenantId: string) { - const user = await doWithGlobalDB(tenantId, (db: Database) => db.get(userId)) + const db = tenancy.getTenantDB(tenantId) + const user = await db.get(userId) user.budibaseAccess = true if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { const account = await accounts.getAccount(user.email) @@ -42,9 +44,9 @@ export async function getUser( } if (!tenantId) { try { - tenantId = getTenantId() + tenantId = context.getTenantId() } catch (err) { - tenantId = await lookupTenantId(userId) + tenantId = await platform.users.lookupTenantId(userId) } } const client = await redis.getUserClient() diff --git a/packages/backend-core/src/configs/configs.ts b/packages/backend-core/src/configs/configs.ts new file mode 100644 index 0000000000..b461497747 --- /dev/null +++ b/packages/backend-core/src/configs/configs.ts @@ -0,0 +1,244 @@ +import { + Config, + ConfigType, + GoogleConfig, + GoogleInnerConfig, + OIDCConfig, + OIDCInnerConfig, + SettingsConfig, + SettingsInnerConfig, + SMTPConfig, + SMTPInnerConfig, +} from "@budibase/types" +import { DocumentType, SEPARATOR } from "../constants" +import { CacheKey, TTL, withCache } from "../cache" +import * as context from "../context" +import env from "../environment" +import environment from "../environment" + +// UTILS + +/** + * Generates a new configuration ID. + * @returns {string} The new configuration ID which the config doc can be stored under. + */ +export function generateConfigID(type: ConfigType) { + return `${DocumentType.CONFIG}${SEPARATOR}${type}` +} + +export async function getConfig( + type: ConfigType +): Promise { + const db = context.getGlobalDB() + try { + // await to catch error + const config = (await db.get(generateConfigID(type))) as T + return config + } catch (e: any) { + if (e.status === 404) { + return + } + throw e + } +} + +export async function save( + config: Config +): Promise<{ id: string; rev: string }> { + const db = context.getGlobalDB() + return db.put(config) +} + +// SETTINGS + +export async function getSettingsConfigDoc(): Promise { + let config = await getConfig(ConfigType.SETTINGS) + + if (!config) { + config = { + _id: generateConfigID(ConfigType.SETTINGS), + type: ConfigType.SETTINGS, + config: {}, + } + } + + // overridden fields + config.config.platformUrl = await getPlatformUrl({ + tenantAware: true, + config: config.config, + }) + config.config.analyticsEnabled = await analyticsEnabled({ + config: config.config, + }) + + return config +} + +export async function getSettingsConfig(): Promise { + return (await getSettingsConfigDoc()).config +} + +export async function getPlatformUrl( + opts: { tenantAware: boolean; config?: SettingsInnerConfig } = { + tenantAware: true, + } +) { + let platformUrl = env.PLATFORM_URL || "http://localhost:10000" + + if (!env.SELF_HOSTED && env.MULTI_TENANCY && opts.tenantAware) { + // cloud and multi tenant - add the tenant to the default platform url + const tenantId = context.getTenantId() + if (!platformUrl.includes("localhost:")) { + platformUrl = platformUrl.replace("://", `://${tenantId}.`) + } + } else if (env.SELF_HOSTED) { + const config = opts?.config + ? opts.config + : // direct to db to prevent infinite loop + (await getConfig(ConfigType.SETTINGS))?.config + if (config?.platformUrl) { + platformUrl = config.platformUrl + } + } + + return platformUrl +} + +export const analyticsEnabled = async (opts?: { + config?: SettingsInnerConfig +}) => { + // cloud - always use the environment variable + if (!env.SELF_HOSTED) { + return !!env.ENABLE_ANALYTICS + } + + // self host - prefer the settings doc + // use cache as events have high throughput + const enabledInDB = await withCache( + CacheKey.ANALYTICS_ENABLED, + TTL.ONE_DAY, + async () => { + const config = opts?.config + ? opts.config + : // direct to db to prevent infinite loop + (await getConfig(ConfigType.SETTINGS))?.config + + // need to do explicit checks in case the field is not set + if (config?.analyticsEnabled === false) { + return false + } else if (config?.analyticsEnabled === true) { + return true + } + } + ) + + if (enabledInDB !== undefined) { + return enabledInDB + } + + // fallback to the environment variable + // explicitly check for 0 or false here, undefined or otherwise is treated as true + const envEnabled: any = env.ENABLE_ANALYTICS + if (envEnabled === 0 || envEnabled === false) { + return false + } else { + return true + } +} + +// GOOGLE + +async function getGoogleConfigDoc(): Promise { + return await getConfig(ConfigType.GOOGLE) +} + +export async function getGoogleConfig(): Promise< + GoogleInnerConfig | undefined +> { + const config = await getGoogleConfigDoc() + return config?.config +} + +export async function getGoogleDatasourceConfig(): Promise< + GoogleInnerConfig | undefined +> { + if (!env.SELF_HOSTED) { + // always use the env vars in cloud + return getDefaultGoogleConfig() + } + + // prefer the config in self-host + let config = await getGoogleConfig() + + // fallback to env vars + if (!config || !config.activated) { + config = getDefaultGoogleConfig() + } + + return config +} + +export function getDefaultGoogleConfig(): GoogleInnerConfig | undefined { + if (environment.GOOGLE_CLIENT_ID && environment.GOOGLE_CLIENT_SECRET) { + return { + clientID: environment.GOOGLE_CLIENT_ID!, + clientSecret: environment.GOOGLE_CLIENT_SECRET!, + activated: true, + } + } +} + +// OIDC + +async function getOIDCConfigDoc(): Promise { + return getConfig(ConfigType.OIDC) +} + +export async function getOIDCConfig(): Promise { + const config = (await getOIDCConfigDoc())?.config + // default to the 0th config + return config?.configs && config.configs[0] +} + +/** + * @param configId The config id of the inner config to retrieve + */ +export async function getOIDCConfigById( + configId: string +): Promise { + const config = (await getConfig(ConfigType.OIDC))?.config + return config && config.configs.filter((c: any) => c.uuid === configId)[0] +} + +// SMTP + +export async function getSMTPConfigDoc(): Promise { + return getConfig(ConfigType.SMTP) +} + +export async function getSMTPConfig( + isAutomation?: boolean +): Promise { + const config = await getSMTPConfigDoc() + if (config) { + return config.config + } + + // always allow fallback in self host + // in cloud don't allow for automations + const allowFallback = env.SELF_HOSTED || !isAutomation + + // Use an SMTP fallback configuration from env variables + if (env.SMTP_FALLBACK_ENABLED && allowFallback) { + return { + port: env.SMTP_PORT, + host: env.SMTP_HOST!, + secure: false, + from: env.SMTP_FROM_ADDRESS!, + auth: { + user: env.SMTP_USER!, + pass: env.SMTP_PASSWORD!, + }, + } + } +} diff --git a/packages/backend-core/src/configs/index.ts b/packages/backend-core/src/configs/index.ts new file mode 100644 index 0000000000..783f22a0b9 --- /dev/null +++ b/packages/backend-core/src/configs/index.ts @@ -0,0 +1 @@ +export * from "./configs" diff --git a/packages/backend-core/src/configs/tests/configs.spec.ts b/packages/backend-core/src/configs/tests/configs.spec.ts new file mode 100644 index 0000000000..079f2ab681 --- /dev/null +++ b/packages/backend-core/src/configs/tests/configs.spec.ts @@ -0,0 +1,116 @@ +import { DBTestConfiguration, generator, testEnv } from "../../../tests" +import { ConfigType } from "@budibase/types" +import env from "../../environment" +import * as configs from "../configs" + +const DEFAULT_URL = "http://localhost:10000" +const ENV_URL = "http://env.com" + +describe("configs", () => { + const config = new DBTestConfiguration() + + const setDbPlatformUrl = async (dbUrl: string) => { + const settingsConfig = { + _id: configs.generateConfigID(ConfigType.SETTINGS), + type: ConfigType.SETTINGS, + config: { + platformUrl: dbUrl, + }, + } + await configs.save(settingsConfig) + } + + beforeEach(async () => { + config.newTenant() + }) + + describe("getPlatformUrl", () => { + describe("self host", () => { + beforeEach(async () => { + testEnv.selfHosted() + }) + + it("gets the default url", async () => { + await config.doInTenant(async () => { + const url = await configs.getPlatformUrl() + expect(url).toBe(DEFAULT_URL) + }) + }) + + it("gets the platform url from the environment", async () => { + await config.doInTenant(async () => { + env._set("PLATFORM_URL", ENV_URL) + const url = await configs.getPlatformUrl() + expect(url).toBe(ENV_URL) + }) + }) + + it("gets the platform url from the database", async () => { + await config.doInTenant(async () => { + const dbUrl = generator.url() + await setDbPlatformUrl(dbUrl) + const url = await configs.getPlatformUrl() + expect(url).toBe(dbUrl) + }) + }) + }) + + describe("cloud", () => { + function getTenantAwareUrl() { + return `http://${config.tenantId}.env.com` + } + + beforeEach(async () => { + testEnv.cloudHosted() + testEnv.multiTenant() + + env._set("PLATFORM_URL", ENV_URL) + }) + + it("gets the platform url from the environment without tenancy", async () => { + await config.doInTenant(async () => { + const url = await configs.getPlatformUrl({ tenantAware: false }) + expect(url).toBe(ENV_URL) + }) + }) + + it("gets the platform url from the environment with tenancy", async () => { + await config.doInTenant(async () => { + const url = await configs.getPlatformUrl() + expect(url).toBe(getTenantAwareUrl()) + }) + }) + + it("never gets the platform url from the database", async () => { + await config.doInTenant(async () => { + await setDbPlatformUrl(generator.url()) + const url = await configs.getPlatformUrl() + expect(url).toBe(getTenantAwareUrl()) + }) + }) + }) + }) + + describe("getSettingsConfig", () => { + beforeAll(async () => { + testEnv.selfHosted() + env._set("PLATFORM_URL", "") + }) + + it("returns the platform url with an existing config", async () => { + await config.doInTenant(async () => { + const dbUrl = generator.url() + await setDbPlatformUrl(dbUrl) + const config = await configs.getSettingsConfig() + expect(config.platformUrl).toBe(dbUrl) + }) + }) + + it("returns the platform url without an existing config", async () => { + await config.doInTenant(async () => { + const config = await configs.getSettingsConfig() + expect(config.platformUrl).toBe(DEFAULT_URL) + }) + }) + }) +}) diff --git a/packages/backend-core/src/constants/db.ts b/packages/backend-core/src/constants/db.ts index f7d15b3880..d41098c405 100644 --- a/packages/backend-core/src/constants/db.ts +++ b/packages/backend-core/src/constants/db.ts @@ -68,6 +68,7 @@ export enum DocumentType { MEM_VIEW = "view", USER_FLAG = "flag", AUTOMATION_METADATA = "meta_au", + AUDIT_LOG = "al", } export const StaticDatabases = { @@ -88,6 +89,9 @@ export const StaticDatabases = { install: "install", }, }, + AUDIT_LOGS: { + name: "audit-logs", + }, } export const APP_PREFIX = DocumentType.APP + SEPARATOR diff --git a/packages/backend-core/src/constants/misc.ts b/packages/backend-core/src/constants/misc.ts index 0bf3df4094..e25c90575f 100644 --- a/packages/backend-core/src/constants/misc.ts +++ b/packages/backend-core/src/constants/misc.ts @@ -41,5 +41,6 @@ export enum Config { OIDC_LOGOS = "logos_oidc", } +export const MIN_VALID_DATE = new Date(-2147483647000) export const MAX_VALID_DATE = new Date(2147483647000) export const DEFAULT_TENANT_ID = "default" diff --git a/packages/backend-core/src/context/Context.ts b/packages/backend-core/src/context/Context.ts index 02b7713764..d29b6935a8 100644 --- a/packages/backend-core/src/context/Context.ts +++ b/packages/backend-core/src/context/Context.ts @@ -1,5 +1,5 @@ import { AsyncLocalStorage } from "async_hooks" -import { ContextMap } from "./mainContext" +import { ContextMap } from "./types" export default class Context { static storage = new AsyncLocalStorage() diff --git a/packages/backend-core/src/context/deprovision.ts b/packages/backend-core/src/context/deprovision.ts deleted file mode 100644 index 81f03096dc..0000000000 --- a/packages/backend-core/src/context/deprovision.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - getGlobalUserParams, - getAllApps, - doWithDB, - StaticDatabases, -} from "../db" -import { doWithGlobalDB } from "../tenancy" -import { App, Tenants, User, Database } from "@budibase/types" - -const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants -const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name - -async function removeTenantFromInfoDB(tenantId: string) { - try { - await doWithDB(PLATFORM_INFO_DB, async (infoDb: Database) => { - const tenants = (await infoDb.get(TENANT_DOC)) as Tenants - tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId) - - await infoDb.put(tenants) - }) - } catch (err) { - console.error(`Error removing tenant ${tenantId} from info db`, err) - throw err - } -} - -export async function removeUserFromInfoDB(dbUser: User) { - await doWithDB(PLATFORM_INFO_DB, async (infoDb: Database) => { - const keys = [dbUser._id!, dbUser.email] - const userDocs = await infoDb.allDocs({ - keys, - include_docs: true, - }) - const toDelete = userDocs.rows.map((row: any) => { - return { - ...row.doc, - _deleted: true, - } - }) - await infoDb.bulkDocs(toDelete) - }) -} - -async function removeUsersFromInfoDB(tenantId: string) { - return doWithGlobalDB(tenantId, async (db: any) => { - try { - const allUsers = await db.allDocs( - getGlobalUserParams(null, { - include_docs: true, - }) - ) - await doWithDB(PLATFORM_INFO_DB, async (infoDb: any) => { - const allEmails = allUsers.rows.map((row: any) => row.doc.email) - // get the id docs - let keys = allUsers.rows.map((row: any) => row.id) - // and the email docs - keys = keys.concat(allEmails) - // retrieve the docs and delete them - const userDocs = await infoDb.allDocs({ - keys, - include_docs: true, - }) - const toDelete = userDocs.rows.map((row: any) => { - return { - ...row.doc, - _deleted: true, - } - }) - await infoDb.bulkDocs(toDelete) - }) - } catch (err) { - console.error(`Error removing tenant ${tenantId} users from info db`, err) - throw err - } - }) -} - -async function removeGlobalDB(tenantId: string) { - return doWithGlobalDB(tenantId, async (db: Database) => { - try { - await db.destroy() - } catch (err) { - console.error(`Error removing tenant ${tenantId} users from info db`, err) - throw err - } - }) -} - -async function removeTenantApps(tenantId: string) { - try { - const apps = (await getAllApps({ all: true })) as App[] - const destroyPromises = apps.map(app => - doWithDB(app.appId, (db: Database) => db.destroy()) - ) - await Promise.allSettled(destroyPromises) - } catch (err) { - console.error(`Error removing tenant ${tenantId} apps`, err) - throw err - } -} - -// can't live in tenancy package due to circular dependency on db/utils -export async function deleteTenant(tenantId: string) { - await removeTenantFromInfoDB(tenantId) - await removeUsersFromInfoDB(tenantId) - await removeGlobalDB(tenantId) - await removeTenantApps(tenantId) -} diff --git a/packages/backend-core/src/context/identity.ts b/packages/backend-core/src/context/identity.ts index 648dd1b5fd..84de3b68c9 100644 --- a/packages/backend-core/src/context/identity.ts +++ b/packages/backend-core/src/context/identity.ts @@ -5,6 +5,8 @@ import { isCloudAccount, Account, AccountUserContext, + UserContext, + Ctx, } from "@budibase/types" import * as context from "." @@ -16,15 +18,22 @@ export function doInIdentityContext(identity: IdentityContext, task: any) { return context.doInIdentityContext(identity, task) } -export function doInUserContext(user: User, task: any) { - const userContext: any = { +// used in server/worker +export function doInUserContext(user: User, ctx: Ctx, task: any) { + const userContext: UserContext = { ...user, _id: user._id as string, type: IdentityType.USER, + hostInfo: { + ipAddress: ctx.request.ip, + // filled in by koa-useragent package + userAgent: ctx.userAgent._agent.source, + }, } return doInIdentityContext(userContext, task) } +// used in account portal export function doInAccountContext(account: Account, task: any) { const _id = getAccountUserId(account) const tenantId = account.tenantId diff --git a/packages/backend-core/src/context/mainContext.ts b/packages/backend-core/src/context/mainContext.ts index 9884d25d5a..02ba16aa8c 100644 --- a/packages/backend-core/src/context/mainContext.ts +++ b/packages/backend-core/src/context/mainContext.ts @@ -11,13 +11,7 @@ import { DEFAULT_TENANT_ID, } from "../constants" import { Database, IdentityContext } from "@budibase/types" - -export type ContextMap = { - tenantId?: string - appId?: string - identity?: IdentityContext - environmentVariables?: Record -} +import { ContextMap } from "./types" let TEST_APP_ID: string | null = null @@ -30,14 +24,23 @@ export function getGlobalDBName(tenantId?: string) { return baseGlobalDBName(tenantId) } -export function baseGlobalDBName(tenantId: string | undefined | null) { - let dbName - if (!tenantId || tenantId === DEFAULT_TENANT_ID) { - dbName = StaticDatabases.GLOBAL.name - } else { - dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}` +export function getAuditLogDBName(tenantId?: string) { + if (!tenantId) { + tenantId = getTenantId() + } + if (tenantId === DEFAULT_TENANT_ID) { + return StaticDatabases.AUDIT_LOGS.name + } else { + return `${tenantId}${SEPARATOR}${StaticDatabases.AUDIT_LOGS.name}` + } +} + +export function baseGlobalDBName(tenantId: string | undefined | null) { + if (!tenantId || tenantId === DEFAULT_TENANT_ID) { + return StaticDatabases.GLOBAL.name + } else { + return `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}` } - return dbName } export function isMultiTenant() { @@ -228,6 +231,13 @@ export function getGlobalDB(): Database { return getDB(baseGlobalDBName(context?.tenantId)) } +export function getAuditLogsDB(): Database { + if (!getTenantId()) { + throw new Error("No tenant ID found - cannot open audit log DB") + } + return getDB(getAuditLogDBName()) +} + /** * Gets the app database based on whatever the request * contained, dev or prod. diff --git a/packages/backend-core/src/context/tests/index.spec.ts b/packages/backend-core/src/context/tests/index.spec.ts index c9b5870ffa..5c8ce6fc19 100644 --- a/packages/backend-core/src/context/tests/index.spec.ts +++ b/packages/backend-core/src/context/tests/index.spec.ts @@ -1,11 +1,14 @@ -require("../../../tests") +import { testEnv } from "../../../tests" const context = require("../") const { DEFAULT_TENANT_ID } = require("../../constants") -import env from "../../environment" describe("context", () => { describe("doInTenant", () => { describe("single-tenancy", () => { + beforeAll(() => { + testEnv.singleTenant() + }) + it("defaults to the default tenant", () => { const tenantId = context.getTenantId() expect(tenantId).toBe(DEFAULT_TENANT_ID) @@ -20,8 +23,8 @@ describe("context", () => { }) describe("multi-tenancy", () => { - beforeEach(() => { - env._set("MULTI_TENANCY", 1) + beforeAll(() => { + testEnv.multiTenant() }) it("fails when no tenant id is set", () => { diff --git a/packages/backend-core/src/context/types.ts b/packages/backend-core/src/context/types.ts new file mode 100644 index 0000000000..78197ed528 --- /dev/null +++ b/packages/backend-core/src/context/types.ts @@ -0,0 +1,9 @@ +import { IdentityContext } from "@budibase/types" + +// keep this out of Budibase types, don't want to expose context info +export type ContextMap = { + tenantId?: string + appId?: string + identity?: IdentityContext + environmentVariables?: Record +} diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index a60a748bdc..a3a398950b 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -83,7 +83,14 @@ export class DatabaseImpl implements Database { throw new Error("DB does not exist") } if (!exists) { - await this.nano().db.create(this.name) + try { + await this.nano().db.create(this.name) + } catch (err: any) { + // Handling race conditions + if (err.statusCode !== 412) { + throw err + } + } } return this.nano().db.use(this.name) } @@ -178,7 +185,7 @@ export class DatabaseImpl implements Database { async destroy() { try { - await this.nano().db.destroy(this.name) + return await this.nano().db.destroy(this.name) } catch (err: any) { // didn't exist, don't worry if (err.statusCode === 404) { diff --git a/packages/backend-core/src/db/couch/pouchDB.ts b/packages/backend-core/src/db/couch/pouchDB.ts index a6f4323d88..f83127d466 100644 --- a/packages/backend-core/src/db/couch/pouchDB.ts +++ b/packages/backend-core/src/db/couch/pouchDB.ts @@ -39,7 +39,7 @@ export const getPouch = (opts: PouchOptions = {}) => { } if (opts.replication) { - const replicationStream = require("pouchdb-replication-stream") + const replicationStream = require("@budibase/pouchdb-replication-stream") PouchDB.plugin(replicationStream.plugin) // @ts-ignore PouchDB.adapter("writableStream", replicationStream.adapters.writableStream) diff --git a/packages/backend-core/src/db/couch/pouchDump.ts b/packages/backend-core/src/db/couch/pouchDump.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/backend-core/src/db/db.ts b/packages/backend-core/src/db/db.ts index 3887e8b09f..f13eb9a965 100644 --- a/packages/backend-core/src/db/db.ts +++ b/packages/backend-core/src/db/db.ts @@ -1,17 +1,10 @@ import env from "../environment" -import { directCouchQuery, getPouchDB } from "./couch" +import { directCouchQuery, DatabaseImpl } from "./couch" import { CouchFindOptions, Database } from "@budibase/types" -import { DatabaseImpl } from "../db" const dbList = new Set() export function getDB(dbName?: string, opts?: any): Database { - // TODO: once using the test image, need to remove this - if (env.isTest()) { - dbList.add(dbName) - // @ts-ignore - return getPouchDB(dbName, opts) - } return new DatabaseImpl(dbName, opts) } diff --git a/packages/backend-core/src/db/index.ts b/packages/backend-core/src/db/index.ts index 0d9f75fa18..a569b17b36 100644 --- a/packages/backend-core/src/db/index.ts +++ b/packages/backend-core/src/db/index.ts @@ -7,3 +7,4 @@ export { default as Replication } from "./Replication" // exports to support old export structure export * from "../constants/db" export { getGlobalDBName, baseGlobalDBName } from "../context" +export * from "./lucene" diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts new file mode 100644 index 0000000000..cba2f0138a --- /dev/null +++ b/packages/backend-core/src/db/lucene.ts @@ -0,0 +1,624 @@ +import fetch from "node-fetch" +import { getCouchInfo } from "./couch" +import { SearchFilters, Row } from "@budibase/types" + +const QUERY_START_REGEX = /\d[0-9]*:/g + +interface SearchResponse { + rows: T[] | any[] + bookmark: string +} + +interface PaginatedSearchResponse extends SearchResponse { + hasNextPage: boolean +} + +export type SearchParams = { + tableId?: string + sort?: string + sortOrder?: string + sortType?: string + limit?: number + bookmark?: string + version?: string + indexer?: () => Promise + disableEscaping?: boolean + rows?: T | Row[] +} + +export function removeKeyNumbering(key: any): string { + if (typeof key === "string" && key.match(QUERY_START_REGEX) != null) { + const parts = key.split(":") + // remove the number + parts.shift() + return parts.join(":") + } else { + return key + } +} + +/** + * Class to build lucene query URLs. + * Optionally takes a base lucene query object. + */ +export class QueryBuilder { + dbName: string + index: string + query: SearchFilters + limit: number + sort?: string + bookmark?: string + sortOrder: string + sortType: string + includeDocs: boolean + version?: string + indexBuilder?: () => Promise + noEscaping = false + + constructor(dbName: string, index: string, base?: SearchFilters) { + this.dbName = dbName + this.index = index + this.query = { + allOr: false, + string: {}, + fuzzy: {}, + range: {}, + equal: {}, + notEqual: {}, + empty: {}, + notEmpty: {}, + oneOf: {}, + contains: {}, + notContains: {}, + containsAny: {}, + ...base, + } + this.limit = 50 + this.sortOrder = "ascending" + this.sortType = "string" + this.includeDocs = true + } + + disableEscaping() { + this.noEscaping = true + return this + } + + setIndexBuilder(builderFn: () => Promise) { + this.indexBuilder = builderFn + return this + } + + setVersion(version?: string) { + if (version != null) { + this.version = version + } + return this + } + + setTable(tableId: string) { + this.query.equal!.tableId = tableId + return this + } + + setLimit(limit?: number) { + if (limit != null) { + this.limit = limit + } + return this + } + + setSort(sort?: string) { + if (sort != null) { + this.sort = sort + } + return this + } + + setSortOrder(sortOrder?: string) { + if (sortOrder != null) { + this.sortOrder = sortOrder + } + return this + } + + setSortType(sortType?: string) { + if (sortType != null) { + this.sortType = sortType + } + return this + } + + setBookmark(bookmark?: string) { + if (bookmark != null) { + this.bookmark = bookmark + } + return this + } + + excludeDocs() { + this.includeDocs = false + return this + } + + addString(key: string, partial: string) { + this.query.string![key] = partial + return this + } + + addFuzzy(key: string, fuzzy: string) { + this.query.fuzzy![key] = fuzzy + return this + } + + addRange(key: string, low: string | number, high: string | number) { + this.query.range![key] = { + low, + high, + } + return this + } + + addEqual(key: string, value: any) { + this.query.equal![key] = value + return this + } + + addNotEqual(key: string, value: any) { + this.query.notEqual![key] = value + return this + } + + addEmpty(key: string, value: any) { + this.query.empty![key] = value + return this + } + + addNotEmpty(key: string, value: any) { + this.query.notEmpty![key] = value + return this + } + + addOneOf(key: string, value: any) { + this.query.oneOf![key] = value + return this + } + + addContains(key: string, value: any) { + this.query.contains![key] = value + return this + } + + addNotContains(key: string, value: any) { + this.query.notContains![key] = value + return this + } + + addContainsAny(key: string, value: any) { + this.query.containsAny![key] = value + return this + } + + handleSpaces(input: string) { + if (this.noEscaping) { + return input + } else { + return input.replace(/ /g, "_") + } + } + + /** + * Preprocesses a value before going into a lucene search. + * Transforms strings to lowercase and wraps strings and bools in quotes. + * @param value The value to process + * @param options The preprocess options + * @returns {string|*} + */ + preprocess(value: any, { escape, lowercase, wrap, type }: any = {}) { + const hasVersion = !!this.version + // Determine if type needs wrapped + const originalType = typeof value + // Convert to lowercase + if (value && lowercase) { + value = value.toLowerCase ? value.toLowerCase() : value + } + // Escape characters + if (!this.noEscaping && escape && originalType === "string") { + value = `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&") + } + + // Wrap in quotes + if (originalType === "string" && !isNaN(value) && !type) { + value = `"${value}"` + } else if (hasVersion && wrap) { + value = originalType === "number" ? value : `"${value}"` + } + return value + } + + buildSearchQuery() { + const builder = this + let allOr = this.query && this.query.allOr + let query = allOr ? "" : "*:*" + const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true } + let tableId + if (this.query.equal!.tableId) { + tableId = this.query.equal!.tableId + delete this.query.equal!.tableId + } + + const equal = (key: string, value: any) => { + // 0 evaluates to false, which means we would return all rows if we don't check it + if (!value && value !== 0) { + return null + } + return `${key}:${builder.preprocess(value, allPreProcessingOpts)}` + } + + const contains = (key: string, value: any, mode = "AND") => { + if (Array.isArray(value) && value.length === 0) { + return null + } + if (!Array.isArray(value)) { + return `${key}:${value}` + } + let statement = `${builder.preprocess(value[0], { escape: true })}` + for (let i = 1; i < value.length; i++) { + statement += ` ${mode} ${builder.preprocess(value[i], { + escape: true, + })}` + } + return `${key}:(${statement})` + } + + const notContains = (key: string, value: any) => { + // @ts-ignore + const allPrefix = allOr === "" ? "*:* AND" : "" + return allPrefix + "NOT " + contains(key, value) + } + + const containsAny = (key: string, value: any) => { + return contains(key, value, "OR") + } + + const oneOf = (key: string, value: any) => { + if (!Array.isArray(value)) { + if (typeof value === "string") { + value = value.split(",") + } else { + return "" + } + } + let orStatement = `${builder.preprocess(value[0], allPreProcessingOpts)}` + for (let i = 1; i < value.length; i++) { + orStatement += ` OR ${builder.preprocess( + value[i], + allPreProcessingOpts + )}` + } + return `${key}:(${orStatement})` + } + + function build(structure: any, queryFn: any) { + for (let [key, value] of Object.entries(structure)) { + // check for new format - remove numbering if needed + key = removeKeyNumbering(key) + key = builder.preprocess(builder.handleSpaces(key), { + escape: true, + }) + const expression = queryFn(key, value) + if (expression == null) { + continue + } + if (query.length > 0) { + query += ` ${allOr ? "OR" : "AND"} ` + } + query += expression + } + } + + // Construct the actual lucene search query string from JSON structure + if (this.query.string) { + build(this.query.string, (key: string, value: any) => { + if (!value) { + return null + } + value = builder.preprocess(value, { + escape: true, + lowercase: true, + type: "string", + }) + return `${key}:${value}*` + }) + } + if (this.query.range) { + build(this.query.range, (key: string, value: any) => { + if (!value) { + return null + } + if (value.low == null || value.low === "") { + return null + } + if (value.high == null || value.high === "") { + return null + } + const low = builder.preprocess(value.low, allPreProcessingOpts) + const high = builder.preprocess(value.high, allPreProcessingOpts) + return `${key}:[${low} TO ${high}]` + }) + } + if (this.query.fuzzy) { + build(this.query.fuzzy, (key: string, value: any) => { + if (!value) { + return null + } + value = builder.preprocess(value, { + escape: true, + lowercase: true, + type: "fuzzy", + }) + return `${key}:${value}~` + }) + } + if (this.query.equal) { + build(this.query.equal, equal) + } + if (this.query.notEqual) { + build(this.query.notEqual, (key: string, value: any) => { + if (!value) { + return null + } + return `!${key}:${builder.preprocess(value, allPreProcessingOpts)}` + }) + } + if (this.query.empty) { + build(this.query.empty, (key: string) => `!${key}:["" TO *]`) + } + if (this.query.notEmpty) { + build(this.query.notEmpty, (key: string) => `${key}:["" TO *]`) + } + if (this.query.oneOf) { + build(this.query.oneOf, oneOf) + } + if (this.query.contains) { + build(this.query.contains, contains) + } + if (this.query.notContains) { + build(this.query.notContains, notContains) + } + if (this.query.containsAny) { + build(this.query.containsAny, containsAny) + } + // make sure table ID is always added as an AND + if (tableId) { + query = `(${query})` + allOr = false + build({ tableId }, equal) + } + return query + } + + buildSearchBody() { + let body: any = { + q: this.buildSearchQuery(), + limit: Math.min(this.limit, 200), + include_docs: this.includeDocs, + } + if (this.bookmark) { + body.bookmark = this.bookmark + } + if (this.sort) { + const order = this.sortOrder === "descending" ? "-" : "" + const type = `<${this.sortType}>` + body.sort = `${order}${this.handleSpaces(this.sort)}${type}` + } + return body + } + + async run() { + const { url, cookie } = getCouchInfo() + const fullPath = `${url}/${this.dbName}/_design/database/_search/${this.index}` + const body = this.buildSearchBody() + try { + return await runQuery(fullPath, body, cookie) + } catch (err: any) { + if (err.status === 404 && this.indexBuilder) { + await this.indexBuilder() + return await runQuery(fullPath, body, cookie) + } else { + throw err + } + } + } +} + +/** + * Executes a lucene search query. + * @param url The query URL + * @param body The request body defining search criteria + * @param cookie The auth cookie for CouchDB + * @returns {Promise<{rows: []}>} + */ +async function runQuery( + url: string, + body: any, + cookie: string +): Promise> { + const response = await fetch(url, { + body: JSON.stringify(body), + method: "POST", + headers: { + Authorization: cookie, + }, + }) + + if (response.status === 404) { + throw response + } + const json = await response.json() + + let output: any = { + rows: [], + } + if (json.rows != null && json.rows.length > 0) { + output.rows = json.rows.map((row: any) => row.doc) + } + if (json.bookmark) { + output.bookmark = json.bookmark + } + return output +} + +/** + * Gets round the fixed limit of 200 results from a query by fetching as many + * pages as required and concatenating the results. This recursively operates + * until enough results have been found. + * @param dbName {string} Which database to run a lucene query on + * @param index {string} Which search index to utilise + * @param query {object} The JSON query structure + * @param params {object} The search params including: + * tableId {string} The table ID to search + * sort {string} The sort column + * sortOrder {string} The sort order ("ascending" or "descending") + * sortType {string} Whether to treat sortable values as strings or + * numbers. ("string" or "number") + * limit {number} The number of results to fetch + * bookmark {string|null} Current bookmark in the recursive search + * rows {array|null} Current results in the recursive search + * @returns {Promise<*[]|*>} + */ +async function recursiveSearch( + dbName: string, + index: string, + query: any, + params: any +): Promise { + const bookmark = params.bookmark + const rows = params.rows || [] + if (rows.length >= params.limit) { + return rows + } + let pageSize = 200 + if (rows.length > params.limit - 200) { + pageSize = params.limit - rows.length + } + const page = await new QueryBuilder(dbName, index, query) + .setVersion(params.version) + .setTable(params.tableId) + .setBookmark(bookmark) + .setLimit(pageSize) + .setSort(params.sort) + .setSortOrder(params.sortOrder) + .setSortType(params.sortType) + .run() + if (!page.rows.length) { + return rows + } + if (page.rows.length < 200) { + return [...rows, ...page.rows] + } + const newParams = { + ...params, + bookmark: page.bookmark, + rows: [...rows, ...page.rows], + } + return await recursiveSearch(dbName, index, query, newParams) +} + +/** + * Performs a paginated search. A bookmark will be returned to allow the next + * page to be fetched. There is a max limit off 200 results per page in a + * paginated search. + * @param dbName {string} Which database to run a lucene query on + * @param index {string} Which search index to utilise + * @param query {object} The JSON query structure + * @param params {object} The search params including: + * tableId {string} The table ID to search + * sort {string} The sort column + * sortOrder {string} The sort order ("ascending" or "descending") + * sortType {string} Whether to treat sortable values as strings or + * numbers. ("string" or "number") + * limit {number} The desired page size + * bookmark {string} The bookmark to resume from + * @returns {Promise<{hasNextPage: boolean, rows: *[]}>} + */ +export async function paginatedSearch( + dbName: string, + index: string, + query: SearchFilters, + params: SearchParams +) { + let limit = params.limit + if (limit == null || isNaN(limit) || limit < 0) { + limit = 50 + } + limit = Math.min(limit, 200) + const search = new QueryBuilder(dbName, index, query) + if (params.version) { + search.setVersion(params.version) + } + if (params.tableId) { + search.setTable(params.tableId) + } + if (params.sort) { + search + .setSort(params.sort) + .setSortOrder(params.sortOrder) + .setSortType(params.sortType) + } + if (params.indexer) { + search.setIndexBuilder(params.indexer) + } + if (params.disableEscaping) { + search.disableEscaping() + } + const searchResults = await search + .setBookmark(params.bookmark) + .setLimit(limit) + .run() + + // Try fetching 1 row in the next page to see if another page of results + // exists or not + search.setBookmark(searchResults.bookmark).setLimit(1) + if (params.tableId) { + search.setTable(params.tableId) + } + const nextResults = await search.run() + + return { + ...searchResults, + hasNextPage: nextResults.rows && nextResults.rows.length > 0, + } +} + +/** + * Performs a full search, fetching multiple pages if required to return the + * desired amount of results. There is a limit of 1000 results to avoid + * heavy performance hits, and to avoid client components breaking from + * handling too much data. + * @param dbName {string} Which database to run a lucene query on + * @param index {string} Which search index to utilise + * @param query {object} The JSON query structure + * @param params {object} The search params including: + * tableId {string} The table ID to search + * sort {string} The sort column + * sortOrder {string} The sort order ("ascending" or "descending") + * sortType {string} Whether to treat sortable values as strings or + * numbers. ("string" or "number") + * limit {number} The desired number of results + * @returns {Promise<{rows: *}>} + */ +export async function fullSearch( + dbName: string, + index: string, + query: SearchFilters, + params: SearchParams +) { + let limit = params.limit + if (limit == null || isNaN(limit) || limit < 0) { + limit = 1000 + } + params.limit = Math.min(limit, 1000) + const rows = await recursiveSearch(dbName, index, query, params) + return { rows } +} diff --git a/packages/backend-core/src/db/tests/index.spec.js b/packages/backend-core/src/db/tests/index.spec.js index fc0094d354..0d257f7ed7 100644 --- a/packages/backend-core/src/db/tests/index.spec.js +++ b/packages/backend-core/src/db/tests/index.spec.js @@ -1,19 +1,19 @@ require("../../../tests") -const { getDB } = require("../") +const { structures } = require("../../../tests") +const { getDB } = require("../db") -describe("db", () => { - +describe("db", () => { describe("getDB", () => { it("returns a db", async () => { - const db = getDB("test") + + const dbName = structures.db.id() + const db = getDB(dbName) expect(db).toBeDefined() - expect(db._adapter).toBe("memory") - expect(db.prefix).toBe("_pouch_") - expect(db.name).toBe("test") + expect(db.name).toBe(dbName) }) it("uses the custom put function", async () => { - const db = getDB("test") + const db = getDB(structures.db.id()) let doc = { _id: "test" } await db.put(doc) doc = await db.get(doc._id) @@ -23,4 +23,3 @@ describe("db", () => { }) }) }) - diff --git a/packages/backend-core/src/db/tests/lucene.spec.ts b/packages/backend-core/src/db/tests/lucene.spec.ts new file mode 100644 index 0000000000..23b01e18df --- /dev/null +++ b/packages/backend-core/src/db/tests/lucene.spec.ts @@ -0,0 +1,161 @@ +import { newid } from "../../newid" +import { getDB } from "../db" +import { Database } from "@budibase/types" +import { QueryBuilder, paginatedSearch, fullSearch } from "../lucene" + +const INDEX_NAME = "main" + +const index = `function(doc) { + let props = ["property", "number"] + for (let key of props) { + if (doc[key]) { + index(key, doc[key]) + } + } +}` + +describe("lucene", () => { + let db: Database, dbName: string + + beforeAll(async () => { + dbName = `db-${newid()}` + // create the DB for testing + db = getDB(dbName) + await db.put({ _id: newid(), property: "word" }) + await db.put({ _id: newid(), property: "word2" }) + await db.put({ _id: newid(), property: "word3", number: 1 }) + }) + + it("should be able to create a lucene index", async () => { + const response = await db.put({ + _id: "_design/database", + indexes: { + [INDEX_NAME]: { + index: index, + analyzer: "standard", + }, + }, + }) + expect(response.ok).toBe(true) + }) + + describe("query builder", () => { + it("should be able to perform a basic query", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.setSort("property") + builder.setSortOrder("desc") + builder.setSortType("string") + const resp = await builder.run() + expect(resp.rows.length).toBe(3) + }) + + it("should handle limits", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.setLimit(1) + const resp = await builder.run() + expect(resp.rows.length).toBe(1) + }) + + it("should be able to perform a string search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addString("property", "wo") + const resp = await builder.run() + expect(resp.rows.length).toBe(3) + }) + + it("should be able to perform a range search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addRange("number", 0, 1) + const resp = await builder.run() + expect(resp.rows.length).toBe(1) + }) + + it("should be able to perform an equal search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addEqual("property", "word2") + const resp = await builder.run() + expect(resp.rows.length).toBe(1) + }) + + it("should be able to perform a not equal search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addNotEqual("property", "word2") + const resp = await builder.run() + expect(resp.rows.length).toBe(2) + }) + + it("should be able to perform an empty search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addEmpty("number", true) + const resp = await builder.run() + expect(resp.rows.length).toBe(2) + }) + + it("should be able to perform a not empty search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addNotEmpty("number", true) + const resp = await builder.run() + expect(resp.rows.length).toBe(1) + }) + + it("should be able to perform a one of search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addOneOf("property", ["word", "word2"]) + const resp = await builder.run() + expect(resp.rows.length).toBe(2) + }) + + it("should be able to perform a contains search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addContains("property", ["word"]) + const resp = await builder.run() + expect(resp.rows.length).toBe(1) + }) + + it("should be able to perform a not contains search", async () => { + const builder = new QueryBuilder(dbName, INDEX_NAME) + builder.addNotContains("property", ["word2"]) + const resp = await builder.run() + expect(resp.rows.length).toBe(2) + }) + }) + + describe("paginated search", () => { + it("should be able to perform a paginated search", async () => { + const page = await paginatedSearch( + dbName, + INDEX_NAME, + { + string: { + property: "wo", + }, + }, + { + limit: 1, + sort: "property", + sortType: "string", + sortOrder: "desc", + } + ) + expect(page.rows.length).toBe(1) + expect(page.hasNextPage).toBe(true) + expect(page.bookmark).toBeDefined() + }) + }) + + describe("full search", () => { + it("should be able to perform a full search", async () => { + const page = await fullSearch( + dbName, + INDEX_NAME, + { + string: { + property: "wo", + }, + }, + {} + ) + expect(page.rows.length).toBe(3) + }) + }) +}) diff --git a/packages/backend-core/src/db/tests/utils.spec.ts b/packages/backend-core/src/db/tests/utils.spec.ts index 37b7ce51e2..138457c65e 100644 --- a/packages/backend-core/src/db/tests/utils.spec.ts +++ b/packages/backend-core/src/db/tests/utils.spec.ts @@ -1,17 +1,13 @@ -require("../../../tests") -const { +import { getDevelopmentAppID, getProdAppID, isDevAppID, isProdAppID, -} = require("../conversions") -const { generateAppID, getPlatformUrl, getScopedConfig } = require("../utils") -const tenancy = require("../../tenancy") -const { Config, DEFAULT_TENANT_ID } = require("../../constants") -import env from "../../environment" +} from "../conversions" +import { generateAppID } from "../utils" describe("utils", () => { - describe("app ID manipulation", () => { + describe("generateAppID", () => { function getID() { const appId = generateAppID() const split = appId.split("_") @@ -65,124 +61,3 @@ describe("utils", () => { }) }) }) - -const DB_URL = "http://dburl.com" -const DEFAULT_URL = "http://localhost:10000" -const ENV_URL = "http://env.com" - -const setDbPlatformUrl = async () => { - const db = tenancy.getGlobalDB() - db.put({ - _id: "config_settings", - type: Config.SETTINGS, - config: { - platformUrl: DB_URL, - }, - }) -} - -const clearSettingsConfig = async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - const db = tenancy.getGlobalDB() - try { - const config = await db.get("config_settings") - await db.remove("config_settings", config._rev) - } catch (e: any) { - if (e.status !== 404) { - throw e - } - } - }) -} - -describe("getPlatformUrl", () => { - describe("self host", () => { - beforeEach(async () => { - env._set("SELF_HOST", 1) - await clearSettingsConfig() - }) - - it("gets the default url", async () => { - await tenancy.doInTenant(null, async () => { - const url = await getPlatformUrl() - expect(url).toBe(DEFAULT_URL) - }) - }) - - it("gets the platform url from the environment", async () => { - await tenancy.doInTenant(null, async () => { - env._set("PLATFORM_URL", ENV_URL) - const url = await getPlatformUrl() - expect(url).toBe(ENV_URL) - }) - }) - - it("gets the platform url from the database", async () => { - await tenancy.doInTenant(null, async () => { - await setDbPlatformUrl() - const url = await getPlatformUrl() - expect(url).toBe(DB_URL) - }) - }) - }) - - describe("cloud", () => { - const TENANT_AWARE_URL = "http://default.env.com" - - beforeEach(async () => { - env._set("SELF_HOSTED", 0) - env._set("MULTI_TENANCY", 1) - env._set("PLATFORM_URL", ENV_URL) - await clearSettingsConfig() - }) - - it("gets the platform url from the environment without tenancy", async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - const url = await getPlatformUrl({ tenantAware: false }) - expect(url).toBe(ENV_URL) - }) - }) - - it("gets the platform url from the environment with tenancy", async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - const url = await getPlatformUrl() - expect(url).toBe(TENANT_AWARE_URL) - }) - }) - - it("never gets the platform url from the database", async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - await setDbPlatformUrl() - const url = await getPlatformUrl() - expect(url).toBe(TENANT_AWARE_URL) - }) - }) - }) -}) - -describe("getScopedConfig", () => { - describe("settings config", () => { - beforeEach(async () => { - env._set("SELF_HOSTED", 1) - env._set("PLATFORM_URL", "") - await clearSettingsConfig() - }) - - it("returns the platform url with an existing config", async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - await setDbPlatformUrl() - const db = tenancy.getGlobalDB() - const config = await getScopedConfig(db, { type: Config.SETTINGS }) - expect(config.platformUrl).toBe(DB_URL) - }) - }) - - it("returns the platform url without an existing config", async () => { - await tenancy.doInTenant(DEFAULT_TENANT_ID, async () => { - const db = tenancy.getGlobalDB() - const config = await getScopedConfig(db, { type: Config.SETTINGS }) - expect(config.platformUrl).toBe(DEFAULT_URL) - }) - }) - }) -}) diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index 5e501c8d22..76c52d08ad 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -9,12 +9,11 @@ import { InternalTable, APP_PREFIX, } from "../constants" -import { getTenantId, getGlobalDB, getGlobalDBName } from "../context" -import { doWithDB, allDbs, directCouchAllDbs } from "./db" +import { getTenantId, getGlobalDBName } from "../context" +import { doWithDB, directCouchAllDbs } from "./db" import { getAppMetadata } from "../cache/appMetadata" import { isDevApp, isDevAppID, getProdAppID } from "./conversions" -import * as events from "../events" -import { App, Database, ConfigType, isSettingsConfig } from "@budibase/types" +import { App, Database } from "@budibase/types" /** * Generates a new app ID. @@ -262,10 +261,7 @@ export function getStartEndKeyURL(baseKey: any, tenantId?: string) { */ export async function getAllDbs(opts = { efficient: false }) { const efficient = opts && opts.efficient - // specifically for testing we use the pouch package for this - if (env.isTest()) { - return allDbs() - } + let dbs: any[] = [] async function addDbs(queryString?: string) { const json = await directCouchAllDbs(queryString) @@ -369,6 +365,16 @@ export async function getAllApps({ } } +export async function getAppsByIDs(appIds: string[]) { + const settled = await Promise.allSettled( + appIds.map(appId => getAppMetadata(appId)) + ) + // have to list the apps which exist, some may have been deleted + return settled + .filter(promise => promise.status === "fulfilled") + .map(promise => (promise as PromiseFulfilledResult).value) +} + /** * Utility function for getAllApps but filters to production apps only. */ @@ -385,6 +391,16 @@ export async function getDevAppIDs() { return apps.filter((id: any) => isDevAppID(id)) } +export function isSameAppID( + appId1: string | undefined, + appId2: string | undefined +) { + if (appId1 == undefined || appId2 == undefined) { + return false + } + return getProdAppID(appId1) === getProdAppID(appId2) +} + export async function dbExists(dbName: any) { return doWithDB( dbName, @@ -395,32 +411,6 @@ export async function dbExists(dbName: any) { ) } -/** - * Generates a new configuration ID. - * @returns {string} The new configuration ID which the config doc can be stored under. - */ -export const generateConfigID = ({ type, workspace, user }: any) => { - const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR) - - return `${DocumentType.CONFIG}${SEPARATOR}${scope}` -} - -/** - * Gets parameters for retrieving configurations. - */ -export const getConfigParams = ( - { type, workspace, user }: any, - otherProps = {} -) => { - const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR) - - return { - ...otherProps, - startkey: `${DocumentType.CONFIG}${SEPARATOR}${scope}`, - endkey: `${DocumentType.CONFIG}${SEPARATOR}${scope}${UNICODE_MAX}`, - } -} - /** * Generates a new dev info document ID - this is scoped to a user. * @returns {string} The new dev info ID which info for dev (like api key) can be stored under. @@ -444,109 +434,6 @@ export const getPluginParams = (pluginId?: string | null, otherProps = {}) => { return getDocParams(DocumentType.PLUGIN, pluginId, otherProps) } -/** - * Returns the most granular configuration document from the DB based on the type, workspace and userID passed. - * @param {Object} db - db instance to query - * @param {Object} scopes - the type, workspace and userID scopes of the configuration. - * @returns The most granular configuration document based on the scope. - */ -export const getScopedFullConfig = async function ( - db: any, - { type, user, workspace }: any -) { - const response = await db.allDocs( - getConfigParams( - { type, user, workspace }, - { - include_docs: true, - } - ) - ) - - function determineScore(row: any) { - const config = row.doc - - // Config is specific to a user and a workspace - if (config._id.includes(generateConfigID({ type, user, workspace }))) { - return 4 - } else if (config._id.includes(generateConfigID({ type, user }))) { - // Config is specific to a user only - return 3 - } else if (config._id.includes(generateConfigID({ type, workspace }))) { - // Config is specific to a workspace only - return 2 - } else if (config._id.includes(generateConfigID({ type }))) { - // Config is specific to a type only - return 1 - } - return 0 - } - - // Find the config with the most granular scope based on context - let scopedConfig = response.rows.sort( - (a: any, b: any) => determineScore(a) - determineScore(b) - )[0] - - // custom logic for settings doc - if (type === ConfigType.SETTINGS) { - if (!scopedConfig || !scopedConfig.doc) { - // defaults - scopedConfig = { - doc: { - _id: generateConfigID({ type, user, workspace }), - type: ConfigType.SETTINGS, - config: { - platformUrl: await getPlatformUrl({ tenantAware: true }), - analyticsEnabled: await events.analytics.enabled(), - }, - }, - } - } - - // will always be true - use assertion function to get type access - if (isSettingsConfig(scopedConfig.doc)) { - // overrides affected by environment - scopedConfig.doc.config.platformUrl = await getPlatformUrl({ - tenantAware: true, - }) - scopedConfig.doc.config.analyticsEnabled = - await events.analytics.enabled() - } - } - - return scopedConfig && scopedConfig.doc -} - -export const getPlatformUrl = async (opts = { tenantAware: true }) => { - let platformUrl = env.PLATFORM_URL || "http://localhost:10000" - - if (!env.SELF_HOSTED && env.MULTI_TENANCY && opts.tenantAware) { - // cloud and multi tenant - add the tenant to the default platform url - const tenantId = getTenantId() - if (!platformUrl.includes("localhost:")) { - platformUrl = platformUrl.replace("://", `://${tenantId}.`) - } - } else if (env.SELF_HOSTED) { - const db = getGlobalDB() - // get the doc directly instead of with getScopedConfig to prevent loop - let settings - try { - settings = await db.get(generateConfigID({ type: ConfigType.SETTINGS })) - } catch (e: any) { - if (e.status !== 404) { - throw e - } - } - - // self hosted - check for platform url override - if (settings && settings.config && settings.config.platformUrl) { - platformUrl = settings.config.platformUrl - } - } - - return platformUrl -} - export function pagination( data: any[], pageSize: number, @@ -580,8 +467,3 @@ export function pagination( nextPage, } } - -export async function getScopedConfig(db: any, params: any) { - const configDoc = await getScopedFullConfig(db, params) - return configDoc && configDoc.config ? configDoc.config : configDoc -} diff --git a/packages/backend-core/src/db/views.ts b/packages/backend-core/src/db/views.ts index 4a87be0a68..8a2c2e7efd 100644 --- a/packages/backend-core/src/db/views.ts +++ b/packages/backend-core/src/db/views.ts @@ -1,13 +1,14 @@ import { - DocumentType, - ViewName, DeprecatedViews, + DocumentType, SEPARATOR, StaticDatabases, + ViewName, } from "../constants" import { getGlobalDB } from "../context" import { doWithDB } from "./" import { Database, DatabaseQueryOpts } from "@budibase/types" +import env from "../environment" const DESIGN_DB = "_design/database" @@ -69,17 +70,6 @@ export const createNewUserEmailView = async () => { await createView(db, viewJs, ViewName.USER_BY_EMAIL) } -export const createAccountEmailView = async () => { - const viewJs = `function(doc) { - if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) { - emit(doc.email.toLowerCase(), doc._id) - } - }` - await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => { - await createView(db, viewJs, ViewName.ACCOUNT_BY_EMAIL) - }) -} - export const createUserAppView = async () => { const db = getGlobalDB() const viewJs = `function(doc) { @@ -113,17 +103,6 @@ export const createUserBuildersView = async () => { await createView(db, viewJs, ViewName.USER_BY_BUILDERS) } -export const createPlatformUserView = async () => { - const viewJs = `function(doc) { - if (doc.tenantId) { - emit(doc._id.toLowerCase(), doc._id) - } - }` - await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => { - await createView(db, viewJs, ViewName.PLATFORM_USERS_LOWERCASE) - }) -} - export interface QueryViewOptions { arrayResponse?: boolean } @@ -162,13 +141,48 @@ export const queryView = async ( } } +// PLATFORM + +async function createPlatformView(viewJs: string, viewName: ViewName) { + try { + await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => { + await createView(db, viewJs, viewName) + }) + } catch (e: any) { + if (e.status === 409 && env.isTest()) { + // multiple tests can try to initialise platforms views + // at once - safe to exit on conflict + return + } + throw e + } +} + +export const createPlatformAccountEmailView = async () => { + const viewJs = `function(doc) { + if (doc._id.startsWith("${DocumentType.ACCOUNT_METADATA}${SEPARATOR}")) { + emit(doc.email.toLowerCase(), doc._id) + } + }` + await createPlatformView(viewJs, ViewName.ACCOUNT_BY_EMAIL) +} + +export const createPlatformUserView = async () => { + const viewJs = `function(doc) { + if (doc.tenantId) { + emit(doc._id.toLowerCase(), doc._id) + } + }` + await createPlatformView(viewJs, ViewName.PLATFORM_USERS_LOWERCASE) +} + export const queryPlatformView = async ( viewName: ViewName, params: DatabaseQueryOpts, opts?: QueryViewOptions ): Promise => { const CreateFuncByName: any = { - [ViewName.ACCOUNT_BY_EMAIL]: createAccountEmailView, + [ViewName.ACCOUNT_BY_EMAIL]: createPlatformAccountEmailView, [ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView, } diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index d742ca1cc9..8dc2cce487 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -28,6 +28,8 @@ const DefaultBucketName = { PLUGINS: "plugins", } +const selfHosted = !!parseInt(process.env.SELF_HOSTED || "") + const environment = { isTest, isJest, @@ -44,8 +46,9 @@ const environment = { GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, SALT_ROUNDS: process.env.SALT_ROUNDS, - REDIS_URL: process.env.REDIS_URL, - REDIS_PASSWORD: process.env.REDIS_PASSWORD, + REDIS_URL: process.env.REDIS_URL || "localhost:6379", + REDIS_PASSWORD: process.env.REDIS_PASSWORD || "budibase", + MOCK_REDIS: process.env.MOCK_REDIS, MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, AWS_REGION: process.env.AWS_REGION, @@ -57,7 +60,7 @@ const environment = { process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app", ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY || "", DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL, - SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED || ""), + SELF_HOSTED: selfHosted, COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, PLATFORM_URL: process.env.PLATFORM_URL || "", POSTHOG_TOKEN: process.env.POSTHOG_TOKEN, @@ -82,6 +85,24 @@ const environment = { SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD, DEPLOYMENT_ENVIRONMENT: process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose", + ENABLE_4XX_HTTP_LOGGING: process.env.ENABLE_4XX_HTTP_LOGGING || true, + ENABLE_AUDIT_LOG_IP_ADDR: process.env.ENABLE_AUDIT_LOG_IP_ADDR, + // smtp + SMTP_FALLBACK_ENABLED: process.env.SMTP_FALLBACK_ENABLED, + SMTP_USER: process.env.SMTP_USER, + SMTP_PASSWORD: process.env.SMTP_PASSWORD, + SMTP_HOST: process.env.SMTP_HOST, + SMTP_PORT: parseInt(process.env.SMTP_PORT || ""), + SMTP_FROM_ADDRESS: process.env.SMTP_FROM_ADDRESS, + DISABLE_JWT_WARNING: process.env.DISABLE_JWT_WARNING, + /** + * Enable to allow an admin user to login using a password. + * This can be useful to prevent lockout when configuring SSO. + * However, this should be turned OFF by default for security purposes. + */ + ENABLE_SSO_MAINTENANCE_MODE: selfHosted + ? process.env.ENABLE_SSO_MAINTENANCE_MODE + : false, _set(key: any, value: any) { process.env[key] = value // @ts-ignore diff --git a/packages/backend-core/src/events/analytics.ts b/packages/backend-core/src/events/analytics.ts index f621a9c98b..dcfd6d5104 100644 --- a/packages/backend-core/src/events/analytics.ts +++ b/packages/backend-core/src/events/analytics.ts @@ -1,55 +1,6 @@ -import env from "../environment" -import * as tenancy from "../tenancy" -import * as dbUtils from "../db/utils" -import { Config } from "../constants" -import { withCache, TTL, CacheKey } from "../cache" +import * as configs from "../configs" +// wrapper utility function export const enabled = async () => { - // cloud - always use the environment variable - if (!env.SELF_HOSTED) { - return !!env.ENABLE_ANALYTICS - } - - // self host - prefer the settings doc - // use cache as events have high throughput - const enabledInDB = await withCache( - CacheKey.ANALYTICS_ENABLED, - TTL.ONE_DAY, - async () => { - const settings = await getSettingsDoc() - - // need to do explicit checks in case the field is not set - if (settings?.config?.analyticsEnabled === false) { - return false - } else if (settings?.config?.analyticsEnabled === true) { - return true - } - } - ) - - if (enabledInDB !== undefined) { - return enabledInDB - } - - // fallback to the environment variable - // explicitly check for 0 or false here, undefined or otherwise is treated as true - const envEnabled: any = env.ENABLE_ANALYTICS - if (envEnabled === 0 || envEnabled === false) { - return false - } else { - return true - } -} - -const getSettingsDoc = async () => { - const db = tenancy.getGlobalDB() - let settings - try { - settings = await db.get(dbUtils.generateConfigID({ type: Config.SETTINGS })) - } catch (e: any) { - if (e.status !== 404) { - throw e - } - } - return settings + return configs.analyticsEnabled() } diff --git a/packages/backend-core/src/events/events.ts b/packages/backend-core/src/events/events.ts index 01928221a0..c2f7cf66ec 100644 --- a/packages/backend-core/src/events/events.ts +++ b/packages/backend-core/src/events/events.ts @@ -1,4 +1,4 @@ -import { Event } from "@budibase/types" +import { Event, AuditedEventFriendlyName } from "@budibase/types" import { processors } from "./processors" import identification from "./identification" import * as backfill from "./backfill" diff --git a/packages/backend-core/src/events/identification.ts b/packages/backend-core/src/events/identification.ts index 8ac22b471c..9534fb293d 100644 --- a/packages/backend-core/src/events/identification.ts +++ b/packages/backend-core/src/events/identification.ts @@ -10,18 +10,17 @@ import { isCloudAccount, isSSOAccount, TenantGroup, - SettingsConfig, CloudAccount, UserIdentity, InstallationGroup, UserContext, Group, + isSSOUser, } from "@budibase/types" import { processors } from "./processors" -import * as dbUtils from "../db/utils" -import { Config } from "../constants" import { newid } from "../utils" import * as installation from "../installation" +import * as configs from "../configs" import { withCache, TTL, CacheKey } from "../cache/generic" const pkg = require("../../package.json") @@ -88,6 +87,7 @@ const getCurrentIdentity = async (): Promise => { installationId, tenantId, environment, + hostInfo: userContext.hostInfo, } } else { throw new Error("Unknown identity type") @@ -166,7 +166,10 @@ const identifyUser = async ( const type = IdentityType.USER let builder = user.builder?.global || false let admin = user.admin?.global || false - let providerType = user.providerType + let providerType + if (isSSOUser(user)) { + providerType = user.providerType + } const accountHolder = account?.budibaseUserId === user._id || false const verified = account && account?.budibaseUserId === user._id ? account.verified : false @@ -266,9 +269,7 @@ const getUniqueTenantId = async (tenantId: string): Promise => { return context.doInTenant(tenantId, () => { return withCache(CacheKey.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => { const db = context.getGlobalDB() - const config: SettingsConfig = await dbUtils.getScopedFullConfig(db, { - type: Config.SETTINGS, - }) + const config = await configs.getSettingsConfigDoc() let uniqueTenantId: string if (config.config.uniqueTenantId) { diff --git a/packages/backend-core/src/events/processors/AuditLogsProcessor.ts b/packages/backend-core/src/events/processors/AuditLogsProcessor.ts new file mode 100644 index 0000000000..94b4e1b09f --- /dev/null +++ b/packages/backend-core/src/events/processors/AuditLogsProcessor.ts @@ -0,0 +1,93 @@ +import { + Event, + Identity, + Group, + IdentityType, + AuditLogQueueEvent, + AuditLogFn, + HostInfo, +} from "@budibase/types" +import { EventProcessor } from "./types" +import { getAppId, doInTenant, getTenantId } from "../../context" +import BullQueue from "bull" +import { createQueue, JobQueue } from "../../queue" +import { isAudited } from "../../utils" +import env from "../../environment" + +export default class AuditLogsProcessor implements EventProcessor { + static auditLogsEnabled = false + static auditLogQueue: BullQueue.Queue + + // can't use constructor as need to return promise + static init(fn: AuditLogFn) { + AuditLogsProcessor.auditLogsEnabled = true + const writeAuditLogs = fn + AuditLogsProcessor.auditLogQueue = createQueue( + JobQueue.AUDIT_LOG + ) + return AuditLogsProcessor.auditLogQueue.process(async job => { + return doInTenant(job.data.tenantId, async () => { + let properties = job.data.properties + if (properties.audited) { + properties = { + ...properties, + ...properties.audited, + } + delete properties.audited + } + + // this feature is disabled by default due to privacy requirements + // in some countries - available as env var in-case it is desired + // in self host deployments + let hostInfo: HostInfo | undefined = {} + if (env.ENABLE_AUDIT_LOG_IP_ADDR) { + hostInfo = job.data.opts.hostInfo + } + + await writeAuditLogs(job.data.event, properties, { + userId: job.data.opts.userId, + timestamp: job.data.opts.timestamp, + appId: job.data.opts.appId, + hostInfo, + }) + }) + }) + } + + async processEvent( + event: Event, + identity: Identity, + properties: any, + timestamp?: string + ): Promise { + if (AuditLogsProcessor.auditLogsEnabled && isAudited(event)) { + // only audit log actual events, don't include backfills + const userId = + identity.type === IdentityType.USER ? identity.id : undefined + // add to the event queue, rather than just writing immediately + await AuditLogsProcessor.auditLogQueue.add({ + event, + properties, + opts: { + userId, + timestamp, + appId: getAppId(), + hostInfo: identity.hostInfo, + }, + tenantId: getTenantId(), + }) + } + } + + async identify(identity: Identity, timestamp?: string | number) { + // no-op + } + + async identifyGroup(group: Group, timestamp?: string | number) { + // no-op + } + + shutdown(): void { + AuditLogsProcessor.auditLogQueue?.close() + } +} diff --git a/packages/backend-core/src/events/processors/index.ts b/packages/backend-core/src/events/processors/index.ts index 0e75f050db..6646764e47 100644 --- a/packages/backend-core/src/events/processors/index.ts +++ b/packages/backend-core/src/events/processors/index.ts @@ -1,8 +1,19 @@ import AnalyticsProcessor from "./AnalyticsProcessor" import LoggingProcessor from "./LoggingProcessor" +import AuditLogsProcessor from "./AuditLogsProcessor" import Processors from "./Processors" +import { AuditLogFn } from "@budibase/types" export const analyticsProcessor = new AnalyticsProcessor() const loggingProcessor = new LoggingProcessor() +const auditLogsProcessor = new AuditLogsProcessor() -export const processors = new Processors([analyticsProcessor, loggingProcessor]) +export function init(auditingFn: AuditLogFn) { + return AuditLogsProcessor.init(auditingFn) +} + +export const processors = new Processors([ + analyticsProcessor, + loggingProcessor, + auditLogsProcessor, +]) diff --git a/packages/backend-core/src/events/processors/posthog/PosthogProcessor.ts b/packages/backend-core/src/events/processors/posthog/PosthogProcessor.ts index 593e5ff082..0dbe70d543 100644 --- a/packages/backend-core/src/events/processors/posthog/PosthogProcessor.ts +++ b/packages/backend-core/src/events/processors/posthog/PosthogProcessor.ts @@ -47,6 +47,8 @@ export default class PosthogProcessor implements EventProcessor { return } + properties = this.clearPIIProperties(properties) + properties.version = pkg.version properties.service = env.SERVICE properties.environment = identity.environment @@ -79,6 +81,16 @@ export default class PosthogProcessor implements EventProcessor { this.posthog.capture(payload) } + clearPIIProperties(properties: any) { + if (properties.email) { + delete properties.email + } + if (properties.audited) { + delete properties.audited + } + return properties + } + async identify(identity: Identity, timestamp?: string | number) { const payload: any = { distinctId: identity.id, properties: identity } if (timestamp) { diff --git a/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts b/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts index 349a0427ac..8df4e40bcf 100644 --- a/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts +++ b/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts @@ -1,4 +1,4 @@ -import "../../../../../tests" +import { testEnv } from "../../../../../tests" import PosthogProcessor from "../PosthogProcessor" import { Event, IdentityType, Hosting } from "@budibase/types" const tk = require("timekeeper") @@ -16,6 +16,10 @@ const newIdentity = () => { } describe("PosthogProcessor", () => { + beforeAll(() => { + testEnv.singleTenant() + }) + beforeEach(async () => { jest.clearAllMocks() await cache.bustCache( @@ -45,6 +49,25 @@ describe("PosthogProcessor", () => { expect(processor.posthog.capture).toHaveBeenCalledTimes(0) }) + it("removes audited information", async () => { + const processor = new PosthogProcessor("test") + + const identity = newIdentity() + const properties = { + email: "test", + audited: { + name: "test", + }, + } + + await processor.processEvent(Event.USER_CREATED, identity, properties) + expect(processor.posthog.capture).toHaveBeenCalled() + // @ts-ignore + const call = processor.posthog.capture.mock.calls[0][0] + expect(call.properties.audited).toBeUndefined() + expect(call.properties.email).toBeUndefined() + }) + describe("rate limiting", () => { it("sends daily event once in same day", async () => { const processor = new PosthogProcessor("test") diff --git a/packages/backend-core/src/events/publishers/app.ts b/packages/backend-core/src/events/publishers/app.ts index 90da21f3f5..d08d59b5f1 100644 --- a/packages/backend-core/src/events/publishers/app.ts +++ b/packages/backend-core/src/events/publishers/app.ts @@ -19,6 +19,9 @@ const created = async (app: App, timestamp?: string | number) => { const properties: AppCreatedEvent = { appId: app.appId, version: app.version, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_CREATED, properties, timestamp) } @@ -27,6 +30,9 @@ async function updated(app: App) { const properties: AppUpdatedEvent = { appId: app.appId, version: app.version, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_UPDATED, properties) } @@ -34,6 +40,9 @@ async function updated(app: App) { async function deleted(app: App) { const properties: AppDeletedEvent = { appId: app.appId, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_DELETED, properties) } @@ -41,6 +50,9 @@ async function deleted(app: App) { async function published(app: App, timestamp?: string | number) { const properties: AppPublishedEvent = { appId: app.appId, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_PUBLISHED, properties, timestamp) } @@ -48,6 +60,9 @@ async function published(app: App, timestamp?: string | number) { async function unpublished(app: App) { const properties: AppUnpublishedEvent = { appId: app.appId, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_UNPUBLISHED, properties) } @@ -55,6 +70,9 @@ async function unpublished(app: App) { async function fileImported(app: App) { const properties: AppFileImportedEvent = { appId: app.appId, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_FILE_IMPORTED, properties) } @@ -63,6 +81,9 @@ async function templateImported(app: App, templateKey: string) { const properties: AppTemplateImportedEvent = { appId: app.appId, templateKey, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_TEMPLATE_IMPORTED, properties) } @@ -76,6 +97,9 @@ async function versionUpdated( appId: app.appId, currentVersion, updatedToVersion, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_VERSION_UPDATED, properties) } @@ -89,6 +113,9 @@ async function versionReverted( appId: app.appId, currentVersion, revertedToVersion, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_VERSION_REVERTED, properties) } @@ -96,6 +123,9 @@ async function versionReverted( async function reverted(app: App) { const properties: AppRevertedEvent = { appId: app.appId, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_REVERTED, properties) } @@ -103,6 +133,9 @@ async function reverted(app: App) { async function exported(app: App) { const properties: AppExportedEvent = { appId: app.appId, + audited: { + name: app.name, + }, } await publishEvent(Event.APP_EXPORTED, properties) } diff --git a/packages/backend-core/src/events/publishers/auditLog.ts b/packages/backend-core/src/events/publishers/auditLog.ts new file mode 100644 index 0000000000..7cfb76147a --- /dev/null +++ b/packages/backend-core/src/events/publishers/auditLog.ts @@ -0,0 +1,26 @@ +import { + Event, + AuditLogSearchParams, + AuditLogFilteredEvent, + AuditLogDownloadedEvent, +} from "@budibase/types" +import { publishEvent } from "../events" + +async function filtered(search: AuditLogSearchParams) { + const properties: AuditLogFilteredEvent = { + filters: search, + } + await publishEvent(Event.AUDIT_LOGS_FILTERED, properties) +} + +async function downloaded(search: AuditLogSearchParams) { + const properties: AuditLogDownloadedEvent = { + filters: search, + } + await publishEvent(Event.AUDIT_LOGS_DOWNLOADED, properties) +} + +export default { + filtered, + downloaded, +} diff --git a/packages/backend-core/src/events/publishers/auth.ts b/packages/backend-core/src/events/publishers/auth.ts index 4436045599..e275d2dbb0 100644 --- a/packages/backend-core/src/events/publishers/auth.ts +++ b/packages/backend-core/src/events/publishers/auth.ts @@ -12,19 +12,25 @@ import { } from "@budibase/types" import { identification } from ".." -async function login(source: LoginSource) { +async function login(source: LoginSource, email: string) { const identity = await identification.getCurrentIdentity() const properties: LoginEvent = { userId: identity.id, source, + audited: { + email, + }, } await publishEvent(Event.AUTH_LOGIN, properties) } -async function logout() { +async function logout(email?: string) { const identity = await identification.getCurrentIdentity() const properties: LogoutEvent = { userId: identity.id, + audited: { + email, + }, } await publishEvent(Event.AUTH_LOGOUT, properties) } diff --git a/packages/backend-core/src/events/publishers/automation.ts b/packages/backend-core/src/events/publishers/automation.ts index 6eb36ab067..419d4136bd 100644 --- a/packages/backend-core/src/events/publishers/automation.ts +++ b/packages/backend-core/src/events/publishers/automation.ts @@ -18,6 +18,9 @@ async function created(automation: Automation, timestamp?: string | number) { automationId: automation._id as string, triggerId: automation.definition?.trigger?.id, triggerType: automation.definition?.trigger?.stepId, + audited: { + name: automation.name, + }, } await publishEvent(Event.AUTOMATION_CREATED, properties, timestamp) } @@ -38,6 +41,9 @@ async function deleted(automation: Automation) { automationId: automation._id as string, triggerId: automation.definition?.trigger?.id, triggerType: automation.definition?.trigger?.stepId, + audited: { + name: automation.name, + }, } await publishEvent(Event.AUTOMATION_DELETED, properties) } @@ -71,6 +77,9 @@ async function stepCreated( triggerType: automation.definition?.trigger?.stepId, stepId: step.id!, stepType: step.stepId, + audited: { + name: automation.name, + }, } await publishEvent(Event.AUTOMATION_STEP_CREATED, properties, timestamp) } @@ -83,6 +92,9 @@ async function stepDeleted(automation: Automation, step: AutomationStep) { triggerType: automation.definition?.trigger?.stepId, stepId: step.id!, stepType: step.stepId, + audited: { + name: automation.name, + }, } await publishEvent(Event.AUTOMATION_STEP_DELETED, properties) } diff --git a/packages/backend-core/src/events/publishers/backup.ts b/packages/backend-core/src/events/publishers/backup.ts index 12263fe1ff..d7d87f09f1 100644 --- a/packages/backend-core/src/events/publishers/backup.ts +++ b/packages/backend-core/src/events/publishers/backup.ts @@ -13,6 +13,7 @@ async function appBackupRestored(backup: AppBackup) { appId: backup.appId, restoreId: backup._id!, backupCreatedAt: backup.timestamp, + name: backup.name as string, } await publishEvent(Event.APP_BACKUP_RESTORED, properties) @@ -22,13 +23,15 @@ async function appBackupTriggered( appId: string, backupId: string, type: AppBackupType, - trigger: AppBackupTrigger + trigger: AppBackupTrigger, + name: string ) { const properties: AppBackupTriggeredEvent = { appId: appId, backupId, type, trigger, + name, } await publishEvent(Event.APP_BACKUP_TRIGGERED, properties) } diff --git a/packages/backend-core/src/events/publishers/group.ts b/packages/backend-core/src/events/publishers/group.ts index d79920562b..a000b880a2 100644 --- a/packages/backend-core/src/events/publishers/group.ts +++ b/packages/backend-core/src/events/publishers/group.ts @@ -8,12 +8,16 @@ import { GroupUsersAddedEvent, GroupUsersDeletedEvent, GroupAddedOnboardingEvent, + GroupPermissionsEditedEvent, UserGroupRoles, } from "@budibase/types" async function created(group: UserGroup, timestamp?: number) { const properties: GroupCreatedEvent = { groupId: group._id as string, + audited: { + name: group.name, + }, } await publishEvent(Event.USER_GROUP_CREATED, properties, timestamp) } @@ -21,6 +25,9 @@ async function created(group: UserGroup, timestamp?: number) { async function updated(group: UserGroup) { const properties: GroupUpdatedEvent = { groupId: group._id as string, + audited: { + name: group.name, + }, } await publishEvent(Event.USER_GROUP_UPDATED, properties) } @@ -28,6 +35,9 @@ async function updated(group: UserGroup) { async function deleted(group: UserGroup) { const properties: GroupDeletedEvent = { groupId: group._id as string, + audited: { + name: group.name, + }, } await publishEvent(Event.USER_GROUP_DELETED, properties) } @@ -36,6 +46,9 @@ async function usersAdded(count: number, group: UserGroup) { const properties: GroupUsersAddedEvent = { count, groupId: group._id as string, + audited: { + name: group.name, + }, } await publishEvent(Event.USER_GROUP_USERS_ADDED, properties) } @@ -44,6 +57,9 @@ async function usersDeleted(count: number, group: UserGroup) { const properties: GroupUsersDeletedEvent = { count, groupId: group._id as string, + audited: { + name: group.name, + }, } await publishEvent(Event.USER_GROUP_USERS_REMOVED, properties) } @@ -56,9 +72,13 @@ async function createdOnboarding(groupId: string) { await publishEvent(Event.USER_GROUP_ONBOARDING, properties) } -async function permissionsEdited(roles: UserGroupRoles) { - const properties: UserGroupRoles = { - ...roles, +async function permissionsEdited(group: UserGroup) { + const properties: GroupPermissionsEditedEvent = { + permissions: group.roles!, + groupId: group._id as string, + audited: { + name: group.name, + }, } await publishEvent(Event.USER_GROUP_PERMISSIONS_EDITED, properties) } diff --git a/packages/backend-core/src/events/publishers/index.ts b/packages/backend-core/src/events/publishers/index.ts index 34e47b2990..87a34bf3f1 100644 --- a/packages/backend-core/src/events/publishers/index.ts +++ b/packages/backend-core/src/events/publishers/index.ts @@ -21,3 +21,4 @@ export { default as group } from "./group" export { default as plugin } from "./plugin" export { default as backup } from "./backup" export { default as environmentVariable } from "./environmentVariable" +export { default as auditLog } from "./auditLog" diff --git a/packages/backend-core/src/events/publishers/screen.ts b/packages/backend-core/src/events/publishers/screen.ts index 27264b5847..df486029e8 100644 --- a/packages/backend-core/src/events/publishers/screen.ts +++ b/packages/backend-core/src/events/publishers/screen.ts @@ -11,6 +11,9 @@ async function created(screen: Screen, timestamp?: string | number) { layoutId: screen.layoutId, screenId: screen._id as string, roleId: screen.routing.roleId, + audited: { + name: screen.routing?.route, + }, } await publishEvent(Event.SCREEN_CREATED, properties, timestamp) } @@ -20,6 +23,9 @@ async function deleted(screen: Screen) { layoutId: screen.layoutId, screenId: screen._id as string, roleId: screen.routing.roleId, + audited: { + name: screen.routing?.route, + }, } await publishEvent(Event.SCREEN_DELETED, properties) } diff --git a/packages/backend-core/src/events/publishers/table.ts b/packages/backend-core/src/events/publishers/table.ts index d50f4df0e1..dc3200291a 100644 --- a/packages/backend-core/src/events/publishers/table.ts +++ b/packages/backend-core/src/events/publishers/table.ts @@ -13,6 +13,9 @@ import { async function created(table: Table, timestamp?: string | number) { const properties: TableCreatedEvent = { tableId: table._id as string, + audited: { + name: table.name, + }, } await publishEvent(Event.TABLE_CREATED, properties, timestamp) } @@ -20,6 +23,9 @@ async function created(table: Table, timestamp?: string | number) { async function updated(table: Table) { const properties: TableUpdatedEvent = { tableId: table._id as string, + audited: { + name: table.name, + }, } await publishEvent(Event.TABLE_UPDATED, properties) } @@ -27,6 +33,9 @@ async function updated(table: Table) { async function deleted(table: Table) { const properties: TableDeletedEvent = { tableId: table._id as string, + audited: { + name: table.name, + }, } await publishEvent(Event.TABLE_DELETED, properties) } @@ -35,6 +44,9 @@ async function exported(table: Table, format: TableExportFormat) { const properties: TableExportedEvent = { tableId: table._id as string, format, + audited: { + name: table.name, + }, } await publishEvent(Event.TABLE_EXPORTED, properties) } @@ -42,6 +54,9 @@ async function exported(table: Table, format: TableExportFormat) { async function imported(table: Table) { const properties: TableImportedEvent = { tableId: table._id as string, + audited: { + name: table.name, + }, } await publishEvent(Event.TABLE_IMPORTED, properties) } diff --git a/packages/backend-core/src/events/publishers/user.ts b/packages/backend-core/src/events/publishers/user.ts index 1fe50149b5..8dbc494d1e 100644 --- a/packages/backend-core/src/events/publishers/user.ts +++ b/packages/backend-core/src/events/publishers/user.ts @@ -19,6 +19,9 @@ import { async function created(user: User, timestamp?: number) { const properties: UserCreatedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_CREATED, properties, timestamp) } @@ -26,6 +29,9 @@ async function created(user: User, timestamp?: number) { async function updated(user: User) { const properties: UserUpdatedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_UPDATED, properties) } @@ -33,6 +39,9 @@ async function updated(user: User) { async function deleted(user: User) { const properties: UserDeletedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_DELETED, properties) } @@ -40,6 +49,9 @@ async function deleted(user: User) { export async function onboardingComplete(user: User) { const properties: UserOnboardingEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_ONBOARDING_COMPLETE, properties) } @@ -49,6 +61,9 @@ export async function onboardingComplete(user: User) { async function permissionAdminAssigned(user: User, timestamp?: number) { const properties: UserPermissionAssignedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent( Event.USER_PERMISSION_ADMIN_ASSIGNED, @@ -60,6 +75,9 @@ async function permissionAdminAssigned(user: User, timestamp?: number) { async function permissionAdminRemoved(user: User) { const properties: UserPermissionRemovedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_PERMISSION_ADMIN_REMOVED, properties) } @@ -67,6 +85,9 @@ async function permissionAdminRemoved(user: User) { async function permissionBuilderAssigned(user: User, timestamp?: number) { const properties: UserPermissionAssignedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent( Event.USER_PERMISSION_BUILDER_ASSIGNED, @@ -78,20 +99,30 @@ async function permissionBuilderAssigned(user: User, timestamp?: number) { async function permissionBuilderRemoved(user: User) { const properties: UserPermissionRemovedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_PERMISSION_BUILDER_REMOVED, properties) } // INVITE -async function invited() { - const properties: UserInvitedEvent = {} +async function invited(email: string) { + const properties: UserInvitedEvent = { + audited: { + email, + }, + } await publishEvent(Event.USER_INVITED, properties) } async function inviteAccepted(user: User) { const properties: UserInviteAcceptedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_INVITED_ACCEPTED, properties) } @@ -101,6 +132,9 @@ async function inviteAccepted(user: User) { async function passwordForceReset(user: User) { const properties: UserPasswordForceResetEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_PASSWORD_FORCE_RESET, properties) } @@ -108,6 +142,9 @@ async function passwordForceReset(user: User) { async function passwordUpdated(user: User) { const properties: UserPasswordUpdatedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_PASSWORD_UPDATED, properties) } @@ -115,6 +152,9 @@ async function passwordUpdated(user: User) { async function passwordResetRequested(user: User) { const properties: UserPasswordResetRequestedEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_PASSWORD_RESET_REQUESTED, properties) } @@ -122,6 +162,9 @@ async function passwordResetRequested(user: User) { async function passwordReset(user: User) { const properties: UserPasswordResetEvent = { userId: user._id as string, + audited: { + email: user.email, + }, } await publishEvent(Event.USER_PASSWORD_RESET, properties) } diff --git a/packages/backend-core/src/featureFlags/index.ts b/packages/backend-core/src/featureFlags/index.ts index 34ee3599a5..877cd60e1a 100644 --- a/packages/backend-core/src/featureFlags/index.ts +++ b/packages/backend-core/src/featureFlags/index.ts @@ -1,5 +1,5 @@ import env from "../environment" -import * as tenancy from "../tenancy" +import * as context from "../context" /** * Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant. @@ -28,7 +28,7 @@ export function buildFeatureFlags() { } export function isEnabled(featureFlag: string) { - const tenantId = tenancy.getTenantId() + const tenantId = context.getTenantId() const flags = getTenantFeatureFlags(tenantId) return flags.includes(featureFlag) } diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts index b38a53e9e4..48569548e3 100644 --- a/packages/backend-core/src/index.ts +++ b/packages/backend-core/src/index.ts @@ -1,14 +1,14 @@ +export * as configs from "./configs" export * as events from "./events" export * as migrations from "./migrations" export * as users from "./users" export * as roles from "./security/roles" export * as permissions from "./security/permissions" -export * as accounts from "./cloud/accounts" +export * as accounts from "./accounts" export * as installation from "./installation" -export * as tenancy from "./tenancy" export * as featureFlags from "./featureFlags" export * as sessions from "./security/sessions" -export * as deprovisioning from "./context/deprovision" +export * as platform from "./platform" export * as auth from "./auth" export * as constants from "./constants" export * as logging from "./logging" @@ -21,9 +21,20 @@ export * as context from "./context" export * as cache from "./cache" export * as objectStore from "./objectStore" export * as redis from "./redis" +export * as locks from "./redis/redlockImpl" export * as utils from "./utils" export * as errors from "./errors" export { default as env } from "./environment" +export { SearchParams } from "./db" +// Add context to tenancy for backwards compatibility +// only do this for external usages to prevent internal +// circular dependencies +import * as context from "./context" +import * as _tenancy from "./tenancy" +export const tenancy = { + ..._tenancy, + ...context, +} // expose error classes directly export * from "./errors" @@ -31,10 +42,6 @@ export * from "./errors" // expose constants directly export * from "./constants" -// expose inner locks from redis directly -import * as redis from "./redis" -export const locks = redis.redlock - // expose package init function import * as db from "./db" export const init = (opts: any = {}) => { diff --git a/packages/backend-core/src/installation.ts b/packages/backend-core/src/installation.ts index 4e78a508a5..64be6f3f43 100644 --- a/packages/backend-core/src/installation.ts +++ b/packages/backend-core/src/installation.ts @@ -2,7 +2,7 @@ import { newid } from "./utils" import * as events from "./events" import { StaticDatabases } from "./db" import { doWithDB } from "./db" -import { Installation, IdentityType } from "@budibase/types" +import { Installation, IdentityType, Database } from "@budibase/types" import * as context from "./context" import semver from "semver" import { bustCache, withCache, TTL, CacheKey } from "./cache/generic" @@ -14,6 +14,24 @@ export const getInstall = async (): Promise => { useTenancy: false, }) } +async function createInstallDoc(platformDb: Database) { + const install: Installation = { + _id: StaticDatabases.PLATFORM_INFO.docs.install, + installId: newid(), + version: pkg.version, + } + try { + const resp = await platformDb.put(install) + install._rev = resp.rev + return install + } catch (err: any) { + if (err.status === 409) { + return getInstallFromDB() + } else { + throw err + } + } +} const getInstallFromDB = async (): Promise => { return doWithDB( @@ -26,13 +44,7 @@ const getInstallFromDB = async (): Promise => { ) } catch (e: any) { if (e.status === 404) { - install = { - _id: StaticDatabases.PLATFORM_INFO.docs.install, - installId: newid(), - version: pkg.version, - } - const resp = await platformDb.put(install) - install._rev = resp.rev + install = await createInstallDoc(platformDb) } else { throw e } diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts index 3b5e9ae162..0708581570 100644 --- a/packages/backend-core/src/middleware/authenticated.ts +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -4,11 +4,11 @@ import { getUser } from "../cache/user" import { getSession, updateSessionTTL } from "../security/sessions" import { buildMatcherRegex, matches } from "./matchers" import { SEPARATOR, queryGlobalView, ViewName } from "../db" -import { getGlobalDB, doInTenant } from "../tenancy" +import { getGlobalDB, doInTenant } from "../context" import { decrypt } from "../security/encryption" import * as identity from "../context/identity" import env from "../environment" -import { BBContext, EndpointMatcher } from "@budibase/types" +import { Ctx, EndpointMatcher } from "@budibase/types" const ONE_MINUTE = env.SESSION_UPDATE_PERIOD ? parseInt(env.SESSION_UPDATE_PERIOD) @@ -73,7 +73,7 @@ export default function ( } ) { const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : [] - return async (ctx: BBContext | any, next: any) => { + return async (ctx: Ctx | any, next: any) => { let publicEndpoint = false const version = ctx.request.headers[Header.API_VER] // the path is not authenticated @@ -115,7 +115,8 @@ export default function ( authenticated = true } catch (err: any) { authenticated = false - console.error("Auth Error", err?.message || err) + console.error(`Auth Error: ${err.message}`) + console.error(err) // remove the cookie as the user does not exist anymore clearCookie(ctx, Cookie.Auth) } @@ -148,12 +149,13 @@ export default function ( finalise(ctx, { authenticated, user, internal, version, publicEndpoint }) if (user && user.email) { - return identity.doInUserContext(user, next) + return identity.doInUserContext(user, ctx, next) } else { return next() } } catch (err: any) { - console.error("Auth Error", err?.message || err) + console.error(`Auth Error: ${err.message}`) + console.error(err) // invalid token, clear the cookie if (err && err.name === "JsonWebTokenError") { clearCookie(ctx, Cookie.Auth) diff --git a/packages/backend-core/src/middleware/errorHandling.ts b/packages/backend-core/src/middleware/errorHandling.ts new file mode 100644 index 0000000000..36aff2cdbc --- /dev/null +++ b/packages/backend-core/src/middleware/errorHandling.ts @@ -0,0 +1,29 @@ +import { APIError } from "@budibase/types" +import * as errors from "../errors" +import env from "../environment" + +export async function errorHandling(ctx: any, next: any) { + try { + await next() + } catch (err: any) { + const status = err.status || err.statusCode || 500 + ctx.status = status + + if (status > 499 || env.ENABLE_4XX_HTTP_LOGGING) { + ctx.log.error(err) + console.trace(err) + } + + const error = errors.getPublicError(err) + const body: APIError = { + message: err.message, + status: status, + validationErrors: err.validation, + error, + } + + ctx.body = body + } +} + +export default errorHandling diff --git a/packages/backend-core/src/middleware/index.ts b/packages/backend-core/src/middleware/index.ts index 4986cde64b..addeac6a1a 100644 --- a/packages/backend-core/src/middleware/index.ts +++ b/packages/backend-core/src/middleware/index.ts @@ -1,7 +1,7 @@ export * as jwt from "./passport/jwt" export * as local from "./passport/local" -export * as google from "./passport/google" -export * as oidc from "./passport/oidc" +export * as google from "./passport/sso/google" +export * as oidc from "./passport/sso/oidc" import * as datasourceGoogle from "./passport/datasource/google" export const datasource = { google: datasourceGoogle, @@ -16,4 +16,6 @@ export { default as adminOnly } from "./adminOnly" export { default as builderOrAdmin } from "./builderOrAdmin" export { default as builderOnly } from "./builderOnly" export { default as logging } from "./logging" +export { default as errorHandling } from "./errorHandling" +export { default as querystringToBody } from "./querystringToBody" export * as joiValidator from "./joi-validator" diff --git a/packages/backend-core/src/middleware/logging.ts b/packages/backend-core/src/middleware/logging.ts index d1f2d6566b..db9b64b883 100644 --- a/packages/backend-core/src/middleware/logging.ts +++ b/packages/backend-core/src/middleware/logging.ts @@ -64,7 +64,9 @@ const print = (fn: any, data: any[]) => { message = message + ` [identityId=${identityId}]` } - fn(message, data) + if (!process.env.CI) { + fn(message, data) + } } const logging = (ctx: any, next: any) => { diff --git a/packages/backend-core/src/middleware/passport/datasource/google.ts b/packages/backend-core/src/middleware/passport/datasource/google.ts index 65620d7aa3..32451cb8d2 100644 --- a/packages/backend-core/src/middleware/passport/datasource/google.ts +++ b/packages/backend-core/src/middleware/passport/datasource/google.ts @@ -1,10 +1,10 @@ -import * as google from "../google" -import { Cookie, Config } from "../../../constants" +import * as google from "../sso/google" +import { Cookie } from "../../../constants" import { clearCookie, getCookie } from "../../../utils" -import { getScopedConfig, getPlatformUrl, doWithDB } from "../../../db" -import environment from "../../../environment" -import { getGlobalDB } from "../../../tenancy" +import { doWithDB } from "../../../db" +import * as configs from "../../../configs" import { BBContext, Database, SSOProfile } from "@budibase/types" +import { ssoSaveUserNoOp } from "../sso/sso" const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy type Passport = { @@ -12,18 +12,12 @@ type Passport = { } async function fetchGoogleCreds() { - // try and get the config from the tenant - const db = getGlobalDB() - const googleConfig = await getScopedConfig(db, { - type: Config.GOOGLE, - }) - // or fall back to env variables - return ( - googleConfig || { - clientID: environment.GOOGLE_CLIENT_ID, - clientSecret: environment.GOOGLE_CLIENT_SECRET, - } - ) + let config = await configs.getGoogleDatasourceConfig() + + if (!config) { + throw new Error("No google configuration found") + } + return config } export async function preAuth( @@ -33,10 +27,14 @@ export async function preAuth( ) { // get the relevant config const googleConfig = await fetchGoogleCreds() - const platformUrl = await getPlatformUrl({ tenantAware: false }) + const platformUrl = await configs.getPlatformUrl({ tenantAware: false }) let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback` - const strategy = await google.strategyFactory(googleConfig, callbackUrl) + const strategy = await google.strategyFactory( + googleConfig, + callbackUrl, + ssoSaveUserNoOp + ) if (!ctx.query.appId || !ctx.query.datasourceId) { ctx.throw(400, "appId and datasourceId query params not present.") @@ -56,7 +54,7 @@ export async function postAuth( ) { // get the relevant config const config = await fetchGoogleCreds() - const platformUrl = await getPlatformUrl({ tenantAware: false }) + const platformUrl = await configs.getPlatformUrl({ tenantAware: false }) let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback` const authStateCookie = getCookie(ctx, Cookie.DatasourceAuth) diff --git a/packages/backend-core/src/middleware/passport/local.ts b/packages/backend-core/src/middleware/passport/local.ts index 8b85d3734c..e198032532 100644 --- a/packages/backend-core/src/middleware/passport/local.ts +++ b/packages/backend-core/src/middleware/passport/local.ts @@ -1,15 +1,10 @@ import { UserStatus } from "../../constants" -import { compare, newid } from "../../utils" -import env from "../../environment" +import { compare } from "../../utils" import * as users from "../../users" import { authError } from "./utils" -import { createASession } from "../../security/sessions" -import { getTenantId } from "../../tenancy" import { BBContext } from "@budibase/types" -const jwt = require("jsonwebtoken") const INVALID_ERR = "Invalid credentials" -const SSO_NO_PASSWORD = "SSO user does not have a password set" const EXPIRED = "This account has expired. Please reset your password" export const options = { @@ -35,50 +30,25 @@ export async function authenticate( const dbUser = await users.getGlobalUserByEmail(email) if (dbUser == null) { - return authError(done, `User not found: [${email}]`) - } - - // check that the user is currently inactive, if this is the case throw invalid - if (dbUser.status === UserStatus.INACTIVE) { + console.info(`user=${email} could not be found`) return authError(done, INVALID_ERR) } - // check that the user has a stored password before proceeding - if (!dbUser.password) { - if ( - (dbUser.account && dbUser.account.authType === "sso") || // root account sso - dbUser.thirdPartyProfile // internal sso - ) { - return authError(done, SSO_NO_PASSWORD) - } + if (dbUser.status === UserStatus.INACTIVE) { + console.info(`user=${email} is inactive`, dbUser) + return authError(done, INVALID_ERR) + } - console.error("Non SSO usser has no password set", dbUser) + if (!dbUser.password) { + console.info(`user=${email} has no password set`, dbUser) return authError(done, EXPIRED) } - // authenticate - if (await compare(password, dbUser.password)) { - const sessionId = newid() - const tenantId = getTenantId() - - await createASession(dbUser._id!, { sessionId, tenantId }) - - const token = jwt.sign( - { - userId: dbUser._id, - sessionId, - tenantId, - }, - env.JWT_SECRET - ) - // Remove users password in payload - delete dbUser.password - - return done(null, { - ...dbUser, - token, - }) - } else { + if (!(await compare(password, dbUser.password))) { return authError(done, INVALID_ERR) } + + // intentionally remove the users password in payload + delete dbUser.password + return done(null, dbUser) } diff --git a/packages/backend-core/src/middleware/passport/google.ts b/packages/backend-core/src/middleware/passport/sso/google.ts similarity index 67% rename from packages/backend-core/src/middleware/passport/google.ts rename to packages/backend-core/src/middleware/passport/sso/google.ts index dd3dc8b86d..ad7593e63d 100644 --- a/packages/backend-core/src/middleware/passport/google.ts +++ b/packages/backend-core/src/middleware/passport/sso/google.ts @@ -1,18 +1,25 @@ -import { ssoCallbackUrl } from "./utils" -import { authenticateThirdParty, SaveUserFunction } from "./third-party-common" -import { ConfigType, GoogleConfig, Database, SSOProfile } from "@budibase/types" +import { ssoCallbackUrl } from "../utils" +import * as sso from "./sso" +import { + ConfigType, + SSOProfile, + SSOAuthDetails, + SSOProviderType, + SaveSSOUserFunction, + GoogleInnerConfig, +} from "@budibase/types" const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy -export function buildVerifyFn(saveUserFn?: SaveUserFunction) { +export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) { return ( accessToken: string, refreshToken: string, profile: SSOProfile, done: Function ) => { - const thirdPartyUser = { - provider: profile.provider, // should always be 'google' - providerType: "google", + const details: SSOAuthDetails = { + provider: "google", + providerType: SSOProviderType.GOOGLE, userId: profile.id, profile: profile, email: profile._json.email, @@ -22,8 +29,8 @@ export function buildVerifyFn(saveUserFn?: SaveUserFunction) { }, } - return authenticateThirdParty( - thirdPartyUser, + return sso.authenticate( + details, true, // require local accounts to exist done, saveUserFn @@ -37,9 +44,9 @@ export function buildVerifyFn(saveUserFn?: SaveUserFunction) { * @returns Dynamically configured Passport Google Strategy */ export async function strategyFactory( - config: GoogleConfig["config"], + config: GoogleInnerConfig, callbackUrl: string, - saveUserFn?: SaveUserFunction + saveUserFn: SaveSSOUserFunction ) { try { const { clientID, clientSecret } = config @@ -65,9 +72,6 @@ export async function strategyFactory( } } -export async function getCallbackUrl( - db: Database, - config: { callbackURL?: string } -) { - return ssoCallbackUrl(db, config, ConfigType.GOOGLE) +export async function getCallbackUrl(config: GoogleInnerConfig) { + return ssoCallbackUrl(ConfigType.GOOGLE, config) } diff --git a/packages/backend-core/src/middleware/passport/oidc.ts b/packages/backend-core/src/middleware/passport/sso/oidc.ts similarity index 82% rename from packages/backend-core/src/middleware/passport/oidc.ts rename to packages/backend-core/src/middleware/passport/sso/oidc.ts index 7caa177cf0..b6d5eb52e9 100644 --- a/packages/backend-core/src/middleware/passport/oidc.ts +++ b/packages/backend-core/src/middleware/passport/sso/oidc.ts @@ -1,22 +1,19 @@ import fetch from "node-fetch" -import { authenticateThirdParty, SaveUserFunction } from "./third-party-common" -import { ssoCallbackUrl } from "./utils" +import * as sso from "./sso" +import { ssoCallbackUrl } from "../utils" import { ConfigType, - OIDCInnerCfg, - Database, + OIDCInnerConfig, SSOProfile, - ThirdPartyUser, - OIDCConfiguration, + OIDCStrategyConfiguration, + SSOAuthDetails, + SSOProviderType, + JwtClaims, + SaveSSOUserFunction, } from "@budibase/types" const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy -type JwtClaims = { - preferred_username: string - email: string -} - -export function buildVerifyFn(saveUserFn?: SaveUserFunction) { +export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) { /** * @param {*} issuer The identity provider base URL * @param {*} sub The user ID @@ -39,10 +36,10 @@ export function buildVerifyFn(saveUserFn?: SaveUserFunction) { params: any, done: Function ) => { - const thirdPartyUser: ThirdPartyUser = { + const details: SSOAuthDetails = { // store the issuer info to enable sync in future provider: issuer, - providerType: "oidc", + providerType: SSOProviderType.OIDC, userId: profile.id, profile: profile, email: getEmail(profile, jwtClaims), @@ -52,8 +49,8 @@ export function buildVerifyFn(saveUserFn?: SaveUserFunction) { }, } - return authenticateThirdParty( - thirdPartyUser, + return sso.authenticate( + details, false, // don't require local accounts to exist done, saveUserFn @@ -104,8 +101,8 @@ function validEmail(value: string) { * @returns Dynamically configured Passport OIDC Strategy */ export async function strategyFactory( - config: OIDCConfiguration, - saveUserFn?: SaveUserFunction + config: OIDCStrategyConfiguration, + saveUserFn: SaveSSOUserFunction ) { try { const verify = buildVerifyFn(saveUserFn) @@ -119,14 +116,14 @@ export async function strategyFactory( } export async function fetchStrategyConfig( - enrichedConfig: OIDCInnerCfg, + oidcConfig: OIDCInnerConfig, callbackUrl?: string -): Promise { +): Promise { try { - const { clientID, clientSecret, configUrl } = enrichedConfig + const { clientID, clientSecret, configUrl } = oidcConfig if (!clientID || !clientSecret || !callbackUrl || !configUrl) { - //check for remote config and all required elements + // check for remote config and all required elements throw new Error( "Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configUrl" ) @@ -159,9 +156,6 @@ export async function fetchStrategyConfig( } } -export async function getCallbackUrl( - db: Database, - config: { callbackURL?: string } -) { - return ssoCallbackUrl(db, config, ConfigType.OIDC) +export async function getCallbackUrl() { + return ssoCallbackUrl(ConfigType.OIDC) } diff --git a/packages/backend-core/src/middleware/passport/sso/sso.ts b/packages/backend-core/src/middleware/passport/sso/sso.ts new file mode 100644 index 0000000000..2fc1184722 --- /dev/null +++ b/packages/backend-core/src/middleware/passport/sso/sso.ts @@ -0,0 +1,165 @@ +import { generateGlobalUserID } from "../../../db" +import { authError } from "../utils" +import * as users from "../../../users" +import * as context from "../../../context" +import fetch from "node-fetch" +import { + SaveSSOUserFunction, + SaveUserOpts, + SSOAuthDetails, + SSOUser, + User, +} from "@budibase/types" + +// no-op function for user save +// - this allows datasource auth and access token refresh to work correctly +// - prefer no-op over an optional argument to ensure function is provided to login flows +export const ssoSaveUserNoOp: SaveSSOUserFunction = ( + user: SSOUser, + opts: SaveUserOpts +) => Promise.resolve(user) + +/** + * Common authentication logic for third parties. e.g. OAuth, OIDC. + */ +export async function authenticate( + details: SSOAuthDetails, + requireLocalAccount: boolean = true, + done: any, + saveUserFn: SaveSSOUserFunction +) { + if (!saveUserFn) { + throw new Error("Save user function must be provided") + } + if (!details.userId) { + return authError(done, "sso user id required") + } + if (!details.email) { + return authError(done, "sso user email required") + } + + // use the third party id + const userId = generateGlobalUserID(details.userId) + + let dbUser: User | undefined + + // try to load by id + try { + dbUser = await users.getById(userId) + } catch (err: any) { + // abort when not 404 error + if (!err.status || err.status !== 404) { + return authError( + done, + "Unexpected error when retrieving existing user", + err + ) + } + } + + // fallback to loading by email + if (!dbUser) { + dbUser = await users.getGlobalUserByEmail(details.email) + } + + // exit early if there is still no user and auto creation is disabled + if (!dbUser && requireLocalAccount) { + return authError( + done, + "Email does not yet exist. You must set up your local budibase account first." + ) + } + + // first time creation + if (!dbUser) { + // setup a blank user using the third party id + dbUser = { + _id: userId, + email: details.email, + roles: {}, + tenantId: context.getTenantId(), + } + } + + let ssoUser = await syncUser(dbUser, details) + // never prompt for password reset + ssoUser.forceResetPassword = false + + try { + // don't try to re-save any existing password + delete ssoUser.password + // create or sync the user + ssoUser = (await saveUserFn(ssoUser, { + hashPassword: false, + requirePassword: false, + })) as SSOUser + } catch (err: any) { + return authError(done, "Error saving user", err) + } + + return done(null, ssoUser) +} + +async function getProfilePictureUrl(user: User, details: SSOAuthDetails) { + const pictureUrl = details.profile?._json.picture + if (pictureUrl) { + const response = await fetch(pictureUrl) + if (response.status === 200) { + const type = response.headers.get("content-type") as string + if (type.startsWith("image/")) { + return pictureUrl + } + } + } +} + +/** + * @returns a user that has been sync'd with third party information + */ +async function syncUser(user: User, details: SSOAuthDetails): Promise { + let firstName + let lastName + let pictureUrl + let oauth2 + let thirdPartyProfile + + if (details.profile) { + const profile = details.profile + + if (profile.name) { + const name = profile.name + // first name + if (name.givenName) { + firstName = name.givenName + } + // last name + if (name.familyName) { + lastName = name.familyName + } + } + + pictureUrl = await getProfilePictureUrl(user, details) + + thirdPartyProfile = { + ...profile._json, + } + } + + // oauth tokens for future use + if (details.oauth2) { + oauth2 = { + ...details.oauth2, + } + } + + return { + ...user, + provider: details.provider, + providerType: details.providerType, + firstName, + lastName, + thirdPartyProfile, + pictureUrl, + oauth2, + } +} diff --git a/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts b/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts new file mode 100644 index 0000000000..d0689a1f0a --- /dev/null +++ b/packages/backend-core/src/middleware/passport/sso/tests/google.spec.ts @@ -0,0 +1,67 @@ +import { generator, structures } from "../../../../../tests" +import { SSOProviderType } from "@budibase/types" + +jest.mock("passport-google-oauth") +const mockStrategy = require("passport-google-oauth").OAuth2Strategy + +jest.mock("../sso") +import * as _sso from "../sso" +const sso = jest.mocked(_sso) + +const mockSaveUserFn = jest.fn() +const mockDone = jest.fn() + +import * as google from "../google" + +describe("google", () => { + describe("strategyFactory", () => { + const googleConfig = structures.sso.googleConfig() + const callbackUrl = generator.url() + + it("should create successfully create a google strategy", async () => { + await google.strategyFactory(googleConfig, callbackUrl, mockSaveUserFn) + + const expectedOptions = { + clientID: googleConfig.clientID, + clientSecret: googleConfig.clientSecret, + callbackURL: callbackUrl, + } + + expect(mockStrategy).toHaveBeenCalledWith( + expectedOptions, + expect.anything() + ) + }) + }) + + describe("authenticate", () => { + const details = structures.sso.authDetails() + details.provider = "google" + details.providerType = SSOProviderType.GOOGLE + + const profile = details.profile! + profile.provider = "google" + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("delegates authentication to third party common", async () => { + const authenticate = await google.buildVerifyFn(mockSaveUserFn) + + await authenticate( + details.oauth2.accessToken, + details.oauth2.refreshToken!, + profile, + mockDone + ) + + expect(sso.authenticate).toHaveBeenCalledWith( + details, + true, + mockDone, + mockSaveUserFn + ) + }) + }) +}) diff --git a/packages/backend-core/src/middleware/passport/sso/tests/oidc.spec.ts b/packages/backend-core/src/middleware/passport/sso/tests/oidc.spec.ts new file mode 100644 index 0000000000..a705739bd6 --- /dev/null +++ b/packages/backend-core/src/middleware/passport/sso/tests/oidc.spec.ts @@ -0,0 +1,152 @@ +import { generator, mocks, structures } from "../../../../../tests" +import { + JwtClaims, + OIDCInnerConfig, + SSOAuthDetails, + SSOProviderType, +} from "@budibase/types" +import * as _sso from "../sso" +import * as oidc from "../oidc" + +jest.mock("@techpass/passport-openidconnect") +const mockStrategy = require("@techpass/passport-openidconnect").Strategy + +jest.mock("../sso") +const sso = jest.mocked(_sso) + +const mockSaveUser = jest.fn() +const mockDone = jest.fn() + +describe("oidc", () => { + const callbackUrl = generator.url() + const oidcConfig: OIDCInnerConfig = structures.sso.oidcConfig() + const wellKnownConfig = structures.sso.oidcWellKnownConfig() + + function mockRetrieveWellKnownConfig() { + // mock the request to retrieve the oidc configuration + mocks.fetch.mockReturnValue({ + ok: true, + json: () => wellKnownConfig, + }) + } + + beforeEach(() => { + mockRetrieveWellKnownConfig() + }) + + describe("strategyFactory", () => { + it("should create successfully create an oidc strategy", async () => { + const strategyConfiguration = await oidc.fetchStrategyConfig( + oidcConfig, + callbackUrl + ) + await oidc.strategyFactory(strategyConfiguration, mockSaveUser) + + expect(mocks.fetch).toHaveBeenCalledWith(oidcConfig.configUrl) + + const expectedOptions = { + issuer: wellKnownConfig.issuer, + authorizationURL: wellKnownConfig.authorization_endpoint, + tokenURL: wellKnownConfig.token_endpoint, + userInfoURL: wellKnownConfig.userinfo_endpoint, + clientID: oidcConfig.clientID, + clientSecret: oidcConfig.clientSecret, + callbackURL: callbackUrl, + } + expect(mockStrategy).toHaveBeenCalledWith( + expectedOptions, + expect.anything() + ) + }) + }) + + describe("authenticate", () => { + const details: SSOAuthDetails = structures.sso.authDetails() + details.providerType = SSOProviderType.OIDC + const profile = details.profile! + const issuer = profile.provider + + const sub = generator.string() + const idToken = generator.string() + const params = {} + + let authenticateFn: any + let jwtClaims: JwtClaims + + beforeEach(async () => { + jest.clearAllMocks() + authenticateFn = await oidc.buildVerifyFn(mockSaveUser) + }) + + async function authenticate() { + await authenticateFn( + issuer, + sub, + profile, + jwtClaims, + details.oauth2.accessToken, + details.oauth2.refreshToken, + idToken, + params, + mockDone + ) + } + + it("passes auth details to sso module", async () => { + await authenticate() + + expect(sso.authenticate).toHaveBeenCalledWith( + details, + false, + mockDone, + mockSaveUser + ) + }) + + it("uses JWT email to get email", async () => { + delete profile._json.email + + jwtClaims = { + email: details.email, + } + + await authenticate() + + expect(sso.authenticate).toHaveBeenCalledWith( + details, + false, + mockDone, + mockSaveUser + ) + }) + + it("uses JWT username to get email", async () => { + delete profile._json.email + + jwtClaims = { + email: details.email, + } + + await authenticate() + + expect(sso.authenticate).toHaveBeenCalledWith( + details, + false, + mockDone, + mockSaveUser + ) + }) + + it("uses JWT invalid username to get email", async () => { + delete profile._json.email + + jwtClaims = { + preferred_username: "invalidUsername", + } + + await expect(authenticate()).rejects.toThrow( + "Could not determine user email from profile" + ) + }) + }) +}) diff --git a/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts b/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts new file mode 100644 index 0000000000..ae42fc01ea --- /dev/null +++ b/packages/backend-core/src/middleware/passport/sso/tests/sso.spec.ts @@ -0,0 +1,196 @@ +import { structures, testEnv, mocks } from "../../../../../tests" +import { SSOAuthDetails, User } from "@budibase/types" + +import { HTTPError } from "../../../../errors" +import * as sso from "../sso" +import * as context from "../../../../context" + +const mockDone = jest.fn() +const mockSaveUser = jest.fn() + +jest.mock("../../../../users") +import * as _users from "../../../../users" +const users = jest.mocked(_users) + +const getErrorMessage = () => { + return mockDone.mock.calls[0][2].message +} + +describe("sso", () => { + describe("authenticate", () => { + beforeEach(() => { + jest.clearAllMocks() + testEnv.singleTenant() + }) + + describe("validation", () => { + const testValidation = async ( + details: SSOAuthDetails, + message: string + ) => { + await sso.authenticate(details, false, mockDone, mockSaveUser) + + expect(mockDone.mock.calls.length).toBe(1) + expect(getErrorMessage()).toContain(message) + } + + it("user id fails", async () => { + const details = structures.sso.authDetails() + details.userId = undefined! + + await testValidation(details, "sso user id required") + }) + + it("email fails", async () => { + const details = structures.sso.authDetails() + details.email = undefined! + + await testValidation(details, "sso user email required") + }) + }) + + function mockGetProfilePicture() { + mocks.fetch.mockReturnValueOnce( + Promise.resolve({ + status: 200, + headers: { get: () => "image/" }, + }) + ) + } + + describe("when the user doesn't exist", () => { + let user: User + let details: SSOAuthDetails + + beforeEach(() => { + users.getById.mockImplementationOnce(() => { + throw new HTTPError("", 404) + }) + mockGetProfilePicture() + + user = structures.users.user() + delete user._rev + delete user._id + + details = structures.sso.authDetails(user) + details.userId = structures.uuid() + }) + + describe("when a local account is required", () => { + it("returns an error message", async () => { + const details = structures.sso.authDetails() + + await sso.authenticate(details, true, mockDone, mockSaveUser) + + expect(mockDone.mock.calls.length).toBe(1) + expect(getErrorMessage()).toContain( + "Email does not yet exist. You must set up your local budibase account first." + ) + }) + }) + + describe("when a local account isn't required", () => { + it("creates and authenticates the user", async () => { + const ssoUser = structures.users.ssoUser({ user, details }) + mockSaveUser.mockReturnValueOnce(ssoUser) + + await sso.authenticate(details, false, mockDone, mockSaveUser) + + // default roles for new user + ssoUser.roles = {} + + // modified external id to match user format + ssoUser._id = "us_" + details.userId + + // new sso user won't have a password + delete ssoUser.password + + // new user isn't saved with rev + delete ssoUser._rev + + // tenant id added + ssoUser.tenantId = context.getTenantId() + + expect(mockSaveUser).toBeCalledWith(ssoUser, { + hashPassword: false, + requirePassword: false, + }) + expect(mockDone).toBeCalledWith(null, ssoUser) + }) + }) + }) + + describe("when the user exists", () => { + let existingUser: User + let details: SSOAuthDetails + + beforeEach(() => { + existingUser = structures.users.user() + existingUser._id = structures.uuid() + details = structures.sso.authDetails(existingUser) + mockGetProfilePicture() + }) + + describe("exists by email", () => { + beforeEach(() => { + users.getById.mockImplementationOnce(() => { + throw new HTTPError("", 404) + }) + users.getGlobalUserByEmail.mockReturnValueOnce( + Promise.resolve(existingUser) + ) + }) + + it("syncs and authenticates the user", async () => { + const ssoUser = structures.users.ssoUser({ + user: existingUser, + details, + }) + mockSaveUser.mockReturnValueOnce(ssoUser) + + await sso.authenticate(details, true, mockDone, mockSaveUser) + + // roles preserved + ssoUser.roles = existingUser.roles + + // existing id preserved + ssoUser._id = existingUser._id + + expect(mockSaveUser).toBeCalledWith(ssoUser, { + hashPassword: false, + requirePassword: false, + }) + expect(mockDone).toBeCalledWith(null, ssoUser) + }) + }) + + describe("exists by id", () => { + beforeEach(() => { + users.getById.mockReturnValueOnce(Promise.resolve(existingUser)) + }) + + it("syncs and authenticates the user", async () => { + const ssoUser = structures.users.ssoUser({ + user: existingUser, + details, + }) + mockSaveUser.mockReturnValueOnce(ssoUser) + + await sso.authenticate(details, true, mockDone, mockSaveUser) + + // roles preserved + ssoUser.roles = existingUser.roles + + // existing id preserved + ssoUser._id = existingUser._id + + expect(mockSaveUser).toBeCalledWith(ssoUser, { + hashPassword: false, + requirePassword: false, + }) + expect(mockDone).toBeCalledWith(null, ssoUser) + }) + }) + }) + }) +}) diff --git a/packages/backend-core/src/middleware/passport/tests/google.spec.js b/packages/backend-core/src/middleware/passport/tests/google.spec.js deleted file mode 100644 index c5580ea309..0000000000 --- a/packages/backend-core/src/middleware/passport/tests/google.spec.js +++ /dev/null @@ -1,79 +0,0 @@ -// Mock data - -const { data } = require("./utilities/mock-data") - -const TENANT_ID = "default" - -const googleConfig = { - clientID: data.clientID, - clientSecret: data.clientSecret, -} - -const profile = { - id: "mockId", - _json: { - email : data.email - }, - provider: "google" -} - -const user = data.buildThirdPartyUser("google", "google", profile) - -describe("google", () => { - describe("strategyFactory", () => { - // mock passport strategy factory - jest.mock("passport-google-oauth") - const mockStrategy = require("passport-google-oauth").OAuth2Strategy - - it("should create successfully create a google strategy", async () => { - const google = require("../google") - - const callbackUrl = `/api/global/auth/${TENANT_ID}/google/callback` - await google.strategyFactory(googleConfig, callbackUrl) - - const expectedOptions = { - clientID: googleConfig.clientID, - clientSecret: googleConfig.clientSecret, - callbackURL: callbackUrl, - } - - expect(mockStrategy).toHaveBeenCalledWith( - expectedOptions, - expect.anything() - ) - }) - }) - - describe("authenticate", () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - // mock third party common authentication - jest.mock("../third-party-common") - const authenticateThirdParty = require("../third-party-common").authenticateThirdParty - - // mock the passport callback - const mockDone = jest.fn() - - it("delegates authentication to third party common", async () => { - const google = require("../google") - const mockSaveUserFn = jest.fn() - const authenticate = await google.buildVerifyFn(mockSaveUserFn) - - await authenticate( - data.accessToken, - data.refreshToken, - profile, - mockDone - ) - - expect(authenticateThirdParty).toHaveBeenCalledWith( - user, - true, - mockDone, - mockSaveUserFn) - }) - }) -}) - diff --git a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js deleted file mode 100644 index 4c8aa94ddf..0000000000 --- a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js +++ /dev/null @@ -1,144 +0,0 @@ -// Mock data -const mockFetch = require("node-fetch") -const { data } = require("./utilities/mock-data") -const issuer = "mockIssuer" -const sub = "mockSub" -const profile = { - id: "mockId", - _json: { - email : data.email - } -} -let jwtClaims = {} -const idToken = "mockIdToken" -const params = {} - -const callbackUrl = "http://somecallbackurl" - -// response from .well-known/openid-configuration -const oidcConfigUrlResponse = { - issuer: issuer, - authorization_endpoint: "mockAuthorizationEndpoint", - token_endpoint: "mockTokenEndpoint", - userinfo_endpoint: "mockUserInfoEndpoint" -} - -const oidcConfig = { - configUrl: "http://someconfigurl", - clientID: data.clientID, - clientSecret: data.clientSecret, -} - -const user = data.buildThirdPartyUser(issuer, "oidc", profile) - -describe("oidc", () => { - describe("strategyFactory", () => { - // mock passport strategy factory - jest.mock("@techpass/passport-openidconnect") - const mockStrategy = require("@techpass/passport-openidconnect").Strategy - - // mock the request to retrieve the oidc configuration - mockFetch.mockReturnValue({ - ok: true, - json: () => oidcConfigUrlResponse - }) - - it("should create successfully create an oidc strategy", async () => { - const oidc = require("../oidc") - const enrichedConfig = await oidc.fetchStrategyConfig(oidcConfig, callbackUrl) - await oidc.strategyFactory(enrichedConfig, callbackUrl) - - expect(mockFetch).toHaveBeenCalledWith(oidcConfig.configUrl) - - const expectedOptions = { - issuer: oidcConfigUrlResponse.issuer, - authorizationURL: oidcConfigUrlResponse.authorization_endpoint, - tokenURL: oidcConfigUrlResponse.token_endpoint, - userInfoURL: oidcConfigUrlResponse.userinfo_endpoint, - clientID: oidcConfig.clientID, - clientSecret: oidcConfig.clientSecret, - callbackURL: callbackUrl, - } - expect(mockStrategy).toHaveBeenCalledWith( - expectedOptions, - expect.anything() - ) - }) - }) - - describe("authenticate", () => { - afterEach(() => { - jest.clearAllMocks() - }); - - // mock third party common authentication - jest.mock("../third-party-common") - const authenticateThirdParty = require("../third-party-common").authenticateThirdParty - - // mock the passport callback - const mockDone = jest.fn() - const mockSaveUserFn = jest.fn() - - async function doAuthenticate() { - const oidc = require("../oidc") - const authenticate = await oidc.buildVerifyFn(mockSaveUserFn) - - await authenticate( - issuer, - sub, - profile, - jwtClaims, - data.accessToken, - data.refreshToken, - idToken, - params, - mockDone - ) - } - - async function doTest() { - await doAuthenticate() - - expect(authenticateThirdParty).toHaveBeenCalledWith( - user, - false, - mockDone, - mockSaveUserFn, - ) - } - - it("delegates authentication to third party common", async () => { - await doTest() - }) - - it("uses JWT email to get email", async () => { - delete profile._json.email - jwtClaims = { - email : "mock@budibase.com" - } - - await doTest() - }) - - it("uses JWT username to get email", async () => { - delete profile._json.email - jwtClaims = { - preferred_username : "mock@budibase.com" - } - - await doTest() - }) - - it("uses JWT invalid username to get email", async () => { - delete profile._json.email - - jwtClaims = { - preferred_username : "invalidUsername" - } - - await expect(doAuthenticate()).rejects.toThrow("Could not determine user email from profile"); - }) - - }) -}) - diff --git a/packages/backend-core/src/middleware/passport/tests/third-party-common.spec.js b/packages/backend-core/src/middleware/passport/tests/third-party-common.spec.js deleted file mode 100644 index d377d602f1..0000000000 --- a/packages/backend-core/src/middleware/passport/tests/third-party-common.spec.js +++ /dev/null @@ -1,178 +0,0 @@ -require("../../../../tests") -const { authenticateThirdParty } = require("../third-party-common") -const { data } = require("./utilities/mock-data") -const { DEFAULT_TENANT_ID } = require("../../../constants") - -const { generateGlobalUserID } = require("../../../db/utils") -const { newid } = require("../../../utils") -const { doWithGlobalDB, doInTenant } = require("../../../tenancy") - -const done = jest.fn() - -const getErrorMessage = () => { - return done.mock.calls[0][2].message -} - -const saveUser = async (user) => { - return doWithGlobalDB(DEFAULT_TENANT_ID, async db => { - return await db.put(user) - }) -} - -function authenticate(user, requireLocal, saveFn) { - return doInTenant(DEFAULT_TENANT_ID, () => { - return authenticateThirdParty(user, requireLocal, done, saveFn) - }) -} - -describe("third party common", () => { - describe("authenticateThirdParty", () => { - let thirdPartyUser - - beforeEach(() => { - thirdPartyUser = data.buildThirdPartyUser() - }) - - afterEach(async () => { - return doWithGlobalDB(DEFAULT_TENANT_ID, async db => { - jest.clearAllMocks() - await db.destroy() - }) - }) - - describe("validation", () => { - const testValidation = async (message) => { - await authenticate(thirdPartyUser, false, saveUser) - expect(done.mock.calls.length).toBe(1) - expect(getErrorMessage()).toContain(message) - } - - it("provider fails", async () => { - delete thirdPartyUser.provider - await testValidation("third party user provider required") - }) - - it("user id fails", async () => { - delete thirdPartyUser.userId - await testValidation("third party user id required") - }) - - it("email fails", async () => { - delete thirdPartyUser.email - await testValidation("third party user email required") - }) - }) - - const expectUserIsAuthenticated = () => { - const user = done.mock.calls[0][1] - expect(user).toBeDefined() - expect(user._id).toBeDefined() - expect(user._rev).toBeDefined() - expect(user.token).toBeDefined() - return user - } - - const expectUserIsSynced = (user, thirdPartyUser) => { - expect(user.provider).toBe(thirdPartyUser.provider) - expect(user.firstName).toBe(thirdPartyUser.profile.name.givenName) - expect(user.lastName).toBe(thirdPartyUser.profile.name.familyName) - expect(user.thirdPartyProfile).toStrictEqual(thirdPartyUser.profile._json) - expect(user.oauth2).toStrictEqual(thirdPartyUser.oauth2) - } - - describe("when the user doesn't exist", () => { - describe("when a local account is required", () => { - it("returns an error message", async () => { - await authenticate(thirdPartyUser, true, saveUser) - expect(done.mock.calls.length).toBe(1) - expect(getErrorMessage()).toContain("Email does not yet exist. You must set up your local budibase account first.") - }) - }) - - describe("when a local account isn't required", () => { - it("creates and authenticates the user", async () => { - await authenticate(thirdPartyUser, false, saveUser) - const user = expectUserIsAuthenticated() - expectUserIsSynced(user, thirdPartyUser) - expect(user.roles).toStrictEqual({}) - }) - }) - }) - - describe("when the user exists", () => { - let dbUser - let id - let email - - const createUser = async () => { - return doWithGlobalDB(DEFAULT_TENANT_ID, async db => { - dbUser = { - _id: id, - email: email, - } - const response = await db.put(dbUser) - dbUser._rev = response.rev - return dbUser - }) - } - - const expectUserIsUpdated = (user) => { - // id is unchanged - expect(user._id).toBe(id) - // user is updated - expect(user._rev).not.toBe(dbUser._rev) - } - - describe("exists by email", () => { - beforeEach(async () => { - id = generateGlobalUserID(newid()) // random id - email = thirdPartyUser.email // matching email - await createUser() - }) - - it("syncs and authenticates the user", async () => { - await authenticate(thirdPartyUser, true, saveUser) - - const user = expectUserIsAuthenticated() - expectUserIsSynced(user, thirdPartyUser) - expectUserIsUpdated(user) - }) - }) - - describe("exists by email with different casing", () => { - beforeEach(async () => { - id = generateGlobalUserID(newid()) // random id - email = thirdPartyUser.email.toUpperCase() // matching email except for casing - await createUser() - }) - - it("syncs and authenticates the user", async () => { - await authenticate(thirdPartyUser, true, saveUser) - - const user = expectUserIsAuthenticated() - expectUserIsSynced(user, thirdPartyUser) - expectUserIsUpdated(user) - expect(user.email).toBe(thirdPartyUser.email.toUpperCase()) - }) - }) - - - describe("exists by id", () => { - beforeEach(async () => { - id = generateGlobalUserID(thirdPartyUser.userId) // matching id - email = "test@test.com" // random email - await createUser() - }) - - it("syncs and authenticates the user", async () => { - await authenticate(thirdPartyUser, true, saveUser) - - const user = expectUserIsAuthenticated() - expectUserIsSynced(user, thirdPartyUser) - expectUserIsUpdated(user) - }) - }) - }) - }) -}) - diff --git a/packages/backend-core/src/middleware/passport/tests/utilities/mock-data.js b/packages/backend-core/src/middleware/passport/tests/utilities/mock-data.js deleted file mode 100644 index 00ae82e47e..0000000000 --- a/packages/backend-core/src/middleware/passport/tests/utilities/mock-data.js +++ /dev/null @@ -1,54 +0,0 @@ -// Mock Data - -const mockClientID = "mockClientID" -const mockClientSecret = "mockClientSecret" - -const mockEmail = "mock@budibase.com" -const mockAccessToken = "mockAccessToken" -const mockRefreshToken = "mockRefreshToken" - -const mockProvider = "mockProvider" -const mockProviderType = "mockProviderType" - -const mockProfile = { - id: "mockId", - name: { - givenName: "mockGivenName", - familyName: "mockFamilyName", - }, - _json: { - email: mockEmail, - }, -} - -const buildOauth2 = ( - accessToken = mockAccessToken, - refreshToken = mockRefreshToken -) => ({ - accessToken: accessToken, - refreshToken: refreshToken, -}) - -const buildThirdPartyUser = ( - provider = mockProvider, - providerType = mockProviderType, - profile = mockProfile, - email = mockEmail, - oauth2 = buildOauth2() -) => ({ - provider: provider, - providerType: providerType, - userId: profile.id, - profile: profile, - email: email, - oauth2: oauth2, -}) - -exports.data = { - clientID: mockClientID, - clientSecret: mockClientSecret, - email: mockEmail, - accessToken: mockAccessToken, - refreshToken: mockRefreshToken, - buildThirdPartyUser, -} diff --git a/packages/backend-core/src/middleware/passport/third-party-common.ts b/packages/backend-core/src/middleware/passport/third-party-common.ts deleted file mode 100644 index 9d7b93f370..0000000000 --- a/packages/backend-core/src/middleware/passport/third-party-common.ts +++ /dev/null @@ -1,177 +0,0 @@ -import env from "../../environment" -import { generateGlobalUserID } from "../../db" -import { authError } from "./utils" -import { newid } from "../../utils" -import { createASession } from "../../security/sessions" -import * as users from "../../users" -import { getGlobalDB, getTenantId } from "../../tenancy" -import fetch from "node-fetch" -import { ThirdPartyUser } from "@budibase/types" -const jwt = require("jsonwebtoken") - -type SaveUserOpts = { - requirePassword?: boolean - hashPassword?: boolean - currentUserId?: string -} - -export type SaveUserFunction = ( - user: ThirdPartyUser, - opts: SaveUserOpts -) => Promise - -/** - * Common authentication logic for third parties. e.g. OAuth, OIDC. - */ -export async function authenticateThirdParty( - thirdPartyUser: ThirdPartyUser, - requireLocalAccount: boolean = true, - done: Function, - saveUserFn?: SaveUserFunction -) { - if (!saveUserFn) { - throw new Error("Save user function must be provided") - } - if (!thirdPartyUser.provider) { - return authError(done, "third party user provider required") - } - if (!thirdPartyUser.userId) { - return authError(done, "third party user id required") - } - if (!thirdPartyUser.email) { - return authError(done, "third party user email required") - } - - // use the third party id - const userId = generateGlobalUserID(thirdPartyUser.userId) - const db = getGlobalDB() - - let dbUser - - // try to load by id - try { - dbUser = await db.get(userId) - } catch (err: any) { - // abort when not 404 error - if (!err.status || err.status !== 404) { - return authError( - done, - "Unexpected error when retrieving existing user", - err - ) - } - } - - // fallback to loading by email - if (!dbUser) { - dbUser = await users.getGlobalUserByEmail(thirdPartyUser.email) - } - - // exit early if there is still no user and auto creation is disabled - if (!dbUser && requireLocalAccount) { - return authError( - done, - "Email does not yet exist. You must set up your local budibase account first." - ) - } - - // first time creation - if (!dbUser) { - // setup a blank user using the third party id - dbUser = { - _id: userId, - email: thirdPartyUser.email, - roles: {}, - } - } - - dbUser = await syncUser(dbUser, thirdPartyUser) - - // never prompt for password reset - dbUser.forceResetPassword = false - - // create or sync the user - try { - await saveUserFn(dbUser, { hashPassword: false, requirePassword: false }) - } catch (err: any) { - return authError(done, "Error saving user", err) - } - - // now that we're sure user exists, load them from the db - dbUser = await db.get(dbUser._id) - - // authenticate - const sessionId = newid() - const tenantId = getTenantId() - await createASession(dbUser._id, { sessionId, tenantId }) - - dbUser.token = jwt.sign( - { - userId: dbUser._id, - sessionId, - }, - env.JWT_SECRET - ) - - return done(null, dbUser) -} - -async function syncProfilePicture( - user: ThirdPartyUser, - thirdPartyUser: ThirdPartyUser -) { - const pictureUrl = thirdPartyUser.profile?._json.picture - if (pictureUrl) { - const response = await fetch(pictureUrl) - - if (response.status === 200) { - const type = response.headers.get("content-type") as string - if (type.startsWith("image/")) { - user.pictureUrl = pictureUrl - } - } - } - - return user -} - -/** - * @returns a user that has been sync'd with third party information - */ -async function syncUser(user: ThirdPartyUser, thirdPartyUser: ThirdPartyUser) { - // provider - user.provider = thirdPartyUser.provider - user.providerType = thirdPartyUser.providerType - - if (thirdPartyUser.profile) { - const profile = thirdPartyUser.profile - - if (profile.name) { - const name = profile.name - // first name - if (name.givenName) { - user.firstName = name.givenName - } - // last name - if (name.familyName) { - user.lastName = name.familyName - } - } - - user = await syncProfilePicture(user, thirdPartyUser) - - // profile - user.thirdPartyProfile = { - ...profile._json, - } - } - - // oauth tokens for future use - if (thirdPartyUser.oauth2) { - user.oauth2 = { - ...thirdPartyUser.oauth2, - } - } - - return user -} diff --git a/packages/backend-core/src/middleware/passport/utils.ts b/packages/backend-core/src/middleware/passport/utils.ts index 3d79aada28..7e0d3863a0 100644 --- a/packages/backend-core/src/middleware/passport/utils.ts +++ b/packages/backend-core/src/middleware/passport/utils.ts @@ -1,6 +1,6 @@ -import { isMultiTenant, getTenantId } from "../../tenancy" -import { getScopedConfig } from "../../db" -import { ConfigType, Database, Config } from "@budibase/types" +import { getTenantId, isMultiTenant } from "../../context" +import * as configs from "../../configs" +import { ConfigType, GoogleInnerConfig } from "@budibase/types" /** * Utility to handle authentication errors. @@ -19,17 +19,14 @@ export function authError(done: Function, message: string, err?: any) { } export async function ssoCallbackUrl( - db: Database, - config?: { callbackURL?: string }, - type?: ConfigType + type: ConfigType, + config?: GoogleInnerConfig ) { // incase there is a callback URL from before - if (config && config.callbackURL) { - return config.callbackURL + if (config && (config as GoogleInnerConfig).callbackURL) { + return (config as GoogleInnerConfig).callbackURL as string } - const publicConfig = await getScopedConfig(db, { - type: ConfigType.SETTINGS, - }) + const settingsConfig = await configs.getSettingsConfig() let callbackUrl = `/api/global/auth` if (isMultiTenant()) { @@ -37,5 +34,5 @@ export async function ssoCallbackUrl( } callbackUrl += `/${type}/callback` - return `${publicConfig.platformUrl}${callbackUrl}` + return `${settingsConfig.platformUrl}${callbackUrl}` } diff --git a/packages/backend-core/src/middleware/querystringToBody.ts b/packages/backend-core/src/middleware/querystringToBody.ts new file mode 100644 index 0000000000..b6f109231a --- /dev/null +++ b/packages/backend-core/src/middleware/querystringToBody.ts @@ -0,0 +1,28 @@ +import { Ctx } from "@budibase/types" + +/** + * Expects a standard "query" query string property which is the JSON body + * of the request, which has to be sent via query string due to the requirement + * of making an endpoint a GET request e.g. downloading a file stream. + */ +export default function (ctx: Ctx, next: any) { + const queryString = ctx.request.query?.query as string | undefined + if (ctx.request.method.toLowerCase() !== "get") { + ctx.throw( + 500, + "Query to download middleware can only be used for get requests." + ) + } + if (!queryString) { + return next() + } + const decoded = decodeURIComponent(queryString) + let json + try { + json = JSON.parse(decoded) + } catch (err) { + return next() + } + ctx.request.body = json + return next() +} diff --git a/packages/backend-core/src/middleware/tenancy.ts b/packages/backend-core/src/middleware/tenancy.ts index a09c463045..22b7cc213d 100644 --- a/packages/backend-core/src/middleware/tenancy.ts +++ b/packages/backend-core/src/middleware/tenancy.ts @@ -1,4 +1,5 @@ -import { doInTenant, getTenantIDFromCtx } from "../tenancy" +import { doInTenant } from "../context" +import { getTenantIDFromCtx } from "../tenancy" import { buildMatcherRegex, matches } from "./matchers" import { Header } from "../constants" import { diff --git a/packages/backend-core/src/migrations/migrations.ts b/packages/backend-core/src/migrations/migrations.ts index 55b8ab1938..ab72091d56 100644 --- a/packages/backend-core/src/migrations/migrations.ts +++ b/packages/backend-core/src/migrations/migrations.ts @@ -4,10 +4,10 @@ import { StaticDatabases, getAllApps, getGlobalDBName, - doWithDB, + getDB, } from "../db" import environment from "../environment" -import { doInTenant, getTenantIds, getTenantId } from "../tenancy" +import * as platform from "../platform" import * as context from "../context" import { DEFINITIONS } from "." import { @@ -47,7 +47,7 @@ export const runMigration = async ( const migrationType = migration.type let tenantId: string | undefined if (migrationType !== MigrationType.INSTALLATION) { - tenantId = getTenantId() + tenantId = context.getTenantId() } const migrationName = migration.name const silent = migration.silent @@ -86,66 +86,66 @@ export const runMigration = async ( count++ const lengthStatement = length > 1 ? `[${count}/${length}]` : "" - await doWithDB(dbName, async (db: any) => { - try { - const doc = await getMigrationsDoc(db) + const db = getDB(dbName) - // the migration has already been run - if (doc[migrationName]) { - // check for force - if ( - options.force && - options.force[migrationType] && - options.force[migrationType].includes(migrationName) - ) { - log( - `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Forcing` - ) - } else { - // no force, exit - return - } - } + try { + const doc = await getMigrationsDoc(db) - // check if the migration is not a no-op - if (!options.noOp) { + // the migration has already been run + if (doc[migrationName]) { + // check for force + if ( + options.force && + options.force[migrationType] && + options.force[migrationType].includes(migrationName) + ) { log( - `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Running ${lengthStatement}` - ) - - if (migration.preventRetry) { - // eagerly set the completion date - // so that we never run this migration twice even upon failure - doc[migrationName] = Date.now() - const response = await db.put(doc) - doc._rev = response.rev - } - - // run the migration - if (migrationType === MigrationType.APP) { - await context.doInAppContext(db.name, async () => { - await migration.fn(db) - }) - } else { - await migration.fn(db) - } - - log( - `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Complete` + `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Forcing` ) + } else { + // no force, exit + return } - - // mark as complete - doc[migrationName] = Date.now() - await db.put(doc) - } catch (err) { - console.error( - `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Error: `, - err - ) - throw err } - }) + + // check if the migration is not a no-op + if (!options.noOp) { + log( + `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Running ${lengthStatement}` + ) + + if (migration.preventRetry) { + // eagerly set the completion date + // so that we never run this migration twice even upon failure + doc[migrationName] = Date.now() + const response = await db.put(doc) + doc._rev = response.rev + } + + // run the migration + if (migrationType === MigrationType.APP) { + await context.doInAppContext(db.name, async () => { + await migration.fn(db) + }) + } else { + await migration.fn(db) + } + + log( + `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Complete` + ) + } + + // mark as complete + doc[migrationName] = Date.now() + await db.put(doc) + } catch (err) { + console.error( + `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Error: `, + err + ) + throw err + } } } @@ -160,7 +160,7 @@ export const runMigrations = async ( tenantIds = [options.noOp.tenantId] } else if (!options.tenantIds || !options.tenantIds.length) { // run for all tenants - tenantIds = await getTenantIds() + tenantIds = await platform.tenants.getTenantIds() } else { tenantIds = options.tenantIds } @@ -185,7 +185,10 @@ export const runMigrations = async ( // for all migrations for (const migration of migrations) { // run the migration - await doInTenant(tenantId, () => runMigration(migration, options)) + await context.doInTenant( + tenantId, + async () => await runMigration(migration, options) + ) } } console.log("Migrations complete") diff --git a/packages/backend-core/src/migrations/tests/__snapshots__/index.spec.js.snap b/packages/backend-core/src/migrations/tests/__snapshots__/migrations.spec.ts.snap similarity index 83% rename from packages/backend-core/src/migrations/tests/__snapshots__/index.spec.js.snap rename to packages/backend-core/src/migrations/tests/__snapshots__/migrations.spec.ts.snap index 532b5a32db..5129869232 100644 --- a/packages/backend-core/src/migrations/tests/__snapshots__/index.spec.js.snap +++ b/packages/backend-core/src/migrations/tests/__snapshots__/migrations.spec.ts.snap @@ -3,7 +3,7 @@ exports[`migrations should match snapshot 1`] = ` Object { "_id": "migrations", - "_rev": "1-a32b0b708e59eeb006ed5e063cfeb36a", + "_rev": "1-2f64479842a0513aa8b97f356b0b9127", "createdAt": "2020-01-01T00:00:00.000Z", "test": 1577836800000, "updatedAt": "2020-01-01T00:00:00.000Z", diff --git a/packages/backend-core/src/migrations/tests/index.spec.js b/packages/backend-core/src/migrations/tests/index.spec.js deleted file mode 100644 index b7d2e14ea5..0000000000 --- a/packages/backend-core/src/migrations/tests/index.spec.js +++ /dev/null @@ -1,54 +0,0 @@ -require("../../../tests") -const { runMigrations, getMigrationsDoc } = require("../index") -const { getDB } = require("../../db") -const { - StaticDatabases, -} = require("../../constants") - -let db - -describe("migrations", () => { - - const migrationFunction = jest.fn() - - const MIGRATIONS = [{ - type: "global", - name: "test", - fn: migrationFunction - }] - - beforeEach(() => { - db = getDB(StaticDatabases.GLOBAL.name) - }) - - afterEach(async () => { - jest.clearAllMocks() - await db.destroy() - }) - - const migrate = () => { - return runMigrations(MIGRATIONS) - } - - it("should run a new migration", async () => { - await migrate() - expect(migrationFunction).toHaveBeenCalled() - const doc = await getMigrationsDoc(db) - expect(doc.test).toBeDefined() - }) - - it("should match snapshot", async () => { - await migrate() - const doc = await getMigrationsDoc(db) - expect(doc).toMatchSnapshot() - }) - - it("should skip a previously run migration", async () => { - await migrate() - const previousMigrationTime = await getMigrationsDoc(db).test - await migrate() - const currentMigrationTime = await getMigrationsDoc(db).test - expect(migrationFunction).toHaveBeenCalledTimes(1) - expect(currentMigrationTime).toBe(previousMigrationTime) - }) -}) \ No newline at end of file diff --git a/packages/backend-core/src/migrations/tests/migrations.spec.ts b/packages/backend-core/src/migrations/tests/migrations.spec.ts new file mode 100644 index 0000000000..c74ab816c1 --- /dev/null +++ b/packages/backend-core/src/migrations/tests/migrations.spec.ts @@ -0,0 +1,64 @@ +import { testEnv, DBTestConfiguration } from "../../../tests" +import * as migrations from "../index" +import * as context from "../../context" +import { MigrationType } from "@budibase/types" + +testEnv.multiTenant() + +describe("migrations", () => { + const config = new DBTestConfiguration() + + const migrationFunction = jest.fn() + + const MIGRATIONS = [ + { + type: MigrationType.GLOBAL, + name: "test" as any, + fn: migrationFunction, + }, + ] + + beforeEach(() => { + config.newTenant() + }) + + afterEach(async () => { + jest.clearAllMocks() + }) + + const migrate = () => { + return migrations.runMigrations(MIGRATIONS, { + tenantIds: [config.tenantId], + }) + } + + it("should run a new migration", async () => { + await config.doInTenant(async () => { + await migrate() + expect(migrationFunction).toHaveBeenCalled() + const db = context.getGlobalDB() + const doc = await migrations.getMigrationsDoc(db) + expect(doc.test).toBeDefined() + }) + }) + + it("should match snapshot", async () => { + await config.doInTenant(async () => { + await migrate() + const doc = await migrations.getMigrationsDoc(context.getGlobalDB()) + expect(doc).toMatchSnapshot() + }) + }) + + it("should skip a previously run migration", async () => { + await config.doInTenant(async () => { + const db = context.getGlobalDB() + await migrate() + const previousDoc = await migrations.getMigrationsDoc(db) + await migrate() + const currentDoc = await migrations.getMigrationsDoc(db) + expect(migrationFunction).toHaveBeenCalledTimes(1) + expect(currentDoc.test).toBe(previousDoc.test) + }) + }) +}) diff --git a/packages/backend-core/src/objectStore/buckets/global.ts b/packages/backend-core/src/objectStore/buckets/global.ts index 8bf883b11e..69e201bb98 100644 --- a/packages/backend-core/src/objectStore/buckets/global.ts +++ b/packages/backend-core/src/objectStore/buckets/global.ts @@ -1,5 +1,5 @@ import env from "../../environment" -import * as tenancy from "../../tenancy" +import * as context from "../../context" import * as objectStore from "../objectStore" import * as cloudfront from "../cloudfront" @@ -22,7 +22,7 @@ export const getGlobalFileUrl = (type: string, name: string, etag?: string) => { export const getGlobalFileS3Key = (type: string, name: string) => { let file = `${type}/${name}` if (env.MULTI_TENANCY) { - const tenantId = tenancy.getTenantId() + const tenantId = context.getTenantId() file = `${tenantId}/${file}` } return file diff --git a/packages/backend-core/src/objectStore/buckets/plugins.ts b/packages/backend-core/src/objectStore/buckets/plugins.ts index cd3bf77e87..f7721afb23 100644 --- a/packages/backend-core/src/objectStore/buckets/plugins.ts +++ b/packages/backend-core/src/objectStore/buckets/plugins.ts @@ -1,6 +1,6 @@ import env from "../../environment" import * as objectStore from "../objectStore" -import * as tenancy from "../../tenancy" +import * as context from "../../context" import * as cloudfront from "../cloudfront" import { Plugin } from "@budibase/types" @@ -61,7 +61,7 @@ const getPluginS3Key = (plugin: Plugin, fileName: string) => { export const getPluginS3Dir = (pluginName: string) => { let s3Key = `${pluginName}` if (env.MULTI_TENANCY) { - const tenantId = tenancy.getTenantId() + const tenantId = context.getTenantId() s3Key = `${tenantId}/${s3Key}` } if (env.CLOUDFRONT_CDN) { diff --git a/packages/backend-core/src/objectStore/objectStore.ts b/packages/backend-core/src/objectStore/objectStore.ts index f601d40a37..059e1b228d 100644 --- a/packages/backend-core/src/objectStore/objectStore.ts +++ b/packages/backend-core/src/objectStore/objectStore.ts @@ -361,8 +361,8 @@ export const deleteFolder = async ( Prefix: folder, } - let response: any = await client.listObjects(listParams).promise() - if (response.Contents.length === 0) { + const existingObjectsResponse = await client.listObjects(listParams).promise() + if (existingObjectsResponse.Contents?.length === 0) { return } const deleteParams: any = { @@ -372,13 +372,13 @@ export const deleteFolder = async ( }, } - response.Contents.forEach((content: any) => { + existingObjectsResponse.Contents?.forEach((content: any) => { deleteParams.Delete.Objects.push({ Key: content.Key }) }) - response = await client.deleteObjects(deleteParams).promise() + const deleteResponse = await client.deleteObjects(deleteParams).promise() // can only empty 1000 items at once - if (response.Deleted.length === 1000) { + if (deleteResponse.Deleted?.length === 1000) { return deleteFolder(bucketName, folder) } } diff --git a/packages/backend-core/src/platform/index.ts b/packages/backend-core/src/platform/index.ts new file mode 100644 index 0000000000..877d85ade0 --- /dev/null +++ b/packages/backend-core/src/platform/index.ts @@ -0,0 +1,3 @@ +export * as users from "./users" +export * as tenants from "./tenants" +export * from "./platformDb" diff --git a/packages/backend-core/src/platform/platformDb.ts b/packages/backend-core/src/platform/platformDb.ts new file mode 100644 index 0000000000..90b683dd33 --- /dev/null +++ b/packages/backend-core/src/platform/platformDb.ts @@ -0,0 +1,6 @@ +import { StaticDatabases } from "../constants" +import { getDB } from "../db/db" + +export function getPlatformDB() { + return getDB(StaticDatabases.PLATFORM_INFO.name) +} diff --git a/packages/backend-core/src/platform/tenants.ts b/packages/backend-core/src/platform/tenants.ts new file mode 100644 index 0000000000..b6bc3410d8 --- /dev/null +++ b/packages/backend-core/src/platform/tenants.ts @@ -0,0 +1,101 @@ +import { StaticDatabases } from "../constants" +import { getPlatformDB } from "./platformDb" +import { LockName, LockOptions, LockType, Tenants } from "@budibase/types" +import * as locks from "../redis/redlockImpl" + +const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants + +export const tenacyLockOptions: LockOptions = { + type: LockType.DEFAULT, + name: LockName.UPDATE_TENANTS_DOC, + ttl: 10 * 1000, // auto expire after 10 seconds + systemLock: true, +} + +// READ + +export async function getTenantIds(): Promise { + const tenants = await getTenants() + return tenants.tenantIds +} + +async function getTenants(): Promise { + const db = getPlatformDB() + let tenants: Tenants + + try { + tenants = await db.get(TENANT_DOC) + } catch (e: any) { + // doesn't exist yet - create + if (e.status === 404) { + tenants = await createTenantsDoc() + } else { + throw e + } + } + + return tenants +} + +export async function exists(tenantId: string) { + const tenants = await getTenants() + return tenants.tenantIds.indexOf(tenantId) !== -1 +} + +// CREATE / UPDATE + +function newTenantsDoc(): Tenants { + return { + _id: TENANT_DOC, + tenantIds: [], + } +} + +async function createTenantsDoc(): Promise { + const db = getPlatformDB() + let tenants = newTenantsDoc() + + try { + const response = await db.put(tenants) + tenants._rev = response.rev + } catch (e: any) { + // don't throw 409 is doc has already been created + if (e.status === 409) { + return db.get(TENANT_DOC) + } + throw e + } + + return tenants +} + +export async function addTenant(tenantId: string) { + const db = getPlatformDB() + + // use a lock as tenant creation is conflict prone + await locks.doWithLock(tenacyLockOptions, async () => { + const tenants = await getTenants() + + // write the new tenant if it doesn't already exist + if (tenants.tenantIds.indexOf(tenantId) === -1) { + tenants.tenantIds.push(tenantId) + await db.put(tenants) + } + }) +} + +// DELETE + +export async function removeTenant(tenantId: string) { + try { + await locks.doWithLock(tenacyLockOptions, async () => { + const db = getPlatformDB() + const tenants = await getTenants() + tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId) + await db.put(tenants) + }) + } catch (err) { + console.error(`Error removing tenant ${tenantId} from info db`, err) + throw err + } +} diff --git a/packages/backend-core/src/platform/tests/tenants.spec.ts b/packages/backend-core/src/platform/tests/tenants.spec.ts new file mode 100644 index 0000000000..92e999cb2d --- /dev/null +++ b/packages/backend-core/src/platform/tests/tenants.spec.ts @@ -0,0 +1,25 @@ +import { DBTestConfiguration, structures } from "../../../tests" +import * as tenants from "../tenants" + +describe("tenants", () => { + const config = new DBTestConfiguration() + + describe("addTenant", () => { + it("concurrently adds multiple tenants safely", async () => { + const tenant1 = structures.tenant.id() + const tenant2 = structures.tenant.id() + const tenant3 = structures.tenant.id() + + await Promise.all([ + tenants.addTenant(tenant1), + tenants.addTenant(tenant2), + tenants.addTenant(tenant3), + ]) + + const tenantIds = await tenants.getTenantIds() + expect(tenantIds.includes(tenant1)).toBe(true) + expect(tenantIds.includes(tenant2)).toBe(true) + expect(tenantIds.includes(tenant3)).toBe(true) + }) + }) +}) diff --git a/packages/backend-core/src/platform/users.ts b/packages/backend-core/src/platform/users.ts new file mode 100644 index 0000000000..c65a7e0ec4 --- /dev/null +++ b/packages/backend-core/src/platform/users.ts @@ -0,0 +1,90 @@ +import { getPlatformDB } from "./platformDb" +import { DEFAULT_TENANT_ID } from "../constants" +import env from "../environment" +import { + PlatformUser, + PlatformUserByEmail, + PlatformUserById, + User, +} from "@budibase/types" + +// READ + +export async function lookupTenantId(userId: string) { + if (!env.MULTI_TENANCY) { + return DEFAULT_TENANT_ID + } + + const user = await getUserDoc(userId) + return user.tenantId +} + +async function getUserDoc(emailOrId: string): Promise { + const db = getPlatformDB() + return db.get(emailOrId) +} + +// CREATE + +function newUserIdDoc(id: string, tenantId: string): PlatformUserById { + return { + _id: id, + tenantId, + } +} + +function newUserEmailDoc( + userId: string, + email: string, + tenantId: string +): PlatformUserByEmail { + return { + _id: email, + userId, + tenantId, + } +} + +/** + * Add a new user id or email doc if it doesn't exist. + */ +async function addUserDoc(emailOrId: string, newDocFn: () => PlatformUser) { + const db = getPlatformDB() + let user: PlatformUser + + try { + await db.get(emailOrId) + } catch (e: any) { + if (e.status === 404) { + user = newDocFn() + await db.put(user) + } else { + throw e + } + } +} + +export async function addUser(tenantId: string, userId: string, email: string) { + await Promise.all([ + addUserDoc(userId, () => newUserIdDoc(userId, tenantId)), + addUserDoc(email, () => newUserEmailDoc(userId, email, tenantId)), + ]) +} + +// DELETE + +export async function removeUser(user: User) { + const db = getPlatformDB() + const keys = [user._id!, user.email] + const userDocs = await db.allDocs({ + keys, + include_docs: true, + }) + const toDelete = userDocs.rows.map((row: any) => { + return { + ...row.doc, + _deleted: true, + } + }) + await db.bulkDocs(toDelete) +} diff --git a/packages/backend-core/src/queue/constants.ts b/packages/backend-core/src/queue/constants.ts index e8323dacb8..9261ed1176 100644 --- a/packages/backend-core/src/queue/constants.ts +++ b/packages/backend-core/src/queue/constants.ts @@ -1,4 +1,5 @@ export enum JobQueue { AUTOMATION = "automationQueue", APP_BACKUP = "appBackupQueue", + AUDIT_LOG = "auditLogQueue", } diff --git a/packages/backend-core/src/queue/queue.ts b/packages/backend-core/src/queue/queue.ts index b34d46e463..c57ebafb1f 100644 --- a/packages/backend-core/src/queue/queue.ts +++ b/packages/backend-core/src/queue/queue.ts @@ -4,7 +4,6 @@ import { JobQueue } from "./constants" import InMemoryQueue from "./inMemoryQueue" import BullQueue from "bull" import { addListeners, StalledFn } from "./listeners" -const { opts: redisOpts, redisProtocolUrl } = getRedisOptions() const CLEANUP_PERIOD_MS = 60 * 1000 let QUEUES: BullQueue.Queue[] | InMemoryQueue[] = [] @@ -20,6 +19,7 @@ export function createQueue( jobQueue: JobQueue, opts: { removeStalledCb?: StalledFn } = {} ): BullQueue.Queue { + const { opts: redisOpts, redisProtocolUrl } = getRedisOptions() const queueConfig: any = redisProtocolUrl || { redis: redisOpts } let queue: any if (!env.isTest()) { @@ -40,8 +40,10 @@ export function createQueue( } export async function shutdown() { - if (QUEUES.length) { + if (cleanupInterval) { clearInterval(cleanupInterval) + } + if (QUEUES.length) { for (let queue of QUEUES) { await queue.close() } diff --git a/packages/backend-core/src/redis/index.ts b/packages/backend-core/src/redis/index.ts index ea4379f048..6585d6e4fa 100644 --- a/packages/backend-core/src/redis/index.ts +++ b/packages/backend-core/src/redis/index.ts @@ -3,4 +3,4 @@ export { default as Client } from "./redis" export * as utils from "./utils" export * as clients from "./init" -export * as redlock from "./redlock" +export * as locks from "./redlockImpl" diff --git a/packages/backend-core/src/redis/init.ts b/packages/backend-core/src/redis/init.ts index 00329ffb84..485268edad 100644 --- a/packages/backend-core/src/redis/init.ts +++ b/packages/backend-core/src/redis/init.ts @@ -20,13 +20,17 @@ async function init() { ).init() } -process.on("exit", async () => { +export async function shutdown() { if (userClient) await userClient.finish() if (sessionClient) await sessionClient.finish() if (appClient) await appClient.finish() if (cacheClient) await cacheClient.finish() if (writethroughClient) await writethroughClient.finish() if (lockClient) await lockClient.finish() +} + +process.on("exit", async () => { + await shutdown() }) export async function getUserClient() { diff --git a/packages/backend-core/src/redis/redis.ts b/packages/backend-core/src/redis/redis.ts index 0267709cdc..951369496a 100644 --- a/packages/backend-core/src/redis/redis.ts +++ b/packages/backend-core/src/redis/redis.ts @@ -1,6 +1,6 @@ import env from "../environment" // ioredis mock is all in memory -const Redis = env.isTest() ? require("ioredis-mock") : require("ioredis") +const Redis = env.MOCK_REDIS ? require("ioredis-mock") : require("ioredis") import { addDbPrefix, removeDbPrefix, @@ -17,8 +17,13 @@ const DEFAULT_SELECT_DB = SelectableDatabase.DEFAULT // for testing just generate the client once let CLOSED = false let CLIENTS: { [key: number]: any } = {} -// if in test always connected -let CONNECTED = env.isTest() + +let CONNECTED = false + +// mock redis always connected +if (env.MOCK_REDIS) { + CONNECTED = true +} function pickClient(selectDb: number): any { return CLIENTS[selectDb] @@ -57,7 +62,7 @@ function init(selectDb = DEFAULT_SELECT_DB) { return } // testing uses a single in memory client - if (env.isTest()) { + if (env.MOCK_REDIS) { CLIENTS[selectDb] = new Redis(getRedisOptions()) } // start the timer - only allowed 5 seconds to connect @@ -86,6 +91,11 @@ function init(selectDb = DEFAULT_SELECT_DB) { } // attach handlers client.on("end", (err: Error) => { + if (env.isTest()) { + // don't try to re-connect in test env + // allow the process to exit + return + } connectionError(selectDb, timeout, err) }) client.on("error", (err: Error) => { diff --git a/packages/backend-core/src/redis/redlock.ts b/packages/backend-core/src/redis/redlockImpl.ts similarity index 73% rename from packages/backend-core/src/redis/redlock.ts rename to packages/backend-core/src/redis/redlockImpl.ts index 54b2c0a8d1..136d7f5d33 100644 --- a/packages/backend-core/src/redis/redlock.ts +++ b/packages/backend-core/src/redis/redlockImpl.ts @@ -1,29 +1,22 @@ import Redlock, { Options } from "redlock" import { getLockClient } from "./init" import { LockOptions, LockType } from "@budibase/types" -import * as tenancy from "../tenancy" - -let noRetryRedlock: Redlock | undefined +import * as context from "../context" +import env from "../environment" const getClient = async (type: LockType): Promise => { + if (env.isTest() && type !== LockType.TRY_ONCE) { + return newRedlock(OPTIONS.TEST) + } switch (type) { case LockType.TRY_ONCE: { - if (!noRetryRedlock) { - noRetryRedlock = await newRedlock(OPTIONS.TRY_ONCE) - } - return noRetryRedlock + return newRedlock(OPTIONS.TRY_ONCE) } case LockType.DEFAULT: { - if (!noRetryRedlock) { - noRetryRedlock = await newRedlock(OPTIONS.DEFAULT) - } - return noRetryRedlock + return newRedlock(OPTIONS.DEFAULT) } case LockType.DELAY_500: { - if (!noRetryRedlock) { - noRetryRedlock = await newRedlock(OPTIONS.DELAY_500) - } - return noRetryRedlock + return newRedlock(OPTIONS.DELAY_500) } default: { throw new Error(`Could not get redlock client: ${type}`) @@ -36,6 +29,11 @@ export const OPTIONS = { // immediately throws an error if the lock is already held retryCount: 0, }, + TEST: { + // higher retry count in unit tests + // due to high contention. + retryCount: 100, + }, DEFAULT: { // the expected clock drift; for more details // see http://redis.io/topics/distlock @@ -69,28 +67,38 @@ export const doWithLock = async (opts: LockOptions, task: any) => { const redlock = await getClient(opts.type) let lock try { - // aquire lock - let name: string = `lock:${tenancy.getTenantId()}_${opts.name}` + // determine lock name + // by default use the tenantId for uniqueness, unless using a system lock + const prefix = opts.systemLock ? "system" : context.getTenantId() + let name: string = `lock:${prefix}_${opts.name}` + + // add additional unique name if required if (opts.nameSuffix) { name = name + `_${opts.nameSuffix}` } + + // create the lock lock = await redlock.lock(name, opts.ttl) + // perform locked task // need to await to ensure completion before unlocking const result = await task() return result } catch (e: any) { - console.log("lock error") + console.warn("lock error") // lock limit exceeded if (e.name === "LockError") { if (opts.type === LockType.TRY_ONCE) { // don't throw for try-once locks, they will always error // due to retry count (0) exceeded + console.warn(e) return } else { + console.error(e) throw e } } else { + console.error(e) throw e } } finally { diff --git a/packages/backend-core/src/redis/utils.ts b/packages/backend-core/src/redis/utils.ts index 4c556ebd54..7606c77b87 100644 --- a/packages/backend-core/src/redis/utils.ts +++ b/packages/backend-core/src/redis/utils.ts @@ -2,8 +2,6 @@ import env from "../environment" const SLOT_REFRESH_MS = 2000 const CONNECT_TIMEOUT_MS = 10000 -const REDIS_URL = !env.REDIS_URL ? "localhost:6379" : env.REDIS_URL -const REDIS_PASSWORD = !env.REDIS_PASSWORD ? "budibase" : env.REDIS_PASSWORD export const SEPARATOR = "-" /** @@ -60,8 +58,8 @@ export enum SelectableDatabase { } export function getRedisOptions(clustered = false) { - let password = REDIS_PASSWORD - let url: string[] | string = REDIS_URL.split("//") + let password = env.REDIS_PASSWORD + let url: string[] | string = env.REDIS_URL.split("//") // get rid of the protocol url = url.length > 1 ? url[1] : url[0] // check for a password etc @@ -78,8 +76,8 @@ export function getRedisOptions(clustered = false) { let redisProtocolUrl // fully qualified redis URL - if (/rediss?:\/\//.test(REDIS_URL)) { - redisProtocolUrl = REDIS_URL + if (/rediss?:\/\//.test(env.REDIS_URL)) { + redisProtocolUrl = env.REDIS_URL } const opts: any = { diff --git a/packages/backend-core/src/tenancy/db.ts b/packages/backend-core/src/tenancy/db.ts new file mode 100644 index 0000000000..10477a8579 --- /dev/null +++ b/packages/backend-core/src/tenancy/db.ts @@ -0,0 +1,6 @@ +import { getDB } from "../db/db" +import { getGlobalDBName } from "../context" + +export function getTenantDB(tenantId: string) { + return getDB(getGlobalDBName(tenantId)) +} diff --git a/packages/backend-core/src/tenancy/index.ts b/packages/backend-core/src/tenancy/index.ts index 1618a136dd..3f17e33271 100644 --- a/packages/backend-core/src/tenancy/index.ts +++ b/packages/backend-core/src/tenancy/index.ts @@ -1,2 +1,2 @@ -export * from "../context" +export * from "./db" export * from "./tenancy" diff --git a/packages/backend-core/src/tenancy/tenancy.ts b/packages/backend-core/src/tenancy/tenancy.ts index 732402bcb7..e8ddf88226 100644 --- a/packages/backend-core/src/tenancy/tenancy.ts +++ b/packages/backend-core/src/tenancy/tenancy.ts @@ -1,4 +1,3 @@ -import { doWithDB, getGlobalDBName } from "../db" import { DEFAULT_TENANT_ID, getTenantId, @@ -11,10 +10,7 @@ import { TenantResolutionStrategy, GetTenantIdOptions, } from "@budibase/types" -import { Header, StaticDatabases } from "../constants" - -const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants -const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name +import { Header } from "../constants" export function addTenantToUrl(url: string) { const tenantId = getTenantId() @@ -27,89 +23,6 @@ export function addTenantToUrl(url: string) { return url } -export async function doesTenantExist(tenantId: string) { - return doWithDB(PLATFORM_INFO_DB, async (db: any) => { - let tenants - try { - tenants = await db.get(TENANT_DOC) - } catch (err) { - // if theres an error the doc doesn't exist, no tenants exist - return false - } - return ( - tenants && - Array.isArray(tenants.tenantIds) && - tenants.tenantIds.indexOf(tenantId) !== -1 - ) - }) -} - -export async function tryAddTenant( - tenantId: string, - userId: string, - email: string, - afterCreateTenant: () => Promise -) { - return doWithDB(PLATFORM_INFO_DB, async (db: any) => { - const getDoc = async (id: string) => { - if (!id) { - return null - } - try { - return await db.get(id) - } catch (err) { - return { _id: id } - } - } - let [tenants, userIdDoc, emailDoc] = await Promise.all([ - getDoc(TENANT_DOC), - getDoc(userId), - getDoc(email), - ]) - if (!Array.isArray(tenants.tenantIds)) { - tenants = { - _id: TENANT_DOC, - tenantIds: [], - } - } - let promises = [] - if (userIdDoc) { - userIdDoc.tenantId = tenantId - promises.push(db.put(userIdDoc)) - } - if (emailDoc) { - emailDoc.tenantId = tenantId - emailDoc.userId = userId - promises.push(db.put(emailDoc)) - } - if (tenants.tenantIds.indexOf(tenantId) === -1) { - tenants.tenantIds.push(tenantId) - promises.push(db.put(tenants)) - await afterCreateTenant() - } - await Promise.all(promises) - }) -} - -export function doWithGlobalDB(tenantId: string, cb: any) { - return doWithDB(getGlobalDBName(tenantId), cb) -} - -export async function lookupTenantId(userId: string) { - return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => { - let tenantId = env.MULTI_TENANCY ? DEFAULT_TENANT_ID : null - try { - const doc = await db.get(userId) - if (doc && doc.tenantId) { - tenantId = doc.tenantId - } - } catch (err) { - // just return the default - } - return tenantId - }) -} - export const isUserInAppTenant = (appId: string, user?: any) => { let userTenantId if (user) { @@ -121,19 +34,6 @@ export const isUserInAppTenant = (appId: string, user?: any) => { return tenantId === userTenantId } -export async function getTenantIds() { - return doWithDB(PLATFORM_INFO_DB, async (db: any) => { - let tenants - try { - tenants = await db.get(TENANT_DOC) - } catch (err) { - // if theres an error the doc doesn't exist, no tenants exist - return [] - } - return (tenants && tenants.tenantIds) || [] - }) -} - const ALL_STRATEGIES = Object.values(TenantResolutionStrategy) export const getTenantIDFromCtx = ( diff --git a/packages/backend-core/src/users.ts b/packages/backend-core/src/users.ts index 1720a79a83..8963f7c141 100644 --- a/packages/backend-core/src/users.ts +++ b/packages/backend-core/src/users.ts @@ -5,18 +5,44 @@ import { generateAppUserID, queryGlobalView, UNICODE_MAX, + directCouchFind, } from "./db" import { BulkDocsResponse, User } from "@budibase/types" import { getGlobalDB } from "./context" +import * as context from "./context" -export const bulkGetGlobalUsersById = async (userIds: string[]) => { +type GetOpts = { cleanup?: boolean } + +function removeUserPassword(users: User | User[]) { + if (Array.isArray(users)) { + return users.map(user => { + if (user) { + delete user.password + return user + } + }) + } else if (users) { + delete users.password + return users + } + return users +} + +export const bulkGetGlobalUsersById = async ( + userIds: string[], + opts?: GetOpts +) => { const db = getGlobalDB() - return ( + let users = ( await db.allDocs({ keys: userIds, include_docs: true, }) ).rows.map(row => row.doc) as User[] + if (opts?.cleanup) { + users = removeUserPassword(users) as User[] + } + return users } export const bulkUpdateGlobalUsers = async (users: User[]) => { @@ -24,13 +50,22 @@ export const bulkUpdateGlobalUsers = async (users: User[]) => { return (await db.bulkDocs(users)) as BulkDocsResponse } +export async function getById(id: string, opts?: GetOpts): Promise { + const db = context.getGlobalDB() + let user = await db.get(id) + if (opts?.cleanup) { + user = removeUserPassword(user) + } + return user +} + /** * Given an email address this will use a view to search through * all the users to find one with this email address. - * @param {string} email the email to lookup the user by. */ export const getGlobalUserByEmail = async ( - email: String + email: String, + opts?: GetOpts ): Promise => { if (email == null) { throw "Must supply an email address to view" @@ -46,10 +81,19 @@ export const getGlobalUserByEmail = async ( throw new Error(`Multiple users found with email address: ${email}`) } - return response + let user = response as User + if (opts?.cleanup) { + user = removeUserPassword(user) as User + } + + return user } -export const searchGlobalUsersByApp = async (appId: any, opts: any) => { +export const searchGlobalUsersByApp = async ( + appId: any, + opts: any, + getOpts?: GetOpts +) => { if (typeof appId !== "string") { throw new Error("Must provide a string based app ID") } @@ -58,10 +102,54 @@ export const searchGlobalUsersByApp = async (appId: any, opts: any) => { }) params.startkey = opts && opts.startkey ? opts.startkey : params.startkey let response = await queryGlobalView(ViewName.USER_BY_APP, params) + if (!response) { response = [] } - return Array.isArray(response) ? response : [response] + let users: User[] = Array.isArray(response) ? response : [response] + if (getOpts?.cleanup) { + users = removeUserPassword(users) as User[] + } + return users +} + +/* + Return any user who potentially has access to the application + Admins, developers and app users with the explicitly role. +*/ +export const searchGlobalUsersByAppAccess = async (appId: any, opts: any) => { + const roleSelector = `roles.${appId}` + + let orQuery: any[] = [ + { + "builder.global": true, + }, + { + "admin.global": true, + }, + ] + + if (appId) { + const roleCheck = { + [roleSelector]: { + $exists: true, + }, + } + orQuery.push(roleCheck) + } + + let searchOptions = { + selector: { + $or: orQuery, + _id: { + $regex: "^us_", + }, + }, + limit: opts?.limit || 50, + } + + const resp = await directCouchFind(context.getGlobalDBName(), searchOptions) + return resp?.rows } export const getGlobalUserByAppPage = (appId: string, user: User) => { @@ -74,7 +162,11 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => { /** * Performs a starts with search on the global email view. */ -export const searchGlobalUsersByEmail = async (email: string, opts: any) => { +export const searchGlobalUsersByEmail = async ( + email: string, + opts: any, + getOpts?: GetOpts +) => { if (typeof email !== "string") { throw new Error("Must provide a string to search by") } @@ -89,5 +181,9 @@ export const searchGlobalUsersByEmail = async (email: string, opts: any) => { if (!response) { response = [] } - return Array.isArray(response) ? response : [response] + let users: User[] = Array.isArray(response) ? response : [response] + if (getOpts?.cleanup) { + users = removeUserPassword(users) as User[] + } + return users } diff --git a/packages/backend-core/src/utils/tests/utils.spec.ts b/packages/backend-core/src/utils/tests/utils.spec.ts index 498aff1555..7d6c5561e8 100644 --- a/packages/backend-core/src/utils/tests/utils.spec.ts +++ b/packages/backend-core/src/utils/tests/utils.spec.ts @@ -1,20 +1,12 @@ -import { structures } from "../../../tests" +import { structures, DBTestConfiguration } from "../../../tests" import * as utils from "../../utils" -import * as events from "../../events" import * as db from "../../db" -import { DEFAULT_TENANT_ID, Header } from "../../constants" -import { doInTenant } from "../../context" +import { Header } from "../../constants" +import { newid } from "../../utils" +import env from "../../environment" describe("utils", () => { - describe("platformLogout", () => { - it("should call platform logout", async () => { - await doInTenant(DEFAULT_TENANT_ID, async () => { - const ctx = structures.koa.newContext() - await utils.platformLogout({ ctx, userId: "test" }) - expect(events.auth.logout).toBeCalledTimes(1) - }) - }) - }) + const config = new DBTestConfiguration() describe("getAppIdFromCtx", () => { it("gets appId from header", async () => { @@ -49,21 +41,28 @@ describe("utils", () => { }) it("gets appId from url", async () => { - const ctx = structures.koa.newContext() - const expected = db.generateAppID() - const app = structures.apps.app(expected) + await config.doInTenant(async () => { + const url = "http://test.com" + env._set("PLATFORM_URL", url) - // set custom url - const appUrl = "custom-url" - app.url = `/${appUrl}` - ctx.path = `/app/${appUrl}` + const ctx = structures.koa.newContext() + ctx.host = `${config.tenantId}.test.com` - // save the app - const database = db.getDB(expected) - await database.put(app) + const expected = db.generateAppID(config.tenantId) + const app = structures.apps.app(expected) - const actual = await utils.getAppIdFromCtx(ctx) - expect(actual).toBe(expected) + // set custom url + const appUrl = newid() + app.url = `/${appUrl}` + ctx.path = `/app/${appUrl}` + + // save the app + const database = db.getDB(expected) + await database.put(app) + + const actual = await utils.getAppIdFromCtx(ctx) + expect(actual).toBe(expected) + }) }) it("doesn't get appId from url when previewing", async () => { diff --git a/packages/backend-core/src/utils/utils.ts b/packages/backend-core/src/utils/utils.ts index c608686431..3efd40ca80 100644 --- a/packages/backend-core/src/utils/utils.ts +++ b/packages/backend-core/src/utils/utils.ts @@ -2,21 +2,19 @@ import { getAllApps, queryGlobalView } from "../db" import { options } from "../middleware/passport/jwt" import { Header, - Cookie, MAX_VALID_DATE, DocumentType, SEPARATOR, ViewName, } from "../constants" import env from "../environment" -import * as userCache from "../cache/user" -import { getSessionsForUser, invalidateSessions } from "../security/sessions" -import * as events from "../events" import * as tenancy from "../tenancy" +import * as context from "../context" import { App, + AuditedEventFriendlyName, Ctx, - PlatformLogoutOpts, + Event, TenantResolutionStrategy, } from "@budibase/types" import { SetOption } from "cookies" @@ -38,7 +36,7 @@ export async function resolveAppUrl(ctx: Ctx) { const appUrl = ctx.path.split("/")[2] let possibleAppUrl = `/${appUrl.toLowerCase()}` - let tenantId: string | null = tenancy.getTenantId() + let tenantId: string | null = context.getTenantId() if (env.MULTI_TENANCY) { // always use the tenant id from the subdomain in multi tenancy // this ensures the logged-in user tenant id doesn't overwrite @@ -49,7 +47,7 @@ export async function resolveAppUrl(ctx: Ctx) { } // search prod apps for a url that matches - const apps: App[] = await tenancy.doInTenant(tenantId, () => + const apps: App[] = await context.doInTenant(tenantId, () => getAllApps({ dev: false }) ) const app = apps.filter( @@ -222,35 +220,10 @@ export async function getBuildersCount() { return builders.length } -/** - * Logs a user out from budibase. Re-used across account portal and builder. - */ -export async function platformLogout(opts: PlatformLogoutOpts) { - const ctx = opts.ctx - const userId = opts.userId - const keepActiveSession = opts.keepActiveSession - - if (!ctx) throw new Error("Koa context must be supplied to logout.") - - const currentSession = getCookie(ctx, Cookie.Auth) - let sessions = await getSessionsForUser(userId) - - if (keepActiveSession) { - sessions = sessions.filter( - session => session.sessionId !== currentSession.sessionId - ) - } else { - // clear cookies - clearCookie(ctx, Cookie.Auth) - clearCookie(ctx, Cookie.CurrentApp) - } - - const sessionIds = sessions.map(({ sessionId }) => sessionId) - await invalidateSessions(userId, { sessionIds, reason: "logout" }) - await events.auth.logout() - await userCache.invalidateUser(userId) -} - export function timeout(timeMs: number) { return new Promise(resolve => setTimeout(resolve, timeMs)) } + +export function isAudited(event: Event) { + return !!AuditedEventFriendlyName[event] +} diff --git a/packages/backend-core/tests/jestEnv.ts b/packages/backend-core/tests/jestEnv.ts new file mode 100644 index 0000000000..ec8de2942e --- /dev/null +++ b/packages/backend-core/tests/jestEnv.ts @@ -0,0 +1,6 @@ +process.env.SELF_HOSTED = "1" +process.env.MULTI_TENANCY = "1" +process.env.NODE_ENV = "jest" +process.env.MOCK_REDIS = "1" +process.env.LOG_LEVEL = process.env.LOG_LEVEL || "error" +process.env.ENABLE_4XX_HTTP_LOGGING = "0" diff --git a/packages/backend-core/tests/jestSetup.ts b/packages/backend-core/tests/jestSetup.ts index b7ab5b49d9..e786086de6 100644 --- a/packages/backend-core/tests/jestSetup.ts +++ b/packages/backend-core/tests/jestSetup.ts @@ -1,5 +1,6 @@ +import "./logging" import env from "../src/environment" -import { mocks } from "./utilities" +import { mocks, testContainerUtils } from "./utilities" // must explicitly enable fetch mock mocks.fetch.enable() @@ -9,16 +10,8 @@ mocks.fetch.enable() import tk from "timekeeper" tk.freeze(mocks.date.MOCK_DATE) -env._set("SELF_HOSTED", "1") -env._set("NODE_ENV", "jest") -env._set("JWT_SECRET", "test-jwtsecret") -env._set("LOG_LEVEL", "silent") -env._set("MINIO_URL", "http://localhost") -env._set("MINIO_ACCESS_KEY", "test") -env._set("MINIO_SECRET_KEY", "test") - if (!process.env.DEBUG) { - global.console.log = jest.fn() // console.log are ignored in tests + console.log = jest.fn() // console.log are ignored in tests } if (!process.env.CI) { @@ -26,3 +19,5 @@ if (!process.env.CI) { // 100 seconds jest.setTimeout(100000) } + +testContainerUtils.setupEnv(env) diff --git a/packages/backend-core/tests/logging.ts b/packages/backend-core/tests/logging.ts new file mode 100644 index 0000000000..271f4d62ff --- /dev/null +++ b/packages/backend-core/tests/logging.ts @@ -0,0 +1,34 @@ +export enum LogLevel { + TRACE = "trace", + DEBUG = "debug", + INFO = "info", + WARN = "warn", + ERROR = "error", +} + +const LOG_INDEX: { [key in LogLevel]: number } = { + [LogLevel.TRACE]: 1, + [LogLevel.DEBUG]: 2, + [LogLevel.INFO]: 3, + [LogLevel.WARN]: 4, + [LogLevel.ERROR]: 5, +} + +const setIndex = LOG_INDEX[process.env.LOG_LEVEL as LogLevel] + +if (setIndex > LOG_INDEX.trace) { + global.console.trace = jest.fn() +} + +if (setIndex > LOG_INDEX.debug) { + global.console.debug = jest.fn() +} + +if (setIndex > LOG_INDEX.info) { + global.console.info = jest.fn() + global.console.log = jest.fn() +} + +if (setIndex > LOG_INDEX.warn) { + global.console.warn = jest.fn() +} diff --git a/packages/backend-core/tests/utilities/DBTestConfiguration.ts b/packages/backend-core/tests/utilities/DBTestConfiguration.ts new file mode 100644 index 0000000000..e5e57a99a3 --- /dev/null +++ b/packages/backend-core/tests/utilities/DBTestConfiguration.ts @@ -0,0 +1,36 @@ +import "./mocks" +import * as structures from "./structures" +import * as testEnv from "./testEnv" +import * as context from "../../src/context" + +class DBTestConfiguration { + tenantId: string + + constructor() { + // db tests need to be multi tenant to prevent conflicts + testEnv.multiTenant() + this.tenantId = structures.tenant.id() + } + + newTenant() { + this.tenantId = structures.tenant.id() + } + + // TENANCY + + doInTenant(task: any) { + return context.doInTenant(this.tenantId, () => { + return task() + }) + } + + getTenantId() { + try { + return context.getTenantId() + } catch (e) { + return this.tenantId! + } + } +} + +export default DBTestConfiguration diff --git a/packages/backend-core/tests/utilities/db.ts b/packages/backend-core/tests/utilities/db.ts deleted file mode 100644 index 84b77bb201..0000000000 --- a/packages/backend-core/tests/utilities/db.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as db from "../../src/db" - -const dbConfig = { - inMemory: true, -} - -export const init = () => { - db.init(dbConfig) -} diff --git a/packages/backend-core/tests/utilities/index.ts b/packages/backend-core/tests/utilities/index.ts index ee96a94152..efe014908b 100644 --- a/packages/backend-core/tests/utilities/index.ts +++ b/packages/backend-core/tests/utilities/index.ts @@ -2,6 +2,6 @@ export * as mocks from "./mocks" export * as structures from "./structures" export { generator } from "./structures" export * as testEnv from "./testEnv" +export * as testContainerUtils from "./testContainerUtils" -import * as dbConfig from "./db" -dbConfig.init() +export { default as DBTestConfiguration } from "./DBTestConfiguration" diff --git a/packages/backend-core/tests/utilities/mocks/accounts.ts b/packages/backend-core/tests/utilities/mocks/accounts.ts deleted file mode 100644 index e40d32b276..0000000000 --- a/packages/backend-core/tests/utilities/mocks/accounts.ts +++ /dev/null @@ -1,13 +0,0 @@ -const mockGetAccount = jest.fn() -const mockGetAccountByTenantId = jest.fn() -const mockGetStatus = jest.fn() - -jest.mock("../../../src/cloud/accounts", () => ({ - getAccount: mockGetAccount, - getAccountByTenantId: mockGetAccountByTenantId, - getStatus: mockGetStatus, -})) - -export const getAccount = mockGetAccount -export const getAccountByTenantId = mockGetAccountByTenantId -export const getStatus = mockGetStatus diff --git a/packages/backend-core/tests/utilities/mocks/index.ts b/packages/backend-core/tests/utilities/mocks/index.ts index 401fd7d7a7..f5f45c0342 100644 --- a/packages/backend-core/tests/utilities/mocks/index.ts +++ b/packages/backend-core/tests/utilities/mocks/index.ts @@ -1,4 +1,7 @@ -export * as accounts from "./accounts" +jest.mock("../../../src/accounts") +import * as _accounts from "../../../src/accounts" +export const accounts = jest.mocked(_accounts) + export * as date from "./date" export * as licenses from "./licenses" export { default as fetch } from "./fetch" diff --git a/packages/backend-core/tests/utilities/mocks/licenses.ts b/packages/backend-core/tests/utilities/mocks/licenses.ts index e374612f5f..2ca41616e4 100644 --- a/packages/backend-core/tests/utilities/mocks/licenses.ts +++ b/packages/backend-core/tests/utilities/mocks/licenses.ts @@ -70,6 +70,10 @@ export const useBackups = () => { return useFeature(Feature.APP_BACKUPS) } +export const useEnforceableSSO = () => { + return useFeature(Feature.ENFORCEABLE_SSO) +} + export const useGroups = () => { return useFeature(Feature.USER_GROUPS) } @@ -78,6 +82,10 @@ export const useEnvironmentVariables = () => { return useFeature(Feature.ENVIRONMENT_VARIABLES) } +export const useAuditLogs = () => { + return useFeature(Feature.AUDIT_LOGS) +} + // QUOTAS export const setAutomationLogsQuota = (value: number) => { diff --git a/packages/backend-core/tests/utilities/structures/accounts.ts b/packages/backend-core/tests/utilities/structures/accounts.ts index f1718aecc0..62a9ac19d1 100644 --- a/packages/backend-core/tests/utilities/structures/accounts.ts +++ b/packages/backend-core/tests/utilities/structures/accounts.ts @@ -1,6 +1,17 @@ import { generator, uuid } from "." import * as db from "../../../src/db/utils" -import { Account, AuthType, CloudAccount, Hosting } from "@budibase/types" +import { + Account, + AccountSSOProvider, + AccountSSOProviderType, + AuthType, + CloudAccount, + Hosting, + SSOAccount, + CreateAccount, + CreatePassswordAccount, +} from "@budibase/types" +import _ from "lodash" export const account = (): Account => { return { @@ -20,6 +31,10 @@ export const account = (): Account => { } } +export function selfHostAccount() { + return account() +} + export const cloudAccount = (): CloudAccount => { return { ...account(), @@ -27,3 +42,74 @@ export const cloudAccount = (): CloudAccount => { budibaseUserId: db.generateGlobalUserID(), } } + +function providerType(): AccountSSOProviderType { + return _.sample( + Object.values(AccountSSOProviderType) + ) as AccountSSOProviderType +} + +function provider(): AccountSSOProvider { + return _.sample(Object.values(AccountSSOProvider)) as AccountSSOProvider +} + +export function ssoAccount(account: Account = cloudAccount()): SSOAccount { + return { + ...account, + authType: AuthType.SSO, + oauth2: { + accessToken: generator.string(), + refreshToken: generator.string(), + }, + pictureUrl: generator.url(), + provider: provider(), + providerType: providerType(), + thirdPartyProfile: {}, + } +} + +export const cloudCreateAccount: CreatePassswordAccount = { + email: "cloud@budibase.com", + tenantId: "cloud", + hosting: Hosting.CLOUD, + authType: AuthType.PASSWORD, + password: "Password123!", + tenantName: "cloud", + name: "Budi Armstrong", + size: "10+", + profession: "Software Engineer", +} + +export const cloudSSOCreateAccount: CreateAccount = { + email: "cloud-sso@budibase.com", + tenantId: "cloud-sso", + hosting: Hosting.CLOUD, + authType: AuthType.SSO, + tenantName: "cloudsso", + name: "Budi Armstrong", + size: "10+", + profession: "Software Engineer", +} + +export const selfCreateAccount: CreatePassswordAccount = { + email: "self@budibase.com", + tenantId: "self", + hosting: Hosting.SELF, + authType: AuthType.PASSWORD, + password: "Password123!", + tenantName: "self", + name: "Budi Armstrong", + size: "10+", + profession: "Software Engineer", +} + +export const selfSSOCreateAccount: CreateAccount = { + email: "self-sso@budibase.com", + tenantId: "self-sso", + hosting: Hosting.SELF, + authType: AuthType.SSO, + tenantName: "selfsso", + name: "Budi Armstrong", + size: "10+", + profession: "Software Engineer", +} diff --git a/packages/backend-core/tests/utilities/structures/db.ts b/packages/backend-core/tests/utilities/structures/db.ts new file mode 100644 index 0000000000..e25b707cb9 --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/db.ts @@ -0,0 +1,5 @@ +import { newid } from "../../../src/newid" + +export function id() { + return `db_${newid()}` +} diff --git a/packages/backend-core/tests/utilities/structures/generator.ts b/packages/backend-core/tests/utilities/structures/generator.ts new file mode 100644 index 0000000000..51567b152e --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/generator.ts @@ -0,0 +1,2 @@ +import Chance from "chance" +export const generator = new Chance() diff --git a/packages/backend-core/tests/utilities/structures/index.ts b/packages/backend-core/tests/utilities/structures/index.ts index 0d0f0c507f..ca77f476d0 100644 --- a/packages/backend-core/tests/utilities/structures/index.ts +++ b/packages/backend-core/tests/utilities/structures/index.ts @@ -1,10 +1,11 @@ export * from "./common" - -import Chance from "chance" -export const generator = new Chance() - export * as accounts from "./accounts" export * as apps from "./apps" +export * as db from "./db" export * as koa from "./koa" export * as licenses from "./licenses" export * as plugins from "./plugins" +export * as sso from "./sso" +export * as tenant from "./tenants" +export * as users from "./users" +export { generator } from "./generator" diff --git a/packages/backend-core/tests/utilities/structures/shared.ts b/packages/backend-core/tests/utilities/structures/shared.ts new file mode 100644 index 0000000000..de0e19486c --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/shared.ts @@ -0,0 +1,19 @@ +import { User } from "@budibase/types" +import { generator } from "./generator" +import { uuid } from "./common" + +export const newEmail = () => { + return `${uuid()}@test.com` +} + +export const user = (userProps?: any): User => { + return { + email: newEmail(), + password: "test", + roles: { app_test: "admin" }, + firstName: generator.first(), + lastName: generator.last(), + pictureUrl: "http://test.com", + ...userProps, + } +} diff --git a/packages/backend-core/tests/utilities/structures/sso.ts b/packages/backend-core/tests/utilities/structures/sso.ts new file mode 100644 index 0000000000..7413fa3c09 --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/sso.ts @@ -0,0 +1,109 @@ +import { + GoogleInnerConfig, + JwtClaims, + OAuth2, + OIDCInnerConfig, + OIDCWellKnownConfig, + SSOAuthDetails, + SSOProfile, + SSOProviderType, + User, +} from "@budibase/types" +import { generator } from "./generator" +import { uuid, email } from "./common" +import * as shared from "./shared" +import _ from "lodash" +import { user } from "./shared" + +export function OAuth(): OAuth2 { + return { + refreshToken: generator.string(), + accessToken: generator.string(), + } +} + +export function authDetails(userDoc?: User): SSOAuthDetails { + if (!userDoc) { + userDoc = user() + } + + const userId = userDoc._id || uuid() + const provider = generator.string() + + const profile = ssoProfile(userDoc) + profile.provider = provider + profile.id = userId + + return { + email: userDoc.email, + oauth2: OAuth(), + profile, + provider, + providerType: providerType(), + userId, + } +} + +export function providerType(): SSOProviderType { + return _.sample(Object.values(SSOProviderType)) as SSOProviderType +} + +export function ssoProfile(user?: User): SSOProfile { + if (!user) { + user = shared.user() + } + return { + id: user._id!, + name: { + givenName: user.firstName, + familyName: user.lastName, + }, + _json: { + email: user.email, + picture: "http://test.com", + }, + provider: generator.string(), + } +} + +// OIDC + +export function oidcConfig(): OIDCInnerConfig { + return { + uuid: uuid(), + activated: true, + logo: "", + name: generator.string(), + configUrl: "http://someconfigurl", + clientID: generator.string(), + clientSecret: generator.string(), + scopes: [], + } +} + +// response from .well-known/openid-configuration +export function oidcWellKnownConfig(): OIDCWellKnownConfig { + return { + issuer: generator.string(), + authorization_endpoint: generator.url(), + token_endpoint: generator.url(), + userinfo_endpoint: generator.url(), + } +} + +export function jwtClaims(): JwtClaims { + return { + email: email(), + preferred_username: email(), + } +} + +// GOOGLE + +export function googleConfig(): GoogleInnerConfig { + return { + activated: true, + clientID: generator.string(), + clientSecret: generator.string(), + } +} diff --git a/packages/backend-core/tests/utilities/structures/tenants.ts b/packages/backend-core/tests/utilities/structures/tenants.ts new file mode 100644 index 0000000000..b23bc8be75 --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/tenants.ts @@ -0,0 +1,5 @@ +import { newid } from "../../../src/newid" + +export function id() { + return `tenant-${newid()}` +} diff --git a/packages/backend-core/tests/utilities/structures/users.ts b/packages/backend-core/tests/utilities/structures/users.ts new file mode 100644 index 0000000000..7a6b4f0d80 --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/users.ts @@ -0,0 +1,54 @@ +import { + AdminUser, + BuilderUser, + SSOAuthDetails, + SSOUser, +} from "@budibase/types" +import { user } from "./shared" +import { authDetails } from "./sso" + +export { user, newEmail } from "./shared" + +export const adminUser = (userProps?: any): AdminUser => { + return { + ...user(userProps), + admin: { + global: true, + }, + builder: { + global: true, + }, + } +} + +export const builderUser = (userProps?: any): BuilderUser => { + return { + ...user(userProps), + builder: { + global: true, + }, + } +} + +export function ssoUser( + opts: { user?: any; details?: SSOAuthDetails } = {} +): SSOUser { + const base = user(opts.user) + delete base.password + + if (!opts.details) { + opts.details = authDetails(base) + } + + return { + ...base, + forceResetPassword: false, + oauth2: opts.details?.oauth2, + provider: opts.details?.provider!, + providerType: opts.details?.providerType!, + thirdPartyProfile: { + email: base.email, + picture: base.pictureUrl, + }, + } +} diff --git a/packages/backend-core/tests/utilities/testContainerUtils.ts b/packages/backend-core/tests/utilities/testContainerUtils.ts new file mode 100644 index 0000000000..f6c702f7ef --- /dev/null +++ b/packages/backend-core/tests/utilities/testContainerUtils.ts @@ -0,0 +1,98 @@ +import { execSync } from "child_process" + +let dockerPsResult: string | undefined + +function formatDockerPsResult(serverName: string, port: number) { + const lines = dockerPsResult?.split("\n") + let first = true + if (!lines) { + return null + } + for (let line of lines) { + if (first) { + first = false + continue + } + let toLookFor = serverName.split("-service")[0] + if (!line.includes(toLookFor)) { + continue + } + const regex = new RegExp(`0.0.0.0:([0-9]*)->${port}`, "g") + const found = line.match(regex) + if (found) { + return found[0].split(":")[1].split("->")[0] + } + } + return null +} + +function getTestContainerSettings( + serverName: string, + key: string +): string | null { + const entry = Object.entries(global).find( + ([k]) => + k.includes(`_${serverName.toUpperCase()}`) && + k.includes(`_${key.toUpperCase()}__`) + ) + if (!entry) { + return null + } + return entry[1] +} + +function getContainerInfo(containerName: string, port: number) { + let assignedPort = getTestContainerSettings( + containerName.toUpperCase(), + `PORT_${port}` + ) + if (!dockerPsResult) { + try { + const outputBuffer = execSync("docker ps") + dockerPsResult = outputBuffer.toString("utf8") + } catch (err) { + //no-op + } + } + const possiblePort = formatDockerPsResult(containerName, port) + if (possiblePort) { + assignedPort = possiblePort + } + const host = getTestContainerSettings(containerName.toUpperCase(), "IP") + return { + port: assignedPort, + host, + url: host && assignedPort && `http://${host}:${assignedPort}`, + } +} + +function getCouchConfig() { + return getContainerInfo("couchdb-service", 5984) +} + +function getMinioConfig() { + return getContainerInfo("minio-service", 9000) +} + +function getRedisConfig() { + return getContainerInfo("redis-service", 6379) +} + +export function setupEnv(...envs: any[]) { + const couch = getCouchConfig(), + minio = getCouchConfig(), + redis = getRedisConfig() + 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)) { + for (const env of envs) { + env._set(config.key, config.value) + } + } +} diff --git a/packages/backend-core/tests/utilities/testEnv.ts b/packages/backend-core/tests/utilities/testEnv.ts index b4f06b5153..b138e019fc 100644 --- a/packages/backend-core/tests/utilities/testEnv.ts +++ b/packages/backend-core/tests/utilities/testEnv.ts @@ -1,12 +1,12 @@ import env from "../../src/environment" -import * as tenancy from "../../src/tenancy" -import { newid } from "../../src/utils" +import * as context from "../../src/context" +import * as structures from "./structures" // TENANCY export async function withTenant(task: (tenantId: string) => any) { - const tenantId = newid() - return tenancy.doInTenant(tenantId, async () => { + const tenantId = structures.tenant.id() + return context.doInTenant(tenantId, async () => { await task(tenantId) }) } @@ -19,6 +19,14 @@ export function multiTenant() { env._set("MULTI_TENANCY", 1) } +export function selfHosted() { + env._set("SELF_HOSTED", 1) +} + +export function cloudHosted() { + env._set("SELF_HOSTED", 0) +} + // NODE export function nodeDev() { diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock index 03393a4ea8..91c5c6c9f3 100644 --- a/packages/backend-core/yarn.lock +++ b/packages/backend-core/yarn.lock @@ -10,64 +10,128 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" + integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + dependencies: + "@babel/highlight" "^7.16.7" + +"@babel/code-frame@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== dependencies: "@babel/highlight" "^7.18.6" -"@babel/compat-data@^7.20.5": - version "7.20.10" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.10.tgz#9d92fa81b87542fff50e848ed585b4212c1d34ec" - integrity sha512-sEnuDPpOJR/fcafHMjpcpGN5M2jbUGUHwmuWKM/YdPzeEDJg8bgmbcWQFUfE32MQjti1koACvoPVsDe8Uq+idg== +"@babel/compat-data@^7.17.10": + version "7.17.10" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.10.tgz#711dc726a492dfc8be8220028b1b92482362baab" + integrity sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw== -"@babel/core@^7.11.6", "@babel/core@^7.12.3": - version "7.20.12" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d" - integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg== +"@babel/compat-data@^7.20.0": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.1.tgz#f2e6ef7790d8c8dbf03d379502dcc246dcce0b30" + integrity sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ== + +"@babel/core@^7.11.6": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.2.tgz#8dc9b1620a673f92d3624bd926dc49a52cf25b92" + integrity sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g== dependencies: "@ampproject/remapping" "^2.1.0" "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helpers" "^7.20.7" - "@babel/parser" "^7.20.7" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.12" - "@babel/types" "^7.20.7" + "@babel/generator" "^7.20.2" + "@babel/helper-compilation-targets" "^7.20.0" + "@babel/helper-module-transforms" "^7.20.2" + "@babel/helpers" "^7.20.1" + "@babel/parser" "^7.20.2" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.20.1" + "@babel/types" "^7.20.2" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" - json5 "^2.2.2" + json5 "^2.2.1" semver "^6.3.0" -"@babel/generator@^7.20.7", "@babel/generator@^7.7.2": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.7.tgz#f8ef57c8242665c5929fe2e8d82ba75460187b4a" - integrity sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw== +"@babel/core@^7.12.3": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.2.tgz#87b2fcd7cce9becaa7f5acebdc4f09f3dd19d876" + integrity sha512-A8pri1YJiC5UnkdrWcmfZTJTV85b4UXTAfImGmCfYmax4TR9Cw8sDS0MOk++Gp2mE/BefVJ5nwy5yzqNJbP/DQ== dependencies: - "@babel/types" "^7.20.7" + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.18.2" + "@babel/helper-compilation-targets" "^7.18.2" + "@babel/helper-module-transforms" "^7.18.0" + "@babel/helpers" "^7.18.2" + "@babel/parser" "^7.18.0" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.2" + "@babel/types" "^7.18.2" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + +"@babel/generator@^7.18.2", "@babel/generator@^7.7.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.2.tgz#33873d6f89b21efe2da63fe554460f3df1c5880d" + integrity sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw== + dependencies: + "@babel/types" "^7.18.2" + "@jridgewell/gen-mapping" "^0.3.0" + jsesc "^2.5.1" + +"@babel/generator@^7.20.1", "@babel/generator@^7.20.2": + version "7.20.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.4.tgz#4d9f8f0c30be75fd90a0562099a26e5839602ab8" + integrity sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA== + dependencies: + "@babel/types" "^7.20.2" "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" -"@babel/helper-compilation-targets@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz#a6cd33e93629f5eb473b021aac05df62c4cd09bb" - integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ== +"@babel/helper-compilation-targets@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.2.tgz#67a85a10cbd5fc7f1457fec2e7f45441dc6c754b" + integrity sha512-s1jnPotJS9uQnzFtiZVBUxe67CuBa679oWFHpxYYnTpRL/1ffhyX44R9uYiXoa/pLXcY9H2moJta0iaanlk/rQ== dependencies: - "@babel/compat-data" "^7.20.5" + "@babel/compat-data" "^7.17.10" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.20.2" + semver "^6.3.0" + +"@babel/helper-compilation-targets@^7.20.0": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz#6bf5374d424e1b3922822f1d9bdaa43b1a139d0a" + integrity sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ== + dependencies: + "@babel/compat-data" "^7.20.0" "@babel/helper-validator-option" "^7.18.6" browserslist "^4.21.3" - lru-cache "^5.1.1" semver "^6.3.0" +"@babel/helper-environment-visitor@^7.16.7", "@babel/helper-environment-visitor@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.2.tgz#8a6d2dedb53f6bf248e31b4baf38739ee4a637bd" + integrity sha512-14GQKWkX9oJzPiQQ7/J36FTXcD4kSp8egKjO9nINlSKiHITRA9q/R74qu8S9xlc/b/yjsJItQUeeh3xnGN0voQ== + "@babel/helper-environment-visitor@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== +"@babel/helper-function-name@^7.17.9": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz#136fcd54bc1da82fcb47565cf16fd8e444b1ff12" + integrity sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg== + dependencies: + "@babel/template" "^7.16.7" + "@babel/types" "^7.17.0" + "@babel/helper-function-name@^7.19.0": version "7.19.0" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" @@ -76,6 +140,13 @@ "@babel/template" "^7.18.10" "@babel/types" "^7.19.0" +"@babel/helper-hoist-variables@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" + integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-hoist-variables@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" @@ -83,6 +154,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-module-imports@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" + integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-module-imports@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" @@ -90,24 +168,45 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-module-transforms@^7.20.11": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz#df4c7af713c557938c50ea3ad0117a7944b2f1b0" - integrity sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg== +"@babel/helper-module-transforms@^7.18.0": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.0.tgz#baf05dec7a5875fb9235bd34ca18bad4e21221cd" + integrity sha512-kclUYSUBIjlvnzN2++K9f2qzYKFgjmnmjwL4zlmU5f8ZtzgWe8s0rUPSTGy2HmK4P8T52MQsS+HTQAgZd3dMEA== + dependencies: + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-simple-access" "^7.17.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.0" + "@babel/types" "^7.18.0" + +"@babel/helper-module-transforms@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz#ac53da669501edd37e658602a21ba14c08748712" + integrity sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA== dependencies: "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-module-imports" "^7.18.6" "@babel/helper-simple-access" "^7.20.2" "@babel/helper-split-export-declaration" "^7.18.6" "@babel/helper-validator-identifier" "^7.19.1" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.10" - "@babel/types" "^7.20.7" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.20.1" + "@babel/types" "^7.20.2" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.8.0": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" - integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.17.12", "@babel/helper-plugin-utils@^7.8.0": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.17.12.tgz#86c2347da5acbf5583ba0a10aed4c9bf9da9cf96" + integrity sha512-JDkf04mqtN3y4iAbO1hv9U2ARpPyPL1zqyWs/2WG1pgSq9llHFjStX5jdxb84himgJm+8Ng+x0oiWF/nw/XQKA== + +"@babel/helper-simple-access@^7.17.7": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.2.tgz#4dc473c2169ac3a1c9f4a51cfcd091d1c36fcff9" + integrity sha512-7LIrjYzndorDY88MycupkpQLKS1AFfsVRm2k/9PtKScSy5tZq0McZTj+DiMRynboZfIqOKvo03pmhTaUgiD6fQ== + dependencies: + "@babel/types" "^7.18.2" "@babel/helper-simple-access@^7.20.2": version "7.20.2" @@ -116,6 +215,13 @@ dependencies: "@babel/types" "^7.20.2" +"@babel/helper-split-export-declaration@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" + integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== + dependencies: + "@babel/types" "^7.16.7" + "@babel/helper-split-export-declaration@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" @@ -128,24 +234,52 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": version "7.19.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-option@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" + integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== + "@babel/helper-validator-option@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== -"@babel/helpers@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.7.tgz#04502ff0feecc9f20ecfaad120a18f011a8e6dce" - integrity sha512-PBPjs5BppzsGaxHQCDKnZ6Gd9s6xl8bBCluz3vEInLGRJmnZan4F6BYCeqtyXqkk4W5IlPmjK4JlOuZkpJ3xZA== +"@babel/helpers@^7.18.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.2.tgz#970d74f0deadc3f5a938bfa250738eb4ac889384" + integrity sha512-j+d+u5xT5utcQSzrh9p+PaJX94h++KN+ng9b9WEJq7pkUPAd61FGqhjuUEdfknb3E/uDBb7ruwEeKkIxNJPIrg== dependencies: - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.7" - "@babel/types" "^7.20.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.2" + "@babel/types" "^7.18.2" + +"@babel/helpers@^7.20.1": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.1.tgz#2ab7a0fcb0a03b5bf76629196ed63c2d7311f4c9" + integrity sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg== + dependencies: + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.20.1" + "@babel/types" "^7.20.0" + +"@babel/highlight@^7.16.7": + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.17.12.tgz#257de56ee5afbd20451ac0a75686b6b404257351" + integrity sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + chalk "^2.0.0" + js-tokens "^4.0.0" "@babel/highlight@^7.18.6": version "7.18.6" @@ -156,10 +290,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.7.tgz#66fe23b3c8569220817d5feb8b9dcdc95bb4f71b" - integrity sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg== +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.7", "@babel/parser@^7.18.0": + version "7.18.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.4.tgz#6774231779dd700e0af29f6ad8d479582d7ce5ef" + integrity sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow== + +"@babel/parser@^7.18.10", "@babel/parser@^7.20.1", "@babel/parser@^7.20.2": + version "7.20.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.3.tgz#5358cf62e380cf69efcb87a7bb922ff88bfac6e2" + integrity sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg== "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -246,62 +385,100 @@ "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-typescript@^7.7.2": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz#4e9a0cfc769c85689b77a2e642d24e9f697fc8c7" - integrity sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ== + version "7.17.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.17.12.tgz#b54fc3be6de734a56b87508f99d6428b5b605a7b" + integrity sha512-TYY0SXFiO31YXtNg3HtFwNJHjLsAyIIhAhNWkQ5whPPS7HWUFlg9z0Ta4qAQNjQbP1wsSt/oKkmZ/4/WWdMUpw== dependencies: - "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-plugin-utils" "^7.17.12" "@babel/runtime@^7.15.4": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd" - integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ== + version "7.18.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4" + integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug== dependencies: - regenerator-runtime "^0.13.11" + regenerator-runtime "^0.13.4" -"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" - integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== +"@babel/template@^7.16.7", "@babel/template@^7.3.3": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" + integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/template@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" + integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== dependencies: "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" + "@babel/parser" "^7.18.10" + "@babel/types" "^7.18.10" -"@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.7", "@babel/traverse@^7.7.2": - version "7.20.12" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.12.tgz#7f0f787b3a67ca4475adef1f56cb94f6abd4a4b5" - integrity sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ== +"@babel/traverse@^7.18.0", "@babel/traverse@^7.18.2", "@babel/traverse@^7.7.2": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.2.tgz#b77a52604b5cc836a9e1e08dca01cba67a12d2e8" + integrity sha512-9eNwoeovJ6KH9zcCNnENY7DMFwTU9JdGCFtqNLfUAqtUHRCOsTOqWoffosP8vKmNYeSBUv3yVJXjfd8ucwOjUA== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.18.2" + "@babel/helper-environment-visitor" "^7.18.2" + "@babel/helper-function-name" "^7.17.9" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.18.0" + "@babel/types" "^7.18.2" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/traverse@^7.20.1": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.1.tgz#9b15ccbf882f6d107eeeecf263fbcdd208777ec8" + integrity sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA== dependencies: "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" + "@babel/generator" "^7.20.1" "@babel/helper-environment-visitor" "^7.18.9" "@babel/helper-function-name" "^7.19.0" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" + "@babel/parser" "^7.20.1" + "@babel/types" "^7.20.0" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.3.0", "@babel/types@^7.3.3": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f" - integrity sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg== +"@babel/types@^7.0.0", "@babel/types@^7.16.7", "@babel/types@^7.17.0", "@babel/types@^7.18.0", "@babel/types@^7.18.2", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.18.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.4.tgz#27eae9b9fd18e9dccc3f9d6ad051336f307be354" + integrity sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + +"@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.2.tgz#67ac09266606190f496322dbaff360fdaa5e7842" + integrity sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog== dependencies: "@babel/helper-string-parser" "^7.19.4" "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@balena/dockerignore@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@balena/dockerignore/-/dockerignore-1.0.2.tgz#9ffe4726915251e8eb69f44ef3547e0da2c03e0d" + integrity sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q== + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/nano@10.1.1": - version "10.1.1" - resolved "https://registry.yarnpkg.com/@budibase/nano/-/nano-10.1.1.tgz#36ccda4d9bb64b5ee14dd2b27a295b40739b1038" - integrity sha512-kbMIzMkjVtl+xI0UPwVU0/pn8/ccxTyfzwBz6Z+ZiN2oUSb0fJCe0qwA6o8dxwSa8nZu4MbGAeMJl3CJndmWtA== +"@budibase/nano@10.1.2": + version "10.1.2" + resolved "https://registry.yarnpkg.com/@budibase/nano/-/nano-10.1.2.tgz#10fae5a1ab39be6a81261f40e7b7ec6d21cbdd4a" + integrity sha512-1w+YN2n/M5aZ9hBKCP4NEjdQbT8BfCLRizkdvm0Je665eEHw3aE1hvo8mon9Ro9QuDdxj1DfDMMFnym6/QUwpQ== dependencies: "@types/tough-cookie" "^4.0.2" axios "^1.1.3" @@ -310,6 +487,19 @@ qs "^6.11.0" tough-cookie "^4.1.2" +"@budibase/pouchdb-replication-stream@1.2.10": + version "1.2.10" + resolved "https://registry.yarnpkg.com/@budibase/pouchdb-replication-stream/-/pouchdb-replication-stream-1.2.10.tgz#4100df2effd7c823edadddcdbdc380f6827eebf5" + integrity sha512-1zeorOwbelZ7HF5vFB+pKE8Mnh31om8k1M6T3AZXVULYTHLsyJrMTozSv5CJ1P8ZfOIJab09HDzCXDh2icFekg== + dependencies: + argsarray "0.0.1" + inherits "^2.0.3" + lodash.pick "^4.0.0" + ndjson "^1.4.3" + pouch-stream "^0.4.0" + pouchdb-promise "^6.0.4" + through2 "^2.0.0" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -564,6 +754,15 @@ "@jridgewell/set-array" "^1.0.0" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz#cf92a983c83466b8c0ce9124fadeaf09f7c66ea9" + integrity sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + "@jridgewell/gen-mapping@^0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" @@ -573,21 +772,36 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3": +"@jridgewell/resolve-uri@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== -"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": +"@jridgewell/resolve-uri@^3.0.3": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe" + integrity sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA== + +"@jridgewell/set-array@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.1.tgz#36a6acc93987adcf0ba50c66908bd0b70de8afea" + integrity sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ== + +"@jridgewell/set-array@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== -"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": +"@jridgewell/sourcemap-codec@1.4.14": version "1.4.14" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.13" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c" + integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== + "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" @@ -596,7 +810,7 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.13", "@jridgewell/trace-mapping@^0.3.9": +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.13": version "0.3.17" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== @@ -604,10 +818,18 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz#dcfe3e95f224c8fe97a87a5235defec999aa92ea" + integrity sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@mapbox/node-pre-gyp@^1.0.0": - version "1.0.10" - resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c" - integrity sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA== + version "1.0.9" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz#09a8781a3a036151cdebbe8719d6f8b25d4058bc" + integrity sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw== dependencies: detect-libc "^2.0.0" https-proxy-agent "^5.0.0" @@ -619,35 +841,35 @@ semver "^7.3.5" tar "^6.1.11" -"@msgpackr-extract/msgpackr-extract-darwin-arm64@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-2.2.0.tgz#901c5937e1441572ea23e631fe6deca68482fe76" - integrity sha512-Z9LFPzfoJi4mflGWV+rv7o7ZbMU5oAU9VmzCgL240KnqDW65Y2HFCT3MW06/ITJSnbVLacmcEJA8phywK7JinQ== +"@msgpackr-extract/msgpackr-extract-darwin-arm64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-2.1.2.tgz#9571b87be3a3f2c46de05585470bc4f3af2f6f00" + integrity sha512-TyVLn3S/+ikMDsh0gbKv2YydKClN8HaJDDpONlaZR+LVJmsxLFUgA+O7zu59h9+f9gX1aj/ahw9wqa6rosmrYQ== -"@msgpackr-extract/msgpackr-extract-darwin-x64@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-2.2.0.tgz#fb877fe6bae3c4d3cea29786737840e2ae689066" - integrity sha512-vq0tT8sjZsy4JdSqmadWVw6f66UXqUCabLmUVHZwUFzMgtgoIIQjT4VVRHKvlof3P/dMCkbMJ5hB1oJ9OWHaaw== +"@msgpackr-extract/msgpackr-extract-darwin-x64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-2.1.2.tgz#bfbc6936ede2955218f5621a675679a5fe8e6f4c" + integrity sha512-YPXtcVkhmVNoMGlqp81ZHW4dMxK09msWgnxtsDpSiZwTzUBG2N+No2bsr7WMtBKCVJMSD6mbAl7YhKUqkp/Few== -"@msgpackr-extract/msgpackr-extract-linux-arm64@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-2.2.0.tgz#986179c38b10ac41fbdaf7d036c825cbc72855d9" - integrity sha512-hlxxLdRmPyq16QCutUtP8Tm6RDWcyaLsRssaHROatgnkOxdleMTgetf9JsdncL8vLh7FVy/RN9i3XR5dnb9cRA== +"@msgpackr-extract/msgpackr-extract-linux-arm64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-2.1.2.tgz#22555e28382af2922e7450634c8a2f240bb9eb82" + integrity sha512-vHZ2JiOWF2+DN9lzltGbhtQNzDo8fKFGrf37UJrgqxU0yvtERrzUugnfnX1wmVfFhSsF8OxrfqiNOUc5hko1Zg== -"@msgpackr-extract/msgpackr-extract-linux-arm@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-2.2.0.tgz#15f2c6fe9e0adc06c21af7e95f484ff4880d79ce" - integrity sha512-SaJ3Qq4lX9Syd2xEo9u3qPxi/OB+5JO/ngJKK97XDpa1C587H9EWYO6KD8995DAjSinWvdHKRrCOXVUC5fvGOg== +"@msgpackr-extract/msgpackr-extract-linux-arm@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-2.1.2.tgz#ffb6ae1beea7ac572b6be6bf2a8e8162ebdd8be7" + integrity sha512-42R4MAFeIeNn+L98qwxAt360bwzX2Kf0ZQkBBucJ2Ircza3asoY4CDbgiu9VWklq8gWJVSJSJBwDI+c/THiWkA== -"@msgpackr-extract/msgpackr-extract-linux-x64@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-2.2.0.tgz#30cae5c9a202f3e1fa1deb3191b18ffcb2f239a2" - integrity sha512-94y5PJrSOqUNcFKmOl7z319FelCLAE0rz/jPCWS+UtdMZvpa4jrQd+cJPQCLp2Fes1yAW/YUQj/Di6YVT3c3Iw== +"@msgpackr-extract/msgpackr-extract-linux-x64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-2.1.2.tgz#7caf62eebbfb1345de40f75e89666b3d4194755f" + integrity sha512-RjRoRxg7Q3kPAdUSC5EUUPlwfMkIVhmaRTIe+cqHbKrGZ4M6TyCA/b5qMaukQ/1CHWrqYY2FbKOAU8Hg0pQFzg== -"@msgpackr-extract/msgpackr-extract-win32-x64@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.2.0.tgz#016d855b6bc459fd908095811f6826e45dd4ba64" - integrity sha512-XrC0JzsqQSvOyM3t04FMLO6z5gCuhPE6k4FXuLK5xf52ZbdvcFe1yBmo7meCew9B8G2f0T9iu9t3kfTYRYROgA== +"@msgpackr-extract/msgpackr-extract-win32-x64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.1.2.tgz#f2d8b9ddd8d191205ed26ce54aba3dfc5ae3e7c9" + integrity sha512-rIZVR48zA8hGkHIK7ED6+ZiXsjRCcAVBJbm8o89OKAMTmEAQ2QvoOxoiu3w2isAaWwzgtQIOFIqHwvZDyLKCvw== "@shopify/jest-koa-mocks@5.0.1": version "5.0.1" @@ -665,9 +887,9 @@ "@hapi/hoek" "^9.0.0" "@sideway/formula@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" - integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c" + integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg== "@sideway/pinpoint@^2.0.0": version "2.0.0" @@ -685,9 +907,9 @@ integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== "@sinonjs/commons@^1.7.0": - version "1.8.6" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9" - integrity sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ== + version "1.8.3" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" + integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== dependencies: type-detect "4.0.8" @@ -790,6 +1012,15 @@ request "^2.88.0" webfinger "^0.4.2" +"@trendyol/jest-testcontainers@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@trendyol/jest-testcontainers/-/jest-testcontainers-2.1.1.tgz#dced95cf9c37b75efe0a65db9b75ae8912f2f14a" + integrity sha512-4iAc2pMsev4BTUzoA7jO1VvbTOU2N3juQUYa8TwiSPXPuQtxKwV9WB9ZEP+JQ+Pj15YqfGOXp5H0WNMPtapjiA== + dependencies: + cwd "^0.10.0" + node-duration "^1.0.4" + testcontainers "4.7.0" + "@tsconfig/node10@^1.0.7": version "1.0.9" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" @@ -818,9 +1049,9 @@ "@types/node" "*" "@types/babel__core@^7.1.14": - version "7.1.20" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.20.tgz#e168cdd612c92a2d335029ed62ac94c95b362359" - integrity sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ== + version "7.1.19" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.19.tgz#7b497495b7d1b4812bdb9d02804d0576f43ee460" + integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" @@ -844,16 +1075,16 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.18.3" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.3.tgz#dfc508a85781e5698d5b33443416b6268c4b3e8d" - integrity sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w== + version "7.17.1" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.17.1.tgz#1a0e73e8c28c7e832656db372b779bfd2ef37314" + integrity sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA== dependencies: "@babel/types" "^7.3.0" "@types/bluebird@*": - version "3.5.38" - resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.38.tgz#7a671e66750ccd21c9fc9d264d0e1e5330bc9908" - integrity sha512-yR/Kxc0dd4FfwtEoLZMoqJbM/VE/W7hXn/MIjb+axcwag0iFmSPK7OBUZq1YWLynJUoWQkfUrI7T0HDqGApNSg== + version "3.5.36" + resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.36.tgz#00d9301d4dc35c2f6465a8aec634bb533674c652" + integrity sha512-HBNx4lhkxN7bx6P0++W8E289foSu8kO8GCk2unhuVggO+cE7rh9DhZUyPhUxNRG9m+5B5BTKxZQ5ZP92x/mx9Q== "@types/body-parser@*": version "1.19.2" @@ -897,29 +1128,36 @@ dependencies: "@types/ms" "*" -"@types/express-serve-static-core@^4.17.31": - version "4.17.32" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.32.tgz#93dda387f5516af616d8d3f05f2c4c79d81e1b82" - integrity sha512-aI5h/VOkxOF2Z1saPy0Zsxs5avets/iaiAJYznQFm5By/pamU31xWKL//epiF4OfUA2qTOc9PV6tCUjhO8wlZA== +"@types/dockerode@^2.5.34": + version "2.5.34" + resolved "https://registry.yarnpkg.com/@types/dockerode/-/dockerode-2.5.34.tgz#9adb884f7cc6c012a6eb4b2ad794cc5d01439959" + integrity sha512-LcbLGcvcBwBAvjH9UrUI+4qotY+A5WCer5r43DR5XHv2ZIEByNXFdPLo1XxR+v/BjkGjlggW8qUiXuVEhqfkpA== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^4.17.18": + version "4.17.28" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8" + integrity sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" "@types/express@*": - version "4.17.15" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.15.tgz#9290e983ec8b054b65a5abccb610411953d417ff" - integrity sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ== + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" + integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== dependencies: "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.31" + "@types/express-serve-static-core" "^4.17.18" "@types/qs" "*" "@types/serve-static" "*" "@types/graceful-fs@^4.1.3": - version "4.1.6" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" - integrity sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw== + version "4.1.5" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" + integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== dependencies: "@types/node" "*" @@ -929,9 +1167,9 @@ integrity sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA== "@types/http-errors@*": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.1.tgz#20172f9578b225f6c7da63446f56d4ce108d5a65" - integrity sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ== + version "1.8.2" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.2.tgz#7315b4c4c54f82d13fa61c228ec5c2ea5cc9e0e1" + integrity sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w== "@types/ioredis@4.28.0": version "4.28.0" @@ -959,10 +1197,10 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.1.tgz#2c8b6dc6ff85c33bcd07d0b62cb3d19ddfdb3ab9" - integrity sha512-fUy7YRpT+rHXto1YlL+J9rs0uLGyiqVt3ZOTQR+4ROc47yNl8WLdVLgUloBRhOxP1PZvguHl44T3H0wAWxahYQ== +"@types/jest@28.1.1": + version "28.1.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-28.1.1.tgz#8c9ba63702a11f8c386ee211280e8b68cb093cd1" + integrity sha512-C2p7yqleUKtCkVjlOur9BWVA4HgUQmEj/HWCt5WzZ5mLXrWnyIfl0wGuArc+kBXsy0ZZfLp+7dywB4HtSVYGVA== dependencies: jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" @@ -988,21 +1226,7 @@ "@types/pino" "*" "@types/pino-http" "*" -"@types/koa@*": - version "2.13.5" - resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.5.tgz#64b3ca4d54e08c0062e89ec666c9f45443b21a61" - integrity sha512-HSUOdzKz3by4fnqagwthW/1w/yJspTgppyyalPVbgZf8jQWvdIXcVW5h2DGtw4zYntOaeRGx49r1hxoPWrD4aA== - dependencies: - "@types/accepts" "*" - "@types/content-disposition" "*" - "@types/cookies" "*" - "@types/http-assert" "*" - "@types/http-errors" "*" - "@types/keygrip" "*" - "@types/koa-compose" "*" - "@types/node" "*" - -"@types/koa@2.13.4": +"@types/koa@*", "@types/koa@2.13.4": version "2.13.4" resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.4.tgz#10620b3f24a8027ef5cbae88b393d1b31205726b" integrity sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw== @@ -1021,10 +1245,10 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670" integrity sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g== -"@types/mime@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" - integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== "@types/ms@*": version "0.7.31" @@ -1040,9 +1264,9 @@ form-data "^3.0.0" "@types/node@*": - version "18.11.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" - integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== + version "17.0.41" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.41.tgz#1607b2fd3da014ae5d4d1b31bc792a39348dfb9b" + integrity sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw== "@types/node@14.18.20": version "14.18.20" @@ -1172,9 +1396,9 @@ "@types/pouchdb-find" "*" "@types/pouchdb-find@*": - version "7.3.0" - resolved "https://registry.yarnpkg.com/@types/pouchdb-find/-/pouchdb-find-7.3.0.tgz#b917030e9f4bf6e56bf8c3b9fe4b2a25e989009a" - integrity sha512-sFPli5tBjGX9UfXioik1jUzPdcN84eV82n0lmEFuoPepWqkLjQcyri0eOa++HYOaNPyMDhKFBqEALEZivK2dRg== + version "6.3.7" + resolved "https://registry.yarnpkg.com/@types/pouchdb-find/-/pouchdb-find-6.3.7.tgz#f713534a53c1a7f3fd8fbbfb74131a1b04711ddc" + integrity sha512-b2dr9xoZRK5Mwl8UiRA9l5j9mmCxNfqXuu63H1KZHwJLILjoIIz7BntCvM0hnlnl7Q8P8wORq0IskuaMq5Nnnw== dependencies: "@types/pouchdb-core" "*" @@ -1234,9 +1458,9 @@ "@types/pouchdb-replication" "*" "@types/prettier@^2.1.5": - version "2.7.2" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0" - integrity sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg== + version "2.6.3" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.6.3.tgz#68ada76827b0010d0db071f739314fa429943d0a" + integrity sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg== "@types/qs@*": version "6.9.7" @@ -1269,11 +1493,11 @@ integrity sha512-4g1jrL98mdOIwSOUh6LTlB0Cs9I0dQPwINUhBg7C6pN4HLr8GS8xsksJxilW6S6dQHVi2K/o+lQuQcg7LroCnw== "@types/serve-static@*": - version "1.15.0" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155" - integrity sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg== + version "1.13.10" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" + integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ== dependencies: - "@types/mime" "*" + "@types/mime" "^1" "@types/node" "*" "@types/stack-utils@^2.0.0": @@ -1319,9 +1543,9 @@ "@types/yargs-parser" "*" "@types/yargs@^17.0.8": - version "17.0.19" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.19.tgz#8dbecdc9ab48bee0cb74f6e3327de3fa0d0c98ae" - integrity sha512-cAx3qamwaYX9R0fzOIZAlFpo4A+1uBVCxqpKz9D26uTF4srRXaGTTsikQmaotCtNdbhzyUH7ft6p9ktz9s6UNQ== + version "17.0.13" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.13.tgz#34cced675ca1b1d51fcf4d34c3c6f0fa142a5c76" + integrity sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg== dependencies: "@types/yargs-parser" "*" @@ -1439,10 +1663,15 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +any-promise@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + anymatch@^3.0.3, anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -1477,7 +1706,7 @@ argsarray@0.0.1: resolved "https://registry.yarnpkg.com/argsarray/-/argsarray-0.0.1.tgz#6e7207b4ecdb39b0af88303fa5ae22bda8df61cb" integrity sha512-u96dg2GcAKtpTrBdDoFIM7PjcBA+6rSP0OR94MOReNRyUECL6MtQt5XXmRr4qrftYaef9+l5hcpO5te7sML1Cg== -asn1@~0.2.3: +asn1@^0.2.4, asn1@~0.2.3: version "0.2.6" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== @@ -1527,14 +1756,14 @@ aws-sign2@~0.7.0: integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== aws4@^1.8.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" - integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== axios-retry@^3.1.9: - version "3.3.1" - resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-3.3.1.tgz#47624646138aedefbad2ac32f226f4ee94b6dcab" - integrity sha512-RohAUQTDxBSWLFEnoIG/6bvmy8l3TfpkclgStjl5MDCMBDgapAWCmr1r/9harQfWC8bzLC8job6UcL1A1Yc+/Q== + version "3.2.5" + resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-3.2.5.tgz#64952992837c7d9a12eec156a2694a7945f60895" + integrity sha512-a8umkKbfIkTiYJQLx3v3TzKM85TGKB8ZQYz4zwykt2fpO64TsRlUhjaPaAb3fqMWCXFm2YhWcd8V5FHDKO9bSA== dependencies: "@babel/runtime" "^7.15.4" is-retry-allowed "^2.2.0" @@ -1547,9 +1776,9 @@ axios@0.24.0: follow-redirects "^1.14.4" axios@^1.1.3: - version "1.2.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.2.tgz#72681724c6e6a43a9fea860fc558127dbe32f9f1" - integrity sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q== + version "1.1.3" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.1.3.tgz#8274250dada2edf53814ed7db644b9c2866c1e35" + integrity sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA== dependencies: follow-redirects "^1.15.0" form-data "^4.0.0" @@ -1630,7 +1859,7 @@ base64url@3.x.x, base64url@^3.0.1: resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== -bcrypt-pbkdf@^1.0.0: +bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== @@ -1705,6 +1934,17 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +browserslist@^4.20.2: + version "4.20.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.4.tgz#98096c9042af689ee1e0271333dbc564b8ce4477" + integrity sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw== + dependencies: + caniuse-lite "^1.0.30001349" + electron-to-chromium "^1.4.147" + escalade "^3.1.1" + node-releases "^2.0.5" + picocolors "^1.0.0" + browserslist@^4.21.3: version "4.21.4" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" @@ -1769,6 +2009,11 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +buildcheck@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.3.tgz#70451897a95d80f7807e68fc412eb2e7e35ff4d5" + integrity sha512-pziaA+p/wdVImfcbsZLNF32EiWyujlQLwolMqUQE8xpKNOH7KmZQaY8sXN7DGOEzPAElo9QTaeNRfGnf3iOJbA== + bull@4.10.1: version "4.10.1" resolved "https://registry.yarnpkg.com/bull/-/bull-4.10.1.tgz#f14974b6089358b62b495a2cbf838aadc098e43f" @@ -1784,6 +2029,11 @@ bull@4.10.1: semver "^7.3.2" uuid "^8.3.0" +byline@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" + integrity sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q== + cache-content-type@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c" @@ -1828,10 +2078,15 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== +caniuse-lite@^1.0.30001349: + version "1.0.30001352" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001352.tgz#cc6f5da3f983979ad1e2cdbae0505dccaa7c6a12" + integrity sha512-GUgH8w6YergqPQDGWhJGt8GDRnY0L/iJVQcU3eJ46GYf52R8tk0Wxp0PymuFVZboJYXGiCqwozAYZNRjVj6IcA== + caniuse-lite@^1.0.30001400: - version "1.0.30001442" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz#40337f1cf3be7c637b061e2f78582dc1daec0614" - integrity sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow== + version "1.0.30001431" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz#e7c59bd1bc518fae03a4656be442ce6c4887a795" + integrity sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ== caseless@~0.12.0: version "0.12.0" @@ -1901,9 +2156,9 @@ ci-info@^2.0.0: integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== ci-info@^3.2.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.1.tgz#708a6cdae38915d597afdf3b145f2f8e1ff55f3f" - integrity sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w== + version "3.3.1" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.1.tgz#58331f6f472a25fe3a50a351ae3052936c2c7f32" + integrity sha512-SXgeMX9VwDe7iFFaEWkA5AstuER9YKqy4EhHqr4DVqkwmD9rpVimkMKWHdjn30Ja45txyjhSn63lVX69eVCckg== cjs-module-lexer@^1.0.0: version "1.2.2" @@ -1930,16 +2185,16 @@ clone-buffer@1.0.0: integrity sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g== clone-response@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" - integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" + integrity sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q== dependencies: mimic-response "^1.0.0" cluster-key-slot@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" - integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + version "1.1.0" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" + integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== co@^4.6.0: version "4.6.0" @@ -2032,9 +2287,11 @@ content-type@^1.0.4: integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" - integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" cookies@~0.8.0: version "0.8.0" @@ -2061,17 +2318,25 @@ correlation-id@4.0.0: dependencies: uuid "^8.3.1" +cpu-features@~0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.4.tgz#0023475bb4f4c525869c162e4108099e35bf19d8" + integrity sha512-fKiZ/zp1mUwQbnzb9IghXtHtDoTMtNeb8oYGx6kX2SYfhnG0HNdBEBIzB9b5KlXu5DQPhfy3mInbBxFcgwAr3A== + dependencies: + buildcheck "0.0.3" + nan "^2.15.0" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== cron-parser@^4.2.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.7.1.tgz#1e325a6a18e797a634ada1e2599ece0b6b5ed177" - integrity sha512-WguFaoQ0hQ61SgsCZLHUcNbAvlK0lypKXu62ARguefYmjzaOXIVRNrAmyXzabTwUn4sQvQLkk6bjH+ipGfw8bA== + version "4.6.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.6.0.tgz#404c3fdbff10ae80eef6b709555d577ef2fd2e0d" + integrity sha512-guZNLMGUgg6z4+eGhmHGw7ft+v6OQeuHzd1gcLxCo9Yg/qoxmG3nindp2/uwGCLizEisf2H0ptqeVXeoCpP6FA== dependencies: - luxon "^3.2.1" + luxon "^3.0.1" cross-spawn@^7.0.3: version "7.0.3" @@ -2092,6 +2357,14 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +cwd@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/cwd/-/cwd-0.10.0.tgz#172400694057c22a13b0cf16162c7e4b7a7fe567" + integrity sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA== + dependencies: + find-pkg "^0.1.2" + fs-exists-sync "^0.1.0" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -2218,6 +2491,32 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +docker-compose@^0.23.5: + version "0.23.17" + resolved "https://registry.yarnpkg.com/docker-compose/-/docker-compose-0.23.17.tgz#8816bef82562d9417dc8c790aa4871350f93a2ba" + integrity sha512-YJV18YoYIcxOdJKeFcCFihE6F4M2NExWM/d4S1ITcS9samHKnNUihz9kjggr0dNtsrbpFNc7/Yzd19DWs+m1xg== + dependencies: + yaml "^1.10.2" + +docker-modem@^3.0.0: + version "3.0.6" + resolved "https://registry.yarnpkg.com/docker-modem/-/docker-modem-3.0.6.tgz#8c76338641679e28ec2323abb65b3276fb1ce597" + integrity sha512-h0Ow21gclbYsZ3mkHDfsYNDqtRhXS8fXr51bU0qr1dxgTMJj0XufbzX+jhNOvA8KuEEzn6JbvLVhXyv+fny9Uw== + dependencies: + debug "^4.1.1" + readable-stream "^3.5.0" + split-ca "^1.0.1" + ssh2 "^1.11.0" + +dockerode@^3.2.1: + version "3.3.4" + resolved "https://registry.yarnpkg.com/dockerode/-/dockerode-3.3.4.tgz#875de614a1be797279caa9fe27e5637cf0e40548" + integrity sha512-3EUwuXnCU+RUlQEheDjmBE0B7q66PV9Rw5NiH1sXwINq0M9c5ERP9fxgkw36ZHOtzf4AGEEYySnkx/sACC9EgQ== + dependencies: + "@balena/dockerignore" "^1.0.2" + docker-modem "^3.0.0" + tar-fs "~2.0.1" + dot-prop@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -2236,9 +2535,9 @@ double-ended-queue@2.1.0-0: integrity sha512-+BNfZ+deCo8hMNpDqDnvT+c0XpJ5cUa6mqYq89bho2Ifze4URTqRkcwR399hWoTrTkbZ/XJYDgP6rc7pRgffEQ== duplexer3@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" - integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA== + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + integrity sha512-CEj8FwwNA4cVH2uFCoHUrmojhYh1vmCdOaneKJXwkeY1i9jnlslVo9dx+hQ5Hl9GnH/Bwy/IjxAyOePyPKYnzA== ecc-jsbn@~0.1.1: version "0.1.2" @@ -2260,6 +2559,11 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== +electron-to-chromium@^1.4.147: + version "1.4.150" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.150.tgz#89f0e12505462d5df7e56c5b91aff7e1dfdd33ec" + integrity sha512-MP3oBer0X7ZeS9GJ0H6lmkn561UxiwOIY9TTkdxVY7lI9G6GVCKfgJaHaDcakwdKxBXA4T3ybeswH/WBIN/KTA== + electron-to-chromium@^1.4.251: version "1.4.284" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" @@ -2390,6 +2694,13 @@ exit@^0.1.2: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== +expand-tilde@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449" + integrity sha512-rtmc+cjLZqnu9dSYosX9EWmSJhTwpACgJQTfj4hgg2JjOD/6SIQalZrt4a3aQeh++oNxkazcaxrhPUj6+g5G/Q== + dependencies: + os-homedir "^1.0.1" + expect@^28.1.3: version "28.1.3" resolved "https://registry.yarnpkg.com/expect/-/expect-28.1.3.tgz#90a7c1a124f1824133dd4533cce2d2bdcb6603ec" @@ -2442,9 +2753,9 @@ fast-safe-stringify@^2.1.1: integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== fb-watchman@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" - integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + version "2.0.1" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" + integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== dependencies: bser "2.1.1" @@ -2483,6 +2794,21 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +find-file-up@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/find-file-up/-/find-file-up-0.1.3.tgz#cf68091bcf9f300a40da411b37da5cce5a2fbea0" + integrity sha512-mBxmNbVyjg1LQIIpgO8hN+ybWBgDQK8qjht+EbrTCGmmPV/sc7RF1i9stPTD6bpvXZywBdrwRYxhSdJv867L6A== + dependencies: + fs-exists-sync "^0.1.0" + resolve-dir "^0.1.0" + +find-pkg@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/find-pkg/-/find-pkg-0.1.2.tgz#1bdc22c06e36365532e2a248046854b9788da557" + integrity sha512-0rnQWcFwZr7eO0513HahrWafsc3CTFioEB7DRiEYCUM/70QXSY8f3mCST17HXLcPvEhzH/Ty/Bxd72ZZsr/yvw== + dependencies: + find-file-up "^0.1.2" + find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -2491,7 +2817,12 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -follow-redirects@^1.14.4, follow-redirects@^1.15.0: +follow-redirects@^1.14.4: + version "1.15.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" + integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== + +follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -2538,6 +2869,11 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-exists-sync@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" + integrity sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg== + fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" @@ -2642,7 +2978,7 @@ glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob@^7.1.3, glob@^7.1.4: +glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -2655,9 +2991,9 @@ glob@^7.1.3, glob@^7.1.4: path-is-absolute "^1.0.0" glob@^8.0.0: - version "8.0.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" - integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -2666,12 +3002,30 @@ glob@^8.0.0: once "^1.3.0" global-dirs@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" - integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686" + integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA== dependencies: ini "2.0.0" +global-modules@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d" + integrity sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA== + dependencies: + global-prefix "^0.1.4" + is-windows "^0.2.0" + +global-prefix@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-0.1.5.tgz#8d3bc6b8da3ca8112a160d8d496ff0462bfef78f" + integrity sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw== + dependencies: + homedir-polyfill "^1.0.0" + ini "^1.3.4" + is-windows "^0.2.0" + which "^1.2.12" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -2759,6 +3113,13 @@ help-me@^4.0.1: glob "^8.0.0" readable-stream "^3.6.0" +homedir-polyfill@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" + integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== + dependencies: + parse-passwd "^1.0.0" + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -2878,7 +3239,7 @@ ini@2.0.0: resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== -ini@~1.3.0: +ini@^1.3.4, ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== @@ -2951,10 +3312,10 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-core-module@^2.9.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" - integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== +is-core-module@^2.8.1: + version "2.9.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" + integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== dependencies: has "^1.0.3" @@ -3030,6 +3391,11 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== +is-windows@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" + integrity sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q== + is-yarn-global@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" @@ -3061,9 +3427,9 @@ istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" - integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + version "5.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz#31d18bdd127f825dd02ea7bfdfd906f8ab840e9f" + integrity sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A== dependencies: "@babel/core" "^7.12.3" "@babel/parser" "^7.14.7" @@ -3090,9 +3456,9 @@ istanbul-lib-source-maps@^4.0.0: source-map "^0.6.1" istanbul-reports@^3.1.3: - version "3.1.5" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.5.tgz#cc9a6ab25cb25659810e4785ed9d9fb742578bae" - integrity sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w== + version "3.1.4" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.4.tgz#1b6f068ecbc6c331040aab5741991273e609e40c" + integrity sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" @@ -3307,9 +3673,9 @@ jest-mock@^28.1.3: "@types/node" "*" jest-pnp-resolver@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" - integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + version "1.2.2" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c" + integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== jest-regex-util@^28.0.2: version "28.0.2" @@ -3394,6 +3760,11 @@ jest-runtime@^28.1.3: slash "^3.0.0" strip-bom "^4.0.0" +jest-serial-runner@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/jest-serial-runner/-/jest-serial-runner-1.2.1.tgz#0f5f8dbe6f077119bd1fdd7e8518f92353c194d5" + integrity sha512-d59fF+7HdjNvQEL7B4WyFE+f8q5tGzlNUqtOnxTrT1ofun7O6/Lgm/j255BBgCY2fmSue/34M7Xy9+VWRByP0Q== + jest-snapshot@^28.1.3: version "28.1.3" resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-28.1.3.tgz#17467b3ab8ddb81e2f605db05583d69388fc0668" @@ -3554,10 +3925,10 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@^2.2.1, json5@^2.2.2: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== jsonc-parser@^3.2.0: version "3.2.0" @@ -3656,7 +4027,7 @@ koa-passport@4.1.4: dependencies: passport "^0.4.0" -koa@2.13.4: +koa@2.13.4, koa@^2.13.4: version "2.13.4" resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.4.tgz#ee5b0cb39e0b8069c38d115139c774833d32462e" integrity sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g== @@ -3685,35 +4056,6 @@ koa@2.13.4: type-is "^1.6.16" vary "^1.1.2" -koa@^2.13.4: - version "2.14.1" - resolved "https://registry.yarnpkg.com/koa/-/koa-2.14.1.tgz#defb9589297d8eb1859936e777f3feecfc26925c" - integrity sha512-USJFyZgi2l0wDgqkfD27gL4YGno7TfUkcmOe6UOLFOVuN+J7FwnNu4Dydl4CUQzraM1lBAiGed0M9OVJoT0Kqw== - dependencies: - accepts "^1.3.5" - cache-content-type "^1.0.0" - content-disposition "~0.5.2" - content-type "^1.0.4" - cookies "~0.8.0" - debug "^4.3.2" - delegates "^1.0.0" - depd "^2.0.0" - destroy "^1.0.4" - encodeurl "^1.0.2" - escape-html "^1.0.3" - fresh "~0.5.2" - http-assert "^1.3.0" - http-errors "^1.6.3" - is-generator-function "^1.0.7" - koa-compose "^4.1.0" - koa-convert "^2.0.0" - on-finished "^2.3.0" - only "~0.0.2" - parseurl "^1.3.2" - statuses "^1.5.0" - type-is "^1.6.16" - vary "^1.1.2" - latest-version@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" @@ -3914,13 +4256,6 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -3933,10 +4268,10 @@ ltgt@2.2.1, ltgt@^2.1.2, ltgt@~2.2.0: resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5" integrity sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA== -luxon@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.2.1.tgz#14f1af209188ad61212578ea7e3d518d18cee45f" - integrity sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg== +luxon@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.0.4.tgz#d179e4e9f05e092241e7044f64aaa54796b03929" + integrity sha512-aV48rGUwP/Vydn8HT+5cdr26YYQiUZ42NM6ToMoaGKwYfWbfLeRkEu1wXWMHBZT6+KyLfcbbtVcoQFCbbPjKlw== make-dir@^3.0.0, make-dir@^3.1.0: version "3.1.0" @@ -4041,28 +4376,26 @@ minimatch@^3.0.4, minimatch@^3.1.1: brace-expansion "^1.1.7" minimatch@^5.0.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.2.tgz#0939d7d6f0898acbd1508abe534d1929368a8fff" - integrity sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg== + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.6: +minimist@^1.2.0: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + +minimist@^1.2.6: version "1.2.7" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== minipass@^3.0.0: - version "3.3.6" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" - integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== - dependencies: - yallist "^4.0.0" - -minipass@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.0.0.tgz#7cebb0f9fa7d56f0c5b17853cbe28838a8dbbd3b" - integrity sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw== + version "3.1.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" + integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ== dependencies: yallist "^4.0.0" @@ -4094,26 +4427,31 @@ ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -msgpackr-extract@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-2.2.0.tgz#4bb749b58d9764cfdc0d91c7977a007b08e8f262" - integrity sha512-0YcvWSv7ZOGl9Od6Y5iJ3XnPww8O7WLcpYMDwX+PAA/uXLDtyw94PJv9GLQV/nnp3cWlDhMoyKZIQLrx33sWog== +msgpackr-extract@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-2.1.2.tgz#56272030f3e163e1b51964ef8b1cd5e7240c03ed" + integrity sha512-cmrmERQFb19NX2JABOGtrKdHMyI6RUyceaPBQ2iRz9GnDkjBWFjNJC0jyyoOfZl2U/LZE3tQCCQc4dlRyA8mcA== dependencies: node-gyp-build-optional-packages "5.0.3" optionalDependencies: - "@msgpackr-extract/msgpackr-extract-darwin-arm64" "2.2.0" - "@msgpackr-extract/msgpackr-extract-darwin-x64" "2.2.0" - "@msgpackr-extract/msgpackr-extract-linux-arm" "2.2.0" - "@msgpackr-extract/msgpackr-extract-linux-arm64" "2.2.0" - "@msgpackr-extract/msgpackr-extract-linux-x64" "2.2.0" - "@msgpackr-extract/msgpackr-extract-win32-x64" "2.2.0" + "@msgpackr-extract/msgpackr-extract-darwin-arm64" "2.1.2" + "@msgpackr-extract/msgpackr-extract-darwin-x64" "2.1.2" + "@msgpackr-extract/msgpackr-extract-linux-arm" "2.1.2" + "@msgpackr-extract/msgpackr-extract-linux-arm64" "2.1.2" + "@msgpackr-extract/msgpackr-extract-linux-x64" "2.1.2" + "@msgpackr-extract/msgpackr-extract-win32-x64" "2.1.2" msgpackr@^1.5.2: - version "1.8.1" - resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.8.1.tgz#2298aed8a14f83e99df77d344cbda3e436f29b5b" - integrity sha512-05fT4J8ZqjYlR4QcRDIhLCYKUOHXk7C/xa62GzMKj74l3up9k2QZ3LgFc6qWdsPHl91QA2WLWqWc8b8t7GLNNw== + version "1.7.2" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.7.2.tgz#68d6debf5999d6b61abb6e7046a689991ebf7261" + integrity sha512-mWScyHTtG6TjivXX9vfIy2nBtRupaiAj0HQ2mtmpmYujAmqZmaaEVPaSZ1NKLMvicaMLFzEaMk0ManxMRg8rMQ== optionalDependencies: - msgpackr-extract "^2.2.0" + msgpackr-extract "^2.1.2" + +nan@^2.15.0, nan@^2.16.0: + version "2.17.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" + integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== napi-macros@~2.0.0: version "2.0.0" @@ -4150,6 +4488,11 @@ node-addon-api@^3.1.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== +node-duration@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/node-duration/-/node-duration-1.0.4.tgz#3e94ecc0e473691c89c4560074503362071cecac" + integrity sha512-eUXYNSY7DL53vqfTosggWkvyIW3bhAcqBDIlolgNYlZhianXTrCL50rlUJWD1eRqkIxMppXTfiFbp+9SjpPrgA== + node-fetch@2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" @@ -4178,9 +4521,9 @@ node-int64@^0.4.0: integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== node-mocks-http@^1.11.0: - version "1.12.1" - resolved "https://registry.yarnpkg.com/node-mocks-http/-/node-mocks-http-1.12.1.tgz#838e176019daf177caff6bb8534e3a32646e7531" - integrity sha512-jrA7Sn3qI6GsHgWtUW3gMj0vO6Yz0nJjzg3jRZYjcfj4tzi8oWPauDK1qHVJoAxTbwuDHF1JiM9GISZ/ocI/ig== + version "1.11.0" + resolved "https://registry.yarnpkg.com/node-mocks-http/-/node-mocks-http-1.11.0.tgz#defc0febf6b935f08245397d47534a8de592996e" + integrity sha512-jS/WzSOcKbOeGrcgKbenZeNhxUNnP36Yw11+hL4TTxQXErGfqYZ+MaYNNvhaTiGIJlzNSqgQkk9j8dSu1YWSuw== dependencies: accepts "^1.3.7" content-disposition "^0.5.3" @@ -4193,10 +4536,15 @@ node-mocks-http@^1.11.0: range-parser "^1.2.0" type-is "^1.6.18" +node-releases@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666" + integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q== + node-releases@^2.0.6: - version "2.0.8" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae" - integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A== + version "2.0.6" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" + integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== nodemon@2.0.16: version "2.0.16" @@ -4306,6 +4654,11 @@ only@~0.0.2: resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ== +os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ== + os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -4379,6 +4732,11 @@ parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q== + parseurl@^1.3.2, parseurl@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -4721,19 +5079,6 @@ pouchdb-promise@^6.0.4: dependencies: lie "3.1.1" -pouchdb-replication-stream@1.2.9: - version "1.2.9" - resolved "https://registry.yarnpkg.com/pouchdb-replication-stream/-/pouchdb-replication-stream-1.2.9.tgz#aa4fa5d8f52df4825392f18e07c7e11acffc650a" - integrity sha512-hM8XRBfamTTUwRhKwLS/jSNouBhn9R/4ugdHNRD1EvJzwV8iImh6sDYbCU9PGuznjyOjXz6vpFRzKeI2KYfwnQ== - dependencies: - argsarray "0.0.1" - inherits "^2.0.3" - lodash.pick "^4.0.0" - ndjson "^1.4.3" - pouch-stream "^0.4.0" - pouchdb-promise "^6.0.4" - through2 "^2.0.0" - pouchdb-selector-core@7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/pouchdb-selector-core/-/pouchdb-selector-core-7.2.2.tgz#264d7436a8c8ac3801f39960e79875ef7f3879a0" @@ -4840,9 +5185,9 @@ prr@~1.0.1: integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== psl@^1.1.28, psl@^1.1.33: - version "1.9.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" - integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + version "1.8.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== pstree.remy@^1.1.8: version "1.1.8" @@ -4906,7 +5251,7 @@ range-parser@^1.2.0: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -rc@1.2.8, rc@^1.2.8: +rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -4936,7 +5281,7 @@ readable-stream@1.1.14, readable-stream@^1.0.27-1: isarray "0.0.1" string_decoder "~0.10.x" -"readable-stream@2 || 3", readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: +"readable-stream@2 || 3", readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -5014,17 +5359,17 @@ redlock@4.2.0: dependencies: bluebird "^3.7.2" -regenerator-runtime@^0.13.11: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerator-runtime@^0.13.4: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== registry-auth-token@^4.0.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.2.tgz#f02d49c3668884612ca031419491a13539e21fac" - integrity sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg== + version "4.2.1" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.1.tgz#6d7b4006441918972ccd5fedcd41dc322c79b250" + integrity sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw== dependencies: - rc "1.2.8" + rc "^1.2.8" registry-url@^5.0.0: version "5.1.0" @@ -5081,6 +5426,14 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" +resolve-dir@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e" + integrity sha512-QxMPqI6le2u0dCLyiGzgy92kjkkL6zO0XyvHzjdTNH3zM6e5Hz3BwG6+aEyNgiQ5Xz6PwTwgQEj3U50dByPKIA== + dependencies: + expand-tilde "^1.2.2" + global-modules "^0.2.3" + resolve-from@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" @@ -5092,11 +5445,11 @@ resolve.exports@^1.1.0: integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== resolve@^1.20.0: - version "1.22.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + version "1.22.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== dependencies: - is-core-module "^2.9.0" + is-core-module "^2.8.1" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -5161,20 +5514,13 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -semver@7.3.7: +semver@7.3.7, semver@7.x, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: version "7.3.7" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: lru-cache "^6.0.0" -semver@7.x, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.8: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== - dependencies: - lru-cache "^6.0.0" - semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -5185,6 +5531,13 @@ semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.3.8: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -5273,6 +5626,11 @@ spark-md5@3.0.2: resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc" integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw== +split-ca@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6" + integrity sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ== + split2@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493" @@ -5295,6 +5653,17 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +ssh2@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.11.0.tgz#ce60186216971e12f6deb553dcf82322498fe2e4" + integrity sha512-nfg0wZWGSsfUe/IBJkXVll3PEZ//YH2guww+mP88gTpuSU4FtZN7zu9JoeTGOyCNx2dTDtT9fOpWwlzyj4uOOw== + dependencies: + asn1 "^0.2.4" + bcrypt-pbkdf "^1.0.2" + optionalDependencies: + cpu-features "~0.0.4" + nan "^2.16.0" + sshpk@^1.7.0: version "1.17.0" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" @@ -5311,9 +5680,9 @@ sshpk@^1.7.0: tweetnacl "~0.14.0" stack-utils@^2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" - integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + version "2.0.5" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" + integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== dependencies: escape-string-regexp "^2.0.0" @@ -5332,6 +5701,13 @@ step@0.0.x: resolved "https://registry.yarnpkg.com/step/-/step-0.0.6.tgz#143e7849a5d7d3f4a088fe29af94915216eeede2" integrity sha512-qSSeQinUJk2w38vUFobjFoE307GqsozMC8VisOCkJLpklvKPT0ptPHwWOrENoag8rgLudvTkfP3bancwP93/Jw== +stream-to-array@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/stream-to-array/-/stream-to-array-2.3.0.tgz#bbf6b39f5f43ec30bc71babcb37557acecf34353" + integrity sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA== + dependencies: + any-promise "^1.1.0" + string-length@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" @@ -5432,9 +5808,9 @@ supports-color@^8.0.0: has-flag "^4.0.0" supports-hyperlinks@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" - integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== + version "2.2.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" + integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ== dependencies: has-flag "^4.0.0" supports-color "^7.0.0" @@ -5444,7 +5820,7 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -tar-fs@2.1.1: +tar-fs@2.1.1, tar-fs@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== @@ -5454,7 +5830,17 @@ tar-fs@2.1.1: pump "^3.0.0" tar-stream "^2.1.4" -tar-stream@^2.1.4: +tar-fs@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.1.tgz#e44086c1c60d31a4f0cf893b1c4e155dabfae9e2" + integrity sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.0.0" + +tar-stream@^2.0.0, tar-stream@^2.1.4: version "2.2.0" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== @@ -5466,13 +5852,13 @@ tar-stream@^2.1.4: readable-stream "^3.1.1" tar@^6.1.11: - version "6.1.13" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" - integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw== + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" - minipass "^4.0.0" + minipass "^3.0.0" minizlib "^2.1.1" mkdirp "^1.0.3" yallist "^4.0.0" @@ -5494,10 +5880,27 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +testcontainers@4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-4.7.0.tgz#5a9a864b1b0cc86984086dcc737c2f5e73490cf3" + integrity sha512-5SrG9RMfDRRZig34fDZeMcGD5i3lHCOJzn0kjouyK4TiEWjZB3h7kCk8524lwNRHROFE1j6DGjceonv/5hl5ag== + dependencies: + "@types/dockerode" "^2.5.34" + byline "^5.0.0" + debug "^4.1.1" + docker-compose "^0.23.5" + dockerode "^3.2.1" + get-port "^5.1.1" + glob "^7.1.6" + node-duration "^1.0.4" + slash "^3.0.0" + stream-to-array "^2.3.0" + tar-fs "^2.1.0" + thread-stream@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.2.0.tgz#310c03a253f729094ce5d4638ef5186dfa80a9e8" - integrity sha512-rUkv4/fnb4rqy/gGy7VuqK6wE1+1DOCOWy4RMeaV69ZHMP11tQKZvZSip1yTgrKCMZzEMcCL/bKfHvSfDHx+iQ== + version "2.3.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.3.0.tgz#4fc07fb39eff32ae7bad803cb7dd9598349fed33" + integrity sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA== dependencies: real-require "^0.2.0" @@ -5537,7 +5940,7 @@ tmpl@1.0.5: to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= to-readable-stream@^1.0.0: version "1.0.0" @@ -5563,7 +5966,16 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -"tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0", tough-cookie@^4.1.2: +"tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" + integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.1.2" + +tough-cookie@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.2.tgz#e53e84b85f24e0b65dd526f46628db6c85f6b874" integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ== @@ -5584,7 +5996,7 @@ tough-cookie@~2.5.0: tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= ts-jest@28.0.4: version "28.0.4" @@ -5636,14 +6048,14 @@ tsscmp@1.0.6: tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= dependencies: safe-buffer "^5.0.1" tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= type-detect@4.0.8: version "4.0.8" @@ -5697,6 +6109,11 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" +universalify@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" @@ -5740,7 +6157,7 @@ uri-js@^4.2.2: url-parse-lax@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" - integrity sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ== + integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= dependencies: prepend-http "^2.0.0" @@ -5755,7 +6172,7 @@ url-parse@^1.5.3: url@0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" - integrity sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ== + integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= dependencies: punycode "1.3.2" querystring "0.2.0" @@ -5763,12 +6180,12 @@ url@0.10.3: util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= utils-merge@1.x.x: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= uuid@3.3.2: version "3.3.2" @@ -5807,12 +6224,12 @@ v8-to-istanbul@^9.0.1: vary@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= dependencies: assert-plus "^1.0.0" core-util-is "1.0.2" @@ -5821,7 +6238,7 @@ verror@1.10.0: vuvuzela@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/vuvuzela/-/vuvuzela-1.0.3.tgz#3be145e58271c73ca55279dd851f12a682114b0b" - integrity sha512-Tm7jR1xTzBbPW+6y1tknKiEhz04Wf/1iZkcTJjSFcpNko43+dFW6+OOeQe9taJIug3NdfUAjFKgUSyQrIKaDvQ== + integrity sha1-O+FF5YJxxzylUnndhR8SpoIRSws= walker@^1.0.8: version "1.0.8" @@ -5833,7 +6250,7 @@ walker@^1.0.8: webfinger@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/webfinger/-/webfinger-0.4.2.tgz#3477a6d97799461896039fcffc650b73468ee76d" - integrity sha512-PvvQ/k74HkC3q5G7bGu4VYeKDt3ePZMzT5qFPtEnOL8eyIU1/06OtDn9X5vlkQ23BlegA3eN89rDLiYUife3xQ== + integrity sha1-NHem2XeZRhiWA5/P/GULc0aO520= dependencies: step "0.0.x" xml2js "0.1.x" @@ -5841,16 +6258,23 @@ webfinger@^0.4.2: webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= dependencies: tr46 "~0.0.3" webidl-conversions "^3.0.0" +which@^1.2.12: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -5884,7 +6308,7 @@ wrap-ansi@^7.0.0: wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= write-file-atomic@^3.0.0: version "3.0.3" @@ -5907,7 +6331,7 @@ write-file-atomic@^4.0.1: write-stream@~0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/write-stream/-/write-stream-0.4.3.tgz#83cc8c0347d0af6057a93862b4e3ae01de5c81c1" - integrity sha512-IJrvkhbAnj89W/GAVdVgbnPiVw5Ntg/B4tc/MUCIEwj/g6JIww1DWJyB/yBMT3yw2/TkT6IUZ0+IYef3flEw8A== + integrity sha1-g8yMA0fQr2BXqThitOOuAd5cgcE= dependencies: readable-stream "~0.0.2" @@ -5919,7 +6343,7 @@ xdg-basedir@^4.0.0: xml2js@0.1.x: version "0.1.14" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.1.14.tgz#5274e67f5a64c5f92974cd85139e0332adc6b90c" - integrity sha512-pbdws4PPPNc1HPluSUKamY4GWMk592K7qwcj6BExbVOhhubub8+pMda/ql68b6L3luZs/OGjGSB5goV7SnmgnA== + integrity sha1-UnTmf1pkxfkpdM2FE54DMq3GuQw= dependencies: sax ">=0.1.1" @@ -5934,7 +6358,7 @@ xml2js@0.4.19: xmlbuilder@~9.0.1: version "9.0.7" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" - integrity sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ== + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= xtend@^4.0.2, xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" @@ -5946,16 +6370,16 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + yargs-parser@^20.x: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" @@ -5997,4 +6421,4 @@ yocto-queue@^0.1.0: zlib@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/zlib/-/zlib-1.0.5.tgz#6e7c972fc371c645a6afb03ab14769def114fcc0" - integrity sha512-40fpE2II+Cd3k8HWTWONfeKE2jL+P42iWJ1zzps5W51qcTsOUKM5Q5m2PFb0CLxlmFAaUuUdJGc3OfZy947v0w== + integrity sha1-bnyXL8NxxkWmr7A6sUdp3vEU/MA= diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 5668cadb01..502d32c7b0 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.3.20", + "version": "2.3.21-alpha.1", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/string-templates": "^2.3.20", + "@budibase/string-templates": "2.3.21-alpha.1", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte index 663128160f..01f5033e6c 100644 --- a/packages/bbui/src/ActionButton/ActionButton.svelte +++ b/packages/bbui/src/ActionButton/ActionButton.svelte @@ -1,6 +1,9 @@ - + + diff --git a/packages/bbui/src/Form/Core/Multiselect.svelte b/packages/bbui/src/Form/Core/Multiselect.svelte index d6c4dc23ac..1d04c210f4 100644 --- a/packages/bbui/src/Form/Core/Multiselect.svelte +++ b/packages/bbui/src/Form/Core/Multiselect.svelte @@ -14,6 +14,9 @@ export let autocomplete = false export let sort = false export let autoWidth = false + export let fetchTerm = null + export let useFetch = false + export let customPopoverHeight const dispatch = createEventDispatcher() @@ -83,10 +86,13 @@ {options} isPlaceholder={!value?.length} {autocomplete} + bind:fetchTerm + {useFetch} {isOptionSelected} {getOptionLabel} {getOptionValue} onSelectOption={toggleOption} {sort} {autoWidth} + {customPopoverHeight} /> diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index 32cfcf3310..bd575600b1 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -24,6 +24,7 @@ export let getOptionLabel = option => option export let getOptionValue = option => option export let getOptionIcon = () => null + export let useOptionIconImage = false export let getOptionColour = () => null export let open = false export let readonly = false @@ -31,6 +32,11 @@ export let autoWidth = false export let autocomplete = false export let sort = false + export let fetchTerm = null + export let useFetch = false + export let customPopoverHeight + export let align = "left" + export let footer = null const dispatch = createEventDispatcher() @@ -71,7 +77,7 @@ } const getFilteredOptions = (options, term, getLabel) => { - if (autocomplete && term) { + if (autocomplete && term && !fetchTerm) { const lowerCaseTerm = term.toLowerCase() return options.filter(option => { return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm) @@ -130,12 +136,13 @@ (open = false)} useAnchorWidth={!autoWidth} maxWidth={autoWidth ? 400 : null} + customHeight={customPopoverHeight} >
{#if autocomplete} (searchTerm = event.detail)} + value={useFetch ? fetchTerm : searchTerm} + on:change={event => + useFetch ? (fetchTerm = event.detail) : (searchTerm = event.detail)} {disabled} placeholder="Search" /> @@ -183,7 +191,16 @@ > {#if getOptionIcon(option, idx)} - + {#if useOptionIconImage} + icon + {:else} + + {/if} {/if} {#if getOptionColour(option, idx)} @@ -205,6 +222,12 @@ {/each} {/if} + + {#if footer} + + {/if}
@@ -247,7 +270,7 @@ } .popover-content.auto-width .spectrum-Menu-itemLabel { white-space: nowrap; - overflow: hidden; + overflow: none; text-overflow: ellipsis; } .popover-content:not(.auto-width) .spectrum-Menu-itemLabel { @@ -281,4 +304,11 @@ .popover-content :global(.spectrum-Search .spectrum-Textfield-icon) { top: 9px; } + + .footer { + padding: 4px 12px 12px 12px; + font-style: italic; + max-width: 170px; + font-size: 12px; + } diff --git a/packages/bbui/src/Form/Core/Select.svelte b/packages/bbui/src/Form/Core/Select.svelte index 721083e3a6..af45c1d9ff 100644 --- a/packages/bbui/src/Form/Core/Select.svelte +++ b/packages/bbui/src/Form/Core/Select.svelte @@ -11,6 +11,7 @@ export let getOptionLabel = option => option export let getOptionValue = option => option export let getOptionIcon = () => null + export let useOptionIconImage = false export let getOptionColour = () => null export let isOptionEnabled export let readonly = false @@ -18,6 +19,8 @@ export let autoWidth = false export let autocomplete = false export let sort = false + export let align + export let footer = null const dispatch = createEventDispatcher() @@ -41,7 +44,7 @@ const getFieldText = (value, options, placeholder) => { // Always use placeholder if no value if (value == null || value === "") { - return placeholder || "Choose an option" + return placeholder !== false ? "Choose an option" : "" } return getFieldAttribute(getOptionLabel, value, options) @@ -66,15 +69,18 @@ {fieldColour} {options} {autoWidth} + {align} + {footer} {getOptionLabel} {getOptionValue} {getOptionIcon} + {useOptionIconImage} {getOptionColour} {isOptionEnabled} {autocomplete} {sort} isPlaceholder={value == null || value === ""} - placeholderOption={placeholder} + placeholderOption={placeholder === false ? null : placeholder} isOptionSelected={option => option === value} onSelectOption={selectOption} /> diff --git a/packages/bbui/src/Form/Multiselect.svelte b/packages/bbui/src/Form/Multiselect.svelte index 7bcf22aa06..185eb7069b 100644 --- a/packages/bbui/src/Form/Multiselect.svelte +++ b/packages/bbui/src/Form/Multiselect.svelte @@ -15,6 +15,11 @@ export let getOptionValue = option => option export let sort = false export let autoWidth = false + export let autocomplete = false + export let fetchTerm = null + export let useFetch = false + export let customPopoverHeight + const dispatch = createEventDispatcher() const onChange = e => { value = e.detail @@ -34,6 +39,10 @@ {getOptionLabel} {getOptionValue} {autoWidth} + {autocomplete} + {customPopoverHeight} + bind:fetchTerm + {useFetch} on:change={onChange} on:click /> diff --git a/packages/bbui/src/Form/Select.svelte b/packages/bbui/src/Form/Select.svelte index 69126e648d..e87496652d 100644 --- a/packages/bbui/src/Form/Select.svelte +++ b/packages/bbui/src/Form/Select.svelte @@ -14,12 +14,17 @@ export let getOptionLabel = option => extractProperty(option, "label") export let getOptionValue = option => extractProperty(option, "value") export let getOptionIcon = option => option?.icon + export let useOptionIconImage = false export let getOptionColour = option => option?.colour export let isOptionEnabled export let quiet = false export let autoWidth = false export let sort = false export let tooltip = "" + export let autocomplete = false + export let customPopoverHeight + export let align + export let footer = null const dispatch = createEventDispatcher() const onChange = e => { @@ -46,11 +51,16 @@ {placeholder} {autoWidth} {sort} + {align} + {footer} {getOptionLabel} {getOptionValue} {getOptionIcon} {getOptionColour} + {useOptionIconImage} {isOptionEnabled} + {autocomplete} + {customPopoverHeight} on:change={onChange} on:click /> diff --git a/packages/bbui/src/Icon/Icon.svelte b/packages/bbui/src/Icon/Icon.svelte index 8290acd7cc..452a8c74a1 100644 --- a/packages/bbui/src/Icon/Icon.svelte +++ b/packages/bbui/src/Icon/Icon.svelte @@ -67,6 +67,9 @@ color: var(--spectrum-alias-icon-color-selected-hover) !important; cursor: pointer; } + svg.hoverable:active { + color: var(--spectrum-global-color-blue-400) !important; + } svg.disabled { color: var(--spectrum-global-color-gray-500) !important; diff --git a/packages/bbui/src/InlineAlert/InlineAlert.svelte b/packages/bbui/src/InlineAlert/InlineAlert.svelte index 57e7296234..bd873042b3 100644 --- a/packages/bbui/src/InlineAlert/InlineAlert.svelte +++ b/packages/bbui/src/InlineAlert/InlineAlert.svelte @@ -57,5 +57,7 @@ --spectrum-semantic-negative-icon-color: #e34850; min-width: 100px; margin: 0; + border-color: var(--spectrum-global-color-gray-400); + border-width: 1px; } diff --git a/packages/bbui/src/Label/Label.svelte b/packages/bbui/src/Label/Label.svelte index ee6d9adf76..261ca946ea 100644 --- a/packages/bbui/src/Label/Label.svelte +++ b/packages/bbui/src/Label/Label.svelte @@ -21,7 +21,7 @@ label { padding: 0; white-space: nowrap; - color: var(--spectrum-global-color-gray-600); + color: var(--spectrum-global-color-gray-700); } .muted { diff --git a/packages/bbui/src/Modal/Modal.svelte b/packages/bbui/src/Modal/Modal.svelte index 47420444a2..45081356c1 100644 --- a/packages/bbui/src/Modal/Modal.svelte +++ b/packages/bbui/src/Modal/Modal.svelte @@ -1,7 +1,7 @@ - + onMount(() => { + document.addEventListener("keydown", handleKey) + return () => { + document.removeEventListener("keydown", handleKey) + } + }) + {#if inline} {#if visible} diff --git a/packages/bbui/src/Popover/Popover.svelte b/packages/bbui/src/Popover/Popover.svelte index 8f6ef06591..081e3a34df 100644 --- a/packages/bbui/src/Popover/Popover.svelte +++ b/packages/bbui/src/Popover/Popover.svelte @@ -18,6 +18,7 @@ export let useAnchorWidth = false export let dismissible = true export let offset = 5 + export let customHeight $: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum" @@ -74,6 +75,7 @@ on:keydown={handleEscape} class="spectrum-Popover is-open" role="presentation" + style="height: {customHeight}" transition:fly|local={{ y: -20, duration: 200 }} > diff --git a/packages/bbui/src/helpers.js b/packages/bbui/src/helpers.js index b02783e0bd..f2246fbb49 100644 --- a/packages/bbui/src/helpers.js +++ b/packages/bbui/src/helpers.js @@ -104,6 +104,9 @@ export const deepSet = (obj, key, value) => { * @param obj the object to clone */ export const cloneDeep = obj => { + if (!obj) { + return obj + } return JSON.parse(JSON.stringify(obj)) } diff --git a/packages/builder/package.json b/packages/builder/package.json index 4bcc1d3f34..acc537e593 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.3.20", + "version": "2.3.21-alpha.1", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,10 +58,10 @@ } }, "dependencies": { - "@budibase/bbui": "^2.3.20", - "@budibase/client": "^2.3.20", - "@budibase/frontend-core": "^2.3.20", - "@budibase/string-templates": "^2.3.20", + "@budibase/bbui": "2.3.21-alpha.1", + "@budibase/client": "2.3.21-alpha.1", + "@budibase/frontend-core": "2.3.21-alpha.1", + "@budibase/string-templates": "2.3.21-alpha.1", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", @@ -72,6 +72,7 @@ "codemirror": "^5.59.0", "dayjs": "^1.11.2", "downloadjs": "1.4.7", + "fast-json-patch": "^3.1.1", "lodash": "4.17.21", "posthog-js": "^1.36.0", "remixicon": "2.5.0", diff --git a/packages/builder/setup.js b/packages/builder/setup.js index 744c20896a..b3fd96877b 100644 --- a/packages/builder/setup.js +++ b/packages/builder/setup.js @@ -19,6 +19,7 @@ process.env.COUCH_DB_USER = "budibase" process.env.COUCH_DB_PASSWORD = "budibase" process.env.INTERNAL_API_KEY = "budibase" process.env.ALLOW_DEV_AUTOMATIONS = 1 +process.env.MOCK_REDIS = 1 // Stop info logs polluting test outputs process.env.LOG_LEVEL = "error" diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index 69bca7eac3..d15cdb6e98 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -5,12 +5,47 @@ import { getThemeStore } from "./store/theme" import { derived } from "svelte/store" import { findComponent, findComponentPath } from "./componentUtils" import { RoleUtils } from "@budibase/frontend-core" +import { createHistoryStore } from "builderStore/store/history" +import { get } from "svelte/store" export const store = getFrontendStore() export const automationStore = getAutomationStore() export const themeStore = getThemeStore() export const temporalStore = getTemporalStore() +// Setup history for screens +export const screenHistoryStore = createHistoryStore({ + getDoc: id => get(store).screens?.find(screen => screen._id === id), + selectDoc: store.actions.screens.select, + afterAction: () => { + // Ensure a valid component is selected + if (!get(selectedComponent)) { + store.update(state => ({ + ...state, + selectedComponentId: get(selectedScreen)?.props._id, + })) + } + }, +}) +store.actions.screens.save = screenHistoryStore.wrapSaveDoc( + store.actions.screens.save +) +store.actions.screens.delete = screenHistoryStore.wrapDeleteDoc( + store.actions.screens.delete +) + +// Setup history for automations +export const automationHistoryStore = createHistoryStore({ + getDoc: automationStore.actions.getDefinition, + selectDoc: automationStore.actions.select, +}) +automationStore.actions.save = automationHistoryStore.wrapSaveDoc( + automationStore.actions.save +) +automationStore.actions.delete = automationHistoryStore.wrapDeleteDoc( + automationStore.actions.delete +) + export const selectedScreen = derived(store, $store => { return $store.screens.find(screen => screen._id === $store.selectedScreenId) }) @@ -71,3 +106,13 @@ export const selectedComponentPath = derived( ).map(component => component._id) } ) + +// Derived automation state +export const selectedAutomation = derived(automationStore, $automationStore => { + if (!$automationStore.selectedAutomationId) { + return null + } + return $automationStore.automations?.find( + x => x._id === $automationStore.selectedAutomationId + ) +}) diff --git a/packages/builder/src/builderStore/store/automation/Automation.js b/packages/builder/src/builderStore/store/automation/Automation.js deleted file mode 100644 index af0c03cb5a..0000000000 --- a/packages/builder/src/builderStore/store/automation/Automation.js +++ /dev/null @@ -1,69 +0,0 @@ -import { generate } from "shortid" - -/** - * Class responsible for the traversing of the automation definition. - * Automation definitions are stored in linked lists. - */ -export default class Automation { - constructor(automation) { - this.automation = automation - } - - hasTrigger() { - return this.automation.definition.trigger - } - - addTestData(data) { - this.automation.testData = { ...this.automation.testData, ...data } - } - - addBlock(block, idx) { - // Make sure to add trigger if doesn't exist - if (!this.hasTrigger() && block.type === "TRIGGER") { - const trigger = { id: generate(), ...block } - this.automation.definition.trigger = trigger - return trigger - } - - const newBlock = { id: generate(), ...block } - this.automation.definition.steps.splice(idx, 0, newBlock) - return newBlock - } - - updateBlock(updatedBlock, id) { - const { steps, trigger } = this.automation.definition - - if (trigger && trigger.id === id) { - this.automation.definition.trigger = updatedBlock - return - } - - const stepIdx = steps.findIndex(step => step.id === id) - if (stepIdx < 0) throw new Error("Block not found.") - steps.splice(stepIdx, 1, updatedBlock) - this.automation.definition.steps = steps - } - - deleteBlock(id) { - const { steps, trigger } = this.automation.definition - - if (trigger && trigger.id === id) { - this.automation.definition.trigger = null - return - } - - const stepIdx = steps.findIndex(step => step.id === id) - if (stepIdx < 0) throw new Error("Block not found.") - steps.splice(stepIdx, 1) - this.automation.definition.steps = steps - } - - constructBlock(type, stepId, blockDefinition) { - return { - ...blockDefinition, - inputs: blockDefinition.inputs || {}, - stepId, - type, - } - } -} diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index af102ab694..dc1e2a2cc1 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -1,16 +1,18 @@ -import { writable } from "svelte/store" +import { writable, get } from "svelte/store" import { API } from "api" -import Automation from "./Automation" import { cloneDeep } from "lodash/fp" +import { generate } from "shortid" +import { selectedAutomation } from "builderStore" const initialAutomationState = { automations: [], + testResults: null, showTestPanel: false, blockDefinitions: { TRIGGER: [], ACTION: [], }, - selectedAutomation: null, + selectedAutomationId: null, } export const getAutomationStore = () => { @@ -37,49 +39,41 @@ const automationActions = store => ({ API.getAutomationDefinitions(), ]) store.update(state => { - let selected = state.selectedAutomation?.automation state.automations = responses[0] + state.automations.sort((a, b) => { + return a.name < b.name ? -1 : 1 + }) state.blockDefinitions = { TRIGGER: responses[1].trigger, ACTION: responses[1].action, } - // If previously selected find the new obj and select it - if (selected) { - selected = responses[0].filter( - automation => automation._id === selected._id - ) - state.selectedAutomation = new Automation(selected[0]) - } return state }) }, - create: async ({ name }) => { + create: async (name, trigger) => { const automation = { name, type: "automation", definition: { steps: [], + trigger, }, } - const response = await API.createAutomation(automation) - store.update(state => { - state.automations = [...state.automations, response.automation] - store.actions.select(response.automation) - return state - }) + const response = await store.actions.save(automation) + await store.actions.fetch() + store.actions.select(response._id) + return response }, duplicate: async automation => { - const response = await API.createAutomation({ + const response = await store.actions.save({ ...automation, name: `${automation.name} - copy`, _id: undefined, _ref: undefined, }) - store.update(state => { - state.automations = [...state.automations, response.automation] - store.actions.select(response.automation) - return state - }) + await store.actions.fetch() + store.actions.select(response._id) + return response }, save: async automation => { const response = await API.updateAutomation(automation) @@ -90,11 +84,13 @@ const automationActions = store => ({ ) if (existingIdx !== -1) { state.automations.splice(existingIdx, 1, updatedAutomation) - state.automations = [...state.automations] - store.actions.select(updatedAutomation) return state + } else { + state.automations = [...state.automations, updatedAutomation] } + return state }) + return response.automation }, delete: async automation => { await API.deleteAutomation({ @@ -102,34 +98,83 @@ const automationActions = store => ({ automationRev: automation?._rev, }) store.update(state => { - const existingIdx = state.automations.findIndex( - existing => existing._id === automation?._id + // Remove the automation + state.automations = state.automations.filter( + x => x._id !== automation._id ) - state.automations.splice(existingIdx, 1) - state.automations = [...state.automations] - state.selectedAutomation = null - state.selectedBlock = null + // Select a new automation if required + if (automation._id === state.selectedAutomationId) { + store.actions.select(state.automations[0]?._id) + } return state }) + await store.actions.fetch() + }, + updateBlockInputs: async (block, data) => { + // Create new modified block + let newBlock = { + ...block, + inputs: { + ...block.inputs, + ...data, + }, + } + + // Remove any nullish or empty string values + Object.keys(newBlock.inputs).forEach(key => { + const val = newBlock.inputs[key] + if (val == null || val === "") { + delete newBlock.inputs[key] + } + }) + + // Create new modified automation + const automation = get(selectedAutomation) + const newAutomation = store.actions.getUpdatedDefinition( + automation, + newBlock + ) + + // Don't save if no changes were made + if (JSON.stringify(newAutomation) === JSON.stringify(automation)) { + return + } + await store.actions.save(newAutomation) }, test: async (automation, testData) => { - store.update(state => { - state.selectedAutomation.testResults = null - return state - }) const result = await API.testAutomation({ automationId: automation?._id, testData, }) + if (!result?.trigger && !result?.steps?.length) { + throw "Something went wrong testing your automation" + } store.update(state => { - state.selectedAutomation.testResults = result + state.testResults = result return state }) }, - select: automation => { + getDefinition: id => { + return get(store).automations?.find(x => x._id === id) + }, + getUpdatedDefinition: (automation, block) => { + let newAutomation = cloneDeep(automation) + if (automation.definition.trigger?.id === block.id) { + newAutomation.definition.trigger = block + } else { + const idx = automation.definition.steps.findIndex(x => x.id === block.id) + newAutomation.definition.steps.splice(idx, 1, block) + } + return newAutomation + }, + select: id => { + if (!id || id === get(store).selectedAutomationId) { + return + } store.update(state => { - state.selectedAutomation = new Automation(cloneDeep(automation)) - state.selectedBlock = null + state.selectedAutomationId = id + state.testResults = null + state.showTestPanel = false return state }) }, @@ -147,48 +192,57 @@ const automationActions = store => ({ appId, }) }, - addTestDataToAutomation: data => { - store.update(state => { - state.selectedAutomation.addTestData(data) - return state - }) + addTestDataToAutomation: async data => { + let newAutomation = cloneDeep(get(selectedAutomation)) + newAutomation.testData = { + ...newAutomation.testData, + ...data, + } + await store.actions.save(newAutomation) }, - addBlockToAutomation: (block, blockIdx) => { - store.update(state => { - state.selectedBlock = state.selectedAutomation.addBlock( - cloneDeep(block), - blockIdx - ) - return state - }) + constructBlock(type, stepId, blockDefinition) { + return { + ...blockDefinition, + inputs: blockDefinition.inputs || {}, + stepId, + type, + id: generate(), + } }, - toggleFieldControl: value => { - store.update(state => { - state.selectedBlock.rowControl = value - return state - }) + addBlockToAutomation: async (block, blockIdx) => { + const automation = get(selectedAutomation) + let newAutomation = cloneDeep(automation) + if (!automation) { + return + } + newAutomation.definition.steps.splice(blockIdx, 0, block) + await store.actions.save(newAutomation) }, - deleteAutomationBlock: block => { - store.update(state => { - const idx = - state.selectedAutomation.automation.definition.steps.findIndex( - x => x.id === block.id - ) - state.selectedAutomation.deleteBlock(block.id) + /** + * "rowControl" appears to be the name of the flag used to determine whether + * a certain automation block uses values or bindings as inputs + */ + toggleRowControl: async (block, rowControl) => { + const newBlock = { ...block, rowControl } + const newAutomation = store.actions.getUpdatedDefinition( + get(selectedAutomation), + newBlock + ) + await store.actions.save(newAutomation) + }, + deleteAutomationBlock: async block => { + const automation = get(selectedAutomation) + let newAutomation = cloneDeep(automation) - // Select next closest step - const steps = state.selectedAutomation.automation.definition.steps - let nextSelectedBlock - if (steps[idx] != null) { - nextSelectedBlock = steps[idx] - } else if (steps[idx - 1] != null) { - nextSelectedBlock = steps[idx - 1] - } else { - nextSelectedBlock = - state.selectedAutomation.automation.definition.trigger || null - } - state.selectedBlock = nextSelectedBlock - return state - }) + // Delete trigger if required + if (newAutomation.definition.trigger?.id === block.id) { + delete newAutomation.definition.trigger + } else { + // Otherwise remove step + newAutomation.definition.steps = newAutomation.definition.steps.filter( + step => step.id !== block.id + ) + } + await store.actions.save(newAutomation) }, }) diff --git a/packages/builder/src/builderStore/store/automation/tests/Automation.spec.js b/packages/builder/src/builderStore/store/automation/tests/Automation.spec.js deleted file mode 100644 index 8378310c2e..0000000000 --- a/packages/builder/src/builderStore/store/automation/tests/Automation.spec.js +++ /dev/null @@ -1,48 +0,0 @@ -import Automation from "../Automation" -import TEST_AUTOMATION from "./testAutomation" - -const TEST_BLOCK = { - id: "AUXJQGZY7", - name: "Delay", - icon: "ri-time-fill", - tagline: "Delay for {{time}} milliseconds", - description: "Delay the automation until an amount of time has passed.", - params: { time: "number" }, - type: "LOGIC", - args: { time: "5000" }, - stepId: "DELAY", -} - -describe("Automation Data Object", () => { - let automation - - beforeEach(() => { - automation = new Automation({ ...TEST_AUTOMATION }) - }) - - it("adds a automation block to the automation", () => { - automation.addBlock(TEST_BLOCK) - expect(automation.automation.definition) - }) - - it("updates a automation block with new attributes", () => { - const firstBlock = automation.automation.definition.steps[0] - const updatedBlock = { - ...firstBlock, - name: "UPDATED", - } - automation.updateBlock(updatedBlock, firstBlock.id) - expect(automation.automation.definition.steps[0]).toEqual(updatedBlock) - }) - - it("deletes a automation block successfully", () => { - const { steps } = automation.automation.definition - const originalLength = steps.length - - const lastBlock = steps[steps.length - 1] - automation.deleteBlock(lastBlock.id) - expect(automation.automation.definition.steps.length).toBeLessThan( - originalLength - ) - }) -}) diff --git a/packages/builder/src/builderStore/store/automation/tests/testAutomation.js b/packages/builder/src/builderStore/store/automation/tests/testAutomation.js deleted file mode 100644 index 3fafbaf1d0..0000000000 --- a/packages/builder/src/builderStore/store/automation/tests/testAutomation.js +++ /dev/null @@ -1,78 +0,0 @@ -export default { - name: "Test automation", - definition: { - steps: [ - { - id: "ANBDINAPS", - description: "Send an email.", - tagline: "Send email to {{to}}", - icon: "ri-mail-open-fill", - name: "Send Email", - params: { - to: "string", - from: "string", - subject: "longText", - text: "longText", - }, - type: "ACTION", - args: { - text: "A user was created!", - subject: "New Budibase User", - from: "budimaster@budibase.com", - to: "test@test.com", - }, - stepId: "SEND_EMAIL", - }, - ], - trigger: { - id: "iRzYMOqND", - name: "Row Saved", - event: "row:save", - icon: "ri-save-line", - tagline: "Row is added to {{table.name}}", - description: "Fired when a row is saved to your database.", - params: { table: "table" }, - type: "TRIGGER", - args: { - table: { - type: "table", - views: {}, - name: "users", - schema: { - name: { - type: "string", - constraints: { - type: "string", - length: { maximum: 123 }, - presence: { allowEmpty: false }, - }, - name: "name", - }, - age: { - type: "number", - constraints: { - type: "number", - presence: { allowEmpty: false }, - numericality: { - greaterThanOrEqualTo: "", - lessThanOrEqualTo: "", - }, - }, - name: "age", - }, - }, - _id: "c6b4e610cd984b588837bca27188a451", - _rev: "7-b8aa1ce0b53e88928bb88fc11bdc0aff", - }, - }, - stepId: "ROW_SAVED", - }, - }, - type: "automation", - ok: true, - id: "b384f861f4754e1693835324a7fcca62", - rev: "1-aa1c2cbd868ef02e26f8fad531dd7e37", - live: false, - _id: "b384f861f4754e1693835324a7fcca62", - _rev: "108-4116829ec375e0481d0ecab9e83a2caf", -} diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 7d19573cce..51f88add27 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -1,6 +1,11 @@ import { get, writable } from "svelte/store" import { cloneDeep } from "lodash/fp" -import { selectedScreen, selectedComponent } from "builderStore" +import { + selectedScreen, + selectedComponent, + screenHistoryStore, + automationHistoryStore, +} from "builderStore" import { datasources, integrations, @@ -67,6 +72,8 @@ const INITIAL_FRONTEND_STATE = { // onboarding onboarding: false, tourNodes: null, + + builderSidePanel: false, } export const getFrontendStore = () => { @@ -122,6 +129,8 @@ export const getFrontendStore = () => { navigation: application.navigation || {}, usedPlugins: application.usedPlugins || [], })) + screenHistoryStore.reset() + automationHistoryStore.reset() // Initialise backend stores database.set(application.instance) @@ -179,10 +188,7 @@ export const getFrontendStore = () => { } // Check screen isn't already selected - if ( - state.selectedScreenId === screen._id && - state.selectedComponentId === screen.props?._id - ) { + if (state.selectedScreenId === screen._id) { return } @@ -256,7 +262,7 @@ export const getFrontendStore = () => { } }, save: async screen => { - /* + /* Temporarily disabled to accomodate migration issues. store.actions.screens.validate(screen) */ @@ -312,7 +318,7 @@ export const getFrontendStore = () => { const screensToDelete = Array.isArray(screens) ? screens : [screens] // Build array of promises to speed up bulk deletions - const promises = [] + let promises = [] let deleteUrls = [] screensToDelete.forEach(screen => { // Delete the screen @@ -326,8 +332,8 @@ export const getFrontendStore = () => { deleteUrls.push(screen.routing.route) }) - promises.push(store.actions.links.delete(deleteUrls)) await Promise.all(promises) + await store.actions.links.delete(deleteUrls) const deletedIds = screensToDelete.map(screen => screen._id) const routesResponse = await API.fetchAppRoutes() store.update(state => { @@ -347,6 +353,7 @@ export const getFrontendStore = () => { return state }) + return null }, updateSetting: async (screen, name, value) => { if (!screen || !name) { diff --git a/packages/builder/src/builderStore/store/history.js b/packages/builder/src/builderStore/store/history.js new file mode 100644 index 0000000000..0f21085c6a --- /dev/null +++ b/packages/builder/src/builderStore/store/history.js @@ -0,0 +1,319 @@ +import * as jsonpatch from "fast-json-patch/index.mjs" +import { writable, derived, get } from "svelte/store" + +const Operations = { + Add: "Add", + Delete: "Delete", + Change: "Change", +} + +const initialState = { + history: [], + position: 0, + loading: false, +} + +export const createHistoryStore = ({ + getDoc, + selectDoc, + beforeAction, + afterAction, +}) => { + // Use a derived store to check if we are able to undo or redo any operations + const store = writable(initialState) + const derivedStore = derived(store, $store => { + return { + ...$store, + canUndo: $store.position > 0, + canRedo: $store.position < $store.history.length, + } + }) + + // Wrapped versions of essential functions which we call ourselves when using + // undo and redo + let saveFn + let deleteFn + + /** + * Internal util to set the loading flag + */ + const startLoading = () => { + store.update(state => { + state.loading = true + return state + }) + } + + /** + * Internal util to unset the loading flag + */ + const stopLoading = () => { + store.update(state => { + state.loading = false + return state + }) + } + + /** + * Resets history state + */ + const reset = () => { + store.set(initialState) + } + + /** + * Adds or updates an operation in history. + * For internal use only. + * @param operation the operation to save + */ + const saveOperation = operation => { + store.update(state => { + // Update history + let history = state.history + let position = state.position + if (!operation.id) { + // Every time a new operation occurs we discard any redo potential + operation.id = Math.random() + history = [...history.slice(0, state.position), operation] + position += 1 + } else { + // If this is a redo/undo of an existing operation, just update history + // to replace the doc object as revisions may have changed + const idx = history.findIndex(op => op.id === operation.id) + history[idx].doc = operation.doc + } + return { history, position } + }) + } + + /** + * Wraps the save function, which asynchronously updates a doc. + * The returned function is an enriched version of the real save function so + * that we can control history. + * @param fn the save function + * @returns {function} a wrapped version of the save function + */ + const wrapSaveDoc = fn => { + saveFn = async (doc, operationId) => { + // Only works on a single doc at a time + if (!doc || Array.isArray(doc)) { + return + } + startLoading() + try { + const oldDoc = getDoc(doc._id) + const newDoc = jsonpatch.deepClone(await fn(doc)) + + // Store the change + if (!oldDoc) { + // If no old doc, this is an add operation + saveOperation({ + type: Operations.Add, + doc: newDoc, + id: operationId, + }) + } else { + // Otherwise this is a change operation + saveOperation({ + type: Operations.Change, + forwardPatch: jsonpatch.compare(oldDoc, doc), + backwardsPatch: jsonpatch.compare(doc, oldDoc), + doc: newDoc, + id: operationId, + }) + } + stopLoading() + return newDoc + } catch (error) { + // We want to allow errors to propagate up to normal handlers, but we + // want to stop loading first + stopLoading() + throw error + } + } + return saveFn + } + + /** + * Wraps the delete function, which asynchronously deletes a doc. + * The returned function is an enriched version of the real delete function so + * that we can control history. + * @param fn the delete function + * @returns {function} a wrapped version of the delete function + */ + const wrapDeleteDoc = fn => { + deleteFn = async (doc, operationId) => { + // Only works on a single doc at a time + if (!doc || Array.isArray(doc)) { + return + } + startLoading() + try { + const oldDoc = jsonpatch.deepClone(doc) + await fn(doc) + saveOperation({ + type: Operations.Delete, + doc: oldDoc, + id: operationId, + }) + stopLoading() + } catch (error) { + // We want to allow errors to propagate up to normal handlers, but we + // want to stop loading first + stopLoading() + throw error + } + } + return deleteFn + } + + /** + * Asynchronously undoes the previous operation. + * Optionally selects the changed document so that changes are visible. + * @returns {Promise} + */ + const undo = async () => { + // Sanity checks + const { canUndo, history, position, loading } = get(derivedStore) + if (!canUndo || loading) { + return + } + const operation = history[position - 1] + if (!operation) { + return + } + startLoading() + + // Before hook + await beforeAction?.(operation) + + // Update state immediately to prevent further clicks and to prevent bad + // history in the event of an update failing + store.update(state => { + return { + ...state, + position: state.position - 1, + } + }) + + // Undo the operation + try { + // Undo ADD + if (operation.type === Operations.Add) { + // Try to get the latest doc version to delete + const latestDoc = getDoc(operation.doc._id) + const doc = latestDoc || operation.doc + await deleteFn(doc, operation.id) + } + + // Undo DELETE + else if (operation.type === Operations.Delete) { + // Delete the _rev from the deleted doc so that we can save it as a new + // doc again without conflicts + let doc = jsonpatch.deepClone(operation.doc) + delete doc._rev + const created = await saveFn(doc, operation.id) + selectDoc?.(created?._id || doc._id) + } + + // Undo CHANGE + else { + // Get the current doc and apply the backwards patch on top of it + let doc = jsonpatch.deepClone(getDoc(operation.doc._id)) + if (doc) { + jsonpatch.applyPatch( + doc, + jsonpatch.deepClone(operation.backwardsPatch) + ) + await saveFn(doc, operation.id) + selectDoc?.(doc._id) + } + } + stopLoading() + } catch (error) { + stopLoading() + throw error + } + + // After hook + await afterAction?.(operation) + } + + /** + * Asynchronously redoes the previous undo. + * Optionally selects the changed document so that changes are visible. + * @returns {Promise} + */ + const redo = async () => { + // Sanity checks + const { canRedo, history, position, loading } = get(derivedStore) + if (!canRedo || loading) { + return + } + const operation = history[position] + if (!operation) { + return + } + startLoading() + + // Before hook + await beforeAction?.(operation) + + // Update state immediately to prevent further clicks and to prevent bad + // history in the event of an update failing + store.update(state => { + return { + ...state, + position: state.position + 1, + } + }) + + // Redo the operation + try { + // Redo ADD + if (operation.type === Operations.Add) { + // Delete the _rev from the deleted doc so that we can save it as a new + // doc again without conflicts + let doc = jsonpatch.deepClone(operation.doc) + delete doc._rev + const created = await saveFn(doc, operation.id) + selectDoc?.(created?._id || doc._id) + } + + // Redo DELETE + else if (operation.type === Operations.Delete) { + // Try to get the latest doc version to delete + const latestDoc = getDoc(operation.doc._id) + const doc = latestDoc || operation.doc + await deleteFn(doc, operation.id) + } + + // Redo CHANGE + else { + // Get the current doc and apply the forwards patch on top of it + let doc = jsonpatch.deepClone(getDoc(operation.doc._id)) + if (doc) { + jsonpatch.applyPatch(doc, jsonpatch.deepClone(operation.forwardPatch)) + await saveFn(doc, operation.id) + selectDoc?.(doc._id) + } + } + stopLoading() + } catch (error) { + stopLoading() + throw error + } + + // After hook + await afterAction?.(operation) + } + + return { + subscribe: derivedStore.subscribe, + wrapSaveDoc, + wrapDeleteDoc, + reset, + undo, + redo, + } +} diff --git a/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte b/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte index e852ee1a0d..b80ba45086 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/AutomationBuilder.svelte @@ -1,10 +1,10 @@ -{#if automation} - +{#if $selectedAutomation} + {#key $selectedAutomation._id} + + {/key} {/if} diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte index caf8835b86..f30b49eb39 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte @@ -5,7 +5,6 @@ Detail, Body, Icon, - Tooltip, notifications, } from "@budibase/bbui" import { automationStore } from "builderStore" @@ -13,7 +12,6 @@ import { externalActions } from "./ExternalActions" export let blockIdx - export let blockComplete const disabled = { SEND_EMAIL_SMTP: { @@ -50,15 +48,12 @@ async function addBlockToAutomation() { try { - const newBlock = $automationStore.selectedAutomation.constructBlock( + const newBlock = automationStore.actions.constructBlock( "ACTION", actionVal.stepId, actionVal ) - automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1) - await automationStore.actions.save( - $automationStore.selectedAutomation?.automation - ) + await automationStore.actions.addBlockToAutomation(newBlock, blockIdx + 1) } catch (error) { notifications.error("Error saving automation") } @@ -66,20 +61,14 @@ { - blockComplete = true - addBlockToAutomation() - }} + onConfirm={addBlockToAutomation} > - Select an app or event. - - - Apps - + + Apps
{#each Object.entries(external) as [idx, action]}
- {idx.charAt(0).toUpperCase() + idx.slice(1)} + + {idx.charAt(0).toUpperCase() + idx.slice(1)} + +
{/each} +
+ Actions -
{#each Object.entries(internal) as [idx, action]} - {#if disabled[idx] && disabled[idx].disabled} - -
selectAction(action)} - > -
- - - {action.name} -
-
-
- {:else} -
selectAction(action)} - > -
- - - {action.name} -
+ {@const isDisabled = disabled[idx] && disabled[idx].disabled} +
selectAction(action)} + > +
+ + {action.name} + {#if isDisabled} + + {/if}
- {/if} +
{/each}
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte index 4b01616b54..63a3478ef3 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte @@ -1,5 +1,5 @@
-
+
{automation.name} -
-
-
-
-
- -
+
+ + +
{ testDataModal.show() @@ -62,15 +60,13 @@ icon="MultipleCheck" size="M">Run test -
- { - $automationStore.showTestPanel = true - }} - size="M">Test Details -
+ { + $automationStore.showTestPanel = true + }} + size="M">Test Details
@@ -80,7 +76,7 @@
{#if block.stepId !== ActionStepID.LOOP} @@ -105,6 +101,9 @@ diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte index d6e5fcb36d..7484a60502 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte @@ -1,5 +1,5 @@
{}}> - {#if loopingSelected} + {#if loopBlock}
{ @@ -174,13 +142,8 @@
-
{ - onSelect(block) - }} - > - +
{}}> +
@@ -198,9 +161,7 @@ $automationStore.blockDefinitions.ACTION.LOOP.schema.inputs .properties )} - block={$automationStore.selectedAutomation?.automation.definition.steps.find( - x => x.blockToLoop === block.id - )} + block={loopBlock} {webhookModal} /> @@ -209,22 +170,28 @@ {/if} {/if} - - {#if !blockComplete} + (open = !open)} + /> + {#if open}
{#if !isTrigger}
- {#if !loopingSelected} - addLooping()} icon="Reuse" - >Add Looping + {#if !loopBlock} + addLooping()} icon="Reuse"> + Add Looping + {/if} {#if showBindingPicker}