diff --git a/.all-contributorsrc b/.all-contributorsrc index 53705907c2..3a416f917e 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -162,6 +162,7 @@ "translation" ] }, + { "login": "mslourens", "name": "Maurits Lourens", "avatar_url": "https://avatars.githubusercontent.com/u/1907152?v=4", diff --git a/.eslintignore b/.eslintignore index 54824be5c7..579bd55947 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,4 +7,5 @@ packages/server/client packages/builder/.routify packages/builder/cypress/support/queryLevelTransformerFunction.js packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js -packages/builder/cypress/reports \ No newline at end of file +packages/builder/cypress/reports +packages/sdk/sdk \ No newline at end of file diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index 1be855e3fa..0000000000 --- a/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,76 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at community@budibase.com. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 120000 index 0000000000..d28e66efdc --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1 @@ +../docs/CODE_OF_CONDUCT.md \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index bd21123709..0000000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,208 +0,0 @@ -# Contributing - -From opening a bug report to creating a pull request: every contribution is appreciated and welcome. If you're planning to implement a new feature or change the api please create an issue first. This way we can ensure that your precious work is not in vain. - -## Not Sure Where to Start? - -Budibase is a low-code web application builder that creates svelte based web applications. - -Budibase is a monorepo managed by [lerna](https://github.com/lerna/lerna). Lerna manages the building and publishing of the budibase packages. At a high level, here are the packages that make up budibase. - -- **packages/builder** - contains code for the budibase builder client side svelte application. - -- **packages/client** - A module that runs in the browser responsible for reading JSON definition and creating living, breathing web apps from it. - -- **packages/server** - The budibase server. This [Koa](https://koajs.com/) app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system. - -- **packages/worker** - This [Koa](https://koajs.com/) app is responsible for providing global apis for managing your budibase installation. Authentication, Users, Email, Org and Auth configs are all provided by the worker. - -## Contributor License Agreement (CLA) - -In order to accept your pull request, we need you to submit a CLA. You only need to do this once. If you are submitting a pull request for the first time, just submit a Pull Request and our CLA Bot will give you instructions on how to sign the CLA before merging your Pull Request. - -All contributors must sign an [Individual Contributor License Agreement](https://github.com/budibase/budibase/blob/next/.github/cla/individual-cla.md). - -If contributing on behalf of your company, your company must sign a [Corporate Contributor License Agreement](https://github.com/budibase/budibase/blob/next/.github/cla/corporate-cla.md). If so, please contact us via community@budibase.com. - -## Glossary of Terms - -To understand the budibase API, it can be helpful to understand the top level entities that make up Budibase. - -### Client - -A client represents a single budibase customer. Each budibase client will have 1 or more budibase servers. Every client is assigned a unique ID. - -### App - -A client can have one or more budibase applications. Budibase applications would be things like "Developer Inventory Management" or "Goat Herder CRM". Think of a budibase application as a tree. - -### Database - -An App can have one or more databases. Keeping with our [dendrology](https://en.wikipedia.org/wiki/Dendrology) analogy - think of an database as a branch on the tree. Databases are used to keep data separate for different instances of your app. For example, if you had a CRM app, you may create a database for your US office, and a database for your Australian office. Databases allow us to support [multitenancy](https://www.gartner.com/en/information-technology/glossary/multitenancy) in budibase applications. - -### Table - -Tables in budibase are almost akin to tables in relational databases. A table may be a "Car" or an "Employee". They are the main building blocks for the creation and management of backend data in budibase. - -### View - -A View is an advanced feature in budibase that allows you to write a custom query using [MapReduce](https://pouchdb.com/guides/queries.html) queries. Views enable powerful query functionality and calculations, allowing you to do more with your data. - -### Page - -A page in budibase is actually a single, self contained svelte web app. There are only 2 pages in budibase. The **login** page and the **main** page. - -### Screen - -A screen is a component within a single page. Generally, screens represent client side routes, and can be switched without refreshing the page. - -### Component - -A component is the basic frontend building block of a budibase app. - -### Component Library - -Component libraries are collections of components as well as the definition of their props contained in a file called `components.json`. - -## Contributing to Budibase - -* Please maintain the existing code style. - -* Please try to keep your commits small and focused. - -* Please write tests. - -* If the project diverges from your branch, please rebase instead of merging. This makes the commit graph easier to read. - -* Once your work is completed, please raise a PR against the `develop` branch with some information about what has changed and why. - -### Getting Started For Contributors -#### 1. Prerequisites - -NodeJS Version `14.x.x` - -*yarn -* `npm install -g yarn` - -*jest* - `npm install -g jest` - -#### 2. Clone this repository - -`git clone https://github.com/Budibase/budibase.git` - -then `cd ` into your local copy. - -#### 3. Install and Build - -| **NOTE**: On Windows, all yarn commands must be executed on a bash shell (e.g. git bash) - -To develop the Budibase platform you'll need [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) installed. - -##### Quick method - -`yarn setup` will check that all necessary components are installed and setup the repo for usage. - -##### Manual method - -The following commands can be executed to manually get Budibase up and running (assuming Docker/Docker Compose has been installed). - -`yarn` to install project dependencies - -`yarn bootstrap` will install all budibase modules and symlink them together using lerna. - -`yarn build` will build all budibase packages. - -#### 4. Running - -To run the budibase server and builder in dev mode (i.e. with live reloading): - -1. Open a new console -2. `yarn dev` (from root) -3. Access the builder on http://localhost:10000/builder - -This will enable watch mode for both the builder app, server, client library and any component libraries. - -#### 5. Debugging using VS Code - -To debug the budibase server and worker a VS Code launch configuration has been provided. - -Visit the debug window and select `Budibase Server` or `Budibase Worker` to debug the respective component. -Alternatively to start both components simultaneously select `Start Budibase`. - -In addition to the above, the remaining budibase components may be ran in dev mode using: `yarn dev:noserver`. - -#### 6. Cleanup - -If you wish to delete all the apps created in development and reset the environment then run the following: - -1. `yarn nuke:docker` will wipe all the Budibase services -2. `yarn dev` will restart all the services - -### Backend - -For the backend we run [Redis](https://redis.io/), [CouchDB](https://couchdb.apache.org/), [MinIO](https://min.io/) and [NGINX](https://www.nginx.com/) in Docker compose. This means that to develop Budibase you will need Docker and Docker compose installed. The backend services are then ran separately as Node services with nodemon so that they can be debugged outside of Docker. - -### Data Storage - -When you are running locally, budibase stores data on disk using docker volumes. The volumes and the types of data associated with each are: - -- `redis_data` - - Sessions, email tokens -- `couchdb3_data` - - Global and app databases -- `minio_data` - - App manifest, budibase client, static assets - -### Devlopment Modes - -A combination of environment variables controls the mode that budibase runs in. -Yarn commands can be used to mimic the different modes that budibase can be ran in - -#### Self Hosted -The default mode. A single tenant installation with no usage restrictions. - -To enable this mode, use: -``` -yarn mode:self -``` - -#### Cloud -The cloud mode, with account portal turned off. - -To enable this mode, use: -``` -yarn mode:cloud -``` -#### Cloud & Account -The cloud mode, with account portal turned on. This is a replica of the mode that runs at https://budibase.app - - -To enable this mode, use: -``` -yarn mode:account -``` -### CI - An overview of the CI pipelines can be found [here](./workflows/README.md) -### Troubleshooting - -Sometimes, things go wrong. This can be due to incompatible updates on the budibase platform. To clear down your development environment and start again follow **Step 6. Cleanup**, then proceed from **Step 3. Install and Build** in the setup guide above. You should have a fresh Budibase installation. -### Running tests - -#### End-to-end Tests - -Budibase uses Cypress to run a number of E2E tests. To run the tests execute the following command in the root folder: - -``` -yarn test:e2e -``` - -Or if you are in the builder you can run `yarn cy:test`. - - -### Other Useful Information - -* The contributors are listed in [AUTHORS.md](https://github.com/Budibase/budibase/blob/master/.github/AUTHORS.md) (add yourself). - -* This project uses a modified version of the MPLv2 license, see [LICENSE](https://github.com/budibase/server/blob/master/LICENSE). - -* We use the [C4 (Collective Code Construction Contract)](https://rfc.zeromq.org/spec:42/C4/) process for contributions. - Please read this if you are unfamiliar with it. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 120000 index 0000000000..c81d5e88a2 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1 @@ +../docs/CONTRIBUTING.md \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index b4f7739293..457d2c1451 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -31,6 +31,9 @@ A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. + +**App Export** +If possible - please attach an export of your budibase application for debugging/reproduction purposes. **Desktop (please complete the following information):** - OS: [e.g. iOS] diff --git a/.github/ISSUE_TEMPLATE/epic.md b/.github/ISSUE_TEMPLATE/epic.md new file mode 100644 index 0000000000..b8cf652125 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/epic.md @@ -0,0 +1,24 @@ +--- +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/README.md b/.github/workflows/README.md index d2fcd16bb0..f77323d85a 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -6,7 +6,7 @@ Welcome to the budibase CI pipelines directory. This document details what each ## All CI Pipelines ### Note -- When running workflow dispatch jobs, ensure you always run them off the `master` branch. It defaults to `develop`, so double check before running any jobs. +- When running workflow dispatch jobs, ensure you always run them off the `master` branch. It defaults to `develop`, so double check before running any jobs. The exception to this case is the `deploy-release` job which requires the develop branch. ### Standard CI Build Job (budibase_ci.yml) Triggers: @@ -24,14 +24,14 @@ The standard CI Build job is what runs when you raise a PR to develop or master. Triggers: - Push to develop -The job responsible for building, tagging and pushing docker images out to the test and staging environments. +The job responsible for building, tagging and pushing docker images out to the test and release environments. - Installs all dependencies - builds the project - run the unit tests - publish the budibase JS packages under a prerelease tag to NPM - build, tag and push docker images under the `develop` tag to docker hub -These images will then be pulled by the test and staging environments, updating the latest automatically. Discord notifications are sent to the #infra channel when this occurs. +These images will then be pulled by the test and release environments, updating the latest automatically. Discord notifications are sent to the #infra channel when this occurs. ### Release Job (release.yml) Triggers: @@ -57,8 +57,33 @@ This job relies on the release job to have run first, so the latest image is pus - Build and release the budibase helm chart for kubernetes users - Perform a github release with the latest version. You can see previous releases here (https://github.com/Budibase/budibase/releases) +### Deploy Release (deploy-release.yml) +Triggers: +- Manual Workflow Dispatch Trigger -### Cloud Deploy (deploy-cloud.yml) +This job is responsible for deploying to our release, cloud kubernetes environment. You must run the release job first, to ensure that the latest images have been built and pushed to docker hub. After kicking off this job, the following will occur: + +- Checks out the release branch +- Pulls the latest `values.yaml` from budibase infra, a private repo containing budibases infrastructure configuration +- Gets the latest budibase version from `lerna.json`, if it hasn't been specified in the workflow when you kicked it off +- Configures AWS Credentials +- Deploys the helm chart in the budibase repo to our preproduction EKS cluster, injecting the `values.yaml` we pulled from budibase-infra +- Fires off a discord webhook in the #infra channel to show that the deployment completely successfully. + +### Deploy Preprod (deploy-preprod.yml) +Triggers: +- Manual Workflow Dispatch Trigger + +This job is responsible for deploying to our preprod, cloud kubernetes environment. You must run the release job first, to ensure that the latest images have been built and pushed to docker hub. After kicking off this job, the following will occur: + +- Checks out the master branch +- Pulls the latest `values.yaml` from budibase infra, a private repo containing budibases infrastructure configuration +- Gets the latest budibase version from `lerna.json`, if it hasn't been specified in the workflow when you kicked it off +- Configures AWS Credentials +- Deploys the helm chart in the budibase repo to our preprod EKS cluster, injecting the `values.yaml` we pulled from budibase-infra +- Fires off a discord webhook in the #infra channel to show that the deployment completely successfully. + +### Deploy Production (deploy-cloud.yml) Triggers: - Manual Workflow Dispatch Trigger @@ -90,4 +115,77 @@ This job is responsible for deploying to our production, cloud kubernetes enviro ### Rollback A Bad Cloud Deployment - Kick off cloud deploy job - Ensure you are running off master -- Enter the version number of the last known good version of budibase. For example `1.0.0` \ No newline at end of file +- Enter the version number of the last known good version of budibase. For example `1.0.0` + +## Pro + +| **NOTE**: When developing for both pro / budibase repositories, your branch names need to match, or else the correct pro doesn't get run within your CI job. + +### Installing Pro + +The pro package is always installed from source in our CI jobs. + +This is done to prevent pro needing to be published prior to CI runs in budiabse. This is required for two reasons: +- To reduce developer need to manually bump versions, i.e: + - release pro, bump pro dep in budibase, now ci can run successfully +- The cyclic dependency on backend-core, i.e: + - pro depends on backend-core + - server depends on pro + - backend-core lives in the monorepo, so it can't be released independently to be used in pro + - therefore the only option is to pull pro from source and release it as a part of the monorepo release, as if it were a mono package + +The install is performed using the same steps as local development, via the `yarn bootstrap` command, see the [Contributing Guide#Pro](../../docs/CONTRIBUTING.md#pro) + +The branch to install pro from can vary depending on ref of the commit that triggered the budibase CI job. This is done to enable branches which have changes in both the monorepo and the pro repo to have their CI pass successfully. + +This is done using the [pro/install.sh](../../scripts/pro/install.sh) script. The script will: +- Clone pro to it's default branch (`develop`) +- Check if the clone worked, on forked versions of budibase this will fail due to no access + - This is fine as the `yarn` command will install the version from NPM + - Community PRs should never touch pro so this will always work +- Checkout the `BRANCH` argument, if this fails fallback to `BASE_BRANCH` + - This enables the more complex case of a feature branch being merged to another feature branch, e.g. + - I am working on a branch `epic/stonks` which exists on budibase and pro. + - I want to merge a change to this branch in budibase from `feature/stonks-ui`, which only exists in budibase + - The base branch ensures that `epic/stonks` in pro will still be checked out for the CI run, rather than falling back to `develop` +- Run `yarn setup` to build and install dependencies + - `yarn` + - `yarn bootstrap` + - `yarn build` + - The will build .ts files, and also update the `main` and `types` of `package.json` to point to `dist` rather than src + - The build command will only ever work in CI, it is prevented in local dev + +#### `BRANCH` and `BASE_BRANCH` arguments +These arguments are supplied by the various budibase build and release pipelines +- `budibase_ci` + - `BRANCH: ${{ github.event.pull_request.head.ref }}` -> The branch being merged + - `BASE_BRANCH: ${{ github.event.pull_request.base.ref}}` -> The base branch +- `release-develop` + - `BRANCH: develop` -> always use the `develop` branch in pro +- `release` + - `BRANCH: master` -> always use the `master` branch in pro + + +### Releasing Pro +After budibase dependencies have been released we will release the new version of pro to match the release version of budibase dependencies. This is to ensure that we are always keeping the version of `backend-core` in sync in the pro package and in budibase packages. Without this we could run into scenarios where different versions are being used when installed via `yarn` inside the docker images, creating very difficult to debug cases. + +Pro is released using the [pro/release.sh](../../scripts/pro/release.sh) script. The script will: +- Inspect the `VERSION` from the `lerna.json` file in budibase +- Determine whether to use the `latest` or `develop` tag based on the command argument +- Go to pro directory + - install npm creds + - update the version of `backend-core` to be `VERSION`, the version just released by lerna + - publish to npm. Uses a `lerna publish` command, pro itself is a mono repo. + - force the version to be the same as `VERSION` to keep pro and budibase in sync + - reverts the changes to `main` and `types` in `package.json` that were made by the build step, to point back to source + - commit & push: `Prep next development iteration` +- Go to budibase + - Update to the new version of pro in `server` and `worker` so the latest pro version is used in the docker builds + - commit & push: `Update pro version to $VERSION` + + +#### `COMMAND` argument +This argument is supplied by the existing `release` and `release:develop` budibase commands, which invoke the pro release +- `release` will supply no command and default to use `latest` +- `release:develop` will supply `develop` + diff --git a/.github/workflows/budibase_ci.yml b/.github/workflows/budibase_ci.yml index 8e235532cf..475bd4f66a 100644 --- a/.github/workflows/budibase_ci.yml +++ b/.github/workflows/budibase_ci.yml @@ -11,6 +11,7 @@ on: branches: - master - develop + - release workflow_dispatch: env: @@ -22,6 +23,15 @@ jobs: build: runs-on: ubuntu-latest + services: + couchdb: + image: ibmcom/couchdb3 + env: + COUCHDB_PASSWORD: budibase + COUCHDB_USER: budibase + ports: + - 4567:5984 + strategy: matrix: node-version: [14.x] @@ -52,9 +62,8 @@ jobs: name: codecov-umbrella verbose: true - # TODO: parallelise this - - name: Cypress run - uses: cypress-io/github-action@v2 - with: - install: false - command: yarn test:e2e:ci + - name: QA Core Integration Tests + run: | + cd qa-core + yarn + yarn api:test:ci \ No newline at end of file diff --git a/.github/workflows/deploy-cloud.yaml b/.github/workflows/deploy-cloud.yaml index a05f97f097..869a88a5b3 100644 --- a/.github/workflows/deploy-cloud.yaml +++ b/.github/workflows/deploy-cloud.yaml @@ -1,4 +1,4 @@ -name: Budibase Cloud Deploy +name: Budibase Deploy Production on: workflow_dispatch: diff --git a/.github/workflows/deploy-preprod.yml b/.github/workflows/deploy-preprod.yml index ac76e9bed8..c3f690f568 100644 --- a/.github/workflows/deploy-preprod.yml +++ b/.github/workflows/deploy-preprod.yml @@ -1,12 +1,10 @@ -name: Budibase Release Preprod +name: Budibase Deploy Preprod on: workflow_dispatch: env: - POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }} INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }} - POSTHOG_URL: ${{ secrets.POSTHOG_URL }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} jobs: diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml new file mode 100644 index 0000000000..b37ff9cee8 --- /dev/null +++ b/.github/workflows/deploy-release.yml @@ -0,0 +1,99 @@ +name: Budibase Deploy Release + +on: + workflow_dispatch: + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - 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: Fail if branch is not develop + if: github.ref != 'refs/heads/develop' + run: | + echo "Ref is not develop, you must run this job from develop." + exit 1 + + - name: Get the latest budibase release version + id: version + run: | + release_version=$(cat lerna.json | jq -r '.version') + echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV + + - name: Tag and release Proxy service docker image + run: | + docker login -u $DOCKER_USER -p $DOCKER_PASSWORD + yarn build:docker:proxy:release + docker tag proxy-service budibase/proxy:$RELEASE_TAG + docker push budibase/proxy:$RELEASE_TAG + env: + DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} + RELEASE_TAG: k8s-release + + - 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: glopezep/helm@v1.7.1 + 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 }} diff --git a/.github/workflows/deploy-single-image.yml b/.github/workflows/deploy-single-image.yml new file mode 100644 index 0000000000..cd16574eea --- /dev/null +++ b/.github/workflows/deploy-single-image.yml @@ -0,0 +1,69 @@ +name: Deploy Budibase Single Container Image to DockerHub + +on: + workflow_dispatch: + +env: + CI: true + PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }} + REGISTRY_URL: registry.hub.docker.com +jobs: + build: + name: "build" + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [14.x] + steps: + - name: Fail if branch is not master + if: github.ref != 'refs/heads/master' + run: | + echo "Ref is not master, you must run this job from master." + exit 1 + - name: "Checkout" + uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: Setup QEMU + uses: docker/setup-qemu-action@v1 + - name: Setup Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + - name: Run Yarn + run: yarn + - name: Run Yarn Bootstrap + run: yarn bootstrap + - name: Runt Yarn Lint + run: yarn lint + - name: Run Yarn Build + run: yarn build:docker:pre + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_API_KEY }} + - name: Get the latest release version + id: version + run: | + release_version=$(cat lerna.json | jq -r '.version') + echo $release_version + echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV + - name: Tag and release Budibase service docker image + uses: docker/build-push-action@v2 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }} + file: ./hosting/single/Dockerfile + - name: Tag and release Budibase Azure App Service docker image + uses: docker/build-push-action@v2 + with: + context: . + push: true + platforms: linux/amd64 + build-args: TARGETBUILD=aas + tags: budibase/budibase-aas,budibase/budibase-aas:v${{ env.RELEASE_VERSION }} + file: ./hosting/single/Dockerfile diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml index d4050ab40e..21c74851e1 100644 --- a/.github/workflows/release-develop.yml +++ b/.github/workflows/release-develop.yml @@ -1,5 +1,5 @@ -name: Budibase Release Staging -concurrency: release-develop +name: Budibase Prerelease +concurrency: release-prerelease on: push: @@ -18,10 +18,12 @@ on: workflow_dispatch: env: - POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }} + # Posthog token used by ui at build time + # disable unless needed for testing + # POSTHOG_TOKEN: phc_uDYOfnFt6wAbBAXkC6STjcrTpAFiWIhqgFcsC1UVO5F INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }} - POSTHOG_URL: ${{ secrets.POSTHOG_URL }} - PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }} + PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + FEATURE_PREVIEW_URL: https://budirelease.live jobs: release: @@ -44,7 +46,8 @@ jobs: - run: yarn - run: yarn bootstrap - run: yarn lint - - run: yarn build + - run: yarn build + - run: yarn build:sdk - run: yarn test - name: Configure AWS Credentials @@ -72,3 +75,77 @@ jobs: env: DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} + + - name: Get the latest budibase release version + id: version + run: | + release_version=$(cat lerna.json | jq -r '.version') + echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV + + - name: Tag and release Proxy service docker image + run: | + docker login -u $DOCKER_USER -p $DOCKER_PASSWORD + yarn build:docker:proxy:release + docker tag proxy-service budibase/proxy:$RELEASE_TAG + docker push budibase/proxy:$RELEASE_TAG + env: + DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} + RELEASE_TAG: k8s-release + + - 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: glopezep/helm@v1.7.1 + 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 }} diff --git a/.github/workflows/release-selfhost.yml b/.github/workflows/release-selfhost.yml index fc2b7b0cca..d78180fdc7 100644 --- a/.github/workflows/release-selfhost.yml +++ b/.github/workflows/release-selfhost.yml @@ -8,19 +8,28 @@ jobs: runs-on: ubuntu-latest steps: + - name: Fail if branch is not master + if: github.ref != 'refs/heads/master' + run: | + echo "Ref is not master, you must run this job from master." + exit 1 + - uses: actions/checkout@v2 with: node-version: 14.x fetch_depth: 0 + - name: Get the latest budibase release version + id: version + run: | + release_version=$(cat lerna.json | jq -r '.version') + echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV + - name: Tag and release Docker images (Self Host) run: | docker login -u $DOCKER_USER -p $DOCKER_PASSWORD - # Get latest release version - release_version=$(cat lerna.json | jq -r '.version') - echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV - release_tag=v$release_version + release_tag=v${{ env.RELEASE_VERSION }} # Pull apps and worker images docker pull budibase/apps:$release_tag @@ -40,13 +49,12 @@ jobs: DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} SELFHOST_TAG: latest - - - name: Build CLI executables + + - name: Bootstrap and build (CLI) run: | - pushd packages/cli yarn + yarn bootstrap yarn build - popd - name: Build OpenAPI spec run: | @@ -93,4 +101,4 @@ jobs: with: webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }} content: "Self Host Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Self Host." - embed-title: ${{ env.RELEASE_VERSION }} \ No newline at end of file + embed-title: ${{ env.RELEASE_VERSION }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fa3aaf28e9..de288dd7db 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,11 +16,21 @@ on: - 'package.json' - 'yarn.lock' workflow_dispatch: + inputs: + versioning: + type: choice + description: "Versioning type: patch, minor, major" + default: patch + options: + - patch + - minor + - major + required: true env: - POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }} + # Posthog token used by ui at build time + POSTHOG_TOKEN: phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }} - POSTHOG_URL: ${{ secrets.POSTHOG_URL }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }} @@ -46,6 +56,7 @@ jobs: - run: yarn bootstrap - run: yarn lint - run: yarn build + - run: yarn build:sdk - run: yarn test - name: Configure AWS Credentials @@ -58,6 +69,7 @@ jobs: - name: Publish budibase packages to NPM env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + RELEASE_VERSION_TYPE: ${{ github.event.inputs.versioning }} run: | # setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default git config --global user.name "Budibase Release Bot" diff --git a/.github/workflows/smoke_test.yaml b/.github/workflows/smoke_test.yaml index 7002c8335b..cffb914aaf 100644 --- a/.github/workflows/smoke_test.yaml +++ b/.github/workflows/smoke_test.yaml @@ -1,4 +1,4 @@ -name: Budibase Smoke Test +name: Budibase Nightly Tests on: workflow_dispatch: @@ -6,7 +6,7 @@ on: - cron: "0 5 * * *" # every day at 5AM jobs: - release: + nightly: runs-on: ubuntu-latest steps: @@ -43,6 +43,18 @@ jobs: name: Test Reports path: packages/builder/cypress/reports/testReport.html + # 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:e2e:ci:notify env: diff --git a/.gitignore b/.gitignore index 03d77c5477..e1d3e6db0e 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ typings/ # dotenv environment variables file .env +!qa-core/.env !hosting/.env hosting/.generated-nginx.dev.conf hosting/proxy/.generated-nginx.prod.conf @@ -101,3 +102,7 @@ packages/builder/cypress.env.json packages/builder/cypress/reports stats.html +# TypeScript cache +*.tsbuildinfo +budibase-component +budibase-datasource diff --git a/.prettierignore b/.prettierignore index bbeff65da7..3a381d255e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,3 +9,4 @@ packages/server/src/definitions/openapi.ts packages/builder/.routify packages/builder/cypress/support/queryLevelTransformerFunction.js packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js +packages/sdk/sdk \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index 39654fd9f9..dae5906124 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -4,7 +4,7 @@ "singleQuote": false, "trailingComma": "es5", "arrowParens": "avoid", - "jsxBracketSameLine": false, + "bracketSameLine": false, "plugins": ["prettier-plugin-svelte"], "svelteSortOrder": "options-scripts-markup-styles" } diff --git a/.vscode/settings.json b/.vscode/settings.json index d471924fe0..4838a4fd89 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,17 @@ "editor.codeActionsOnSave": { "source.fixAll": true }, - "editor.defaultFormatter": "svelte.svelte-vscode" + "editor.defaultFormatter": "svelte.svelte-vscode", + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "debug.javascript.terminalOptions": { + "skipFiles": [ + "${workspaceFolder}/packages/backend-core/node_modules/**", + "/**" + ] + }, } diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 0000000000..21fa517e23 --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +network-timeout 100000 diff --git a/README.md b/README.md index 17a3ab1ef2..bd38610566 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Budibase is open-source - licensed as GPL v3. This should fill you with confiden

### Load data or start from scratch -Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). +Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no datasources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).

Budibase data @@ -135,13 +135,18 @@ You can learn more about the Budibase API at the following places: ## 🏁 Get started - - Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean. Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly. ### [Get started with self-hosting Budibase](https://docs.budibase.com/docs/hosting-methods) +- [Docker - single ARM compatible image](https://docs.budibase.com/docs/docker) +- [Docker Compose](https://docs.budibase.com/docs/docker-compose) +- [Kubernetes](https://docs.budibase.com/docs/kubernetes-k8s) +- [Digital Ocean](https://docs.budibase.com/docs/digitalocean) +- [Portainer](https://docs.budibase.com/docs/portainer) + + ### [Get started with Budibase Cloud](https://budibase.com) @@ -164,7 +169,7 @@ If you have a question or would like to talk with other Budibase users and join ## ❗ Code of conduct -Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). Please read it. +Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/docs/CODE_OF_CONDUCT.md). Please read it.
@@ -174,6 +179,7 @@ Budibase is dedicated to providing a welcoming, diverse, and harrassment-free ex ## 🙌 Contributing to Budibase From opening a bug report to creating a pull request: every contribution is appreciated and welcomed. If you're planning to implement a new feature or change the API please create an issue first. This way we can ensure your work is not in vain. +Environment setup instructions are available for [Debian](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-DEBIAN.md) and [MacOSX](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-MACOSX.md) ### Not Sure Where to Start? A good place to start contributing, is the [First time issues project](https://github.com/Budibase/budibase/projects/22). @@ -187,7 +193,7 @@ Budibase is a monorepo managed by lerna. Lerna manages the building and publishi - [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - The budibase server. This Koa app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system. -For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md) +For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/docs/CONTRIBUTING.md)

@@ -202,7 +208,7 @@ Budibase is open-source, licensed as [GPL v3](https://www.gnu.org/licenses/gpl-3 [![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase) -If you are having issues between updates of the builder, please use the guide [here](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting) to clear down your environment. +If you are having issues between updates of the builder, please use the guide [here](https://github.com/Budibase/budibase/blob/HEAD/docs/CONTRIBUTING.md#troubleshooting) to clear down your environment.

diff --git a/charts/budibase/Chart.yaml b/charts/budibase/Chart.yaml index 694c8c77fe..570aa04d8e 100644 --- a/charts/budibase/Chart.yaml +++ b/charts/budibase/Chart.yaml @@ -11,8 +11,8 @@ sources: - https://github.com/Budibase/budibase - https://budibase.com type: application -version: 0.2.9 -appVersion: 1.0.48 +version: 0.2.11 +appVersion: 1.0.214 dependencies: - name: couchdb version: 3.6.1 diff --git a/charts/budibase/templates/app-service-deployment.yaml b/charts/budibase/templates/app-service-deployment.yaml index 98a949418c..f72d1aef03 100644 --- a/charts/budibase/templates/app-service-deployment.yaml +++ b/charts/budibase/templates/app-service-deployment.yaml @@ -28,6 +28,8 @@ spec: - env: - name: BUDIBASE_ENVIRONMENT value: {{ .Values.globals.budibaseEnv }} + - name: DEPLOYMENT_ENVIRONMENT + value: "kubernetes" - name: COUCH_DB_URL {{ if .Values.services.couchdb.url }} value: {{ .Values.services.couchdb.url }} @@ -76,8 +78,14 @@ spec: key: objectStoreSecret - name: MINIO_URL value: {{ .Values.services.objectStore.url }} + - name: PLUGIN_BUCKET_NAME + value: {{ .Values.services.objectStore.pluginBucketName | default "plugins" | quote }} - name: PORT value: {{ .Values.services.apps.port | quote }} + {{ if .Values.services.worker.publicApiRateLimitPerSecond }} + - name: API_REQ_LIMIT_PER_SEC + value: {{ .Values.globals.apps.publicApiRateLimitPerSecond | quote }} + {{ end }} - name: MULTI_TENANCY value: {{ .Values.globals.multiTenancy | quote }} - name: LOG_LEVEL @@ -116,13 +124,50 @@ spec: value: {{ .Values.globals.automationMaxIterations | quote }} - name: TENANT_FEATURE_FLAGS value: {{ .Values.globals.tenantFeatureFlags | quote }} + {{ if .Values.globals.bbAdminUserEmail }} + - name: BB_ADMIN_USER_EMAIL + value: {{ .Values.globals.bbAdminUserEmail | quote }} + {{ end }} + {{ if .Values.globals.bbAdminUserPassword }} + - name: BB_ADMIN_USER_PASSWORD + value: {{ .Values.globals.bbAdminUserPassword | quote }} + {{ end }} + {{ if .Values.globals.pluginsDir }} + - name: PLUGINS_DIR + value: {{ .Values.globals.pluginsDir | quote }} + {{ end }} + {{ if .Values.services.apps.nodeDebug }} + - name: NODE_DEBUG + value: {{ .Values.services.apps.nodeDebug | quote }} + {{ end }} + {{ if .Values.globals.elasticApmEnabled }} + - name: ELASTIC_APM_ENABLED + value: {{ .Values.globals.elasticApmEnabled | quote }} + {{ end }} + {{ if .Values.globals.elasticApmSecretToken }} + - name: ELASTIC_APM_SECRET_TOKEN + value: {{ .Values.globals.elasticApmSecretToken | quote }} + {{ end }} + {{ if .Values.globals.elasticApmServerUrl }} + - name: ELASTIC_APM_SERVER_URL + value: {{ .Values.globals.elasticApmServerUrl | quote }} + {{ end }} image: budibase/apps:{{ .Values.globals.appVersion }} imagePullPolicy: Always + livenessProbe: + httpGet: + path: /health + port: {{ .Values.services.apps.port }} + initialDelaySeconds: 5 + periodSeconds: 5 name: bbapps ports: - containerPort: {{ .Values.services.apps.port }} - resources: {} + {{ with .Values.services.apps.resources }} + resources: + {{- toYaml . | nindent 10 }} + {{ end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} @@ -131,6 +176,10 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{ if .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.imagePullSecrets | nindent 6 }} + {{ end }} restartPolicy: Always serviceAccountName: "" status: {} diff --git a/charts/budibase/templates/couchdb-backup.yaml b/charts/budibase/templates/couchdb-backup.yaml index ae062475ce..68e5eab617 100644 --- a/charts/budibase/templates/couchdb-backup.yaml +++ b/charts/budibase/templates/couchdb-backup.yaml @@ -38,7 +38,10 @@ spec: image: redgeoff/replicate-couchdb-cluster imagePullPolicy: Always name: couchdb-backup - resources: {} + {{ with .Values.services.couchdb.backup.resources }} + resources: + {{- toYaml . | nindent 10 }} + {{ end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} diff --git a/charts/budibase/templates/minio-service-deployment.yaml b/charts/budibase/templates/minio-service-deployment.yaml index 901fb61ad9..144dbe539a 100644 --- a/charts/budibase/templates/minio-service-deployment.yaml +++ b/charts/budibase/templates/minio-service-deployment.yaml @@ -56,7 +56,10 @@ spec: name: minio-service ports: - containerPort: {{ .Values.services.objectStore.port }} - resources: {} + {{ with .Values.services.objectStore.resources }} + resources: + {{- toYaml . | nindent 10 }} + {{ end }} volumeMounts: - mountPath: /data name: minio-data @@ -68,6 +71,10 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{ if .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.imagePullSecrets | nindent 6 }} + {{ end }} restartPolicy: Always serviceAccountName: "" volumes: @@ -75,4 +82,4 @@ spec: persistentVolumeClaim: claimName: minio-data status: {} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/charts/budibase/templates/proxy-service-deployment.yaml b/charts/budibase/templates/proxy-service-deployment.yaml index bd6a5e311f..5588022032 100644 --- a/charts/budibase/templates/proxy-service-deployment.yaml +++ b/charts/budibase/templates/proxy-service-deployment.yaml @@ -30,7 +30,10 @@ spec: name: proxy-service ports: - containerPort: {{ .Values.services.proxy.port }} - resources: {} + {{ with .Values.services.proxy.resources }} + resources: + {{- toYaml . | nindent 10 }} + {{ end }} volumeMounts: {{- with .Values.affinity }} affinity: @@ -40,6 +43,10 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{ if .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.imagePullSecrets | nindent 6 }} + {{ end }} restartPolicy: Always serviceAccountName: "" volumes: diff --git a/charts/budibase/templates/redis-service-deployment.yaml b/charts/budibase/templates/redis-service-deployment.yaml index 0b6cb12562..d94e4d70f8 100644 --- a/charts/budibase/templates/redis-service-deployment.yaml +++ b/charts/budibase/templates/redis-service-deployment.yaml @@ -35,7 +35,10 @@ spec: name: redis-service ports: - containerPort: {{ .Values.services.redis.port }} - resources: {} + {{ with .Values.services.redis.resources }} + resources: + {{- toYaml . | nindent 10 }} + {{ end }} volumeMounts: - mountPath: /data name: redis-data @@ -47,6 +50,10 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{ if .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.imagePullSecrets | nindent 6 }} + {{ end }} restartPolicy: Always serviceAccountName: "" volumes: @@ -54,4 +61,4 @@ spec: persistentVolumeClaim: claimName: redis-data status: {} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/charts/budibase/templates/worker-service-deployment.yaml b/charts/budibase/templates/worker-service-deployment.yaml index 15ff05e214..b1c6110d95 100644 --- a/charts/budibase/templates/worker-service-deployment.yaml +++ b/charts/budibase/templates/worker-service-deployment.yaml @@ -27,6 +27,10 @@ spec: spec: containers: - env: + - name: BUDIBASE_ENVIRONMENT + value: {{ .Values.globals.budibaseEnv }} + - name: DEPLOYMENT_ENVIRONMENT + value: "kubernetes" - name: CLUSTER_PORT value: {{ .Values.services.worker.port | quote }} {{ if .Values.services.couchdb.enabled }} @@ -73,6 +77,8 @@ spec: key: objectStoreSecret - name: MINIO_URL value: {{ .Values.services.objectStore.url }} + - name: PLUGIN_BUCKET_NAME + value: {{ .Values.services.objectStore.pluginBucketName | default "plugins" | quote }} - name: PORT value: {{ .Values.services.worker.port | quote }} - name: MULTI_TENANCY @@ -91,6 +97,10 @@ spec: value: {{ .Values.globals.selfHosted | quote }} - name: SENTRY_DSN value: {{ .Values.globals.sentryDSN }} + - name: ENABLE_ANALYTICS + value: {{ .Values.globals.enableAnalytics | quote }} + - name: POSTHOG_TOKEN + value: {{ .Values.globals.posthogToken }} - name: ACCOUNT_PORTAL_URL value: {{ .Values.globals.accountPortalUrl | quote }} - name: ACCOUNT_PORTAL_API_KEY @@ -117,12 +127,36 @@ spec: value: {{ .Values.globals.google.clientId | quote }} - name: GOOGLE_CLIENT_SECRET value: {{ .Values.globals.google.secret | quote }} + - name: TENANT_FEATURE_FLAGS + value: {{ .Values.globals.tenantFeatureFlags | quote }} + {{ if .Values.globals.elasticApmEnabled }} + - name: ELASTIC_APM_ENABLED + value: {{ .Values.globals.elasticApmEnabled | quote }} + {{ end }} + {{ if .Values.globals.elasticApmSecretToken }} + - name: ELASTIC_APM_SECRET_TOKEN + value: {{ .Values.globals.elasticApmSecretToken | quote }} + {{ end }} + {{ if .Values.globals.elasticApmServerUrl }} + - name: ELASTIC_APM_SERVER_URL + value: {{ .Values.globals.elasticApmServerUrl | quote }} + {{ end }} + image: budibase/worker:{{ .Values.globals.appVersion }} imagePullPolicy: Always + livenessProbe: + httpGet: + path: /health + port: {{ .Values.services.worker.port }} + initialDelaySeconds: 5 + periodSeconds: 5 name: bbworker ports: - containerPort: {{ .Values.services.worker.port }} - resources: {} + {{ with .Values.services.worker.resources }} + resources: + {{- toYaml . | nindent 10 }} + {{ end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} @@ -131,6 +165,10 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{ if .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.imagePullSecrets | nindent 6 }} + {{ end }} restartPolicy: Always serviceAccountName: "" status: {} diff --git a/charts/budibase/values.yaml b/charts/budibase/values.yaml index 52ead6d076..5c4004cb57 100644 --- a/charts/budibase/values.yaml +++ b/charts/budibase/values.yaml @@ -60,19 +60,6 @@ ingress: port: number: 10000 -resources: - {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - autoscaling: enabled: false minReplicas: 1 @@ -89,9 +76,10 @@ affinity: {} globals: appVersion: "latest" budibaseEnv: PRODUCTION - enableAnalytics: true + tenantFeatureFlags: "*:LICENSING,*:USER_GROUPS" + enableAnalytics: "1" sentryDSN: "" - posthogToken: "" + posthogToken: "phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU" logLevel: info selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs @@ -103,7 +91,7 @@ globals: google: clientId: "" secret: "" - automationMaxIterations: "500" + automationMaxIterations: "200" createSecrets: true # creates an internal API key, JWT secrets and redis password for you @@ -114,6 +102,10 @@ globals: smtp: enabled: false +# elasticApmEnabled: +# elasticApmSecretToken: +# elasticApmServerUrl: + services: budibaseVersion: latest dns: cluster.local @@ -121,15 +113,19 @@ services: proxy: port: 10000 replicaCount: 1 + resources: {} apps: port: 4002 replicaCount: 1 logLevel: info + resources: {} +# nodeDebug: "" # set the value of NODE_DEBUG worker: port: 4003 replicaCount: 1 + resources: {} couchdb: enabled: true @@ -143,6 +139,7 @@ services: target: "" # backup interval in seconds interval: "" + resources: {} redis: enabled: true # disable if using external redis @@ -156,6 +153,7 @@ services: ## If undefined (the default) or set to null, no storageClassName spec is ## set, choosing the default provisioner. storageClass: "" + resources: {} objectStore: minio: true @@ -172,6 +170,7 @@ services: ## If undefined (the default) or set to null, no storageClassName spec is ## set, choosing the default provisioner. storageClass: "" + resources: {} # Override values in couchDB subchart couchdb: diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..1be855e3fa --- /dev/null +++ b/docs/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at community@budibase.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000000..fb0848596c --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,237 @@ +# Contributing + +From opening a bug report to creating a pull request: every contribution is appreciated and welcome. If you're planning to implement a new feature or change the api please [create an issue](https://github.com/Budibase/budibase/issues/new/choose) first. This way we can ensure that your precious work is not in vain. + +## Table of contents + +- [Where to start](#not-sure-where-to-start) +- [Contributor Licence Agreement](#contributor-license-agreement-cla) +- [Glossary of Terms](#glossary-of-terms) +- [Contributing to Budibase](#contributing-to-budibase) + + +## Not Sure Where to Start? + +Budibase is a low-code web application builder that creates svelte-based web applications. + +Budibase is a monorepo managed by [lerna](https://github.com/lerna/lerna). Lerna manages the building and publishing of the budibase packages. At a high level, here are the packages that make up budibase. + +- **packages/builder** - contains code for the budibase builder client side svelte application. + +- **packages/client** - A module that runs in the browser responsible for reading JSON definition and creating living, breathing web apps from it. + +- **packages/server** - The budibase server. This [Koa](https://koajs.com/) app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system. + +- **packages/worker** - This [Koa](https://koajs.com/) app is responsible for providing global apis for managing your budibase installation. Authentication, Users, Email, Org and Auth configs are all provided by the worker. + +## Contributor License Agreement (CLA) + +In order to accept your pull request, we need you to submit a CLA. You only need to do this once. If you are submitting a pull request for the first time, just submit a Pull Request and our CLA Bot will give you instructions on how to sign the CLA before merging your Pull Request. + +All contributors must sign an [Individual Contributor License Agreement](https://github.com/budibase/budibase/blob/next/.github/cla/individual-cla.md). + +If contributing on behalf of your company, your company must sign a [Corporate Contributor License Agreement](https://github.com/budibase/budibase/blob/next/.github/cla/corporate-cla.md). If so, please contact us via community@budibase.com. + +If for any reason, your first contribution is in a PR created by other contributor, please just add a comment to the PR +with the following text to agree our CLA: "I have read the CLA Document and I hereby sign the CLA". + +## Glossary of Terms + +To understand the budibase API, it can be helpful to understand the top level entities that make up Budibase. + +### Client + +A client represents a single budibase customer. Each budibase client will have 1 or more budibase servers. Every client is assigned a unique ID. + +### App + +A client can have one or more budibase applications. Budibase applications would be things like "Developer Inventory Management" or "Goat Herder CRM". Think of a budibase application as a tree. + +### Database + +An App can have one or more databases. Keeping with our [dendrology](https://en.wikipedia.org/wiki/Dendrology) analogy - think of an database as a branch on the tree. Databases are used to keep data separate for different instances of your app. For example, if you had a CRM app, you may create a database for your US office, and a database for your Australian office. Databases allow us to support [multitenancy](https://www.gartner.com/en/information-technology/glossary/multitenancy) in budibase applications. + +### Table + +Tables in budibase are almost akin to tables in relational databases. A table may be a "Car" or an "Employee". They are the main building blocks for the creation and management of backend data in budibase. + +### View + +A View is an advanced feature in budibase that allows you to write a custom query using [MapReduce](https://pouchdb.com/guides/queries.html) queries. Views enable powerful query functionality and calculations, allowing you to do more with your data. + +### Page + +A page in budibase is actually a single, self contained svelte web app. There are only 2 pages in budibase. The **login** page and the **main** page. + +### Screen + +A screen is a component within a single page. Generally, screens represent client side routes, and can be switched without refreshing the page. + +### Component + +A component is the basic frontend building block of a budibase app. + +### Component Library + +Component libraries are collections of components as well as the definition of their props contained in a file called `components.json`. + +## Contributing to Budibase + +* Please maintain the existing code style. + +* Please try to keep your commits small and focused. + +* Please write tests. + +* If the project diverges from your branch, please rebase instead of merging. This makes the commit graph easier to read. + +* Once your work is completed, please raise a PR against the `develop` branch with some information about what has changed and why. + +### Getting Started For Contributors +#### 1. Prerequisites + +NodeJS Version `14.x.x` + +*yarn -* `npm install -g yarn` + +*jest* - `npm install -g jest` + +#### 2. Clone this repository + +`git clone https://github.com/Budibase/budibase.git` + +then `cd ` into your local copy. + +#### 3. Install and Build + +| **NOTE**: On Windows, all yarn commands must be executed on a bash shell (e.g. git bash) + +To develop the Budibase platform you'll need [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) installed. + +##### Quick method + +`yarn setup` will check that all necessary components are installed and setup the repo for usage. + +##### Manual method + +The following commands can be executed to manually get Budibase up and running (assuming Docker/Docker Compose has been installed). + +`yarn` to install project dependencies + +`yarn bootstrap` will install all budibase modules and symlink them together using lerna. + +`yarn build` will build all budibase packages. + +#### 4. Running + +To run the budibase server and builder in dev mode (i.e. with live reloading): + +1. Open a new console +2. `yarn dev` (from root) +3. Access the builder on http://localhost:10000/builder + +This will enable watch mode for both the builder app, server, client library and any component libraries. + +#### 5. Debugging using VS Code + +To debug the budibase server and worker a VS Code launch configuration has been provided. + +Visit the debug window and select `Budibase Server` or `Budibase Worker` to debug the respective component. +Alternatively to start both components simultaneously select `Start Budibase`. + +In addition to the above, the remaining budibase components may be run in dev mode using: `yarn dev:noserver`. + +#### 6. Cleanup + +If you wish to delete all the apps created in development and reset the environment then run the following: + +1. `yarn nuke:docker` will wipe all the Budibase services +2. `yarn dev` will restart all the services + +### Backend + +For the backend we run [Redis](https://redis.io/), [CouchDB](https://couchdb.apache.org/), [MinIO](https://min.io/) and [NGINX](https://www.nginx.com/) in Docker compose. This means that to develop Budibase you will need Docker and Docker compose installed. The backend services are then run separately as Node services with nodemon so that they can be debugged outside of Docker. + +### Data Storage + +When you are running locally, budibase stores data on disk using docker volumes. The volumes and the types of data associated with each are: + +- `redis_data` + - Sessions, email tokens +- `couchdb3_data` + - Global and app databases +- `minio_data` + - App manifest, budibase client, static assets + +### Development Modes + +A combination of environment variables controls the mode budibase runs in. + +| **NOTE**: You need to clean your browser cookies when you change between different modes. + +Yarn commands can be used to mimic the different modes as described in the sections below: + +#### Self Hosted +The default mode. A single tenant installation with no usage restrictions. + +To enable this mode, use: +``` +yarn mode:self +``` + +#### Cloud +The cloud mode, with account portal turned off. + +To enable this mode, use: +``` +yarn mode:cloud +``` +#### Cloud & Account +The cloud mode, with account portal turned on. This is a replica of the mode that runs at https://budibase.app + + +To enable this mode, use: +``` +yarn mode:account +``` +### CI + An overview of the CI pipelines can be found [here](../.github/workflows/README.md) + +### Pro + +@budibase/pro is the closed source package that supports licensed features in budibase. By default the package will be pulled from NPM and will not normally need to be touched in local development. If you require to update code inside the pro package it can be cloned to the same root level as budibase, e.g. + +``` +. +|_ budibase +|_ budibase-pro +``` + +Note that only budibase maintainers will be able to access the pro repo. + +The `yarn bootstrap` command can be used to replace the NPM supplied dependency with the local source aware version. This is achieved using the `yarn link` command. To see specifically how dependencies are linked see [scripts/link-dependencies.sh](../scripts/link-dependencies.sh). The same link script is used to link dependencies to account-portal in local dev. + +### Troubleshooting + +Sometimes, things go wrong. This can be due to incompatible updates on the budibase platform. To clear down your development environment and start again follow **Step 6. Cleanup**, then proceed from **Step 3. Install and Build** in the setup guide above to create a fresh Budibase installation. +### Running tests + +#### End-to-end Tests + +Budibase uses Cypress to run a number of E2E tests. To run the tests execute the following command in the root folder: + +``` +yarn test:e2e +``` + +Or if you are in the builder you can run `yarn cy:test`. + + +### Other Useful Information + +* The contributors are listed in [AUTHORS.md](https://github.com/Budibase/budibase/blob/master/.github/AUTHORS.md) (add yourself). + +* This project uses a modified version of the MPLv2 license, see [LICENSE](https://github.com/budibase/server/blob/master/LICENSE). + +* We use the [C4 (Collective Code Construction Contract)](https://rfc.zeromq.org/spec:42/C4/) process for contributions. + Please read this if you are unfamiliar with it. diff --git a/docs/DEV-SETUP-DEBIAN.md b/docs/DEV-SETUP-DEBIAN.md new file mode 100644 index 0000000000..88a124708c --- /dev/null +++ b/docs/DEV-SETUP-DEBIAN.md @@ -0,0 +1,52 @@ +## Dev Environment on Debian 11 + +### Install Node + +Budibase requires a recent version of node (14+): +``` +curl -sL https://deb.nodesource.com/setup_16.x | sudo bash - +apt -y install nodejs +node -v +``` + +### Install npm requirements + +``` +npm install -g yarn jest lerna +``` +### Install Docker and Docker Compose + +``` +apt install docker.io +pip3 install docker-compose +``` +### Clone the repo +``` +git clone https://github.com/Budibase/budibase.git +``` + +### Check Versions + +This setup process was tested on Debian 11 (bullseye) with version numbers show below. Your mileage may vary using anything else. + +- Docker: 20.10.5 +- Docker-Compose: 1.29.2 +- Node: v16.15.1 +- Yarn: 1.22.19 +- Lerna: 5.1.4 + +### Build + +``` +cd budibase +yarn setup +``` +The yarn setup command runs several build steps i.e. +``` +node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev +``` +So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose. + +The dev version will be available on port 10000 i.e. + +http://127.0.0.1:10000/builder/admin \ No newline at end of file diff --git a/docs/DEV-SETUP-MACOSX.md b/docs/DEV-SETUP-MACOSX.md new file mode 100644 index 0000000000..c5990e58da --- /dev/null +++ b/docs/DEV-SETUP-MACOSX.md @@ -0,0 +1,62 @@ +## Dev Environment on MAC OSX 12 (Monterey) + +### Install Homebrew + +Install instructions [here](https://brew.sh/) + +| **NOTE**: If you are working on a M1 Apple Silicon which is running Z shell, you could need to add +`eval $(/opt/homebrew/bin/brew shellenv)` line to your `.zshrc`. This will make your zsh to find the apps you install +through brew. + + +### Install Node + +Budibase requires a recent version of node (14+): +``` +brew install node npm +node -v +``` + +### Install npm requirements + +``` +npm install -g yarn jest lerna +``` +### Install Docker and Docker Compose + +``` +brew install docker docker-compose +``` +### Clone the repo +``` +git clone https://github.com/Budibase/budibase.git +``` + +### Check Versions + +This setup process was tested on Mac OSX 12 (Monterey) with version numbers shown below. Your mileage may vary using anything else. + +- Docker: 20.10.14 +- Docker-Compose: 2.6.0 +- Node: 18.3.0 +- Yarn: 1.22.19 +- Lerna: 5.1.4 + +### Build + +``` +cd budibase +yarn setup +``` +The yarn setup command runs several build steps i.e. +``` +node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev +``` +So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose. + +The dev version will be available on port 10000 i.e. + +http://127.0.0.1:10000/builder/admin + +| **NOTE**: If you are working on a M1 Apple Silicon, you will need to uncomment `# platform: linux/amd64` line in +[hosting/docker-compose-dev.yaml](../hosting/docker-compose.dev.yaml) \ No newline at end of file diff --git a/examples/nextjs-api-sales/definitions/openapi.ts b/examples/nextjs-api-sales/definitions/openapi.ts index 4f4ad45fc6..7f7f6befec 100644 --- a/examples/nextjs-api-sales/definitions/openapi.ts +++ b/examples/nextjs-api-sales/definitions/openapi.ts @@ -348,7 +348,7 @@ export interface paths { } } responses: { - /** Returns the created table, including the ID which has been generated for it. This can be internal or external data sources. */ + /** Returns the created table, including the ID which has been generated for it. This can be internal or external datasources. */ 200: { content: { "application/json": components["schemas"]["tableOutput"] @@ -959,7 +959,7 @@ export interface components { query: { /** @description The ID of the query. */ _id: string - /** @description The ID of the data source the query belongs to. */ + /** @description The ID of the datasource the query belongs to. */ datasourceId?: string /** @description The bindings which are required to perform this query. */ parameters?: string[] @@ -983,7 +983,7 @@ export interface components { data: { /** @description The ID of the query. */ _id: string - /** @description The ID of the data source the query belongs to. */ + /** @description The ID of the datasource the query belongs to. */ datasourceId?: string /** @description The bindings which are required to perform this query. */ parameters?: string[] diff --git a/examples/nextjs-api-sales/package.json b/examples/nextjs-api-sales/package.json index 6d75c85f01..41ce52e952 100644 --- a/examples/nextjs-api-sales/package.json +++ b/examples/nextjs-api-sales/package.json @@ -11,8 +11,8 @@ "dependencies": { "bulma": "^0.9.3", "next": "12.1.0", - "node-fetch": "^3.2.2", - "node-sass": "^7.0.1", + "node-fetch": "^3.2.10", + "sass": "^1.52.3", "react": "17.0.2", "react-dom": "17.0.2", "react-notifications-component": "^3.4.1" @@ -24,4 +24,4 @@ "eslint-config-next": "12.1.0", "typescript": "4.6.2" } -} +} \ No newline at end of file diff --git a/examples/nextjs-api-sales/yarn.lock b/examples/nextjs-api-sales/yarn.lock index 52c89967b2..f47fb84e33 100644 --- a/examples/nextjs-api-sales/yarn.lock +++ b/examples/nextjs-api-sales/yarn.lock @@ -2020,10 +2020,10 @@ node-domexception@^1.0.0: resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== -node-fetch@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.2.tgz#16d33fbe32ca7c6ca1ca8ba5dfea1dd885c59f04" - integrity sha512-Cwhq1JFIoon15wcIkFzubVNFE5GvXGV82pKf4knXXjvGmn7RJKcypeuqcVNZMGDZsAFWyIRya/anwAJr7TWJ7w== +node-fetch@^3.2.10: + version "3.2.10" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.10.tgz#e8347f94b54ae18b57c9c049ef641cef398a85c8" + integrity sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA== dependencies: data-uri-to-buffer "^4.0.0" fetch-blob "^3.1.4" diff --git a/hosting/.env b/hosting/.env index 39df76d01e..c5638a266f 100644 --- a/hosting/.env +++ b/hosting/.env @@ -18,4 +18,11 @@ MINIO_PORT=4004 COUCH_DB_PORT=4005 REDIS_PORT=6379 WATCHTOWER_PORT=6161 -BUDIBASE_ENVIRONMENT=PRODUCTION \ No newline at end of file +BUDIBASE_ENVIRONMENT=PRODUCTION + +# 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 diff --git a/hosting/docker-compose.dev.yaml b/hosting/docker-compose.dev.yaml index be0bc74a26..7322b0e8a9 100644 --- a/hosting/docker-compose.dev.yaml +++ b/hosting/docker-compose.dev.yaml @@ -11,10 +11,11 @@ services: - minio_data:/data ports: - "${MINIO_PORT}:9000" + - "9001:9001" environment: MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} - command: server /data + command: server /data --console-address ":9001" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml index f9d9eaf1c5..5b2adc2665 100644 --- a/hosting/docker-compose.yaml +++ b/hosting/docker-compose.yaml @@ -23,9 +23,14 @@ services: ENABLE_ANALYTICS: "true" REDIS_URL: redis-service:6379 REDIS_PASSWORD: ${REDIS_PASSWORD} + BB_ADMIN_USER_EMAIL: ${BB_ADMIN_USER_EMAIL} + BB_ADMIN_USER_PASSWORD: ${BB_ADMIN_USER_PASSWORD} + PLUGINS_DIR: ${PLUGINS_DIR} depends_on: - worker-service - redis-service +# volumes: +# - /some/path/to/plugins:/plugins worker-service: restart: unless-stopped @@ -61,7 +66,7 @@ services: MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} MINIO_BROWSER: "off" - command: server /data + command: server /data --console-address ":9001" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s @@ -74,6 +79,9 @@ services: - "${MAIN_PORT}:10000" container_name: bbproxy image: budibase/proxy + environment: + - PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10 + - PROXY_RATE_LIMIT_API_PER_SECOND=20 depends_on: - minio-service - worker-service diff --git a/hosting/hosting.properties b/hosting/hosting.properties index c8e2f5c606..c5638a266f 100644 --- a/hosting/hosting.properties +++ b/hosting/hosting.properties @@ -19,3 +19,10 @@ COUCH_DB_PORT=4005 REDIS_PORT=6379 WATCHTOWER_PORT=6161 BUDIBASE_ENVIRONMENT=PRODUCTION + +# 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 diff --git a/hosting/letsencrypt/certificate-renew.sh b/hosting/letsencrypt/certificate-renew.sh new file mode 100644 index 0000000000..df88b44322 --- /dev/null +++ b/hosting/letsencrypt/certificate-renew.sh @@ -0,0 +1,13 @@ +#!/bin/bash +CUSTOM_DOMAIN="$1" + +if [[ ! -z "${CUSTOM_DOMAIN}" ]]; then + certbot certonly --webroot --webroot-path="/var/www/html" \ + --register-unsafely-without-email \ + --domains $CUSTOM_DOMAIN \ + --rsa-key-size 4096 \ + --agree-tos \ + --force-renewal + + nginx -s reload +fi diff --git a/hosting/letsencrypt/certificate-request.sh b/hosting/letsencrypt/certificate-request.sh new file mode 100644 index 0000000000..83f314fc88 --- /dev/null +++ b/hosting/letsencrypt/certificate-request.sh @@ -0,0 +1,23 @@ +#!/bin/bash +CUSTOM_DOMAIN="$1" +# Request from Lets Encrypt +certbot certonly --webroot --webroot-path="/var/www/html" \ + --register-unsafely-without-email \ + --domains $CUSTOM_DOMAIN \ + --rsa-key-size 4096 \ + --agree-tos \ + --force-renewal + +if (($? != 0)); then + echo "ERROR: certbot request failed for $CUSTOM_DOMAIN use http on port 80 - exiting" + exit 1 +else + cp /app/letsencrypt/options-ssl-nginx.conf /etc/letsencrypt/options-ssl-nginx.conf + cp /app/letsencrypt/ssl-dhparams.pem /etc/letsencrypt/ssl-dhparams.pem + cp /app/letsencrypt/nginx-ssl.conf /etc/nginx/sites-available/nginx-ssl.conf + sed -i "s/CUSTOM_DOMAIN/$CUSTOM_DOMAIN/g" /etc/nginx/sites-available/nginx-ssl.conf + ln -s /etc/nginx/sites-available/nginx-ssl.conf /etc/nginx/sites-enabled/nginx-ssl.conf + + echo "INFO: restart nginx after certbot request" + /etc/init.d/nginx restart +fi diff --git a/hosting/letsencrypt/nginx-ssl.conf b/hosting/letsencrypt/nginx-ssl.conf new file mode 100644 index 0000000000..50c5e0198a --- /dev/null +++ b/hosting/letsencrypt/nginx-ssl.conf @@ -0,0 +1,96 @@ +server { + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + server_name _; + ssl_certificate /etc/letsencrypt/live/CUSTOM_DOMAIN/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/CUSTOM_DOMAIN/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + client_max_body_size 1000m; + ignore_invalid_headers off; + proxy_buffering off; + # port_in_redirect off; + + location ^~ /.well-known/acme-challenge/ { + default_type "text/plain"; + root /var/www/html; + break; + } + location = /.well-known/acme-challenge/ { + return 404; + } + + location /app { + proxy_pass http://127.0.0.1:4001; + } + + location = / { + proxy_pass http://127.0.0.1:4001; + } + + location ~ ^/(builder|app_) { + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://127.0.0.1:4001; + } + + location ~ ^/api/(system|admin|global)/ { + proxy_pass http://127.0.0.1:4002; + } + + location /worker/ { + proxy_pass http://127.0.0.1:4002; + rewrite ^/worker/(.*)$ /$1 break; + } + + location /api/ { + # calls to the API are rate limited with bursting + limit_req zone=ratelimit burst=20 nodelay; + + # 120s timeout on API requests + proxy_read_timeout 120s; + proxy_connect_timeout 120s; + proxy_send_timeout 120s; + + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_pass http://127.0.0.1:4001; + } + + location /db/ { + proxy_pass http://127.0.0.1:5984; + rewrite ^/db/(.*)$ /$1 break; + } + + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 300; + proxy_http_version 1.1; + proxy_set_header Connection ""; + chunked_transfer_encoding off; + proxy_pass http://127.0.0.1:9000; + } + + client_header_timeout 60; + client_body_timeout 60; + keepalive_timeout 60; + + # gzip + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; + +} diff --git a/hosting/letsencrypt/options-ssl-nginx.conf b/hosting/letsencrypt/options-ssl-nginx.conf new file mode 100644 index 0000000000..52fdfde245 --- /dev/null +++ b/hosting/letsencrypt/options-ssl-nginx.conf @@ -0,0 +1,13 @@ +# This file contains important security parameters. If you modify this file +# manually, Certbot will be unable to automatically provide future security +# updates. Instead, Certbot will print and log an error message with a path to +# the up-to-date file that you will need to refer to when manually updating +# this file. + +ssl_session_cache shared:le_nginx_SSL:10m; +ssl_session_timeout 1440m; + +ssl_protocols TLSv1.2 TLSv1.3; +ssl_prefer_server_ciphers off; + +ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; diff --git a/hosting/letsencrypt/ssl-dhparams.pem b/hosting/letsencrypt/ssl-dhparams.pem new file mode 100644 index 0000000000..088f9673dc --- /dev/null +++ b/hosting/letsencrypt/ssl-dhparams.pem @@ -0,0 +1,8 @@ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz ++8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a +87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7 +YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi +7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD +ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg== +-----END DH PARAMETERS----- \ No newline at end of file diff --git a/hosting/nginx.dev.conf.hbs b/hosting/nginx.dev.conf.hbs index 9398b7e719..14c32b1bba 100644 --- a/hosting/nginx.dev.conf.hbs +++ b/hosting/nginx.dev.conf.hbs @@ -15,7 +15,10 @@ http { log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; + '"$http_user_agent" "$http_x_forwarded_for" ' + 'response_time=$upstream_response_time proxy_host=$proxy_host upstream_addr=$upstream_addr'; + + access_log /var/log/nginx/access.log main; map $http_upgrade $connection_upgrade { default "upgrade"; @@ -77,6 +80,20 @@ http { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } + location /vite/ { + proxy_pass http://{{ address }}:3000; + rewrite ^/vite(.*)$ /$1 break; + } + + location /socket/ { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_pass http://{{ address }}:4001; + } + location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/hosting/nginx.prod.conf.hbs b/hosting/nginx.prod.conf.hbs index a127dfbd5c..f3202ad4a4 100644 --- a/hosting/nginx.prod.conf.hbs +++ b/hosting/nginx.prod.conf.hbs @@ -9,7 +9,11 @@ events { } http { - limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s; + # rate limiting + limit_req_status 429; + limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=${PROXY_RATE_LIMIT_API_PER_SECOND}r/s; + limit_req_zone $binary_remote_addr zone=webhooks:10m rate=${PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND}r/s; + include /etc/nginx/mime.types; default_type application/octet-stream; proxy_set_header Host $host; @@ -29,7 +33,10 @@ http { log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; + '"$http_user_agent" "$http_x_forwarded_for" ' + 'response_time=$upstream_response_time proxy_host=$proxy_host upstream_addr=$upstream_addr'; + + access_log /var/log/nginx/access.log main; map $http_upgrade $connection_upgrade { default "upgrade"; @@ -48,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://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.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"; + set $csp_connect "connect-src 'self' 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"; 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:"; @@ -90,6 +97,7 @@ http { proxy_pass http://$watchtower:8080; } {{/if}} + location ~ ^/(builder|app_) { proxy_http_version 1.1; proxy_set_header Connection $connection_upgrade; @@ -126,11 +134,39 @@ http { proxy_pass http://$apps:4002; } + location /api/webhooks/ { + # calls to webhooks are rate limited + limit_req zone=webhooks nodelay; + + # Rest of configuration copied from /api/ location above + # 120s timeout on API requests + proxy_read_timeout 120s; + proxy_connect_timeout 120s; + proxy_send_timeout 120s; + + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_pass http://$apps:4002; + } + location /db/ { proxy_pass http://$couchdb:5984; rewrite ^/db/(.*)$ /$1 break; } + location /socket/ { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_pass http://$apps:4002; + } + location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/hosting/proxy/Dockerfile b/hosting/proxy/Dockerfile index a2b17d3333..298762aaf1 100644 --- a/hosting/proxy/Dockerfile +++ b/hosting/proxy/Dockerfile @@ -1,3 +1,14 @@ FROM nginx:latest -COPY .generated-nginx.prod.conf /etc/nginx/nginx.conf -COPY error.html /usr/share/nginx/html/error.html \ No newline at end of file + +# nginx.conf +# use the default nginx behaviour for *.template files which are processed with envsubst +# override the output dir to output directly to /etc/nginx instead of /etc/nginx/conf.d +ENV NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx +COPY .generated-nginx.prod.conf /etc/nginx/templates/nginx.conf.template + +# Error handling +COPY error.html /usr/share/nginx/html/error.html + +# Default environment +ENV PROXY_RATE_LIMIT_WEBHOOKS_PER_SECOND=10 +ENV PROXY_RATE_LIMIT_API_PER_SECOND=20 \ No newline at end of file diff --git a/hosting/scripts/build-target-paths.sh b/hosting/scripts/build-target-paths.sh new file mode 100644 index 0000000000..c974d9a304 --- /dev/null +++ b/hosting/scripts/build-target-paths.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +echo ${TARGETBUILD} > /buildtarget.txt +if [[ "${TARGETBUILD}" = "aas" ]]; then + # Azure AppService uses /home for persisent data & SSH on port 2222 + DATA_DIR=/home + 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/Dockerfile b/hosting/single/Dockerfile index 2123d237b5..f34290f627 100644 --- a/hosting/single/Dockerfile +++ b/hosting/single/Dockerfile @@ -1,97 +1,139 @@ -FROM couchdb +FROM node:14-slim as build -ENV COUCHDB_PASSWORD=budibase -ENV COUCHDB_USER=budibase -ENV COUCH_DB_URL=http://budibase:budibase@localhost:5984 -ENV BUDIBASE_ENVIRONMENT=PRODUCTION -ENV MINIO_URL=http://localhost:9000 -ENV REDIS_URL=localhost:6379 -ENV WORKER_URL=http://localhost:4002 -ENV INTERNAL_API_KEY=budibase -ENV JWT_SECRET=testsecret -ENV MINIO_ACCESS_KEY=budibase -ENV MINIO_SECRET_KEY=budibase -ENV SELF_HOSTED=1 -ENV CLUSTER_PORT=10000 -ENV REDIS_PASSWORD=budibase -ENV ARCHITECTURE=amd -ENV APP_PORT=4001 -ENV WORKER_PORT=4002 +# install node-gyp dependencies +RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python -RUN apt-get update -RUN apt-get install software-properties-common wget nginx -y -RUN apt-add-repository 'deb http://security.debian.org/debian-security stretch/updates main' -RUN apt-get update +# add pin script +WORKDIR / +ADD scripts/pinVersions.js scripts/cleanup.sh ./ +RUN chmod +x /cleanup.sh -# setup nginx -ADD hosting/single/nginx.conf /etc/nginx -RUN mkdir /etc/nginx/logs -RUN useradd www -RUN touch /etc/nginx/logs/error.log -RUN touch /etc/nginx/logs/nginx.pid - -# install java -RUN apt-get install openjdk-8-jdk -y - -# setup nodejs -WORKDIR /nodejs -RUN curl -sL https://deb.nodesource.com/setup_16.x -o /tmp/nodesource_setup.sh -RUN bash /tmp/nodesource_setup.sh -RUN apt-get install nodejs -RUN npm install --global yarn -RUN npm install --global pm2 - -# setup redis -RUN apt install redis-server -y - -# setup server +# build server WORKDIR /app ADD packages/server . -RUN ls -al -RUN yarn -RUN yarn build -# Install client for oracle datasource -RUN apt-get install unzip libaio1 -RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh +RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh -# setup worker +# build worker WORKDIR /worker ADD packages/worker . -RUN yarn -RUN yarn build +RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh + +FROM couchdb:3.2.1 +# TARGETARCH can be amd64 or arm e.g. docker build --build-arg TARGETARCH=amd64 +ARG TARGETARCH=amd64 +#TARGETBUILD can be set to single (for single docker image) or aas (for azure app service) +# e.g. docker build --build-arg TARGETBUILD=aas .... +ARG TARGETBUILD=single +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-add-repository 'deb http://security.debian.org/debian-security stretch/updates main' && \ + apt-get update + +# install other dependencies, nodejs, oracle requirements, jdk8, redis, nginx +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 && \ + npm install --global yarn pm2 + +# setup nginx +ADD hosting/single/nginx/nginx.conf /etc/nginx +ADD hosting/single/nginx/nginx-default-site.conf /etc/nginx/sites-enabled/default +RUN mkdir -p /var/log/nginx && \ + touch /var/log/nginx/error.log && \ + touch /var/run/nginx.pid + +WORKDIR / +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 -RUN unzip clouseau-2.21.0-dist.zip -RUN mv clouseau-2.21.0 /opt/clouseau -RUN rm clouseau-2.21.0-dist.zip +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 ./bin/ -ADD hosting/single/log4j.properties . -ADD hosting/single/clouseau.ini . +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/vm.args ./etc/ +ADD hosting/single/couch/vm.args hosting/single/couch/local.ini ./etc/ # setup minio WORKDIR /minio -RUN wget https://dl.min.io/server/minio/release/linux-${ARCHITECTURE}64/minio -RUN chmod +x minio +ADD scripts/install-minio.sh ./install.sh +RUN chmod +x install.sh && ./install.sh # setup runner file WORKDIR / ADD hosting/single/runner.sh . RUN chmod +x ./runner.sh +ADD hosting/single/healthcheck.sh . +RUN chmod +x ./healthcheck.sh -EXPOSE 10000 -VOLUME /opt/couchdb/data -VOLUME /minio +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/ +ADD hosting/single/ssh/ssh_setup.sh /tmp +RUN /build-target-paths.sh + +# cleanup cache +RUN yarn cache clean -f + +EXPOSE 80 +EXPOSE 443 +# Expose port 2222 for SSH on Azure App Service build +EXPOSE 2222 +VOLUME /data + +# setup letsencrypt certificate +RUN apt-get install -y certbot python3-certbot-nginx +ADD hosting/letsencrypt /app/letsencrypt +RUN chmod +x /app/letsencrypt/certificate-request.sh /app/letsencrypt/certificate-renew.sh +# Remove cached files +RUN rm -rf \ + /root/.cache \ + /root/.npm \ + /root/.pip \ + /usr/local/share/doc \ + /usr/share/doc \ + /usr/share/man \ + /var/lib/apt/lists/* \ + /tmp/* + +HEALTHCHECK --interval=15s --timeout=15s --start-period=45s CMD "/healthcheck.sh" # must set this just before running ENV NODE_ENV=production +WORKDIR / + CMD ["./runner.sh"] diff --git a/hosting/single/README.md b/hosting/single/README.md new file mode 100644 index 0000000000..1147d55c89 --- /dev/null +++ b/hosting/single/README.md @@ -0,0 +1,112 @@ +# Docker Single Image for Budibase + +## Overview +As an alternative to running several docker containers via docker-compose, the files under ./hosting/single can be used to build a docker image containing all of the Budibase components (minio, couch, clouseau etc). +We call this the 'single image' container as the Dockerfile adds all the components to a single docker image. + +## Usage + +- Amend Environment Variables +- Build Requirements +- Build the Image +- Run the Container + +### Amend Environment Variables + +Edit the Dockerfile in this directory amending the environment variables to suit your usage. Pay particular attention to changing passwords. +The CUSTOM_DOMAIN variable will be used to request a certificate from LetsEncrypt and if successful you can point traffic to port 443. If you choose to use the CUSTOM_DOMAIN variable ensure that the DNS for your custom domain points to the public IP address where you are running Budibase - otherwise the certificate issuance will fail. +If you have other arrangements for a proxy in front of the single image container you can omit the CUSTOM_DOMAIN environment variable and the request to LetsEncrypt will be skipped. You can then point traffic to port 80. + +### Build Requirements +We would suggest building the image with 6GB of RAM and 20GB of free disk space for build artifacts. The resulting image size will use approx 2GB of disk space. + +### 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:single +``` +If the docker build step fails try running that step again manually with: +``` +docker build --build-arg TARGETARCH=amd --no-cache -t budibase:latest -f ./hosting/single/Dockerfile . +``` + +#### Azure App Services +Azure have some specific requirements for running a container in their App Service. Specifically, installation of SSH to port 2222 and data storage under /home. If you would like to build a budibase container for Azure App Service add the build argument shown below setting it to 'aas'. You can remove the CUSTOM_DOMAIN env variable from the Dockerfile too as Azure terminate SSL before requests reach the container. +``` +docker build --build-arg TARGETARCH=amd --build-arg TARGETBUILD=aas -t budibase:latest -f ./hosting/single/Dockerfile . +``` + +### Run the Container +``` +docker run -d -p 80:80 -p 443:443 --name budibase budibase:latest +``` +Where: +- -d runs the container in detached mode +- -p forwards ports from your host to the ports inside the container. If you are already using port 80 on your host for something else you can try running with an alternative port e.g. `-p 8080:80` +- --name is the name for the container as shown in `docker ps` and can be used with other docker commands e.g. `docker restart budibase` + +When the container runs you should be able to access the container over http at your host address e.g. http://1.2.3.4/ or using your custom domain e.g. https://my.custom.domain/ + +When the Budibase UI appears you will be prompted to create an account to get started. + +### Podman +The single image container builds fine when using podman in place of docker. You may be prompted for the registry to use for the CouchDB image and the HEALTHCHECK parameter is not OCI compliant so is ignored. + +### Check +There are many things that could go wrong so if your container is not building or running as expected please check the following before opening a support issue. +Verify the healthcheck status of the container: +``` +docker ps +``` +Check the container logs: +``` +docker logs budibase +``` + +### Support +This single image build is still a work-in-progress so if you open an issue please provide the following information: +- The OS and OS version you are building on +- The versions you are using of docker, docker-compose, yarn, node, lerna +- For build errors please provide zipped output +- For container errors please provide zipped container logs diff --git a/hosting/single/clouseau b/hosting/single/clouseau/clouseau similarity index 100% rename from hosting/single/clouseau rename to hosting/single/clouseau/clouseau diff --git a/hosting/single/clouseau.ini b/hosting/single/clouseau/clouseau.ini similarity index 92% rename from hosting/single/clouseau.ini rename to hosting/single/clouseau/clouseau.ini index f086cf0398..578a5acafa 100644 --- a/hosting/single/clouseau.ini +++ b/hosting/single/clouseau/clouseau.ini @@ -7,7 +7,7 @@ name=clouseau@127.0.0.1 cookie=monster ; the path where you would like to store the search index files -dir=/opt/couchdb/data/search +dir=DATA_DIR/search ; the number of search indexes that can be open simultaneously max_indexes_open=500 diff --git a/hosting/single/log4j.properties b/hosting/single/clouseau/log4j.properties similarity index 100% rename from hosting/single/log4j.properties rename to hosting/single/clouseau/log4j.properties diff --git a/hosting/single/couch/local.ini b/hosting/single/couch/local.ini new file mode 100644 index 0000000000..266c0d4b60 --- /dev/null +++ b/hosting/single/couch/local.ini @@ -0,0 +1,5 @@ +; CouchDB Configuration Settings + +[couchdb] +database_dir = DATA_DIR/couch/dbs +view_index_dir = DATA_DIR/couch/views diff --git a/hosting/single/vm.args b/hosting/single/couch/vm.args similarity index 100% rename from hosting/single/vm.args rename to hosting/single/couch/vm.args diff --git a/hosting/single/healthcheck.sh b/hosting/single/healthcheck.sh new file mode 100644 index 0000000000..592b3e94fa --- /dev/null +++ b/hosting/single/healthcheck.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +healthy=true + +if [ -f "/data/.env" ]; then + export $(cat /data/.env | xargs) +elif [ -f "/home/.env" ]; then + export $(cat /home/.env | xargs) +else + echo "No .env file found" + healthy=false +fi + +if [[ $(curl -Lfk -s -w "%{http_code}\n" http://localhost/ -o /dev/null) -ne 200 ]]; then + echo 'ERROR: Budibase is not running'; + healthy=false +fi + +if [[ $(curl -s -w "%{http_code}\n" http://localhost:4001/health -o /dev/null) -ne 200 ]]; then + echo 'ERROR: Budibase backend is not running'; + healthy=false +fi + +if [[ $(curl -s -w "%{http_code}\n" http://localhost:4002/health -o /dev/null) -ne 200 ]]; then + echo 'ERROR: Budibase worker is not running'; + healthy=false +fi + +if [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/ -o /dev/null) -ne 200 ]]; then + echo 'ERROR: CouchDB is not running'; + healthy=false +fi +if [[ $(redis-cli -a $REDIS_PASSWORD --no-auth-warning ping) != 'PONG' ]]; then + echo 'ERROR: Redis is down'; + healthy=false +fi +# mino, clouseau, +nginx -t -q +NGINX_STATUS=$? + +if [[ $NGINX_STATUS -gt 0 ]]; then + echo 'ERROR: Nginx config problem'; + healthy=false +fi + +if [ $healthy == true ]; then + exit 0 +else + exit 1 +fi diff --git a/hosting/single/nginx.conf b/hosting/single/nginx.conf deleted file mode 100644 index 86938ced4e..0000000000 --- a/hosting/single/nginx.conf +++ /dev/null @@ -1,116 +0,0 @@ -user www www; -error_log /etc/nginx/logs/error.log; -pid /etc/nginx/logs/nginx.pid; -worker_processes auto; -worker_rlimit_nofile 8192; - -events { - worker_connections 1024; -} - -http { - limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s; - proxy_set_header Host $host; - charset utf-8; - sendfile on; - tcp_nopush on; - tcp_nodelay on; - server_tokens off; - types_hash_max_size 2048; - - # buffering - client_header_buffer_size 1k; - client_max_body_size 20M; - ignore_invalid_headers off; - proxy_buffering off; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - map $http_upgrade $connection_upgrade { - default "upgrade"; - } - - server { - listen 10000 default_server; - listen [::]:10000 default_server; - server_name _; - client_max_body_size 1000m; - ignore_invalid_headers off; - proxy_buffering off; - # port_in_redirect off; - - location /app { - proxy_pass http://127.0.0.1:4001; - } - - location = / { - proxy_pass http://127.0.0.1:4001; - } - - location ~ ^/(builder|app_) { - proxy_http_version 1.1; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_pass http://127.0.0.1:4001; - } - - location ~ ^/api/(system|admin|global)/ { - proxy_pass http://127.0.0.1:4002; - } - - location /worker/ { - proxy_pass http://127.0.0.1:4002; - rewrite ^/worker/(.*)$ /$1 break; - } - - location /api/ { - # calls to the API are rate limited with bursting - limit_req zone=ratelimit burst=20 nodelay; - - # 120s timeout on API requests - proxy_read_timeout 120s; - proxy_connect_timeout 120s; - proxy_send_timeout 120s; - - proxy_http_version 1.1; - proxy_set_header Connection $connection_upgrade; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - proxy_pass http://127.0.0.1:4001; - } - - location /db/ { - proxy_pass http://127.0.0.1:5984; - rewrite ^/db/(.*)$ /$1 break; - } - - location / { - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_connect_timeout 300; - proxy_http_version 1.1; - proxy_set_header Connection ""; - chunked_transfer_encoding off; - proxy_pass http://127.0.0.1:9000; - } - - client_header_timeout 60; - client_body_timeout 60; - keepalive_timeout 60; - - # gzip - gzip on; - gzip_vary on; - gzip_proxied any; - gzip_comp_level 6; - gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; - } -} diff --git a/hosting/single/nginx/nginx-default-site.conf b/hosting/single/nginx/nginx-default-site.conf new file mode 100644 index 0000000000..bd89e21251 --- /dev/null +++ b/hosting/single/nginx/nginx-default-site.conf @@ -0,0 +1,100 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + client_max_body_size 1000m; + ignore_invalid_headers off; + proxy_buffering off; + # port_in_redirect off; + + location ^~ /.well-known/acme-challenge/ { + default_type "text/plain"; + root /var/www/html; + break; + } + location = /.well-known/acme-challenge/ { + return 404; + } + + location /app { + proxy_pass http://127.0.0.1:4001; + } + + location = / { + proxy_pass http://127.0.0.1:4001; + } + + location ~ ^/(builder|app_) { + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass http://127.0.0.1:4001; + } + + location ~ ^/api/(system|admin|global)/ { + proxy_pass http://127.0.0.1:4002; + } + + location /worker/ { + proxy_pass http://127.0.0.1:4002; + rewrite ^/worker/(.*)$ /$1 break; + } + + location /api/ { + # calls to the API are rate limited with bursting + limit_req zone=ratelimit burst=20 nodelay; + + # 120s timeout on API requests + proxy_read_timeout 120s; + proxy_connect_timeout 120s; + proxy_send_timeout 120s; + + proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_pass http://127.0.0.1:4001; + } + + location /db/ { + proxy_pass http://127.0.0.1:5984; + rewrite ^/db/(.*)$ /$1 break; + } + + location /socket/ { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_pass http://127.0.0.1:4001; + } + + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 300; + proxy_http_version 1.1; + proxy_set_header Connection ""; + chunked_transfer_encoding off; + proxy_pass http://127.0.0.1:9000; + } + + client_header_timeout 60; + client_body_timeout 60; + keepalive_timeout 60; + + # gzip + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; +} diff --git a/hosting/single/nginx/nginx.conf b/hosting/single/nginx/nginx.conf new file mode 100644 index 0000000000..1e5d1c20d2 --- /dev/null +++ b/hosting/single/nginx/nginx.conf @@ -0,0 +1,37 @@ +user www-data www-data; +error_log /var/log/nginx/error.log; +pid /var/run/nginx.pid; +worker_processes auto; +worker_rlimit_nofile 8192; + +events { + worker_connections 1024; +} + +http { + limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s; + proxy_set_header Host $host; + charset utf-8; + sendfile on; + tcp_nopush on; + tcp_nodelay on; + server_tokens off; + types_hash_max_size 2048; + + # buffering + client_header_buffer_size 1k; + client_max_body_size 20M; + ignore_invalid_headers off; + proxy_buffering off; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + map $http_upgrade $connection_upgrade { + default "upgrade"; + } + + include /etc/nginx/sites-enabled/*; + +} diff --git a/hosting/single/runner.sh b/hosting/single/runner.sh index fab8431796..e02b33d771 100644 --- a/hosting/single/runner.sh +++ b/hosting/single/runner.sh @@ -1,7 +1,83 @@ +#!/bin/bash +declare -a ENV_VARS=("COUCHDB_USER" "COUCHDB_PASSWORD" "DATA_DIR" "MINIO_ACCESS_KEY" "MINIO_SECRET_KEY" "INTERNAL_API_KEY" "JWT_SECRET" "REDIS_PASSWORD") +declare -a DOCKER_VARS=("APP_PORT" "APPS_URL" "ARCHITECTURE" "BUDIBASE_ENVIRONMENT" "CLUSTER_PORT" "DEPLOYMENT_ENVIRONMENT" "MINIO_URL" "NODE_ENV" "POSTHOG_TOKEN" "REDIS_URL" "SELF_HOSTED" "WORKER_PORT" "WORKER_URL" "TENANT_FEATURE_FLAGS" "ACCOUNT_PORTAL_URL") +# Check the env vars set in Dockerfile have come through, AAS seems to drop them +[[ -z "${APP_PORT}" ]] && export APP_PORT=4001 +[[ -z "${ARCHITECTURE}" ]] && export ARCHITECTURE=amd +[[ -z "${BUDIBASE_ENVIRONMENT}" ]] && export BUDIBASE_ENVIRONMENT=PRODUCTION +[[ -z "${CLUSTER_PORT}" ]] && export CLUSTER_PORT=80 +[[ -z "${DEPLOYMENT_ENVIRONMENT}" ]] && export DEPLOYMENT_ENVIRONMENT=docker +[[ -z "${MINIO_URL}" ]] && export MINIO_URL=http://localhost:9000 +[[ -z "${NODE_ENV}" ]] && export NODE_ENV=production +[[ -z "${POSTHOG_TOKEN}" ]] && export POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU +[[ -z "${TENANT_FEATURE_FLAGS}" ]] && export TENANT_FEATURE_FLAGS="*:LICENSING,*:USER_GROUPS" +[[ -z "${ACCOUNT_PORTAL_URL}" ]] && export ACCOUNT_PORTAL_URL=https://account.budibase.app +[[ -z "${REDIS_URL}" ]] && export REDIS_URL=localhost:6379 +[[ -z "${SELF_HOSTED}" ]] && export SELF_HOSTED=1 +[[ -z "${WORKER_PORT}" ]] && export WORKER_PORT=4002 +[[ -z "${WORKER_URL}" ]] && export WORKER_URL=http://localhost:4002 +[[ -z "${APPS_URL}" ]] && export APPS_URL=http://localhost:4001 +# export CUSTOM_DOMAIN=budi001.custom.com +# Azure App Service customisations +if [[ "${TARGETBUILD}" = "aas" ]]; then + DATA_DIR=/home + /etc/init.d/ssh start +else + DATA_DIR=${DATA_DIR:-/data} +fi + +if [ -f "${DATA_DIR}/.env" ]; then + # Read in the .env file and export the variables + for LINE in $(cat ${DATA_DIR}/.env); do export $LINE; done +fi +# randomise any unset environment variables +for ENV_VAR in "${ENV_VARS[@]}" +do + temp=$(eval "echo \$$ENV_VAR") + if [[ -z "${temp}" ]]; then + eval "export $ENV_VAR=$(uuidgen | sed -e 's/-//g')" + fi +done +if [[ -z "${COUCH_DB_URL}" ]]; then + export COUCH_DB_URL=http://$COUCHDB_USER:$COUCHDB_PASSWORD@localhost:5984 +fi +if [ ! -f "${DATA_DIR}/.env" ]; then + touch ${DATA_DIR}/.env + for ENV_VAR in "${ENV_VARS[@]}" + do + temp=$(eval "echo \$$ENV_VAR") + echo "$ENV_VAR=$temp" >> ${DATA_DIR}/.env + done + for ENV_VAR in "${DOCKER_VARS[@]}" + do + temp=$(eval "echo \$$ENV_VAR") + echo "$ENV_VAR=$temp" >> ${DATA_DIR}/.env + done + echo "COUCH_DB_URL=${COUCH_DB_URL}" >> ${DATA_DIR}/.env +fi + +# Read in the .env file and export the variables +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 & /opt/clouseau/bin/clouseau & -/minio/minio server /minio & +/minio/minio server ${DATA_DIR}/minio & /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 + echo -n "* * 2 * * root exec /app/letsencrypt/certificate-renew.sh ${CUSTOM_DOMAIN}" >> /etc/cron.d/certificate-renew + chmod +x /etc/cron.d/certificate-renew + # Request the certbot certificate + /app/letsencrypt/certificate-request.sh ${CUSTOM_DOMAIN} +fi + /etc/init.d/nginx restart pushd app pm2 start --name app "yarn run:docker" @@ -10,7 +86,6 @@ pushd worker pm2 start --name worker "yarn run:docker" popd sleep 10 -URL=http://${COUCHDB_USER}:${COUCHDB_PASSWORD}@localhost:5984 -curl -X PUT ${URL}/_users -curl -X PUT ${URL}/_replicator -sleep infinity \ No newline at end of file +curl -X PUT ${COUCH_DB_URL}/_users +curl -X PUT ${COUCH_DB_URL}/_replicator +sleep infinity diff --git a/hosting/single/ssh/ssh_setup.sh b/hosting/single/ssh/ssh_setup.sh new file mode 100644 index 0000000000..0af0b6d7ad --- /dev/null +++ b/hosting/single/ssh/ssh_setup.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +ssh-keygen -A + +#prepare run dir +if [ ! -d "/var/run/sshd" ]; then + mkdir -p /var/run/sshd +fi \ No newline at end of file diff --git a/hosting/single/ssh/sshd_config b/hosting/single/ssh/sshd_config new file mode 100644 index 0000000000..7eb5df953a --- /dev/null +++ b/hosting/single/ssh/sshd_config @@ -0,0 +1,12 @@ +Port 2222 +ListenAddress 0.0.0.0 +LoginGraceTime 180 +X11Forwarding yes +Ciphers aes128-cbc,3des-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr +MACs hmac-sha1,hmac-sha1-96 +StrictModes yes +SyslogFacility DAEMON +PasswordAuthentication yes +PermitEmptyPasswords no +PermitRootLogin yes +Subsystem sftp internal-sftp diff --git a/hosting/single/test.sh b/hosting/single/test.sh new file mode 100755 index 0000000000..8830426a47 --- /dev/null +++ b/hosting/single/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash +id=$(docker run -t -d -p 8080:80 budibase:latest) +docker exec -it $id bash +docker kill $id diff --git a/i18n/README.es.md b/i18n/README.es.md index 7245dc8656..21eb8caef7 100644 --- a/i18n/README.es.md +++ b/i18n/README.es.md @@ -8,10 +8,11 @@

- Construye herramientas empresariales personalizadas en cuestión de minutos y en su propia infraestructura. + Construye herramientas empresariales personalizadas en cuestión de minutos y en tu propia infraestructura.

- Budibase es una plataforma de código bajo de código abierto, que ayuda a desarrolladores y profesionales de TI a crear, automatizar y enviar aplicaciones empresariales personalizadas en cuestión de minutos y en su propia infraestructura + Budibase es una plataforma low code de código abierto, que ayuda a desarrolladores y profesionales de TI a crear y +automatizar aplicaciones personalizadas en cuestión de minutos

@@ -20,7 +21,7 @@

- + Budibase design ui

@@ -30,9 +31,6 @@ GitHub release (latest by date) - - Discord - Follow @budibase @@ -43,130 +41,213 @@

- Sign-up + Comenzar con Budibase en la nube · - Docs + Comenzar con Docker, K8s, DO · - Feature request + Documentaciones · - Report a bug + Pedir una funcionalidad · - Support: Discussions - & - Discord + Reportar un error + · + Support: Comunidad

+

+## ✨ Caracteristicas -## ✨ Features -When other platforms chose the closed source route, we decided to go open source. When other platforms chose cloud builders, we decided a local builder offered the better developer experience. We like to do things differently at Budibase. +### Construir aplicaciones reales +Con Budibase podras construir aplicaciones de pagina unica de gran rendimiento. Ademas, puedes hacerlas con un diseño +adaptativo para darles a tus usuarios una gran experiencia. +

-- **Build and ship real software.** Unlike other platforms, with Budibase you build and ship single page applications. Budibase applications have performance baked in and can be designed responsively, providing your users with a great experience. +### Codigo abierto y ampliable +Budibase es de codigo abierto con licencia GPL v3. Puedes ampliarlo o modificarlo para adaptarlo a tus necesidades y preferencias. -- **Open source and extensable.** Budibase is open-source. The builder is licensed AGPL v3, the server is GPL v3, and the client is MPL. This should fill you with confidence that Budibase will always be around. You can also code against Budibase or fork it and make changes as you please, providing a developer-friendly experience. +De esta manera proveemos una buena experiencia para el desarrollador asi como establecemos la confianza de que Budibase siempre estara funcional. +

-- **Load data or start from scratch.** Budibase pulls in data from multiple sources, including MongoDB, CouchDB, PostgreSQL, mySQL, Airtable, Google Sheets, S3, DyanmoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new data sources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). +### Cargar informacion o empezar desde cero +Budibase permite importar datos desde multiples fuentes, entre las que estan incluidas: MondoDB, CouchDB, PostgreSQL, MySQL, +Airtable, S3, DynamoDB o API REST. -- **Design and build apps with powerful pre-made components.** Budibase comes out of the box with beautifully designed, powerful components which you can use like building blocks to build your UI. We also expose a lot of your favourite CSS styling options so you can go that extra creative mile. [Request new components](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). - -- **Automate processes, integrate with other tools, and connect to webhooks.** Save time by automating manual processes and workflows. From connecting to webhooks, to automating emails, simply tell Budibase what to do and let it work for you. You can easily [create new automations for Budibase here](https://github.com/Budibase/automations) or [request new integrations here](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). - -- **Cloud hosting and self-hosting.** Users can self-host (see below), or host their apps with Budibase. Currently, our cloud hosting offering is limited to the free tier but we aim to change this in the future. For heavy usage, we advise users to self-host. +O si lo prefieres, con Budibase puedes empezar desde cero y construir tus propias aplicaciones +sin necesidad de herramientas externas. +[Sugerir fuente de datos](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).

- Budibase design ui + Budibase data

+

+### Diseña y construye aplicaciones con componentes profesionales prediseñados -## ⌛ Status -- [x] Alpha: We are demoing Budibase to users and receiving feedback -- [x] Private Beta: We are testing Budibase with a closed set of customers -- [x] Public Beta: Anyone can [sign-up and use Budibase](https://portal.budi.live/signup). -- [ ] Official Launch +Budibase incorpora componentes profesionales prediseñados que podras usar de manera facil e intuitiva +como bloques de construccion para la interfaz de tu aplicacion. -Watch "releases" of this repo to get notified of major updates, and give the star button a click whilst you're there. +Tambien mostramos gran parte del CSS para que puedas adaptar los componentes a tus diseños. +[Sugerir componente](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).

- + Budibase design

+

-### Stargazers over time +### Procesos automatizados, integra tu aplicacion con otras herramientas y conectala a eventos webhook + +Ahorra tiempo automatizando flujos de trabajo y procesos manuales. Podras desde conectar eventos webhook hasta automatizar emails, +simplemente dile a Budibase que hacer y deja que el haga el trabajo por ti. +[Crear nuevos procesos automatizados](https://github.com/Budibase/automations) o [Sugerir proceso automatizado](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas). + +

+ Budibase automations +

+

+ +### Tus herramientas favoritas + +Budibase integra un gran numero de herramientas que te permitiran construir tus aplicaciones ajustandose a tus preferencias. + +

+ Budibase integrations +

+

+ +### Un paraiso para administradores + +Puedes albergar Budibase en tu propia infraestructura y gestionar globalmente usuarios, incorporaciones, SMTP, aplicaciones, +grupos, diseños de temas, etc. + +Tambien puedes gestionar los usuarios y grupos, o delegar en personas asignadas para ello, desde nuestra aplicacion sin +mucho esfuerzo. + +Budibase is made to scale. With Budibase, you can self-host on your own infrastructure and globally manage users, onboarding, SMTP, apps, groups, theming and more. You can also provide users/groups with an app portal and disseminate user-management to the group manager. + +- Video Promocional: https://youtu.be/xoljVpty_Kw + +
+ +--- + +
+ + +## Budibase API Publica + +Como todo lo que construimos en Budibase, nuestra nueva API publica es facil de usar, flexible e introduce nueva ampliacion +del sistema. Budibase API ofrece: +- Uso de Budibase como backend +- Interoperabilidad + +#### Documentacion + +Puedes aprender mas acerca de Budibase API en los siguientes documentos: +- [Documentacion general](https://docs.budibase.com/docs/public-api) : Como optener tu clave para la API, usar Insomnia y Postman +- [API Interactiva](https://docs.budibase.com/reference/post_applications) : Aprende como trabajar con la API + +#### Guias + +- [Construye una aplicacion con Budibase y Next.js](https://budibase.com/blog/building-a-crud-app-with-budibase-and-next.js/) + +

+ Budibase data +

+

+ +


+ +## 🏁 Comenzar con Budibase + +Puedes alojar Budibase en tu propia infraestructura con Docker, Kubernetes o Digital Ocean; o usa Budibase en la nube si +quieres empezar a crear tus aplicaciones rapidamente y sin ningun tipo de preocupacion. + +### [Comenzar con Budibase self-hosting](https://docs.budibase.com/docs/hosting-methods) + +- [Docker - single ARM compatible image](https://docs.budibase.com/docs/docker) +- [Docker Compose](https://docs.budibase.com/docs/docker-compose) +- [Kubernetes](https://docs.budibase.com/docs/kubernetes-k8s) +- [Digital Ocean](https://docs.budibase.com/docs/digitalocean) +- [Portainer](https://docs.budibase.com/docs/portainer) + + +### [Comenzar con Budibase en la nube](https://budibase.com) + +

+ +## 🎓 Aprende a usar Budibase + +Aqui tienes la [documentacion de Budibase](https://docs.budibase.com/docs). +
+ + +

+ +## 💬 Comunidad + +Te invitamos a que te unas a nuestra comunidad de Budibase, alli podras hacer las preguntas que quieras, ayudar a otras +personas o tener una charla entretenida con otros usuarios de Budibase. +[Acceder a la comunidad de Budibase](https://github.com/Budibase/budibase/discussions) +


+ + +## ❗ Codigo de conducta + +Budibase presta especial atencion en acoger a personas de toda diversidad y ofrecer un entorno de respeto mutuo. Asi mismo +esperamos lo mismo de nuestra comunidad, por favor lee el +[**Codigo de conducta**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). +
+ +

+ + +## 🙌 Contribuir en Budibase + +Desde comunicar un bug a solventar un error en el codigo, toda contribucion es apreciada y bienvenida. Si estas planeando +implementar una nueva funcionalidad o un realizar un cambio en la API, por favor crea un [nuevo mensaje aqui](https://github.com/Budibase/budibase/issues), +de esta manera nos encargaremos que tu trabajo no sea en vano. + +Aqui tienes instrucciones de como configurar tu entorno Budibase para [Debian](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-DEBIAN.md) +y [MacOSX](https://github.com/Budibase/budibase/tree/HEAD/docs/DEV-SETUP-MACOSX.md) + +### No estas seguro por donde empezar? +Un buen lugar para empezar a contribuir con nosotros es [aqui](https://github.com/Budibase/budibase/projects/22). + +### Organizacion del repositorio + +Budibase es un repositorio unico gestionado por Lerna. Lerna construye y publica los paquetes de Budibase sincronizandolos +cada ves que se realiza un cambio. A rasgos generales, estos son los paquetes que conforman Budibase: + +- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contiene el codigo del builder de la parte cliente, esta es una aplicacion svelte. + +- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - Este modulo se ejecuta en el browser y es el responsable de leer definiciones JSON y crear aplicaciones web en el momento. + +- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - La parte servidor de Budibase. Esta aplicacion Koa es responsable de suministrar lo necesario al builder para asi generar las aplicaciones Budibase. Tambien provee una API para interaccionar con la base de datos y el almacenamiento de ficheros. + +Para mas informacion, por favor lee el siguiente documento [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/docs/CONTRIBUTING.md) + +

+ + +## 📝 Licencia + +Budibase es open-source, licenciado como [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html). El cliente y las librerias +de componentes estan licenciadas como [MPL](https://directory.fsf.org/wiki/License:MPL-2.0) - de esta manera, puedes licenciar +como tu quieras las aplicaciones que construyas. + +

+ +## ⭐ Historia de nuestros Stargazers [![Stargazers over time](https://starchart.cc/Budibase/budibase.svg)](https://starchart.cc/Budibase/budibase) -If you are having issues between updates of the builder, please use the guide [here](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md#troubleshooting) to clear down your environment. +Si estas teniendo problemas con el builder despues de actualizar, por favor [lee esta guia](https://github.com/Budibase/budibase/blob/HEAD/docs/CONTRIBUTING.md#troubleshooting) to clear down your environment. +

-## 🏁 Getting Started with Budibase +## Contribuidores ✨ -The Budibase builder runs in Electron, on Mac, PC and Linux. Follow the steps below to get started: -- [ ] [Sign-up to Budibase](https://portal.budi.live/signup) -- [ ] Create a username and password -- [ ] Copy your API key -- [ ] Download Budibase -- [ ] Open Budibase and enter your API key - -[Here is a guided tutorial](https://docs.budibase.com/tutorial/tutorial-signing-up) if you need extra help. - - -## 🤖 Self-hosting - -Budibase wants to make sure anyone can use the tools we develop and we know a lot of people need to be able to host the apps they make on their own systems - that is why we've decided to try and make self hosting as easy as possible! - -Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/docs/hosting-methods). - -[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/droplets/new?onboarding_origin=marketplace&i=09038e&fleetUuid=bb04f9c8-1de8-4687-b2ae-1d5177a0535b&appId=77729671&type=applications&size=s-4vcpu-8gb®ion=nyc1&refcode=0caaa6085a82&image=budibase-20-04) - - -## 🎓 Learning Budibase - -The Budibase [documentation lives here](https://docs.budibase.com). - -You can also follow a quick tutorial on [how to build a CRM with Budibase](https://docs.budibase.com/tutorial/tutorial-introduction) - - -## Roadmap - -Checkout our [Public Roadmap](https://github.com/Budibase/budibase/projects/10). If you would like to discuss some of the items on the roadmap, please feel to reach out on [Discord](https://discord.gg/rCYayfe), or via [Github discussions](https://github.com/Budibase/budibase/discussions) - - -## ❗ Code of Conduct - -Budibase is dedicated to providing a welcoming, diverse, and harrassment-free experience for everyone. We expect everyone in the Budibase community to abide by our [**Code of Conduct**](https://github.com/Budibase/budibase/blob/HEAD/.github/CODE_OF_CONDUCT.md). Please read it. - -## 🙌 Contributing to Budibase - -From opening a bug report to creating a pull request: every contribution is appreciated and welcomed. If you're planning to implement a new feature or change the API please create an issue first. This way we can ensure your work is not in vain. - -### Not Sure Where to Start? -A good place to start contributing, is the [First time issues project](https://github.com/Budibase/budibase/projects/22). - -### How the repository is organized -Budibase is a monorepo managed by lerna. Lerna manages the building and publishing of the budibase packages. At a high level, here are the packages that make up Budibase. - -- [packages/builder](https://github.com/Budibase/budibase/tree/HEAD/packages/builder) - contains code for the budibase builder client side svelte application. - -- [packages/client](https://github.com/Budibase/budibase/tree/HEAD/packages/client) - A module that runs in the browser responsible for reading JSON definition and creating living, breathing web apps from it. - -- [packages/server](https://github.com/Budibase/budibase/tree/HEAD/packages/server) - The budibase server. This Koa app is responsible for serving the JS for the builder and budibase apps, as well as providing the API for interaction with the database and file system. - -For more information, see [CONTRIBUTING.md](https://github.com/Budibase/budibase/blob/HEAD/.github/CONTRIBUTING.md) - -## 📝 License - -Budibase is open-source. The builder is licensed [AGPL v3](https://www.gnu.org/licenses/agpl-3.0.en.html), the server is licensed [GPL v3](https://www.gnu.org/licenses/gpl-3.0.en.html), and the client is licensed [MPL](https://directory.fsf.org/wiki/License:MPL-2.0). - -## 💬 Get in touch - -If you have a question or would like to talk with other Budibase users, please hop over to [Github discussions](https://github.com/Budibase/budibase/discussions) or join our Discord server: - -[Discord chatroom](https://discord.gg/rCYayfe) - -![Discord Shield](https://discordapp.com/api/guilds/733030666647765003/widget.png?style=shield) - - -## Contributors ✨ - -Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): +Queremos prestar un especial agradecimiento a nuestra maravillosa gente ([emoji key](https://allcontributors.org/docs/en/emoji-key)): @@ -179,14 +260,18 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Michael Shanks

📖 💻 ⚠️
Kevin Åberg Kultalahti

📖 💻 ⚠️
Joe

📖 💻 🖋 🎨 -
Conor_Mack

💻 ⚠️ +
Rory Powell

💻 📖 ⚠️ +
Peter Clement

💻 📖 ⚠️ +
Conor_Mack

💻 ⚠️
pngwn

💻 ⚠️
HugoLd

💻
victoriasloan

💻
yashank09

💻
SOVLOOKUP

💻 +
seoulaja

🌍 +
Maurits Lourens

⚠️ 💻 @@ -195,4 +280,5 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! +Este proyecto sigue las especificaciones de [all-contributors](https://github.com/all-contributors/all-contributors). +Todo tipo de contribuciones son agradecidas! diff --git a/lerna.json b/lerna.json index da3e27e84f..111a13702b 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.0.185-alpha.0", + "version": "1.4.18-alpha.1", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index bd71dd6bc7..579e86802e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "js-yaml": "^4.1.0", "kill-port": "^1.6.1", "lerna": "3.14.1", + "madge": "^5.0.1", "prettier": "^2.3.1", "prettier-plugin-svelte": "^2.3.0", "rimraf": "^3.0.2", @@ -22,10 +23,13 @@ }, "scripts": { "setup": "node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev", - "bootstrap": "lerna link && lerna bootstrap && ./scripts/link-dependencies.sh", + "bootstrap": "lerna bootstrap && lerna link && ./scripts/link-dependencies.sh", "build": "lerna run build", - "release": "lerna publish patch --yes --force-publish && yarn release:pro", - "release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop && yarn release:pro:develop", + "build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput", + "build:sdk": "lerna run build:sdk", + "deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular", + "release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro", + "release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop --exact && yarn release:pro:develop", "release:pro": "bash scripts/pro/release.sh", "release:pro:develop": "bash scripts/pro/release.sh develop", "restore": "yarn run clean && yarn run bootstrap && yarn run build", @@ -37,14 +41,15 @@ "kill-server": "kill-port 4001 4002", "kill-all": "yarn run kill-builder && yarn run kill-server", "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/server --ignore @budibase/worker", - "dev:server": "yarn run kill-server && lerna run --parallel dev:builder --concurrency 1 --scope @budibase/worker --scope @budibase/server", - "test": "lerna run test", + "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:pro": "bash scripts/pro/test.sh", "lint:eslint": "eslint packages", "lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"", "lint": "yarn run lint:eslint && yarn run lint:prettier", - "lint:fix:eslint": "eslint --fix packages", - "lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"", + "lint:fix:eslint": "eslint --fix packages qa-core", + "lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --write \"qa-core/**/*.{js,ts,svelte}\"", "lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint", "test:e2e": "lerna run cy:test --stream", "test:e2e:ci": "lerna run cy:ci --stream", @@ -52,16 +57,19 @@ "test:e2e:ci:notify": "lerna run cy:ci:notify", "build:specs": "lerna run specs", "build:docker": "lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -", + "build:docker:pre": "lerna run build && lerna run predocker", "build:docker:proxy": "docker build hosting/proxy -t proxy-service", "build:docker:proxy:compose": "node scripts/proxy/generateProxyConfig compose && npm run build:docker:proxy", "build:docker:proxy:preprod": "node scripts/proxy/generateProxyConfig preprod && npm run build:docker:proxy", + "build:docker:proxy:release": "node scripts/proxy/generateProxyConfig release && npm run build:docker:proxy", "build:docker:proxy:prod": "node scripts/proxy/generateProxyConfig prod && npm run build:docker:proxy", "build:docker:selfhost": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh latest && cd -", "build:docker:develop": "node scripts/pinVersions && lerna run build:docker && npm run build:docker:proxy:compose && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -", "build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild", "build:digitalocean": "cd hosting/digitalocean && ./build.sh && cd -", + "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": "lerna run build && lerna run predocker && npm run build:docker:single:image", + "build:docker:single": "npm run build:docker:pre && npm run build:docker:single:image", "build:docs": "lerna run build:docs", "release:helm": "node scripts/releaseHelmChart", "env:multi:enable": "lerna run env:multi:enable", @@ -80,4 +88,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/.gitignore b/packages/backend-core/.gitignore index 2528ad91a4..3784b93408 100644 --- a/packages/backend-core/.gitignore +++ b/packages/backend-core/.gitignore @@ -44,9 +44,6 @@ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ -# TypeScript cache -*.tsbuildinfo - # Optional npm cache directory .npm diff --git a/packages/backend-core/cache.js b/packages/backend-core/cache.js index 932fd7b901..c8bd3c9b6f 100644 --- a/packages/backend-core/cache.js +++ b/packages/backend-core/cache.js @@ -3,5 +3,7 @@ const generic = require("./src/cache/generic") module.exports = { user: require("./src/cache/user"), app: require("./src/cache/appMetadata"), + writethrough: require("./src/cache/writethrough"), ...generic, + cache: generic, } diff --git a/packages/backend-core/context.js b/packages/backend-core/context.js index 2afd762536..c6fa87a337 100644 --- a/packages/backend-core/context.js +++ b/packages/backend-core/context.js @@ -5,9 +5,12 @@ const { getAppId, updateAppId, doInAppContext, + doInTenant, doInContext, } = require("./src/context") +const identity = require("./src/context/identity") + module.exports = { getAppDB, getDevAppDB, @@ -15,5 +18,7 @@ module.exports = { getAppId, updateAppId, doInAppContext, + doInTenant, + identity, doInContext, } diff --git a/packages/backend-core/logging.js b/packages/backend-core/logging.js new file mode 100644 index 0000000000..da40fe3100 --- /dev/null +++ b/packages/backend-core/logging.js @@ -0,0 +1 @@ +module.exports = require("./src/logging") diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 434ee80234..146e41e2e0 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,48 +1,85 @@ { "name": "@budibase/backend-core", - "version": "1.0.185-alpha.0", + "version": "1.4.18-alpha.1", "description": "Budibase backend core libraries used in server and worker", - "main": "src/index.js", + "main": "dist/src/index.js", + "types": "dist/src/index.d.ts", + "exports": { + ".": "./dist/src/index.js", + "./tests": "./dist/tests/index.js", + "./*": "./dist/*.js" + }, "author": "Budibase", "license": "GPL-3.0", "scripts": { + "prebuild": "rimraf dist/", + "prepack": "cp package.json dist", + "build": "tsc -p tsconfig.build.json", + "build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput", "test": "jest", "test:watch": "jest --watchAll" }, "dependencies": { - "@techpass/passport-openidconnect": "^0.3.0", - "aws-sdk": "^2.901.0", - "bcrypt": "^5.0.1", - "dotenv": "^16.0.1", - "emitter-listener": "^1.1.2", - "ioredis": "^4.27.1", - "jsonwebtoken": "^8.5.1", - "koa-passport": "^4.1.4", - "lodash": "^4.17.21", - "lodash.isarguments": "^3.1.0", - "node-fetch": "^2.6.1", - "passport-google-auth": "^1.0.2", - "passport-google-oauth": "^2.0.0", - "passport-jwt": "^4.0.0", - "passport-local": "^1.0.0", - "posthog-node": "^1.3.0", + "@budibase/types": "1.4.18-alpha.1", + "@shopify/jest-koa-mocks": "5.0.1", + "@techpass/passport-openidconnect": "0.3.2", + "aws-sdk": "2.1030.0", + "bcrypt": "5.0.1", + "bcryptjs": "2.4.3", + "dotenv": "16.0.1", + "emitter-listener": "1.1.2", + "ioredis": "4.28.0", + "joi": "17.6.0", + "jsonwebtoken": "8.5.1", + "koa-passport": "4.1.4", + "lodash": "4.17.21", + "lodash.isarguments": "3.1.0", + "node-fetch": "2.6.7", + "passport-google-auth": "1.0.2", + "passport-google-oauth": "2.0.0", + "passport-jwt": "4.0.0", + "passport-local": "1.0.0", + "passport-oauth2-refresh": "^2.1.0", + "posthog-node": "1.3.0", "pouchdb": "7.3.0", - "pouchdb-find": "^7.2.2", - "pouchdb-replication-stream": "^1.2.9", - "sanitize-s3-objectkey": "^0.0.1", - "tar-fs": "^2.1.1", - "uuid": "^8.3.2", - "zlib": "^1.0.5" + "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", + "tar-fs": "2.1.1", + "uuid": "8.3.2", + "zlib": "1.0.5" }, "jest": { + "preset": "ts-jest", + "testEnvironment": "node", + "moduleNameMapper": { + "@budibase/types": "/../types/src" + }, "setupFiles": [ - "./scripts/jestSetup.js" + "./scripts/jestSetup.ts" ] }, "devDependencies": { - "ioredis-mock": "^5.5.5", - "jest": "^26.6.3", - "pouchdb-adapter-memory": "^7.2.2" + "@types/jest": "27.5.1", + "@types/koa": "2.0.52", + "@types/lodash": "4.14.180", + "@types/node": "14.18.20", + "@types/node-fetch": "2.6.1", + "@types/pouchdb": "6.4.0", + "@types/redlock": "4.0.3", + "@types/semver": "7.3.7", + "@types/tar-fs": "2.0.1", + "@types/uuid": "8.3.4", + "ioredis-mock": "5.8.0", + "jest": "27.5.1", + "koa": "2.7.0", + "nodemon": "2.0.16", + "pouchdb-adapter-memory": "7.2.2", + "timekeeper": "2.2.0", + "ts-jest": "27.1.5", + "typescript": "4.7.3" }, "gitHead": "d1836a898cab3f8ab80ee6d8f42be1a9eed7dcdc" } diff --git a/packages/backend-core/plugins.js b/packages/backend-core/plugins.js new file mode 100644 index 0000000000..018e214dcb --- /dev/null +++ b/packages/backend-core/plugins.js @@ -0,0 +1,3 @@ +module.exports = { + ...require("./src/plugin"), +} diff --git a/packages/backend-core/redis.js b/packages/backend-core/redis.js index 0a9dc91881..1f7a48540a 100644 --- a/packages/backend-core/redis.js +++ b/packages/backend-core/redis.js @@ -1,4 +1,5 @@ module.exports = { Client: require("./src/redis"), utils: require("./src/redis/utils"), + clients: require("./src/redis/init"), } diff --git a/packages/backend-core/scripts/jestSetup.js b/packages/backend-core/scripts/jestSetup.js deleted file mode 100644 index 93dbf3fd5a..0000000000 --- a/packages/backend-core/scripts/jestSetup.js +++ /dev/null @@ -1,6 +0,0 @@ -const env = require("../src/environment") - -env._set("SELF_HOSTED", "1") -env._set("NODE_ENV", "jest") -env._set("JWT_SECRET", "test-jwtsecret") -env._set("LOG_LEVEL", "silent") diff --git a/packages/backend-core/scripts/jestSetup.ts b/packages/backend-core/scripts/jestSetup.ts new file mode 100644 index 0000000000..1e86649a24 --- /dev/null +++ b/packages/backend-core/scripts/jestSetup.ts @@ -0,0 +1,12 @@ +import env from "../src/environment" +import { mocks } from "../tests/utilities" + +// mock all dates to 2020-01-01T00:00:00.000Z +// use tk.reset() to use real dates in individual tests +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") diff --git a/packages/backend-core/src/auth.js b/packages/backend-core/src/auth.js deleted file mode 100644 index b13cd932c6..0000000000 --- a/packages/backend-core/src/auth.js +++ /dev/null @@ -1,49 +0,0 @@ -const passport = require("koa-passport") -const LocalStrategy = require("passport-local").Strategy -const JwtStrategy = require("passport-jwt").Strategy -const { getGlobalDB } = require("./tenancy") -const { - jwt, - local, - authenticated, - google, - oidc, - auditLog, - tenancy, - appTenancy, - authError, - csrf, - internalApi, -} = require("./middleware") - -// Strategies -passport.use(new LocalStrategy(local.options, local.authenticate)) -passport.use(new JwtStrategy(jwt.options, jwt.authenticate)) - -passport.serializeUser((user, done) => done(null, user)) - -passport.deserializeUser(async (user, done) => { - const db = getGlobalDB() - - try { - const user = await db.get(user._id) - return done(null, user) - } catch (err) { - console.error(`User not found`, err) - return done(null, false, { message: "User not found" }) - } -}) - -module.exports = { - buildAuthMiddleware: authenticated, - passport, - google, - oidc, - jwt: require("jsonwebtoken"), - buildTenancyMiddleware: tenancy, - buildAppTenancyMiddleware: appTenancy, - auditLog, - authError, - buildCsrfMiddleware: csrf, - internalApi, -} diff --git a/packages/backend-core/src/auth.ts b/packages/backend-core/src/auth.ts new file mode 100644 index 0000000000..23873b84e7 --- /dev/null +++ b/packages/backend-core/src/auth.ts @@ -0,0 +1,196 @@ +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 { Configs } from "./constants" +import { getScopedConfig } from "./db/utils" +import { + jwt, + local, + authenticated, + google, + oidc, + auditLog, + tenancy, + authError, + ssoCallbackUrl, + csrf, + internalApi, + adminOnly, + builderOnly, + builderOrAdmin, + joiValidator, +} from "./middleware" +import { invalidateUser } from "./cache/user" +import { User } from "@budibase/types" + +// Strategies +passport.use(new LocalStrategy(local.options, local.authenticate)) +passport.use(new JwtStrategy(jwt.options, jwt.authenticate)) + +passport.serializeUser((user: User, done: any) => done(null, user)) + +passport.deserializeUser(async (user: User, done: any) => { + const db = getGlobalDB() + + try { + const dbUser = await db.get(user._id) + return done(null, dbUser) + } catch (err) { + console.error(`User not found`, err) + return done(null, false, { message: "User not found" }) + } +}) + +async function refreshOIDCAccessToken( + db: any, + chosenConfig: any, + refreshToken: string +) { + const callbackUrl = await oidc.getCallbackUrl(db, chosenConfig) + let enrichedConfig: any + let strategy: any + + try { + enrichedConfig = await oidc.fetchStrategyConfig(chosenConfig, callbackUrl) + if (!enrichedConfig) { + throw new Error("OIDC Config contents invalid") + } + strategy = await oidc.strategyFactory(enrichedConfig) + } catch (err) { + console.error(err) + throw new Error("Could not refresh OAuth Token") + } + + refresh.use(strategy, { + setRefreshOAuth2() { + return strategy._getOAuth2Client(enrichedConfig) + }, + }) + + return new Promise(resolve => { + refresh.requestNewAccessToken( + Configs.OIDC, + refreshToken, + (err: any, accessToken: string, refreshToken: any, params: any) => { + resolve({ err, accessToken, refreshToken, params }) + } + ) + }) +} + +async function refreshGoogleAccessToken( + db: any, + config: any, + refreshToken: any +) { + let callbackUrl = await google.getCallbackUrl(db, config) + + let strategy + try { + strategy = await google.strategyFactory(config, callbackUrl) + } catch (err: any) { + console.error(err) + throw new Error( + `Error constructing OIDC refresh strategy: message=${err.message}` + ) + } + + refresh.use(strategy) + + return new Promise(resolve => { + refresh.requestNewAccessToken( + Configs.GOOGLE, + refreshToken, + (err: any, accessToken: string, refreshToken: string, params: any) => { + resolve({ err, accessToken, refreshToken, params }) + } + ) + }) +} + +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 === Configs.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 + ) + } + + return refreshResponse +} + +async function updateUserOAuth(userId: string, oAuthConfig: any) { + const details = { + accessToken: oAuthConfig.accessToken, + refreshToken: oAuthConfig.refreshToken, + } + + try { + const db = getGlobalDB() + const dbUser = await db.get(userId) + + //Do not overwrite the refresh token if a valid one is not provided. + if (typeof details.refreshToken !== "string") { + delete details.refreshToken + } + + dbUser.oauth2 = { + ...dbUser.oauth2, + ...details, + } + + await db.put(dbUser) + + await invalidateUser(userId) + } catch (e) { + console.error("Could not update OAuth details for current user", e) + } +} + +export = { + buildAuthMiddleware: authenticated, + passport, + google, + oidc, + jwt: require("jsonwebtoken"), + buildTenancyMiddleware: tenancy, + auditLog, + authError, + buildCsrfMiddleware: csrf, + internalApi, + refreshOAuthToken, + updateUserOAuth, + ssoCallbackUrl, + adminOnly, + builderOnly, + builderOrAdmin, + joiValidator, +} diff --git a/packages/backend-core/src/cache/appMetadata.js b/packages/backend-core/src/cache/appMetadata.js index effdc886d7..a7ff0d2fc1 100644 --- a/packages/backend-core/src/cache/appMetadata.js +++ b/packages/backend-core/src/cache/appMetadata.js @@ -1,6 +1,6 @@ -const redis = require("../redis/authRedis") +const redis = require("../redis/init") const { doWithDB } = require("../db") -const { DocumentTypes } = require("../db/constants") +const { DocumentType } = require("../db/constants") const AppState = { INVALID: "invalid", @@ -14,7 +14,7 @@ const populateFromDB = async appId => { return doWithDB( appId, db => { - return db.get(DocumentTypes.APP_METADATA) + return db.get(DocumentType.APP_METADATA) }, { skip_setup: true } ) diff --git a/packages/backend-core/src/cache/base/index.ts b/packages/backend-core/src/cache/base/index.ts new file mode 100644 index 0000000000..f3216531f4 --- /dev/null +++ b/packages/backend-core/src/cache/base/index.ts @@ -0,0 +1,92 @@ +import { getTenantId } from "../../context" +import redis from "../../redis/init" +import RedisWrapper from "../../redis" + +function generateTenantKey(key: string) { + const tenantId = getTenantId() + return `${key}:${tenantId}` +} + +export = class BaseCache { + client: RedisWrapper | undefined + + constructor(client: RedisWrapper | undefined = undefined) { + this.client = client + } + + async getClient() { + return !this.client ? await redis.getCacheClient() : this.client + } + + async keys(pattern: string) { + const client = await this.getClient() + return client.keys(pattern) + } + + /** + * Read only from the cache. + */ + async get(key: string, opts = { useTenancy: true }) { + key = opts.useTenancy ? generateTenantKey(key) : key + const client = await this.getClient() + return client.get(key) + } + + /** + * Write to the cache. + */ + async store( + key: string, + value: any, + ttl: number | null = null, + opts = { useTenancy: true } + ) { + key = opts.useTenancy ? generateTenantKey(key) : key + const client = await this.getClient() + await client.store(key, value, ttl) + } + + /** + * Remove from cache. + */ + async delete(key: string, opts = { useTenancy: true }) { + key = opts.useTenancy ? generateTenantKey(key) : key + const client = await this.getClient() + return client.delete(key) + } + + /** + * Read from the cache. Write to the cache if not exists. + */ + async withCache( + key: string, + ttl: number, + fetchFn: any, + opts = { useTenancy: true } + ) { + const cachedValue = await this.get(key, opts) + if (cachedValue) { + return cachedValue + } + + try { + const fetchedValue = await fetchFn() + + await this.store(key, fetchedValue, ttl, opts) + return fetchedValue + } catch (err) { + console.error("Error fetching before cache - ", err) + throw err + } + } + + async bustCache(key: string, opts = { client: null }) { + const client = await this.getClient() + try { + await client.delete(generateTenantKey(key)) + } catch (err) { + console.error("Error busting cache - ", err) + throw err + } + } +} diff --git a/packages/backend-core/src/cache/generic.js b/packages/backend-core/src/cache/generic.js index b23568f8b9..26ef0c6bb0 100644 --- a/packages/backend-core/src/cache/generic.js +++ b/packages/backend-core/src/cache/generic.js @@ -1,9 +1,15 @@ -const redis = require("../redis/authRedis") -const env = require("../environment") -const { getTenantId } = require("../context") +const BaseCache = require("./base") + +const GENERIC = new BaseCache() exports.CacheKeys = { CHECKLIST: "checklist", + INSTALLATION: "installation", + ANALYTICS_ENABLED: "analyticsEnabled", + UNIQUE_TENANT_ID: "uniqueTenantId", + EVENTS: "events", + BACKFILL_METADATA: "backfillMetadata", + EVENTS_RATE_LIMIT: "eventsRateLimit", } exports.TTL = { @@ -12,38 +18,13 @@ exports.TTL = { ONE_DAY: 86400, } -function generateTenantKey(key) { - const tenantId = getTenantId() - return `${key}:${tenantId}` +function performExport(funcName) { + return (...args) => GENERIC[funcName](...args) } -exports.withCache = async (key, ttl, fetchFn) => { - key = generateTenantKey(key) - const client = await redis.getCacheClient() - const cachedValue = await client.get(key) - if (cachedValue) { - return cachedValue - } - - try { - const fetchedValue = await fetchFn() - - if (!env.isTest()) { - await client.store(key, fetchedValue, ttl) - } - return fetchedValue - } catch (err) { - console.error("Error fetching before cache - ", err) - throw err - } -} - -exports.bustCache = async key => { - const client = await redis.getCacheClient() - try { - await client.delete(generateTenantKey(key)) - } catch (err) { - console.error("Error busting cache - ", err) - throw err - } -} +exports.keys = performExport("keys") +exports.get = performExport("get") +exports.store = performExport("store") +exports.delete = performExport("delete") +exports.withCache = performExport("withCache") +exports.bustCache = performExport("bustCache") diff --git a/packages/backend-core/src/cache/tests/writethrough.spec.js b/packages/backend-core/src/cache/tests/writethrough.spec.js new file mode 100644 index 0000000000..68db24b325 --- /dev/null +++ b/packages/backend-core/src/cache/tests/writethrough.spec.js @@ -0,0 +1,59 @@ +require("../../../tests/utilities/TestConfiguration") +const { Writethrough } = require("../writethrough") +const { dangerousGetDB } = require("../../db") +const tk = require("timekeeper") + +const START_DATE = Date.now() +tk.freeze(START_DATE) + +const DELAY = 5000 + +const db = dangerousGetDB("test") +const db2 = dangerousGetDB("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/user.js b/packages/backend-core/src/cache/user.js index faac6de725..130da1915e 100644 --- a/packages/backend-core/src/cache/user.js +++ b/packages/backend-core/src/cache/user.js @@ -1,4 +1,4 @@ -const redis = require("../redis/authRedis") +const redis = require("../redis/init") const { getTenantId, lookupTenantId, doWithGlobalDB } = require("../tenancy") const env = require("../environment") const accounts = require("../cloud/accounts") diff --git a/packages/backend-core/src/cache/writethrough.ts b/packages/backend-core/src/cache/writethrough.ts new file mode 100644 index 0000000000..ec6b1604c8 --- /dev/null +++ b/packages/backend-core/src/cache/writethrough.ts @@ -0,0 +1,119 @@ +import BaseCache from "./base" +import { getWritethroughClient } from "../redis/init" +import { logWarn } from "../logging" + +const DEFAULT_WRITE_RATE_MS = 10000 +let CACHE: BaseCache | null = null + +interface CacheItem { + doc: any + lastWrite: number +} + +async function getCache() { + if (!CACHE) { + const client = await getWritethroughClient() + CACHE = new BaseCache(client) + } + return CACHE +} + +function makeCacheKey(db: PouchDB.Database, key: string) { + return db.name + key +} + +function makeCacheItem(doc: any, lastWrite: number | null = null): CacheItem { + return { doc, lastWrite: lastWrite || Date.now() } +} + +export async function put( + db: PouchDB.Database, + doc: any, + writeRateMs: number = DEFAULT_WRITE_RATE_MS +) { + const cache = await getCache() + const key = doc._id + let cacheItem: CacheItem | undefined = await cache.get(makeCacheKey(db, key)) + const updateDb = !cacheItem || cacheItem.lastWrite < Date.now() - writeRateMs + let output = doc + if (updateDb) { + const writeDb = async (toWrite: any) => { + // doc should contain the _id and _rev + const response = await db.put(toWrite) + output = { + ...doc, + _id: response.id, + _rev: response.rev, + } + } + try { + await writeDb(doc) + } catch (err: any) { + if (err.status !== 409) { + throw err + } else { + // Swallow 409s but log them + logWarn(`Ignoring conflict in write-through cache`) + } + } + } + // if we are updating the DB then need to set the lastWrite to now + cacheItem = makeCacheItem(output, updateDb ? null : cacheItem?.lastWrite) + await cache.store(makeCacheKey(db, key), cacheItem) + return { ok: true, id: output._id, rev: output._rev } +} + +export async function get(db: PouchDB.Database, id: string): Promise { + const cache = await getCache() + const cacheKey = makeCacheKey(db, id) + let cacheItem: CacheItem = await cache.get(cacheKey) + if (!cacheItem) { + const doc = await db.get(id) + cacheItem = makeCacheItem(doc) + await cache.store(cacheKey, cacheItem) + } + return cacheItem.doc +} + +export async function remove( + db: PouchDB.Database, + docOrId: any, + rev?: any +): Promise { + const cache = await getCache() + if (!docOrId) { + throw new Error("No ID/Rev provided.") + } + const id = typeof docOrId === "string" ? docOrId : docOrId._id + rev = typeof docOrId === "string" ? rev : docOrId._rev + try { + await cache.delete(makeCacheKey(db, id)) + } finally { + await db.remove(id, rev) + } +} + +export class Writethrough { + db: PouchDB.Database + writeRateMs: number + + constructor( + db: PouchDB.Database, + writeRateMs: number = DEFAULT_WRITE_RATE_MS + ) { + this.db = db + this.writeRateMs = writeRateMs + } + + async put(doc: any) { + return put(this.db, doc, this.writeRateMs) + } + + async get(id: string) { + return get(this.db, id) + } + + async remove(docOrId: any, rev?: any) { + return remove(this.db, docOrId, rev) + } +} diff --git a/packages/backend-core/src/cloud/accounts.js b/packages/backend-core/src/cloud/accounts.js deleted file mode 100644 index 5730bc67a5..0000000000 --- a/packages/backend-core/src/cloud/accounts.js +++ /dev/null @@ -1,39 +0,0 @@ -const API = require("./api") -const env = require("../environment") -const { Headers } = require("../constants") - -const api = new API(env.ACCOUNT_PORTAL_URL) - -exports.getAccount = async email => { - const payload = { - email, - } - const response = await api.post(`/api/accounts/search`, { - body: payload, - headers: { - [Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, - }, - }) - const json = await response.json() - - if (response.status !== 200) { - throw new Error(`Error getting account by email ${email}`, json) - } - - return json[0] -} - -exports.getStatus = async () => { - const response = await api.get(`/api/status`, { - headers: { - [Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, - }, - }) - const json = await response.json() - - if (response.status !== 200) { - throw new Error(`Error getting status`) - } - - return json -} diff --git a/packages/backend-core/src/cloud/accounts.ts b/packages/backend-core/src/cloud/accounts.ts new file mode 100644 index 0000000000..cca7469060 --- /dev/null +++ b/packages/backend-core/src/cloud/accounts.ts @@ -0,0 +1,63 @@ +import API from "./api" +import env from "../environment" +import { Headers } from "../constants" +import { CloudAccount } from "@budibase/types" + +const api = new API(env.ACCOUNT_PORTAL_URL) + +export const getAccount = async ( + email: string +): Promise => { + const payload = { + email, + } + const response = await api.post(`/api/accounts/search`, { + body: payload, + headers: { + [Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, + }, + }) + + if (response.status !== 200) { + throw new Error(`Error getting account by email ${email}`) + } + + const json: CloudAccount[] = await response.json() + return json[0] +} + +export const getAccountByTenantId = async ( + tenantId: string +): Promise => { + const payload = { + tenantId, + } + const response = await api.post(`/api/accounts/search`, { + body: payload, + headers: { + [Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, + }, + }) + + if (response.status !== 200) { + throw new Error(`Error getting account by tenantId ${tenantId}`) + } + + const json: CloudAccount[] = await response.json() + return json[0] +} + +export const getStatus = async () => { + const response = await api.get(`/api/status`, { + headers: { + [Headers.API_KEY]: env.ACCOUNT_PORTAL_API_KEY, + }, + }) + const json = await response.json() + + if (response.status !== 200) { + throw new Error(`Error getting status`) + } + + return json +} diff --git a/packages/backend-core/src/constants.js b/packages/backend-core/src/constants.js index 172e66e603..44c271a4f8 100644 --- a/packages/backend-core/src/constants.js +++ b/packages/backend-core/src/constants.js @@ -7,6 +7,7 @@ exports.Cookies = { CurrentApp: "budibase:currentapp", Auth: "budibase:auth", Init: "budibase:init", + ACCOUNT_RETURN_URL: "budibase:account:returnurl", DatasourceAuth: "budibase:datasourceauth", OIDC_CONFIG: "budibase:oidc:config", } diff --git a/packages/backend-core/src/context/constants.ts b/packages/backend-core/src/context/constants.ts new file mode 100644 index 0000000000..937ad8f248 --- /dev/null +++ b/packages/backend-core/src/context/constants.ts @@ -0,0 +1,17 @@ +export enum ContextKey { + TENANT_ID = "tenantId", + GLOBAL_DB = "globalDb", + APP_ID = "appId", + IDENTITY = "identity", + // whatever the request app DB was + CURRENT_DB = "currentDb", + // get the prod app DB from the request + PROD_DB = "prodDb", + // get the dev app DB from the request + DEV_DB = "devDb", + DB_OPTS = "dbOpts", + // check if something else is using the context, don't close DB + TENANCY_IN_USE = "tenancyInUse", + APP_IN_USE = "appInUse", + IDENTITY_IN_USE = "identityInUse", +} diff --git a/packages/backend-core/src/context/identity.ts b/packages/backend-core/src/context/identity.ts new file mode 100644 index 0000000000..37e1ecf40a --- /dev/null +++ b/packages/backend-core/src/context/identity.ts @@ -0,0 +1,50 @@ +import { + IdentityContext, + IdentityType, + User, + UserContext, + isCloudAccount, + Account, + AccountUserContext, +} from "@budibase/types" +import * as context from "." + +export const getIdentity = (): IdentityContext | undefined => { + return context.getIdentity() +} + +export const doInIdentityContext = (identity: IdentityContext, task: any) => { + return context.doInIdentityContext(identity, task) +} + +export const doInUserContext = (user: User, task: any) => { + const userContext: UserContext = { + ...user, + _id: user._id as string, + type: IdentityType.USER, + } + return doInIdentityContext(userContext, task) +} + +export const doInAccountContext = (account: Account, task: any) => { + const _id = getAccountUserId(account) + const tenantId = account.tenantId + const accountContext: AccountUserContext = { + _id, + type: IdentityType.USER, + tenantId, + account, + } + return doInIdentityContext(accountContext, task) +} + +export const getAccountUserId = (account: Account) => { + let userId: string + if (isCloudAccount(account)) { + userId = account.budibaseUserId + } else { + // use account id as user id for self hosting + userId = account.accountId + } + return userId +} diff --git a/packages/backend-core/src/context/index.js b/packages/backend-core/src/context/index.js deleted file mode 100644 index fa8f09b659..0000000000 --- a/packages/backend-core/src/context/index.js +++ /dev/null @@ -1,346 +0,0 @@ -const env = require("../environment") -const { Headers } = require("../../constants") -const { SEPARATOR, DocumentTypes } = require("../db/constants") -const { DEFAULT_TENANT_ID } = require("../constants") -const cls = require("./FunctionContext") -const { dangerousGetDB, closeDB } = require("../db") -const { getProdAppID, getDevelopmentAppID } = require("../db/conversions") -const { baseGlobalDBName } = require("../tenancy/utils") -const { isEqual } = require("lodash") - -// some test cases call functions directly, need to -// store an app ID to pretend there is a context -let TEST_APP_ID = null - -const ContextKeys = { - TENANT_ID: "tenantId", - GLOBAL_DB: "globalDb", - APP_ID: "appId", - // whatever the request app DB was - CURRENT_DB: "currentDb", - // get the prod app DB from the request - PROD_DB: "prodDb", - // get the dev app DB from the request - DEV_DB: "devDb", - DB_OPTS: "dbOpts", - // check if something else is using the context, don't close DB - IN_USE: "inUse", -} - -exports.DEFAULT_TENANT_ID = DEFAULT_TENANT_ID - -// this function makes sure the PouchDB objects are closed and -// fully deleted when finished - this protects against memory leaks -async function closeAppDBs() { - const dbKeys = [ - ContextKeys.CURRENT_DB, - ContextKeys.PROD_DB, - ContextKeys.DEV_DB, - ] - for (let dbKey of dbKeys) { - const db = cls.getFromContext(dbKey) - if (!db) { - continue - } - await closeDB(db) - // clear the DB from context, incase someone tries to use it again - cls.setOnContext(dbKey, null) - } - // clear the app ID now that the databases are closed - if (cls.getFromContext(ContextKeys.APP_ID)) { - cls.setOnContext(ContextKeys.APP_ID, null) - } - if (cls.getFromContext(ContextKeys.DB_OPTS)) { - cls.setOnContext(ContextKeys.DB_OPTS, null) - } -} - -exports.closeTenancy = async () => { - if (env.USE_COUCH) { - await closeDB(exports.getGlobalDB()) - } - // clear from context now that database is closed/task is finished - cls.setOnContext(ContextKeys.TENANT_ID, null) - cls.setOnContext(ContextKeys.GLOBAL_DB, null) -} - -exports.isDefaultTenant = () => { - return exports.getTenantId() === exports.DEFAULT_TENANT_ID -} - -exports.isMultiTenant = () => { - return env.MULTI_TENANCY -} - -// used for automations, API endpoints should always be in context already -exports.doInTenant = (tenantId, task, { forceNew } = {}) => { - // the internal function is so that we can re-use an existing - // context - don't want to close DB on a parent context - async function internal(opts = { existing: false }) { - // set the tenant id - if (!opts.existing) { - cls.setOnContext(ContextKeys.TENANT_ID, tenantId) - if (env.USE_COUCH) { - exports.setGlobalDB(tenantId) - } - } - - try { - // invoke the task - return await task() - } finally { - const using = cls.getFromContext(ContextKeys.IN_USE) - if (!using || using <= 1) { - await exports.closeTenancy() - } else { - cls.setOnContext(using - 1) - } - } - } - const using = cls.getFromContext(ContextKeys.IN_USE) - if ( - !forceNew && - using && - cls.getFromContext(ContextKeys.TENANT_ID) === tenantId - ) { - cls.setOnContext(ContextKeys.IN_USE, using + 1) - return internal({ existing: true }) - } else { - return cls.run(async () => { - cls.setOnContext(ContextKeys.IN_USE, 1) - return internal() - }) - } -} - -/** - * Given an app ID this will attempt to retrieve the tenant ID from it. - * @return {null|string} The tenant ID found within the app ID. - */ -exports.getTenantIDFromAppID = appId => { - if (!appId) { - return null - } - const split = appId.split(SEPARATOR) - const hasDev = split[1] === DocumentTypes.DEV - if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) { - return null - } - if (hasDev) { - return split[2] - } else { - return split[1] - } -} - -const setAppTenantId = appId => { - const appTenantId = - exports.getTenantIDFromAppID(appId) || exports.DEFAULT_TENANT_ID - exports.updateTenantId(appTenantId) -} - -// gets the tenant ID from the app ID -exports.doInContext = async (appId, task) => { - const tenantId = exports.getTenantIDFromAppID(appId) - return exports.doInTenant(tenantId, async () => { - return exports.doInAppContext(appId, async () => { - return task() - }) - }) -} - -exports.doInAppContext = (appId, task, { forceNew } = {}) => { - if (!appId) { - throw new Error("appId is required") - } - - // the internal function is so that we can re-use an existing - // context - don't want to close DB on a parent context - async function internal(opts = { existing: false }) { - // set the app tenant id - if (!opts.existing) { - setAppTenantId(appId) - } - // set the app ID - cls.setOnContext(ContextKeys.APP_ID, appId) - try { - // invoke the task - return await task() - } finally { - const using = cls.getFromContext(ContextKeys.IN_USE) - if (!using || using <= 1) { - await closeAppDBs() - } else { - cls.setOnContext(using - 1) - } - } - } - const using = cls.getFromContext(ContextKeys.IN_USE) - if (!forceNew && using && cls.getFromContext(ContextKeys.APP_ID) === appId) { - cls.setOnContext(ContextKeys.IN_USE, using + 1) - return internal({ existing: true }) - } else { - return cls.run(async () => { - cls.setOnContext(ContextKeys.IN_USE, 1) - return internal() - }) - } -} - -exports.updateTenantId = tenantId => { - cls.setOnContext(ContextKeys.TENANT_ID, tenantId) - exports.setGlobalDB(tenantId) -} - -exports.updateAppId = async appId => { - try { - // have to close first, before removing the databases from context - await closeAppDBs() - cls.setOnContext(ContextKeys.APP_ID, appId) - } catch (err) { - if (env.isTest()) { - TEST_APP_ID = appId - } else { - throw err - } - } -} - -exports.setTenantId = ( - ctx, - opts = { allowQs: false, allowNoTenant: false } -) => { - let tenantId - // exit early if not multi-tenant - if (!exports.isMultiTenant()) { - cls.setOnContext(ContextKeys.TENANT_ID, exports.DEFAULT_TENANT_ID) - return exports.DEFAULT_TENANT_ID - } - - const allowQs = opts && opts.allowQs - const allowNoTenant = opts && opts.allowNoTenant - const header = ctx.request.headers[Headers.TENANT_ID] - const user = ctx.user || {} - if (allowQs) { - const query = ctx.request.query || {} - tenantId = query.tenantId - } - // override query string (if allowed) by user, or header - // URL params cannot be used in a middleware, as they are - // processed later in the chain - tenantId = user.tenantId || header || tenantId - - // Set the tenantId from the subdomain - if (!tenantId) { - tenantId = ctx.subdomains && ctx.subdomains[0] - } - - if (!tenantId && !allowNoTenant) { - ctx.throw(403, "Tenant id not set") - } - // check tenant ID just incase no tenant was allowed - if (tenantId) { - cls.setOnContext(ContextKeys.TENANT_ID, tenantId) - } - return tenantId -} - -exports.setGlobalDB = tenantId => { - const dbName = baseGlobalDBName(tenantId) - const db = dangerousGetDB(dbName) - cls.setOnContext(ContextKeys.GLOBAL_DB, db) - return db -} - -exports.getGlobalDB = () => { - const db = cls.getFromContext(ContextKeys.GLOBAL_DB) - if (!db) { - throw new Error("Global DB not found") - } - return db -} - -exports.isTenantIdSet = () => { - const tenantId = cls.getFromContext(ContextKeys.TENANT_ID) - return !!tenantId -} - -exports.getTenantId = () => { - if (!exports.isMultiTenant()) { - return exports.DEFAULT_TENANT_ID - } - const tenantId = cls.getFromContext(ContextKeys.TENANT_ID) - if (!tenantId) { - throw new Error("Tenant id not found") - } - return tenantId -} - -exports.getAppId = () => { - const foundId = cls.getFromContext(ContextKeys.APP_ID) - if (!foundId && env.isTest() && TEST_APP_ID) { - return TEST_APP_ID - } else { - return foundId - } -} - -function getContextDB(key, opts) { - const dbOptsKey = `${key}${ContextKeys.DB_OPTS}` - let storedOpts = cls.getFromContext(dbOptsKey) - let db = cls.getFromContext(key) - if (db && isEqual(opts, storedOpts)) { - return db - } - - const appId = exports.getAppId() - let toUseAppId - - switch (key) { - case ContextKeys.CURRENT_DB: - toUseAppId = appId - break - case ContextKeys.PROD_DB: - toUseAppId = getProdAppID(appId) - break - case ContextKeys.DEV_DB: - toUseAppId = getDevelopmentAppID(appId) - break - } - db = dangerousGetDB(toUseAppId, opts) - try { - cls.setOnContext(key, db) - if (opts) { - cls.setOnContext(dbOptsKey, opts) - } - } catch (err) { - if (!env.isTest()) { - throw err - } - } - return db -} - -/** - * Opens the app database based on whatever the request - * contained, dev or prod. - */ -exports.getAppDB = opts => { - return getContextDB(ContextKeys.CURRENT_DB, opts) -} - -/** - * This specifically gets the prod app ID, if the request - * contained a development app ID, this will open the prod one. - */ -exports.getProdAppDB = opts => { - return getContextDB(ContextKeys.PROD_DB, opts) -} - -/** - * This specifically gets the dev app ID, if the request - * contained a prod app ID, this will open the dev one. - */ -exports.getDevAppDB = opts => { - return getContextDB(ContextKeys.DEV_DB, opts) -} diff --git a/packages/backend-core/src/context/index.ts b/packages/backend-core/src/context/index.ts new file mode 100644 index 0000000000..35eeee608b --- /dev/null +++ b/packages/backend-core/src/context/index.ts @@ -0,0 +1,264 @@ +import env from "../environment" +import { SEPARATOR, DocumentType } from "../db/constants" +import cls from "./FunctionContext" +import { dangerousGetDB, closeDB } from "../db" +import { baseGlobalDBName } from "../db/tenancy" +import { IdentityContext } from "@budibase/types" +import { DEFAULT_TENANT_ID as _DEFAULT_TENANT_ID } from "../constants" +import { ContextKey } from "./constants" +import { + updateUsing, + closeWithUsing, + setAppTenantId, + setIdentity, + closeAppDBs, + getContextDB, +} from "./utils" + +export const DEFAULT_TENANT_ID = _DEFAULT_TENANT_ID + +// some test cases call functions directly, need to +// store an app ID to pretend there is a context +let TEST_APP_ID: string | null = null + +export const closeTenancy = async () => { + let db + try { + if (env.USE_COUCH) { + db = getGlobalDB() + } + } catch (err) { + // no DB found - skip closing + return + } + await closeDB(db) + // clear from context now that database is closed/task is finished + cls.setOnContext(ContextKey.TENANT_ID, null) + cls.setOnContext(ContextKey.GLOBAL_DB, null) +} + +// export const isDefaultTenant = () => { +// return getTenantId() === DEFAULT_TENANT_ID +// } + +export const isMultiTenant = () => { + return env.MULTI_TENANCY +} + +/** + * Given an app ID this will attempt to retrieve the tenant ID from it. + * @return {null|string} The tenant ID found within the app ID. + */ +export const getTenantIDFromAppID = (appId: string) => { + if (!appId) { + return null + } + const split = appId.split(SEPARATOR) + const hasDev = split[1] === DocumentType.DEV + if ((hasDev && split.length === 3) || (!hasDev && split.length === 2)) { + return null + } + if (hasDev) { + return split[2] + } else { + return split[1] + } +} + +export const doInContext = async (appId: string, task: any) => { + // gets the tenant ID from the app ID + const tenantId = getTenantIDFromAppID(appId) + return doInTenant(tenantId, async () => { + return doInAppContext(appId, async () => { + return task() + }) + }) +} + +export const doInTenant = (tenantId: string | null, task: any) => { + // make sure default always selected in single tenancy + if (!env.MULTI_TENANCY) { + tenantId = tenantId || DEFAULT_TENANT_ID + } + // the internal function is so that we can re-use an existing + // context - don't want to close DB on a parent context + async function internal(opts = { existing: false }) { + // set the tenant id + global db if this is a new context + if (!opts.existing) { + updateTenantId(tenantId) + } + + try { + // invoke the task + return await task() + } finally { + await closeWithUsing(ContextKey.TENANCY_IN_USE, () => { + return closeTenancy() + }) + } + } + + const existing = cls.getFromContext(ContextKey.TENANT_ID) === tenantId + return updateUsing(ContextKey.TENANCY_IN_USE, existing, internal) +} + +export const doInAppContext = (appId: string, task: any) => { + if (!appId) { + throw new Error("appId is required") + } + + const identity = getIdentity() + + // the internal function is so that we can re-use an existing + // context - don't want to close DB on a parent context + async function internal(opts = { existing: false }) { + // set the app tenant id + if (!opts.existing) { + setAppTenantId(appId) + } + // set the app ID + cls.setOnContext(ContextKey.APP_ID, appId) + + // preserve the identity + if (identity) { + setIdentity(identity) + } + try { + // invoke the task + return await task() + } finally { + await closeWithUsing(ContextKey.APP_IN_USE, async () => { + await closeAppDBs() + await closeTenancy() + }) + } + } + const existing = cls.getFromContext(ContextKey.APP_ID) === appId + return updateUsing(ContextKey.APP_IN_USE, existing, internal) +} + +export const doInIdentityContext = (identity: IdentityContext, task: any) => { + if (!identity) { + throw new Error("identity is required") + } + + async function internal(opts = { existing: false }) { + if (!opts.existing) { + cls.setOnContext(ContextKey.IDENTITY, identity) + // set the tenant so that doInTenant will preserve identity + if (identity.tenantId) { + updateTenantId(identity.tenantId) + } + } + + try { + // invoke the task + return await task() + } finally { + await closeWithUsing(ContextKey.IDENTITY_IN_USE, async () => { + setIdentity(null) + await closeTenancy() + }) + } + } + + const existing = cls.getFromContext(ContextKey.IDENTITY) + return updateUsing(ContextKey.IDENTITY_IN_USE, existing, internal) +} + +export const getIdentity = (): IdentityContext | undefined => { + try { + return cls.getFromContext(ContextKey.IDENTITY) + } catch (e) { + // do nothing - identity is not in context + } +} + +export const updateTenantId = (tenantId: string | null) => { + cls.setOnContext(ContextKey.TENANT_ID, tenantId) + if (env.USE_COUCH) { + setGlobalDB(tenantId) + } +} + +export const updateAppId = async (appId: string) => { + try { + // have to close first, before removing the databases from context + await closeAppDBs() + cls.setOnContext(ContextKey.APP_ID, appId) + } catch (err) { + if (env.isTest()) { + TEST_APP_ID = appId + } else { + throw err + } + } +} + +export const setGlobalDB = (tenantId: string | null) => { + const dbName = baseGlobalDBName(tenantId) + const db = dangerousGetDB(dbName) + cls.setOnContext(ContextKey.GLOBAL_DB, db) + return db +} + +export const getGlobalDB = () => { + const db = cls.getFromContext(ContextKey.GLOBAL_DB) + if (!db) { + throw new Error("Global DB not found") + } + return db +} + +export const isTenantIdSet = () => { + const tenantId = cls.getFromContext(ContextKey.TENANT_ID) + return !!tenantId +} + +export const getTenantId = () => { + if (!isMultiTenant()) { + return DEFAULT_TENANT_ID + } + const tenantId = cls.getFromContext(ContextKey.TENANT_ID) + if (!tenantId) { + throw new Error("Tenant id not found") + } + return tenantId +} + +export const getAppId = () => { + const foundId = cls.getFromContext(ContextKey.APP_ID) + if (!foundId && env.isTest() && TEST_APP_ID) { + return TEST_APP_ID + } else { + return foundId + } +} + +export const isTenancyEnabled = () => { + return env.MULTI_TENANCY +} + +/** + * Opens the app database based on whatever the request + * contained, dev or prod. + */ +export const getAppDB = (opts?: any) => { + return getContextDB(ContextKey.CURRENT_DB, opts) +} + +/** + * This specifically gets the prod app ID, if the request + * contained a development app ID, this will open the prod one. + */ +export const getProdAppDB = (opts?: any) => { + return getContextDB(ContextKey.PROD_DB, opts) +} + +/** + * This specifically gets the dev app ID, if the request + * contained a prod app ID, this will open the dev one. + */ +export const getDevAppDB = (opts?: any) => { + return getContextDB(ContextKey.DEV_DB, opts) +} diff --git a/packages/backend-core/src/context/tests/index.spec.ts b/packages/backend-core/src/context/tests/index.spec.ts new file mode 100644 index 0000000000..55ecd333a3 --- /dev/null +++ b/packages/backend-core/src/context/tests/index.spec.ts @@ -0,0 +1,148 @@ +import "../../../tests/utilities/TestConfiguration" +import * as context from ".." +import { DEFAULT_TENANT_ID } from "../../constants" +import env from "../../environment" + +// must use require to spy index file exports due to known issue in jest +const dbUtils = require("../../db") +jest.spyOn(dbUtils, "closeDB") +jest.spyOn(dbUtils, "dangerousGetDB") + +describe("context", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe("doInTenant", () => { + describe("single-tenancy", () => { + it("defaults to the default tenant", () => { + const tenantId = context.getTenantId() + expect(tenantId).toBe(DEFAULT_TENANT_ID) + }) + + it("defaults to the default tenant db", async () => { + await context.doInTenant(DEFAULT_TENANT_ID, () => { + const db = context.getGlobalDB() + expect(db.name).toBe("global-db") + }) + expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1) + expect(dbUtils.closeDB).toHaveBeenCalledTimes(1) + }) + }) + + describe("multi-tenancy", () => { + beforeEach(() => { + env._set("MULTI_TENANCY", 1) + }) + + it("fails when no tenant id is set", () => { + const test = () => { + let error + try { + context.getTenantId() + } catch (e: any) { + error = e + } + expect(error.message).toBe("Tenant id not found") + } + + // test under no tenancy + test() + + // test after tenancy has been accessed to ensure cleanup + context.doInTenant("test", () => {}) + test() + }) + + it("fails when no tenant db is set", () => { + const test = () => { + let error + try { + context.getGlobalDB() + } catch (e: any) { + error = e + } + expect(error.message).toBe("Global DB not found") + } + + // test under no tenancy + test() + + // test after tenancy has been accessed to ensure cleanup + context.doInTenant("test", () => {}) + test() + }) + + it("sets tenant id", () => { + context.doInTenant("test", () => { + const tenantId = context.getTenantId() + expect(tenantId).toBe("test") + }) + }) + + it("initialises the tenant db", async () => { + await context.doInTenant("test", () => { + const db = context.getGlobalDB() + expect(db.name).toBe("test_global-db") + }) + expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1) + expect(dbUtils.closeDB).toHaveBeenCalledTimes(1) + }) + + it("sets the tenant id when nested with same tenant id", async () => { + await context.doInTenant("test", async () => { + const tenantId = context.getTenantId() + expect(tenantId).toBe("test") + + await context.doInTenant("test", async () => { + const tenantId = context.getTenantId() + expect(tenantId).toBe("test") + + await context.doInTenant("test", () => { + const tenantId = context.getTenantId() + expect(tenantId).toBe("test") + }) + }) + }) + }) + + it("initialises the tenant db when nested with same tenant id", async () => { + await context.doInTenant("test", async () => { + const db = context.getGlobalDB() + expect(db.name).toBe("test_global-db") + + await context.doInTenant("test", async () => { + const db = context.getGlobalDB() + expect(db.name).toBe("test_global-db") + + await context.doInTenant("test", () => { + const db = context.getGlobalDB() + expect(db.name).toBe("test_global-db") + }) + }) + }) + + // only 1 db is opened and closed + expect(dbUtils.dangerousGetDB).toHaveBeenCalledTimes(1) + expect(dbUtils.closeDB).toHaveBeenCalledTimes(1) + }) + + it("sets different tenant id inside another context", () => { + context.doInTenant("test", () => { + const tenantId = context.getTenantId() + expect(tenantId).toBe("test") + + context.doInTenant("nested", () => { + const tenantId = context.getTenantId() + expect(tenantId).toBe("nested") + + context.doInTenant("double-nested", () => { + const tenantId = context.getTenantId() + expect(tenantId).toBe("double-nested") + }) + }) + }) + }) + }) + }) +}) diff --git a/packages/backend-core/src/context/utils.ts b/packages/backend-core/src/context/utils.ts new file mode 100644 index 0000000000..6e7100b594 --- /dev/null +++ b/packages/backend-core/src/context/utils.ts @@ -0,0 +1,109 @@ +import { + DEFAULT_TENANT_ID, + getAppId, + getTenantIDFromAppID, + updateTenantId, +} from "./index" +import cls from "./FunctionContext" +import { IdentityContext } from "@budibase/types" +import { ContextKey } from "./constants" +import { dangerousGetDB, closeDB } from "../db" +import { isEqual } from "lodash" +import { getDevelopmentAppID, getProdAppID } from "../db/conversions" +import env from "../environment" + +export async function updateUsing( + usingKey: string, + existing: boolean, + internal: (opts: { existing: boolean }) => Promise +) { + const using = cls.getFromContext(usingKey) + if (using && existing) { + cls.setOnContext(usingKey, using + 1) + return internal({ existing: true }) + } else { + return cls.run(async () => { + cls.setOnContext(usingKey, 1) + return internal({ existing: false }) + }) + } +} + +export async function closeWithUsing( + usingKey: string, + closeFn: () => Promise +) { + const using = cls.getFromContext(usingKey) + if (!using || using <= 1) { + await closeFn() + } else { + cls.setOnContext(usingKey, using - 1) + } +} + +export const setAppTenantId = (appId: string) => { + const appTenantId = getTenantIDFromAppID(appId) || DEFAULT_TENANT_ID + updateTenantId(appTenantId) +} + +export const setIdentity = (identity: IdentityContext | null) => { + cls.setOnContext(ContextKey.IDENTITY, identity) +} + +// this function makes sure the PouchDB objects are closed and +// fully deleted when finished - this protects against memory leaks +export async function closeAppDBs() { + const dbKeys = [ContextKey.CURRENT_DB, ContextKey.PROD_DB, ContextKey.DEV_DB] + for (let dbKey of dbKeys) { + const db = cls.getFromContext(dbKey) + if (!db) { + continue + } + await closeDB(db) + // clear the DB from context, incase someone tries to use it again + cls.setOnContext(dbKey, null) + } + // clear the app ID now that the databases are closed + if (cls.getFromContext(ContextKey.APP_ID)) { + cls.setOnContext(ContextKey.APP_ID, null) + } + if (cls.getFromContext(ContextKey.DB_OPTS)) { + cls.setOnContext(ContextKey.DB_OPTS, null) + } +} + +export function getContextDB(key: string, opts: any) { + const dbOptsKey = `${key}${ContextKey.DB_OPTS}` + let storedOpts = cls.getFromContext(dbOptsKey) + let db = cls.getFromContext(key) + if (db && isEqual(opts, storedOpts)) { + return db + } + + const appId = getAppId() + let toUseAppId + + switch (key) { + case ContextKey.CURRENT_DB: + toUseAppId = appId + break + case ContextKey.PROD_DB: + toUseAppId = getProdAppID(appId) + break + case ContextKey.DEV_DB: + toUseAppId = getDevelopmentAppID(appId) + break + } + db = dangerousGetDB(toUseAppId, opts) + try { + cls.setOnContext(key, db) + if (opts) { + cls.setOnContext(dbOptsKey, opts) + } + } catch (err) { + if (!env.isTest()) { + throw err + } + } + return db +} diff --git a/packages/backend-core/src/db/Replication.js b/packages/backend-core/src/db/Replication.ts similarity index 72% rename from packages/backend-core/src/db/Replication.js rename to packages/backend-core/src/db/Replication.ts index 437d07e536..e0bd3c7a43 100644 --- a/packages/backend-core/src/db/Replication.js +++ b/packages/backend-core/src/db/Replication.ts @@ -1,12 +1,17 @@ -const { dangerousGetDB, closeDB } = require(".") +import { dangerousGetDB, closeDB } from "." +import { DocumentType } from "./constants" class Replication { + source: any + target: any + replication: any + /** * * @param {String} source - the DB you want to replicate or rollback to * @param {String} target - the DB you want to replicate to, or rollback from */ - constructor({ source, target }) { + constructor({ source, target }: any) { this.source = dangerousGetDB(source) this.target = dangerousGetDB(target) } @@ -15,17 +20,17 @@ class Replication { return Promise.all([closeDB(this.source), closeDB(this.target)]) } - promisify(operation, opts = {}) { + promisify(operation: any, opts = {}) { return new Promise(resolve => { operation(this.target, opts) - .on("denied", function (err) { + .on("denied", function (err: any) { // a document failed to replicate (e.g. due to permissions) throw new Error(`Denied: Document failed to replicate ${err}`) }) - .on("complete", function (info) { + .on("complete", function (info: any) { return resolve(info) }) - .on("error", function (err) { + .on("error", function (err: any) { throw new Error(`Replication Error: ${err}`) }) }) @@ -49,6 +54,14 @@ class Replication { return this.replication } + appReplicateOpts() { + return { + filter: (doc: any) => { + return doc._id !== DocumentType.APP_METADATA + }, + } + } + /** * Rollback the target DB back to the state of the source DB */ @@ -56,6 +69,7 @@ class Replication { await this.target.destroy() // Recreate the DB again this.target = dangerousGetDB(this.target.name) + // take the opportunity to remove deleted tombstones await this.replicate() } @@ -64,4 +78,4 @@ class Replication { } } -module.exports = Replication +export default Replication diff --git a/packages/backend-core/src/db/constants.js b/packages/backend-core/src/db/constants.js deleted file mode 100644 index 221f51264b..0000000000 --- a/packages/backend-core/src/db/constants.js +++ /dev/null @@ -1,44 +0,0 @@ -exports.SEPARATOR = "_" - -const PRE_APP = "app" -const PRE_DEV = "dev" - -exports.DocumentTypes = { - USER: "us", - WORKSPACE: "workspace", - CONFIG: "config", - TEMPLATE: "template", - APP: PRE_APP, - DEV: PRE_DEV, - APP_DEV: `${PRE_APP}${exports.SEPARATOR}${PRE_DEV}`, - APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`, - ROLE: "role", - MIGRATIONS: "migrations", - DEV_INFO: "devinfo", - TABLE: "ta", - ROW: "ro", - DATASOURCE: "datasource", - DATASOURCE_PLUS: "datasource_plus", -} - -exports.StaticDatabases = { - GLOBAL: { - name: "global-db", - docs: { - apiKeys: "apikeys", - usageQuota: "usage_quota", - licenseInfo: "license_info", - }, - }, - // contains information about tenancy and so on - PLATFORM_INFO: { - name: "global-info", - docs: { - tenants: "tenants", - }, - }, -} - -exports.APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR -exports.APP_DEV = exports.APP_DEV_PREFIX = - exports.DocumentTypes.APP_DEV + exports.SEPARATOR diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/db/constants.ts new file mode 100644 index 0000000000..a61e8a2af2 --- /dev/null +++ b/packages/backend-core/src/db/constants.ts @@ -0,0 +1,75 @@ +export const SEPARATOR = "_" +export const UNICODE_MAX = "\ufff0" + +/** + * Can be used to create a few different forms of querying a view. + */ +export enum AutomationViewMode { + ALL = "all", + AUTOMATION = "automation", + STATUS = "status", +} + +export enum ViewName { + USER_BY_APP = "by_app", + USER_BY_EMAIL = "by_email2", + BY_API_KEY = "by_api_key", + USER_BY_BUILDERS = "by_builders", + LINK = "by_link", + ROUTING = "screen_routes", + AUTOMATION_LOGS = "automation_logs", + ACCOUNT_BY_EMAIL = "account_by_email", + PLATFORM_USERS_LOWERCASE = "platform_users_lowercase", + USER_BY_GROUP = "by_group_user", +} + +export const DeprecatedViews = { + [ViewName.USER_BY_EMAIL]: [ + // removed due to inaccuracy in view doc filter logic + "by_email", + ], +} + +export enum DocumentType { + USER = "us", + GROUP = "gr", + WORKSPACE = "workspace", + CONFIG = "config", + TEMPLATE = "template", + APP = "app", + DEV = "dev", + APP_DEV = "app_dev", + APP_METADATA = "app_metadata", + ROLE = "role", + MIGRATIONS = "migrations", + DEV_INFO = "devinfo", + AUTOMATION_LOG = "log_au", + ACCOUNT_METADATA = "acc_metadata", + PLUGIN = "plg", + TABLE = "ta", + DATASOURCE = "datasource", + DATASOURCE_PLUS = "datasource_plus", +} + +export const StaticDatabases = { + GLOBAL: { + name: "global-db", + docs: { + apiKeys: "apikeys", + usageQuota: "usage_quota", + licenseInfo: "license_info", + }, + }, + // contains information about tenancy and so on + PLATFORM_INFO: { + name: "global-info", + docs: { + tenants: "tenants", + install: "install", + }, + }, +} + +export const APP_PREFIX = DocumentType.APP + SEPARATOR +export const APP_DEV = DocumentType.APP_DEV + SEPARATOR +export const APP_DEV_PREFIX = APP_DEV diff --git a/packages/backend-core/src/db/conversions.js b/packages/backend-core/src/db/conversions.js index 455cc712d8..90c04e9251 100644 --- a/packages/backend-core/src/db/conversions.js +++ b/packages/backend-core/src/db/conversions.js @@ -50,3 +50,8 @@ exports.getProdAppID = appId => { const rest = split.join(APP_DEV_PREFIX) return `${APP_PREFIX}${rest}` } + +exports.extractAppUUID = id => { + const split = id?.split("_") || [] + return split.length ? split[split.length - 1] : null +} diff --git a/packages/backend-core/src/db/index.js b/packages/backend-core/src/db/index.js index d179186988..aa6f7ebc2c 100644 --- a/packages/backend-core/src/db/index.js +++ b/packages/backend-core/src/db/index.js @@ -1,15 +1,26 @@ const pouch = require("./pouch") const env = require("../environment") +const openDbs = [] let PouchDB let initialised = false const dbList = new Set() +if (env.MEMORY_LEAK_CHECK) { + setInterval(() => { + console.log("--- OPEN DBS ---") + console.log(openDbs) + }, 5000) +} + const put = dbPut => async (doc, options = {}) => { - // TODO: add created / updated - return await dbPut(doc, options) + if (!doc.createdAt) { + doc.createdAt = new Date().toISOString() + } + doc.updatedAt = new Date().toISOString() + return dbPut(doc, options) } const checkInitialised = () => { @@ -32,6 +43,9 @@ exports.dangerousGetDB = (dbName, opts) => { dbList.add(dbName) } const db = new PouchDB(dbName, opts) + if (env.MEMORY_LEAK_CHECK) { + openDbs.push(db.name) + } const dbPut = db.put db.put = put(dbPut) return db @@ -43,6 +57,9 @@ exports.closeDB = async db => { if (!db || env.isTest()) { return } + if (env.MEMORY_LEAK_CHECK) { + openDbs.splice(openDbs.indexOf(db.name), 1) + } try { // specifically await so that if there is an error, it can be ignored return await db.close() @@ -54,7 +71,7 @@ exports.closeDB = async db => { // we have to use a callback for this so that we can close // the DB when we're done, without this manual requests would // need to close the database when done with it to avoid memory leaks -exports.doWithDB = async (dbName, cb, opts) => { +exports.doWithDB = async (dbName, cb, opts = {}) => { const db = exports.dangerousGetDB(dbName, opts) // need this to be async so that we can correctly close DB after all // async operations have been completed diff --git a/packages/backend-core/src/db/pouch.js b/packages/backend-core/src/db/pouch.js index 76390ac644..12d7d787e3 100644 --- a/packages/backend-core/src/db/pouch.js +++ b/packages/backend-core/src/db/pouch.js @@ -1,21 +1,42 @@ const PouchDB = require("pouchdb") const env = require("../environment") -function getUrlInfo() { - let url = env.COUCH_DB_URL - let username, password, host - const [protocol, rest] = url.split("://") - if (url.includes("@")) { - const hostParts = rest.split("@") - host = hostParts[1] - const authParts = hostParts[0].split(":") - username = authParts[0] - password = authParts[1] - } else { - host = rest +exports.getUrlInfo = (url = env.COUCH_DB_URL) => { + let cleanUrl, username, password, host + if (url) { + // Ensure the URL starts with a protocol + const protoRegex = /^https?:\/\//i + if (!protoRegex.test(url)) { + url = `http://${url}` + } + + // Split into protocol and remainder + const split = url.split("://") + const protocol = split[0] + const rest = split.slice(1).join("://") + + // Extract auth if specified + if (url.includes("@")) { + // Split into host and remainder + let parts = rest.split("@") + host = parts[parts.length - 1] + let auth = parts.slice(0, -1).join("@") + + // Split auth into username and password + if (auth.includes(":")) { + const authParts = auth.split(":") + username = authParts[0] + password = authParts.slice(1).join(":") + } else { + username = auth + } + } else { + host = rest + } + cleanUrl = `${protocol}://${host}` } return { - url: `${protocol}://${host}`, + url: cleanUrl, auth: { username, password, @@ -24,7 +45,7 @@ function getUrlInfo() { } exports.getCouchInfo = () => { - const urlInfo = getUrlInfo() + const urlInfo = exports.getUrlInfo() let username let password if (env.COUCH_DB_USERNAME) { @@ -81,6 +102,13 @@ exports.getPouch = (opts = {}) => { } } + if (opts.onDisk) { + POUCH_DB_DEFAULTS = { + prefix: undefined, + adapter: "leveldb", + } + } + if (opts.replication) { const replicationStream = require("pouchdb-replication-stream") PouchDB.plugin(replicationStream.plugin) diff --git a/packages/backend-core/src/db/tenancy.ts b/packages/backend-core/src/db/tenancy.ts new file mode 100644 index 0000000000..d920f7cd41 --- /dev/null +++ b/packages/backend-core/src/db/tenancy.ts @@ -0,0 +1,22 @@ +import { DEFAULT_TENANT_ID } from "../constants" +import { StaticDatabases, SEPARATOR } from "./constants" +import { getTenantId } from "../context" + +export const getGlobalDBName = (tenantId?: string) => { + // tenant ID can be set externally, for example user API where + // new tenants are being created, this may be the case + if (!tenantId) { + tenantId = getTenantId() + } + return baseGlobalDBName(tenantId) +} + +export const baseGlobalDBName = (tenantId: string | undefined | null) => { + let dbName + if (!tenantId || tenantId === DEFAULT_TENANT_ID) { + dbName = StaticDatabases.GLOBAL.name + } else { + dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}` + } + return dbName +} diff --git a/packages/backend-core/src/db/tests/index.spec.js b/packages/backend-core/src/db/tests/index.spec.js new file mode 100644 index 0000000000..bc0c638126 --- /dev/null +++ b/packages/backend-core/src/db/tests/index.spec.js @@ -0,0 +1,26 @@ +require("../../../tests/utilities/TestConfiguration") +const { dangerousGetDB } = require("../") + +describe("db", () => { + + describe("getDB", () => { + it("returns a db", async () => { + const db = dangerousGetDB("test") + expect(db).toBeDefined() + expect(db._adapter).toBe("memory") + expect(db.prefix).toBe("_pouch_") + expect(db.name).toBe("test") + }) + + it("uses the custom put function", async () => { + const db = dangerousGetDB("test") + let doc = { _id: "test" } + await db.put(doc) + doc = await db.get(doc._id) + expect(doc.createdAt).toBe(new Date().toISOString()) + expect(doc.updatedAt).toBe(new Date().toISOString()) + await db.destroy() + }) + }) +}) + diff --git a/packages/backend-core/src/db/tests/pouch.spec.js b/packages/backend-core/src/db/tests/pouch.spec.js new file mode 100644 index 0000000000..30cdd0f5ec --- /dev/null +++ b/packages/backend-core/src/db/tests/pouch.spec.js @@ -0,0 +1,62 @@ +require("../../../tests/utilities/TestConfiguration") +const getUrlInfo = require("../pouch").getUrlInfo + +describe("pouch", () => { + describe("Couch DB URL parsing", () => { + it("should handle a null Couch DB URL", () => { + const info = getUrlInfo(null) + expect(info.url).toBeUndefined() + expect(info.auth.username).toBeUndefined() + }) + it("should be able to parse a basic Couch DB URL", () => { + const info = getUrlInfo("http://host.com") + expect(info.url).toBe("http://host.com") + expect(info.auth.username).toBeUndefined() + }) + it("should be able to parse a Couch DB basic URL with HTTPS", () => { + const info = getUrlInfo("https://host.com") + expect(info.url).toBe("https://host.com") + expect(info.auth.username).toBeUndefined() + }) + it("should be able to parse a basic Couch DB URL with a custom port", () => { + const info = getUrlInfo("https://host.com:1234") + expect(info.url).toBe("https://host.com:1234") + expect(info.auth.username).toBeUndefined() + }) + it("should be able to parse a Couch DB URL with auth", () => { + const info = getUrlInfo("https://user:pass@host.com:1234") + expect(info.url).toBe("https://host.com:1234") + expect(info.auth.username).toBe("user") + expect(info.auth.password).toBe("pass") + }) + it("should be able to parse a Couch DB URL with auth and special chars", () => { + const info = getUrlInfo("https://user:s:p@s://@://:d@;][~s@host.com:1234") + expect(info.url).toBe("https://host.com:1234") + expect(info.auth.username).toBe("user") + expect(info.auth.password).toBe("s:p@s://@://:d@;][~s") + }) + it("should be able to parse a Couch DB URL without a protocol", () => { + const info = getUrlInfo("host.com:1234") + expect(info.url).toBe("http://host.com:1234") + expect(info.auth.username).toBeUndefined() + }) + it("should be able to parse a Couch DB URL with auth and without a protocol", () => { + const info = getUrlInfo("user:s:p@s://@://:d@;][~s@host.com:1234") + expect(info.url).toBe("http://host.com:1234") + expect(info.auth.username).toBe("user") + expect(info.auth.password).toBe("s:p@s://@://:d@;][~s") + }) + it("should be able to parse a Couch DB URL with only username auth", () => { + const info = getUrlInfo("https://user@host.com:1234") + expect(info.url).toBe("https://host.com:1234") + expect(info.auth.username).toBe("user") + expect(info.auth.password).toBeUndefined() + }) + it("should be able to parse a Couch DB URL with only username auth and without a protocol", () => { + const info = getUrlInfo("user@host.com:1234") + expect(info.url).toBe("http://host.com:1234") + expect(info.auth.username).toBe("user") + expect(info.auth.password).toBeUndefined() + }) + }) +}) diff --git a/packages/backend-core/src/db/tests/utils.spec.js b/packages/backend-core/src/db/tests/utils.spec.js index f8b9549d46..5f9a224e7a 100644 --- a/packages/backend-core/src/db/tests/utils.spec.js +++ b/packages/backend-core/src/db/tests/utils.spec.js @@ -1,4 +1,4 @@ -require("../../tests/utilities/dbConfig"); +require("../../../tests/utilities/TestConfiguration"); const { generateAppID, getDevelopmentAppID, diff --git a/packages/backend-core/src/db/utils.js b/packages/backend-core/src/db/utils.ts similarity index 56% rename from packages/backend-core/src/db/utils.js rename to packages/backend-core/src/db/utils.ts index 7f0857acc1..ab36b497e7 100644 --- a/packages/backend-core/src/db/utils.js +++ b/packages/backend-core/src/db/utils.ts @@ -1,53 +1,28 @@ -const { newid } = require("../hashing") -const Replication = require("./Replication") -const { DEFAULT_TENANT_ID, Configs } = require("../constants") -const env = require("../environment") -const { - StaticDatabases, - SEPARATOR, - DocumentTypes, - APP_PREFIX, - APP_DEV, -} = require("./constants") -const { getTenantId, getGlobalDBName, getGlobalDB } = require("../tenancy") -const fetch = require("node-fetch") -const { doWithDB, allDbs } = require("./index") -const { getCouchInfo } = require("./pouch") -const { getAppMetadata } = require("../cache/appMetadata") -const { checkSlashesInUrl } = require("../helpers") -const { - isDevApp, - isProdAppID, - isDevAppID, - getDevelopmentAppID, - getProdAppID, -} = require("./conversions") +import { newid } from "../hashing" +import { DEFAULT_TENANT_ID, Configs } from "../constants" +import env from "../environment" +import { SEPARATOR, DocumentType, UNICODE_MAX, ViewName } from "./constants" +import { getTenantId, getGlobalDB } from "../context" +import { getGlobalDBName } from "./tenancy" +import fetch from "node-fetch" +import { doWithDB, allDbs } from "./index" +import { getCouchInfo } from "./pouch" +import { getAppMetadata } from "../cache/appMetadata" +import { checkSlashesInUrl } from "../helpers" +import { isDevApp, isDevAppID, getProdAppID } from "./conversions" +import { APP_PREFIX } from "./constants" +import * as events from "../events" -const UNICODE_MAX = "\ufff0" - -exports.ViewNames = { - USER_BY_EMAIL: "by_email", - BY_API_KEY: "by_api_key", - USER_BY_BUILDERS: "by_builders", -} - -exports.StaticDatabases = StaticDatabases - -exports.DocumentTypes = DocumentTypes -exports.APP_PREFIX = APP_PREFIX -exports.APP_DEV = exports.APP_DEV_PREFIX = APP_DEV -exports.SEPARATOR = SEPARATOR -exports.isDevApp = isDevApp -exports.isProdAppID = isProdAppID -exports.isDevAppID = isDevAppID -exports.getDevelopmentAppID = getDevelopmentAppID -exports.getProdAppID = getProdAppID +export * from "./constants" +export * from "./conversions" +export { default as Replication } from "./Replication" +export * from "./tenancy" /** * Generates a new app ID. * @returns {string} The new app ID which the app doc can be stored under. */ -exports.generateAppID = (tenantId = null) => { +export const generateAppID = (tenantId = null) => { let id = APP_PREFIX if (tenantId) { id += `${tenantId}${SEPARATOR}` @@ -67,7 +42,11 @@ exports.generateAppID = (tenantId = null) => { * @param {object} otherProps Add any other properties onto the request, e.g. include_docs. * @returns {object} Parameters which can then be used with an allDocs request. */ -function getDocParams(docType, docId = null, otherProps = {}) { +export function getDocParams( + docType: any, + docId: any = null, + otherProps: any = {} +) { if (docId == null) { docId = "" } @@ -77,18 +56,24 @@ function getDocParams(docType, docId = null, otherProps = {}) { endkey: `${docType}${SEPARATOR}${docId}${UNICODE_MAX}`, } } -exports.getDocParams = getDocParams + +/** + * Retrieve the correct index for a view based on default design DB. + */ +export function getQueryIndex(viewName: ViewName) { + return `database/${viewName}` +} /** * Check if a given ID is that of a table. * @returns {boolean} */ -exports.isTableId = id => { +exports.isTableId = (id: string) => { // this includes datasource plus tables return ( id && - (id.startsWith(`${DocumentTypes.TABLE}${SEPARATOR}`) || - id.startsWith(`${DocumentTypes.DATASOURCE_PLUS}${SEPARATOR}`)) + (id.startsWith(`${DocumentType.TABLE}${SEPARATOR}`) || + id.startsWith(`${DocumentType.DATASOURCE_PLUS}${SEPARATOR}`)) ) } @@ -96,27 +81,27 @@ exports.isTableId = id => { * Check if a given ID is that of a datasource or datasource plus. * @returns {boolean} */ -exports.isDatasourceId = id => { +exports.isDatasourceId = (id: string) => { // this covers both datasources and datasource plus - return id && id.startsWith(`${DocumentTypes.DATASOURCE}${SEPARATOR}`) + return id && id.startsWith(`${DocumentType.DATASOURCE}${SEPARATOR}`) } /** * Generates a new workspace ID. * @returns {string} The new workspace ID which the workspace doc can be stored under. */ -exports.generateWorkspaceID = () => { - return `${DocumentTypes.WORKSPACE}${SEPARATOR}${newid()}` +export function generateWorkspaceID() { + return `${DocumentType.WORKSPACE}${SEPARATOR}${newid()}` } /** * Gets parameters for retrieving workspaces. */ -exports.getWorkspaceParams = (id = "", otherProps = {}) => { +export function getWorkspaceParams(id = "", otherProps = {}) { return { ...otherProps, - startkey: `${DocumentTypes.WORKSPACE}${SEPARATOR}${id}`, - endkey: `${DocumentTypes.WORKSPACE}${SEPARATOR}${id}${UNICODE_MAX}`, + startkey: `${DocumentType.WORKSPACE}${SEPARATOR}${id}`, + endkey: `${DocumentType.WORKSPACE}${SEPARATOR}${id}${UNICODE_MAX}`, } } @@ -124,21 +109,34 @@ exports.getWorkspaceParams = (id = "", otherProps = {}) => { * Generates a new global user ID. * @returns {string} The new user ID which the user doc can be stored under. */ -exports.generateGlobalUserID = id => { - return `${DocumentTypes.USER}${SEPARATOR}${id || newid()}` +export function generateGlobalUserID(id?: any) { + return `${DocumentType.USER}${SEPARATOR}${id || newid()}` } /** * Gets parameters for retrieving users. */ -exports.getGlobalUserParams = (globalId, otherProps = {}) => { +export function getGlobalUserParams(globalId: any, otherProps: any = {}) { if (!globalId) { globalId = "" } + const startkey = otherProps?.startkey return { ...otherProps, - startkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}`, - endkey: `${DocumentTypes.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`, + // need to include this incase pagination + startkey: startkey + ? startkey + : `${DocumentType.USER}${SEPARATOR}${globalId}`, + endkey: `${DocumentType.USER}${SEPARATOR}${globalId}${UNICODE_MAX}`, + } +} + +export function getUsersByAppParams(appId: any, otherProps: any = {}) { + const prodAppId = getProdAppID(appId) + return { + ...otherProps, + startkey: prodAppId, + endkey: `${prodAppId}${UNICODE_MAX}`, } } @@ -146,14 +144,22 @@ exports.getGlobalUserParams = (globalId, otherProps = {}) => { * Generates a template ID. * @param ownerId The owner/user of the template, this could be global or a workspace level. */ -exports.generateTemplateID = ownerId => { - return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}` +export function generateTemplateID(ownerId: any) { + return `${DocumentType.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}` +} + +export function generateAppUserID(prodAppId: string, userId: string) { + return `${prodAppId}${SEPARATOR}${userId}` } /** * Gets parameters for retrieving templates. Owner ID must be specified, either global or a workspace level. */ -exports.getTemplateParams = (ownerId, templateId, otherProps = {}) => { +export function getTemplateParams( + ownerId: any, + templateId: any, + otherProps = {} +) { if (!templateId) { templateId = "" } @@ -161,7 +167,7 @@ exports.getTemplateParams = (ownerId, templateId, otherProps = {}) => { if (templateId) { final = templateId } else { - final = `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}` + final = `${DocumentType.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}` } return { ...otherProps, @@ -174,18 +180,18 @@ exports.getTemplateParams = (ownerId, templateId, otherProps = {}) => { * Generates a new role ID. * @returns {string} The new role ID which the role doc can be stored under. */ -exports.generateRoleID = id => { - return `${DocumentTypes.ROLE}${SEPARATOR}${id || newid()}` +export function generateRoleID(id: any) { + return `${DocumentType.ROLE}${SEPARATOR}${id || newid()}` } /** * Gets parameters for retrieving a role, this is a utility function for the getDocParams function. */ -exports.getRoleParams = (roleId = null, otherProps = {}) => { - return getDocParams(DocumentTypes.ROLE, roleId, otherProps) +export function getRoleParams(roleId = null, otherProps = {}) { + return getDocParams(DocumentType.ROLE, roleId, otherProps) } -exports.getStartEndKeyURL = (base, baseKey, tenantId = null) => { +export function getStartEndKeyURL(base: any, baseKey: any, tenantId = null) { const tenancy = tenantId ? `${SEPARATOR}${tenantId}` : "" return `${base}?startkey="${baseKey}${tenancy}"&endkey="${baseKey}${tenancy}${UNICODE_MAX}"` } @@ -196,15 +202,15 @@ exports.getStartEndKeyURL = (base, baseKey, tenantId = null) => { * opts.efficient can be provided to make sure this call is always quick in a multi-tenant environment, * but it may not be 100% accurate in full efficiency mode (some tenantless apps may be missed). */ -exports.getAllDbs = async (opts = { efficient: false }) => { +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 = [] + let dbs: any[] = [] let { url, cookie } = getCouchInfo() - async function addDbs(couchUrl) { + async function addDbs(couchUrl: string) { const response = await fetch(checkSlashesInUrl(encodeURI(couchUrl)), { method: "GET", headers: { @@ -229,13 +235,9 @@ exports.getAllDbs = async (opts = { efficient: false }) => { await addDbs(couchUrl) } else { // get prod apps - await addDbs( - exports.getStartEndKeyURL(couchUrl, DocumentTypes.APP, tenantId) - ) + await addDbs(getStartEndKeyURL(couchUrl, DocumentType.APP, tenantId)) // get dev apps - await addDbs( - exports.getStartEndKeyURL(couchUrl, DocumentTypes.APP_DEV, tenantId) - ) + await addDbs(getStartEndKeyURL(couchUrl, DocumentType.APP_DEV, tenantId)) // add global db name dbs.push(getGlobalDBName(tenantId)) } @@ -248,21 +250,25 @@ exports.getAllDbs = async (opts = { efficient: false }) => { * * @return {Promise} returns the app information document stored in each app database. */ -exports.getAllApps = async ({ dev, all, idsOnly, efficient } = {}) => { +export async function getAllApps({ dev, all, idsOnly, efficient }: any = {}) { let tenantId = getTenantId() if (!env.MULTI_TENANCY && !tenantId) { tenantId = DEFAULT_TENANT_ID } - let dbs = await exports.getAllDbs({ efficient }) - const appDbNames = dbs.filter(dbName => { + let dbs = await getAllDbs({ efficient }) + const appDbNames = dbs.filter((dbName: any) => { + if (env.isTest() && !dbName) { + return false + } + const split = dbName.split(SEPARATOR) // it is an app, check the tenantId - if (split[0] === DocumentTypes.APP) { + if (split[0] === DocumentType.APP) { // tenantId is always right before the UUID const possibleTenantId = split[split.length - 2] const noTenantId = - split.length === 2 || possibleTenantId === DocumentTypes.DEV + split.length === 2 || possibleTenantId === DocumentType.DEV return ( (tenantId === DEFAULT_TENANT_ID && noTenantId) || @@ -272,9 +278,18 @@ exports.getAllApps = async ({ dev, all, idsOnly, efficient } = {}) => { return false }) if (idsOnly) { - return appDbNames + const devAppIds = appDbNames.filter(appId => isDevAppID(appId)) + const prodAppIds = appDbNames.filter(appId => !isDevAppID(appId)) + switch (dev) { + case true: + return devAppIds + case false: + return prodAppIds + default: + return appDbNames + } } - const appPromises = appDbNames.map(app => + const appPromises = appDbNames.map((app: any) => // skip setup otherwise databases could be re-created getAppMetadata(app) ) @@ -283,17 +298,19 @@ exports.getAllApps = async ({ dev, all, idsOnly, efficient } = {}) => { } else { const response = await Promise.allSettled(appPromises) const apps = response - .filter(result => result.status === "fulfilled" && result.value != null) - .map(({ value }) => value) + .filter( + (result: any) => result.status === "fulfilled" && result.value != null + ) + .map(({ value }: any) => value) if (!all) { - return apps.filter(app => { + return apps.filter((app: any) => { if (dev) { return isDevApp(app) } return !isDevApp(app) }) } else { - return apps.map(app => ({ + return apps.map((app: any) => ({ ...app, status: isDevApp(app) ? "development" : "published", })) @@ -304,26 +321,26 @@ exports.getAllApps = async ({ dev, all, idsOnly, efficient } = {}) => { /** * Utility function for getAllApps but filters to production apps only. */ -exports.getProdAppIDs = async () => { - return (await exports.getAllApps({ idsOnly: true })).filter( - id => !exports.isDevAppID(id) +export async function getProdAppIDs() { + return (await getAllApps({ idsOnly: true })).filter( + (id: any) => !isDevAppID(id) ) } /** * Utility function for the inverse of above. */ -exports.getDevAppIDs = async () => { - return (await exports.getAllApps({ idsOnly: true })).filter(id => - exports.isDevAppID(id) +export async function getDevAppIDs() { + return (await getAllApps({ idsOnly: true })).filter((id: any) => + isDevAppID(id) ) } -exports.dbExists = async dbName => { +export async function dbExists(dbName: any) { let exists = false return doWithDB( dbName, - async db => { + async (db: any) => { try { // check if database exists const info = await db.info() @@ -343,22 +360,25 @@ exports.dbExists = async dbName => { * Generates a new configuration ID. * @returns {string} The new configuration ID which the config doc can be stored under. */ -const generateConfigID = ({ type, workspace, user }) => { +export const generateConfigID = ({ type, workspace, user }: any) => { const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR) - return `${DocumentTypes.CONFIG}${SEPARATOR}${scope}` + return `${DocumentType.CONFIG}${SEPARATOR}${scope}` } /** * Gets parameters for retrieving configurations. */ -const getConfigParams = ({ type, workspace, user }, otherProps = {}) => { +export const getConfigParams = ( + { type, workspace, user }: any, + otherProps = {} +) => { const scope = [type, workspace, user].filter(Boolean).join(SEPARATOR) return { ...otherProps, - startkey: `${DocumentTypes.CONFIG}${SEPARATOR}${scope}`, - endkey: `${DocumentTypes.CONFIG}${SEPARATOR}${scope}${UNICODE_MAX}`, + startkey: `${DocumentType.CONFIG}${SEPARATOR}${scope}`, + endkey: `${DocumentType.CONFIG}${SEPARATOR}${scope}${UNICODE_MAX}`, } } @@ -366,8 +386,23 @@ const getConfigParams = ({ type, workspace, user }, otherProps = {}) => { * 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. */ -const generateDevInfoID = userId => { - return `${DocumentTypes.DEV_INFO}${SEPARATOR}${userId}` +export const generateDevInfoID = (userId: any) => { + return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}` +} + +/** + * Generates a new plugin ID - to be used in the global DB. + * @returns {string} The new plugin ID which a plugin metadata document can be stored under. + */ +export const generatePluginID = (name: string) => { + return `${DocumentType.PLUGIN}${SEPARATOR}${name}` +} + +/** + * Gets parameters for retrieving automations, this is a utility function for the getDocParams function. + */ +export const getPluginParams = (pluginId?: string | null, otherProps = {}) => { + return getDocParams(DocumentType.PLUGIN, pluginId, otherProps) } /** @@ -376,7 +411,10 @@ const generateDevInfoID = userId => { * @param {Object} scopes - the type, workspace and userID scopes of the configuration. * @returns The most granular configuration document based on the scope. */ -const getScopedFullConfig = async function (db, { type, user, workspace }) { +export const getScopedFullConfig = async function ( + db: any, + { type, user, workspace }: any +) { const response = await db.allDocs( getConfigParams( { type, user, workspace }, @@ -386,7 +424,7 @@ const getScopedFullConfig = async function (db, { type, user, workspace }) { ) ) - function determineScore(row) { + function determineScore(row: any) { const config = row.doc // Config is specific to a user and a workspace @@ -407,19 +445,26 @@ const getScopedFullConfig = async function (db, { type, user, workspace }) { // Find the config with the most granular scope based on context let scopedConfig = response.rows.sort( - (a, b) => determineScore(a) - determineScore(b) + (a: any, b: any) => determineScore(a) - determineScore(b) )[0] // custom logic for settings doc - // always provide the platform URL if (type === Configs.SETTINGS) { if (scopedConfig && scopedConfig.doc) { - scopedConfig.doc.config.platformUrl = await getPlatformUrl() + // overrides affected by environment variables + scopedConfig.doc.config.platformUrl = await getPlatformUrl({ + tenantAware: true, + }) + scopedConfig.doc.config.analyticsEnabled = + await events.analytics.enabled() } else { + // defaults scopedConfig = { doc: { + _id: generateConfigID({ type, user, workspace }), config: { - platformUrl: await getPlatformUrl(), + platformUrl: await getPlatformUrl({ tenantAware: true }), + analyticsEnabled: await events.analytics.enabled(), }, }, } @@ -429,7 +474,7 @@ const getScopedFullConfig = async function (db, { type, user, workspace }) { return scopedConfig && scopedConfig.doc } -const getPlatformUrl = async (opts = { tenantAware: true }) => { +export const getPlatformUrl = async (opts = { tenantAware: true }) => { let platformUrl = env.PLATFORM_URL || "http://localhost:10000" if (!env.SELF_HOSTED && env.MULTI_TENANCY && opts.tenantAware) { @@ -444,7 +489,7 @@ const getPlatformUrl = async (opts = { tenantAware: true }) => { let settings try { settings = await db.get(generateConfigID({ type: Configs.SETTINGS })) - } catch (e) { + } catch (e: any) { if (e.status !== 404) { throw e } @@ -459,15 +504,41 @@ const getPlatformUrl = async (opts = { tenantAware: true }) => { return platformUrl } -async function getScopedConfig(db, params) { +export function pagination( + data: any[], + pageSize: number, + { + paginate, + property, + getKey, + }: { + paginate: boolean + property: string + getKey?: (doc: any) => string | undefined + } = { + paginate: true, + property: "_id", + } +) { + if (!paginate) { + return { data, hasNextPage: false } + } + const hasNextPage = data.length > pageSize + let nextPage = undefined + if (!getKey) { + getKey = (doc: any) => (property ? doc?.[property] : doc?._id) + } + if (hasNextPage) { + nextPage = getKey(data[pageSize]) + } + return { + data: data.slice(0, pageSize), + hasNextPage, + nextPage, + } +} + +export async function getScopedConfig(db: any, params: any) { const configDoc = await getScopedFullConfig(db, params) return configDoc && configDoc.config ? configDoc.config : configDoc } - -exports.Replication = Replication -exports.getScopedConfig = getScopedConfig -exports.generateConfigID = generateConfigID -exports.getConfigParams = getConfigParams -exports.getScopedFullConfig = getScopedFullConfig -exports.generateDevInfoID = generateDevInfoID -exports.getPlatformUrl = getPlatformUrl diff --git a/packages/backend-core/src/db/views.js b/packages/backend-core/src/db/views.js deleted file mode 100644 index e0281c6584..0000000000 --- a/packages/backend-core/src/db/views.js +++ /dev/null @@ -1,107 +0,0 @@ -const { DocumentTypes, ViewNames } = require("./utils") -const { getGlobalDB } = require("../tenancy") - -function DesignDoc() { - return { - _id: "_design/database", - // view collation information, read before writing any complex views: - // https://docs.couchdb.org/en/master/ddocs/views/collation.html#collation-specification - views: {}, - } -} - -exports.createUserEmailView = async () => { - const db = getGlobalDB() - let designDoc - try { - designDoc = await db.get("_design/database") - } catch (err) { - // no design doc, make one - designDoc = DesignDoc() - } - const view = { - // if using variables in a map function need to inject them before use - map: `function(doc) { - if (doc._id.startsWith("${DocumentTypes.USER}")) { - emit(doc.email.toLowerCase(), doc._id) - } - }`, - } - designDoc.views = { - ...designDoc.views, - [ViewNames.USER_BY_EMAIL]: view, - } - await db.put(designDoc) -} - -exports.createApiKeyView = async () => { - const db = getGlobalDB() - let designDoc - try { - designDoc = await db.get("_design/database") - } catch (err) { - designDoc = DesignDoc() - } - const view = { - map: `function(doc) { - if (doc._id.startsWith("${DocumentTypes.DEV_INFO}") && doc.apiKey) { - emit(doc.apiKey, doc.userId) - } - }`, - } - designDoc.views = { - ...designDoc.views, - [ViewNames.BY_API_KEY]: view, - } - await db.put(designDoc) -} - -exports.createUserBuildersView = async () => { - const db = getGlobalDB() - let designDoc - try { - designDoc = await db.get("_design/database") - } catch (err) { - // no design doc, make one - designDoc = DesignDoc() - } - const view = { - map: `function(doc) { - if (doc.builder && doc.builder.global === true) { - emit(doc._id, doc._id) - } - }`, - } - designDoc.views = { - ...designDoc.views, - [ViewNames.USER_BY_BUILDERS]: view, - } - await db.put(designDoc) -} - -exports.queryGlobalView = async (viewName, params, db = null) => { - const CreateFuncByName = { - [ViewNames.USER_BY_EMAIL]: exports.createUserEmailView, - [ViewNames.BY_API_KEY]: exports.createApiKeyView, - [ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView, - } - // can pass DB in if working with something specific - if (!db) { - db = getGlobalDB() - } - try { - let response = (await db.query(`database/${viewName}`, params)).rows - response = response.map(resp => - params.include_docs ? resp.doc : resp.value - ) - return response.length <= 1 ? response[0] : response - } catch (err) { - if (err != null && err.name === "not_found") { - const createFunc = CreateFuncByName[viewName] - await createFunc() - return exports.queryGlobalView(viewName, params) - } else { - throw err - } - } -} diff --git a/packages/backend-core/src/db/views.ts b/packages/backend-core/src/db/views.ts new file mode 100644 index 0000000000..f0fff918fc --- /dev/null +++ b/packages/backend-core/src/db/views.ts @@ -0,0 +1,199 @@ +import { DocumentType, ViewName, DeprecatedViews, SEPARATOR } from "./utils" +import { getGlobalDB } from "../context" +import PouchDB from "pouchdb" +import { StaticDatabases } from "./constants" +import { doWithDB } from "./" + +const DESIGN_DB = "_design/database" + +function DesignDoc() { + return { + _id: DESIGN_DB, + // view collation information, read before writing any complex views: + // https://docs.couchdb.org/en/master/ddocs/views/collation.html#collation-specification + views: {}, + } +} + +interface DesignDocument { + views: any +} + +async function removeDeprecated(db: PouchDB.Database, viewName: ViewName) { + // @ts-ignore + if (!DeprecatedViews[viewName]) { + return + } + try { + const designDoc = await db.get(DESIGN_DB) + // @ts-ignore + for (let deprecatedNames of DeprecatedViews[viewName]) { + delete designDoc.views[deprecatedNames] + } + await db.put(designDoc) + } catch (err) { + // doesn't exist, ignore + } +} + +export async function createView(db: any, viewJs: string, viewName: string) { + let designDoc + try { + designDoc = (await db.get(DESIGN_DB)) as DesignDocument + } catch (err) { + // no design doc, make one + designDoc = DesignDoc() + } + const view = { + map: viewJs, + } + designDoc.views = { + ...designDoc.views, + [viewName]: view, + } + await db.put(designDoc) +} + +export const createNewUserEmailView = async () => { + const db = getGlobalDB() + const viewJs = `function(doc) { + if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}")) { + emit(doc.email.toLowerCase(), doc._id) + } + }` + 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: PouchDB.Database) => { + await createView(db, viewJs, ViewName.ACCOUNT_BY_EMAIL) + } + ) +} + +export const createUserAppView = async () => { + const db = getGlobalDB() as PouchDB.Database + const viewJs = `function(doc) { + if (doc._id.startsWith("${DocumentType.USER}${SEPARATOR}") && doc.roles) { + for (let prodAppId of Object.keys(doc.roles)) { + let emitted = prodAppId + "${SEPARATOR}" + doc._id + emit(emitted, null) + } + } + }` + await createView(db, viewJs, ViewName.USER_BY_APP) +} + +export const createApiKeyView = async () => { + const db = getGlobalDB() + const viewJs = `function(doc) { + if (doc._id.startsWith("${DocumentType.DEV_INFO}") && doc.apiKey) { + emit(doc.apiKey, doc.userId) + } + }` + await createView(db, viewJs, ViewName.BY_API_KEY) +} + +export const createUserBuildersView = async () => { + const db = getGlobalDB() + const viewJs = `function(doc) { + if (doc.builder && doc.builder.global === true) { + emit(doc._id, doc._id) + } + }` + await createView(db, viewJs, ViewName.USER_BY_BUILDERS) +} + +export const createPlatformUserView = async () => { + const viewJs = `function(doc) { + if (doc.tenantId) { + emit(doc._id.toLowerCase(), doc._id) + } + }` + await doWithDB( + StaticDatabases.PLATFORM_INFO.name, + async (db: PouchDB.Database) => { + await createView(db, viewJs, ViewName.PLATFORM_USERS_LOWERCASE) + } + ) +} + +export interface QueryViewOptions { + arrayResponse?: boolean +} + +export const queryView = async ( + viewName: ViewName, + params: PouchDB.Query.Options, + db: PouchDB.Database, + createFunc: any, + opts?: QueryViewOptions +): Promise => { + try { + let response = await db.query(`database/${viewName}`, params) + const rows = response.rows + const docs = rows.map(row => (params.include_docs ? row.doc : row.value)) + + // if arrayResponse has been requested, always return array regardless of length + if (opts?.arrayResponse) { + return docs + } else { + // return the single document if there is only one + return docs.length <= 1 ? docs[0] : docs + } + } catch (err: any) { + if (err != null && err.name === "not_found") { + await removeDeprecated(db, viewName) + await createFunc() + return queryView(viewName, params, db, createFunc, opts) + } else { + throw err + } + } +} + +export const queryPlatformView = async ( + viewName: ViewName, + params: PouchDB.Query.Options, + opts?: QueryViewOptions +): Promise => { + const CreateFuncByName: any = { + [ViewName.ACCOUNT_BY_EMAIL]: createAccountEmailView, + [ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView, + } + + return doWithDB( + StaticDatabases.PLATFORM_INFO.name, + async (db: PouchDB.Database) => { + const createFn = CreateFuncByName[viewName] + return queryView(viewName, params, db, createFn, opts) + } + ) +} + +export const queryGlobalView = async ( + viewName: ViewName, + params: PouchDB.Query.Options, + db?: PouchDB.Database, + opts?: QueryViewOptions +): Promise => { + const CreateFuncByName: any = { + [ViewName.USER_BY_EMAIL]: createNewUserEmailView, + [ViewName.BY_API_KEY]: createApiKeyView, + [ViewName.USER_BY_BUILDERS]: createUserBuildersView, + [ViewName.USER_BY_APP]: createUserAppView, + } + // can pass DB in if working with something specific + if (!db) { + db = getGlobalDB() as PouchDB.Database + } + const createFn = CreateFuncByName[viewName] + return queryView(viewName, params, db, createFn, opts) +} diff --git a/packages/backend-core/src/environment.js b/packages/backend-core/src/environment.ts similarity index 54% rename from packages/backend-core/src/environment.js rename to packages/backend-core/src/environment.ts index fe56697011..6e2ac94be9 100644 --- a/packages/backend-core/src/environment.js +++ b/packages/backend-core/src/environment.ts @@ -16,9 +16,19 @@ if (!LOADED && isDev() && !isTest()) { LOADED = true } -module.exports = { +const DefaultBucketName = { + BACKUPS: "backups", + APPS: "prod-budi-app-assets", + TEMPLATES: "templates", + GLOBAL: "global", + CLOUD: "prod-budi-tenant-uploads", + PLUGINS: "plugins", +} + +const env = { isTest, isDev, + JS_BCRYPT: process.env.JS_BCRYPT, JWT_SECRET: process.env.JWT_SECRET, COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005", COUCH_DB_USERNAME: process.env.COUCH_DB_USER, @@ -36,31 +46,47 @@ module.exports = { MULTI_TENANCY: process.env.MULTI_TENANCY, ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app", - ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY, + 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: !!parseInt(process.env.SELF_HOSTED || ""), COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, - PLATFORM_URL: process.env.PLATFORM_URL, + PLATFORM_URL: process.env.PLATFORM_URL || "", + POSTHOG_TOKEN: process.env.POSTHOG_TOKEN, + ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS, TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS, - BACKUPS_BUCKET_NAME: process.env.BACKUPS_BUCKET_NAME || "backups", - APPS_BUCKET_NAME: process.env.APPS_BUCKET_NAME || "prod-budi-app-assets", - TEMPLATES_BUCKET_NAME: process.env.TEMPLATES_BUCKET_NAME || "templates", - GLOBAL_BUCKET_NAME: process.env.GLOBAL_BUCKET_NAME || "global", + BACKUPS_BUCKET_NAME: + process.env.BACKUPS_BUCKET_NAME || DefaultBucketName.BACKUPS, + APPS_BUCKET_NAME: process.env.APPS_BUCKET_NAME || DefaultBucketName.APPS, + TEMPLATES_BUCKET_NAME: + process.env.TEMPLATES_BUCKET_NAME || DefaultBucketName.TEMPLATES, + GLOBAL_BUCKET_NAME: + process.env.GLOBAL_BUCKET_NAME || DefaultBucketName.GLOBAL, GLOBAL_CLOUD_BUCKET_NAME: - process.env.GLOBAL_CLOUD_BUCKET_NAME || "prod-budi-tenant-uploads", + process.env.GLOBAL_CLOUD_BUCKET_NAME || DefaultBucketName.CLOUD, + PLUGIN_BUCKET_NAME: + process.env.PLUGIN_BUCKET_NAME || DefaultBucketName.PLUGINS, USE_COUCH: process.env.USE_COUCH || true, DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE, DEFAULT_LICENSE: process.env.DEFAULT_LICENSE, - _set(key, value) { + SERVICE: process.env.SERVICE || "budibase", + MEMORY_LEAK_CHECK: process.env.MEMORY_LEAK_CHECK || false, + LOG_LEVEL: process.env.LOG_LEVEL, + SESSION_UPDATE_PERIOD: process.env.SESSION_UPDATE_PERIOD, + DEPLOYMENT_ENVIRONMENT: + process.env.DEPLOYMENT_ENVIRONMENT || "docker-compose", + _set(key: any, value: any) { process.env[key] = value module.exports[key] = value }, } // clean up any environment variable edge cases -for (let [key, value] of Object.entries(module.exports)) { +for (let [key, value] of Object.entries(env)) { // handle the edge case of "0" to disable an environment variable if (value === "0") { - module.exports[key] = 0 + // @ts-ignore + env[key] = 0 } } + +export = env diff --git a/packages/backend-core/src/errors/base.js b/packages/backend-core/src/errors/base.js deleted file mode 100644 index d31f9838f4..0000000000 --- a/packages/backend-core/src/errors/base.js +++ /dev/null @@ -1,11 +0,0 @@ -class BudibaseError extends Error { - constructor(message, type, code) { - super(message) - this.type = type - this.code = code - } -} - -module.exports = { - BudibaseError, -} diff --git a/packages/backend-core/src/errors/base.ts b/packages/backend-core/src/errors/base.ts new file mode 100644 index 0000000000..801dcf168d --- /dev/null +++ b/packages/backend-core/src/errors/base.ts @@ -0,0 +1,10 @@ +export class BudibaseError extends Error { + code: string + type: string + + constructor(message: string, code: string, type: string) { + super(message) + this.code = code + this.type = type + } +} diff --git a/packages/backend-core/src/errors/generic.ts b/packages/backend-core/src/errors/generic.ts new file mode 100644 index 0000000000..71b3352438 --- /dev/null +++ b/packages/backend-core/src/errors/generic.ts @@ -0,0 +1,7 @@ +import { BudibaseError } from "./base" + +export class GenericError extends BudibaseError { + constructor(message: string, code: string, type: string) { + super(message, code, type ? type : "generic") + } +} diff --git a/packages/backend-core/src/errors/http.ts b/packages/backend-core/src/errors/http.ts new file mode 100644 index 0000000000..182e009f58 --- /dev/null +++ b/packages/backend-core/src/errors/http.ts @@ -0,0 +1,15 @@ +import { GenericError } from "./generic" + +export class HTTPError extends GenericError { + status: number + + constructor( + message: string, + httpStatus: number, + code = "http", + type = "generic" + ) { + super(message, code, type) + this.status = httpStatus + } +} diff --git a/packages/backend-core/src/errors/index.js b/packages/backend-core/src/errors/index.ts similarity index 59% rename from packages/backend-core/src/errors/index.js rename to packages/backend-core/src/errors/index.ts index 4f3b4e0c41..be6657093d 100644 --- a/packages/backend-core/src/errors/index.js +++ b/packages/backend-core/src/errors/index.ts @@ -1,18 +1,18 @@ -const licensing = require("./licensing") +import { HTTPError } from "./http" +import { UsageLimitError, FeatureDisabledError } from "./licensing" +import * as licensing from "./licensing" const codes = { ...licensing.codes, } -const types = { - ...licensing.types, -} +const types = [licensing.type] const context = { ...licensing.context, } -const getPublicError = err => { +const getPublicError = (err: any) => { let error if (err.code || err.type) { // add generic error information @@ -33,9 +33,15 @@ const getPublicError = err => { return error } -module.exports = { +const pkg = { codes, types, - UsageLimitError: licensing.UsageLimitError, + errors: { + UsageLimitError, + FeatureDisabledError, + HTTPError, + }, getPublicError, } + +export = pkg diff --git a/packages/backend-core/src/errors/licensing.js b/packages/backend-core/src/errors/licensing.js deleted file mode 100644 index c05f9c561e..0000000000 --- a/packages/backend-core/src/errors/licensing.js +++ /dev/null @@ -1,32 +0,0 @@ -const { BudibaseError } = require("./base") - -const types = { - LICENSE_ERROR: "license_error", -} - -const codes = { - USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded", -} - -const context = { - [codes.USAGE_LIMIT_EXCEEDED]: err => { - return { - limitName: err.limitName, - } - }, -} - -class UsageLimitError extends BudibaseError { - constructor(message, limitName) { - super(message, types.LICENSE_ERROR, codes.USAGE_LIMIT_EXCEEDED) - this.limitName = limitName - this.status = 400 - } -} - -module.exports = { - types, - codes, - context, - UsageLimitError, -} diff --git a/packages/backend-core/src/errors/licensing.ts b/packages/backend-core/src/errors/licensing.ts new file mode 100644 index 0000000000..7ffcefa167 --- /dev/null +++ b/packages/backend-core/src/errors/licensing.ts @@ -0,0 +1,39 @@ +import { HTTPError } from "./http" + +export const type = "license_error" + +export const codes = { + USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded", + FEATURE_DISABLED: "feature_disabled", +} + +export const context = { + [codes.USAGE_LIMIT_EXCEEDED]: (err: any) => { + return { + limitName: err.limitName, + } + }, + [codes.FEATURE_DISABLED]: (err: any) => { + return { + featureName: err.featureName, + } + }, +} + +export class UsageLimitError extends HTTPError { + limitName: string + + constructor(message: string, limitName: string) { + super(message, 400, codes.USAGE_LIMIT_EXCEEDED, type) + this.limitName = limitName + } +} + +export class FeatureDisabledError extends HTTPError { + featureName: string + + constructor(message: string, featureName: string) { + super(message, 400, codes.FEATURE_DISABLED, type) + this.featureName = featureName + } +} diff --git a/packages/backend-core/src/events/analytics.ts b/packages/backend-core/src/events/analytics.ts new file mode 100644 index 0000000000..802b6d6314 --- /dev/null +++ b/packages/backend-core/src/events/analytics.ts @@ -0,0 +1,57 @@ +import env from "../environment" +import tenancy from "../tenancy" +import * as dbUtils from "../db/utils" +import { Configs } from "../constants" +import { withCache, TTL, CacheKeys } from "../cache/generic" + +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( + CacheKeys.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: Configs.SETTINGS }) + ) + } catch (e: any) { + if (e.status !== 404) { + throw e + } + } + return settings +} diff --git a/packages/backend-core/src/events/backfill.ts b/packages/backend-core/src/events/backfill.ts new file mode 100644 index 0000000000..e4577c5ab4 --- /dev/null +++ b/packages/backend-core/src/events/backfill.ts @@ -0,0 +1,183 @@ +import { + Event, + BackfillMetadata, + CachedEvent, + SSOCreatedEvent, + AutomationCreatedEvent, + AutomationStepCreatedEvent, + DatasourceCreatedEvent, + LayoutCreatedEvent, + QueryCreatedEvent, + RoleCreatedEvent, + ScreenCreatedEvent, + TableCreatedEvent, + ViewCreatedEvent, + ViewCalculationCreatedEvent, + ViewFilterCreatedEvent, + AppPublishedEvent, + UserCreatedEvent, + RoleAssignedEvent, + UserPermissionAssignedEvent, + AppCreatedEvent, +} from "@budibase/types" +import * as context from "../context" +import { CacheKeys } from "../cache/generic" +import * as cache from "../cache/generic" + +// LIFECYCLE + +export const start = async (events: Event[]) => { + const metadata: BackfillMetadata = { + eventWhitelist: events, + } + return saveBackfillMetadata(metadata) +} + +export const recordEvent = async (event: Event, properties: any) => { + const eventKey = getEventKey(event, properties) + // don't use a ttl - cleaned up by migration + // don't use tenancy - already in the key + await cache.store(eventKey, properties, undefined, { useTenancy: false }) +} + +export const end = async () => { + await deleteBackfillMetadata() + await clearEvents() +} + +// CRUD + +const getBackfillMetadata = async (): Promise => { + return cache.get(CacheKeys.BACKFILL_METADATA) +} + +const saveBackfillMetadata = async ( + backfill: BackfillMetadata +): Promise => { + // no TTL - deleted by backfill + return cache.store(CacheKeys.BACKFILL_METADATA, backfill) +} + +const deleteBackfillMetadata = async (): Promise => { + await cache.delete(CacheKeys.BACKFILL_METADATA) +} + +const clearEvents = async () => { + // wildcard + const pattern = getEventKey() + const keys = await cache.keys(pattern) + + for (const key of keys) { + // delete each key + // don't use tenancy, already in the key + await cache.delete(key, { useTenancy: false }) + } +} + +// HELPERS + +export const isBackfillingEvent = async (event: Event) => { + const backfill = await getBackfillMetadata() + const events = backfill?.eventWhitelist + if (events && events.includes(event)) { + return true + } else { + return false + } +} + +export const isAlreadySent = async (event: Event, properties: any) => { + const eventKey = getEventKey(event, properties) + const cachedEvent: CachedEvent = await cache.get(eventKey, { + useTenancy: false, + }) + return !!cachedEvent +} + +const CUSTOM_PROPERTY_SUFFIX: any = { + // APP EVENTS + [Event.AUTOMATION_CREATED]: (properties: AutomationCreatedEvent) => { + return properties.automationId + }, + [Event.AUTOMATION_STEP_CREATED]: (properties: AutomationStepCreatedEvent) => { + return properties.stepId + }, + [Event.DATASOURCE_CREATED]: (properties: DatasourceCreatedEvent) => { + return properties.datasourceId + }, + [Event.LAYOUT_CREATED]: (properties: LayoutCreatedEvent) => { + return properties.layoutId + }, + [Event.QUERY_CREATED]: (properties: QueryCreatedEvent) => { + return properties.queryId + }, + [Event.ROLE_CREATED]: (properties: RoleCreatedEvent) => { + return properties.roleId + }, + [Event.SCREEN_CREATED]: (properties: ScreenCreatedEvent) => { + return properties.screenId + }, + [Event.TABLE_CREATED]: (properties: TableCreatedEvent) => { + return properties.tableId + }, + [Event.VIEW_CREATED]: (properties: ViewCreatedEvent) => { + return properties.tableId // best uniqueness + }, + [Event.VIEW_CALCULATION_CREATED]: ( + properties: ViewCalculationCreatedEvent + ) => { + return properties.tableId // best uniqueness + }, + [Event.VIEW_FILTER_CREATED]: (properties: ViewFilterCreatedEvent) => { + return properties.tableId // best uniqueness + }, + [Event.APP_CREATED]: (properties: AppCreatedEvent) => { + return properties.appId // best uniqueness + }, + [Event.APP_PUBLISHED]: (properties: AppPublishedEvent) => { + return properties.appId // best uniqueness + }, + // GLOBAL EVENTS + [Event.AUTH_SSO_CREATED]: (properties: SSOCreatedEvent) => { + return properties.type + }, + [Event.AUTH_SSO_ACTIVATED]: (properties: SSOCreatedEvent) => { + return properties.type + }, + [Event.USER_CREATED]: (properties: UserCreatedEvent) => { + return properties.userId + }, + [Event.USER_PERMISSION_ADMIN_ASSIGNED]: ( + properties: UserPermissionAssignedEvent + ) => { + return properties.userId + }, + [Event.USER_PERMISSION_BUILDER_ASSIGNED]: ( + properties: UserPermissionAssignedEvent + ) => { + return properties.userId + }, + [Event.ROLE_ASSIGNED]: (properties: RoleAssignedEvent) => { + return `${properties.roleId}-${properties.userId}` + }, +} + +const getEventKey = (event?: Event, properties?: any) => { + let eventKey: string + + const tenantId = context.getTenantId() + if (event) { + eventKey = `${CacheKeys.EVENTS}:${tenantId}:${event}` + + // use some properties to make the key more unique + const custom = CUSTOM_PROPERTY_SUFFIX[event] + const suffix = custom ? custom(properties) : undefined + if (suffix) { + eventKey = `${eventKey}:${suffix}` + } + } else { + eventKey = `${CacheKeys.EVENTS}:${tenantId}:*` + } + + return eventKey +} diff --git a/packages/backend-core/src/events/events.ts b/packages/backend-core/src/events/events.ts new file mode 100644 index 0000000000..cda90d12c9 --- /dev/null +++ b/packages/backend-core/src/events/events.ts @@ -0,0 +1,31 @@ +import { Event } from "@budibase/types" +import { processors } from "./processors" +import * as identification from "./identification" +import * as backfill from "./backfill" + +export const publishEvent = async ( + event: Event, + properties: any, + timestamp?: string | number +) => { + // in future this should use async events via a distributed queue. + const identity = await identification.getCurrentIdentity() + + const backfilling = await backfill.isBackfillingEvent(event) + // no backfill - send the event and exit + if (!backfilling) { + await processors.processEvent(event, identity, properties, timestamp) + return + } + + // backfill active - check if the event has been sent already + const alreadySent = await backfill.isAlreadySent(event, properties) + if (alreadySent) { + // do nothing + return + } else { + // send and record the event + await processors.processEvent(event, identity, properties, timestamp) + await backfill.recordEvent(event, properties) + } +} diff --git a/packages/backend-core/src/events/identification.ts b/packages/backend-core/src/events/identification.ts new file mode 100644 index 0000000000..a29a6821cd --- /dev/null +++ b/packages/backend-core/src/events/identification.ts @@ -0,0 +1,302 @@ +import * as context from "../context" +import * as identityCtx from "../context/identity" +import env from "../environment" +import { + Hosting, + User, + Identity, + IdentityType, + Account, + isCloudAccount, + isSSOAccount, + TenantGroup, + SettingsConfig, + CloudAccount, + UserIdentity, + InstallationGroup, + UserContext, + Group, +} from "@budibase/types" +import { processors } from "./processors" +import * as dbUtils from "../db/utils" +import { Configs } from "../constants" +import * as hashing from "../hashing" +import * as installation from "../installation" +import { withCache, TTL, CacheKeys } from "../cache/generic" + +const pkg = require("../../package.json") + +/** + * An identity can be: + * - account user (Self host) + * - budibase user + * - tenant + * - installation + */ +export const getCurrentIdentity = async (): Promise => { + let identityContext = identityCtx.getIdentity() + const environment = getDeploymentEnvironment() + + let identityType + + if (!identityContext) { + identityType = IdentityType.TENANT + } else { + identityType = identityContext.type + } + + if (identityType === IdentityType.INSTALLATION) { + const installationId = await getInstallationId() + const hosting = getHostingFromEnv() + return { + id: formatDistinctId(installationId, identityType), + hosting, + type: identityType, + installationId, + environment, + } + } else if (identityType === IdentityType.TENANT) { + const installationId = await getInstallationId() + const tenantId = await getEventTenantId(context.getTenantId()) + const hosting = getHostingFromEnv() + + return { + id: formatDistinctId(tenantId, identityType), + type: identityType, + hosting, + installationId, + tenantId, + environment, + } + } else if (identityType === IdentityType.USER) { + const userContext = identityContext as UserContext + const tenantId = await getEventTenantId(context.getTenantId()) + const installationId = await getInstallationId() + + const account = userContext.account + let hosting + if (account) { + hosting = account.hosting + } else { + hosting = getHostingFromEnv() + } + + return { + id: userContext._id, + type: identityType, + hosting, + installationId, + tenantId, + environment, + } + } else { + throw new Error("Unknown identity type") + } +} + +export const identifyInstallationGroup = async ( + installId: string, + timestamp?: string | number +): Promise => { + const id = installId + const type = IdentityType.INSTALLATION + const hosting = getHostingFromEnv() + const version = pkg.version + const environment = getDeploymentEnvironment() + + const group: InstallationGroup = { + id, + type, + hosting, + version, + environment, + } + + await identifyGroup(group, timestamp) + // need to create a normal identity for the group to be able to query it globally + // match the posthog syntax to link this identity to the empty auto generated one + await identify({ ...group, id: `$${type}_${id}` }, timestamp) +} + +export const identifyTenantGroup = async ( + tenantId: string, + account: Account | undefined, + timestamp?: string | number +): Promise => { + const id = await getEventTenantId(tenantId) + const type = IdentityType.TENANT + const installationId = await getInstallationId() + const environment = getDeploymentEnvironment() + + let hosting: Hosting + let profession: string | undefined + let companySize: string | undefined + + if (account) { + profession = account.profession + companySize = account.size + hosting = account.hosting + } else { + hosting = getHostingFromEnv() + } + + const group: TenantGroup = { + id, + type, + hosting, + environment, + installationId, + profession, + companySize, + } + + await identifyGroup(group, timestamp) + // need to create a normal identity for the group to be able to query it globally + // match the posthog syntax to link this identity to the auto generated one + await identify({ ...group, id: `$${type}_${id}` }, timestamp) +} + +export const identifyUser = async ( + user: User, + account: CloudAccount | undefined, + timestamp?: string | number +) => { + const id = user._id as string + const tenantId = await getEventTenantId(user.tenantId) + const type = IdentityType.USER + let builder = user.builder?.global || false + let admin = user.admin?.global || false + let providerType = user.providerType + const accountHolder = account?.budibaseUserId === user._id || false + const verified = + account && account?.budibaseUserId === user._id ? account.verified : false + const installationId = await getInstallationId() + const hosting = account ? account.hosting : getHostingFromEnv() + const environment = getDeploymentEnvironment() + + const identity: UserIdentity = { + id, + type, + hosting, + installationId, + tenantId, + verified, + accountHolder, + providerType, + builder, + admin, + environment, + } + + await identify(identity, timestamp) +} + +export const identifyAccount = async (account: Account) => { + let id = account.accountId + const tenantId = account.tenantId + let type = IdentityType.USER + let providerType = isSSOAccount(account) ? account.providerType : undefined + const verified = account.verified + const accountHolder = true + const hosting = account.hosting + const installationId = await getInstallationId() + const environment = getDeploymentEnvironment() + + if (isCloudAccount(account)) { + if (account.budibaseUserId) { + // use the budibase user as the id if set + id = account.budibaseUserId + } + } + + const identity: UserIdentity = { + id, + type, + hosting, + installationId, + tenantId, + providerType, + verified, + accountHolder, + environment, + } + + await identify(identity) +} + +export const identify = async ( + identity: Identity, + timestamp?: string | number +) => { + await processors.identify(identity, timestamp) +} + +export const identifyGroup = async ( + group: Group, + timestamp?: string | number +) => { + await processors.identifyGroup(group, timestamp) +} + +const getDeploymentEnvironment = () => { + if (env.isDev()) { + return "development" + } else { + return env.DEPLOYMENT_ENVIRONMENT + } +} + +const getHostingFromEnv = () => { + return env.SELF_HOSTED ? Hosting.SELF : Hosting.CLOUD +} + +export const getInstallationId = async () => { + if (isAccountPortal()) { + return "account-portal" + } + const install = await installation.getInstall() + return install.installId +} + +const getEventTenantId = async (tenantId: string): Promise => { + if (env.SELF_HOSTED) { + return getUniqueTenantId(tenantId) + } else { + // tenant id's in the cloud are already unique + return tenantId + } +} + +const getUniqueTenantId = async (tenantId: string): Promise => { + // make sure this tenantId always matches the tenantId in context + return context.doInTenant(tenantId, () => { + return withCache(CacheKeys.UNIQUE_TENANT_ID, TTL.ONE_DAY, async () => { + const db = context.getGlobalDB() + const config: SettingsConfig = await dbUtils.getScopedFullConfig(db, { + type: Configs.SETTINGS, + }) + + let uniqueTenantId: string + if (config.config.uniqueTenantId) { + return config.config.uniqueTenantId + } else { + uniqueTenantId = `${hashing.newid()}_${tenantId}` + config.config.uniqueTenantId = uniqueTenantId + await db.put(config) + return uniqueTenantId + } + }) + }) +} + +const isAccountPortal = () => { + return env.SERVICE === "account-portal" +} + +const formatDistinctId = (id: string, type: IdentityType) => { + if (type === IdentityType.INSTALLATION || type === IdentityType.TENANT) { + return `$${type}_${id}` + } else { + return id + } +} diff --git a/packages/backend-core/src/events/index.ts b/packages/backend-core/src/events/index.ts new file mode 100644 index 0000000000..f94c8b0267 --- /dev/null +++ b/packages/backend-core/src/events/index.ts @@ -0,0 +1,12 @@ +export * from "./publishers" +export * as processors from "./processors" +export * as analytics from "./analytics" +export * as identification from "./identification" +export * as backfillCache from "./backfill" + +import { processors } from "./processors" + +export const shutdown = () => { + processors.shutdown() + console.log("Events shutdown") +} diff --git a/packages/backend-core/src/events/processors/AnalyticsProcessor.ts b/packages/backend-core/src/events/processors/AnalyticsProcessor.ts new file mode 100644 index 0000000000..f9d7547120 --- /dev/null +++ b/packages/backend-core/src/events/processors/AnalyticsProcessor.ts @@ -0,0 +1,64 @@ +import { Event, Identity, Group, IdentityType } from "@budibase/types" +import { EventProcessor } from "./types" +import env from "../../environment" +import * as analytics from "../analytics" +import PosthogProcessor from "./posthog" + +/** + * Events that are always captured. + */ +const EVENT_WHITELIST = [ + Event.INSTALLATION_VERSION_UPGRADED, + Event.INSTALLATION_VERSION_DOWNGRADED, +] +const IDENTITY_WHITELIST = [IdentityType.INSTALLATION, IdentityType.TENANT] + +export default class AnalyticsProcessor implements EventProcessor { + posthog: PosthogProcessor | undefined + + constructor() { + if (env.POSTHOG_TOKEN && !env.isTest()) { + this.posthog = new PosthogProcessor(env.POSTHOG_TOKEN) + } + } + + async processEvent( + event: Event, + identity: Identity, + properties: any, + timestamp?: string | number + ): Promise { + if (!EVENT_WHITELIST.includes(event) && !(await analytics.enabled())) { + return + } + if (this.posthog) { + await this.posthog.processEvent(event, identity, properties, timestamp) + } + } + + async identify(identity: Identity, timestamp?: string | number) { + // Group indentifications (tenant and installation) always on + if ( + !IDENTITY_WHITELIST.includes(identity.type) && + !(await analytics.enabled()) + ) { + return + } + if (this.posthog) { + await this.posthog.identify(identity, timestamp) + } + } + + async identifyGroup(group: Group, timestamp?: string | number) { + // Group indentifications (tenant and installation) always on + if (this.posthog) { + await this.posthog.identifyGroup(group, timestamp) + } + } + + shutdown() { + if (this.posthog) { + this.posthog.shutdown() + } + } +} diff --git a/packages/backend-core/src/events/processors/LoggingProcessor.ts b/packages/backend-core/src/events/processors/LoggingProcessor.ts new file mode 100644 index 0000000000..d41a82fbb4 --- /dev/null +++ b/packages/backend-core/src/events/processors/LoggingProcessor.ts @@ -0,0 +1,56 @@ +import { Event, Identity, Group } from "@budibase/types" +import { EventProcessor } from "./types" +import env from "../../environment" + +const getTimestampString = (timestamp?: string | number) => { + let timestampString = "" + if (timestamp) { + timestampString = `[timestamp=${new Date(timestamp).toISOString()}]` + } + return timestampString +} + +const skipLogging = env.SELF_HOSTED && !env.isDev() + +export default class LoggingProcessor implements EventProcessor { + async processEvent( + event: Event, + identity: Identity, + properties: any, + timestamp?: string + ): Promise { + if (skipLogging) { + return + } + let timestampString = getTimestampString(timestamp) + let message = `[audit] [tenant=${identity.tenantId}] [identityType=${identity.type}] [identity=${identity.id}] ${timestampString} ${event} ` + if (env.isDev()) { + message = message + `[debug: [properties=${JSON.stringify(properties)}] ]` + } + console.log(message) + } + + async identify(identity: Identity, timestamp?: string | number) { + if (skipLogging) { + return + } + let timestampString = getTimestampString(timestamp) + console.log( + `[audit] [${JSON.stringify(identity)}] ${timestampString} identified` + ) + } + + async identifyGroup(group: Group, timestamp?: string | number) { + if (skipLogging) { + return + } + let timestampString = getTimestampString(timestamp) + console.log( + `[audit] [${JSON.stringify(group)}] ${timestampString} group identified` + ) + } + + shutdown(): void { + // no-op + } +} diff --git a/packages/backend-core/src/events/processors/Processors.ts b/packages/backend-core/src/events/processors/Processors.ts new file mode 100644 index 0000000000..4baedd909f --- /dev/null +++ b/packages/backend-core/src/events/processors/Processors.ts @@ -0,0 +1,46 @@ +import { Event, Identity, Group } from "@budibase/types" +import { EventProcessor } from "./types" + +export default class Processor implements EventProcessor { + initialised: boolean = false + processors: EventProcessor[] = [] + + constructor(processors: EventProcessor[]) { + this.processors = processors + } + + async processEvent( + event: Event, + identity: Identity, + properties: any, + timestamp?: string | number + ): Promise { + for (const eventProcessor of this.processors) { + await eventProcessor.processEvent(event, identity, properties, timestamp) + } + } + + async identify( + identity: Identity, + timestamp?: string | number + ): Promise { + for (const eventProcessor of this.processors) { + await eventProcessor.identify(identity, timestamp) + } + } + + async identifyGroup( + identity: Group, + timestamp?: string | number + ): Promise { + for (const eventProcessor of this.processors) { + await eventProcessor.identifyGroup(identity, timestamp) + } + } + + shutdown() { + for (const eventProcessor of this.processors) { + eventProcessor.shutdown() + } + } +} diff --git a/packages/backend-core/src/events/processors/index.ts b/packages/backend-core/src/events/processors/index.ts new file mode 100644 index 0000000000..0e75f050db --- /dev/null +++ b/packages/backend-core/src/events/processors/index.ts @@ -0,0 +1,8 @@ +import AnalyticsProcessor from "./AnalyticsProcessor" +import LoggingProcessor from "./LoggingProcessor" +import Processors from "./Processors" + +export const analyticsProcessor = new AnalyticsProcessor() +const loggingProcessor = new LoggingProcessor() + +export const processors = new Processors([analyticsProcessor, loggingProcessor]) diff --git a/packages/backend-core/src/events/processors/posthog/PosthogProcessor.ts b/packages/backend-core/src/events/processors/posthog/PosthogProcessor.ts new file mode 100644 index 0000000000..593e5ff082 --- /dev/null +++ b/packages/backend-core/src/events/processors/posthog/PosthogProcessor.ts @@ -0,0 +1,107 @@ +import PostHog from "posthog-node" +import { Event, Identity, Group, BaseEvent } from "@budibase/types" +import { EventProcessor } from "../types" +import env from "../../../environment" +import * as context from "../../../context" +import * as rateLimiting from "./rateLimiting" +const pkg = require("../../../../package.json") + +const EXCLUDED_EVENTS: Event[] = [ + Event.USER_UPDATED, + Event.EMAIL_SMTP_UPDATED, + Event.AUTH_SSO_UPDATED, + Event.APP_UPDATED, + Event.ROLE_UPDATED, + Event.DATASOURCE_UPDATED, + Event.QUERY_UPDATED, + Event.TABLE_UPDATED, + Event.VIEW_UPDATED, + Event.VIEW_FILTER_UPDATED, + Event.VIEW_CALCULATION_UPDATED, + Event.AUTOMATION_TRIGGER_UPDATED, + Event.USER_GROUP_UPDATED, +] + +export default class PosthogProcessor implements EventProcessor { + posthog: PostHog + + constructor(token: string | undefined) { + if (!token) { + throw new Error("Posthog token is not defined") + } + this.posthog = new PostHog(token) + } + + async processEvent( + event: Event, + identity: Identity, + properties: BaseEvent, + timestamp?: string | number + ): Promise { + // don't send excluded events + if (EXCLUDED_EVENTS.includes(event)) { + return + } + + if (await rateLimiting.limited(event)) { + return + } + + properties.version = pkg.version + properties.service = env.SERVICE + properties.environment = identity.environment + properties.hosting = identity.hosting + + const appId = context.getAppId() + if (appId) { + properties.appId = appId + } + + const payload: any = { distinctId: identity.id, event, properties } + + if (timestamp) { + payload.timestamp = new Date(timestamp) + } + + // add groups to the event + if (identity.installationId || identity.tenantId) { + payload.groups = {} + if (identity.installationId) { + payload.groups.installation = identity.installationId + payload.properties.installationId = identity.installationId + } + if (identity.tenantId) { + payload.groups.tenant = identity.tenantId + payload.properties.tenantId = identity.tenantId + } + } + + this.posthog.capture(payload) + } + + async identify(identity: Identity, timestamp?: string | number) { + const payload: any = { distinctId: identity.id, properties: identity } + if (timestamp) { + payload.timestamp = new Date(timestamp) + } + this.posthog.identify(payload) + } + + async identifyGroup(group: Group, timestamp?: string | number) { + const payload: any = { + distinctId: group.id, + groupType: group.type, + groupKey: group.id, + properties: group, + } + + if (timestamp) { + payload.timestamp = new Date(timestamp) + } + this.posthog.groupIdentify(payload) + } + + shutdown() { + this.posthog.shutdown() + } +} diff --git a/packages/backend-core/src/events/processors/posthog/index.ts b/packages/backend-core/src/events/processors/posthog/index.ts new file mode 100644 index 0000000000..dceb10d2cd --- /dev/null +++ b/packages/backend-core/src/events/processors/posthog/index.ts @@ -0,0 +1,2 @@ +import PosthogProcessor from "./PosthogProcessor" +export default PosthogProcessor diff --git a/packages/backend-core/src/events/processors/posthog/rateLimiting.ts b/packages/backend-core/src/events/processors/posthog/rateLimiting.ts new file mode 100644 index 0000000000..9c7b7876d6 --- /dev/null +++ b/packages/backend-core/src/events/processors/posthog/rateLimiting.ts @@ -0,0 +1,106 @@ +import { Event } from "@budibase/types" +import { CacheKeys, TTL } from "../../../cache/generic" +import * as cache from "../../../cache/generic" +import * as context from "../../../context" + +type RateLimitedEvent = + | Event.SERVED_BUILDER + | Event.SERVED_APP_PREVIEW + | Event.SERVED_APP + +const isRateLimited = (event: Event): event is RateLimitedEvent => { + return ( + event === Event.SERVED_BUILDER || + event === Event.SERVED_APP_PREVIEW || + event === Event.SERVED_APP + ) +} + +const isPerApp = (event: RateLimitedEvent) => { + return event === Event.SERVED_APP_PREVIEW || event === Event.SERVED_APP +} + +interface EventProperties { + timestamp: number +} + +enum RateLimit { + CALENDAR_DAY = "calendarDay", +} + +const RATE_LIMITS = { + [Event.SERVED_APP]: RateLimit.CALENDAR_DAY, + [Event.SERVED_APP_PREVIEW]: RateLimit.CALENDAR_DAY, + [Event.SERVED_BUILDER]: RateLimit.CALENDAR_DAY, +} + +/** + * Check if this event should be sent right now + * Return false to signal the event SHOULD be sent + * Return true to signal the event should NOT be sent + */ +export const limited = async (event: Event): Promise => { + // not a rate limited event -- send + if (!isRateLimited(event)) { + return false + } + + const cachedEvent = await readEvent(event) + if (cachedEvent) { + const timestamp = new Date(cachedEvent.timestamp) + const limit = RATE_LIMITS[event] + switch (limit) { + case RateLimit.CALENDAR_DAY: { + // get midnight at the start of the next day for the timestamp + timestamp.setDate(timestamp.getDate() + 1) + timestamp.setHours(0, 0, 0, 0) + + // if we have passed the threshold into the next day + if (Date.now() > timestamp.getTime()) { + // update the timestamp in the event -- send + await recordEvent(event, { timestamp: Date.now() }) + return false + } else { + // still within the limited period -- don't send + return true + } + } + } + } else { + // no event present i.e. expired -- send + await recordEvent(event, { timestamp: Date.now() }) + return false + } +} + +const eventKey = (event: RateLimitedEvent) => { + let key = `${CacheKeys.EVENTS_RATE_LIMIT}:${event}` + if (isPerApp(event)) { + key = key + ":" + context.getAppId() + } + return key +} + +const readEvent = async ( + event: RateLimitedEvent +): Promise => { + const key = eventKey(event) + const result = await cache.get(key) + return result as EventProperties +} + +const recordEvent = async ( + event: RateLimitedEvent, + properties: EventProperties +) => { + const key = eventKey(event) + const limit = RATE_LIMITS[event] + let ttl + switch (limit) { + case RateLimit.CALENDAR_DAY: { + ttl = TTL.ONE_DAY + } + } + + await cache.store(key, properties, ttl) +} 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 new file mode 100644 index 0000000000..d14b697966 --- /dev/null +++ b/packages/backend-core/src/events/processors/posthog/tests/PosthogProcessor.spec.ts @@ -0,0 +1,145 @@ +import "../../../../../tests/utilities/TestConfiguration" +import PosthogProcessor from "../PosthogProcessor" +import { Event, IdentityType, Hosting } from "@budibase/types" +const tk = require("timekeeper") +import * as cache from "../../../../cache/generic" +import { CacheKeys } from "../../../../cache/generic" +import * as context from "../../../../context" + +const newIdentity = () => { + return { + id: "test", + type: IdentityType.USER, + hosting: Hosting.SELF, + environment: "test", + } +} + +describe("PosthogProcessor", () => { + beforeEach(async () => { + jest.clearAllMocks() + await cache.bustCache( + `${CacheKeys.EVENTS_RATE_LIMIT}:${Event.SERVED_BUILDER}` + ) + }) + + describe("processEvent", () => { + it("processes event", async () => { + const processor = new PosthogProcessor("test") + + const identity = newIdentity() + const properties = {} + + await processor.processEvent(Event.APP_CREATED, identity, properties) + + expect(processor.posthog.capture).toHaveBeenCalledTimes(1) + }) + + it("honours exclusions", async () => { + const processor = new PosthogProcessor("test") + + const identity = newIdentity() + const properties = {} + + await processor.processEvent(Event.AUTH_SSO_UPDATED, identity, properties) + expect(processor.posthog.capture).toHaveBeenCalledTimes(0) + }) + + describe("rate limiting", () => { + it("sends daily event once in same day", async () => { + const processor = new PosthogProcessor("test") + const identity = newIdentity() + const properties = {} + + tk.freeze(new Date(2022, 0, 1, 14, 0)) + await processor.processEvent(Event.SERVED_BUILDER, identity, properties) + // go forward one hour + tk.freeze(new Date(2022, 0, 1, 15, 0)) + await processor.processEvent(Event.SERVED_BUILDER, identity, properties) + + expect(processor.posthog.capture).toHaveBeenCalledTimes(1) + }) + + it("sends daily event once per unique day", async () => { + const processor = new PosthogProcessor("test") + const identity = newIdentity() + const properties = {} + + tk.freeze(new Date(2022, 0, 1, 14, 0)) + await processor.processEvent(Event.SERVED_BUILDER, identity, properties) + // go forward into next day + tk.freeze(new Date(2022, 0, 2, 9, 0)) + await processor.processEvent(Event.SERVED_BUILDER, identity, properties) + // go forward into next day + tk.freeze(new Date(2022, 0, 3, 5, 0)) + await processor.processEvent(Event.SERVED_BUILDER, identity, properties) + // go forward one hour + tk.freeze(new Date(2022, 0, 3, 6, 0)) + await processor.processEvent(Event.SERVED_BUILDER, identity, properties) + + expect(processor.posthog.capture).toHaveBeenCalledTimes(3) + }) + + it("sends event again after cache expires", async () => { + const processor = new PosthogProcessor("test") + const identity = newIdentity() + const properties = {} + + tk.freeze(new Date(2022, 0, 1, 14, 0)) + await processor.processEvent(Event.SERVED_BUILDER, identity, properties) + + await cache.bustCache( + `${CacheKeys.EVENTS_RATE_LIMIT}:${Event.SERVED_BUILDER}` + ) + + tk.freeze(new Date(2022, 0, 1, 14, 0)) + await processor.processEvent(Event.SERVED_BUILDER, identity, properties) + + expect(processor.posthog.capture).toHaveBeenCalledTimes(2) + }) + + it("sends per app events once per day per app", async () => { + const processor = new PosthogProcessor("test") + const identity = newIdentity() + const properties = {} + + const runAppEvents = async (appId: string) => { + await context.doInAppContext(appId, async () => { + tk.freeze(new Date(2022, 0, 1, 14, 0)) + await processor.processEvent(Event.SERVED_APP, identity, properties) + await processor.processEvent( + Event.SERVED_APP_PREVIEW, + identity, + properties + ) + + // go forward one hour - should be ignored + tk.freeze(new Date(2022, 0, 1, 15, 0)) + await processor.processEvent(Event.SERVED_APP, identity, properties) + await processor.processEvent( + Event.SERVED_APP_PREVIEW, + identity, + properties + ) + + // go forward into next day + tk.freeze(new Date(2022, 0, 2, 9, 0)) + + await processor.processEvent(Event.SERVED_APP, identity, properties) + await processor.processEvent( + Event.SERVED_APP_PREVIEW, + identity, + properties + ) + }) + } + + await runAppEvents("app_1") + expect(processor.posthog.capture).toHaveBeenCalledTimes(4) + + await runAppEvents("app_2") + expect(processor.posthog.capture).toHaveBeenCalledTimes(8) + }) + }) + }) +}) diff --git a/packages/backend-core/src/events/processors/types.ts b/packages/backend-core/src/events/processors/types.ts new file mode 100644 index 0000000000..f4066fe248 --- /dev/null +++ b/packages/backend-core/src/events/processors/types.ts @@ -0,0 +1,18 @@ +import { Event, Identity, Group } from "@budibase/types" + +export enum EventProcessorType { + POSTHOG = "posthog", + LOGGING = "logging", +} + +export interface EventProcessor { + processEvent( + event: Event, + identity: Identity, + properties: any, + timestamp?: string | number + ): Promise + identify(identity: Identity, timestamp?: string | number): Promise + identifyGroup(group: Group, timestamp?: string | number): Promise + shutdown(): void +} diff --git a/packages/backend-core/src/events/publishers/account.ts b/packages/backend-core/src/events/publishers/account.ts new file mode 100644 index 0000000000..3f1a8a9161 --- /dev/null +++ b/packages/backend-core/src/events/publishers/account.ts @@ -0,0 +1,29 @@ +import { publishEvent } from "../events" +import { + Event, + Account, + AccountCreatedEvent, + AccountDeletedEvent, + AccountVerifiedEvent, +} from "@budibase/types" + +export async function created(account: Account) { + const properties: AccountCreatedEvent = { + tenantId: account.tenantId, + } + await publishEvent(Event.ACCOUNT_CREATED, properties) +} + +export async function deleted(account: Account) { + const properties: AccountDeletedEvent = { + tenantId: account.tenantId, + } + await publishEvent(Event.ACCOUNT_DELETED, properties) +} + +export async function verified(account: Account) { + const properties: AccountVerifiedEvent = { + tenantId: account.tenantId, + } + await publishEvent(Event.ACCOUNT_VERIFIED, properties) +} diff --git a/packages/backend-core/src/events/publishers/app.ts b/packages/backend-core/src/events/publishers/app.ts new file mode 100644 index 0000000000..dd77b0b8a2 --- /dev/null +++ b/packages/backend-core/src/events/publishers/app.ts @@ -0,0 +1,108 @@ +import { publishEvent } from "../events" +import { + Event, + App, + AppCreatedEvent, + AppUpdatedEvent, + AppDeletedEvent, + AppPublishedEvent, + AppUnpublishedEvent, + AppFileImportedEvent, + AppTemplateImportedEvent, + AppVersionUpdatedEvent, + AppVersionRevertedEvent, + AppRevertedEvent, + AppExportedEvent, +} from "@budibase/types" + +export const created = async (app: App, timestamp?: string | number) => { + const properties: AppCreatedEvent = { + appId: app.appId, + version: app.version, + } + await publishEvent(Event.APP_CREATED, properties, timestamp) +} + +export async function updated(app: App) { + const properties: AppUpdatedEvent = { + appId: app.appId, + version: app.version, + } + await publishEvent(Event.APP_UPDATED, properties) +} + +export async function deleted(app: App) { + const properties: AppDeletedEvent = { + appId: app.appId, + } + await publishEvent(Event.APP_DELETED, properties) +} + +export async function published(app: App, timestamp?: string | number) { + const properties: AppPublishedEvent = { + appId: app.appId, + } + await publishEvent(Event.APP_PUBLISHED, properties, timestamp) +} + +export async function unpublished(app: App) { + const properties: AppUnpublishedEvent = { + appId: app.appId, + } + await publishEvent(Event.APP_UNPUBLISHED, properties) +} + +export async function fileImported(app: App) { + const properties: AppFileImportedEvent = { + appId: app.appId, + } + await publishEvent(Event.APP_FILE_IMPORTED, properties) +} + +export async function templateImported(app: App, templateKey: string) { + const properties: AppTemplateImportedEvent = { + appId: app.appId, + templateKey, + } + await publishEvent(Event.APP_TEMPLATE_IMPORTED, properties) +} + +export async function versionUpdated( + app: App, + currentVersion: string, + updatedToVersion: string +) { + const properties: AppVersionUpdatedEvent = { + appId: app.appId, + currentVersion, + updatedToVersion, + } + await publishEvent(Event.APP_VERSION_UPDATED, properties) +} + +export async function versionReverted( + app: App, + currentVersion: string, + revertedToVersion: string +) { + const properties: AppVersionRevertedEvent = { + appId: app.appId, + currentVersion, + revertedToVersion, + } + await publishEvent(Event.APP_VERSION_REVERTED, properties) +} + +export async function reverted(app: App) { + const properties: AppRevertedEvent = { + appId: app.appId, + } + await publishEvent(Event.APP_REVERTED, properties) +} + +export async function exported(app: App) { + const properties: AppExportedEvent = { + appId: app.appId, + } + await publishEvent(Event.APP_EXPORTED, properties) +} diff --git a/packages/backend-core/src/events/publishers/auth.ts b/packages/backend-core/src/events/publishers/auth.ts new file mode 100644 index 0000000000..93378501f3 --- /dev/null +++ b/packages/backend-core/src/events/publishers/auth.ts @@ -0,0 +1,58 @@ +import { publishEvent } from "../events" +import { + Event, + LoginEvent, + LoginSource, + LogoutEvent, + SSOActivatedEvent, + SSOCreatedEvent, + SSODeactivatedEvent, + SSOType, + SSOUpdatedEvent, +} from "@budibase/types" +import { identification } from ".." + +export async function login(source: LoginSource) { + const identity = await identification.getCurrentIdentity() + const properties: LoginEvent = { + userId: identity.id, + source, + } + await publishEvent(Event.AUTH_LOGIN, properties) +} + +export async function logout() { + const identity = await identification.getCurrentIdentity() + const properties: LogoutEvent = { + userId: identity.id, + } + await publishEvent(Event.AUTH_LOGOUT, properties) +} + +export async function SSOCreated(type: SSOType, timestamp?: string | number) { + const properties: SSOCreatedEvent = { + type, + } + await publishEvent(Event.AUTH_SSO_CREATED, properties, timestamp) +} + +export async function SSOUpdated(type: SSOType) { + const properties: SSOUpdatedEvent = { + type, + } + await publishEvent(Event.AUTH_SSO_UPDATED, properties) +} + +export async function SSOActivated(type: SSOType, timestamp?: string | number) { + const properties: SSOActivatedEvent = { + type, + } + await publishEvent(Event.AUTH_SSO_ACTIVATED, properties, timestamp) +} + +export async function SSODeactivated(type: SSOType) { + const properties: SSODeactivatedEvent = { + type, + } + await publishEvent(Event.AUTH_SSO_DEACTIVATED, properties) +} diff --git a/packages/backend-core/src/events/publishers/automation.ts b/packages/backend-core/src/events/publishers/automation.ts new file mode 100644 index 0000000000..95f9cb8db6 --- /dev/null +++ b/packages/backend-core/src/events/publishers/automation.ts @@ -0,0 +1,94 @@ +import { publishEvent } from "../events" +import { + Automation, + Event, + AutomationStep, + AutomationCreatedEvent, + AutomationDeletedEvent, + AutomationTestedEvent, + AutomationStepCreatedEvent, + AutomationStepDeletedEvent, + AutomationTriggerUpdatedEvent, + AutomationsRunEvent, +} from "@budibase/types" + +export async function created( + automation: Automation, + timestamp?: string | number +) { + const properties: AutomationCreatedEvent = { + appId: automation.appId, + automationId: automation._id as string, + triggerId: automation.definition?.trigger?.id, + triggerType: automation.definition?.trigger?.stepId, + } + await publishEvent(Event.AUTOMATION_CREATED, properties, timestamp) +} + +export async function triggerUpdated(automation: Automation) { + const properties: AutomationTriggerUpdatedEvent = { + appId: automation.appId, + automationId: automation._id as string, + triggerId: automation.definition?.trigger?.id, + triggerType: automation.definition?.trigger?.stepId, + } + await publishEvent(Event.AUTOMATION_TRIGGER_UPDATED, properties) +} + +export async function deleted(automation: Automation) { + const properties: AutomationDeletedEvent = { + appId: automation.appId, + automationId: automation._id as string, + triggerId: automation.definition?.trigger?.id, + triggerType: automation.definition?.trigger?.stepId, + } + await publishEvent(Event.AUTOMATION_DELETED, properties) +} + +export async function tested(automation: Automation) { + const properties: AutomationTestedEvent = { + appId: automation.appId, + automationId: automation._id as string, + triggerId: automation.definition?.trigger?.id, + triggerType: automation.definition?.trigger?.stepId, + } + await publishEvent(Event.AUTOMATION_TESTED, properties) +} + +export const run = async (count: number, timestamp?: string | number) => { + const properties: AutomationsRunEvent = { + count, + } + await publishEvent(Event.AUTOMATIONS_RUN, properties, timestamp) +} + +export async function stepCreated( + automation: Automation, + step: AutomationStep, + timestamp?: string | number +) { + const properties: AutomationStepCreatedEvent = { + appId: automation.appId, + automationId: automation._id as string, + triggerId: automation.definition?.trigger?.id, + triggerType: automation.definition?.trigger?.stepId, + stepId: step.id, + stepType: step.stepId, + } + await publishEvent(Event.AUTOMATION_STEP_CREATED, properties, timestamp) +} + +export async function stepDeleted( + automation: Automation, + step: AutomationStep +) { + const properties: AutomationStepDeletedEvent = { + appId: automation.appId, + automationId: automation._id as string, + triggerId: automation.definition?.trigger?.id, + triggerType: automation.definition?.trigger?.stepId, + stepId: step.id, + stepType: step.stepId, + } + await publishEvent(Event.AUTOMATION_STEP_DELETED, properties) +} diff --git a/packages/backend-core/src/events/publishers/backfill.ts b/packages/backend-core/src/events/publishers/backfill.ts new file mode 100644 index 0000000000..c16b3cc9dc --- /dev/null +++ b/packages/backend-core/src/events/publishers/backfill.ts @@ -0,0 +1,67 @@ +import { publishEvent } from "../events" +import { + Event, + AppBackfillSucceededEvent, + AppBackfillFailedEvent, + TenantBackfillSucceededEvent, + TenantBackfillFailedEvent, + InstallationBackfillSucceededEvent, + InstallationBackfillFailedEvent, +} from "@budibase/types" +const env = require("../../environment") + +const shouldSkip = !env.SELF_HOSTED && !env.isDev() + +export async function appSucceeded(properties: AppBackfillSucceededEvent) { + if (shouldSkip) { + return + } + await publishEvent(Event.APP_BACKFILL_SUCCEEDED, properties) +} + +export async function appFailed(error: any) { + if (shouldSkip) { + return + } + const properties: AppBackfillFailedEvent = { + error: JSON.stringify(error, Object.getOwnPropertyNames(error)), + } + await publishEvent(Event.APP_BACKFILL_FAILED, properties) +} + +export async function tenantSucceeded( + properties: TenantBackfillSucceededEvent +) { + if (shouldSkip) { + return + } + await publishEvent(Event.TENANT_BACKFILL_SUCCEEDED, properties) +} + +export async function tenantFailed(error: any) { + if (shouldSkip) { + return + } + const properties: TenantBackfillFailedEvent = { + error: JSON.stringify(error, Object.getOwnPropertyNames(error)), + } + await publishEvent(Event.TENANT_BACKFILL_FAILED, properties) +} + +export async function installationSucceeded() { + if (shouldSkip) { + return + } + const properties: InstallationBackfillSucceededEvent = {} + await publishEvent(Event.INSTALLATION_BACKFILL_SUCCEEDED, properties) +} + +export async function installationFailed(error: any) { + if (shouldSkip) { + return + } + const properties: InstallationBackfillFailedEvent = { + error: JSON.stringify(error, Object.getOwnPropertyNames(error)), + } + await publishEvent(Event.INSTALLATION_BACKFILL_FAILED, properties) +} diff --git a/packages/backend-core/src/events/publishers/datasource.ts b/packages/backend-core/src/events/publishers/datasource.ts new file mode 100644 index 0000000000..d3ea7402f9 --- /dev/null +++ b/packages/backend-core/src/events/publishers/datasource.ts @@ -0,0 +1,45 @@ +import { publishEvent } from "../events" +import { + Event, + Datasource, + DatasourceCreatedEvent, + DatasourceUpdatedEvent, + DatasourceDeletedEvent, + SourceName, +} from "@budibase/types" + +function isCustom(datasource: Datasource) { + const sources = Object.values(SourceName) + // if not in the base source list, then it must be custom + return !sources.includes(datasource.source) +} + +export async function created( + datasource: Datasource, + timestamp?: string | number +) { + const properties: DatasourceCreatedEvent = { + datasourceId: datasource._id as string, + source: datasource.source, + custom: isCustom(datasource), + } + await publishEvent(Event.DATASOURCE_CREATED, properties, timestamp) +} + +export async function updated(datasource: Datasource) { + const properties: DatasourceUpdatedEvent = { + datasourceId: datasource._id as string, + source: datasource.source, + custom: isCustom(datasource), + } + await publishEvent(Event.DATASOURCE_UPDATED, properties) +} + +export async function deleted(datasource: Datasource) { + const properties: DatasourceDeletedEvent = { + datasourceId: datasource._id as string, + source: datasource.source, + custom: isCustom(datasource), + } + await publishEvent(Event.DATASOURCE_DELETED, properties) +} diff --git a/packages/backend-core/src/events/publishers/email.ts b/packages/backend-core/src/events/publishers/email.ts new file mode 100644 index 0000000000..42c5fb4d4c --- /dev/null +++ b/packages/backend-core/src/events/publishers/email.ts @@ -0,0 +1,12 @@ +import { publishEvent } from "../events" +import { Event, SMTPCreatedEvent, SMTPUpdatedEvent } from "@budibase/types" + +export async function SMTPCreated(timestamp?: string | number) { + const properties: SMTPCreatedEvent = {} + await publishEvent(Event.EMAIL_SMTP_CREATED, properties, timestamp) +} + +export async function SMTPUpdated() { + const properties: SMTPUpdatedEvent = {} + await publishEvent(Event.EMAIL_SMTP_UPDATED, properties) +} diff --git a/packages/backend-core/src/events/publishers/group.ts b/packages/backend-core/src/events/publishers/group.ts new file mode 100644 index 0000000000..b4fd0d1469 --- /dev/null +++ b/packages/backend-core/src/events/publishers/group.ts @@ -0,0 +1,64 @@ +import { publishEvent } from "../events" +import { + Event, + UserGroup, + GroupCreatedEvent, + GroupDeletedEvent, + GroupUpdatedEvent, + GroupUsersAddedEvent, + GroupUsersDeletedEvent, + GroupAddedOnboardingEvent, + UserGroupRoles, +} from "@budibase/types" + +export async function created(group: UserGroup, timestamp?: number) { + const properties: GroupCreatedEvent = { + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_CREATED, properties, timestamp) +} + +export async function updated(group: UserGroup) { + const properties: GroupUpdatedEvent = { + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_UPDATED, properties) +} + +export async function deleted(group: UserGroup) { + const properties: GroupDeletedEvent = { + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_DELETED, properties) +} + +export async function usersAdded(count: number, group: UserGroup) { + const properties: GroupUsersAddedEvent = { + count, + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_USERS_ADDED, properties) +} + +export async function usersDeleted(count: number, group: UserGroup) { + const properties: GroupUsersDeletedEvent = { + count, + groupId: group._id as string, + } + await publishEvent(Event.USER_GROUP_USERS_REMOVED, properties) +} + +export async function createdOnboarding(groupId: string) { + const properties: GroupAddedOnboardingEvent = { + groupId: groupId, + onboarding: true, + } + await publishEvent(Event.USER_GROUP_ONBOARDING, properties) +} + +export async function permissionsEdited(roles: UserGroupRoles) { + const properties: UserGroupRoles = { + ...roles, + } + 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 new file mode 100644 index 0000000000..6fe42c4bda --- /dev/null +++ b/packages/backend-core/src/events/publishers/index.ts @@ -0,0 +1,21 @@ +export * as account from "./account" +export * as app from "./app" +export * as auth from "./auth" +export * as automation from "./automation" +export * as datasource from "./datasource" +export * as email from "./email" +export * as license from "./license" +export * as layout from "./layout" +export * as org from "./org" +export * as query from "./query" +export * as role from "./role" +export * as screen from "./screen" +export * as rows from "./rows" +export * as table from "./table" +export * as serve from "./serve" +export * as user from "./user" +export * as view from "./view" +export * as installation from "./installation" +export * as backfill from "./backfill" +export * as group from "./group" +export * as plugin from "./plugin" diff --git a/packages/backend-core/src/events/publishers/installation.ts b/packages/backend-core/src/events/publishers/installation.ts new file mode 100644 index 0000000000..ef27935210 --- /dev/null +++ b/packages/backend-core/src/events/publishers/installation.ts @@ -0,0 +1,31 @@ +import { publishEvent } from "../events" +import { Event, VersionCheckedEvent, VersionChangeEvent } from "@budibase/types" + +export async function versionChecked(version: string) { + const properties: VersionCheckedEvent = { + currentVersion: version, + } + await publishEvent(Event.INSTALLATION_VERSION_CHECKED, properties) +} + +export async function upgraded(from: string, to: string) { + const properties: VersionChangeEvent = { + from, + to, + } + + await publishEvent(Event.INSTALLATION_VERSION_UPGRADED, properties) +} + +export async function downgraded(from: string, to: string) { + const properties: VersionChangeEvent = { + from, + to, + } + await publishEvent(Event.INSTALLATION_VERSION_DOWNGRADED, properties) +} + +export async function firstStartup() { + const properties = {} + await publishEvent(Event.INSTALLATION_FIRST_STARTUP, properties) +} diff --git a/packages/backend-core/src/events/publishers/layout.ts b/packages/backend-core/src/events/publishers/layout.ts new file mode 100644 index 0000000000..1eede40143 --- /dev/null +++ b/packages/backend-core/src/events/publishers/layout.ts @@ -0,0 +1,21 @@ +import { publishEvent } from "../events" +import { + Event, + Layout, + LayoutCreatedEvent, + LayoutDeletedEvent, +} from "@budibase/types" + +export async function created(layout: Layout, timestamp?: string | number) { + const properties: LayoutCreatedEvent = { + layoutId: layout._id as string, + } + await publishEvent(Event.LAYOUT_CREATED, properties, timestamp) +} + +export async function deleted(layoutId: string) { + const properties: LayoutDeletedEvent = { + layoutId, + } + await publishEvent(Event.LAYOUT_DELETED, properties) +} diff --git a/packages/backend-core/src/events/publishers/license.ts b/packages/backend-core/src/events/publishers/license.ts new file mode 100644 index 0000000000..84472e408f --- /dev/null +++ b/packages/backend-core/src/events/publishers/license.ts @@ -0,0 +1,78 @@ +import { publishEvent } from "../events" +import { + Event, + LicenseActivatedEvent, + LicensePlanChangedEvent, + LicenseTierChangedEvent, + PlanType, + Account, + LicensePortalOpenedEvent, + LicenseCheckoutSuccessEvent, + LicenseCheckoutOpenedEvent, + LicensePaymentFailedEvent, + LicensePaymentRecoveredEvent, +} from "@budibase/types" + +export async function tierChanged(account: Account, from: number, to: number) { + const properties: LicenseTierChangedEvent = { + accountId: account.accountId, + to, + from, + } + await publishEvent(Event.LICENSE_TIER_CHANGED, properties) +} + +export async function planChanged( + account: Account, + from: PlanType, + to: PlanType +) { + const properties: LicensePlanChangedEvent = { + accountId: account.accountId, + to, + from, + } + await publishEvent(Event.LICENSE_PLAN_CHANGED, properties) +} + +export async function activated(account: Account) { + const properties: LicenseActivatedEvent = { + accountId: account.accountId, + } + await publishEvent(Event.LICENSE_ACTIVATED, properties) +} + +export async function checkoutOpened(account: Account) { + const properties: LicenseCheckoutOpenedEvent = { + accountId: account.accountId, + } + await publishEvent(Event.LICENSE_CHECKOUT_OPENED, properties) +} + +export async function checkoutSuccess(account: Account) { + const properties: LicenseCheckoutSuccessEvent = { + accountId: account.accountId, + } + await publishEvent(Event.LICENSE_CHECKOUT_SUCCESS, properties) +} + +export async function portalOpened(account: Account) { + const properties: LicensePortalOpenedEvent = { + accountId: account.accountId, + } + await publishEvent(Event.LICENSE_PORTAL_OPENED, properties) +} + +export async function paymentFailed(account: Account) { + const properties: LicensePaymentFailedEvent = { + accountId: account.accountId, + } + await publishEvent(Event.LICENSE_PAYMENT_FAILED, properties) +} + +export async function paymentRecovered(account: Account) { + const properties: LicensePaymentRecoveredEvent = { + accountId: account.accountId, + } + await publishEvent(Event.LICENSE_PAYMENT_RECOVERED, properties) +} diff --git a/packages/backend-core/src/events/publishers/org.ts b/packages/backend-core/src/events/publishers/org.ts new file mode 100644 index 0000000000..4567357db8 --- /dev/null +++ b/packages/backend-core/src/events/publishers/org.ts @@ -0,0 +1,29 @@ +import { publishEvent } from "../events" +import { Event } from "@budibase/types" + +export async function nameUpdated(timestamp?: string | number) { + const properties = {} + await publishEvent(Event.ORG_NAME_UPDATED, properties, timestamp) +} + +export async function logoUpdated(timestamp?: string | number) { + const properties = {} + await publishEvent(Event.ORG_LOGO_UPDATED, properties, timestamp) +} + +export async function platformURLUpdated(timestamp?: string | number) { + const properties = {} + await publishEvent(Event.ORG_PLATFORM_URL_UPDATED, properties, timestamp) +} + +// TODO + +export async function analyticsOptOut() { + const properties = {} + await publishEvent(Event.ANALYTICS_OPT_OUT, properties) +} + +export async function analyticsOptIn() { + const properties = {} + await publishEvent(Event.ANALYTICS_OPT_OUT, properties) +} diff --git a/packages/backend-core/src/events/publishers/plugin.ts b/packages/backend-core/src/events/publishers/plugin.ts new file mode 100644 index 0000000000..4e4d87cf56 --- /dev/null +++ b/packages/backend-core/src/events/publishers/plugin.ts @@ -0,0 +1,41 @@ +import { publishEvent } from "../events" +import { + Event, + Plugin, + PluginDeletedEvent, + PluginImportedEvent, + PluginInitEvent, +} from "@budibase/types" + +export async function init(plugin: Plugin) { + const properties: PluginInitEvent = { + type: plugin.schema.type, + name: plugin.name, + description: plugin.description, + version: plugin.version, + } + await publishEvent(Event.PLUGIN_INIT, properties) +} + +export async function imported(plugin: Plugin) { + const properties: PluginImportedEvent = { + pluginId: plugin._id as string, + type: plugin.schema.type, + source: plugin.source, + name: plugin.name, + description: plugin.description, + version: plugin.version, + } + await publishEvent(Event.PLUGIN_IMPORTED, properties) +} + +export async function deleted(plugin: Plugin) { + const properties: PluginDeletedEvent = { + pluginId: plugin._id as string, + type: plugin.schema.type, + name: plugin.name, + description: plugin.description, + version: plugin.version, + } + await publishEvent(Event.PLUGIN_DELETED, properties) +} diff --git a/packages/backend-core/src/events/publishers/query.ts b/packages/backend-core/src/events/publishers/query.ts new file mode 100644 index 0000000000..1bcf561fd0 --- /dev/null +++ b/packages/backend-core/src/events/publishers/query.ts @@ -0,0 +1,79 @@ +import { publishEvent } from "../events" +import { + Event, + Datasource, + Query, + QueryCreatedEvent, + QueryUpdatedEvent, + QueryDeletedEvent, + QueryImportedEvent, + QueryPreviewedEvent, + QueriesRunEvent, +} from "@budibase/types" + +/* eslint-disable */ + +export const created = async ( + datasource: Datasource, + query: Query, + timestamp?: string | number +) => { + const properties: QueryCreatedEvent = { + queryId: query._id as string, + datasourceId: datasource._id as string, + source: datasource.source, + queryVerb: query.queryVerb, + } + await publishEvent(Event.QUERY_CREATED, properties, timestamp) +} + +export const updated = async (datasource: Datasource, query: Query) => { + const properties: QueryUpdatedEvent = { + queryId: query._id as string, + datasourceId: datasource._id as string, + source: datasource.source, + queryVerb: query.queryVerb, + } + await publishEvent(Event.QUERY_UPDATED, properties) +} + +export const deleted = async (datasource: Datasource, query: Query) => { + const properties: QueryDeletedEvent = { + queryId: query._id as string, + datasourceId: datasource._id as string, + source: datasource.source, + queryVerb: query.queryVerb, + } + await publishEvent(Event.QUERY_DELETED, properties) +} + +export const imported = async ( + datasource: Datasource, + importSource: any, + count: any +) => { + const properties: QueryImportedEvent = { + datasourceId: datasource._id as string, + source: datasource.source, + count, + importSource, + } + await publishEvent(Event.QUERY_IMPORT, properties) +} + +export const run = async (count: number, timestamp?: string | number) => { + const properties: QueriesRunEvent = { + count, + } + await publishEvent(Event.QUERIES_RUN, properties, timestamp) +} + +export const previewed = async (datasource: Datasource, query: Query) => { + const properties: QueryPreviewedEvent = { + queryId: query._id, + datasourceId: datasource._id as string, + source: datasource.source, + queryVerb: query.queryVerb, + } + await publishEvent(Event.QUERY_PREVIEWED, properties) +} diff --git a/packages/backend-core/src/events/publishers/role.ts b/packages/backend-core/src/events/publishers/role.ts new file mode 100644 index 0000000000..99074d76a5 --- /dev/null +++ b/packages/backend-core/src/events/publishers/role.ts @@ -0,0 +1,54 @@ +import { publishEvent } from "../events" +import { + Event, + Role, + RoleAssignedEvent, + RoleCreatedEvent, + RoleDeletedEvent, + RoleUnassignedEvent, + RoleUpdatedEvent, + User, +} from "@budibase/types" + +export async function created(role: Role, timestamp?: string | number) { + const properties: RoleCreatedEvent = { + roleId: role._id as string, + permissionId: role.permissionId, + inherits: role.inherits, + } + await publishEvent(Event.ROLE_CREATED, properties, timestamp) +} + +export async function updated(role: Role) { + const properties: RoleUpdatedEvent = { + roleId: role._id as string, + permissionId: role.permissionId, + inherits: role.inherits, + } + await publishEvent(Event.ROLE_UPDATED, properties) +} + +export async function deleted(role: Role) { + const properties: RoleDeletedEvent = { + roleId: role._id as string, + permissionId: role.permissionId, + inherits: role.inherits, + } + await publishEvent(Event.ROLE_DELETED, properties) +} + +export async function assigned(user: User, roleId: string, timestamp?: number) { + const properties: RoleAssignedEvent = { + userId: user._id as string, + roleId, + } + await publishEvent(Event.ROLE_ASSIGNED, properties, timestamp) +} + +export async function unassigned(user: User, roleId: string) { + const properties: RoleUnassignedEvent = { + userId: user._id as string, + roleId, + } + await publishEvent(Event.ROLE_UNASSIGNED, properties) +} diff --git a/packages/backend-core/src/events/publishers/rows.ts b/packages/backend-core/src/events/publishers/rows.ts new file mode 100644 index 0000000000..487e7ea074 --- /dev/null +++ b/packages/backend-core/src/events/publishers/rows.ts @@ -0,0 +1,30 @@ +import { publishEvent } from "../events" +import { + Event, + RowsImportedEvent, + RowsCreatedEvent, + RowImportFormat, + Table, +} from "@budibase/types" + +/* eslint-disable */ + +export const created = async (count: number, timestamp?: string | number) => { + const properties: RowsCreatedEvent = { + count, + } + await publishEvent(Event.ROWS_CREATED, properties, timestamp) +} + +export const imported = async ( + table: Table, + format: RowImportFormat, + count: number +) => { + const properties: RowsImportedEvent = { + tableId: table._id as string, + format, + count, + } + await publishEvent(Event.ROWS_IMPORTED, properties) +} diff --git a/packages/backend-core/src/events/publishers/screen.ts b/packages/backend-core/src/events/publishers/screen.ts new file mode 100644 index 0000000000..966bf72e52 --- /dev/null +++ b/packages/backend-core/src/events/publishers/screen.ts @@ -0,0 +1,25 @@ +import { publishEvent } from "../events" +import { + Event, + Screen, + ScreenCreatedEvent, + ScreenDeletedEvent, +} from "@budibase/types" + +export async function created(screen: Screen, timestamp?: string | number) { + const properties: ScreenCreatedEvent = { + layoutId: screen.layoutId, + screenId: screen._id as string, + roleId: screen.routing.roleId, + } + await publishEvent(Event.SCREEN_CREATED, properties, timestamp) +} + +export async function deleted(screen: Screen) { + const properties: ScreenDeletedEvent = { + layoutId: screen.layoutId, + screenId: screen._id as string, + roleId: screen.routing.roleId, + } + await publishEvent(Event.SCREEN_DELETED, properties) +} diff --git a/packages/backend-core/src/events/publishers/serve.ts b/packages/backend-core/src/events/publishers/serve.ts new file mode 100644 index 0000000000..128e0b9b11 --- /dev/null +++ b/packages/backend-core/src/events/publishers/serve.ts @@ -0,0 +1,32 @@ +import { publishEvent } from "../events" +import { + App, + BuilderServedEvent, + Event, + AppPreviewServedEvent, + AppServedEvent, +} from "@budibase/types" + +export async function servedBuilder(timezone: string) { + const properties: BuilderServedEvent = { + timezone, + } + await publishEvent(Event.SERVED_BUILDER, properties) +} + +export async function servedApp(app: App, timezone: string) { + const properties: AppServedEvent = { + appVersion: app.version, + timezone, + } + await publishEvent(Event.SERVED_APP, properties) +} + +export async function servedAppPreview(app: App, timezone: string) { + const properties: AppPreviewServedEvent = { + appId: app.appId, + appVersion: app.version, + timezone, + } + await publishEvent(Event.SERVED_APP_PREVIEW, properties) +} diff --git a/packages/backend-core/src/events/publishers/table.ts b/packages/backend-core/src/events/publishers/table.ts new file mode 100644 index 0000000000..ba53163ca3 --- /dev/null +++ b/packages/backend-core/src/events/publishers/table.ts @@ -0,0 +1,49 @@ +import { publishEvent } from "../events" +import { + Event, + TableExportFormat, + TableImportFormat, + Table, + TableCreatedEvent, + TableUpdatedEvent, + TableDeletedEvent, + TableExportedEvent, + TableImportedEvent, +} from "@budibase/types" + +export async function created(table: Table, timestamp?: string | number) { + const properties: TableCreatedEvent = { + tableId: table._id as string, + } + await publishEvent(Event.TABLE_CREATED, properties, timestamp) +} + +export async function updated(table: Table) { + const properties: TableUpdatedEvent = { + tableId: table._id as string, + } + await publishEvent(Event.TABLE_UPDATED, properties) +} + +export async function deleted(table: Table) { + const properties: TableDeletedEvent = { + tableId: table._id as string, + } + await publishEvent(Event.TABLE_DELETED, properties) +} + +export async function exported(table: Table, format: TableExportFormat) { + const properties: TableExportedEvent = { + tableId: table._id as string, + format, + } + await publishEvent(Event.TABLE_EXPORTED, properties) +} + +export async function imported(table: Table, format: TableImportFormat) { + const properties: TableImportedEvent = { + tableId: table._id as string, + format, + } + 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 new file mode 100644 index 0000000000..2c83b267d6 --- /dev/null +++ b/packages/backend-core/src/events/publishers/user.ts @@ -0,0 +1,122 @@ +import { publishEvent } from "../events" +import { + Event, + User, + UserCreatedEvent, + UserDeletedEvent, + UserInviteAcceptedEvent, + UserInvitedEvent, + UserPasswordForceResetEvent, + UserPasswordResetEvent, + UserPasswordResetRequestedEvent, + UserPasswordUpdatedEvent, + UserPermissionAssignedEvent, + UserPermissionRemovedEvent, + UserUpdatedEvent, +} from "@budibase/types" + +export async function created(user: User, timestamp?: number) { + const properties: UserCreatedEvent = { + userId: user._id as string, + } + await publishEvent(Event.USER_CREATED, properties, timestamp) +} + +export async function updated(user: User) { + const properties: UserUpdatedEvent = { + userId: user._id as string, + } + await publishEvent(Event.USER_UPDATED, properties) +} + +export async function deleted(user: User) { + const properties: UserDeletedEvent = { + userId: user._id as string, + } + await publishEvent(Event.USER_DELETED, properties) +} + +// PERMISSIONS + +export async function permissionAdminAssigned(user: User, timestamp?: number) { + const properties: UserPermissionAssignedEvent = { + userId: user._id as string, + } + await publishEvent( + Event.USER_PERMISSION_ADMIN_ASSIGNED, + properties, + timestamp + ) +} + +export async function permissionAdminRemoved(user: User) { + const properties: UserPermissionRemovedEvent = { + userId: user._id as string, + } + await publishEvent(Event.USER_PERMISSION_ADMIN_REMOVED, properties) +} + +export async function permissionBuilderAssigned( + user: User, + timestamp?: number +) { + const properties: UserPermissionAssignedEvent = { + userId: user._id as string, + } + await publishEvent( + Event.USER_PERMISSION_BUILDER_ASSIGNED, + properties, + timestamp + ) +} + +export async function permissionBuilderRemoved(user: User) { + const properties: UserPermissionRemovedEvent = { + userId: user._id as string, + } + await publishEvent(Event.USER_PERMISSION_BUILDER_REMOVED, properties) +} + +// INVITE + +export async function invited() { + const properties: UserInvitedEvent = {} + await publishEvent(Event.USER_INVITED, properties) +} + +export async function inviteAccepted(user: User) { + const properties: UserInviteAcceptedEvent = { + userId: user._id as string, + } + await publishEvent(Event.USER_INVITED_ACCEPTED, properties) +} + +// PASSWORD + +export async function passwordForceReset(user: User) { + const properties: UserPasswordForceResetEvent = { + userId: user._id as string, + } + await publishEvent(Event.USER_PASSWORD_FORCE_RESET, properties) +} + +export async function passwordUpdated(user: User) { + const properties: UserPasswordUpdatedEvent = { + userId: user._id as string, + } + await publishEvent(Event.USER_PASSWORD_UPDATED, properties) +} + +export async function passwordResetRequested(user: User) { + const properties: UserPasswordResetRequestedEvent = { + userId: user._id as string, + } + await publishEvent(Event.USER_PASSWORD_RESET_REQUESTED, properties) +} + +export async function passwordReset(user: User) { + const properties: UserPasswordResetEvent = { + userId: user._id as string, + } + await publishEvent(Event.USER_PASSWORD_RESET, properties) +} diff --git a/packages/backend-core/src/events/publishers/view.ts b/packages/backend-core/src/events/publishers/view.ts new file mode 100644 index 0000000000..17947758d5 --- /dev/null +++ b/packages/backend-core/src/events/publishers/view.ts @@ -0,0 +1,97 @@ +import { publishEvent } from "../events" +import { + Event, + ViewCalculationCreatedEvent, + ViewCalculationDeletedEvent, + ViewCalculationUpdatedEvent, + ViewCreatedEvent, + ViewDeletedEvent, + ViewExportedEvent, + ViewFilterCreatedEvent, + ViewFilterDeletedEvent, + ViewFilterUpdatedEvent, + ViewUpdatedEvent, + View, + ViewCalculation, + Table, + TableExportFormat, +} from "@budibase/types" + +/* eslint-disable */ + +export async function created(view: View, timestamp?: string | number) { + const properties: ViewCreatedEvent = { + tableId: view.tableId, + } + await publishEvent(Event.VIEW_CREATED, properties, timestamp) +} + +export async function updated(view: View) { + const properties: ViewUpdatedEvent = { + tableId: view.tableId, + } + await publishEvent(Event.VIEW_UPDATED, properties) +} + +export async function deleted(view: View) { + const properties: ViewDeletedEvent = { + tableId: view.tableId, + } + await publishEvent(Event.VIEW_DELETED, properties) +} + +export async function exported(table: Table, format: TableExportFormat) { + const properties: ViewExportedEvent = { + tableId: table._id as string, + format, + } + await publishEvent(Event.VIEW_EXPORTED, properties) +} + +export async function filterCreated(view: View, timestamp?: string | number) { + const properties: ViewFilterCreatedEvent = { + tableId: view.tableId, + } + await publishEvent(Event.VIEW_FILTER_CREATED, properties, timestamp) +} + +export async function filterUpdated(view: View) { + const properties: ViewFilterUpdatedEvent = { + tableId: view.tableId, + } + await publishEvent(Event.VIEW_FILTER_UPDATED, properties) +} + +export async function filterDeleted(view: View) { + const properties: ViewFilterDeletedEvent = { + tableId: view.tableId, + } + await publishEvent(Event.VIEW_FILTER_DELETED, properties) +} + +export async function calculationCreated( + view: View, + timestamp?: string | number +) { + const properties: ViewCalculationCreatedEvent = { + tableId: view.tableId, + calculation: view.calculation as ViewCalculation, + } + await publishEvent(Event.VIEW_CALCULATION_CREATED, properties, timestamp) +} + +export async function calculationUpdated(view: View) { + const properties: ViewCalculationUpdatedEvent = { + tableId: view.tableId, + calculation: view.calculation as ViewCalculation, + } + await publishEvent(Event.VIEW_CALCULATION_UPDATED, properties) +} + +export async function calculationDeleted(existingView: View) { + const properties: ViewCalculationDeletedEvent = { + tableId: existingView.tableId, + calculation: existingView.calculation as ViewCalculation, + } + await publishEvent(Event.VIEW_CALCULATION_DELETED, properties) +} diff --git a/packages/backend-core/src/featureFlags/index.js b/packages/backend-core/src/featureFlags/index.js index c050cbdfef..8a8162d0ba 100644 --- a/packages/backend-core/src/featureFlags/index.js +++ b/packages/backend-core/src/featureFlags/index.js @@ -31,23 +31,30 @@ const TENANT_FEATURE_FLAGS = getFeatureFlags() exports.isEnabled = featureFlag => { const tenantId = tenancy.getTenantId() - - return ( - TENANT_FEATURE_FLAGS && - TENANT_FEATURE_FLAGS[tenantId] && - TENANT_FEATURE_FLAGS[tenantId].includes(featureFlag) - ) + const flags = exports.getTenantFeatureFlags(tenantId) + return flags.includes(featureFlag) } exports.getTenantFeatureFlags = tenantId => { - if (TENANT_FEATURE_FLAGS && TENANT_FEATURE_FLAGS[tenantId]) { - return TENANT_FEATURE_FLAGS[tenantId] + const flags = [] + + if (TENANT_FEATURE_FLAGS) { + const globalFlags = TENANT_FEATURE_FLAGS["*"] + const tenantFlags = TENANT_FEATURE_FLAGS[tenantId] + + if (globalFlags) { + flags.push(...globalFlags) + } + if (tenantFlags) { + flags.push(...tenantFlags) + } } - return [] + return flags } -exports.FeatureFlag = { +exports.TenantFeatureFlag = { LICENSING: "LICENSING", GOOGLE_SHEETS: "GOOGLE_SHEETS", + USER_GROUPS: "USER_GROUPS", } diff --git a/packages/backend-core/src/hashing.js b/packages/backend-core/src/hashing.js index 45abe2f9bd..7524e66043 100644 --- a/packages/backend-core/src/hashing.js +++ b/packages/backend-core/src/hashing.js @@ -1,5 +1,5 @@ -const bcrypt = require("bcrypt") const env = require("./environment") +const bcrypt = env.JS_BCRYPT ? require("bcryptjs") : require("bcrypt") const { v4 } = require("uuid") const SALT_ROUNDS = env.SALT_ROUNDS || 10 diff --git a/packages/backend-core/src/index.js b/packages/backend-core/src/index.js deleted file mode 100644 index 572b61fbeb..0000000000 --- a/packages/backend-core/src/index.js +++ /dev/null @@ -1,24 +0,0 @@ -const db = require("./db") - -module.exports = { - init(opts = {}) { - db.init(opts.db) - }, - // some default exports from the library, however these ideally shouldn't - // be used, instead the syntax require("@budibase/backend-core/db") should be used - StaticDatabases: require("./db/utils").StaticDatabases, - db: require("../db"), - redis: require("../redis"), - objectStore: require("../objectStore"), - utils: require("../utils"), - cache: require("../cache"), - auth: require("../auth"), - constants: require("../constants"), - migrations: require("../migrations"), - errors: require("./errors"), - env: require("./environment"), - accounts: require("./cloud/accounts"), - tenancy: require("./tenancy"), - context: require("../context"), - featureFlags: require("./featureFlags"), -} diff --git a/packages/backend-core/src/index.ts b/packages/backend-core/src/index.ts new file mode 100644 index 0000000000..83b23b479d --- /dev/null +++ b/packages/backend-core/src/index.ts @@ -0,0 +1,67 @@ +import errors from "./errors" +const errorClasses = errors.errors +import * as events from "./events" +import * as migrations from "./migrations" +import * as users from "./users" +import * as roles from "./security/roles" +import * as accounts from "./cloud/accounts" +import * as installation from "./installation" +import env from "./environment" +import tenancy from "./tenancy" +import featureFlags from "./featureFlags" +import * as sessions from "./security/sessions" +import deprovisioning from "./context/deprovision" +import auth from "./auth" +import constants from "./constants" +import * as dbConstants from "./db/constants" +import * as logging from "./logging" +import pino from "./pino" +import * as middleware from "./middleware" +import plugins from "./plugin" +import encryption from "./security/encryption" + +// mimic the outer package exports +import * as db from "./pkg/db" +import * as objectStore from "./pkg/objectStore" +import * as utils from "./pkg/utils" +import redis from "./pkg/redis" +import cache from "./pkg/cache" +import context from "./pkg/context" + +const init = (opts: any = {}) => { + db.init(opts.db) +} + +const core = { + init, + db, + ...dbConstants, + redis, + objectStore, + utils, + users, + cache, + auth, + constants, + ...constants, + migrations, + env, + accounts, + tenancy, + context, + featureFlags, + events, + sessions, + deprovisioning, + installation, + errors, + logging, + roles, + plugins, + ...pino, + ...errorClasses, + middleware, + encryption, +} + +export = core diff --git a/packages/backend-core/src/installation.ts b/packages/backend-core/src/installation.ts new file mode 100644 index 0000000000..da9b6c5b76 --- /dev/null +++ b/packages/backend-core/src/installation.ts @@ -0,0 +1,96 @@ +import * as hashing from "./hashing" +import * as events from "./events" +import { StaticDatabases } from "./db/constants" +import { doWithDB } from "./db" +import { Installation, IdentityType } from "@budibase/types" +import * as context from "./context" +import semver from "semver" +import { bustCache, withCache, TTL, CacheKeys } from "./cache/generic" + +const pkg = require("../package.json") + +export const getInstall = async (): Promise => { + return withCache(CacheKeys.INSTALLATION, TTL.ONE_DAY, getInstallFromDB, { + useTenancy: false, + }) +} + +const getInstallFromDB = async (): Promise => { + return doWithDB( + StaticDatabases.PLATFORM_INFO.name, + async (platformDb: any) => { + let install: Installation + try { + install = await platformDb.get( + StaticDatabases.PLATFORM_INFO.docs.install + ) + } catch (e: any) { + if (e.status === 404) { + install = { + _id: StaticDatabases.PLATFORM_INFO.docs.install, + installId: hashing.newid(), + version: pkg.version, + } + const resp = await platformDb.put(install) + install._rev = resp.rev + } else { + throw e + } + } + return install + } + ) +} + +const updateVersion = async (version: string): Promise => { + try { + await doWithDB( + StaticDatabases.PLATFORM_INFO.name, + async (platformDb: any) => { + const install = await getInstall() + install.version = version + await platformDb.put(install) + await bustCache(CacheKeys.INSTALLATION) + } + ) + } catch (e: any) { + if (e.status === 409) { + // do nothing - version has already been updated + // likely in clustered environment + return false + } + throw e + } + return true +} + +export const checkInstallVersion = async (): Promise => { + const install = await getInstall() + + const currentVersion = install.version + const newVersion = pkg.version + + if (currentVersion !== newVersion) { + const isUpgrade = semver.gt(newVersion, currentVersion) + const isDowngrade = semver.lt(newVersion, currentVersion) + + const success = await updateVersion(newVersion) + + if (success) { + await context.doInIdentityContext( + { + _id: install.installId, + type: IdentityType.INSTALLATION, + }, + async () => { + if (isUpgrade) { + await events.installation.upgraded(currentVersion, newVersion) + } else if (isDowngrade) { + await events.installation.downgraded(currentVersion, newVersion) + } + } + ) + await events.identification.identifyInstallationGroup(install.installId) + } + } +} diff --git a/packages/backend-core/src/logging.ts b/packages/backend-core/src/logging.ts new file mode 100644 index 0000000000..3fc79a5fe7 --- /dev/null +++ b/packages/backend-core/src/logging.ts @@ -0,0 +1,36 @@ +const NonErrors = ["AccountError"] + +function isSuppressed(e?: any) { + return e && e["suppressAlert"] +} + +export function logAlert(message: string, e?: any) { + if (e && NonErrors.includes(e.name) && isSuppressed(e)) { + return + } + let errorJson = "" + if (e) { + errorJson = ": " + JSON.stringify(e, Object.getOwnPropertyNames(e)) + } + console.error(`bb-alert: ${message} ${errorJson}`) +} + +export function logAlertWithInfo( + message: string, + db: string, + id: string, + error: any +) { + message = `${message} - db: ${db} - doc: ${id} - error: ` + logAlert(message, error) +} + +export function logWarn(message: string) { + console.warn(`bb-warn: ${message}`) +} + +export default { + logAlert, + logAlertWithInfo, + logWarn, +} diff --git a/packages/worker/src/middleware/adminOnly.js b/packages/backend-core/src/middleware/adminOnly.js similarity index 100% rename from packages/worker/src/middleware/adminOnly.js rename to packages/backend-core/src/middleware/adminOnly.js diff --git a/packages/backend-core/src/middleware/authenticated.js b/packages/backend-core/src/middleware/authenticated.ts similarity index 56% rename from packages/backend-core/src/middleware/authenticated.js rename to packages/backend-core/src/middleware/authenticated.ts index ef982c799b..a3c6b67cde 100644 --- a/packages/backend-core/src/middleware/authenticated.js +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -1,25 +1,39 @@ -const { Cookies, Headers } = require("../constants") -const { getCookie, clearCookie, openJwt } = require("../utils") -const { getUser } = require("../cache/user") -const { getSession, updateSessionTTL } = require("../security/sessions") -const { buildMatcherRegex, matches } = require("./matchers") +import { Cookies, Headers } from "../constants" +import { getCookie, clearCookie, openJwt } from "../utils" +import { getUser } from "../cache/user" +import { getSession, updateSessionTTL } from "../security/sessions" +import { buildMatcherRegex, matches } from "./matchers" +import { SEPARATOR } from "../db/constants" +import { ViewName } from "../db/utils" +import { queryGlobalView } from "../db/views" +import { getGlobalDB, doInTenant } from "../tenancy" +import { decrypt } from "../security/encryption" +const identity = require("../context/identity") const env = require("../environment") -const { SEPARATOR, ViewNames, queryGlobalView } = require("../../db") -const { getGlobalDB, doInTenant } = require("../tenancy") -const { decrypt } = require("../security/encryption") -function finalise( - ctx, - { authenticated, user, internal, version, publicEndpoint } = {} -) { - ctx.publicEndpoint = publicEndpoint || false - ctx.isAuthenticated = authenticated || false - ctx.user = user - ctx.internal = internal || false - ctx.version = version +const ONE_MINUTE = env.SESSION_UPDATE_PERIOD || 60 * 1000 + +interface FinaliseOpts { + authenticated?: boolean + internal?: boolean + publicEndpoint?: boolean + version?: string + user?: any } -async function checkApiKey(apiKey, populateUser) { +function timeMinusOneMinute() { + return new Date(Date.now() - ONE_MINUTE).toISOString() +} + +function finalise(ctx: any, opts: FinaliseOpts = {}) { + ctx.publicEndpoint = opts.publicEndpoint || false + ctx.isAuthenticated = opts.authenticated || false + ctx.user = opts.user + ctx.internal = opts.internal || false + ctx.version = opts.version +} + +async function checkApiKey(apiKey: string, populateUser?: Function) { if (apiKey === env.INTERNAL_API_KEY) { return { valid: true } } @@ -29,7 +43,7 @@ async function checkApiKey(apiKey, populateUser) { const db = getGlobalDB() // api key is encrypted in the database const userId = await queryGlobalView( - ViewNames.BY_API_KEY, + ViewName.BY_API_KEY, { key: apiKey, }, @@ -51,12 +65,14 @@ async function checkApiKey(apiKey, populateUser) { * The tenancy modules should not be used here and it should be assumed that the tenancy context * has not yet been populated. */ -module.exports = ( +export = ( noAuthPatterns = [], - opts = { publicAllowed: false, populateUser: null } + opts: { publicAllowed: boolean; populateUser?: Function } = { + publicAllowed: false, + } ) => { const noAuthOptions = noAuthPatterns ? buildMatcherRegex(noAuthPatterns) : [] - return async (ctx, next) => { + return async (ctx: any, next: any) => { let publicEndpoint = false const version = ctx.request.headers[Headers.API_VER] // the path is not authenticated @@ -68,46 +84,41 @@ module.exports = ( // check the actual user is authenticated first, try header or cookie const headerToken = ctx.request.headers[Headers.TOKEN] const authCookie = getCookie(ctx, Cookies.Auth) || openJwt(headerToken) + const apiKey = ctx.request.headers[Headers.API_KEY] + const tenantId = ctx.request.headers[Headers.TENANT_ID] let authenticated = false, user = null, internal = false - if (authCookie) { - let error = null + if (authCookie && !apiKey) { const sessionId = authCookie.sessionId const userId = authCookie.userId - - const session = await getSession(userId, sessionId) - if (!session) { - error = "No session found" - } else { - try { - if (opts && opts.populateUser) { - user = await getUser( - userId, - session.tenantId, - opts.populateUser(ctx) - ) - } else { - user = await getUser(userId, session.tenantId) - } - user.csrfToken = session.csrfToken - delete user.password - authenticated = true - } catch (err) { - error = err + let session + try { + // getting session handles error checking (if session exists etc) + session = await getSession(userId, sessionId) + if (opts && opts.populateUser) { + user = await getUser( + userId, + session.tenantId, + opts.populateUser(ctx) + ) + } else { + user = await getUser(userId, session.tenantId) } - } - if (error) { - console.error("Auth Error", error) + user.csrfToken = session.csrfToken + + if (session?.lastAccessedAt < timeMinusOneMinute()) { + // make sure we denote that the session is still in use + await updateSessionTTL(session) + } + authenticated = true + } catch (err: any) { + authenticated = false + console.error("Auth Error", err?.message || err) // remove the cookie as the user does not exist anymore clearCookie(ctx, Cookies.Auth) - } else { - // make sure we denote that the session is still in use - await updateSessionTTL(session) } } - const apiKey = ctx.request.headers[Headers.API_KEY] - const tenantId = ctx.request.headers[Headers.TENANT_ID] // this is an internal request, no user made it if (!authenticated && apiKey) { const populateUser = opts.populateUser ? opts.populateUser(ctx) : null @@ -125,6 +136,8 @@ module.exports = ( } if (!user && tenantId) { user = { tenantId } + } else if (user) { + delete user.password } // be explicit if (authenticated !== true) { @@ -132,8 +145,13 @@ module.exports = ( } // isAuthenticated is a function, so use a variable to be able to check authed state finalise(ctx, { authenticated, user, internal, version, publicEndpoint }) - return next() - } catch (err) { + + if (user && user.email) { + return identity.doInUserContext(user, next) + } else { + return next() + } + } catch (err: any) { // invalid token, clear the cookie if (err && err.name === "JsonWebTokenError") { clearCookie(ctx, Cookies.Auth) diff --git a/packages/worker/src/middleware/builderOnly.js b/packages/backend-core/src/middleware/builderOnly.js similarity index 100% rename from packages/worker/src/middleware/builderOnly.js rename to packages/backend-core/src/middleware/builderOnly.js diff --git a/packages/backend-core/src/middleware/builderOrAdmin.js b/packages/backend-core/src/middleware/builderOrAdmin.js new file mode 100644 index 0000000000..6440766298 --- /dev/null +++ b/packages/backend-core/src/middleware/builderOrAdmin.js @@ -0,0 +1,10 @@ +module.exports = async (ctx, next) => { + if ( + !ctx.internal && + (!ctx.user || !ctx.user.builder || !ctx.user.builder.global) && + (!ctx.user || !ctx.user.admin || !ctx.user.admin.global) + ) { + ctx.throw(403, "Builder user only endpoint.") + } + return next() +} diff --git a/packages/backend-core/src/middleware/index.js b/packages/backend-core/src/middleware/index.ts similarity index 62% rename from packages/backend-core/src/middleware/index.js rename to packages/backend-core/src/middleware/index.ts index 6c4c0d8883..998c231b3d 100644 --- a/packages/backend-core/src/middleware/index.js +++ b/packages/backend-core/src/middleware/index.ts @@ -2,15 +2,19 @@ const jwt = require("./passport/jwt") const local = require("./passport/local") const google = require("./passport/google") const oidc = require("./passport/oidc") -const { authError } = require("./passport/utils") +const { authError, ssoCallbackUrl } = require("./passport/utils") const authenticated = require("./authenticated") const auditLog = require("./auditLog") const tenancy = require("./tenancy") const internalApi = require("./internalApi") const datasourceGoogle = require("./passport/datasource/google") const csrf = require("./csrf") +const adminOnly = require("./adminOnly") +const builderOrAdmin = require("./builderOrAdmin") +const builderOnly = require("./builderOnly") +const joiValidator = require("./joi-validator") -module.exports = { +const pkg = { google, oidc, jwt, @@ -20,8 +24,15 @@ module.exports = { tenancy, authError, internalApi, + ssoCallbackUrl, datasource: { google: datasourceGoogle, }, csrf, + adminOnly, + builderOnly, + builderOrAdmin, + joiValidator, } + +export = pkg diff --git a/packages/worker/src/middleware/joi-validator.js b/packages/backend-core/src/middleware/joi-validator.js similarity index 71% rename from packages/worker/src/middleware/joi-validator.js rename to packages/backend-core/src/middleware/joi-validator.js index 1686b0e727..6812dbdd54 100644 --- a/packages/worker/src/middleware/joi-validator.js +++ b/packages/backend-core/src/middleware/joi-validator.js @@ -1,3 +1,5 @@ +const Joi = require("joi") + function validate(schema, property) { // Return a Koa middleware function return (ctx, next) => { @@ -10,6 +12,15 @@ function validate(schema, property) { } else if (ctx.request[property] != null) { params = ctx.request[property] } + + // not all schemas have the append property e.g. array schemas + if (schema.append) { + schema = schema.append({ + createdAt: Joi.any().optional(), + updatedAt: Joi.any().optional(), + }) + } + const { error } = schema.validate(params) if (error) { ctx.throw(400, `Invalid ${property} - ${error.message}`) diff --git a/packages/backend-core/src/middleware/passport/datasource/google.js b/packages/backend-core/src/middleware/passport/datasource/google.js index 53719b8350..8f2022c2d7 100644 --- a/packages/backend-core/src/middleware/passport/datasource/google.js +++ b/packages/backend-core/src/middleware/passport/datasource/google.js @@ -1,4 +1,5 @@ const google = require("../google") +const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy const { Cookies, Configs } = require("../../../constants") const { clearCookie, getCookie } = require("../../../utils") const { getScopedConfig, getPlatformUrl } = require("../../../db/utils") @@ -46,19 +47,20 @@ async function postAuth(passport, ctx, next) { const platformUrl = await getPlatformUrl({ tenantAware: false }) let callbackUrl = `${platformUrl}/api/global/auth/datasource/google/callback` - const strategy = await google.strategyFactory( - config, - callbackUrl, - (accessToken, refreshToken, profile, done) => { - clearCookie(ctx, Cookies.DatasourceAuth) - done(null, { refreshToken }) - } - ) - const authStateCookie = getCookie(ctx, Cookies.DatasourceAuth) return passport.authenticate( - strategy, + new GoogleStrategy( + { + clientID: config.clientID, + clientSecret: config.clientSecret, + callbackURL: callbackUrl, + }, + (accessToken, refreshToken, profile, done) => { + clearCookie(ctx, Cookies.DatasourceAuth) + done(null, { accessToken, refreshToken }) + } + ), { successRedirect: "/", failureRedirect: "/error" }, async (err, tokens) => { // update the DB for the datasource with all the user info diff --git a/packages/backend-core/src/middleware/passport/google.js b/packages/backend-core/src/middleware/passport/google.js index b12a668327..7419974cd7 100644 --- a/packages/backend-core/src/middleware/passport/google.js +++ b/packages/backend-core/src/middleware/passport/google.js @@ -1,6 +1,7 @@ const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy - +const { ssoCallbackUrl } = require("./utils") const { authenticateThirdParty } = require("./third-party-common") +const { Configs } = require("../../../constants") const buildVerifyFn = saveUserFn => { return (accessToken, refreshToken, profile, done) => { @@ -11,8 +12,8 @@ const buildVerifyFn = saveUserFn => { profile: profile, email: profile._json.email, oauth2: { - accessToken: accessToken, - refreshToken: refreshToken, + accessToken, + refreshToken, }, } @@ -57,5 +58,10 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { ) } } + +exports.getCallbackUrl = async function (db, config) { + return ssoCallbackUrl(db, config, Configs.GOOGLE) +} + // expose for testing exports.buildVerifyFn = buildVerifyFn diff --git a/packages/backend-core/src/middleware/passport/local.js b/packages/backend-core/src/middleware/passport/local.js index 716ebc1755..b955d29102 100644 --- a/packages/backend-core/src/middleware/passport/local.js +++ b/packages/backend-core/src/middleware/passport/local.js @@ -2,7 +2,7 @@ const jwt = require("jsonwebtoken") const { UserStatus } = require("../../constants") const { compare } = require("../../hashing") const env = require("../../environment") -const { getGlobalUserByEmail } = require("../../utils") +const users = require("../../users") const { authError } = require("./utils") const { newid } = require("../../hashing") const { createASession } = require("../../security/sessions") @@ -28,7 +28,7 @@ exports.authenticate = async function (ctx, email, password, done) { if (!email) return authError(done, "Email Required") if (!password) return authError(done, "Password Required") - const dbUser = await getGlobalUserByEmail(email) + const dbUser = await users.getGlobalUserByEmail(email) if (dbUser == null) { return authError(done, `User not found: [${email}]`) } @@ -55,6 +55,7 @@ exports.authenticate = async function (ctx, email, password, done) { if (await compare(password, dbUser.password)) { const sessionId = newid() const tenantId = getTenantId() + await createASession(dbUser._id, { sessionId, tenantId }) dbUser.token = jwt.sign( diff --git a/packages/backend-core/src/middleware/passport/oidc.js b/packages/backend-core/src/middleware/passport/oidc.js index 1e93e20b1c..20dbd4669b 100644 --- a/packages/backend-core/src/middleware/passport/oidc.js +++ b/packages/backend-core/src/middleware/passport/oidc.js @@ -1,6 +1,8 @@ const fetch = require("node-fetch") const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy const { authenticateThirdParty } = require("./third-party-common") +const { ssoCallbackUrl } = require("./utils") +const { Configs } = require("../../../constants") const buildVerifyFn = saveUserFn => { /** @@ -89,11 +91,24 @@ function validEmail(value) { * from couchDB rather than environment variables, using this factory is necessary for dynamically configuring passport. * @returns Dynamically configured Passport OIDC Strategy */ -exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { +exports.strategyFactory = async function (config, saveUserFn) { try { - const { clientID, clientSecret, configUrl } = config + const verify = buildVerifyFn(saveUserFn) + const strategy = new OIDCStrategy(config, verify) + strategy.name = "oidc" + return strategy + } catch (err) { + console.error(err) + throw new Error("Error constructing OIDC authentication strategy", err) + } +} + +exports.fetchStrategyConfig = async function (enrichedConfig, callbackUrl) { + try { + const { clientID, clientSecret, configUrl } = enrichedConfig if (!clientID || !clientSecret || !callbackUrl || !configUrl) { + //check for remote config and all required elements throw new Error( "Configuration invalid. Must contain clientID, clientSecret, callbackUrl and configUrl" ) @@ -109,24 +124,24 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) { const body = await response.json() - const verify = buildVerifyFn(saveUserFn) - return new OIDCStrategy( - { - issuer: body.issuer, - authorizationURL: body.authorization_endpoint, - tokenURL: body.token_endpoint, - userInfoURL: body.userinfo_endpoint, - clientID: clientID, - clientSecret: clientSecret, - callbackURL: callbackUrl, - }, - verify - ) + return { + issuer: body.issuer, + authorizationURL: body.authorization_endpoint, + tokenURL: body.token_endpoint, + userInfoURL: body.userinfo_endpoint, + clientID: clientID, + clientSecret: clientSecret, + callbackURL: callbackUrl, + } } catch (err) { console.error(err) - throw new Error("Error constructing OIDC authentication strategy", err) + throw new Error("Error constructing OIDC authentication configuration", err) } } +exports.getCallbackUrl = async function (db, config) { + return ssoCallbackUrl(db, config, Configs.OIDC) +} + // expose for testing exports.buildVerifyFn = buildVerifyFn diff --git a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js index c5e9fe0034..c00ab2ea7d 100644 --- a/packages/backend-core/src/middleware/passport/tests/oidc.spec.js +++ b/packages/backend-core/src/middleware/passport/tests/oidc.spec.js @@ -48,8 +48,8 @@ describe("oidc", () => { it("should create successfully create an oidc strategy", async () => { const oidc = require("../oidc") - - await oidc.strategyFactory(oidcConfig, callbackUrl) + const enrichedConfig = await oidc.fetchStrategyConfig(oidcConfig, callbackUrl) + await oidc.strategyFactory(enrichedConfig, callbackUrl) expect(mockFetch).toHaveBeenCalledWith(oidcConfig.configUrl) 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 index 2a1c4b6360..41a253b328 100644 --- 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 @@ -1,7 +1,4 @@ -// Mock data - -require("../../../tests/utilities/dbConfig") - +require("../../../../tests/utilities/TestConfiguration") const { authenticateThirdParty } = require("../third-party-common") const { data } = require("./utilities/mock-data") const { DEFAULT_TENANT_ID } = require("../../../constants") diff --git a/packages/backend-core/src/middleware/passport/third-party-common.js b/packages/backend-core/src/middleware/passport/third-party-common.js index 3fbfb145bc..1c5891fce7 100644 --- a/packages/backend-core/src/middleware/passport/third-party-common.js +++ b/packages/backend-core/src/middleware/passport/third-party-common.js @@ -4,7 +4,7 @@ const { generateGlobalUserID } = require("../../db/utils") const { authError } = require("./utils") const { newid } = require("../../hashing") const { createASession } = require("../../security/sessions") -const { getGlobalUserByEmail } = require("../../utils") +const users = require("../../users") const { getGlobalDB, getTenantId } = require("../../tenancy") const fetch = require("node-fetch") @@ -52,7 +52,7 @@ exports.authenticateThirdParty = async function ( // fallback to loading by email if (!dbUser) { - dbUser = await getGlobalUserByEmail(thirdPartyUser.email) + dbUser = await users.getGlobalUserByEmail(thirdPartyUser.email) } // exit early if there is still no user and auto creation is disabled @@ -79,14 +79,14 @@ exports.authenticateThirdParty = async function ( dbUser.forceResetPassword = false // create or sync the user - let response try { - response = await saveUserFn(dbUser, getTenantId(), false, false) + await saveUserFn(dbUser, false, false) } catch (err) { return authError(done, err) } - dbUser._rev = response.rev + // now that we're sure user exists, load them from the db + dbUser = await db.get(dbUser._id) // authenticate const sessionId = newid() diff --git a/packages/backend-core/src/middleware/passport/utils.js b/packages/backend-core/src/middleware/passport/utils.js index cbb93bfa3b..217130cd6d 100644 --- a/packages/backend-core/src/middleware/passport/utils.js +++ b/packages/backend-core/src/middleware/passport/utils.js @@ -1,3 +1,7 @@ +const { isMultiTenant, getTenantId } = require("../../tenancy") +const { getScopedConfig } = require("../../db/utils") +const { Configs } = require("../../constants") + /** * Utility to handle authentication errors. * @@ -5,6 +9,7 @@ * @param {*} message Message that will be returned in the response body * @param {*} err (Optional) error that will be logged */ + exports.authError = function (done, message, err = null) { return done( err, @@ -12,3 +17,21 @@ exports.authError = function (done, message, err = null) { { message: message } ) } + +exports.ssoCallbackUrl = async (db, config, type) => { + // incase there is a callback URL from before + if (config && config.callbackURL) { + return config.callbackURL + } + const publicConfig = await getScopedConfig(db, { + type: Configs.SETTINGS, + }) + + let callbackUrl = `/api/global/auth` + if (isMultiTenant()) { + callbackUrl += `/${getTenantId()}` + } + callbackUrl += `/${type}/callback` + + return `${publicConfig.platformUrl}${callbackUrl}` +} diff --git a/packages/backend-core/src/middleware/tenancy.js b/packages/backend-core/src/middleware/tenancy.js index 9a0cb8a0c6..8083322b29 100644 --- a/packages/backend-core/src/middleware/tenancy.js +++ b/packages/backend-core/src/middleware/tenancy.js @@ -1,6 +1,38 @@ -const { setTenantId, setGlobalDB, closeTenancy } = require("../tenancy") -const cls = require("../context/FunctionContext") +const { doInTenant, isMultiTenant, DEFAULT_TENANT_ID } = require("../tenancy") const { buildMatcherRegex, matches } = require("./matchers") +const { Headers } = require("../constants") + +const getTenantID = (ctx, opts = { allowQs: false, allowNoTenant: false }) => { + // exit early if not multi-tenant + if (!isMultiTenant()) { + return DEFAULT_TENANT_ID + } + + let tenantId + const allowQs = opts && opts.allowQs + const allowNoTenant = opts && opts.allowNoTenant + const header = ctx.request.headers[Headers.TENANT_ID] + const user = ctx.user || {} + if (allowQs) { + const query = ctx.request.query || {} + tenantId = query.tenantId + } + // override query string (if allowed) by user, or header + // URL params cannot be used in a middleware, as they are + // processed later in the chain + tenantId = user.tenantId || header || tenantId + + // Set the tenantId from the subdomain + if (!tenantId) { + tenantId = ctx.subdomains && ctx.subdomains[0] + } + + if (!tenantId && !allowNoTenant) { + ctx.throw(403, "Tenant id not set") + } + + return tenantId +} module.exports = ( allowQueryStringPatterns, @@ -11,15 +43,10 @@ module.exports = ( const noTenancyOptions = buildMatcherRegex(noTenancyPatterns) return async function (ctx, next) { - return cls.run(async () => { - const allowNoTenant = - opts.noTenancyRequired || !!matches(ctx, noTenancyOptions) - const allowQs = !!matches(ctx, allowQsOptions) - const tenantId = setTenantId(ctx, { allowQs, allowNoTenant }) - setGlobalDB(tenantId) - const res = await next() - await closeTenancy() - return res - }) + const allowNoTenant = + opts.noTenancyRequired || !!matches(ctx, noTenancyOptions) + const allowQs = !!matches(ctx, allowQsOptions) + const tenantId = getTenantID(ctx, { allowQs, allowNoTenant }) + return doInTenant(tenantId, next) } } diff --git a/packages/backend-core/src/migrations/definitions.ts b/packages/backend-core/src/migrations/definitions.ts new file mode 100644 index 0000000000..946fc3f364 --- /dev/null +++ b/packages/backend-core/src/migrations/definitions.ts @@ -0,0 +1,40 @@ +import { + MigrationType, + MigrationName, + MigrationDefinition, +} from "@budibase/types" + +export const DEFINITIONS: MigrationDefinition[] = [ + { + type: MigrationType.GLOBAL, + name: MigrationName.USER_EMAIL_VIEW_CASING, + }, + { + type: MigrationType.GLOBAL, + name: MigrationName.QUOTAS_1, + }, + { + type: MigrationType.APP, + name: MigrationName.APP_URLS, + }, + { + type: MigrationType.APP, + name: MigrationName.EVENT_APP_BACKFILL, + }, + { + type: MigrationType.GLOBAL, + name: MigrationName.EVENT_GLOBAL_BACKFILL, + }, + { + type: MigrationType.INSTALLATION, + name: MigrationName.EVENT_INSTALLATION_BACKFILL, + }, + { + type: MigrationType.GLOBAL, + name: MigrationName.GLOBAL_INFO_SYNC_USERS, + }, + { + type: MigrationType.GLOBAL, + name: MigrationName.PLUGIN_COUNT, + }, +] diff --git a/packages/backend-core/src/migrations/index.js b/packages/backend-core/src/migrations/index.js deleted file mode 100644 index ada1478ace..0000000000 --- a/packages/backend-core/src/migrations/index.js +++ /dev/null @@ -1,117 +0,0 @@ -const { DEFAULT_TENANT_ID } = require("../constants") -const { doWithDB } = require("../db") -const { DocumentTypes } = require("../db/constants") -const { getAllApps } = require("../db/utils") -const environment = require("../environment") -const { - doInTenant, - getTenantIds, - getGlobalDBName, - getTenantId, -} = require("../tenancy") - -exports.MIGRATION_TYPES = { - GLOBAL: "global", // run once, recorded in global db, global db is provided as an argument - APP: "app", // run per app, recorded in each app db, app db is provided as an argument -} - -exports.getMigrationsDoc = async db => { - // get the migrations doc - try { - return await db.get(DocumentTypes.MIGRATIONS) - } catch (err) { - if (err.status && err.status === 404) { - return { _id: DocumentTypes.MIGRATIONS } - } - console.error(err) - } -} - -const runMigration = async (migration, options = {}) => { - const tenantId = getTenantId() - const migrationType = migration.type - const migrationName = migration.name - - // get the db to store the migration in - let dbNames - if (migrationType === exports.MIGRATION_TYPES.GLOBAL) { - dbNames = [getGlobalDBName()] - } else if (migrationType === exports.MIGRATION_TYPES.APP) { - const apps = await getAllApps(migration.opts) - dbNames = apps.map(app => app.appId) - } else { - throw new Error( - `[Tenant: ${tenantId}] Unrecognised migration type [${migrationType}]` - ) - } - - // run the migration against each db - for (const dbName of dbNames) { - await doWithDB(dbName, async db => { - try { - const doc = await exports.getMigrationsDoc(db) - - // exit if the migration has been performed already - if (doc[migrationName]) { - if ( - options.force && - options.force[migrationType] && - options.force[migrationType].includes(migrationName) - ) { - console.log( - `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Forcing` - ) - } else { - // the migration has already been performed - return - } - } - - console.log( - `[Tenant: ${tenantId}] [Migration: ${migrationName}] [DB: ${dbName}] Running` - ) - // run the migration with tenant context - await migration.fn(db) - console.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 - } - }) - } -} - -exports.runMigrations = async (migrations, options = {}) => { - console.log("Running migrations") - let tenantIds - if (environment.MULTI_TENANCY) { - if (!options.tenantIds || !options.tenantIds.length) { - // run for all tenants - tenantIds = await getTenantIds() - } else { - tenantIds = options.tenantIds - } - } else { - // single tenancy - tenantIds = [DEFAULT_TENANT_ID] - } - - // for all tenants - for (const tenantId of tenantIds) { - // for all migrations - for (const migration of migrations) { - // run the migration - await doInTenant(tenantId, () => runMigration(migration, options)) - } - } - console.log("Migrations complete") -} diff --git a/packages/backend-core/src/migrations/index.ts b/packages/backend-core/src/migrations/index.ts new file mode 100644 index 0000000000..bce0cfc75c --- /dev/null +++ b/packages/backend-core/src/migrations/index.ts @@ -0,0 +1,2 @@ +export * from "./migrations" +export * from "./definitions" diff --git a/packages/backend-core/src/migrations/migrations.ts b/packages/backend-core/src/migrations/migrations.ts new file mode 100644 index 0000000000..90a12acec2 --- /dev/null +++ b/packages/backend-core/src/migrations/migrations.ts @@ -0,0 +1,185 @@ +import { DEFAULT_TENANT_ID } from "../constants" +import { doWithDB } from "../db" +import { DocumentType, StaticDatabases } from "../db/constants" +import { getAllApps } from "../db/utils" +import environment from "../environment" +import { doInTenant, getTenantIds, getTenantId } from "../tenancy" +import { getGlobalDBName } from "../db/tenancy" +import * as context from "../context" +import { DEFINITIONS } from "." +import { + Migration, + MigrationOptions, + MigrationType, + MigrationNoOpOptions, +} from "@budibase/types" + +export const getMigrationsDoc = async (db: any) => { + // get the migrations doc + try { + return await db.get(DocumentType.MIGRATIONS) + } catch (err: any) { + if (err.status && err.status === 404) { + return { _id: DocumentType.MIGRATIONS } + } else { + console.error(err) + throw err + } + } +} + +export const backPopulateMigrations = async (opts: MigrationNoOpOptions) => { + // filter migrations to the type and populate a no-op migration + const migrations: Migration[] = DEFINITIONS.filter( + def => def.type === opts.type + ).map(d => ({ ...d, fn: () => {} })) + await runMigrations(migrations, { noOp: opts }) +} + +export const runMigration = async ( + migration: Migration, + options: MigrationOptions = {} +) => { + const migrationType = migration.type + let tenantId: string + if (migrationType !== MigrationType.INSTALLATION) { + tenantId = getTenantId() + } + const migrationName = migration.name + const silent = migration.silent + + const log = (message: string) => { + if (!silent) { + console.log(message) + } + } + + // get the db to store the migration in + let dbNames + if (migrationType === MigrationType.GLOBAL) { + dbNames = [getGlobalDBName()] + } else if (migrationType === MigrationType.APP) { + if (options.noOp) { + dbNames = [options.noOp.appId] + } else { + const apps = await getAllApps(migration.appOpts) + dbNames = apps.map(app => app.appId) + } + } else if (migrationType === MigrationType.INSTALLATION) { + dbNames = [StaticDatabases.PLATFORM_INFO.name] + } else { + throw new Error(`Unrecognised migration type [${migrationType}]`) + } + + const length = dbNames.length + let count = 0 + + // run the migration against each db + for (const dbName of dbNames) { + count++ + const lengthStatement = length > 1 ? `[${count}/${length}]` : "" + + await doWithDB(dbName, async (db: any) => { + try { + const doc = await exports.getMigrationsDoc(db) + + // 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 + } + } + + // 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 + } + }) + } +} + +export const runMigrations = async ( + migrations: Migration[], + options: MigrationOptions = {} +) => { + let tenantIds + + if (environment.MULTI_TENANCY) { + if (options.noOp) { + tenantIds = [options.noOp.tenantId] + } else if (!options.tenantIds || !options.tenantIds.length) { + // run for all tenants + tenantIds = await getTenantIds() + } else { + tenantIds = options.tenantIds + } + } else { + // single tenancy + tenantIds = [DEFAULT_TENANT_ID] + } + + if (tenantIds.length > 1) { + console.log(`Checking migrations for ${tenantIds.length} tenants`) + } else { + console.log("Checking migrations") + } + + let count = 0 + // for all tenants + for (const tenantId of tenantIds) { + count++ + if (tenantIds.length > 1) { + console.log(`Progress [${count}/${tenantIds.length}]`) + } + // for all migrations + for (const migration of migrations) { + // run the migration + await doInTenant(tenantId, () => 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__/index.spec.js.snap index 222c3b1228..532b5a32db 100644 --- a/packages/backend-core/src/migrations/tests/__snapshots__/index.spec.js.snap +++ b/packages/backend-core/src/migrations/tests/__snapshots__/index.spec.js.snap @@ -3,7 +3,9 @@ exports[`migrations should match snapshot 1`] = ` Object { "_id": "migrations", - "_rev": "1-6277abc4e3db950221768e5a2618a059", - "test": 1487076708000, + "_rev": "1-a32b0b708e59eeb006ed5e063cfeb36a", + "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 index 8d9cb4e4b5..c5ec143143 100644 --- a/packages/backend-core/src/migrations/tests/index.spec.js +++ b/packages/backend-core/src/migrations/tests/index.spec.js @@ -1,12 +1,10 @@ -require("../../tests/utilities/dbConfig") - +require("../../../tests/utilities/TestConfiguration") const { runMigrations, getMigrationsDoc } = require("../index") const { dangerousGetDB } = require("../../db") const { StaticDatabases, } = require("../../db/utils") -Date.now = jest.fn(() => 1487076708000) let db describe("migrations", () => { diff --git a/packages/backend-core/src/objectStore/index.js b/packages/backend-core/src/objectStore/index.ts similarity index 64% rename from packages/backend-core/src/objectStore/index.js rename to packages/backend-core/src/objectStore/index.ts index 2385149f4d..a97aa8f65d 100644 --- a/packages/backend-core/src/objectStore/index.js +++ b/packages/backend-core/src/objectStore/index.ts @@ -1,16 +1,16 @@ const sanitize = require("sanitize-s3-objectkey") -const AWS = require("aws-sdk") -const stream = require("stream") -const fetch = require("node-fetch") -const tar = require("tar-fs") +import AWS from "aws-sdk" +import stream from "stream" +import fetch from "node-fetch" +import tar from "tar-fs" const zlib = require("zlib") -const { promisify } = require("util") -const { join } = require("path") -const fs = require("fs") -const env = require("../environment") -const { budibaseTempDir, ObjectStoreBuckets } = require("./utils") -const { v4 } = require("uuid") -const { APP_PREFIX, APP_DEV_PREFIX } = require("../db/utils") +import { promisify } from "util" +import { join } from "path" +import fs from "fs" +import env from "../environment" +import { budibaseTempDir, ObjectStoreBuckets } from "./utils" +import { v4 } from "uuid" +import { APP_PREFIX, APP_DEV_PREFIX } from "../db/utils" const streamPipeline = promisify(stream.pipeline) // use this as a temporary store of buckets that are being created @@ -18,7 +18,7 @@ const STATE = { bucketCreationPromises: {}, } -const CONTENT_TYPE_MAP = { +const CONTENT_TYPE_MAP: any = { html: "text/html", css: "text/css", js: "application/javascript", @@ -32,20 +32,16 @@ const STRING_CONTENT_TYPES = [ ] // does normal sanitization and then swaps dev apps to apps -function sanitizeKey(input) { +export function sanitizeKey(input: any) { return sanitize(sanitizeBucket(input)).replace(/\\/g, "/") } -exports.sanitizeKey = sanitizeKey - // simply handles the dev app to app conversion -function sanitizeBucket(input) { +export function sanitizeBucket(input: any) { return input.replace(new RegExp(APP_DEV_PREFIX, "g"), APP_PREFIX) } -exports.sanitizeBucket = sanitizeBucket - -function publicPolicy(bucketName) { +function publicPolicy(bucketName: any) { return { Version: "2012-10-17", Statement: [ @@ -61,7 +57,11 @@ function publicPolicy(bucketName) { } } -const PUBLIC_BUCKETS = [ObjectStoreBuckets.APPS, ObjectStoreBuckets.GLOBAL] +const PUBLIC_BUCKETS = [ + ObjectStoreBuckets.APPS, + ObjectStoreBuckets.GLOBAL, + ObjectStoreBuckets.PLUGINS, +] /** * Gets a connection to the object store using the S3 SDK. @@ -69,19 +69,19 @@ const PUBLIC_BUCKETS = [ObjectStoreBuckets.APPS, ObjectStoreBuckets.GLOBAL] * @return {Object} an S3 object store object, check S3 Nodejs SDK for usage. * @constructor */ -exports.ObjectStore = bucket => { - AWS.config.update({ - accessKeyId: env.MINIO_ACCESS_KEY, - secretAccessKey: env.MINIO_SECRET_KEY, - region: env.AWS_REGION, - }) - const config = { +export const ObjectStore = (bucket: any) => { + const config: any = { s3ForcePathStyle: true, signatureVersion: "v4", apiVersion: "2006-03-01", - params: { + accessKeyId: env.MINIO_ACCESS_KEY, + secretAccessKey: env.MINIO_SECRET_KEY, + region: env.AWS_REGION, + } + if (bucket) { + config.params = { Bucket: sanitizeBucket(bucket), - }, + } } if (env.MINIO_URL) { config.endpoint = env.MINIO_URL @@ -93,7 +93,7 @@ exports.ObjectStore = bucket => { * Given an object store and a bucket name this will make sure the bucket exists, * if it does not exist then it will create it. */ -exports.makeSureBucketExists = async (client, bucketName) => { +export const makeSureBucketExists = async (client: any, bucketName: any) => { bucketName = sanitizeBucket(bucketName) try { await client @@ -101,8 +101,8 @@ exports.makeSureBucketExists = async (client, bucketName) => { Bucket: bucketName, }) .promise() - } catch (err) { - const promises = STATE.bucketCreationPromises + } catch (err: any) { + const promises: any = STATE.bucketCreationPromises const doesntExist = err.statusCode === 404, noAccess = err.statusCode === 403 if (promises[bucketName]) { @@ -138,20 +138,20 @@ exports.makeSureBucketExists = async (client, bucketName) => { * Uploads the contents of a file given the required parameters, useful when * temp files in use (for example file uploaded as an attachment). */ -exports.upload = async ({ +export const upload = async ({ bucket: bucketName, filename, path, type, metadata, -}) => { +}: any) => { const extension = [...filename.split(".")].pop() const fileBytes = fs.readFileSync(path) - const objectStore = exports.ObjectStore(bucketName) - await exports.makeSureBucketExists(objectStore, bucketName) + const objectStore = ObjectStore(bucketName) + await makeSureBucketExists(objectStore, bucketName) - const config = { + const config: any = { // windows file paths need to be converted to forward slashes for s3 Key: sanitizeKey(filename), Body: fileBytes, @@ -167,9 +167,22 @@ exports.upload = async ({ * Similar to the upload function but can be used to send a file stream * through to the object store. */ -exports.streamUpload = async (bucketName, filename, stream, extra = {}) => { - const objectStore = exports.ObjectStore(bucketName) - await exports.makeSureBucketExists(objectStore, bucketName) +export const streamUpload = async ( + bucketName: any, + filename: any, + stream: any, + extra = {} +) => { + const objectStore = ObjectStore(bucketName) + await makeSureBucketExists(objectStore, bucketName) + + // Set content type for certain known extensions + if (filename?.endsWith(".js")) { + extra = { + ...extra, + ContentType: "application/javascript", + } + } const params = { Bucket: sanitizeBucket(bucketName), @@ -184,13 +197,13 @@ exports.streamUpload = async (bucketName, filename, stream, extra = {}) => { * retrieves the contents of a file from the object store, if it is a known content type it * will be converted, otherwise it will be returned as a buffer stream. */ -exports.retrieve = async (bucketName, filepath) => { - const objectStore = exports.ObjectStore(bucketName) +export const retrieve = async (bucketName: any, filepath: any) => { + const objectStore = ObjectStore(bucketName) const params = { Bucket: sanitizeBucket(bucketName), Key: sanitizeKey(filepath), } - const response = await objectStore.getObject(params).promise() + const response: any = await objectStore.getObject(params).promise() // currently these are all strings if (STRING_CONTENT_TYPES.includes(response.ContentType)) { return response.Body.toString("utf8") @@ -202,10 +215,10 @@ exports.retrieve = async (bucketName, filepath) => { /** * Same as retrieval function but puts to a temporary file. */ -exports.retrieveToTmp = async (bucketName, filepath) => { +export const retrieveToTmp = async (bucketName: any, filepath: any) => { bucketName = sanitizeBucket(bucketName) filepath = sanitizeKey(filepath) - const data = await exports.retrieve(bucketName, filepath) + const data = await retrieve(bucketName, filepath) const outputPath = join(budibaseTempDir(), v4()) fs.writeFileSync(outputPath, data) return outputPath @@ -214,9 +227,9 @@ exports.retrieveToTmp = async (bucketName, filepath) => { /** * Delete a single file. */ -exports.deleteFile = async (bucketName, filepath) => { - const objectStore = exports.ObjectStore(bucketName) - await exports.makeSureBucketExists(objectStore, bucketName) +export const deleteFile = async (bucketName: any, filepath: any) => { + const objectStore = ObjectStore(bucketName) + await makeSureBucketExists(objectStore, bucketName) const params = { Bucket: bucketName, Key: filepath, @@ -224,13 +237,13 @@ exports.deleteFile = async (bucketName, filepath) => { return objectStore.deleteObject(params) } -exports.deleteFiles = async (bucketName, filepaths) => { - const objectStore = exports.ObjectStore(bucketName) - await exports.makeSureBucketExists(objectStore, bucketName) +export const deleteFiles = async (bucketName: any, filepaths: any) => { + const objectStore = ObjectStore(bucketName) + await makeSureBucketExists(objectStore, bucketName) const params = { Bucket: bucketName, Delete: { - Objects: filepaths.map(path => ({ Key: path })), + Objects: filepaths.map((path: any) => ({ Key: path })), }, } return objectStore.deleteObjects(params).promise() @@ -239,38 +252,45 @@ exports.deleteFiles = async (bucketName, filepaths) => { /** * Delete a path, including everything within. */ -exports.deleteFolder = async (bucketName, folder) => { +export const deleteFolder = async ( + bucketName: any, + folder: any +): Promise => { bucketName = sanitizeBucket(bucketName) folder = sanitizeKey(folder) - const client = exports.ObjectStore(bucketName) + const client = ObjectStore(bucketName) const listParams = { Bucket: bucketName, Prefix: folder, } - let response = await client.listObjects(listParams).promise() + let response: any = await client.listObjects(listParams).promise() if (response.Contents.length === 0) { return } - const deleteParams = { + const deleteParams: any = { Bucket: bucketName, Delete: { Objects: [], }, } - response.Contents.forEach(content => { + response.Contents.forEach((content: any) => { deleteParams.Delete.Objects.push({ Key: content.Key }) }) response = await client.deleteObjects(deleteParams).promise() // can only empty 1000 items at once if (response.Deleted.length === 1000) { - return exports.deleteFolder(bucketName, folder) + return deleteFolder(bucketName, folder) } } -exports.uploadDirectory = async (bucketName, localPath, bucketPath) => { +export const uploadDirectory = async ( + bucketName: any, + localPath: any, + bucketPath: any +) => { bucketName = sanitizeBucket(bucketName) let uploads = [] const files = fs.readdirSync(localPath, { withFileTypes: true }) @@ -278,17 +298,30 @@ exports.uploadDirectory = async (bucketName, localPath, bucketPath) => { const path = sanitizeKey(join(bucketPath, file.name)) const local = join(localPath, file.name) if (file.isDirectory()) { - uploads.push(exports.uploadDirectory(bucketName, local, path)) + uploads.push(uploadDirectory(bucketName, local, path)) } else { - uploads.push( - exports.streamUpload(bucketName, path, fs.createReadStream(local)) - ) + uploads.push(streamUpload(bucketName, path, fs.createReadStream(local))) } } await Promise.all(uploads) + return files } -exports.downloadTarball = async (url, bucketName, path) => { +exports.downloadTarballDirect = async ( + url: string, + path: string, + headers = {} +) => { + path = sanitizeKey(path) + const response = await fetch(url, { headers }) + if (!response.ok) { + throw new Error(`unexpected response ${response.statusText}`) + } + + await streamPipeline(response.body, zlib.Unzip(), tar.extract(path)) +} + +export const downloadTarball = async (url: any, bucketName: any, path: any) => { bucketName = sanitizeBucket(bucketName) path = sanitizeKey(path) const response = await fetch(url) @@ -299,7 +332,7 @@ exports.downloadTarball = async (url, bucketName, path) => { const tmpPath = join(budibaseTempDir(), path) await streamPipeline(response.body, zlib.Unzip(), tar.extract(tmpPath)) if (!env.isTest() && env.SELF_HOSTED) { - await exports.uploadDirectory(bucketName, tmpPath, path) + await uploadDirectory(bucketName, tmpPath, path) } // return the temporary path incase there is a use for it return tmpPath diff --git a/packages/backend-core/src/objectStore/utils.js b/packages/backend-core/src/objectStore/utils.js index a243553df8..9cf4f5f70e 100644 --- a/packages/backend-core/src/objectStore/utils.js +++ b/packages/backend-core/src/objectStore/utils.js @@ -2,12 +2,18 @@ const { join } = require("path") const { tmpdir } = require("os") const env = require("../environment") +/**************************************************** + * NOTE: When adding a new bucket - name * + * sure that S3 usages (like budibase-infra) * + * have been updated to have a unique bucket name. * + ****************************************************/ exports.ObjectStoreBuckets = { BACKUPS: env.BACKUPS_BUCKET_NAME, APPS: env.APPS_BUCKET_NAME, TEMPLATES: env.TEMPLATES_BUCKET_NAME, GLOBAL: env.GLOBAL_BUCKET_NAME, GLOBAL_CLOUD: env.GLOBAL_CLOUD_BUCKET_NAME, + PLUGINS: env.PLUGIN_BUCKET_NAME, } exports.budibaseTempDir = function () { diff --git a/packages/backend-core/src/pino.js b/packages/backend-core/src/pino.js new file mode 100644 index 0000000000..69962b3841 --- /dev/null +++ b/packages/backend-core/src/pino.js @@ -0,0 +1,11 @@ +const env = require("./environment") + +exports.pinoSettings = () => ({ + prettyPrint: { + levelFirst: true, + }, + level: env.LOG_LEVEL || "error", + autoLogging: { + ignore: req => req.url.includes("/health"), + }, +}) diff --git a/packages/backend-core/src/pkg/cache.ts b/packages/backend-core/src/pkg/cache.ts new file mode 100644 index 0000000000..1aaa40370d --- /dev/null +++ b/packages/backend-core/src/pkg/cache.ts @@ -0,0 +1,11 @@ +// Mimic the outer package export for usage in index.ts +// The outer exports can't be used as they now reference dist directly +import * as generic from "../cache/generic" +import * as user from "../cache/user" +import * as app from "../cache/appMetadata" + +export = { + app, + user, + ...generic, +} diff --git a/packages/backend-core/src/pkg/context.ts b/packages/backend-core/src/pkg/context.ts new file mode 100644 index 0000000000..4915cc6e41 --- /dev/null +++ b/packages/backend-core/src/pkg/context.ts @@ -0,0 +1,26 @@ +// Mimic the outer package export for usage in index.ts +// The outer exports can't be used as they now reference dist directly +import { + getAppDB, + getDevAppDB, + getProdAppDB, + getAppId, + updateAppId, + doInAppContext, + doInTenant, + doInContext, +} from "../context" + +import * as identity from "../context/identity" + +export = { + getAppDB, + getDevAppDB, + getProdAppDB, + getAppId, + updateAppId, + doInAppContext, + doInTenant, + doInContext, + identity, +} diff --git a/packages/backend-core/src/pkg/db.ts b/packages/backend-core/src/pkg/db.ts new file mode 100644 index 0000000000..0254adddd5 --- /dev/null +++ b/packages/backend-core/src/pkg/db.ts @@ -0,0 +1,7 @@ +// Mimic the outer package export for usage in index.ts +// The outer exports can't be used as they now reference dist directly +export * from "../db" +export * from "../db/utils" +export * from "../db/views" +export * from "../db/pouch" +export * from "../db/constants" diff --git a/packages/backend-core/src/pkg/objectStore.ts b/packages/backend-core/src/pkg/objectStore.ts new file mode 100644 index 0000000000..0447c6b3c2 --- /dev/null +++ b/packages/backend-core/src/pkg/objectStore.ts @@ -0,0 +1,4 @@ +// Mimic the outer package export for usage in index.ts +// The outer exports can't be used as they now reference dist directly +export * from "../objectStore" +export * from "../objectStore/utils" diff --git a/packages/backend-core/src/pkg/redis.ts b/packages/backend-core/src/pkg/redis.ts new file mode 100644 index 0000000000..65ab186d9a --- /dev/null +++ b/packages/backend-core/src/pkg/redis.ts @@ -0,0 +1,11 @@ +// Mimic the outer package export for usage in index.ts +// The outer exports can't be used as they now reference dist directly +import Client from "../redis" +import utils from "../redis/utils" +import clients from "../redis/init" + +export = { + Client, + utils, + clients, +} diff --git a/packages/backend-core/src/pkg/utils.ts b/packages/backend-core/src/pkg/utils.ts new file mode 100644 index 0000000000..5272046524 --- /dev/null +++ b/packages/backend-core/src/pkg/utils.ts @@ -0,0 +1,4 @@ +// Mimic the outer package export for usage in index.ts +// The outer exports can't be used as they now reference dist directly +export * from "../utils" +export * from "../hashing" diff --git a/packages/backend-core/src/plugin/index.ts b/packages/backend-core/src/plugin/index.ts new file mode 100644 index 0000000000..a6d1853007 --- /dev/null +++ b/packages/backend-core/src/plugin/index.ts @@ -0,0 +1,7 @@ +import * as utils from "./utils" + +const pkg = { + ...utils, +} + +export = pkg diff --git a/packages/backend-core/src/plugin/utils.js b/packages/backend-core/src/plugin/utils.js new file mode 100644 index 0000000000..ade84bf44a --- /dev/null +++ b/packages/backend-core/src/plugin/utils.js @@ -0,0 +1,103 @@ +const { + DatasourceFieldType, + QueryType, + PluginType, +} = require("@budibase/types") +const joi = require("joi") + +const DATASOURCE_TYPES = [ + "Relational", + "Non-relational", + "Spreadsheet", + "Object store", + "Graph", + "API", +] + +function runJoi(validator, schema) { + const { error } = validator.validate(schema) + if (error) { + throw error + } +} + +function validateComponent(schema) { + const validator = joi.object({ + type: joi.string().allow("component").required(), + metadata: joi.object().unknown(true).required(), + hash: joi.string().optional(), + version: joi.string().optional(), + schema: joi + .object({ + name: joi.string().required(), + settings: joi.array().items(joi.object().unknown(true)).required(), + }) + .unknown(true), + }) + runJoi(validator, schema) +} + +function validateDatasource(schema) { + const fieldValidator = joi.object({ + type: joi + .string() + .allow(...Object.values(DatasourceFieldType)) + .required(), + required: joi.boolean().required(), + default: joi.any(), + display: joi.string(), + }) + + const queryValidator = joi + .object({ + type: joi.string().allow(...Object.values(QueryType)), + fields: joi.object().pattern(joi.string(), fieldValidator), + }) + .required() + + const validator = joi.object({ + type: joi.string().allow("datasource").required(), + metadata: joi.object().unknown(true).required(), + hash: joi.string().optional(), + version: joi.string().optional(), + schema: joi.object({ + docs: joi.string(), + friendlyName: joi.string().required(), + type: joi.string().allow(...DATASOURCE_TYPES), + description: joi.string().required(), + datasource: joi.object().pattern(joi.string(), fieldValidator).required(), + query: joi + .object({ + create: queryValidator, + read: queryValidator, + update: queryValidator, + delete: queryValidator, + }) + .unknown(true) + .required(), + extra: joi.object().pattern( + joi.string(), + joi.object({ + type: joi.string().required(), + displayName: joi.string().required(), + required: joi.boolean(), + data: joi.object(), + }) + ), + }), + }) + runJoi(validator, schema) +} + +exports.validate = schema => { + switch (schema?.type) { + case PluginType.COMPONENT: + validateComponent(schema) + break + case PluginType.DATASOURCE: + validateDatasource(schema) + break + default: + throw new Error(`Unknown plugin type - check schema.json: ${schema.type}`) + } +} diff --git a/packages/backend-core/src/redis/index.js b/packages/backend-core/src/redis/index.ts similarity index 54% rename from packages/backend-core/src/redis/index.js rename to packages/backend-core/src/redis/index.ts index 0ee17265ce..206110366f 100644 --- a/packages/backend-core/src/redis/index.js +++ b/packages/backend-core/src/redis/index.ts @@ -1,3 +1,4 @@ +import RedisWrapper from "../redis" const env = require("../environment") // ioredis mock is all in memory const Redis = env.isTest() ? require("ioredis-mock") : require("ioredis") @@ -6,24 +7,34 @@ const { removeDbPrefix, getRedisOptions, SEPARATOR, + SelectableDatabases, } = require("./utils") const RETRY_PERIOD_MS = 2000 const STARTUP_TIMEOUT_MS = 5000 const CLUSTERED = false +const DEFAULT_SELECT_DB = SelectableDatabases.DEFAULT // for testing just generate the client once let CLOSED = false -let CLIENT = env.isTest() ? new Redis(getRedisOptions()) : null +let CLIENTS: { [key: number]: any } = {} // if in test always connected -let CONNECTED = !!env.isTest() +let CONNECTED = env.isTest() -function connectionError(timeout, err) { +function pickClient(selectDb: number): any { + return CLIENTS[selectDb] +} + +function connectionError( + selectDb: number, + timeout: NodeJS.Timeout, + err: Error | string +) { // manually shut down, ignore errors if (CLOSED) { return } - CLIENT.end() + pickClient(selectDb).disconnect() CLOSED = true // always clear this on error clearTimeout(timeout) @@ -38,59 +49,69 @@ function connectionError(timeout, err) { * Inits the system, will error if unable to connect to redis cluster (may take up to 10 seconds) otherwise * will return the ioredis client which will be ready to use. */ -function init() { - let timeout +function init(selectDb = DEFAULT_SELECT_DB) { + let timeout: NodeJS.Timeout CLOSED = false - // testing uses a single in memory client - if (env.isTest() || (CLIENT && CONNECTED)) { + let client = pickClient(selectDb) + // already connected, ignore + if (client && CONNECTED) { return } + // testing uses a single in memory client + if (env.isTest()) { + CLIENTS[selectDb] = new Redis(getRedisOptions()) + } // start the timer - only allowed 5 seconds to connect timeout = setTimeout(() => { if (!CONNECTED) { - connectionError(timeout, "Did not successfully connect in timeout") + connectionError( + selectDb, + timeout, + "Did not successfully connect in timeout" + ) } }, STARTUP_TIMEOUT_MS) // disconnect any lingering client - if (CLIENT) { - CLIENT.disconnect() + if (client) { + client.disconnect() } const { redisProtocolUrl, opts, host, port } = getRedisOptions(CLUSTERED) if (CLUSTERED) { - CLIENT = new Redis.Cluster([{ host, port }], opts) + client = new Redis.Cluster([{ host, port }], opts) } else if (redisProtocolUrl) { - CLIENT = new Redis(redisProtocolUrl) + client = new Redis(redisProtocolUrl) } else { - CLIENT = new Redis(opts) + client = new Redis(opts) } // attach handlers - CLIENT.on("end", err => { - connectionError(timeout, err) + client.on("end", (err: Error) => { + connectionError(selectDb, timeout, err) }) - CLIENT.on("error", err => { - connectionError(timeout, err) + client.on("error", (err: Error) => { + connectionError(selectDb, timeout, err) }) - CLIENT.on("connect", () => { + client.on("connect", () => { clearTimeout(timeout) CONNECTED = true }) + CLIENTS[selectDb] = client } -function waitForConnection() { +function waitForConnection(selectDb: number = DEFAULT_SELECT_DB) { return new Promise(resolve => { - if (CLIENT == null) { + if (pickClient(selectDb) == null) { init() } else if (CONNECTED) { - resolve() + resolve("") return } // check if the connection is ready const interval = setInterval(() => { if (CONNECTED) { clearInterval(interval) - resolve() + resolve("") } }, 500) }) @@ -100,25 +121,26 @@ function waitForConnection() { * Utility function, takes a redis stream and converts it to a promisified response - * this can only be done with redis streams because they will have an end. * @param stream A redis stream, specifically as this type of stream will have an end. + * @param client The client to use for further lookups. * @return {Promise} The final output of the stream */ -function promisifyStream(stream) { +function promisifyStream(stream: any, client: RedisWrapper) { return new Promise((resolve, reject) => { const outputKeys = new Set() - stream.on("data", keys => { + stream.on("data", (keys: string[]) => { keys.forEach(key => { outputKeys.add(key) }) }) - stream.on("error", err => { + stream.on("error", (err: Error) => { reject(err) }) stream.on("end", async () => { - const keysArray = Array.from(outputKeys) + const keysArray: string[] = Array.from(outputKeys) as string[] try { let getPromises = [] for (let key of keysArray) { - getPromises.push(CLIENT.get(key)) + getPromises.push(client.get(key)) } const jsonArray = await Promise.all(getPromises) resolve( @@ -134,39 +156,52 @@ function promisifyStream(stream) { }) } -class RedisWrapper { - constructor(db) { +export = class RedisWrapper { + _db: string + _select: number + + constructor(db: string, selectDb: number | null = null) { this._db = db + this._select = selectDb || DEFAULT_SELECT_DB + } + + getClient() { + return pickClient(this._select) } async init() { CLOSED = false - init() - await waitForConnection() + init(this._select) + await waitForConnection(this._select) return this } async finish() { CLOSED = true - CLIENT.disconnect() + this.getClient().disconnect() } - async scan(key = "") { + async scan(key = ""): Promise { const db = this._db key = `${db}${SEPARATOR}${key}` let stream if (CLUSTERED) { - let node = CLIENT.nodes("master") + let node = this.getClient().nodes("master") stream = node[0].scanStream({ match: key + "*", count: 100 }) } else { - stream = CLIENT.scanStream({ match: key + "*", count: 100 }) + stream = this.getClient().scanStream({ match: key + "*", count: 100 }) } - return promisifyStream(stream) + return promisifyStream(stream, this.getClient()) } - async get(key) { + async keys(pattern: string) { const db = this._db - let response = await CLIENT.get(addDbPrefix(db, key)) + return this.getClient().keys(addDbPrefix(db, pattern)) + } + + async get(key: string) { + const db = this._db + let response = await this.getClient().get(addDbPrefix(db, key)) // overwrite the prefixed key if (response != null && response.key) { response.key = key @@ -179,39 +214,37 @@ class RedisWrapper { } } - async store(key, value, expirySeconds = null) { + async store(key: string, value: any, expirySeconds: number | null = null) { const db = this._db if (typeof value === "object") { value = JSON.stringify(value) } const prefixedKey = addDbPrefix(db, key) - await CLIENT.set(prefixedKey, value) + await this.getClient().set(prefixedKey, value) if (expirySeconds) { - await CLIENT.expire(prefixedKey, expirySeconds) + await this.getClient().expire(prefixedKey, expirySeconds) } } - async getTTL(key) { + async getTTL(key: string) { const db = this._db const prefixedKey = addDbPrefix(db, key) - return CLIENT.ttl(prefixedKey) + return this.getClient().ttl(prefixedKey) } - async setExpiry(key, expirySeconds) { + async setExpiry(key: string, expirySeconds: number | null) { const db = this._db const prefixedKey = addDbPrefix(db, key) - await CLIENT.expire(prefixedKey, expirySeconds) + await this.getClient().expire(prefixedKey, expirySeconds) } - async delete(key) { + async delete(key: string) { const db = this._db - await CLIENT.del(addDbPrefix(db, key)) + await this.getClient().del(addDbPrefix(db, key)) } async clear() { let items = await this.scan() - await Promise.all(items.map(obj => this.delete(obj.key))) + await Promise.all(items.map((obj: any) => this.delete(obj.key))) } } - -module.exports = RedisWrapper diff --git a/packages/backend-core/src/redis/authRedis.js b/packages/backend-core/src/redis/init.js similarity index 55% rename from packages/backend-core/src/redis/authRedis.js rename to packages/backend-core/src/redis/init.js index b9f6d8d0b0..8e5d10f838 100644 --- a/packages/backend-core/src/redis/authRedis.js +++ b/packages/backend-core/src/redis/init.js @@ -1,13 +1,27 @@ const Client = require("./index") const utils = require("./utils") +const { getRedlock } = require("./redlock") -let userClient, sessionClient, appClient, cacheClient +let userClient, sessionClient, appClient, cacheClient, writethroughClient +let migrationsRedlock + +// turn retry off so that only one instance can ever hold the lock +const migrationsRedlockConfig = { retryCount: 0 } async function init() { userClient = await new Client(utils.Databases.USER_CACHE).init() sessionClient = await new Client(utils.Databases.SESSIONS).init() appClient = await new Client(utils.Databases.APP_METADATA).init() cacheClient = await new Client(utils.Databases.GENERIC_CACHE).init() + writethroughClient = await new Client( + utils.Databases.WRITE_THROUGH, + utils.SelectableDatabases.WRITE_THROUGH + ).init() + // pass the underlying ioredis client to redlock + migrationsRedlock = getRedlock( + cacheClient.getClient(), + migrationsRedlockConfig + ) } process.on("exit", async () => { @@ -15,6 +29,7 @@ process.on("exit", async () => { if (sessionClient) await sessionClient.finish() if (appClient) await appClient.finish() if (cacheClient) await cacheClient.finish() + if (writethroughClient) await writethroughClient.finish() }) module.exports = { @@ -42,4 +57,16 @@ module.exports = { } return cacheClient }, + getWritethroughClient: async () => { + if (!writethroughClient) { + await init() + } + return writethroughClient + }, + getMigrationsRedlock: async () => { + if (!migrationsRedlock) { + await init() + } + return migrationsRedlock + }, } diff --git a/packages/backend-core/src/redis/redlock.ts b/packages/backend-core/src/redis/redlock.ts new file mode 100644 index 0000000000..beef375b55 --- /dev/null +++ b/packages/backend-core/src/redis/redlock.ts @@ -0,0 +1,21 @@ +import Redlock from "redlock" + +export const getRedlock = (redisClient: any, opts = { retryCount: 10 }) => { + return new Redlock([redisClient], { + // the expected clock drift; for more details + // see http://redis.io/topics/distlock + driftFactor: 0.01, // multiplied by lock ttl to determine drift time + + // the max number of times Redlock will attempt + // to lock a resource before erroring + retryCount: opts.retryCount, + + // the time in ms between attempts + retryDelay: 200, // time in ms + + // the max time in ms randomly added to retries + // to improve performance under high contention + // see https://www.awsarchitectureblog.com/2015/03/backoff.html + retryJitter: 200, // time in ms + }) +} diff --git a/packages/backend-core/src/redis/utils.js b/packages/backend-core/src/redis/utils.js index 90ea5c33f9..90b3561f31 100644 --- a/packages/backend-core/src/redis/utils.js +++ b/packages/backend-core/src/redis/utils.js @@ -6,6 +6,14 @@ const SEPARATOR = "-" const REDIS_URL = !env.REDIS_URL ? "localhost:6379" : env.REDIS_URL const REDIS_PASSWORD = !env.REDIS_PASSWORD ? "budibase" : env.REDIS_PASSWORD +/** + * These Redis databases help us to segment up a Redis keyspace by prepending the + * specified database name onto the cache key. This means that a single real Redis database + * can be split up a bit; allowing us to use scans on small databases to find some particular + * keys within. + * If writing a very large volume of keys is expected (say 10K+) then it is better to keep these out + * of the default keyspace and use a separate one - the SelectableDatabases can be used for this. + */ exports.Databases = { PW_RESETS: "pwReset", VERIFICATIONS: "verification", @@ -19,6 +27,35 @@ exports.Databases = { QUERY_VARS: "queryVars", LICENSES: "license", GENERIC_CACHE: "data_cache", + WRITE_THROUGH: "writeThrough", +} + +/** + * These define the numeric Redis databases that can be access with the SELECT command - + * (https://redis.io/commands/select/). By default a Redis server/cluster will have 16 selectable + * databases, increasing this count increases the amount of CPU/memory required to run the server. + * Ideally new Redis keyspaces should be used sparingly, only when absolutely necessary for performance + * to be maintained. Generally a keyspace can grow to be very large is scans are not needed or desired, + * but if you need to walk through all values in a database periodically then a separate selectable + * keyspace should be used. + */ +exports.SelectableDatabases = { + DEFAULT: 0, + WRITE_THROUGH: 1, + UNUSED_1: 2, + UNUSED_2: 3, + UNUSED_3: 4, + UNUSED_4: 5, + UNUSED_5: 6, + UNUSED_6: 7, + UNUSED_7: 8, + UNUSED_8: 9, + UNUSED_9: 10, + UNUSED_10: 11, + UNUSED_11: 12, + UNUSED_12: 13, + UNUSED_13: 14, + UNUSED_14: 15, } exports.SEPARATOR = SEPARATOR diff --git a/packages/backend-core/src/security/roles.js b/packages/backend-core/src/security/roles.js index 7c57cadcbf..33c9123b63 100644 --- a/packages/backend-core/src/security/roles.js +++ b/packages/backend-core/src/security/roles.js @@ -3,7 +3,7 @@ const { BUILTIN_PERMISSION_IDS, PermissionLevels } = require("./permissions") const { generateRoleID, getRoleParams, - DocumentTypes, + DocumentType, SEPARATOR, } = require("../db/utils") const { getAppDB } = require("../context") @@ -76,9 +76,9 @@ function isBuiltin(role) { /** * Works through the inheritance ranks to see how far up the builtin stack this ID is. */ -function builtinRoleToNumber(id) { +exports.builtinRoleToNumber = id => { const builtins = exports.getBuiltinRoles() - const MAX = Object.values(BUILTIN_IDS).length + 1 + const MAX = Object.values(builtins).length + 1 if (id === BUILTIN_IDS.ADMIN || id === BUILTIN_IDS.BUILDER) { return MAX } @@ -94,6 +94,22 @@ function builtinRoleToNumber(id) { return count } +/** + * Converts any role to a number, but has to be async to get the roles from db. + */ +exports.roleToNumber = async id => { + if (exports.isBuiltin(id)) { + return exports.builtinRoleToNumber(id) + } + const hierarchy = await exports.getUserRoleHierarchy(id) + for (let role of hierarchy) { + if (isBuiltin(role.inherits)) { + return exports.builtinRoleToNumber(role.inherits) + 1 + } + } + return 0 +} + /** * Returns whichever builtin roleID is lower. */ @@ -104,7 +120,8 @@ exports.lowerBuiltinRoleID = (roleId1, roleId2) => { if (!roleId2) { return roleId1 } - return builtinRoleToNumber(roleId1) > builtinRoleToNumber(roleId2) + return exports.builtinRoleToNumber(roleId1) > + exports.builtinRoleToNumber(roleId2) ? roleId2 : roleId1 } @@ -171,7 +188,7 @@ async function getAllUserRoles(userRoleId) { * to determine if a user can access something that requires a specific role. * @param {string} userRoleId The user's role ID, this can be found in their access token. * @param {object} opts Various options, such as whether to only retrieve the IDs (default true). - * @returns {Promise} returns an ordered array of the roles, with the first being their + * @returns {Promise} returns an ordered array of the roles, with the first being their * highest level of access and the last being the lowest level. */ exports.getUserRoleHierarchy = async (userRoleId, opts = { idOnly: true }) => { @@ -202,15 +219,24 @@ exports.getAllRoles = async appId => { if (appId) { return doWithDB(appId, internal) } else { - return internal(getAppDB()) + let appDB + try { + appDB = getAppDB() + } catch (error) { + // We don't have any apps, so we'll just use the built-in roles + } + return internal(appDB) } async function internal(db) { - const body = await db.allDocs( - getRoleParams(null, { - include_docs: true, - }) - ) - let roles = body.rows.map(row => row.doc) + let roles = [] + if (db) { + const body = await db.allDocs( + getRoleParams(null, { + include_docs: true, + }) + ) + roles = body.rows.map(row => row.doc) + } const builtinRoles = exports.getBuiltinRoles() // need to combine builtin with any DB record of them (for sake of permissions) @@ -328,7 +354,7 @@ class AccessController { * Adds the "role_" for builtin role IDs which are to be written to the DB (for permissions). */ exports.getDBRoleID = roleId => { - if (roleId.startsWith(DocumentTypes.ROLE)) { + if (roleId.startsWith(DocumentType.ROLE)) { return roleId } return generateRoleID(roleId) @@ -339,8 +365,8 @@ exports.getDBRoleID = roleId => { */ exports.getExternalRoleID = roleId => { // for built in roles we want to remove the DB role ID element (role_) - if (roleId.startsWith(DocumentTypes.ROLE) && isBuiltin(roleId)) { - return roleId.split(`${DocumentTypes.ROLE}${SEPARATOR}`)[1] + if (roleId.startsWith(DocumentType.ROLE) && isBuiltin(roleId)) { + return roleId.split(`${DocumentType.ROLE}${SEPARATOR}`)[1] } return roleId } diff --git a/packages/backend-core/src/security/sessions.js b/packages/backend-core/src/security/sessions.js deleted file mode 100644 index 4e6899c248..0000000000 --- a/packages/backend-core/src/security/sessions.js +++ /dev/null @@ -1,95 +0,0 @@ -const redis = require("../redis/authRedis") -const { v4: uuidv4 } = require("uuid") - -// a week in seconds -const EXPIRY_SECONDS = 86400 * 7 - -async function getSessionsForUser(userId) { - const client = await redis.getSessionClient() - const sessions = await client.scan(userId) - return sessions.map(session => session.value) -} - -function makeSessionID(userId, sessionId) { - return `${userId}/${sessionId}` -} - -async function invalidateSessions(userId, sessionIds = null) { - try { - let sessions = [] - - // If no sessionIds, get all the sessions for the user - if (!sessionIds) { - sessions = await getSessionsForUser(userId) - sessions.forEach( - session => - (session.key = makeSessionID(session.userId, session.sessionId)) - ) - } else { - // use the passed array of sessionIds - sessions = Array.isArray(sessionIds) ? sessionIds : [sessionIds] - sessions = sessions.map(sessionId => ({ - key: makeSessionID(userId, sessionId), - })) - } - - const client = await redis.getSessionClient() - const promises = [] - for (let session of sessions) { - promises.push(client.delete(session.key)) - } - await Promise.all(promises) - } catch (err) { - console.error(`Error invalidating sessions: ${err}`) - } -} - -exports.createASession = async (userId, session) => { - // invalidate all other sessions - await invalidateSessions(userId) - - const client = await redis.getSessionClient() - const sessionId = session.sessionId - if (!session.csrfToken) { - session.csrfToken = uuidv4() - } - session = { - createdAt: new Date().toISOString(), - lastAccessedAt: new Date().toISOString(), - ...session, - userId, - } - await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS) -} - -exports.updateSessionTTL = async session => { - const client = await redis.getSessionClient() - const key = makeSessionID(session.userId, session.sessionId) - session.lastAccessedAt = new Date().toISOString() - await client.store(key, session, EXPIRY_SECONDS) -} - -exports.endSession = async (userId, sessionId) => { - const client = await redis.getSessionClient() - await client.delete(makeSessionID(userId, sessionId)) -} - -exports.getSession = async (userId, sessionId) => { - try { - const client = await redis.getSessionClient() - return client.get(makeSessionID(userId, sessionId)) - } catch (err) { - // if can't get session don't error, just don't return anything - console.error(err) - return null - } -} - -exports.getAllSessions = async () => { - const client = await redis.getSessionClient() - const sessions = await client.scan() - return sessions.map(session => session.value) -} - -exports.getUserSessions = getSessionsForUser -exports.invalidateSessions = invalidateSessions diff --git a/packages/backend-core/src/security/sessions.ts b/packages/backend-core/src/security/sessions.ts new file mode 100644 index 0000000000..33230afc60 --- /dev/null +++ b/packages/backend-core/src/security/sessions.ts @@ -0,0 +1,119 @@ +const redis = require("../redis/init") +const { v4: uuidv4 } = require("uuid") +const { logWarn } = require("../logging") +const env = require("../environment") +import { + Session, + ScannedSession, + SessionKey, + CreateSession, +} from "@budibase/types" + +// a week in seconds +const EXPIRY_SECONDS = 86400 * 7 + +function makeSessionID(userId: string, sessionId: string) { + return `${userId}/${sessionId}` +} + +export async function getSessionsForUser(userId: string): Promise { + if (!userId) { + console.trace("Cannot get sessions for undefined userId") + return [] + } + const client = await redis.getSessionClient() + const sessions: ScannedSession[] = await client.scan(userId) + return sessions.map(session => session.value) +} + +export async function invalidateSessions( + userId: string, + opts: { sessionIds?: string[]; reason?: string } = {} +) { + try { + const reason = opts?.reason || "unknown" + let sessionIds: string[] = opts.sessionIds || [] + let sessionKeys: SessionKey[] + + // If no sessionIds, get all the sessions for the user + if (sessionIds.length === 0) { + const sessions = await getSessionsForUser(userId) + sessionKeys = sessions.map(session => ({ + key: makeSessionID(session.userId, session.sessionId), + })) + } else { + // use the passed array of sessionIds + sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds] + sessionKeys = sessionIds.map(sessionId => ({ + key: makeSessionID(userId, sessionId), + })) + } + + if (sessionKeys && sessionKeys.length > 0) { + const client = await redis.getSessionClient() + const promises = [] + for (let sessionKey of sessionKeys) { + promises.push(client.delete(sessionKey.key)) + } + if (!env.isTest()) { + logWarn( + `Invalidating sessions for ${userId} (reason: ${reason}) - ${sessionKeys + .map(sessionKey => sessionKey.key) + .join(", ")}` + ) + } + await Promise.all(promises) + } + } catch (err) { + console.error(`Error invalidating sessions: ${err}`) + } +} + +export async function createASession( + userId: string, + createSession: CreateSession +) { + // invalidate all other sessions + await invalidateSessions(userId, { reason: "creation" }) + + const client = await redis.getSessionClient() + const sessionId = createSession.sessionId + const csrfToken = createSession.csrfToken ? createSession.csrfToken : uuidv4() + const key = makeSessionID(userId, sessionId) + + const session: Session = { + ...createSession, + csrfToken, + createdAt: new Date().toISOString(), + lastAccessedAt: new Date().toISOString(), + userId, + } + await client.store(key, session, EXPIRY_SECONDS) +} + +export async function updateSessionTTL(session: Session) { + const client = await redis.getSessionClient() + const key = makeSessionID(session.userId, session.sessionId) + session.lastAccessedAt = new Date().toISOString() + await client.store(key, session, EXPIRY_SECONDS) +} + +export async function endSession(userId: string, sessionId: string) { + const client = await redis.getSessionClient() + await client.delete(makeSessionID(userId, sessionId)) +} + +export async function getSession( + userId: string, + sessionId: string +): Promise { + if (!userId || !sessionId) { + throw new Error(`Invalid session details - ${userId} - ${sessionId}`) + } + const client = await redis.getSessionClient() + const session = await client.get(makeSessionID(userId, sessionId)) + if (!session) { + throw new Error(`Session not found - ${userId} - ${sessionId}`) + } + return session +} diff --git a/packages/backend-core/src/security/tests/sessions.spec.ts b/packages/backend-core/src/security/tests/sessions.spec.ts new file mode 100644 index 0000000000..7f01bdcdb7 --- /dev/null +++ b/packages/backend-core/src/security/tests/sessions.spec.ts @@ -0,0 +1,12 @@ +import * as sessions from "../sessions" + +describe("sessions", () => { + describe("getSessionsForUser", () => { + it("returns empty when user is undefined", async () => { + // @ts-ignore - allow the undefined to be passed + const results = await sessions.getSessionsForUser(undefined) + + expect(results).toStrictEqual([]) + }) + }) +}) diff --git a/packages/backend-core/src/tenancy/index.js b/packages/backend-core/src/tenancy/index.js deleted file mode 100644 index c847033a12..0000000000 --- a/packages/backend-core/src/tenancy/index.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - ...require("../context"), - ...require("./tenancy"), -} diff --git a/packages/backend-core/src/tenancy/index.ts b/packages/backend-core/src/tenancy/index.ts new file mode 100644 index 0000000000..e0006abab2 --- /dev/null +++ b/packages/backend-core/src/tenancy/index.ts @@ -0,0 +1,9 @@ +import * as context from "../context" +import * as tenancy from "./tenancy" + +const pkg = { + ...context, + ...tenancy, +} + +export = pkg diff --git a/packages/backend-core/src/tenancy/tenancy.js b/packages/backend-core/src/tenancy/tenancy.ts similarity index 60% rename from packages/backend-core/src/tenancy/tenancy.js rename to packages/backend-core/src/tenancy/tenancy.ts index b9d5ad7fbe..ad5c6b5287 100644 --- a/packages/backend-core/src/tenancy/tenancy.js +++ b/packages/backend-core/src/tenancy/tenancy.ts @@ -1,18 +1,20 @@ -const { doWithDB } = require("../db") -const { StaticDatabases } = require("../db/constants") -const { baseGlobalDBName } = require("./utils") -const { +import { doWithDB } from "../db" +import { queryPlatformView } from "../db/views" +import { StaticDatabases, ViewName } from "../db/constants" +import { getGlobalDBName } from "../db/tenancy" +import { getTenantId, DEFAULT_TENANT_ID, isMultiTenant, getTenantIDFromAppID, -} = require("../context") -const env = require("../environment") +} from "../context" +import env from "../environment" +import { PlatformUser } from "@budibase/types" const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name -exports.addTenantToUrl = url => { +export const addTenantToUrl = (url: string) => { const tenantId = getTenantId() if (isMultiTenant()) { @@ -23,8 +25,8 @@ exports.addTenantToUrl = url => { return url } -exports.doesTenantExist = async tenantId => { - return doWithDB(PLATFORM_INFO_DB, async db => { +export const doesTenantExist = async (tenantId: string) => { + return doWithDB(PLATFORM_INFO_DB, async (db: any) => { let tenants try { tenants = await db.get(TENANT_DOC) @@ -40,9 +42,14 @@ exports.doesTenantExist = async tenantId => { }) } -exports.tryAddTenant = async (tenantId, userId, email) => { - return doWithDB(PLATFORM_INFO_DB, async db => { - const getDoc = async id => { +export const tryAddTenant = async ( + 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 } @@ -76,26 +83,18 @@ exports.tryAddTenant = async (tenantId, userId, email) => { if (tenants.tenantIds.indexOf(tenantId) === -1) { tenants.tenantIds.push(tenantId) promises.push(db.put(tenants)) + await afterCreateTenant() } await Promise.all(promises) }) } -exports.getGlobalDBName = (tenantId = null) => { - // tenant ID can be set externally, for example user API where - // new tenants are being created, this may be the case - if (!tenantId) { - tenantId = getTenantId() - } - return baseGlobalDBName(tenantId) +export const doWithGlobalDB = (tenantId: string, cb: any) => { + return doWithDB(getGlobalDBName(tenantId), cb) } -exports.doWithGlobalDB = (tenantId, cb) => { - return doWithDB(exports.getGlobalDBName(tenantId), cb) -} - -exports.lookupTenantId = async userId => { - return doWithDB(StaticDatabases.PLATFORM_INFO.name, async db => { +export const lookupTenantId = async (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) @@ -110,17 +109,19 @@ exports.lookupTenantId = async userId => { } // lookup, could be email or userId, either will return a doc -exports.getTenantUser = async identifier => { - return doWithDB(PLATFORM_INFO_DB, async db => { - try { - return await db.get(identifier) - } catch (err) { - return null - } - }) +export const getTenantUser = async ( + identifier: string +): Promise => { + // use the view here and allow to find anyone regardless of casing + // Use lowercase to ensure email login is case insensitive + const response = queryPlatformView(ViewName.PLATFORM_USERS_LOWERCASE, { + keys: [identifier.toLowerCase()], + include_docs: true, + }) as Promise + return response } -exports.isUserInAppTenant = (appId, user = null) => { +export const isUserInAppTenant = (appId: string, user?: any) => { let userTenantId if (user) { userTenantId = user.tenantId || DEFAULT_TENANT_ID @@ -131,8 +132,8 @@ exports.isUserInAppTenant = (appId, user = null) => { return tenantId === userTenantId } -exports.getTenantIds = async () => { - return doWithDB(PLATFORM_INFO_DB, async db => { +export const getTenantIds = async () => { + return doWithDB(PLATFORM_INFO_DB, async (db: any) => { let tenants try { tenants = await db.get(TENANT_DOC) diff --git a/packages/backend-core/src/tenancy/utils.js b/packages/backend-core/src/tenancy/utils.js deleted file mode 100644 index 70a965ddb7..0000000000 --- a/packages/backend-core/src/tenancy/utils.js +++ /dev/null @@ -1,12 +0,0 @@ -const { DEFAULT_TENANT_ID } = require("../constants") -const { StaticDatabases, SEPARATOR } = require("../db/constants") - -exports.baseGlobalDBName = tenantId => { - let dbName - if (!tenantId || tenantId === DEFAULT_TENANT_ID) { - dbName = StaticDatabases.GLOBAL.name - } else { - dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}` - } - return dbName -} diff --git a/packages/backend-core/src/tests/utils.spec.js b/packages/backend-core/src/tests/utils.spec.js new file mode 100644 index 0000000000..b487f23c6d --- /dev/null +++ b/packages/backend-core/src/tests/utils.spec.js @@ -0,0 +1,17 @@ +require("../../tests/utilities/TestConfiguration") +const { structures } = require("../../tests/utilities") +const utils = require("../utils") +const events = require("../events") +const { doInTenant, DEFAULT_TENANT_ID }= require("../context") + +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) + }) + }) + }) +}) \ No newline at end of file diff --git a/packages/backend-core/src/users.ts b/packages/backend-core/src/users.ts new file mode 100644 index 0000000000..44f04749c9 --- /dev/null +++ b/packages/backend-core/src/users.ts @@ -0,0 +1,94 @@ +import { + ViewName, + getUsersByAppParams, + getProdAppID, + generateAppUserID, +} from "./db/utils" +import { queryGlobalView } from "./db/views" +import { UNICODE_MAX } from "./db/constants" +import { BulkDocsResponse, User } from "@budibase/types" +import { getGlobalDB } from "./context" +import PouchDB from "pouchdb" + +export const bulkGetGlobalUsersById = async (userIds: string[]) => { + const db = getGlobalDB() as PouchDB.Database + return ( + await db.allDocs({ + keys: userIds, + include_docs: true, + }) + ).rows.map(row => row.doc) as User[] +} + +export const bulkUpdateGlobalUsers = async (users: User[]) => { + const db = getGlobalDB() as PouchDB.Database + return (await db.bulkDocs(users)) as BulkDocsResponse +} + +/** + * 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 +): Promise => { + if (email == null) { + throw "Must supply an email address to view" + } + + const response = await queryGlobalView(ViewName.USER_BY_EMAIL, { + key: email.toLowerCase(), + include_docs: true, + }) + + if (Array.isArray(response)) { + // shouldn't be able to happen, but need to handle just in case + throw new Error(`Multiple users found with email address: ${email}`) + } + + return response +} + +export const searchGlobalUsersByApp = async (appId: any, opts: any) => { + if (typeof appId !== "string") { + throw new Error("Must provide a string based app ID") + } + const params = getUsersByAppParams(appId, { + include_docs: true, + }) + 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] +} + +export const getGlobalUserByAppPage = (appId: string, user: User) => { + if (!user) { + return + } + return generateAppUserID(getProdAppID(appId), user._id!) +} + +/** + * Performs a starts with search on the global email view. + */ +export const searchGlobalUsersByEmail = async (email: string, opts: any) => { + if (typeof email !== "string") { + throw new Error("Must provide a string to search by") + } + const lcEmail = email.toLowerCase() + // handle if passing up startkey for pagination + const startkey = opts && opts.startkey ? opts.startkey : lcEmail + let response = await queryGlobalView(ViewName.USER_BY_EMAIL, { + ...opts, + startkey, + endkey: `${lcEmail}${UNICODE_MAX}`, + }) + if (!response) { + response = [] + } + return Array.isArray(response) ? response : [response] +} diff --git a/packages/backend-core/src/utils.js b/packages/backend-core/src/utils.js index e764f35803..6b59c7cb72 100644 --- a/packages/backend-core/src/utils.js +++ b/packages/backend-core/src/utils.js @@ -1,29 +1,18 @@ -const { - DocumentTypes, - SEPARATOR, - ViewNames, - generateGlobalUserID, - getAllApps, -} = require("./db/utils") +const { DocumentType, SEPARATOR, ViewName, getAllApps } = require("./db/utils") const jwt = require("jsonwebtoken") const { options } = require("./middleware/passport/jwt") const { queryGlobalView } = require("./db/views") -const { Headers, UserStatus, Cookies, MAX_VALID_DATE } = require("./constants") -const { - doWithGlobalDB, - updateTenantId, - getTenantUser, - tryAddTenant, -} = require("./tenancy") -const environment = require("./environment") -const accounts = require("./cloud/accounts") -const { hash } = require("./hashing") -const userCache = require("./cache/user") +const { Headers, Cookies, MAX_VALID_DATE } = require("./constants") const env = require("./environment") -const { getUserSessions, invalidateSessions } = require("./security/sessions") +const userCache = require("./cache/user") +const { + getSessionsForUser, + invalidateSessions, +} = require("./security/sessions") +const events = require("./events") const tenancy = require("./tenancy") -const APP_PREFIX = DocumentTypes.APP + SEPARATOR +const APP_PREFIX = DocumentType.APP + SEPARATOR const PROD_APP_PREFIX = "/app/" function confirmAppId(possibleAppId) { @@ -53,6 +42,18 @@ async function resolveAppUrl(ctx) { return app && app.appId ? app.appId : undefined } +exports.isServingApp = ctx => { + // dev app + if (ctx.path.startsWith(`/${APP_PREFIX}`)) { + return true + } + // prod app + if (ctx.path.startsWith(PROD_APP_PREFIX)) { + return true + } + return false +} + /** * Given a request tries to find the appId, which can be located in various places * @param {object} ctx The main request body to look through. @@ -135,8 +136,8 @@ exports.setCookie = (ctx, value, name = "builder", opts = { sign: true }) => { overwrite: true, } - if (environment.COOKIE_DOMAIN) { - config.domain = environment.COOKIE_DOMAIN + if (env.COOKIE_DOMAIN) { + config.domain = env.COOKIE_DOMAIN } ctx.cookies.set(name, value, config) @@ -159,25 +160,8 @@ exports.isClient = ctx => { return ctx.headers[Headers.TYPE] === "client" } -/** - * 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. - * @return {Promise} - */ -exports.getGlobalUserByEmail = async email => { - if (email == null) { - throw "Must supply an email address to view" - } - - return queryGlobalView(ViewNames.USER_BY_EMAIL, { - key: email.toLowerCase(), - include_docs: true, - }) -} - const getBuilders = async () => { - const builders = await queryGlobalView(ViewNames.USER_BY_BUILDERS, { + const builders = await queryGlobalView(ViewName.USER_BY_BUILDERS, { include_docs: false, }) @@ -197,124 +181,6 @@ exports.getBuildersCount = async () => { return builders.length } -const DEFAULT_SAVE_USER = { - hashPassword: true, - requirePassword: true, - bulkCreate: false, -} - -exports.internalSaveUser = async ( - user, - tenantId, - { hashPassword, requirePassword, bulkCreate } = DEFAULT_SAVE_USER -) => { - if (!tenantId) { - throw "No tenancy specified." - } - // need to set the context for this request, as specified - updateTenantId(tenantId) - // specify the tenancy incase we're making a new admin user (public) - return doWithGlobalDB(tenantId, async db => { - let { email, password, _id } = user - // make sure another user isn't using the same email - let dbUser - // user can't exist in bulk creation - if (bulkCreate) { - dbUser = null - } else if (email) { - // check budibase users inside the tenant - dbUser = await exports.getGlobalUserByEmail(email) - if (dbUser != null && (dbUser._id !== _id || Array.isArray(dbUser))) { - throw `Email address ${email} already in use.` - } - - // check budibase users in other tenants - if (env.MULTI_TENANCY) { - const tenantUser = await getTenantUser(email) - if (tenantUser != null && tenantUser.tenantId !== tenantId) { - throw `Email address ${email} already in use.` - } - } - - // check root account users in account portal - if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { - const account = await accounts.getAccount(email) - if (account && account.verified && account.tenantId !== tenantId) { - throw `Email address ${email} already in use.` - } - } - } else { - dbUser = await db.get(_id) - } - - // get the password, make sure one is defined - let hashedPassword - if (password) { - hashedPassword = hashPassword ? await hash(password) : password - } else if (dbUser) { - hashedPassword = dbUser.password - } else if (requirePassword) { - throw "Password must be specified." - } - - _id = _id || generateGlobalUserID() - user = { - createdAt: Date.now(), - ...dbUser, - ...user, - _id, - password: hashedPassword, - tenantId, - } - // make sure the roles object is always present - if (!user.roles) { - user.roles = {} - } - // add the active status to a user if its not provided - if (user.status == null) { - user.status = UserStatus.ACTIVE - } - try { - const putOpts = { - password: hashedPassword, - ...user, - } - if (bulkCreate) { - return putOpts - } - const response = await db.put(putOpts) - if (env.MULTI_TENANCY) { - await tryAddTenant(tenantId, _id, email) - } - await userCache.invalidateUser(response.id) - return { - _id: response.id, - _rev: response.rev, - email, - } - } catch (err) { - if (err.status === 409) { - throw "User exists already" - } else { - throw err - } - } - }) -} - -// maintained for api compat, don't want to change function signature -exports.saveUser = async ( - user, - tenantId, - hashPassword = true, - requirePassword = true -) => { - return exports.internalSaveUser(user, tenantId, { - hashPassword, - requirePassword, - }) -} - /** * Logs a user out from budibase. Re-used across account portal and builder. */ @@ -322,7 +188,7 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => { if (!ctx) throw new Error("Koa context must be supplied to logout.") const currentSession = exports.getCookie(ctx, Cookies.Auth) - let sessions = await getUserSessions(userId) + let sessions = await getSessionsForUser(userId) if (keepActiveSession) { sessions = sessions.filter( @@ -334,9 +200,12 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => { exports.clearCookie(ctx, Cookies.CurrentApp) } - await invalidateSessions( - userId, - sessions.map(({ sessionId }) => sessionId) - ) + const sessionIds = sessions.map(({ sessionId }) => sessionId) + await invalidateSessions(userId, { sessionIds, reason: "logout" }) + await events.auth.logout() await userCache.invalidateUser(userId) } + +exports.timeout = timeMs => { + return new Promise(resolve => setTimeout(resolve, timeMs)) +} diff --git a/packages/backend-core/tests/index.ts b/packages/backend-core/tests/index.ts new file mode 100644 index 0000000000..b23d52e6e0 --- /dev/null +++ b/packages/backend-core/tests/index.ts @@ -0,0 +1 @@ +export * from "./utilities" diff --git a/packages/backend-core/tests/utilities/TestConfiguration.js b/packages/backend-core/tests/utilities/TestConfiguration.js new file mode 100644 index 0000000000..207b1d937f --- /dev/null +++ b/packages/backend-core/tests/utilities/TestConfiguration.js @@ -0,0 +1 @@ +require("./db") diff --git a/packages/backend-core/src/tests/utilities/dbConfig.js b/packages/backend-core/tests/utilities/db.js similarity index 54% rename from packages/backend-core/src/tests/utilities/dbConfig.js rename to packages/backend-core/tests/utilities/db.js index acd692df40..9d94bc2c7c 100644 --- a/packages/backend-core/src/tests/utilities/dbConfig.js +++ b/packages/backend-core/tests/utilities/db.js @@ -1,5 +1,6 @@ -const core = require("../../index") +const core = require("../../src/index") const dbConfig = { inMemory: true, + allDbs: true, } core.init({ db: dbConfig }) diff --git a/packages/backend-core/tests/utilities/index.ts b/packages/backend-core/tests/utilities/index.ts new file mode 100644 index 0000000000..1e73be4c17 --- /dev/null +++ b/packages/backend-core/tests/utilities/index.ts @@ -0,0 +1,2 @@ +export * as mocks from "./mocks" +export * as structures from "./structures" diff --git a/packages/backend-core/tests/utilities/mocks/accounts.ts b/packages/backend-core/tests/utilities/mocks/accounts.ts new file mode 100644 index 0000000000..79436443db --- /dev/null +++ b/packages/backend-core/tests/utilities/mocks/accounts.ts @@ -0,0 +1,7 @@ +export const getAccount = jest.fn() +export const getAccountByTenantId = jest.fn() + +jest.mock("../../../src/cloud/accounts", () => ({ + getAccount, + getAccountByTenantId, +})) diff --git a/packages/backend-core/tests/utilities/mocks/date.ts b/packages/backend-core/tests/utilities/mocks/date.ts new file mode 100644 index 0000000000..f580b68349 --- /dev/null +++ b/packages/backend-core/tests/utilities/mocks/date.ts @@ -0,0 +1,2 @@ +export const MOCK_DATE = new Date("2020-01-01T00:00:00.000Z") +export const MOCK_DATE_TIMESTAMP = 1577836800000 diff --git a/packages/backend-core/tests/utilities/mocks/events.ts b/packages/backend-core/tests/utilities/mocks/events.ts new file mode 100644 index 0000000000..415d59019d --- /dev/null +++ b/packages/backend-core/tests/utilities/mocks/events.ts @@ -0,0 +1,119 @@ +const processors = require("../../../src/events/processors") + +jest.spyOn(processors.analyticsProcessor, "processEvent") + +const events = require("../../../src/events") + +jest.spyOn(events.identification, "identifyTenantGroup") +jest.spyOn(events.identification, "identifyUser") + +jest.spyOn(events.backfill, "appSucceeded") +jest.spyOn(events.backfill, "tenantSucceeded") + +jest.spyOn(events.account, "created") +jest.spyOn(events.account, "deleted") +jest.spyOn(events.account, "verified") + +jest.spyOn(events.app, "created") +jest.spyOn(events.app, "updated") +jest.spyOn(events.app, "deleted") +jest.spyOn(events.app, "published") +jest.spyOn(events.app, "unpublished") +jest.spyOn(events.app, "templateImported") +jest.spyOn(events.app, "fileImported") +jest.spyOn(events.app, "versionUpdated") +jest.spyOn(events.app, "versionReverted") +jest.spyOn(events.app, "reverted") +jest.spyOn(events.app, "exported") + +jest.spyOn(events.auth, "login") +jest.spyOn(events.auth, "logout") +jest.spyOn(events.auth, "SSOCreated") +jest.spyOn(events.auth, "SSOUpdated") +jest.spyOn(events.auth, "SSOActivated") +jest.spyOn(events.auth, "SSODeactivated") + +jest.spyOn(events.automation, "created") +jest.spyOn(events.automation, "deleted") +jest.spyOn(events.automation, "tested") +jest.spyOn(events.automation, "stepCreated") +jest.spyOn(events.automation, "stepDeleted") +jest.spyOn(events.automation, "triggerUpdated") + +jest.spyOn(events.datasource, "created") +jest.spyOn(events.datasource, "updated") +jest.spyOn(events.datasource, "deleted") + +jest.spyOn(events.email, "SMTPCreated") +jest.spyOn(events.email, "SMTPUpdated") + +jest.spyOn(events.layout, "created") +jest.spyOn(events.layout, "deleted") + +jest.spyOn(events.org, "nameUpdated") +jest.spyOn(events.org, "logoUpdated") +jest.spyOn(events.org, "platformURLUpdated") +jest.spyOn(events.org, "analyticsOptOut") + +jest.spyOn(events.installation, "versionChecked") + +jest.spyOn(events.query, "created") +jest.spyOn(events.query, "updated") +jest.spyOn(events.query, "deleted") +jest.spyOn(events.query, "imported") +jest.spyOn(events.query, "previewed") + +jest.spyOn(events.role, "created") +jest.spyOn(events.role, "updated") +jest.spyOn(events.role, "deleted") +jest.spyOn(events.role, "assigned") +jest.spyOn(events.role, "unassigned") + +jest.spyOn(events.rows, "imported") +jest.spyOn(events.rows, "created") + +jest.spyOn(events.screen, "created") +jest.spyOn(events.screen, "deleted") + +jest.spyOn(events.user, "created") +jest.spyOn(events.user, "updated") +jest.spyOn(events.user, "deleted") +jest.spyOn(events.user, "permissionAdminAssigned") +jest.spyOn(events.user, "permissionAdminRemoved") +jest.spyOn(events.user, "permissionBuilderAssigned") +jest.spyOn(events.user, "permissionBuilderRemoved") +jest.spyOn(events.user, "invited") +jest.spyOn(events.user, "inviteAccepted") +jest.spyOn(events.user, "passwordForceReset") +jest.spyOn(events.user, "passwordUpdated") +jest.spyOn(events.user, "passwordResetRequested") +jest.spyOn(events.user, "passwordReset") + +jest.spyOn(events.group, "created") +jest.spyOn(events.group, "updated") +jest.spyOn(events.group, "deleted") +jest.spyOn(events.group, "usersAdded") +jest.spyOn(events.group, "usersDeleted") +jest.spyOn(events.group, "createdOnboarding") +jest.spyOn(events.group, "permissionsEdited") + +jest.spyOn(events.serve, "servedBuilder") +jest.spyOn(events.serve, "servedApp") +jest.spyOn(events.serve, "servedAppPreview") + +jest.spyOn(events.table, "created") +jest.spyOn(events.table, "updated") +jest.spyOn(events.table, "deleted") +jest.spyOn(events.table, "exported") +jest.spyOn(events.table, "imported") + +jest.spyOn(events.view, "created") +jest.spyOn(events.view, "updated") +jest.spyOn(events.view, "deleted") +jest.spyOn(events.view, "exported") +jest.spyOn(events.view, "filterCreated") +jest.spyOn(events.view, "filterUpdated") +jest.spyOn(events.view, "filterDeleted") +jest.spyOn(events.view, "calculationCreated") +jest.spyOn(events.view, "calculationUpdated") +jest.spyOn(events.view, "calculationDeleted") diff --git a/packages/backend-core/tests/utilities/mocks/index.ts b/packages/backend-core/tests/utilities/mocks/index.ts new file mode 100644 index 0000000000..7031b225ec --- /dev/null +++ b/packages/backend-core/tests/utilities/mocks/index.ts @@ -0,0 +1,4 @@ +import "./posthog" +import "./events" +export * as accounts from "./accounts" +export * as date from "./date" diff --git a/packages/backend-core/tests/utilities/mocks/posthog.ts b/packages/backend-core/tests/utilities/mocks/posthog.ts new file mode 100644 index 0000000000..e9cc653ccc --- /dev/null +++ b/packages/backend-core/tests/utilities/mocks/posthog.ts @@ -0,0 +1,7 @@ +jest.mock("posthog-node", () => { + return jest.fn().mockImplementation(() => { + return { + capture: jest.fn(), + } + }) +}) diff --git a/packages/backend-core/tests/utilities/structures/index.ts b/packages/backend-core/tests/utilities/structures/index.ts new file mode 100644 index 0000000000..12b6ab7ad6 --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/index.ts @@ -0,0 +1 @@ +export * as koa from "./koa" diff --git a/packages/backend-core/tests/utilities/structures/koa.ts b/packages/backend-core/tests/utilities/structures/koa.ts new file mode 100644 index 0000000000..6f0f7866e6 --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/koa.ts @@ -0,0 +1,5 @@ +import { createMockContext } from "@shopify/jest-koa-mocks" + +export const newContext = () => { + return createMockContext() +} diff --git a/packages/backend-core/tsconfig.build.json b/packages/backend-core/tsconfig.build.json new file mode 100644 index 0000000000..40ffe6b827 --- /dev/null +++ b/packages/backend-core/tsconfig.build.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "lib": ["es2020"], + "allowJs": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "incremental": true, + "sourceMap": true, + "declaration": true, + "types": [ "node", "jest" ], + "outDir": "dist" + }, + "include": [ + "**/*.js", + "**/*.ts", + "package.json" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.spec.ts", + "**/*.spec.js" + ] +} \ No newline at end of file diff --git a/packages/backend-core/tsconfig.json b/packages/backend-core/tsconfig.json new file mode 100644 index 0000000000..ccefd149a0 --- /dev/null +++ b/packages/backend-core/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.build.json", + "compilerOptions": { + "composite": true, + "baseUrl": ".", + "paths": { + "@budibase/types": ["../types/src"] + } + }, + "references": [ + { "path": "../types" }, + ] +} \ No newline at end of file diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock index e1d178b32c..2e62aea734 100644 --- a/packages/backend-core/yarn.lock +++ b/packages/backend-core/yarn.lock @@ -2,177 +2,158 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431" - integrity sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA== +"@ampproject/remapping@^2.1.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== dependencies: - "@babel/highlight" "^7.16.0" + "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/trace-mapping" "^0.3.9" -"@babel/compat-data@^7.16.0": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.4.tgz#081d6bbc336ec5c2435c6346b2ae1fb98b5ac68e" - integrity sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q== - -"@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.7.5": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.0.tgz#c4ff44046f5fe310525cc9eb4ef5147f0c5374d4" - integrity sha512-mYZEvshBRHGsIAiyH5PzCFTCfbWfoYbO/jcSdXQSUQu1/pW0xDZAUP7KEc32heqWTAfAHhV9j1vH8Sav7l+JNQ== +"@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/code-frame" "^7.16.0" - "@babel/generator" "^7.16.0" - "@babel/helper-compilation-targets" "^7.16.0" - "@babel/helper-module-transforms" "^7.16.0" - "@babel/helpers" "^7.16.0" - "@babel/parser" "^7.16.0" - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" + "@babel/highlight" "^7.16.7" + +"@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.1.0", "@babel/core@^7.12.3", "@babel/core@^7.7.2", "@babel/core@^7.8.0": + version "7.18.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.2.tgz#87b2fcd7cce9becaa7f5acebdc4f09f3dd19d876" + integrity sha512-A8pri1YJiC5UnkdrWcmfZTJTV85b4UXTAfImGmCfYmax4TR9Cw8sDS0MOk++Gp2mE/BefVJ5nwy5yzqNJbP/DQ== + dependencies: + "@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.1.2" + json5 "^2.2.1" semver "^6.3.0" - source-map "^0.5.0" -"@babel/generator@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.0.tgz#d40f3d1d5075e62d3500bccb67f3daa8a95265b2" - integrity sha512-RR8hUCfRQn9j9RPKEVXo9LiwoxLPYn6hNZlvUOR8tSnaxlD0p0+la00ZP9/SnRt6HchKr+X0fO2r8vrETiJGew== +"@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.16.0" + "@babel/types" "^7.18.2" + "@jridgewell/gen-mapping" "^0.3.0" jsesc "^2.5.1" - source-map "^0.5.0" -"@babel/helper-compilation-targets@^7.16.0": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.3.tgz#5b480cd13f68363df6ec4dc8ac8e2da11363cbf0" - integrity sha512-vKsoSQAyBmxS35JUOOt+07cLc6Nk/2ljLIHwmq2/NM6hdioUaqEXq/S+nXvbvXbZkNDlWOymPanJGOc4CBjSJA== +"@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.16.0" - "@babel/helper-validator-option" "^7.14.5" - browserslist "^4.17.5" + "@babel/compat-data" "^7.17.10" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.20.2" semver "^6.3.0" -"@babel/helper-function-name@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.0.tgz#b7dd0797d00bbfee4f07e9c4ea5b0e30c8bb1481" - integrity sha512-BZh4mEk1xi2h4HFjWUXRQX5AEx4rvaZxHgax9gcjdLWdkjsY7MKt5p0otjsg5noXw+pB+clMCjw+aEVYADMjog== +"@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-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/helper-get-function-arity" "^7.16.0" - "@babel/template" "^7.16.0" - "@babel/types" "^7.16.0" + "@babel/template" "^7.16.7" + "@babel/types" "^7.17.0" -"@babel/helper-get-function-arity@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.0.tgz#0088c7486b29a9cb5d948b1a1de46db66e089cfa" - integrity sha512-ASCquNcywC1NkYh/z7Cgp3w31YW8aojjYIlNg4VeJiHkqyP4AzIvr4qx7pYDb4/s8YcsZWqqOSxgkvjUz1kpDQ== +"@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.0" + "@babel/types" "^7.16.7" -"@babel/helper-hoist-variables@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.0.tgz#4c9023c2f1def7e28ff46fc1dbcd36a39beaa81a" - integrity sha512-1AZlpazjUR0EQZQv3sgRNfM9mEVWPK3M6vlalczA+EECcPz3XPh6VplbErL5UoMpChhSck5wAJHthlj1bYpcmg== +"@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.0" + "@babel/types" "^7.16.7" -"@babel/helper-member-expression-to-functions@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.0.tgz#29287040efd197c77636ef75188e81da8bccd5a4" - integrity sha512-bsjlBFPuWT6IWhl28EdrQ+gTvSvj5tqVP5Xeftp07SEuz5pLnsXZuDkDD3Rfcxy0IsHmbZ+7B2/9SHzxO0T+sQ== +"@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/types" "^7.16.0" + "@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-imports@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz#90538e60b672ecf1b448f5f4f5433d37e79a3ec3" - integrity sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg== +"@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.16.0" + "@babel/types" "^7.18.2" -"@babel/helper-module-transforms@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.0.tgz#1c82a8dd4cb34577502ebd2909699b194c3e9bb5" - integrity sha512-My4cr9ATcaBbmaEa8M0dZNA74cfI6gitvUAskgDtAFmAqyFKDSHQo5YstxPbN+lzHl2D9l/YOEFqb2mtUh4gfA== +"@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/helper-module-imports" "^7.16.0" - "@babel/helper-replace-supers" "^7.16.0" - "@babel/helper-simple-access" "^7.16.0" - "@babel/helper-split-export-declaration" "^7.16.0" - "@babel/helper-validator-identifier" "^7.15.7" - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" + "@babel/types" "^7.16.7" -"@babel/helper-optimise-call-expression@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.0.tgz#cecdb145d70c54096b1564f8e9f10cd7d193b338" - integrity sha512-SuI467Gi2V8fkofm2JPnZzB/SUuXoJA5zXe/xzyPP2M04686RzFKFHPK6HDVN6JvWBIEW8tt9hPR7fXdn2Lgpw== +"@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-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/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/types" "^7.16.0" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.2" + "@babel/types" "^7.18.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.8.0": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" - integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== - -"@babel/helper-replace-supers@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.16.0.tgz#73055e8d3cf9bcba8ddb55cad93fedc860f68f17" - integrity sha512-TQxuQfSCdoha7cpRNJvfaYxxxzmbxXw/+6cS7V02eeDYyhxderSoMVALvwupA54/pZcOTtVeJ0xccp1nGWladA== +"@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-member-expression-to-functions" "^7.16.0" - "@babel/helper-optimise-call-expression" "^7.16.0" - "@babel/traverse" "^7.16.0" - "@babel/types" "^7.16.0" - -"@babel/helper-simple-access@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.0.tgz#21d6a27620e383e37534cf6c10bba019a6f90517" - integrity sha512-o1rjBT/gppAqKsYfUdfHq5Rk03lMQrkPHG1OWzHWpLgVXRH4HnMM9Et9CVdIqwkCQlobnGHEJMsgWP/jE1zUiw== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-split-export-declaration@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.0.tgz#29672f43663e936df370aaeb22beddb3baec7438" - integrity sha512-0YMMRpuDFNGTHNRiiqJX19GjNXA4H0E8jZ2ibccfSxaCogbm3am5WN/2nQNj0YnQwGWM1J06GOcQ2qnh3+0paw== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-validator-identifier@^7.15.7": - version "7.15.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" - integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== - -"@babel/helper-validator-option@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" - integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow== - -"@babel/helpers@^7.16.0": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.3.tgz#27fc64f40b996e7074dc73128c3e5c3e7f55c43c" - integrity sha512-Xn8IhDlBPhvYTvgewPKawhADichOsbkZuzN7qz2BusOM0brChsyXMDJvldWaYMMUNiCQdQzNEioXTp3sC8Nt8w== - dependencies: - "@babel/template" "^7.16.0" - "@babel/traverse" "^7.16.3" - "@babel/types" "^7.16.0" - -"@babel/highlight@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.0.tgz#6ceb32b2ca4b8f5f361fb7fd821e3fddf4a1725a" - integrity sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g== - dependencies: - "@babel/helper-validator-identifier" "^7.15.7" + "@babel/helper-validator-identifier" "^7.16.7" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.0", "@babel/parser@^7.16.3": - version "7.16.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.4.tgz#d5f92f57cf2c74ffe9b37981c0e72fee7311372e" - integrity sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng== +"@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/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -258,43 +239,51 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" +"@babel/plugin-syntax-typescript@^7.7.2": + 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.17.12" + "@babel/runtime@^7.15.4": - version "7.17.9" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72" - integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg== + 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.4" -"@babel/template@^7.16.0", "@babel/template@^7.3.3": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6" - integrity sha512-MnZdpFD/ZdYhXwiunMqqgyZyucaYsbL0IrjoGjaVhGilz+x8YB++kRfygSOIj1yOtWKPlx7NBp+9I1RQSgsd5A== +"@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.0" - "@babel/parser" "^7.16.0" - "@babel/types" "^7.16.0" + "@babel/code-frame" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/types" "^7.16.7" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.16.0", "@babel/traverse@^7.16.3": - version "7.16.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.3.tgz#f63e8a938cc1b780f66d9ed3c54f532ca2d14787" - integrity sha512-eolumr1vVMjqevCpwVO99yN/LoGL0EyHiLO5I043aYQvwOJ9eR5UsZSClHVCzfhBduMAsSzgA/6AyqPjNayJag== +"@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.0" - "@babel/generator" "^7.16.0" - "@babel/helper-function-name" "^7.16.0" - "@babel/helper-hoist-variables" "^7.16.0" - "@babel/helper-split-export-declaration" "^7.16.0" - "@babel/parser" "^7.16.3" - "@babel/types" "^7.16.0" + "@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/types@^7.0.0", "@babel/types@^7.16.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.0.tgz#db3b313804f96aadd0b776c4823e127ad67289ba" - integrity sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg== +"@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.15.7" + "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" "@bcoe/v8-coverage@^0.2.3": @@ -302,13 +291,17 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@cnakazawa/watch@^1.0.3": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" - integrity sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ== +"@hapi/hoek@^9.0.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== + +"@hapi/topo@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== dependencies: - exec-sh "^0.3.2" - minimist "^1.2.0" + "@hapi/hoek" "^9.0.0" "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" @@ -326,177 +319,215 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2" - integrity sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g== +"@jest/console@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-27.5.1.tgz#260fe7239602fe5130a94f1aa386eff54b014bba" + integrity sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg== dependencies: - "@jest/types" "^26.6.2" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^26.6.2" - jest-util "^26.6.2" + jest-message-util "^27.5.1" + jest-util "^27.5.1" slash "^3.0.0" -"@jest/core@^26.6.3": - version "26.6.3" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.6.3.tgz#7639fcb3833d748a4656ada54bde193051e45fad" - integrity sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw== +"@jest/core@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-27.5.1.tgz#267ac5f704e09dc52de2922cbf3af9edcd64b626" + integrity sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ== dependencies: - "@jest/console" "^26.6.2" - "@jest/reporters" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/console" "^27.5.1" + "@jest/reporters" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" + emittery "^0.8.1" exit "^0.1.2" - graceful-fs "^4.2.4" - jest-changed-files "^26.6.2" - jest-config "^26.6.3" - jest-haste-map "^26.6.2" - jest-message-util "^26.6.2" - jest-regex-util "^26.0.0" - jest-resolve "^26.6.2" - jest-resolve-dependencies "^26.6.3" - jest-runner "^26.6.3" - jest-runtime "^26.6.3" - jest-snapshot "^26.6.2" - jest-util "^26.6.2" - jest-validate "^26.6.2" - jest-watcher "^26.6.2" - micromatch "^4.0.2" - p-each-series "^2.1.0" + graceful-fs "^4.2.9" + jest-changed-files "^27.5.1" + jest-config "^27.5.1" + jest-haste-map "^27.5.1" + jest-message-util "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-resolve-dependencies "^27.5.1" + jest-runner "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + jest-watcher "^27.5.1" + micromatch "^4.0.4" rimraf "^3.0.0" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/environment@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.6.2.tgz#ba364cc72e221e79cc8f0a99555bf5d7577cf92c" - integrity sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA== +"@jest/environment@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-27.5.1.tgz#d7425820511fe7158abbecc010140c3fd3be9c74" + integrity sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA== dependencies: - "@jest/fake-timers" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" - jest-mock "^26.6.2" + jest-mock "^27.5.1" -"@jest/fake-timers@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" - integrity sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA== +"@jest/fake-timers@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-27.5.1.tgz#76979745ce0579c8a94a4678af7a748eda8ada74" + integrity sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ== dependencies: - "@jest/types" "^26.6.2" - "@sinonjs/fake-timers" "^6.0.1" + "@jest/types" "^27.5.1" + "@sinonjs/fake-timers" "^8.0.1" "@types/node" "*" - jest-message-util "^26.6.2" - jest-mock "^26.6.2" - jest-util "^26.6.2" + jest-message-util "^27.5.1" + jest-mock "^27.5.1" + jest-util "^27.5.1" -"@jest/globals@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.6.2.tgz#5b613b78a1aa2655ae908eba638cc96a20df720a" - integrity sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA== +"@jest/globals@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-27.5.1.tgz#7ac06ce57ab966566c7963431cef458434601b2b" + integrity sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q== dependencies: - "@jest/environment" "^26.6.2" - "@jest/types" "^26.6.2" - expect "^26.6.2" + "@jest/environment" "^27.5.1" + "@jest/types" "^27.5.1" + expect "^27.5.1" -"@jest/reporters@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.6.2.tgz#1f518b99637a5f18307bd3ecf9275f6882a667f6" - integrity sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw== +"@jest/reporters@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-27.5.1.tgz#ceda7be96170b03c923c37987b64015812ffec04" + integrity sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/console" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" glob "^7.1.2" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^4.0.3" + istanbul-lib-instrument "^5.1.0" istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.0.2" - jest-haste-map "^26.6.2" - jest-resolve "^26.6.2" - jest-util "^26.6.2" - jest-worker "^26.6.2" + istanbul-reports "^3.1.3" + jest-haste-map "^27.5.1" + jest-resolve "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" slash "^3.0.0" source-map "^0.6.0" string-length "^4.0.1" terminal-link "^2.0.0" - v8-to-istanbul "^7.0.0" - optionalDependencies: - node-notifier "^8.0.0" + v8-to-istanbul "^8.1.0" -"@jest/source-map@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.6.2.tgz#29af5e1e2e324cafccc936f218309f54ab69d535" - integrity sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA== +"@jest/source-map@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.5.1.tgz#6608391e465add4205eae073b55e7f279e04e8cf" + integrity sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg== dependencies: callsites "^3.0.0" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" source-map "^0.6.0" -"@jest/test-result@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.6.2.tgz#55da58b62df134576cc95476efa5f7949e3f5f18" - integrity sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ== +"@jest/test-result@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-27.5.1.tgz#56a6585fa80f7cdab72b8c5fc2e871d03832f5bb" + integrity sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag== dependencies: - "@jest/console" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/console" "^27.5.1" + "@jest/types" "^27.5.1" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^26.6.3": - version "26.6.3" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz#98e8a45100863886d074205e8ffdc5a7eb582b17" - integrity sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw== +"@jest/test-sequencer@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz#4057e0e9cea4439e544c6353c6affe58d095745b" + integrity sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ== dependencies: - "@jest/test-result" "^26.6.2" - graceful-fs "^4.2.4" - jest-haste-map "^26.6.2" - jest-runner "^26.6.3" - jest-runtime "^26.6.3" + "@jest/test-result" "^27.5.1" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-runtime "^27.5.1" -"@jest/transform@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.6.2.tgz#5ac57c5fa1ad17b2aae83e73e45813894dcf2e4b" - integrity sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA== +"@jest/transform@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409" + integrity sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw== dependencies: "@babel/core" "^7.1.0" - "@jest/types" "^26.6.2" - babel-plugin-istanbul "^6.0.0" + "@jest/types" "^27.5.1" + babel-plugin-istanbul "^6.1.1" chalk "^4.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" - graceful-fs "^4.2.4" - jest-haste-map "^26.6.2" - jest-regex-util "^26.0.0" - jest-util "^26.6.2" - micromatch "^4.0.2" - pirates "^4.0.1" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-regex-util "^27.5.1" + jest-util "^27.5.1" + micromatch "^4.0.4" + pirates "^4.0.4" slash "^3.0.0" source-map "^0.6.1" write-file-atomic "^3.0.0" -"@jest/types@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" - integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== +"@jest/types@^27.5.1": + version "27.5.1" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" + integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" "@types/node" "*" - "@types/yargs" "^15.0.0" + "@types/yargs" "^16.0.0" chalk "^4.0.0" +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== + dependencies: + "@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/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/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.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.9" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz#09a8781a3a036151cdebbe8719d6f8b25d4058bc" @@ -512,6 +543,36 @@ semver "^7.3.5" tar "^6.1.11" +"@shopify/jest-koa-mocks@5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@shopify/jest-koa-mocks/-/jest-koa-mocks-5.0.1.tgz#fba490b6b7985fbb571eb9974897d396a3642e94" + integrity sha512-4YskS9q8+TEHNoyopmuoy2XyhInyqeOl7CF5ShJs19sm6m0EA/jGGvgf/osv2PeTfuf42/L2G9CzWUSg49yTSg== + dependencies: + koa "^2.13.4" + node-mocks-http "^1.11.0" + +"@sideway/address@^4.1.3": + version "4.1.4" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" + integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.0": + 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" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -519,14 +580,21 @@ dependencies: type-detect "4.0.8" -"@sinonjs/fake-timers@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" - integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== +"@sinonjs/fake-timers@^8.0.1": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz#3fdc2b6cb58935b21bfb8d1625eb1300484316e7" + integrity sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg== dependencies: "@sinonjs/commons" "^1.7.0" -"@techpass/passport-openidconnect@^0.3.0": +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + +"@techpass/passport-openidconnect@0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@techpass/passport-openidconnect/-/passport-openidconnect-0.3.2.tgz#f8fd5d97256286665dbf26dac92431f977ab1e63" integrity sha512-fnCtEiexXSHA029B//hJcCJlLJrT3lhpNCyA0rnz58Qttz0BLGCVv6yMT8HmOnGThH6vcDOVwdgKM3kbCQtEhw== @@ -542,10 +610,17 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": - version "7.1.16" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.16.tgz#bc12c74b7d65e82d29876b5d0baf5c625ac58702" - integrity sha512-EAEHtisTMM+KaKwfWdC3oyllIqswlznXCIVCt7/oRNrh+DhgT4UEBNC/jlADNjvw7UnfbcdkGQcPVZ1xYiLcrQ== +"@types/accepts@*": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575" + integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ== + dependencies: + "@types/node" "*" + +"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": + 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" @@ -554,9 +629,9 @@ "@types/babel__traverse" "*" "@types/babel__generator@*": - version "7.6.3" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.3.tgz#f456b4b2ce79137f768aa130d2423d2f0ccfaba5" - integrity sha512-/GWCmzJWqV7diQW54smJZzWbSFf4QYtF71WCKhcx6Ru/tFyQIY2eiiITcCAeuPbNSvT9YCGkVMqqvSk2Z0mXiA== + version "7.6.4" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" + integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== dependencies: "@babel/types" "^7.0.0" @@ -569,12 +644,73 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.14.2.tgz#ffcd470bbb3f8bf30481678fb5502278ca833a43" - integrity sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA== + 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.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" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/content-disposition@*": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.5.tgz#650820e95de346e1f84e30667d168c8fd25aa6e3" + integrity sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA== + +"@types/cookies@*": + version "0.7.7" + resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.7.tgz#7a92453d1d16389c05a5301eef566f34946cfd81" + integrity sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA== + dependencies: + "@types/connect" "*" + "@types/express" "*" + "@types/keygrip" "*" + "@types/node" "*" + +"@types/debug@*": + version "4.1.7" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" + integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== + dependencies: + "@types/ms" "*" + +"@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.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.18" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/graceful-fs@^4.1.2": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -582,10 +718,20 @@ dependencies: "@types/node" "*" +"@types/http-assert@*": + version "1.5.3" + resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.3.tgz#ef8e3d1a8d46c387f04ab0f2e8ab8cb0c5078661" + integrity sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA== + +"@types/http-errors@*": + 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/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" - integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw== + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== "@types/istanbul-lib-report@*": version "3.0.0" @@ -601,42 +747,315 @@ 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== + dependencies: + jest-matcher-utils "^27.0.0" + pretty-format "^27.0.0" + +"@types/keygrip@*": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" + integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw== + +"@types/koa-compose@*": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d" + integrity sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ== + dependencies: + "@types/koa" "*" + +"@types/koa@*": + version "2.13.4" + resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.4.tgz#10620b3f24a8027ef5cbae88b393d1b31205726b" + integrity sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw== + dependencies: + "@types/accepts" "*" + "@types/content-disposition" "*" + "@types/cookies" "*" + "@types/http-assert" "*" + "@types/http-errors" "*" + "@types/keygrip" "*" + "@types/koa-compose" "*" + "@types/node" "*" + +"@types/koa@2.0.52": + version "2.0.52" + resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.0.52.tgz#7dd11de4189ab339ad66c4ccad153716b14e525f" + integrity sha512-cp/GTOhOYwomlSKqEoG0kaVEVJEzP4ojYmfa7EKaGkmkkRwJ4B/1VBLbQZ49Z+WJNvzXejQB/9GIKqMo9XLgFQ== + dependencies: + "@types/accepts" "*" + "@types/cookies" "*" + "@types/http-assert" "*" + "@types/keygrip" "*" + "@types/koa-compose" "*" + "@types/node" "*" + +"@types/lodash@4.14.180": + version "4.14.180" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670" + integrity sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g== + +"@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" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" + integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + +"@types/node-fetch@2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975" + integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + "@types/node@*": - version "16.11.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.7.tgz#36820945061326978c42a01e56b61cd223dfdc42" - integrity sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw== + version "17.0.41" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.41.tgz#1607b2fd3da014ae5d4d1b31bc792a39348dfb9b" + integrity sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw== -"@types/normalize-package-data@^2.4.0": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" - integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== +"@types/node@14.18.20": + version "14.18.20" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.20.tgz#268f028b36eaf51181c3300252f605488c4f0650" + integrity sha512-Q8KKwm9YqEmUBRsqJ2GWJDtXltBDxTdC4m5vTdXBolu2PeQh8LX+f6BTwU+OuXPu37fLxoN6gidqBmnky36FXA== -"@types/prettier@^2.0.0": - version "2.4.2" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.2.tgz#4c62fae93eb479660c3bd93f9d24d561597a8281" - integrity sha512-ekoj4qOQYp7CvjX8ZDBgN86w3MqQhLE1hczEJbEIjgFEumDy+na/4AJAbLXfgEWFNB2pKadM5rPFtuSGMWK7xA== +"@types/pouchdb-adapter-cordova-sqlite@*": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/pouchdb-adapter-cordova-sqlite/-/pouchdb-adapter-cordova-sqlite-1.0.1.tgz#49e5ee6df7cc0c23196fcb340f43a560e74eb1d6" + integrity sha512-nqlXpW1ho3KBg1mUQvZgH2755y3z/rw4UA7ZJCPMRTHofxGMY8izRVw5rHBL4/7P615or0J2udpRYxgkT3D02g== + dependencies: + "@types/pouchdb-core" "*" + +"@types/pouchdb-adapter-fruitdown@*": + version "6.1.3" + resolved "https://registry.yarnpkg.com/@types/pouchdb-adapter-fruitdown/-/pouchdb-adapter-fruitdown-6.1.3.tgz#9b140ad9645cc56068728acf08ec19ac0046658e" + integrity sha512-Wz1Z1JLOW1hgmFQjqnSkmyyfH7by/iWb4abKn684WMvQfmxx6BxKJpJ4+eulkVPQzzgMMSgU1MpnQOm9FgRkbw== + dependencies: + "@types/pouchdb-core" "*" + +"@types/pouchdb-adapter-http@*": + version "6.1.3" + resolved "https://registry.yarnpkg.com/@types/pouchdb-adapter-http/-/pouchdb-adapter-http-6.1.3.tgz#6e592d5f48deb6274a21ddac1498dd308096bcf3" + integrity sha512-9Z4TLbF/KJWy/D2sWRPBA+RNU0odQimfdvlDX+EY7rGcd3aVoH8qjD/X0Xcd/0dfBH5pKrNIMFFQgW/TylRCmA== + dependencies: + "@types/pouchdb-core" "*" + +"@types/pouchdb-adapter-idb@*": + version "6.1.4" + resolved "https://registry.yarnpkg.com/@types/pouchdb-adapter-idb/-/pouchdb-adapter-idb-6.1.4.tgz#cb9a18864585d600820cd325f007614c5c3989cd" + integrity sha512-KIAXbkF4uYUz0ZwfNEFLtEkK44mEWopAsD76UhucH92XnJloBysav+TjI4FFfYQyTjoW3S1s6V+Z14CUJZ0F6w== + dependencies: + "@types/pouchdb-core" "*" + +"@types/pouchdb-adapter-leveldb@*": + version "6.1.3" + resolved "https://registry.yarnpkg.com/@types/pouchdb-adapter-leveldb/-/pouchdb-adapter-leveldb-6.1.3.tgz#17c7e75d75b992050bca15991e97fba575c61bb3" + integrity sha512-ex8NFqQGFwEpFi7AaZ5YofmuemfZNsL3nTFZBUCAKYMBkazQij1pe2ILLStSvJr0XS0qxgXjCEW19T5Wqiiskg== + dependencies: + "@types/pouchdb-core" "*" + +"@types/pouchdb-adapter-localstorage@*": + version "6.1.3" + resolved "https://registry.yarnpkg.com/@types/pouchdb-adapter-localstorage/-/pouchdb-adapter-localstorage-6.1.3.tgz#0dde02ba6b9d6073a295a20196563942ba9a54bd" + integrity sha512-oor040tye1KKiGLWYtIy7rRT7C2yoyX3Tf6elEJRpjOA7Ja/H8lKc4LaSh9ATbptIcES6MRqZDxtp7ly9hsW3Q== + dependencies: + "@types/pouchdb-core" "*" + +"@types/pouchdb-adapter-memory@*": + version "6.1.3" + resolved "https://registry.yarnpkg.com/@types/pouchdb-adapter-memory/-/pouchdb-adapter-memory-6.1.3.tgz#9eabdbc890fcf58960ee8b68b8685f837e75c844" + integrity sha512-gVbsIMzDzgZYThFVT4eVNsmuZwVm/4jDxP1sjlgc3qtDIxbtBhGgyNfcskwwz9Zu5Lv1avkDsIWvcxQhnvRlHg== + dependencies: + "@types/pouchdb-core" "*" + +"@types/pouchdb-adapter-node-websql@*": + version "6.1.3" + resolved "https://registry.yarnpkg.com/@types/pouchdb-adapter-node-websql/-/pouchdb-adapter-node-websql-6.1.3.tgz#aa18bc68af8cf509acd12c400010dcd5fab2243d" + integrity sha512-F/P+os6Jsa7CgHtH64+Z0HfwIcj0hIRB5z8gNhF7L7dxPWoAfkopK5H2gydrP3sQrlGyN4WInF+UJW/Zu1+FKg== + dependencies: + "@types/pouchdb-adapter-websql" "*" + "@types/pouchdb-core" "*" + +"@types/pouchdb-adapter-websql@*": + version "6.1.4" + resolved "https://registry.yarnpkg.com/@types/pouchdb-adapter-websql/-/pouchdb-adapter-websql-6.1.4.tgz#359fbe42ccac0ac90b492ddb8c32fafd0aa96d79" + integrity sha512-zMJQCtXC40hBsIDRn0GhmpeGMK0f9l/OGWfLguvczROzxxcOD7REI+e6SEmX7gJKw5JuMvlfuHzkQwjmvSJbtg== + dependencies: + "@types/pouchdb-core" "*" + +"@types/pouchdb-browser@*": + version "6.1.3" + resolved "https://registry.yarnpkg.com/@types/pouchdb-browser/-/pouchdb-browser-6.1.3.tgz#8f33d6ef58d6817d1f6d36979148a1c7f63244d8" + integrity sha512-EdYowrWxW9SWBMX/rux2eq7dbHi5Zeyzz+FF/IAsgQKnUxgeCO5VO2j4zTzos0SDyJvAQU+EYRc11r7xGn5tvA== + dependencies: + "@types/pouchdb-adapter-http" "*" + "@types/pouchdb-adapter-idb" "*" + "@types/pouchdb-adapter-websql" "*" + "@types/pouchdb-core" "*" + "@types/pouchdb-mapreduce" "*" + "@types/pouchdb-replication" "*" + +"@types/pouchdb-core@*": + version "7.0.10" + resolved "https://registry.yarnpkg.com/@types/pouchdb-core/-/pouchdb-core-7.0.10.tgz#d1ea1549e7fad6cb579f71459b1bc27252e06a5a" + integrity sha512-mKhjLlWWXyV3PTTjDhzDV1kc2dolO7VYFa75IoKM/hr8Er9eo8RIbS7mJLfC8r/C3p6ihZu9yZs1PWC1LQ0SOA== + dependencies: + "@types/debug" "*" + "@types/pouchdb-find" "*" + +"@types/pouchdb-find@*": + 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" "*" + +"@types/pouchdb-http@*": + version "6.1.3" + resolved "https://registry.yarnpkg.com/@types/pouchdb-http/-/pouchdb-http-6.1.3.tgz#09576c0d409da1f8dee34ec5b768415e2472ea52" + integrity sha512-0e9E5SqNOyPl/3FnEIbENssB4FlJsNYuOy131nxrZk36S+y1R/6qO7ZVRypWpGTqBWSuVd7gCsq2UDwO/285+w== + dependencies: + "@types/pouchdb-adapter-http" "*" + "@types/pouchdb-core" "*" + +"@types/pouchdb-mapreduce@*": + version "6.1.7" + resolved "https://registry.yarnpkg.com/@types/pouchdb-mapreduce/-/pouchdb-mapreduce-6.1.7.tgz#9ab32d1e0f234f1bf6d1e4c5d7e216e9e23ac0a3" + integrity sha512-WzBwm7tmO9QhfRzVaWT4v6JQSS/fG2OoUDrWrhX87rPe2Pn6laPvdK5li6myNRxCoI/l5e8Jd+oYBAFnaiFucA== + dependencies: + "@types/pouchdb-core" "*" + +"@types/pouchdb-node@*": + version "6.1.4" + resolved "https://registry.yarnpkg.com/@types/pouchdb-node/-/pouchdb-node-6.1.4.tgz#5214c0169fcfd2237d373380bbd65a934feb5dfb" + integrity sha512-wnTCH8X1JOPpNOfVhz8HW0AvmdHh6pt40MuRj0jQnK7QEHsHS79WujsKTKSOF8QXtPwpvCNSsI7ut7H7tfxxJQ== + dependencies: + "@types/pouchdb-adapter-http" "*" + "@types/pouchdb-adapter-leveldb" "*" + "@types/pouchdb-core" "*" + "@types/pouchdb-mapreduce" "*" + "@types/pouchdb-replication" "*" + +"@types/pouchdb-replication@*": + version "6.4.4" + resolved "https://registry.yarnpkg.com/@types/pouchdb-replication/-/pouchdb-replication-6.4.4.tgz#743406c90f13a988fa3e346ea74ce40acd170d00" + integrity sha512-BsE5LKpjJK4iAf6Fx5kyrMw+33V+Ip7uWldUnU2BYrrvtR+MLD22dcImm7DZN1st2wPPb91i0XEnQzvP0w1C/Q== + dependencies: + "@types/pouchdb-core" "*" + "@types/pouchdb-find" "*" + +"@types/pouchdb@6.4.0": + version "6.4.0" + resolved "https://registry.yarnpkg.com/@types/pouchdb/-/pouchdb-6.4.0.tgz#f9c41ca64b23029f9bf2eb4bf6956e6431cb79f8" + integrity sha512-eGCpX+NXhd5VLJuJMzwe3L79fa9+IDTrAG3CPaf4s/31PD56hOrhDJTSmRELSXuiqXr6+OHzzP0PldSaWsFt7w== + dependencies: + "@types/pouchdb-adapter-cordova-sqlite" "*" + "@types/pouchdb-adapter-fruitdown" "*" + "@types/pouchdb-adapter-http" "*" + "@types/pouchdb-adapter-idb" "*" + "@types/pouchdb-adapter-leveldb" "*" + "@types/pouchdb-adapter-localstorage" "*" + "@types/pouchdb-adapter-memory" "*" + "@types/pouchdb-adapter-node-websql" "*" + "@types/pouchdb-adapter-websql" "*" + "@types/pouchdb-browser" "*" + "@types/pouchdb-core" "*" + "@types/pouchdb-http" "*" + "@types/pouchdb-mapreduce" "*" + "@types/pouchdb-node" "*" + "@types/pouchdb-replication" "*" + +"@types/prettier@^2.1.5": + 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" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/redis@^2.8.0": + version "2.8.32" + resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.32.tgz#1d3430219afbee10f8cfa389dad2571a05ecfb11" + integrity sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w== + dependencies: + "@types/node" "*" + +"@types/redlock@4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/redlock/-/redlock-4.0.3.tgz#aeab5fe5f0d433a125f6dcf9a884372ac0cddd4b" + integrity sha512-mcvvrquwREbAqyZALNBIlf49AL9Aa324BG+J/Dv4TAP8g+nxQMBI4/APNqqS99QEY7VTNT9XvsaczCVGK8uNnQ== + dependencies: + "@types/bluebird" "*" + "@types/redis" "^2.8.0" + +"@types/semver@7.3.7": + version "7.3.7" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.7.tgz#b9eb89d7dfa70d5d1ce525bc1411a35347f533a3" + integrity sha512-4g1jrL98mdOIwSOUh6LTlB0Cs9I0dQPwINUhBg7C6pN4HLr8GS8xsksJxilW6S6dQHVi2K/o+lQuQcg7LroCnw== + +"@types/serve-static@*": + 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" "^1" + "@types/node" "*" "@types/stack-utils@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/yargs-parser@*": - version "20.2.1" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.1.tgz#3b9ce2489919d9e4fea439b76916abc34b2df129" - integrity sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw== +"@types/tar-fs@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-2.0.1.tgz#6391dcad1b03dea2d79fac07371585ab54472bb1" + integrity sha512-qlsQyIY9sN7p221xHuXKNoMfUenOcvEBN4zI8dGsYbYCqHtTarXOEXSIgUnK+GcR0fZDse6pAIc5pIrCh9NefQ== + dependencies: + "@types/node" "*" + "@types/tar-stream" "*" -"@types/yargs@^15.0.0": - version "15.0.14" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.14.tgz#26d821ddb89e70492160b66d10a0eb6df8f6fb06" - integrity sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ== +"@types/tar-stream@*": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@types/tar-stream/-/tar-stream-2.2.2.tgz#be9d0be9404166e4b114151f93e8442e6ab6fb1d" + integrity sha512-1AX+Yt3icFuU6kxwmPakaiGrJUwG44MpuiqPg4dSolRFk6jmvs4b3IbUol9wKDLIgU76gevn3EwE8y/DkSJCZQ== + dependencies: + "@types/node" "*" + +"@types/uuid@8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + +"@types/yargs-parser@*": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + +"@types/yargs@^16.0.0": + version "16.0.4" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" + integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== dependencies: "@types/yargs-parser" "*" abab@^2.0.3, abab@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" - integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== abbrev@1: version "1.1.1" @@ -650,10 +1069,10 @@ abort-controller@3.0.0: dependencies: event-target-shim "^5.0.0" -abstract-leveldown@^6.2.1, abstract-leveldown@~6.2.1, abstract-leveldown@~6.2.3: - version "6.2.3" - resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz#036543d87e3710f2528e47040bc3261b77a9a8eb" - integrity sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ== +abstract-leveldown@^6.2.1: + version "6.3.0" + resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.3.0.tgz#d25221d1e6612f820c35963ba4bd739928f6026a" + integrity sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ== dependencies: buffer "^5.5.0" immediate "^3.2.3" @@ -668,6 +1087,25 @@ abstract-leveldown@~2.7.1: dependencies: xtend "~4.0.0" +abstract-leveldown@~6.2.1, abstract-leveldown@~6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz#036543d87e3710f2528e47040bc3261b77a9a8eb" + integrity sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ== + dependencies: + buffer "^5.5.0" + immediate "^3.2.3" + level-concat-iterator "~2.0.0" + level-supports "~1.0.0" + xtend "~4.0.0" + +accepts@^1.3.5, accepts@^1.3.7: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + acorn-globals@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" @@ -687,9 +1125,9 @@ acorn@^7.1.1: integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== acorn@^8.2.4: - version "8.5.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2" - integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q== + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== agent-base@6: version "6.0.2" @@ -698,14 +1136,6 @@ agent-base@6: dependencies: debug "4" -ajv@^4.9.1: - version "4.11.8" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" - integrity sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY= - dependencies: - co "^4.6.0" - json-stable-stringify "^1.0.1" - ajv@^6.12.3: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -716,6 +1146,13 @@ ajv@^6.12.3: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -723,7 +1160,7 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.21.3" -ansi-regex@^5.0.0, ansi-regex@^5.0.1: +ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== @@ -742,15 +1179,17 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -anymatch@^3.0.3: +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.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== @@ -781,66 +1220,33 @@ argparse@^1.0.7: argsarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/argsarray/-/argsarray-0.0.1.tgz#6e7207b4ecdb39b0af88303fa5ae22bda8df61cb" - integrity sha1-bnIHtOzbObCviDA/pa4ivajfYcs= - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + integrity sha512-u96dg2GcAKtpTrBdDoFIM7PjcBA+6rSP0OR94MOReNRyUECL6MtQt5XXmRr4qrftYaef9+l5hcpO5te7sML1Cg== asn1@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" - integrity sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y= + 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== + dependencies: + safer-buffer "~2.1.0" assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - -assert-plus@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" - integrity sha1-104bh+ev/A24qttwIfP+SBAasjQ= - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== async@~2.1.4: version "2.1.5" resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc" - integrity sha1-5YfGhYCZSsZ/xW/4bTrFa9voELw= + integrity sha512-+g/Ncjbx0JSq2Mk03WQkyKvNh5q9Qvyo/RIqIqnmC5feJY70PNl2ESwZU2BhAB+AZPkHNzzyC2Dq2AS5VnTKhQ== dependencies: lodash "^4.14.0" asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -aws-sdk@^2.901.0: +aws-sdk@2.1030.0: version "2.1030.0" resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1030.0.tgz#24a856af3d2b8b37c14a8f59974993661c66fd82" integrity sha512-to0STOb8DsSGuSsUb/WCbg/UFnMGfIYavnJH5ZlRCHzvCFjTyR+vfE8ku+qIZvfFM4+5MNTQC/Oxfun2X/TuyA== @@ -855,20 +1261,10 @@ aws-sdk@^2.901.0: uuid "3.3.2" xml2js "0.4.19" -aws-sign2@~0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" - integrity sha1-FDQt0428yU0OW4fXY81jYSwOeU8= - aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.2.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" - integrity sha1-g+9cqGCysy5KDe7e6MdxudtXRx4= + integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== aws4@^1.8.0: version "1.11.0" @@ -876,9 +1272,9 @@ aws4@^1.8.0: integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== axios-retry@^3.1.9: - version "3.2.4" - resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-3.2.4.tgz#f447a53c3456f5bfeca18f20c3a3272207d082ae" - integrity sha512-Co3UXiv4npi6lM963mfnuH90/YFLKWWDmoBYfxkHT5xtkSSWNqK9zdG3fw5/CP/dsoKB5aMMJCsgab+tp1OxLQ== + 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" @@ -890,21 +1286,21 @@ axios@0.24.0: dependencies: follow-redirects "^1.14.4" -babel-jest@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" - integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA== +babel-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444" + integrity sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg== dependencies: - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/babel__core" "^7.1.7" - babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^26.6.2" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^27.5.1" chalk "^4.0.0" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" slash "^3.0.0" -babel-plugin-istanbul@^6.0.0: +babel-plugin-istanbul@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== @@ -915,10 +1311,10 @@ babel-plugin-istanbul@^6.0.0: istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz#8185bd030348d254c6d7dd974355e6a28b21e62d" - integrity sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw== +babel-plugin-jest-hoist@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz#9be98ecf28c331eb9f5df9c72d6f89deb8181c2e" + integrity sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" @@ -943,12 +1339,12 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-jest@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee" - integrity sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ== +babel-preset-jest@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz#91f10f58034cb7989cb4f962b69fa6eef6a6bc81" + integrity sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag== dependencies: - babel-plugin-jest-hoist "^26.6.2" + babel-plugin-jest-hoist "^27.5.1" babel-preset-current-node-syntax "^1.0.0" balanced-match@^1.0.0: @@ -961,37 +1357,19 @@ base64-js@^1.0.2, base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -base64url@2.0.0, base64url@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/base64url/-/base64url-2.0.0.tgz#eac16e03ea1438eff9423d69baa36262ed1f70bb" - integrity sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs= - base64url@3.x.x, base64url@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - bcrypt-pbkdf@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" - integrity sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40= + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== dependencies: tweetnacl "^0.14.3" -bcrypt@^5.0.1: +bcrypt@5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.1.tgz#f1a2c20f208e2ccdceea4433df0c8b2c54ecdf71" integrity sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw== @@ -999,6 +1377,16 @@ bcrypt@^5.0.1: "@mapbox/node-pre-gyp" "^1.0.0" node-addon-api "^3.1.0" +bcryptjs@2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" + integrity sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + bl@^4.0.3: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -1008,12 +1396,24 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -boom@2.x.x: - version "2.10.1" - resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" - integrity sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8= +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +boxen@^5.0.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" + integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== dependencies: - hoek "2.x.x" + ansi-align "^3.0.0" + camelcase "^6.2.0" + chalk "^4.1.0" + cli-boxes "^2.2.1" + string-width "^4.2.2" + type-fest "^0.20.2" + widest-line "^3.1.0" + wrap-ansi "^7.0.0" brace-expansion@^1.1.7: version "1.1.11" @@ -1023,23 +1423,7 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^2.3.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -braces@^3.0.1: +braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -1051,17 +1435,24 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserslist@^4.17.5: - version "4.18.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.18.1.tgz#60d3920f25b6860eb917c6c7b185576f4d8b017f" - integrity sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ== +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.30001280" - electron-to-chromium "^1.3.896" + caniuse-lite "^1.0.30001349" + electron-to-chromium "^1.4.147" escalade "^3.1.1" - node-releases "^2.0.1" + node-releases "^2.0.5" picocolors "^1.0.0" +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -1072,7 +1463,7 @@ bser@2.1.1: buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== buffer-from@1.1.1: version "1.1.1" @@ -1101,52 +1492,51 @@ buffer@^5.5.0, buffer@^5.6.0: base64-js "^1.3.1" ieee754 "^1.1.13" -cache-base@^1.0.1: +cache-content-type@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c" + integrity sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA== dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" + mime-types "^2.1.18" + ylru "^1.2.0" + +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase@^5.0.0, camelcase@^5.3.1: +camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.0.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.1.tgz#250fd350cfd555d0d2160b1d51510eaf8326e86e" - integrity sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA== +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001280: - version "1.0.30001282" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001282.tgz#38c781ee0a90ccfe1fe7fefd00e43f5ffdcb96fd" - integrity sha512-YhF/hG6nqBEllymSIjLtR2iWDDnChvhnVJqp+vloyt2tEHFG1yBR+ac2B/rOw0qOK0m0lEXU2dv4E/sMk5P9Kg== - -capture-exit@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" - integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== - dependencies: - rsvp "^4.8.4" +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== caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== chalk@^2.0.0: version "2.4.2" @@ -1157,7 +1547,7 @@ chalk@^2.0.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0: +chalk@^4.0.0, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1173,7 +1563,22 @@ char-regex@^1.0.2: charenc@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" - integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== + +chokidar@^3.5.2: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" chownr@^1.1.1: version "1.1.4" @@ -1190,34 +1595,41 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== -cjs-module-lexer@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f" - integrity sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw== +ci-info@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.1.tgz#58331f6f472a25fe3a50a351ae3052936c2c7f32" + integrity sha512-SXgeMX9VwDe7iFFaEWkA5AstuER9YKqy4EhHqr4DVqkwmD9rpVimkMKWHdjn30Ja45txyjhSn63lVX69eVCckg== -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" +cjs-module-lexer@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" + integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== +cli-boxes@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== dependencies: string-width "^4.2.0" strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" + wrap-ansi "^7.0.0" clone-buffer@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" - integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= + integrity sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g== + +clone-response@^1.0.2: + 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.0" @@ -1227,21 +1639,13 @@ cluster-key-slot@^1.1.0: co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -1259,7 +1663,7 @@ color-convert@^2.0.1: color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== color-name@~1.1.4: version "1.1.4" @@ -1271,13 +1675,6 @@ color-support@^1.1.2: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== -combined-stream@^1.0.5, combined-stream@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" - integrity sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk= - dependencies: - delayed-stream "~1.0.0" - combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -1285,25 +1682,44 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - component-type@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/component-type/-/component-type-1.2.1.tgz#8a47901700238e4fc32269771230226f24b415a9" - integrity sha1-ikeQFwAjjk/DIml3EjAibyS0Fak= + integrity sha512-Kgy+2+Uwr75vAi6ChWXgHuLvd+QLD7ssgpaRq2zCvt80ptvAfMc/hijcJxXkBa2wMlEZcJvC2H8Ubo+A9ATHIg== concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== + dependencies: + dot-prop "^5.2.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" console-control-strings@^1.0.0, console-control-strings@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + +content-disposition@^0.5.3, content-disposition@~0.5.2: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + 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.8.0" @@ -1312,28 +1728,33 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= +cookies@~0.7.1: + version "0.7.3" + resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.3.tgz#7912ce21fbf2e8c2da70cf1c3f351aecf59dadfa" + integrity sha512-+gixgxYSgQLTaTIilDHAdlNPZDENDQernEMiIcZpYYP14zgHsCt4Ce1FEjFtcp6GefhozebB6orvhAAWx/IS0A== + dependencies: + depd "~1.1.2" + keygrip "~1.0.3" -core-util-is@~1.0.0: +cookies@~0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" + integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow== + dependencies: + depd "~2.0.0" + keygrip "~1.1.0" + +core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== -cross-spawn@^6.0.0: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -cross-spawn@^7.0.0: +cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -1345,14 +1766,12 @@ cross-spawn@^7.0.0: crypt@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" - integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== -cryptiles@2.x.x: - version "2.0.5" - resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" - integrity sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g= - dependencies: - boom "2.x.x" +crypto-random-string@^2.0.0: + version "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== cssom@^0.4.4: version "0.4.4" @@ -1374,7 +1793,7 @@ cssstyle@^2.3.0: dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== dependencies: assert-plus "^1.0.0" @@ -1387,34 +1806,53 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" -debug@^2.2.0, debug@^2.3.3: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== dependencies: ms "2.0.0" -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - decimal.js@^10.2.1: version "10.3.1" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + integrity sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA== + dependencies: + mimic-response "^1.0.0" + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== + +deep-equal@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + integrity sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw== + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== deep-is@~0.1.3: version "0.1.4" @@ -1426,6 +1864,11 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +defer-to-connect@^1.0.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" + integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== + deferred-leveldown@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz#27a997ad95408b61161aa69bd489b86c71b78058" @@ -1434,43 +1877,36 @@ deferred-leveldown@~5.3.0: abstract-leveldown "~6.2.1" inherits "^2.0.3" -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== denque@^1.1.0: version "1.5.1" resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== +depd@^1.1.0, depd@^1.1.2, depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +depd@^2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + detect-libc@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" @@ -1481,10 +1917,10 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== -diff-sequences@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" - integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== +diff-sequences@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" + integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== domexception@^2.0.1: version "2.0.1" @@ -1493,7 +1929,14 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" -dotenv@^16.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" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + +dotenv@16.0.1: version "16.0.1" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d" integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ== @@ -1501,14 +1944,20 @@ dotenv@^16.0.1: double-ended-queue@2.1.0-0: version "2.1.0-0" resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" - integrity sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw= + integrity sha512-+BNfZ+deCo8hMNpDqDnvT+c0XpJ5cUa6mqYq89bho2Ifze4URTqRkcwR399hWoTrTkbZ/XJYDgP6rc7pRgffEQ== + +duplexer3@^0.1.4: + 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.1" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" - integrity sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU= + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== dependencies: jsbn "~0.1.0" + safer-buffer "^2.1.0" ecdsa-sig-formatter@1.0.11: version "1.0.11" @@ -1517,36 +1966,38 @@ ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer "^5.0.1" -ecdsa-sig-formatter@1.0.9: - version "1.0.9" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz#4bc926274ec3b5abb5016e7e1d60921ac262b2a1" - integrity sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE= - dependencies: - base64url "^2.0.0" - safe-buffer "^5.0.1" +ee-first@1.1.1: + version "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.3.896: - version "1.3.900" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.900.tgz#5be2c5818a2a012c511b4b43e87b6ab7a296d4f5" - integrity sha512-SuXbQD8D4EjsaBaJJxySHbC+zq8JrFfxtb4GIr4E9n1BcROyMcRrJCYQNpJ9N+Wjf5mFp7Wp0OHykd14JNEzzQ== +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== -emitter-listener@^1.1.2: +emitter-listener@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/emitter-listener/-/emitter-listener-1.1.2.tgz#56b140e8f6992375b3d7cb2cab1cc7432d9632e8" integrity sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ== dependencies: shimmer "^1.2.0" -emittery@^0.7.1: - version "0.7.2" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" - integrity sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ== +emittery@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" + integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +encodeurl@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + encoding-down@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/encoding-down/-/encoding-down-6.3.0.tgz#b1c4eb0e1728c146ecaef8e32963c549e76d082b" @@ -1567,7 +2018,7 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: end-stream@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/end-stream/-/end-stream-0.1.0.tgz#32003f3f438a2b0143168137f8fa6e9866c81ed5" - integrity sha1-MgA/P0OKKwFDFoE3+PpumGbIHtU= + integrity sha512-Brl10T8kYnc75IepKizW6Y9liyW8ikz1B7n/xoHrJxoVSSjoqPn30sb7XVFfQERK4QfUMYRGs9dhWwtt2eu6uA== dependencies: write-stream "~0.4.3" @@ -1585,15 +2036,30 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +error-inject@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" + integrity sha512-JM8N6PytDbmIYm1IhPWlo8vr3NtfjhDY/1MhD/a5b/aad/USE8a0+NsqE9d5n+GVGmuNkPQWm4bFQWv18d8tMg== + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escape-goat@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" + integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== + +escape-html@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== escape-string-regexp@^2.0.0: version "2.0.0" @@ -1635,121 +2101,59 @@ event-target-shim@^5.0.0: events@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" - integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= + integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw== -exec-sh@^0.3.2: - version "0.3.6" - resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.6.tgz#ff264f9e325519a60cb5e273692943483cca63bc" - integrity sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w== - -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -execa@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" - integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== - dependencies: - cross-spawn "^7.0.0" - get-stream "^5.0.0" - human-signals "^1.1.1" + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" is-stream "^2.0.0" merge-stream "^2.0.0" - npm-run-path "^4.0.0" - onetime "^5.1.0" - signal-exit "^3.0.2" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" strip-final-newline "^2.0.0" exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= +expect@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/expect/-/expect-27.5.1.tgz#83ce59f1e5bdf5f9d2b94b61d2050db48f3fef74" + integrity sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw== dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -expect@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" - integrity sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA== - dependencies: - "@jest/types" "^26.6.2" - ansi-styles "^4.0.0" - jest-get-type "^26.3.0" - jest-matcher-utils "^26.6.2" - jest-message-util "^26.6.2" - jest-regex-util "^26.0.0" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@~3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" - integrity sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ= + "@jest/types" "^27.5.1" + jest-get-type "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== -extsprintf@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" - integrity sha1-4QgOBljjALBilJkMxw4VAiNf1VA= +extsprintf@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== fast-deep-equal@^3.1.1: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -1757,7 +2161,7 @@ fast-json-stable-stringify@^2.0.0: fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fb-watchman@^2.0.0: version "2.0.1" @@ -1767,9 +2171,9 @@ fb-watchman@^2.0.0: bser "2.1.1" fengari-interop@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/fengari-interop/-/fengari-interop-0.1.2.tgz#f7731dcdd2ff4449073fb7ac3c451a8841ce1e87" - integrity sha512-8iTvaByZVoi+lQJhHH9vC+c/Yaok9CwOqNQZN6JrVpjmWwW4dDkeblBXhnHC+BoI6eF4Cy5NKW3z6ICEjvgywQ== + version "0.1.3" + resolved "https://registry.yarnpkg.com/fengari-interop/-/fengari-interop-0.1.3.tgz#3ad37a90e7430b69b365441e9fc0ba168942a146" + integrity sha512-EtZ+oTu3kEwVJnoymFPBVLIbQcCoy9uWCVnMA6h3M/RqHkUBsLYp29+RRHf9rKr6GwjubWREU1O7RretFIXjHw== fengari@^0.1.4: version "0.1.4" @@ -1794,16 +2198,6 @@ fetch-cookie@0.11.0: dependencies: tough-cookie "^2.3.3 || ^3.0.1 || ^4.0.0" -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -1820,19 +2214,14 @@ find-up@^4.0.0, find-up@^4.1.0: path-exists "^4.0.0" follow-redirects@^1.14.4: - version "1.14.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" - integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== - -for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + 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== forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== form-data@^3.0.0: version "3.0.1" @@ -1843,15 +2232,6 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@~2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" - integrity sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE= - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.5" - mime-types "^2.1.12" - form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -1861,12 +2241,10 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" +fresh@^0.5.2, fresh@~0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== fs-constants@^1.0.0: version "1.0.0" @@ -1883,9 +2261,9 @@ fs-minipass@^2.0.0: fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.1.2: +fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -1898,7 +2276,7 @@ function-bind@^1.1.1: functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== gauge@^3.0.0: version "3.0.2" @@ -1920,7 +2298,7 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-caller-file@^2.0.1: +get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -1930,44 +2308,58 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-stream@^4.0.0: +get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== dependencies: pump "^3.0.0" -get-stream@^5.0.0: +get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== dependencies: pump "^3.0.0" -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== dependencies: assert-plus "^1.0.0" +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: - version "7.2.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "^3.1.1" once "^1.3.0" path-is-absolute "^1.0.0" +global-dirs@^3.0.0: + 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" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -1976,7 +2368,7 @@ globals@^11.1.0: google-auth-library@~0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.10.0.tgz#6e15babee85fd1dd14d8d128a295b6838d52136e" - integrity sha1-bhW6vuhf0d0U2NEoopW2g41SE24= + integrity sha512-KM54Y9GhdAzfXUHmWEoYmaOykSLuMG7W4HvVLYqyogxOyE6px8oSS8W13ngqW0oDGZ915GFW3V6OM6+qcdvPOA== dependencies: gtoken "^1.2.1" jws "^3.1.4" @@ -1986,56 +2378,55 @@ google-auth-library@~0.10.0: google-p12-pem@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-0.1.2.tgz#33c46ab021aa734fa0332b3960a9a3ffcb2f3177" - integrity sha1-M8RqsCGqc0+gMys5YKmj/8svMXc= + integrity sha512-puhMlJ2+E/rgvxWaqgN/nC7x623OAE8MR9vBUqxF0inCE7HoVfCHvTeQ9+BR+rj9KM0fIg6XV6tmbt7XHHssoQ== dependencies: node-forge "^0.7.1" googleapis@^16.0.0: version "16.1.0" resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-16.1.0.tgz#0f19f2d70572d918881a0f626e3b1a2fa8629576" - integrity sha1-Dxny1wVy2RiIGg9ibjsaL6hilXY= + integrity sha512-5czmF7xkIlJKc1+/+5tltrI1skoR3HKtkDOld9rk+DOucTpZRjOhCoJzoSjxB3M8rP2tEb1VIr1TPyzR3V2PUQ== dependencies: async "~2.1.4" google-auth-library "~0.10.0" string-template "~1.0.0" -graceful-fs@^4.2.4: - version "4.2.8" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" -growly@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" - integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= +graceful-fs@^4.1.2, graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== gtoken@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-1.2.2.tgz#172776a1a9d96ac09fc22a00f5be83cee6de8820" - integrity sha1-Fyd2oanZasCfwioA9b6DzubeiCA= + version "1.2.3" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-1.2.3.tgz#5509571b8afd4322e124cf66cf68115284c476d8" + integrity sha512-wQAJflfoqSgMWrSBk9Fg86q+sd6s7y6uJhIvvIPz++RElGlMtEqsdAR2oWwZ/WTEtp7P9xFbJRrT976oRgzJ/w== dependencies: google-p12-pem "^0.1.0" jws "^3.0.0" - mime "^1.2.11" + mime "^1.4.1" request "^2.72.0" -har-schema@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" - integrity sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4= - har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" - integrity sha1-M0gdDxu/9gDdID11gSpqX7oALio= - dependencies: - ajv "^4.9.1" - har-schema "^1.0.5" + integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== har-validator@~5.1.3: version "5.1.5" @@ -2048,48 +2439,34 @@ har-validator@~5.1.3: has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-symbols@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" +has-yarn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" + integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== has@^1.0.3: version "1.0.3" @@ -2098,26 +2475,6 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -hawk@~3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" - integrity sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ= - dependencies: - boom "2.x.x" - cryptiles "2.x.x" - hoek "2.x.x" - sntp "1.x.x" - -hoek@2.x.x: - version "2.16.3" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" - integrity sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0= - -hosted-git-info@^2.1.4: - version "2.8.9" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== - html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -2130,6 +2487,30 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +http-assert@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.5.0.tgz#c389ccd87ac16ed2dfa6246fd73b926aa00e6b8f" + integrity sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w== + dependencies: + deep-equal "~1.0.1" + http-errors "~1.8.0" + +http-cache-semantics@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + +http-errors@^1.6.3, http-errors@~1.8.0: + version "1.8.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" + integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.1" + http-proxy-agent@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" @@ -2139,36 +2520,27 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" -http-signature@~1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" - integrity sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8= - dependencies: - assert-plus "^0.2.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== dependencies: assert-plus "^1.0.0" jsprim "^1.2.2" sshpk "^1.7.0" https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" debug "4" -human-signals@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" - integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== iconv-lite@0.4.24: version "0.4.24" @@ -2177,11 +2549,21 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -ieee754@1.1.13, ieee754@^1.1.13, ieee754@^1.1.4: +ieee754@1.1.13: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== +ieee754@^1.1.13, ieee754@^1.1.4: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== + immediate@3.3.0, immediate@^3.2.3: version "3.3.0" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266" @@ -2190,12 +2572,17 @@ immediate@3.3.0, immediate@^3.2.3: immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" - integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + integrity sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A== import-local@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.3.tgz#4d51c2c495ca9393da259ec66b62e022920211e0" - integrity sha512-bE9iaUY3CXH8Cwfan/abDKAxe1KGT9kyGsBPqf6DMK/z0a2OzAsrukeYNgIH6cH5Xr452jb1TUL8rSfCLjZ9uA== + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== dependencies: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" @@ -2203,12 +2590,12 @@ import-local@^3.0.2: imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" wrappy "1" @@ -2218,7 +2605,17 @@ inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, i resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ioredis-mock@^5.5.5: +ini@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + +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== + +ioredis-mock@5.8.0: version "5.8.0" resolved "https://registry.yarnpkg.com/ioredis-mock/-/ioredis-mock-5.8.0.tgz#99d3f0fe58419c2ff15c81cb4e13709aff8f2a44" integrity sha512-PokVyNC/d3qaAyHAdf+Ex4HHBe7GdaSGrcn8pP7ran+pVmSrJ6Aofwm7Od5bQ5JtjDSiylEtIoZzEPYIjUKMHA== @@ -2228,7 +2625,7 @@ ioredis-mock@^5.5.5: lodash "^4.17.21" standard-as-callback "^2.1.0" -ioredis@^4.27.1: +ioredis@4.28.0: version "4.28.0" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.0.tgz#5a2be3f37ff2075e2332f280eaeb02ab4d9ff0d3" integrity sha512-I+zkeeWp3XFgPT2CtJKxvaF5FjGBGt4yGYljRjQecdQKteThuAsKqffeF1lgHVlYnuNeozRbPOCDNZ7tDWPeig== @@ -2245,26 +2642,19 @@ ioredis@^4.27.1: redis-parser "^3.0.0" standard-as-callback "^2.1.0" -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== -is-buffer@^1.1.5, is-buffer@~1.1.6: +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-buffer@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -2276,61 +2666,17 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-core-module@^2.2.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548" - integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw== +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" -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-docker@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-fullwidth-code-point@^3.0.0: version "3.0.0" @@ -2342,24 +2688,47 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= +is-generator-function@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== dependencies: - kind-of "^3.0.2" + has-tostringtag "^1.0.0" + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-installed-globally@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== + dependencies: + global-dirs "^3.0.0" + is-path-inside "^3.0.2" + +is-npm@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8" + integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA== is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== is-potential-custom-element-name@^1.0.1: version "1.0.1" @@ -2371,11 +2740,6 @@ is-retry-allowed@^2.2.0: resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz#88f34cbd236e043e71b6932d09b0c65fb7b4d71d" integrity sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg== -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -2384,71 +2748,42 @@ is-stream@^2.0.0: is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== -is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" +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" + integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: +isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== -istanbul-lib-instrument@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" - integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== - dependencies: - "@babel/core" "^7.7.5" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.0.0" - semver "^6.3.0" - -istanbul-lib-instrument@^5.0.4: - version "5.1.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz#7b49198b657b27a730b8e9cb601f1e1bff24c59a" - integrity sha512-czwUz525rkOFDJxfKK6mYfIs9zBKILyrZQxjz3ABhjQXhbhFsSbo1HW/BFcsDnfJYJWA6thRR5/TUY2qs5W99Q== +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: + 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" @@ -2474,208 +2809,236 @@ istanbul-lib-source-maps@^4.0.0: istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@^3.0.2: - version "3.0.5" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.5.tgz#a2580107e71279ea6d661ddede929ffc6d693384" - integrity sha512-5+19PlhnGabNWB7kOFnuxT8H3T/iIyQzIbQMxXsURmmvKg86P2sbkrGOT77VnHw0Qr0gc2XzRaRfMZYYbSQCJQ== +istanbul-reports@^3.1.3: + 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" -jest-changed-files@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" - integrity sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ== +jest-changed-files@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5" + integrity sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw== dependencies: - "@jest/types" "^26.6.2" - execa "^4.0.0" - throat "^5.0.0" + "@jest/types" "^27.5.1" + execa "^5.0.0" + throat "^6.0.1" -jest-cli@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.3.tgz#43117cfef24bc4cd691a174a8796a532e135e92a" - integrity sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg== +jest-circus@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-27.5.1.tgz#37a5a4459b7bf4406e53d637b49d22c65d125ecc" + integrity sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw== dependencies: - "@jest/core" "^26.6.3" - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/environment" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^0.7.0" + expect "^27.5.1" + is-generator-fn "^2.0.0" + jest-each "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" + slash "^3.0.0" + stack-utils "^2.0.3" + throat "^6.0.1" + +jest-cli@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-27.5.1.tgz#278794a6e6458ea8029547e6c6cbf673bd30b145" + integrity sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw== + dependencies: + "@jest/core" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" chalk "^4.0.0" exit "^0.1.2" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" import-local "^3.0.2" - is-ci "^2.0.0" - jest-config "^26.6.3" - jest-util "^26.6.2" - jest-validate "^26.6.2" + jest-config "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" prompts "^2.0.1" - yargs "^15.4.1" + yargs "^16.2.0" -jest-config@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.3.tgz#64f41444eef9eb03dc51d5c53b75c8c71f645349" - integrity sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg== +jest-config@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-27.5.1.tgz#5c387de33dca3f99ad6357ddeccd91bf3a0e4a41" + integrity sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA== dependencies: - "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^26.6.3" - "@jest/types" "^26.6.2" - babel-jest "^26.6.3" + "@babel/core" "^7.8.0" + "@jest/test-sequencer" "^27.5.1" + "@jest/types" "^27.5.1" + babel-jest "^27.5.1" chalk "^4.0.0" + ci-info "^3.2.0" deepmerge "^4.2.2" glob "^7.1.1" - graceful-fs "^4.2.4" - jest-environment-jsdom "^26.6.2" - jest-environment-node "^26.6.2" - jest-get-type "^26.3.0" - jest-jasmine2 "^26.6.3" - jest-regex-util "^26.0.0" - jest-resolve "^26.6.2" - jest-util "^26.6.2" - jest-validate "^26.6.2" - micromatch "^4.0.2" - pretty-format "^26.6.2" + graceful-fs "^4.2.9" + jest-circus "^27.5.1" + jest-environment-jsdom "^27.5.1" + jest-environment-node "^27.5.1" + jest-get-type "^27.5.1" + jest-jasmine2 "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-runner "^27.5.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^27.5.1" + slash "^3.0.0" + strip-json-comments "^3.1.1" -jest-diff@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" - integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== +jest-diff@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" + integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== dependencies: chalk "^4.0.0" - diff-sequences "^26.6.2" - jest-get-type "^26.3.0" - pretty-format "^26.6.2" + diff-sequences "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" -jest-docblock@^26.0.0: - version "26.0.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" - integrity sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w== +jest-docblock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.5.1.tgz#14092f364a42c6108d42c33c8cf30e058e25f6c0" + integrity sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ== dependencies: detect-newline "^3.0.0" -jest-each@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.6.2.tgz#02526438a77a67401c8a6382dfe5999952c167cb" - integrity sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A== +jest-each@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-27.5.1.tgz#5bc87016f45ed9507fed6e4702a5b468a5b2c44e" + integrity sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ== dependencies: - "@jest/types" "^26.6.2" + "@jest/types" "^27.5.1" chalk "^4.0.0" - jest-get-type "^26.3.0" - jest-util "^26.6.2" - pretty-format "^26.6.2" + jest-get-type "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" -jest-environment-jsdom@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz#78d09fe9cf019a357009b9b7e1f101d23bd1da3e" - integrity sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q== +jest-environment-jsdom@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz#ea9ccd1fc610209655a77898f86b2b559516a546" + integrity sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw== dependencies: - "@jest/environment" "^26.6.2" - "@jest/fake-timers" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" - jest-mock "^26.6.2" - jest-util "^26.6.2" - jsdom "^16.4.0" + jest-mock "^27.5.1" + jest-util "^27.5.1" + jsdom "^16.6.0" -jest-environment-node@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.6.2.tgz#824e4c7fb4944646356f11ac75b229b0035f2b0c" - integrity sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag== +jest-environment-node@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-27.5.1.tgz#dedc2cfe52fab6b8f5714b4808aefa85357a365e" + integrity sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw== dependencies: - "@jest/environment" "^26.6.2" - "@jest/fake-timers" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" - jest-mock "^26.6.2" - jest-util "^26.6.2" + jest-mock "^27.5.1" + jest-util "^27.5.1" -jest-get-type@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" - integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== +jest-get-type@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" + integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== -jest-haste-map@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" - integrity sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w== +jest-haste-map@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f" + integrity sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng== dependencies: - "@jest/types" "^26.6.2" + "@jest/types" "^27.5.1" "@types/graceful-fs" "^4.1.2" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" - graceful-fs "^4.2.4" - jest-regex-util "^26.0.0" - jest-serializer "^26.6.2" - jest-util "^26.6.2" - jest-worker "^26.6.2" - micromatch "^4.0.2" - sane "^4.0.3" + graceful-fs "^4.2.9" + jest-regex-util "^27.5.1" + jest-serializer "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" + micromatch "^4.0.4" walker "^1.0.7" optionalDependencies: - fsevents "^2.1.2" + fsevents "^2.3.2" -jest-jasmine2@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz#adc3cf915deacb5212c93b9f3547cd12958f2edd" - integrity sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg== +jest-jasmine2@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz#a037b0034ef49a9f3d71c4375a796f3b230d1ac4" + integrity sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ== dependencies: - "@babel/traverse" "^7.1.0" - "@jest/environment" "^26.6.2" - "@jest/source-map" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/environment" "^27.5.1" + "@jest/source-map" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" - expect "^26.6.2" + expect "^27.5.1" is-generator-fn "^2.0.0" - jest-each "^26.6.2" - jest-matcher-utils "^26.6.2" - jest-message-util "^26.6.2" - jest-runtime "^26.6.3" - jest-snapshot "^26.6.2" - jest-util "^26.6.2" - pretty-format "^26.6.2" - throat "^5.0.0" + jest-each "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-runtime "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" + pretty-format "^27.5.1" + throat "^6.0.1" -jest-leak-detector@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz#7717cf118b92238f2eba65054c8a0c9c653a91af" - integrity sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg== +jest-leak-detector@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz#6ec9d54c3579dd6e3e66d70e3498adf80fde3fb8" + integrity sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ== dependencies: - jest-get-type "^26.3.0" - pretty-format "^26.6.2" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" -jest-matcher-utils@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a" - integrity sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw== +jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" + integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== dependencies: chalk "^4.0.0" - jest-diff "^26.6.2" - jest-get-type "^26.3.0" - pretty-format "^26.6.2" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" -jest-message-util@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" - integrity sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA== +jest-message-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf" + integrity sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g== dependencies: - "@babel/code-frame" "^7.0.0" - "@jest/types" "^26.6.2" + "@babel/code-frame" "^7.12.13" + "@jest/types" "^27.5.1" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" - graceful-fs "^4.2.4" - micromatch "^4.0.2" - pretty-format "^26.6.2" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^27.5.1" slash "^3.0.0" - stack-utils "^2.0.2" + stack-utils "^2.0.3" -jest-mock@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" - integrity sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew== +jest-mock@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6" + integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og== dependencies: - "@jest/types" "^26.6.2" + "@jest/types" "^27.5.1" "@types/node" "*" jest-pnp-resolver@^1.2.2: @@ -2683,194 +3046,202 @@ jest-pnp-resolver@^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@^26.0.0: - version "26.0.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" - integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== +jest-regex-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95" + integrity sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg== -jest-resolve-dependencies@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz#6680859ee5d22ee5dcd961fe4871f59f4c784fb6" - integrity sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg== +jest-resolve-dependencies@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz#d811ecc8305e731cc86dd79741ee98fed06f1da8" + integrity sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg== dependencies: - "@jest/types" "^26.6.2" - jest-regex-util "^26.0.0" - jest-snapshot "^26.6.2" + "@jest/types" "^27.5.1" + jest-regex-util "^27.5.1" + jest-snapshot "^27.5.1" -jest-resolve@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.6.2.tgz#a3ab1517217f469b504f1b56603c5bb541fbb507" - integrity sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ== +jest-resolve@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-27.5.1.tgz#a2f1c5a0796ec18fe9eb1536ac3814c23617b384" + integrity sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw== dependencies: - "@jest/types" "^26.6.2" + "@jest/types" "^27.5.1" chalk "^4.0.0" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" jest-pnp-resolver "^1.2.2" - jest-util "^26.6.2" - read-pkg-up "^7.0.1" - resolve "^1.18.1" + jest-util "^27.5.1" + jest-validate "^27.5.1" + resolve "^1.20.0" + resolve.exports "^1.1.0" slash "^3.0.0" -jest-runner@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.3.tgz#2d1fed3d46e10f233fd1dbd3bfaa3fe8924be159" - integrity sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ== +jest-runner@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-27.5.1.tgz#071b27c1fa30d90540805c5645a0ec167c7b62e5" + integrity sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ== dependencies: - "@jest/console" "^26.6.2" - "@jest/environment" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/console" "^27.5.1" + "@jest/environment" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" - emittery "^0.7.1" - exit "^0.1.2" - graceful-fs "^4.2.4" - jest-config "^26.6.3" - jest-docblock "^26.0.0" - jest-haste-map "^26.6.2" - jest-leak-detector "^26.6.2" - jest-message-util "^26.6.2" - jest-resolve "^26.6.2" - jest-runtime "^26.6.3" - jest-util "^26.6.2" - jest-worker "^26.6.2" + emittery "^0.8.1" + graceful-fs "^4.2.9" + jest-docblock "^27.5.1" + jest-environment-jsdom "^27.5.1" + jest-environment-node "^27.5.1" + jest-haste-map "^27.5.1" + jest-leak-detector "^27.5.1" + jest-message-util "^27.5.1" + jest-resolve "^27.5.1" + jest-runtime "^27.5.1" + jest-util "^27.5.1" + jest-worker "^27.5.1" source-map-support "^0.5.6" - throat "^5.0.0" + throat "^6.0.1" -jest-runtime@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.3.tgz#4f64efbcfac398331b74b4b3c82d27d401b8fa2b" - integrity sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw== +jest-runtime@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-27.5.1.tgz#4896003d7a334f7e8e4a53ba93fb9bcd3db0a1af" + integrity sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A== dependencies: - "@jest/console" "^26.6.2" - "@jest/environment" "^26.6.2" - "@jest/fake-timers" "^26.6.2" - "@jest/globals" "^26.6.2" - "@jest/source-map" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/yargs" "^15.0.0" + "@jest/environment" "^27.5.1" + "@jest/fake-timers" "^27.5.1" + "@jest/globals" "^27.5.1" + "@jest/source-map" "^27.5.1" + "@jest/test-result" "^27.5.1" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" chalk "^4.0.0" - cjs-module-lexer "^0.6.0" + cjs-module-lexer "^1.0.0" collect-v8-coverage "^1.0.0" - exit "^0.1.2" + execa "^5.0.0" glob "^7.1.3" - graceful-fs "^4.2.4" - jest-config "^26.6.3" - jest-haste-map "^26.6.2" - jest-message-util "^26.6.2" - jest-mock "^26.6.2" - jest-regex-util "^26.0.0" - jest-resolve "^26.6.2" - jest-snapshot "^26.6.2" - jest-util "^26.6.2" - jest-validate "^26.6.2" + graceful-fs "^4.2.9" + jest-haste-map "^27.5.1" + jest-message-util "^27.5.1" + jest-mock "^27.5.1" + jest-regex-util "^27.5.1" + jest-resolve "^27.5.1" + jest-snapshot "^27.5.1" + jest-util "^27.5.1" slash "^3.0.0" strip-bom "^4.0.0" - yargs "^15.4.1" -jest-serializer@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1" - integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== +jest-serializer@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64" + integrity sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w== dependencies: "@types/node" "*" - graceful-fs "^4.2.4" + graceful-fs "^4.2.9" -jest-snapshot@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.2.tgz#f3b0af1acb223316850bd14e1beea9837fb39c84" - integrity sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og== +jest-snapshot@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-27.5.1.tgz#b668d50d23d38054a51b42c4039cab59ae6eb6a1" + integrity sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA== dependencies: + "@babel/core" "^7.7.2" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/traverse" "^7.7.2" "@babel/types" "^7.0.0" - "@jest/types" "^26.6.2" + "@jest/transform" "^27.5.1" + "@jest/types" "^27.5.1" "@types/babel__traverse" "^7.0.4" - "@types/prettier" "^2.0.0" + "@types/prettier" "^2.1.5" + babel-preset-current-node-syntax "^1.0.0" chalk "^4.0.0" - expect "^26.6.2" - graceful-fs "^4.2.4" - jest-diff "^26.6.2" - jest-get-type "^26.3.0" - jest-haste-map "^26.6.2" - jest-matcher-utils "^26.6.2" - jest-message-util "^26.6.2" - jest-resolve "^26.6.2" + expect "^27.5.1" + graceful-fs "^4.2.9" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + jest-haste-map "^27.5.1" + jest-matcher-utils "^27.5.1" + jest-message-util "^27.5.1" + jest-util "^27.5.1" natural-compare "^1.4.0" - pretty-format "^26.6.2" + pretty-format "^27.5.1" semver "^7.3.2" -jest-util@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" - integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== +jest-util@^27.0.0, jest-util@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9" + integrity sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw== dependencies: - "@jest/types" "^26.6.2" + "@jest/types" "^27.5.1" "@types/node" "*" chalk "^4.0.0" - graceful-fs "^4.2.4" - is-ci "^2.0.0" - micromatch "^4.0.2" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" -jest-validate@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec" - integrity sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ== +jest-validate@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-27.5.1.tgz#9197d54dc0bdb52260b8db40b46ae668e04df067" + integrity sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ== dependencies: - "@jest/types" "^26.6.2" - camelcase "^6.0.0" + "@jest/types" "^27.5.1" + camelcase "^6.2.0" chalk "^4.0.0" - jest-get-type "^26.3.0" + jest-get-type "^27.5.1" leven "^3.1.0" - pretty-format "^26.6.2" + pretty-format "^27.5.1" -jest-watcher@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.6.2.tgz#a5b683b8f9d68dbcb1d7dae32172d2cca0592975" - integrity sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ== +jest-watcher@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-27.5.1.tgz#71bd85fb9bde3a2c2ec4dc353437971c43c642a2" + integrity sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw== dependencies: - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" + "@jest/test-result" "^27.5.1" + "@jest/types" "^27.5.1" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - jest-util "^26.6.2" + jest-util "^27.5.1" string-length "^4.0.1" -jest-worker@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" - integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== +jest-worker@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== dependencies: "@types/node" "*" merge-stream "^2.0.0" - supports-color "^7.0.0" + supports-color "^8.0.0" -jest@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef" - integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q== +jest@27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest/-/jest-27.5.1.tgz#dadf33ba70a779be7a6fc33015843b51494f63fc" + integrity sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ== dependencies: - "@jest/core" "^26.6.3" + "@jest/core" "^27.5.1" import-local "^3.0.2" - jest-cli "^26.6.3" + jest-cli "^27.5.1" jmespath@0.15.0: version "0.15.0" resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" - integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= + integrity sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w== -jodid25519@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" - integrity sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc= +joi@17.6.0: + version "17.6.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.6.0.tgz#0bb54f2f006c09a96e75ce687957bd04290054b2" + integrity sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw== dependencies: - jsbn "~0.1.0" + "@hapi/hoek" "^9.0.0" + "@hapi/topo" "^5.0.0" + "@sideway/address" "^4.1.3" + "@sideway/formula" "^3.0.0" + "@sideway/pinpoint" "^2.0.0" join-component@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5" - integrity sha1-uEF7dQZho5K+4sJTfGiyqdSXfNU= + integrity sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ== js-tokens@^4.0.0: version "4.0.0" @@ -2888,9 +3259,9 @@ js-yaml@^3.13.1: jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== -jsdom@^16.4.0: +jsdom@^16.6.0: version "16.7.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== @@ -2928,6 +3299,11 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + integrity sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ== + json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -2938,36 +3314,22 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= - -json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" - integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8= - dependencies: - jsonify "~0.0.0" +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@^2.1.2: - version "2.2.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" - integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== - dependencies: - minimist "^1.2.5" +json5@2.x, json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" - integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= - -jsonwebtoken@^8.2.0, jsonwebtoken@^8.5.1: +jsonwebtoken@8.5.1, jsonwebtoken@^8.2.0: version "8.5.1" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== @@ -2984,24 +3346,14 @@ jsonwebtoken@^8.2.0, jsonwebtoken@^8.5.1: semver "^5.6.0" jsprim@^1.2.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918" - integrity sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg= + version "1.4.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== dependencies: assert-plus "1.0.0" - extsprintf "1.0.2" - json-schema "0.2.3" - verror "1.3.6" - -jwa@^1.1.4: - version "1.1.5" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.5.tgz#a0552ce0220742cd52e153774a32905c30e756e5" - integrity sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU= - dependencies: - base64url "2.0.0" - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.9" - safe-buffer "^5.0.1" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" jwa@^1.4.1: version "1.4.1" @@ -3012,16 +3364,7 @@ jwa@^1.4.1: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" -jws@^3.0.0, jws@^3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" - integrity sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI= - dependencies: - base64url "^2.0.0" - jwa "^1.1.4" - safe-buffer "^5.0.1" - -jws@^3.2.2: +jws@^3.0.0, jws@^3.1.4, jws@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== @@ -3029,42 +3372,136 @@ jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= +keygrip@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc" + integrity sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g== + +keygrip@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" + integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ== dependencies: - is-buffer "^1.1.5" + tsscmp "1.0.6" -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + json-buffer "3.0.0" kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -koa-passport@^4.1.4: +koa-compose@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" + integrity sha512-8gen2cvKHIZ35eDEik5WOo8zbVp9t4cP8p4hW4uE55waxolLRexKKrqfCpwhGVppnB40jWeF8bZeTVg99eZgPw== + dependencies: + any-promise "^1.1.0" + +koa-compose@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" + integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== + +koa-convert@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" + integrity sha512-K9XqjmEDStGX09v3oxR7t5uPRy0jqJdvodHa6wxWTHrTfDq0WUNnYTOOUZN6g8OM8oZQXprQASbiIXG2Ez8ehA== + dependencies: + co "^4.6.0" + koa-compose "^3.0.0" + +koa-convert@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-2.0.0.tgz#86a0c44d81d40551bae22fee6709904573eea4f5" + integrity sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA== + dependencies: + co "^4.6.0" + koa-compose "^4.1.0" + +koa-is-json@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" + integrity sha512-+97CtHAlWDx0ndt0J8y3P12EWLwTLMXIfMnYDev3wOTwH/RpBGMlfn4bDXlMEg1u73K6XRE9BbUp+5ZAYoRYWw== + +koa-passport@4.1.4: version "4.1.4" resolved "https://registry.yarnpkg.com/koa-passport/-/koa-passport-4.1.4.tgz#5f1665c1c2a37ace79af9f970b770885ca30ccfa" integrity sha512-dJBCkl4X+zdYxbI2V2OtoGy0PUenpvp2ZLLWObc8UJhsId0iQpTFT8RVcuA0709AL2txGwRHnSPoT1bYNGa6Kg== dependencies: passport "^0.4.0" +koa@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/koa/-/koa-2.7.0.tgz#7e00843506942b9d82c6cc33749f657c6e5e7adf" + integrity sha512-7ojD05s2Q+hFudF8tDLZ1CpCdVZw8JQELWSkcfG9bdtoTDzMmkRF6BQBU7JzIzCCOY3xd3tftiy/loHBUYaY2Q== + dependencies: + accepts "^1.3.5" + cache-content-type "^1.0.0" + content-disposition "~0.5.2" + content-type "^1.0.4" + cookies "~0.7.1" + debug "~3.1.0" + delegates "^1.0.0" + depd "^1.1.2" + destroy "^1.0.4" + error-inject "^1.0.0" + 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 "^1.2.0" + koa-is-json "^1.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" + +koa@^2.13.4: + version "2.13.4" + resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.4.tgz#ee5b0cb39e0b8069c38d115139c774833d32462e" + integrity sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g== + 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" + integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== + dependencies: + package-json "^6.3.0" + level-codec@9.0.2, level-codec@^9.0.0: version "9.0.2" resolved "https://registry.yarnpkg.com/level-codec/-/level-codec-9.0.2.tgz#fd60df8c64786a80d44e63423096ffead63d8cbc" @@ -3121,7 +3558,7 @@ level-supports@~1.0.0: level-write-stream@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/level-write-stream/-/level-write-stream-1.0.0.tgz#3f7fbb679a55137c0feb303dee766e12ee13c1dc" - integrity sha1-P3+7Z5pVE3wP6zA97nZuEu4Twdw= + integrity sha512-bBNKOEOMl8msO+uIM9YX/gUO6ckokZ/4pCwTm/lwvs46x6Xs8Zy0sn3Vh37eDqse4mhy4fOMIb/JsSM2nyQFtw== dependencies: end-stream "~0.1.0" @@ -3162,7 +3599,7 @@ leven@^3.1.0: levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== dependencies: prelude-ls "~1.1.2" type-check "~0.3.2" @@ -3170,14 +3607,14 @@ levn@~0.3.0: lie@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" - integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4= + integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== dependencies: immediate "~3.0.5" lines-and-columns@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" - integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== locate-path@^5.0.0: version "5.0.0" @@ -3189,73 +3626,83 @@ locate-path@^5.0.0: lodash.defaults@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" - integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== lodash.flatten@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= + integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== -lodash.isarguments@^3.1.0: +lodash.isarguments@3.1.0, lodash.isarguments@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo= + integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== lodash.isboolean@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== lodash.isinteger@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== lodash.isnumber@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== lodash.isstring@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== lodash.noop@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c" - integrity sha1-OBiPTWUKOkdCWEObluxFsyYXEzw= + integrity sha512-TmYdmu/pebrdTIBDK/FDx9Bmfzs9x0sZG6QIJuMDTqEPfeciLcN13ij+cOd0i9vwJfBtbG9UQ+C7MkXgYxrIJg== lodash.once@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== lodash.pick@^4.0.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" - integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= + integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== -lodash@^4.14.0: - version "4.17.4" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" - integrity sha1-eCA6TRwyiuHYbcpkYONptX9AVa4= - -lodash@^4.17.21, lodash@^4.7.0: +lodash@4.17.21, lodash@^4.14.0, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -3266,7 +3713,7 @@ lru-cache@^6.0.0: ltgt@2.2.1, ltgt@^2.1.2, ltgt@~2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5" - integrity sha1-81ypHEk/e3PaDgdJUwTxezH4fuU= + integrity sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA== make-dir@^3.0.0, make-dir@^3.1.0: version "3.1.0" @@ -3275,6 +3722,11 @@ make-dir@^3.0.0, make-dir@^3.1.0: dependencies: semver "^6.0.0" +make-error@1.x: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + makeerror@1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" @@ -3282,18 +3734,6 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - md5@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" @@ -3303,10 +3743,15 @@ md5@^2.3.0: crypt "0.0.2" is-buffer "~1.1.6" +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + memdown@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/memdown/-/memdown-1.4.1.tgz#b4e4e192174664ffbae41361aa500f3119efe215" - integrity sha1-tOThkhdGZP+65BNhqlAPMRnv4hU= + integrity sha512-iVrGHZB8i4OQfM155xx8akvG9FIj+ht14DX5CQkCTG4EHzZ3d3sgckIf/Lm9ivZalEsFuEVnWv2B2WZvbrro2w== dependencies: abstract-leveldown "~2.7.1" functional-red-black-tree "^1.0.1" @@ -3315,80 +3760,64 @@ memdown@1.4.1: ltgt "~2.2.0" safe-buffer "~5.1.1" +merge-descriptors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== +methods@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" + braces "^3.0.2" + picomatch "^2.3.1" -micromatch@^4.0.2: - version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" - integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: - braces "^3.0.1" - picomatch "^2.2.3" + mime-db "1.52.0" -mime-db@1.51.0: - version "1.51.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" - integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== - -mime-db@~1.27.0: - version "1.27.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" - integrity sha1-gg9XIpa70g7CXtVeW13oaeVDbrE= - -mime-types@^2.1.12, mime-types@~2.1.7: - version "2.1.15" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" - integrity sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0= - dependencies: - mime-db "~1.27.0" - -mime-types@~2.1.19: - version "2.1.34" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" - integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== - dependencies: - mime-db "1.51.0" - -mime@^1.2.11: - version "1.3.6" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.6.tgz#591d84d3653a6b0b4a3b9df8de5aa8108e72e5e0" - integrity sha1-WR2E02U6awtKO5343lqoEI5y5eA= +mime@^1.3.4, mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== +mimic-response@^1.0.0, mimic-response@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" -minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: +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== @@ -3408,14 +3837,6 @@ minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - mkdirp-classic@^0.5.2: version "0.5.3" resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" @@ -3429,35 +3850,18 @@ mkdirp@^1.0.3: ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== -ms@2.1.2, ms@^2.1.1: +ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.3: +ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - napi-macros@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b" @@ -3466,22 +3870,22 @@ napi-macros@~2.0.0: natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== ndjson@^1.4.3: version "1.5.0" resolved "https://registry.yarnpkg.com/ndjson/-/ndjson-1.5.0.tgz#ae603b36b134bcec347b452422b0bf98d5832ec8" - integrity sha1-rmA7NrE0vOw0e0UkIrC/mNWDLsg= + integrity sha512-hUPLuaziboGjNF7wHngkgVc0FOclR8dDk/HfEvTtDr/iUrqBWiRcRSTK3/nLOqKH33th714BrMmTPtObI9gZxQ== dependencies: json-stringify-safe "^5.0.1" minimist "^1.2.0" split2 "^2.1.0" through2 "^2.0.3" -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== node-addon-api@^3.1.0: version "3.2.1" @@ -3493,7 +3897,7 @@ node-fetch@2.6.0: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== -node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7: +node-fetch@2.6.7, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== @@ -3501,9 +3905,9 @@ node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7: whatwg-url "^5.0.0" node-forge@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300" - integrity sha1-naYR6giYL0uUIGs760zJZl8gwwA= + version "0.7.6" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" + integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== node-gyp-build@~4.1.0: version "4.1.1" @@ -3513,29 +3917,44 @@ node-gyp-build@~4.1.0: node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" - integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== -node-modules-regexp@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" - integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= - -node-notifier@^8.0.0: - version "8.0.2" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.2.tgz#f3167a38ef0d2c8a866a83e318c1ba0efeb702c5" - integrity sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg== +node-mocks-http@^1.11.0: + 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: - growly "^1.3.0" - is-wsl "^2.2.0" - semver "^7.3.2" - shellwords "^0.1.1" - uuid "^8.3.0" - which "^2.0.2" + accepts "^1.3.7" + content-disposition "^0.5.3" + depd "^1.1.0" + fresh "^0.5.2" + merge-descriptors "^1.0.1" + methods "^1.1.2" + mime "^1.3.4" + parseurl "^1.3.3" + range-parser "^1.2.0" + type-is "^1.6.18" -node-releases@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" - integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== +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== + +nodemon@2.0.16: + version "2.0.16" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.16.tgz#d71b31bfdb226c25de34afea53486c8ef225fdef" + integrity sha512-zsrcaOfTWRuUzBn3P44RDliLlp263Z/76FPoHFr3cFFkOz0lTPAcIw8dCzfdVIx/t3AtDYCZRCDkoCojJqaG3w== + dependencies: + chokidar "^3.5.2" + debug "^3.2.7" + ignore-by-default "^1.0.1" + minimatch "^3.0.4" + pstree.remy "^1.1.8" + semver "^5.7.1" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.5" + update-notifier "^5.1.0" nopt@^5.0.0: version "5.0.0" @@ -3544,36 +3963,24 @@ nopt@^5.0.0: dependencies: abbrev "1" -normalize-package-data@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg== dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" + abbrev "1" -normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" +normalize-url@^4.1.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" + integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== -npm-run-path@^4.0.0: +npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== @@ -3595,11 +4002,6 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== -oauth-sign@~0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" - integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM= - oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" @@ -3608,50 +4010,39 @@ oauth-sign@~0.9.0: oauth@0.9.x, oauth@^0.9.15: version "0.9.15" resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" - integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE= + integrity sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA== object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= +on-finished@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" + ee-first "1.1.1" once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" -onetime@^5.1.0: +onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" +only@~0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" + integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ== + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -3667,17 +4058,12 @@ optionator@^0.8.1: os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== -p-each-series@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a" - integrity sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA== - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== p-limit@^2.2.0: version "2.3.0" @@ -3703,7 +4089,17 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -parse-json@^5.0.0: +package-json@^6.3.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" + integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== + dependencies: + got "^9.6.0" + registry-auth-token "^4.0.0" + registry-url "^5.0.0" + semver "^6.2.0" + +parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -3718,15 +4114,15 @@ parse5@6.0.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= +parseurl@^1.3.2, parseurl@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -passport-google-auth@^1.0.2: +passport-google-auth@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/passport-google-auth/-/passport-google-auth-1.0.2.tgz#8b300b5aa442ef433de1d832ed3112877d0b2938" - integrity sha1-izALWqRC70M94dgy7TESh30LKTg= + integrity sha512-cfAqna6jZLyMEwUdd4PIwAh2mQKQVEDAaRIaom1pG6h4x4Gwjllf/Jflt3TkR1Sen5Rkvr3l7kSXCWE1EKkh8g== dependencies: googleapis "^16.0.0" passport-strategy "1.x" @@ -3734,7 +4130,7 @@ passport-google-auth@^1.0.2: passport-google-oauth1@1.x.x: version "1.0.0" resolved "https://registry.yarnpkg.com/passport-google-oauth1/-/passport-google-oauth1-1.0.0.tgz#af74a803df51ec646f66a44d82282be6f108e0cc" - integrity sha1-r3SoA99R7GRvZqRNgigr5vEI4Mw= + integrity sha512-qpCEhuflJgYrdg5zZIpAq/K3gTqa1CtHjbubsEsidIdpBPLkEVq6tB1I8kBNcH89RdSiYbnKpCBXAZXX/dtx1Q== dependencies: passport-oauth1 "1.x.x" @@ -3745,7 +4141,7 @@ passport-google-oauth20@2.x.x: dependencies: passport-oauth2 "1.x.x" -passport-google-oauth@^2.0.0: +passport-google-oauth@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/passport-google-oauth/-/passport-google-oauth-2.0.0.tgz#f6eb4bc96dd6c16ec0ecfdf4e05ec48ca54d4dae" integrity sha512-JKxZpBx6wBQXX1/a1s7VmdBgwOugohH+IxCy84aPTZNq/iIPX6u7Mqov1zY7MKRz3niFPol0KJz8zPLBoHKtYA== @@ -3753,7 +4149,7 @@ passport-google-oauth@^2.0.0: passport-google-oauth1 "1.x.x" passport-google-oauth20 "2.x.x" -passport-jwt@^4.0.0: +passport-jwt@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.0.tgz#7f0be7ba942e28b9f5d22c2ebbb8ce96ef7cf065" integrity sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg== @@ -3761,10 +4157,10 @@ passport-jwt@^4.0.0: jsonwebtoken "^8.2.0" passport-strategy "^1.0.0" -passport-local@^1.0.0: +passport-local@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee" - integrity sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4= + integrity sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow== dependencies: passport-strategy "1.x.x" @@ -3777,6 +4173,11 @@ passport-oauth1@1.x.x: passport-strategy "1.x.x" utils-merge "1.x.x" +passport-oauth2-refresh@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/passport-oauth2-refresh/-/passport-oauth2-refresh-2.1.0.tgz#c31cd133826383f5539d16ad8ab4f35ca73ce4a4" + integrity sha512-4ML7ooCESCqiTgdDBzNUFTBcPR8zQq9iM6eppEUGMMvLdsjqRL93jKwWm4Az3OJcI+Q2eIVyI8sVRcPFvxcF/A== + passport-oauth2@1.x.x: version "1.6.1" resolved "https://registry.yarnpkg.com/passport-oauth2/-/passport-oauth2-1.6.1.tgz#c5aee8f849ce8bd436c7f81d904a3cd1666f181b" @@ -3791,7 +4192,7 @@ passport-oauth2@1.x.x: passport-strategy@1.x, passport-strategy@1.x.x, passport-strategy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" - integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= + integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== passport@^0.4.0: version "0.4.1" @@ -3809,19 +4210,14 @@ path-exists@^4.0.0: path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-key@^2.0.0, path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6: +path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== @@ -3829,34 +4225,27 @@ path-parse@^1.0.6: pause@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" - integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= - -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU= + integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pirates@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" - integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== - dependencies: - node-modules-regexp "^1.0.0" +pirates@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== pkg-dir@^4.2.0: version "4.2.0" @@ -3865,12 +4254,7 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -posthog-node@^1.3.0: +posthog-node@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/posthog-node/-/posthog-node-1.3.0.tgz#804ed2f213a2f05253f798bf9569d55a9cad94f7" integrity sha512-2+VhqiY/rKIqKIXyvemBFHbeijHE25sP7eKltnqcFqAssUE6+sX6vusN9A4luzToOqHQkUZexiCKxvuGagh7JA== @@ -3887,7 +4271,7 @@ posthog-node@^1.3.0: pouch-stream@^0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/pouch-stream/-/pouch-stream-0.4.1.tgz#0c6d8475c9307677627991a2f079b301c3b89bdd" - integrity sha1-DG2EdckwdndieZGi8HmzAcO4m90= + integrity sha512-RAWFhsGDbG4xZQpvrrQlhrITVUNVCKmglfe5WWDnJaDf1u9DMaRLHv//m65tBZevuo4QTGjwcyggwYxd7AGLsg== dependencies: inherits "^2.0.1" readable-stream "^1.0.27-1" @@ -3926,7 +4310,7 @@ pouchdb-adapter-leveldb-core@7.2.2: sublevel-pouchdb "7.2.2" through2 "3.0.2" -pouchdb-adapter-memory@^7.2.2: +pouchdb-adapter-memory@7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/pouchdb-adapter-memory/-/pouchdb-adapter-memory-7.2.2.tgz#c0ec2e87928d516ca9d1b5badc7269df6f95e5ea" integrity sha512-9o+zdItPEq7rIrxdkUxgsLNaZkDJAGEqqoYgeYdrHidOCZnlhxhX3g7/R/HcpDKC513iEPqJWDJQSfeT6nVKkw== @@ -3980,7 +4364,7 @@ pouchdb-fetch@7.2.2: fetch-cookie "0.10.1" node-fetch "2.6.0" -pouchdb-find@^7.2.2: +pouchdb-find@7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/pouchdb-find/-/pouchdb-find-7.2.2.tgz#1227afdd761812d508fe0794b3e904518a721089" integrity sha512-BmFeFVQ0kHmDehvJxNZl9OmIztCjPlZlVSdpijuFbk/Fi1EFPU1BAv3kLC+6DhZuOqU/BCoaUBY9sn66pPY2ag== @@ -4030,10 +4414,10 @@ pouchdb-promise@^6.0.4: dependencies: lie "3.1.1" -pouchdb-replication-stream@^1.2.9: +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 sha1-qk+l2PUt9IJTkvGOB8fhGs/8ZQo= + integrity sha512-hM8XRBfamTTUwRhKwLS/jSNouBhn9R/4ugdHNRD1EvJzwV8iImh6sDYbCU9PGuznjyOjXz6vpFRzKeI2KYfwnQ== dependencies: argsarray "0.0.1" inherits "^2.0.3" @@ -4094,16 +4478,20 @@ pouchdb@7.3.0: prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== -pretty-format@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" - integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA== + +pretty-format@^27.0.0, pretty-format@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== dependencies: - "@jest/types" "^26.6.2" - ansi-regex "^5.0.0" - ansi-styles "^4.0.0" + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" react-is "^17.0.1" process-nextick-args@~2.0.0: @@ -4122,13 +4510,18 @@ prompts@^2.0.1: prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= + integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== psl@^1.1.28, psl@^1.1.33: 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" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -4140,61 +4533,54 @@ pump@^3.0.0: punycode@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -qs@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" - integrity sha1-E+JtKK1rD/qpExLNO/cI7TUecjM= +pupa@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" + integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== + dependencies: + escape-goat "^2.0.0" qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== querystring@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== + +range-parser@^1.2.0: + version "1.2.1" + 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: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" react-is@^17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -read-pkg-up@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" - integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== - dependencies: - find-up "^4.1.0" - read-pkg "^5.2.0" - type-fest "^0.8.1" - -read-pkg@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" - integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== - dependencies: - "@types/normalize-package-data" "^2.4.0" - normalize-package-data "^2.5.0" - parse-json "^5.0.0" - type-fest "^0.6.0" - readable-stream@1.1.14, readable-stream@^1.0.27-1: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + integrity sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ== dependencies: core-util-is "~1.0.0" inherits "~2.0.1" @@ -4213,7 +4599,7 @@ readable-stream@1.1.14, readable-stream@^1.0.27-1: readable-stream@~0.0.2: version "0.0.4" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-0.0.4.tgz#f32d76e3fb863344a548d79923007173665b3b8d" - integrity sha1-8y124/uGM0SlSNeZIwBxc2ZbO40= + integrity sha512-azrivNydKRYt7zwLV5wWUK7YzKTWs3q87xSmY6DlHapPrCvaT6ZrukvM5erV+yCSSPmZT8zkSdttOHQpWWm9zw== readable-stream@~2.3.6: version "2.3.7" @@ -4228,6 +4614,13 @@ readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + readline-sync@^1.4.9: version "1.4.10" resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b" @@ -4241,77 +4634,47 @@ redis-commands@1.7.0: redis-errors@^1.0.0, redis-errors@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" - integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= + integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== redis-parser@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" - integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= + integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== dependencies: redis-errors "^1.0.0" +redlock@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/redlock/-/redlock-4.2.0.tgz#c26590768559afd5fff76aa1133c94b411ff4f5f" + integrity sha512-j+oQlG+dOwcetUt2WJWttu4CZVeRzUrcVcISFmEmfyuwCVSJ93rDT7YSgg7H7rnxwoRyk/jU46kycVka5tW7jA== + dependencies: + bluebird "^3.7.2" + 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== -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== +registry-auth-token@^4.0.0: + 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: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" + rc "^1.2.8" -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= +registry-url@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" + integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== + dependencies: + rc "^1.2.8" remove-trailing-slash@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz#be2285a59f39c74d1bce4f825950061915e3780d" integrity sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA== -repeat-element@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" - integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== - -repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -request@^2.72.0, request@^2.74.0: - version "2.81.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" - integrity sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA= - dependencies: - aws-sign2 "~0.6.0" - aws4 "^1.2.1" - caseless "~0.12.0" - combined-stream "~1.0.5" - extend "~3.0.0" - forever-agent "~0.6.1" - form-data "~2.1.1" - har-validator "~4.2.1" - hawk "~3.1.3" - http-signature "~1.1.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.7" - oauth-sign "~0.8.1" - performance-now "^0.2.0" - qs "~6.4.0" - safe-buffer "^5.0.1" - stringstream "~0.0.4" - tough-cookie "~2.3.0" - tunnel-agent "^0.6.0" - uuid "^3.0.0" - -request@^2.88.0: +request@^2.72.0, request@^2.74.0, request@^2.88.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -4340,12 +4703,7 @@ request@^2.88.0: require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== resolve-cwd@^3.0.0: version "3.0.0" @@ -4359,23 +4717,26 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= +resolve.exports@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9" + integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== -resolve@^1.10.0, resolve@^1.18.1: - version "1.20.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" - integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== +resolve@^1.20.0: + 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.2.0" - path-parse "^1.0.6" + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +responselike@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + integrity sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ== + dependencies: + lowercase-keys "^1.0.0" rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" @@ -4384,12 +4745,7 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -rsvp@^4.8.4: - version "4.8.5" - resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" - integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== - -safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -4399,44 +4755,22 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -"safer-buffer@>= 2.1.2 < 3": +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sane@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" - integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== - dependencies: - "@cnakazawa/watch" "^1.0.3" - anymatch "^2.0.0" - capture-exit "^2.0.0" - exec-sh "^0.3.2" - execa "^1.0.0" - fb-watchman "^2.0.0" - micromatch "^3.1.4" - minimist "^1.1.1" - walker "~1.0.5" - -sanitize-s3-objectkey@^0.0.1: +sanitize-s3-objectkey@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/sanitize-s3-objectkey/-/sanitize-s3-objectkey-0.0.1.tgz#efa9887cd45275b40234fb4bb12fc5754fe64e7e" integrity sha512-ZTk7aqLxy4sD40GWcYWoLfbe05XLmkKvh6vGKe13ADlei24xlezcvjgKy1qRArlaIbIMYaqK7PCalvZtulZlaQ== -sax@1.2.1, sax@>=0.6.0: +sax@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" - integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= + integrity sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA== -sax@>=0.1.1: +sax@>=0.1.1, sax@>=0.6.0: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -4448,51 +4782,39 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.3.2: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== +semver-diff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" + integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== dependencies: - lru-cache "^6.0.0" + semver "^6.3.0" -semver@^7.3.5: +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@^5.6.0, semver@^5.7.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -shebang-command@^1.2.0: +setprototypeof@1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== shebang-command@^2.0.0: version "2.0.0" @@ -4501,30 +4823,20 @@ shebang-command@^2.0.0: dependencies: shebang-regex "^3.0.0" -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - shebang-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shellwords@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" - integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== - shimmer@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== -signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.5" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f" - integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ== +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== sisteransi@^1.0.5: version "1.0.5" @@ -4536,81 +4848,23 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -sntp@1.x.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" - integrity sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg= - dependencies: - hoek "2.x.x" - -source-map-resolve@^0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - source-map-support@^0.5.6: - version "0.5.20" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" - integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw== + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-url@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" - integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== - -source-map@^0.5.0, source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== source-map@^0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== spark-md5@3.0.1: version "3.0.1" @@ -4622,39 +4876,6 @@ spark-md5@3.0.2: resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc" integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw== -spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.11" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95" - integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g== - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - split2@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493" @@ -4670,25 +4891,24 @@ sprintf-js@^1.1.1: sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== sshpk@^1.7.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.0.tgz#ff2a3e4fd04497555fed97b39a0fd82fafb3a33c" - integrity sha1-/yo+T9BEl1Vf7Zezmg/YL6+zozw= + version "1.17.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" + integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" - dashdash "^1.12.0" - getpass "^0.1.1" - optionalDependencies: bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" ecc-jsbn "~0.1.1" - jodid25519 "^1.0.0" + getpass "^0.1.1" jsbn "~0.1.0" + safer-buffer "^2.0.2" tweetnacl "~0.14.0" -stack-utils@^2.0.2: +stack-utils@^2.0.3: 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== @@ -4700,18 +4920,15 @@ standard-as-callback@^2.1.0: resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" +"statuses@>= 1.5.0 < 2", statuses@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== step@0.0.x: version "0.0.6" resolved "https://registry.yarnpkg.com/step/-/step-0.0.6.tgz#143e7849a5d7d3f4a088fe29af94915216eeede2" - integrity sha1-FD54SaXX0/SgiP4pr5SRUhbu7eI= + integrity sha512-qSSeQinUJk2w38vUFobjFoE307GqsozMC8VisOCkJLpklvKPT0ptPHwWOrENoag8rgLudvTkfP3bancwP93/Jw== string-length@^4.0.1: version "4.0.2" @@ -4724,9 +4941,9 @@ string-length@^4.0.1: string-template@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96" - integrity sha1-np8iM9wA8hhxjsN5oopWc+zKi5Y= + integrity sha512-SLqR3GBUXuoPP5MmYtD7ompvXiG87QjT6lzOszyXjTM86Uu7At7vNnt2xgyTLq5o9T4IxTYFyGxcULqpsmsfdg== -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4745,7 +4962,7 @@ string_decoder@^1.1.1: string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== string_decoder@~1.1.1: version "1.1.1" @@ -4754,11 +4971,6 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -stringstream@~0.0.4: - version "0.0.6" - resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.6.tgz#7880225b0d4ad10e30927d167a1d6f2fd3b33a72" - integrity sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA== - strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -4771,16 +4983,21 @@ strip-bom@^4.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + sublevel-pouchdb@7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/sublevel-pouchdb/-/sublevel-pouchdb-7.2.2.tgz#49e46cd37883bf7ff5006d7c5b9bcc7bcc1f422f" @@ -4791,7 +5008,7 @@ sublevel-pouchdb@7.2.2: ltgt "2.2.1" readable-stream "1.1.14" -supports-color@^5.3.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -4805,6 +5022,13 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-hyperlinks@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz#4f77b42488765891774b70c79babd87f9bd594bb" @@ -4813,12 +5037,17 @@ supports-hyperlinks@^2.0.0: has-flag "^4.0.0" supports-color "^7.0.0" +supports-preserve-symlinks-flag@^1.0.0: + version "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== + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -tar-fs@^2.1.1: +tar-fs@2.1.1: 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== @@ -4868,10 +5097,10 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -throat@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" - integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== +throat@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" + integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== through2@3.0.2: version "3.0.2" @@ -4889,6 +5118,11 @@ through2@^2.0.0, through2@^2.0.2, through2@^2.0.3: readable-stream "~2.3.6" xtend "~4.0.1" +timekeeper@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/timekeeper/-/timekeeper-2.2.0.tgz#9645731fce9e3280a18614a57a9d1b72af3ca368" + integrity sha512-W3AmPTJWZkRwu+iSNxPIsLZ2ByADsOLbbLxe46UJyWj3mlYLlwucKiq+/dPm0l9wTzqoF3/2PH0AGFCebjq23A== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -4906,20 +5140,10 @@ to-fast-properties@^2.0.0: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== to-regex-range@^5.0.1: version "5.0.1" @@ -4928,25 +5152,19 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== -"tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0", tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== dependencies: - psl "^1.1.28" - punycode "^2.1.1" + nopt "~1.0.10" -tough-cookie@^4.0.0: +"tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0", tough-cookie@^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== @@ -4955,12 +5173,13 @@ tough-cookie@^4.0.0: punycode "^2.1.1" universalify "^0.1.2" -tough-cookie@~2.3.0: - version "2.3.2" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" - integrity sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo= +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== dependencies: - punycode "^1.4.1" + psl "^1.1.28" + punycode "^2.1.1" tr46@^2.1.0: version "2.1.0" @@ -4974,6 +5193,25 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +ts-jest@27.1.5: + version "27.1.5" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.1.5.tgz#0ddf1b163fbaae3d5b7504a1e65c914a95cff297" + integrity sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA== + dependencies: + bs-logger "0.x" + fast-json-stable-stringify "2.x" + jest-util "^27.0.0" + json5 "2.x" + lodash.memoize "4.x" + make-error "1.x" + semver "7.x" + yargs-parser "20.x" + +tsscmp@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" + integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -4998,20 +5236,23 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + type-fest@^0.21.3: version "0.21.3" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== -type-fest@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" - integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== - -type-fest@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" - integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-is@^1.6.16, type-is@^1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" typedarray-to-buffer@^3.1.5: version "3.1.5" @@ -5020,33 +5261,52 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typescript@4.7.3: + version "4.7.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d" + integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA== + uid2@0.0.x: version "0.0.4" resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.4.tgz#033f3b1d5d32505f5ce5f888b9f3b667123c0a44" integrity sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA== -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" + 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== -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= +update-notifier@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9" + integrity sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw== dependencies: - has-value "^0.3.1" - isobject "^3.0.0" + boxen "^5.0.0" + chalk "^4.1.0" + configstore "^5.0.1" + has-yarn "^2.1.0" + import-lazy "^2.1.0" + is-ci "^2.0.0" + is-installed-globally "^0.4.0" + is-npm "^5.0.0" + is-yarn-global "^0.3.0" + latest-version "^5.1.0" + pupa "^2.1.1" + semver "^7.3.4" + semver-diff "^3.1.1" + xdg-basedir "^4.0.0" uri-js@^4.2.2: version "4.4.1" @@ -5055,10 +5315,12 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +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 sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= + dependencies: + prepend-http "^2.0.0" url@0.10.3: version "0.10.3" @@ -5068,11 +5330,6 @@ url@0.10.3: punycode "1.3.2" querystring "0.2.0" -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - 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" @@ -5093,44 +5350,38 @@ uuid@8.1.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d" integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg== -uuid@8.3.2, uuid@^8.3.0, uuid@^8.3.2: +uuid@8.3.2, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1" - integrity sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE= - uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -v8-to-istanbul@^7.0.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz#30898d1a7fa0c84d225a2c1434fb958f290883c1" - integrity sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow== +v8-to-istanbul@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed" + integrity sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w== dependencies: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" source-map "^0.7.3" -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" +vary@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= -verror@1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" - integrity sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw= +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= dependencies: - extsprintf "1.0.2" + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" vuvuzela@1.0.3: version "1.0.3" @@ -5151,7 +5402,7 @@ w3c-xmlserializer@^2.0.0: dependencies: xml-name-validator "^3.0.0" -walker@^1.0.7, walker@~1.0.5: +walker@^1.0.7: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== @@ -5210,19 +5461,7 @@ whatwg-url@^8.0.0, whatwg-url@^8.5.0: tr46 "^2.1.0" webidl-conversions "^6.1.0" -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@^1.2.9: - 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, which@^2.0.2: +which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== @@ -5236,15 +5475,22 @@ wide-align@^1.1.2: dependencies: string-width "^1.0.2 || 2 || 3 || 4" +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" string-width "^4.1.0" @@ -5273,9 +5519,14 @@ write-stream@~0.4.3: readable-stream "~0.0.2" ws@^7.4.6: - version "7.5.5" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" - integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== + version "7.5.8" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.8.tgz#ac2729881ab9e7cbaf8787fe3469a48c5c7f636a" + integrity sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw== + +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== xml-name-validator@^3.0.0: version "3.0.0" @@ -5312,42 +5563,40 @@ xtend@^4.0.2, xtend@~4.0.0, xtend@~4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -y18n@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" - integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== 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== -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" +yargs-parser@20.x, yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs@^15.4.1: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" + y18n "^5.0.5" + yargs-parser "^20.2.2" -zlib@^1.0.5: +ylru@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.3.2.tgz#0de48017473275a4cbdfc83a1eaf67c01af8a785" + integrity sha512-RXRJzMiK6U2ye0BlGGZnmpwJDPgakn6aNQ0A7gHRbD4I0uvK4TW6UqkK1V0pp9jskjJBAXd3dRrbzWkqJ+6cxA== + +zlib@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/zlib/-/zlib-1.0.5.tgz#6e7c972fc371c645a6afb03ab14769def114fcc0" integrity sha1-bnyXL8NxxkWmr7A6sUdp3vEU/MA= diff --git a/packages/bbui/package.json b/packages/bbui/package.json index f2caac1cbc..b081f17943 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": "1.0.185-alpha.0", + "version": "1.4.18-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": "^1.0.185-alpha.0", + "@budibase/string-templates": "1.4.18-alpha.1", "@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", @@ -66,11 +66,12 @@ "@spectrum-css/radio": "^3.0.2", "@spectrum-css/search": "^3.0.2", "@spectrum-css/sidenav": "^3.0.2", + "@spectrum-css/slider": "3.0.1", "@spectrum-css/statuslight": "^3.0.2", "@spectrum-css/stepper": "^3.0.3", "@spectrum-css/switch": "^1.0.2", "@spectrum-css/table": "^3.0.1", - "@spectrum-css/tabs": "^3.0.1", + "@spectrum-css/tabs": "^3.2.12", "@spectrum-css/tags": "^3.0.2", "@spectrum-css/textfield": "^3.0.1", "@spectrum-css/toast": "^3.0.1", diff --git a/packages/bbui/src/ActionButton/ActionButton.svelte b/packages/bbui/src/ActionButton/ActionButton.svelte index b518ac3d92..cfc810807e 100644 --- a/packages/bbui/src/ActionButton/ActionButton.svelte +++ b/packages/bbui/src/ActionButton/ActionButton.svelte @@ -13,6 +13,7 @@ export let size = "M" export let active = false export let fullWidth = false + export let noPadding = false function longPress(element) { if (!longPressable) return @@ -41,6 +42,7 @@ class:spectrum-ActionButton--quiet={quiet} class:spectrum-ActionButton--emphasized={emphasized} class:is-selected={selected} + class:noPadding class:fullWidth class="spectrum-ActionButton spectrum-ActionButton--size{size}" class:active @@ -80,4 +82,21 @@ .active svg { color: var(--spectrum-global-color-blue-600); } + :global([dir="ltr"] .spectrum-ActionButton .spectrum-Icon) { + margin-left: 0; + transition: color ease-out 130ms; + } + .is-selected:not(.spectrum-ActionButton--emphasized) { + background: var(--spectrum-global-color-gray-300); + } + .noPadding { + padding: 0; + min-width: 0; + } + .spectrum-ActionButton--quiet { + padding: 0 8px; + } + .is-selected:not(.emphasized) .spectrum-Icon { + color: var(--spectrum-global-color-gray-900); + } diff --git a/packages/bbui/src/Actions/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.js index a25cc1bbd5..7570a39c8c 100644 --- a/packages/bbui/src/Actions/position_dropdown.js +++ b/packages/bbui/src/Actions/position_dropdown.js @@ -1,4 +1,4 @@ -export default function positionDropdown(element, { anchor, align }) { +export default function positionDropdown(element, { anchor, align, maxWidth }) { let positionSide = "top" let maxHeight = 0 let dimensions = getDimensions(anchor) @@ -34,13 +34,24 @@ export default function positionDropdown(element, { anchor, align }) { } function calcLeftPosition() { - return align === "right" - ? dimensions.left + dimensions.width - dimensions.containerWidth - : dimensions.left + let left + + if (align == "right") { + left = dimensions.left + dimensions.width - dimensions.containerWidth + } else if (align == "right-side") { + left = dimensions.left + dimensions.width + } else { + left = dimensions.left + } + + return left } element.style.position = "absolute" element.style.zIndex = "9999" + if (maxWidth) { + element.style.maxWidth = `${maxWidth}px` + } element.style.minWidth = `${dimensions.width}px` element.style.maxHeight = `${maxHeight.toFixed(0)}px` element.style.transformOrigin = `center ${positionSide}` @@ -54,10 +65,8 @@ export default function positionDropdown(element, { anchor, align }) { element.style.left = `${calcLeftPosition(dimensions).toFixed(0)}px` }) }) - resizeObserver.observe(anchor) resizeObserver.observe(element) - return { destroy() { resizeObserver.disconnect() diff --git a/packages/bbui/src/Avatar/Avatar.svelte b/packages/bbui/src/Avatar/Avatar.svelte index f8acd9024c..136a4fe24b 100644 --- a/packages/bbui/src/Avatar/Avatar.svelte +++ b/packages/bbui/src/Avatar/Avatar.svelte @@ -4,7 +4,7 @@ ["XXS", "--spectrum-alias-avatar-size-50"], ["XS", "--spectrum-alias-avatar-size-75"], ["S", "--spectrum-alias-avatar-size-200"], - ["M", "--spectrum-alias-avatar-size-300"], + ["M", "--spectrum-alias-avatar-size-400"], ["L", "--spectrum-alias-avatar-size-500"], ["XL", "--spectrum-alias-avatar-size-600"], ["XXL", "--spectrum-alias-avatar-size-700"], @@ -13,6 +13,19 @@ export let url = "" export let disabled = false export let initials = "JD" + + const DefaultColor = "#3aab87" + + $: color = getColor(initials) + + const getColor = initials => { + if (!initials?.length) { + return DefaultColor + } + const code = initials[0].toLowerCase().charCodeAt(0) + const hue = ((code % 26) / 26) * 360 + return `hsl(${hue}, 50%, 50%)` + } {#if url} @@ -25,10 +38,11 @@ /> {:else}
{initials || ""}
@@ -40,7 +54,6 @@ display: grid; place-items: center; font-weight: 600; - background: #3aab87; border-radius: 50%; overflow: hidden; user-select: none; diff --git a/packages/bbui/src/Banner/Banner.svelte b/packages/bbui/src/Banner/Banner.svelte index f41fb5f803..3810021a61 100644 --- a/packages/bbui/src/Banner/Banner.svelte +++ b/packages/bbui/src/Banner/Banner.svelte @@ -8,6 +8,7 @@ export let size = "S" export let extraButtonText export let extraButtonAction + export let showCloseButton = true let show = true @@ -39,22 +40,24 @@ {/if} -
- -
+ {#if showCloseButton} +
+ +
+ {/if} {/if} @@ -63,4 +66,7 @@ pointer-events: all; width: 100%; } + .spectrum-Button { + border: 1px solid rgba(255, 255, 255, 0.2); + } diff --git a/packages/bbui/src/Banner/BannerDisplay.svelte b/packages/bbui/src/Banner/BannerDisplay.svelte index aad742b1bd..9ea2eaf2ec 100644 --- a/packages/bbui/src/Banner/BannerDisplay.svelte +++ b/packages/bbui/src/Banner/BannerDisplay.svelte @@ -4,22 +4,32 @@ import { banner } from "../Stores/banner" import Banner from "./Banner.svelte" import { fly } from "svelte/transition" + import TooltipWrapper from "../Tooltip/TooltipWrapper.svelte" diff --git a/packages/bbui/src/Button/Button.svelte b/packages/bbui/src/Button/Button.svelte index e8f6b4500e..36abcbf4da 100644 --- a/packages/bbui/src/Button/Button.svelte +++ b/packages/bbui/src/Button/Button.svelte @@ -14,6 +14,7 @@ export let active = false export let tooltip = undefined export let dataCy + export let newStyles = false let showTooltip = false @@ -25,6 +26,7 @@ class:spectrum-Button--warning={warning} class:spectrum-Button--overBackground={overBackground} class:spectrum-Button--quiet={quiet} + class:new-styles={newStyles} class:active class="spectrum-Button spectrum-Button--size{size.toUpperCase()}" {disabled} @@ -93,4 +95,20 @@ padding-left: var(--spacing-m); line-height: 0; } + .spectrum-Button--primary.new-styles { + background: var(--spectrum-global-color-gray-800); + border-color: transparent; + color: var(--spectrum-global-color-gray-50); + } + .spectrum-Button--primary.new-styles:hover { + background: var(--spectrum-global-color-gray-900); + } + .spectrum-Button--secondary.new-styles { + background: var(--spectrum-global-color-gray-200); + border-color: transparent; + color: var(--spectrum-global-color-gray-900); + } + .spectrum-Button--secondary.new-styles:hover { + background: var(--spectrum-global-color-gray-300); + } diff --git a/packages/bbui/src/Divider/Divider.svelte b/packages/bbui/src/Divider/Divider.svelte index 2b4de9cfb0..e4f0f2fb61 100644 --- a/packages/bbui/src/Divider/Divider.svelte +++ b/packages/bbui/src/Divider/Divider.svelte @@ -16,6 +16,9 @@ /> diff --git a/packages/bbui/src/Form/Core/Multiselect.svelte b/packages/bbui/src/Form/Core/Multiselect.svelte index 3eb1add267..eb39e39042 100644 --- a/packages/bbui/src/Form/Core/Multiselect.svelte +++ b/packages/bbui/src/Form/Core/Multiselect.svelte @@ -13,6 +13,7 @@ export let readonly = false export let autocomplete = false export let sort = false + export let autoWidth = false const dispatch = createEventDispatcher() $: selectedLookupMap = getSelectedLookupMap(value) @@ -22,7 +23,7 @@ $: toggleOption = makeToggleOption(selectedLookupMap, value) const getFieldText = (value, map, placeholder) => { - if (value?.length) { + if (Array.isArray(value) && value.length > 0) { if (!map) { return "" } @@ -35,7 +36,7 @@ const getSelectedLookupMap = value => { let map = {} - if (value?.length) { + if (Array.isArray(value) && value.length > 0) { value.forEach(option => { if (option) { map[option] = true @@ -85,4 +86,5 @@ {getOptionValue} onSelectOption={toggleOption} {sort} + {autoWidth} /> diff --git a/packages/bbui/src/Form/Core/Picker.svelte b/packages/bbui/src/Form/Core/Picker.svelte index 2585f11939..cdaf00aded 100644 --- a/packages/bbui/src/Form/Core/Picker.svelte +++ b/packages/bbui/src/Form/Core/Picker.svelte @@ -6,12 +6,15 @@ import { createEventDispatcher } from "svelte" import clickOutside from "../../Actions/click_outside" import Search from "./Search.svelte" + import Icon from "../../Icon/Icon.svelte" + import StatusLight from "../../StatusLight/StatusLight.svelte" export let id = null export let disabled = false export let error = null export let fieldText = "" export let fieldIcon = "" + export let fieldColour = "" export let isPlaceholder = false export let placeholderOption = null export let options = [] @@ -20,6 +23,7 @@ export let getOptionLabel = option => option export let getOptionValue = option => option export let getOptionIcon = () => null + export let getOptionColour = () => null export let open = false export let readonly = false export let quiet = false @@ -83,11 +87,15 @@ on:mousedown={onClick} > {#if fieldIcon} - - icon + + + + {/if} + {#if fieldColour} + + {/if} - onSelectOption(getOptionValue(option, idx))} > {#if getOptionIcon(option, idx)} - - icon + + + + {/if} + {#if getOptionColour(option, idx)} + + {/if} @@ -199,6 +207,10 @@ } .spectrum-Picker { width: 100%; + box-shadow: none; + } + .spectrum-Picker-label.auto-width { + margin-right: var(--spacing-xs); } .spectrum-Picker-label:not(.auto-width) { overflow: hidden; @@ -212,14 +224,19 @@ .spectrum-Picker-label.auto-width.is-placeholder { padding-right: 2px; } + .auto-width .spectrum-Menu-item { + padding-right: var(--spacing-xl); + } - .icon-Padding { - padding-right: 10px; + /* Icon and colour alignment */ + .spectrum-Menu-checkmark { + align-self: center; + margin-top: 0; } - .icon-Placeholder-Padding { - padding-top: 5px; - padding-right: 10px; + .option-extra { + padding-right: 8px; } + .spectrum-Popover :global(.spectrum-Search) { margin-top: -1px; margin-left: -1px; diff --git a/packages/bbui/src/Form/Core/PickerDropdown.svelte b/packages/bbui/src/Form/Core/PickerDropdown.svelte new file mode 100644 index 0000000000..1607876b46 --- /dev/null +++ b/packages/bbui/src/Form/Core/PickerDropdown.svelte @@ -0,0 +1,415 @@ + + +
+
+ {#if iconData} + + + + {/if} + (primaryOpen = true)} + on:blur + on:focus + on:input + on:keyup + on:blur={onBlur} + on:input={onInput} + on:keyup={updateValueOnEnter} + value={primaryLabel || ""} + placeholder={placeholder || ""} + {disabled} + readonly + class="spectrum-Textfield-input spectrum-InputGroup-input" + class:labelPadding={iconData} + class:open={primaryOpen} + /> + {#if primaryValue && showClearIcon} + + {/if} +
+ {#if primaryOpen} +
(primaryOpen = false)} + transition:fly|local={{ y: -20, duration: 200 }} + class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" + class:auto-width={autoWidth} + class:is-full-width={!secondaryOptions.length} + > + {#if autocomplete} + updateSearch(event)} + {disabled} + placeholder="Search" + /> + {/if} + +
    + {#if placeholderOption} +
  • onSelectOption(null)} + > + {placeholderOption} + +
  • + {/if} + {#each groupTitles as title} +
    + {title} +
    + {#if primaryOptions} + {#each primaryOptions[title].data as option, idx} +
  • + onPickPrimary({ + value: primaryOptions[title].getValue(option), + label: primaryOptions[title].getLabel(option), + })} + > + {#if primaryOptions[title].getIcon(option)} + + {:else if getPrimaryOptionColour(option, idx)} + + + + {/if} + +
    + {primaryOptions[title].getLabel(option)} + +
    + + {#if getPrimaryOptionIcon(option, idx) && getPrimaryOptionColour(option, idx)} + + + + {/if} +
    +
  • + {/each} + {/if} + {/each} +
+
+ {/if} + {#if secondaryOptions.length} +
+ + {#if secondaryOpen} +
(secondaryOpen = false)} + transition:fly|local={{ y: -20, duration: 200 }} + class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" + style="width: 30%" + > +
    + {#each secondaryOptions as option, idx} +
  • + onPickSecondary(getSecondaryOptionValue(option, idx))} + > + {#if getSecondaryOptionColour(option, idx)} + + + + {/if} + + + {getSecondaryOptionLabel(option, idx)} + + +
  • + {/each} +
+
+ {/if} +
+ {/if} +
+ + diff --git a/packages/bbui/src/Form/Core/RadioGroup.svelte b/packages/bbui/src/Form/Core/RadioGroup.svelte index 18a1e82ee8..a3952a9759 100644 --- a/packages/bbui/src/Form/Core/RadioGroup.svelte +++ b/packages/bbui/src/Form/Core/RadioGroup.svelte @@ -10,6 +10,7 @@ export let disabled = false export let getOptionLabel = option => option export let getOptionValue = option => option + export let getOptionTitle = option => option const dispatch = createEventDispatcher() const onChange = e => dispatch("change", e.target.value) @@ -19,7 +20,7 @@ {#if options && Array.isArray(options)} {#each options as option}
diff --git a/packages/bbui/src/Form/Core/Search.svelte b/packages/bbui/src/Form/Core/Search.svelte index e6384e242a..f19c2a7f4c 100644 --- a/packages/bbui/src/Form/Core/Search.svelte +++ b/packages/bbui/src/Form/Core/Search.svelte @@ -8,6 +8,8 @@ export let id = null export let updateOnChange = true export let quiet = false + export let inputRef + const dispatch = createEventDispatcher() let focus = false @@ -68,6 +70,7 @@ type="search" class="spectrum-Textfield-input spectrum-Search-input" autocomplete="off" + bind:this={inputRef} />
+ {/if} diff --git a/packages/bbui/src/Link/Link.svelte b/packages/bbui/src/Link/Link.svelte index f66554bd75..3bbfdd8282 100644 --- a/packages/bbui/src/Link/Link.svelte +++ b/packages/bbui/src/Link/Link.svelte @@ -8,12 +8,14 @@ export let secondary = false export let overBackground = false export let target + export let download - import { View } from "svench"; - import DetailSummary from "./DetailSummary.svelte"; - - - - - - - - - -
- - 1 - 2 - 3 - 4 - - - 1 - 2 - 3 - 4 - -
-
- - -
- - 1 - 2 - 3 - 4 - - - 1 - 2 - 3 - 4 - -
-
diff --git a/packages/bbui/src/List/List.svelte b/packages/bbui/src/List/List.svelte new file mode 100644 index 0000000000..243b04da50 --- /dev/null +++ b/packages/bbui/src/List/List.svelte @@ -0,0 +1,28 @@ + + +
+ {#if title} +
+ {title} +
+ {/if} +
+ +
+
+ + diff --git a/packages/bbui/src/List/ListItem.svelte b/packages/bbui/src/List/ListItem.svelte new file mode 100644 index 0000000000..40d3c5541c --- /dev/null +++ b/packages/bbui/src/List/ListItem.svelte @@ -0,0 +1,90 @@ + + +
+
+ {#if icon} + + {/if} + {#if avatar} + + {/if} + {#if title} + {title} + {/if} + {#if subtitle} + + {/if} +
+
+ +
+
+ + diff --git a/packages/bbui/src/Menu/Item.svelte b/packages/bbui/src/Menu/Item.svelte index a5609683a8..dfe61c1736 100644 --- a/packages/bbui/src/Menu/Item.svelte +++ b/packages/bbui/src/Menu/Item.svelte @@ -1,5 +1,6 @@ -
+
{#if icon} {/if} -
+
{message || ""}
+ {#if action} + +
{actionMessage}
+
+ {/if}
{#if dismissable}
@@ -46,4 +56,15 @@ .spectrum-Toast { pointer-events: all; } + + .wide { + width: 100%; + } + + .actionBody { + justify-content: space-between; + display: flex; + width: 100%; + align-items: center; + } diff --git a/packages/bbui/src/Notification/NotificationDisplay.svelte b/packages/bbui/src/Notification/NotificationDisplay.svelte index eb778f3aa0..0f7e93eb23 100644 --- a/packages/bbui/src/Notification/NotificationDisplay.svelte +++ b/packages/bbui/src/Notification/NotificationDisplay.svelte @@ -8,13 +8,15 @@
- {#each $notifications as { type, icon, message, id, dismissable } (id)} -
+ {#each $notifications as { type, icon, message, id, dismissable, action, wide } (id)} +
notifications.dismiss(id)} />
@@ -25,7 +27,7 @@ diff --git a/packages/bbui/src/SideNavigation/Item.svelte b/packages/bbui/src/SideNavigation/Item.svelte index 30da1fa172..aa86fc02a8 100644 --- a/packages/bbui/src/SideNavigation/Item.svelte +++ b/packages/bbui/src/SideNavigation/Item.svelte @@ -1,6 +1,7 @@
  • {/if} + {#if badge} +
    + {badge} +
    + {/if}
    + {#if multilevel && $$slots.subnav}
    {/if}
  • + + diff --git a/packages/bbui/src/StatusLight/StatusLight.svelte b/packages/bbui/src/StatusLight/StatusLight.svelte index f56fee0c2a..5b7257891f 100644 --- a/packages/bbui/src/StatusLight/StatusLight.svelte +++ b/packages/bbui/src/StatusLight/StatusLight.svelte @@ -17,10 +17,18 @@ export let negative = false export let disabled = false export let active = false + export let color = null + export let square = false + export let hoverable = false
    + + diff --git a/packages/bbui/src/Stores/banner.js b/packages/bbui/src/Stores/banner.js index 81a9ee2204..ba6d187d97 100644 --- a/packages/bbui/src/Stores/banner.js +++ b/packages/bbui/src/Stores/banner.js @@ -1,7 +1,14 @@ import { writable } from "svelte/store" +export const BANNER_TYPES = { + INFO: "info", + NEGATIVE: "negative", +} + export function createBannerStore() { - const DEFAULT_CONFIG = {} + const DEFAULT_CONFIG = { + messages: [], + } const banner = writable(DEFAULT_CONFIG) @@ -20,17 +27,38 @@ export function createBannerStore() { const showStatus = async () => { const config = { message: "Some systems are experiencing issues", - type: "negative", + type: BANNER_TYPES.NEGATIVE, extraButtonText: "View Status", extraButtonAction: () => window.open("https://status.budibase.com/"), } - await show(config) + await queue([config]) + } + + const queue = async entries => { + const priority = { + [BANNER_TYPES.NEGATIVE]: 0, + [BANNER_TYPES.INFO]: 1, + } + banner.update(store => { + const sorted = [...store.messages, ...entries].sort((a, b) => { + if (priority[a.type] == priority[b.type]) { + return 0 + } + return priority[a.type] < priority[b.type] ? -1 : 1 + }) + return { + ...store, + messages: sorted, + } + }) } return { subscribe: banner.subscribe, showStatus, + show, + queue, } } diff --git a/packages/bbui/src/Stores/notifications.js b/packages/bbui/src/Stores/notifications.js index 74eed8628a..449d282f24 100644 --- a/packages/bbui/src/Stores/notifications.js +++ b/packages/bbui/src/Stores/notifications.js @@ -20,7 +20,16 @@ export const createNotificationStore = () => { setTimeout(() => (block = false), timeout) } - const send = (message, type = "default", icon = "", autoDismiss = true) => { + const send = ( + message, + { + type = "default", + icon = "", + autoDismiss = true, + action = null, + wide = false, + } + ) => { if (block) { return } @@ -28,7 +37,15 @@ export const createNotificationStore = () => { _notifications.update(state => { return [ ...state, - { id: _id, type, message, icon, dismissable: !autoDismiss }, + { + id: _id, + type, + message, + icon, + dismissable: !autoDismiss, + action, + wide, + }, ] }) if (autoDismiss) { @@ -50,10 +67,11 @@ export const createNotificationStore = () => { return { subscribe, send, - info: msg => send(msg, "info", "Info"), - error: msg => send(msg, "error", "Alert", false), - warning: msg => send(msg, "warning", "Alert"), - success: msg => send(msg, "success", "CheckmarkCircle"), + info: msg => send(msg, { type: "info", icon: "Info" }), + error: msg => + send(msg, { type: "error", icon: "Alert", autoDismiss: false }), + warning: msg => send(msg, { type: "warning", icon: "Alert" }), + success: msg => send(msg, { type: "success", icon: "CheckmarkCircle" }), blockNotifications, dismiss: dismissNotification, } diff --git a/packages/bbui/src/Table/ArrayRenderer.svelte b/packages/bbui/src/Table/ArrayRenderer.svelte index 679973a03a..3755850666 100644 --- a/packages/bbui/src/Table/ArrayRenderer.svelte +++ b/packages/bbui/src/Table/ArrayRenderer.svelte @@ -5,7 +5,7 @@ const displayLimit = 5 - $: badges = value?.slice(0, displayLimit) ?? [] + $: badges = Array.isArray(value) ? value.slice(0, displayLimit) : [] $: leftover = (value?.length ?? 0) - badges.length diff --git a/packages/bbui/src/Table/AttachmentRenderer.svelte b/packages/bbui/src/Table/AttachmentRenderer.svelte index 97ce1394cc..b4de8672ae 100644 --- a/packages/bbui/src/Table/AttachmentRenderer.svelte +++ b/packages/bbui/src/Table/AttachmentRenderer.svelte @@ -1,5 +1,4 @@ @@ -19,7 +20,7 @@ on:mouseleave={() => (showTooltip = false)} on:focus > - +
    {#if showTooltip}
    @@ -39,7 +40,6 @@ position: relative; display: flex; justify-content: center; - margin-top: 1px; margin-left: 5px; margin-right: 5px; } @@ -48,14 +48,13 @@ display: flex; justify-content: center; top: 15px; - z-index: 100; + z-index: 200; width: 160px; } .icon { transform: scale(0.75); } .icon-small { - margin-top: -2px; - margin-bottom: -5px; + margin-bottom: -2px; } diff --git a/packages/bbui/src/Typography/Detail.svelte b/packages/bbui/src/Typography/Detail.svelte index bb5c78c11e..76437ffb3c 100644 --- a/packages/bbui/src/Typography/Detail.svelte +++ b/packages/bbui/src/Typography/Detail.svelte @@ -1,9 +1,7 @@ diff --git a/packages/bbui/src/index.js b/packages/bbui/src/index.js index 2b16f32b84..538a62188f 100644 --- a/packages/bbui/src/index.js +++ b/packages/bbui/src/index.js @@ -20,9 +20,12 @@ export { default as Button } from "./Button/Button.svelte" export { default as ButtonGroup } from "./ButtonGroup/ButtonGroup.svelte" export { default as ClearButton } from "./ClearButton/ClearButton.svelte" export { default as Icon, directions } from "./Icon/Icon.svelte" +export { default as IconAvatar } from "./Icon/IconAvatar.svelte" export { default as Toggle } from "./Form/Toggle.svelte" export { default as RadioGroup } from "./Form/RadioGroup.svelte" export { default as Checkbox } from "./Form/Checkbox.svelte" +export { default as InputDropdown } from "./Form/InputDropdown.svelte" +export { default as PickerDropdown } from "./Form/PickerDropdown.svelte" export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte" export { default as Popover } from "./Popover/Popover.svelte" export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte" @@ -32,6 +35,7 @@ export { default as Layout } from "./Layout/Layout.svelte" export { default as Page } from "./Layout/Page.svelte" export { default as Link } from "./Link/Link.svelte" export { default as Tooltip } from "./Tooltip/Tooltip.svelte" +export { default as TooltipWrapper } from "./Tooltip/TooltipWrapper.svelte" export { default as Menu } from "./Menu/Menu.svelte" export { default as MenuSection } from "./Menu/Section.svelte" export { default as MenuSeparator } from "./Menu/Separator.svelte" @@ -58,16 +62,23 @@ export { default as Pagination } from "./Pagination/Pagination.svelte" export { default as Badge } from "./Badge/Badge.svelte" export { default as StatusLight } from "./StatusLight/StatusLight.svelte" export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte" +export { default as IconPicker } from "./IconPicker/IconPicker.svelte" export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte" export { default as Banner } from "./Banner/Banner.svelte" export { default as BannerDisplay } from "./Banner/BannerDisplay.svelte" export { default as MarkdownEditor } from "./Markdown/MarkdownEditor.svelte" export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte" export { default as RichTextField } from "./Form/RichTextField.svelte" +export { default as List } from "./List/List.svelte" +export { default as ListItem } from "./List/ListItem.svelte" +export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte" +export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte" +export { default as Slider } from "./Form/Slider.svelte" // Renderers export { default as BoldRenderer } from "./Table/BoldRenderer.svelte" export { default as CodeRenderer } from "./Table/CodeRenderer.svelte" +export { default as InternalRenderer } from "./Table/InternalRenderer.svelte" // Typography export { default as Body } from "./Typography/Body.svelte" @@ -85,7 +96,7 @@ export { default as clickOutside } from "./Actions/click_outside" // Stores export { notifications, createNotificationStore } from "./Stores/notifications" -export { banner } from "./Stores/banner" +export { banner, BANNER_TYPES } from "./Stores/banner" // Helpers export * as Helpers from "./helpers" diff --git a/packages/bbui/yarn.lock b/packages/bbui/yarn.lock index 0bff3e86d9..6200487df3 100644 --- a/packages/bbui/yarn.lock +++ b/packages/bbui/yarn.lock @@ -28,6 +28,46 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + 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.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/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@^1.4.10": + 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/trace-mapping@^0.3.9": + version "0.3.14" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" + integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@rollup/plugin-commonjs@^16.0.0": version "16.0.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-16.0.0.tgz#169004d56cd0f0a1d0f35915d31a036b0efe281f" @@ -206,6 +246,11 @@ resolved "https://registry.yarnpkg.com/@spectrum-css/sidenav/-/sidenav-3.0.2.tgz#9d70f408d588ee79c69857751010333671f32713" integrity sha512-YpIdH/F0jEICYmoduGrnkTmxwJq1kfKxEp0wOs+ZkQOsvKMv1an7nyhsfOKCQqcGNfYzJ9mJAk7/u5+vsxHa8g== +"@spectrum-css/slider@3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@spectrum-css/slider/-/slider-3.0.1.tgz#5281e6f47eb5a4fd3d1816c138bf66d01d7f2e49" + integrity sha512-DI2dtMRnQuDM1miVzl3SGyR1khUEKnwdXfO5EHDFwkC3yav43F5QogkfjmjFmWWobMVovdJlAuiaaJ/IHejD0Q== + "@spectrum-css/statuslight@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@spectrum-css/statuslight/-/statuslight-3.0.2.tgz#dc54b6cd113413dcdb909c486b5d7bae60db65c5" @@ -226,10 +271,10 @@ resolved "https://registry.yarnpkg.com/@spectrum-css/table/-/table-3.0.2.tgz#c666743d569fef81ddc8810fac8cda53b315f8d7" integrity sha512-nt/QNC7NmUank0wozd4FySEX1UIYXuvuOKDyN1II3sxfwFSpJfp/Df9KVMhrYs4EsmB4XMGcoxp8ND/CrvH3ow== -"@spectrum-css/tabs@^3.0.1": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@spectrum-css/tabs/-/tabs-3.0.2.tgz#822316672e7b0dfba66faa988e638ddae18c700e" - integrity sha512-4RNcmwf0wxLpB7M54H02owlj0mKE8neL1+lytQpxOOhlwTO5zdsD82zjvx9tIc8tRnRKuhCCCwTuBxHYstnBmw== +"@spectrum-css/tabs@^3.2.12": + version "3.2.12" + resolved "https://registry.yarnpkg.com/@spectrum-css/tabs/-/tabs-3.2.12.tgz#9b08f23d5aa881b3441af7757800c7173e5685ff" + integrity sha512-rPFUW9SSW4+3/UJ3UrtY2/l3sQvlqB1fqxHLPDjgykvbfrnMejcCTNV4ZrFNHXpE/6+kGnk+yVViSPtWGwJzkA== "@spectrum-css/tags@^3.0.2": version "3.0.2" @@ -335,6 +380,11 @@ acorn@^7.3.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.5.0: + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== + alphanum-sort@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" @@ -442,9 +492,9 @@ browserslist@^4.0.0: node-releases "^1.1.71" buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== builtin-modules@^3.1.0: version "3.2.0" @@ -2367,16 +2417,15 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" -"source-map-fast@npm:source-map@0.7.3", source-map@~0.7.2: - name source-map-fast +"source-map-fast@npm:source-map@0.7.3": version "0.7.3" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== -source-map-support@~0.5.19: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -2480,9 +2529,9 @@ svelte-portal@^1.0.0: integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q== svelte@^3.38.2: - version "3.38.2" - resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.38.2.tgz#55e5c681f793ae349b5cc2fe58e5782af4275ef5" - integrity sha512-q5Dq0/QHh4BLJyEVWGe7Cej5NWs040LWjMbicBGZ+3qpFWJ1YObRmUDZKbbovddLC9WW7THTj3kYbTOFmU9fbg== + version "3.49.0" + resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.49.0.tgz#5baee3c672306de1070c3b7888fc2204e36a4029" + integrity sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA== svgo@^1.0.0: version "1.3.2" @@ -2504,13 +2553,14 @@ svgo@^1.0.0: util.promisify "~1.0.0" terser@^5.0.0: - version "5.6.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.6.1.tgz#a48eeac5300c0a09b36854bf90d9c26fb201973c" - integrity sha512-yv9YLFQQ+3ZqgWCUk+pvNJwgUTdlIxUk1WTN+RnaFJe2L7ipG2csPT0ra2XRm7Cs8cxN7QXmK1rFzEwYEQkzXw== + version "5.14.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10" + integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA== dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" commander "^2.20.0" - source-map "~0.7.2" - source-map-support "~0.5.19" + source-map-support "~0.5.20" timsort@^0.3.0: version "0.3.0" @@ -2575,7 +2625,7 @@ unquote@~1.1.1: util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== util.promisify@~1.0.0: version "1.0.1" diff --git a/packages/builder/cypress.json b/packages/builder/cypress.json index 06bf558946..f1eada481f 100644 --- a/packages/builder/cypress.json +++ b/packages/builder/cypress.json @@ -13,7 +13,7 @@ "HOST_IP": "" }, "retries": { - "runMode": 2, + "runMode": 1, "openMode": 0 } -} +} \ No newline at end of file diff --git a/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js b/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js index 3e0ba92ba4..f436f3ff39 100644 --- a/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js +++ b/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js @@ -16,20 +16,16 @@ filterTests(['all'], () => { it("should add form with multi select picker, containing 5 options", () => { cy.navigateToFrontend() - cy.wait(500) // Add data provider - cy.get(interact.CATEGORY_DATA).click() - cy.get(interact.COMPONENT_DATA_PROVIDER).click() + cy.searchAndAddComponent("Data Provider") cy.get(interact.DATASOURCE_PROP_CONTROL).click() cy.get(interact.DROPDOWN).contains("Multi Data").click() - cy.wait(500) // Add Form with schema to match table - cy.addComponent("Form", "Form") + cy.searchAndAddComponent("Form") cy.get(interact.DATASOURCE_PROP_CONTROL).click() cy.get(interact.DROPDOWN).contains("Multi Data").click() - cy.wait(500) // Add multi-select picker to form - cy.addComponent("Form", "Multi-select Picker").then(componentId => { + cy.searchAndAddComponent("Multi-select Picker").then(componentId => { cy.get(interact.DATASOURCE_FIELD_CONTROL).type("Test Data").type("{enter}") cy.wait(1000) cy.getComponent(componentId).contains("Choose some options").click() @@ -41,7 +37,7 @@ filterTests(['all'], () => { } // Check items have been selected cy.getComponent(componentId) - .find(interact.SPECTRUM_Picker_LABEL) + .find(interact.SPECTRUM_PICKER_LABEL) .contains("(5)") }) }) diff --git a/packages/builder/cypress/integration/addRadioButtons.spec.js b/packages/builder/cypress/integration/addRadioButtons.spec.js index 8f5b1a527b..91cfd0c7fa 100644 --- a/packages/builder/cypress/integration/addRadioButtons.spec.js +++ b/packages/builder/cypress/integration/addRadioButtons.spec.js @@ -10,14 +10,13 @@ filterTests(['all'], () => { it("should add Radio Buttons options picker on form, add data, and confirm", () => { cy.navigateToFrontend() - cy.addComponent("Form", "Form") - cy.addComponent("Form", "Options Picker").then((componentId) => { - // Provide field setting + cy.searchAndAddComponent("Form") + cy.searchAndAddComponent("Options Picker").then((componentId) => { + // Provide field setting cy.get(interact.DATASOURCE_FIELD_CONTROL).type("1") // Open dropdown and select Radio buttons cy.get(interact.OPTION_TYPE_PROP_CONTROL).click().then(() => { cy.get(interact.SPECTRUM_POPOVER).contains('Radio buttons') - .wait(500) .click() }) const radioButtonsTotal = 3 @@ -31,10 +30,14 @@ filterTests(['all'], () => { const addRadioButtonData = (totalRadioButtons) => { cy.get(interact.OPTION_SOURCE_PROP_CONROL).click().then(() => { cy.get(interact.SPECTRUM_POPOVER).contains('Custom') - .wait(500) .click() + .wait(1000) }) cy.addCustomSourceOptions(totalRadioButtons) } + + after(() => { + cy.deleteAllApps() + }) }) }) diff --git a/packages/builder/cypress/integration/adminAndManagement/accountPortals.spec.js b/packages/builder/cypress/integration/adminAndManagement/accountPortals.spec.js new file mode 100644 index 0000000000..448240f81d --- /dev/null +++ b/packages/builder/cypress/integration/adminAndManagement/accountPortals.spec.js @@ -0,0 +1,116 @@ +import filterTests from "../../support/filterTests" +const interact = require('../../support/interact') + +filterTests(["smoke", "all"], () => { + context("Account Portals", () => { + + const bbUserEmail = "bbuser@test.com" + + before(() => { + cy.login() + cy.deleteApp("Cypress Tests") + cy.createApp("Cypress Tests", false) + + // Create new user + cy.wait(500) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000}) + cy.createUser(bbUserEmail) + cy.contains("bbuser").click() + cy.wait(500) + + // Reset password + cy.get(".title").within(() => { + cy.get(interact.SPECTRUM_ICON).click({ force: true }) + }) + cy.get(interact.SPECTRUM_MENU).within(() => { + cy.get(interact.SPECTRUM_MENU_ITEM).contains("Force password reset").click({ force: true }) + }) + + cy.get(interact.SPECTRUM_DIALOG_GRID) + .find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd') + + cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true }) + + // Login as new user and set password + cy.logOut() + cy.get('@pwd').then((pwd) => { + cy.login(bbUserEmail, pwd) + }) + + for (let i = 0; i < 2; i++) { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test") + } + cy.get(interact.SPECTRUM_BUTTON).contains("Reset your password").click({ force: true }) + //cy.logoutNoAppGrid() + }) + + it("should verify Standard Portal", () => { + // Development access should be disabled (Admin access is already disabled) + cy.login() + cy.setUserRole("bbuser", "App User") + bbUserLogin() + + // Verify Standard Portal + cy.get(interact.SPECTRUM_SIDENAV).should('not.exist') // No config sections + cy.get(interact.CREATE_APP_BUTTON).should('not.exist') // No create app button + cy.get(".app").should('not.exist') // No apps -> no roles assigned to user + cy.get(interact.CONTAINER).should('contain', bbUserEmail) // Message containing users email + + cy.logoutNoAppGrid() + }) + + it("should verify Admin Portal", () => { + cy.login() + // Configure user role + cy.setUserRole("bbuser", "Admin") + bbUserLogin() + + // Verify available options for Admin portal + cy.get(interact.SPECTRUM_SIDENAV) + .should('contain', 'Apps') + //.and('contain', 'Usage') + .and('contain', 'Users') + .and('contain', 'Auth') + .and('contain', 'Email') + .and('contain', 'Organisation') + .and('contain', 'Theming') + .and('contain', 'Update') + //.and('contain', 'Upgrade') + + cy.logOut() + }) + + it("should verify Development Portal", () => { + // Only Development access should be enabled + cy.login() + cy.setUserRole("bbuser", "Developer") + bbUserLogin() + + // Verify available options for Admin portal + cy.get(interact.SPECTRUM_SIDENAV) + .should('contain', 'Apps') + //.and('contain', 'Usage') + .and('not.contain', 'Users') + .and('not.contain', 'Auth') + .and('not.contain', 'Email') + .and('not.contain', 'Organisation') + .and('contain', 'Theming') + .and('not.contain', 'Update') + .and('not.contain', 'Upgrade') + + cy.logOut() + }) + + const bbUserLogin = () => { + // Login as bbuser + cy.logOut() + cy.login(bbUserEmail, "test") + } + + after(() => { + cy.login() + // Delete BB user + cy.deleteUser(bbUserEmail) + }) + }) +}) diff --git a/packages/builder/cypress/integration/adminAndManagement/authentication.spec.js b/packages/builder/cypress/integration/adminAndManagement/authentication.spec.js new file mode 100644 index 0000000000..5cc42cb59a --- /dev/null +++ b/packages/builder/cypress/integration/adminAndManagement/authentication.spec.js @@ -0,0 +1,178 @@ +import filterTests from "../../support/filterTests" +// const interact = require("../support/interact") + +filterTests(["smoke", "all"], () => { + context("Auth Configuration", () => { + before(() => { + cy.login() + }) + + after(() => { + cy.get(".spectrum-SideNav li").contains("Auth").click() + cy.location().should(loc => { + expect(loc.pathname).to.eq("/builder/portal/manage/auth") + }) + + cy.get("[data-cy=new-scope-input]").clear() + + cy.get("div.content").scrollTo("bottom") + cy.get("[data-cy=oidc-active]").click() + + cy.get("[data-cy=oidc-active]").should('not.be.checked') + + cy.intercept("POST", "/api/global/configs").as("updateAuth") + cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true}) + cy.wait("@updateAuth") + cy.get("@updateAuth").its("response.statusCode").should("eq", 200) + + cy.get(".spectrum-Toast-content") + .contains("Settings saved") + .should("be.visible") + }) + + it("Should allow updating of the OIDC config", () => { + cy.get(".spectrum-SideNav li").contains("Auth").click() + cy.location().should(loc => { + expect(loc.pathname).to.eq("/builder/portal/manage/auth") + }) + cy.get("div.content").scrollTo("bottom") + cy.get(".spectrum-Toast .spectrum-ClearButton").click() + + cy.get("input[data-cy=configUrl]").type("http://budi-auth.com/v2") + cy.get("input[data-cy=clientID]").type("34ac6a13-f24a-4b52-c70d-fa544ffd11b2") + cy.get("input[data-cy=clientSecret]").type("12A8Q~4nS_DWhOOJ2vWIRsNyDVsdtXPD.Zxa9df_") + + cy.get("button[data-cy=oidc-save]").should("not.be.disabled"); + + cy.intercept("POST", "/api/global/configs").as("updateAuth") + cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true}) + cy.wait("@updateAuth") + cy.get("@updateAuth").its("response.statusCode").should("eq", 200) + + cy.get(".spectrum-Toast-content") + .contains("Settings saved") + .should("be.visible") + }) + + it("Should display default scopes in advanced config.", () => { + cy.get(".spectrum-SideNav li").contains("Auth").click() + cy.location().should(loc => { + expect(loc.pathname).to.eq("/builder/portal/manage/auth") + }) + cy.get("div.content").scrollTo("bottom") + + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4) + + cy.get(".spectrum-Tags-item").contains("openid") + cy.get(".spectrum-Tags-item").contains("openid").find(".spectrum-ClearButton").should("not.exist") + + cy.get(".spectrum-Tags-item").contains("offline_access") + cy.get(".spectrum-Tags-item").contains("email") + cy.get(".spectrum-Tags-item").contains("profile") + }) + + it("Add a new scopes", () => { + cy.get(".spectrum-SideNav li").contains("Auth").click() + cy.location().should(loc => { + expect(loc.pathname).to.eq("/builder/portal/manage/auth") + }) + cy.get("div.content").scrollTo("bottom") + + cy.get("[data-cy=new-scope-input]").type("Sample{enter}") + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 5) + cy.get(".spectrum-Tags-item").contains("Sample") + + cy.get(".auth-form input.spectrum-Textfield-input").type("Another ") + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 6) + cy.get(".spectrum-Tags-item").contains("Another") + + cy.get("button[data-cy=oidc-save]").should("not.be.disabled"); + + cy.intercept("POST", "/api/global/configs").as("updateAuth") + cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true}) + cy.wait("@updateAuth") + cy.get("@updateAuth").its("response.statusCode").should("eq", 200) + + cy.reload() + + cy.get("div.content").scrollTo("bottom") + + cy.get(".spectrum-Tags-item").contains("openid") + cy.get(".spectrum-Tags-item").contains("offline_access") + cy.get(".spectrum-Tags-item").contains("email") + cy.get(".spectrum-Tags-item").contains("profile") + cy.get(".spectrum-Tags-item").contains("Sample") + cy.get(".spectrum-Tags-item").contains("Another") + }) + + it("Should allow the removal of auth scopes", () => { + cy.get(".spectrum-SideNav li").contains("Auth").click() + cy.location().should(loc => { + expect(loc.pathname).to.eq("/builder/portal/manage/auth") + }) + cy.get("div.content").scrollTo("bottom") + + cy.get(".spectrum-Tags-item").contains("offline_access").parent().find(".spectrum-ClearButton").click() + cy.get(".spectrum-Tags-item").contains("profile").parent().find(".spectrum-ClearButton").click() + + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4) + + cy.get(".spectrum-Tags-item").contains("offline_access").should("not.exist") + cy.get(".spectrum-Tags-item").contains("profile").should("not.exist") + + cy.get("button[data-cy=oidc-save]").should("not.be.disabled"); + + cy.intercept("POST", "/api/global/configs").as("updateAuth") + cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true}) + cy.wait("@updateAuth") + cy.get("@updateAuth").its("response.statusCode").should("eq", 200) + + cy.get(".spectrum-Toast-content") + .contains("Settings saved") + .should("be.visible") + + cy.reload() + + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4) + + cy.get(".spectrum-Tags-item").contains("offline_access").should("not.exist") + cy.get(".spectrum-Tags-item").contains("profile").should("not.exist") + }) + + it("Should allow auth scopes to be reset to the core defaults.", () => { + cy.get(".spectrum-SideNav li").contains("Auth").click() + + cy.get("div.content").scrollTo("bottom") + + cy.get("[data-cy=restore-oidc-default-scopes]").click({force: true}) + + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4) + + cy.get(".spectrum-Tags-item").contains("openid") + cy.get(".spectrum-Tags-item").contains("offline_access") + cy.get(".spectrum-Tags-item").contains("email") + cy.get(".spectrum-Tags-item").contains("profile") + }) + + it("Should not allow invalid characters in the auth scopes", () => { + cy.get("[data-cy=new-scope-input]").type("thisIsInvalid\\{enter}") + cy.get(".spectrum-Form-itemField .error").contains("Auth scopes cannot contain spaces, double quotes or backslashes") + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4) + + cy.get("[data-cy=new-scope-input]").clear() + + cy.get("[data-cy=new-scope-input]").type("alsoInvalid\"{enter}") + cy.get(".spectrum-Form-itemField .error").contains("Auth scopes cannot contain spaces, double quotes or backslashes") + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4) + + cy.get("[data-cy=new-scope-input]").clear() + }) + + it("Should not allow duplicate auth scopes", () => { + cy.get("[data-cy=new-scope-input]").type("offline_access{enter}") + cy.get(".spectrum-Form-itemField .error").contains("Auth scope already exists") + cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4) + }) + + }) +}) \ No newline at end of file diff --git a/packages/builder/cypress/integration/adminAndManagement/userManagement.spec.js b/packages/builder/cypress/integration/adminAndManagement/userManagement.spec.js new file mode 100644 index 0000000000..000ca7cb54 --- /dev/null +++ b/packages/builder/cypress/integration/adminAndManagement/userManagement.spec.js @@ -0,0 +1,236 @@ +import filterTests from "../../support/filterTests" +const interact = require('../../support/interact') + +filterTests(["smoke", "all"], () => { + context("User Management", () => { + before(() => { + cy.login() + cy.deleteApp("Cypress Tests") + cy.createApp("Cypress Tests", false) + }) + + it("should create a user via basic onboarding", () => { + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000}) + cy.createUser("bbuser@test.com") + cy.get(interact.SPECTRUM_TABLE).should("contain", "bbuser") + }) + + it("should confirm App User role for a New User", () => { + cy.contains("bbuser").click() + cy.get(".spectrum-Form-itemField").eq(3).should('contain', 'App User') + + // User should not have app access + cy.get(interact.LIST_ITEMS, { timeout: 500 }).should("contain", "No apps") + }) + + if (Cypress.env("TEST_ENV")) { + xit("should assign role types", () => { + // 3 apps minimum required - to assign an app to each role type + cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) + .its("body") + .then(val => { + if (val.length < 3) { + for (let i = 1; i < 3; i++) { + const uuid = () => Cypress._.random(0, 1e6) + const name = uuid() + if(i < 1){ + cy.createApp(name, false) + } else { + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000}) + cy.wait(1000) + cy.get(interact.CREATE_APP_BUTTON, { timeout: 2000 }).click({ force: true }) + cy.createAppFromScratch(name) + } + } + } + }) + // Navigate back to the user + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000}) + cy.get(interact.SPECTRUM_SIDENAV).contains("Users").click() + cy.get(interact.SPECTRUM_TABLE, { timeout: 1000 }).contains("bbuser").click() + cy.get(interact.SPECTRUM_HEADING).contains("bbuser", { timeout: 2000}) + for (let i = 0; i < 3; i++) { + cy.get(interact.SPECTRUM_TABLE, { timeout: 3000}) + .eq(1) + .find(interact.SPECTRUM_TABLE_ROW) + .eq(0) + .find(interact.SPECTRUM_TABLE_CELL) + .eq(0) + .click() + cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 1000 }) + .contains("Choose an option") + .click() + .then(() => { + if (i == 0) { + cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Admin").click({ force: true }) + } + else if (i == 1) { + cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Power").click({ force: true }) + } + else if (i == 2) { + cy.get(interact.SPECTRUM_MENU, { timeout: 2000 }).contains("Basic").click({ force: true }) + } + cy.get(interact.SPECTRUM_BUTTON, { timeout: 2000 }) + .contains("Update role") + .click({ force: true }) + }) + cy.reload() + cy.wait(1000) + } + // Confirm roles exist within Configure roles table + cy.get(interact.SPECTRUM_TABLE, { timeout: 20000 }) + .eq(0) + .within(assginedRoles => { + expect(assginedRoles).to.contain("Admin") + expect(assginedRoles).to.contain("Power") + expect(assginedRoles).to.contain("Basic") + }) + }) + + xit("should unassign role types", () => { + // Set each app within Configure roles table to 'No Access' + cy.get(interact.SPECTRUM_TABLE) + .eq(0) + .find(interact.SPECTRUM_TABLE_ROW) + .its("length") + .then(len => { + for (let i = 0; i < len; i++) { + cy.get(interact.SPECTRUM_TABLE) + .eq(0) + .find(interact.SPECTRUM_TABLE_ROW) + .eq(0) + .find(interact.SPECTRUM_TABLE_CELL) + .eq(0) + .click() + .then(() => { + cy.get(interact.SPECTRUM_PICKER).eq(1).click({ force: true }) + cy.get(interact.SPECTRUM_POPOVER, { timeout: 500 }).contains("No Access").click() + }) + cy.get(interact.SPECTRUM_BUTTON) + .contains("Update role") + .click({ force: true }) + } + }) + // Confirm Configure roles table no longer has any apps in it + cy.get(interact.SPECTRUM_TABLE, { timeout: 1000 }).eq(0).contains("No rows found") + }) + } + + xit("should enable Developer access and verify application access", () => { + // Enable Developer access + cy.get(interact.FIELD) + .eq(4) + .within(() => { + cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true }) + }) + // No Access table should now be empty + cy.get(interact.CONTAINER) + .contains("No Access") + .parent() + .within(() => { + cy.get(interact.SPECTRUM_TABLE).contains("No rows found") + }) + + // Each app within Configure roles should have Admin access + cy.get(interact.SPECTRUM_TABLE) + .eq(0) + .find(interact.SPECTRUM_TABLE_ROW) + .its("length") + .then(len => { + for (let i = 0; i < len; i++) { + cy.get(interact.SPECTRUM_TABLE) + .eq(0) + .find(interact.SPECTRUM_TABLE_ROW) + .eq(i) + .contains("Admin") + cy.wait(500) + } + }) + }) + + xit("should disable Developer access and verify application access", () => { + // Disable Developer access + cy.get(interact.FIELD) + .eq(4) + .within(() => { + cy.get(".spectrum-Switch-input").click({ force: true }) + }) + // Configure roles table should now be empty + cy.get(interact.CONTAINER) + .contains("Configure roles") + .parent() + .within(() => { + cy.get(interact.SPECTRUM_TABLE).contains("No rows found") + }) + }) + + it("Should edit user details within user details page", () => { + // Add First name + cy.get(interact.FIELD, { timeout: 1000 }).eq(1).within(() => { + cy.wait(500) + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).wait(500).clear().click().type("bb") + }) + // Add Last name + cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => { + cy.wait(500) + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).click().wait(500).clear().type("test") + }) + cy.get(interact.FIELD, { timeout: 1000 }).eq(0).click() + // Reload page + cy.reload() + + // Confirm details have been saved + cy.get(interact.FIELD, { timeout: 20000 }).eq(1).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb") + }) + cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).should('have.value', "test") + }) + }) + + it("should reset the users password", () => { + cy.get(".title").within(() => { + cy.get(interact.SPECTRUM_ICON).click({ force: true }) + }) + cy.get(interact.SPECTRUM_MENU).within(() => { + cy.get(interact.SPECTRUM_MENU_ITEM).contains("Force password reset").click({ force: true }) + }) + + // Reset password modal + cy.get(interact.SPECTRUM_DIALOG_GRID) + .find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd') + cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").should('not.exist') + + // Logout, then login with new password + cy.logOut() + cy.get('@pwd').then((pwd) => { + cy.login("bbuser@test.com", pwd) + }) + + // Reset password screen + for (let i = 0; i < 2; i++) { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test") + } + cy.get(interact.SPECTRUM_BUTTON).contains("Reset your password").click({ force: true }) + + // Confirm user logged in afer password change + cy.login("bbuser@test.com", "test") + cy.get(".avatar > .icon").click({ force: true }) + + cy.get(".spectrum-Menu-item").contains("Update user information").click({ force: true }) + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT) + .eq(0) + .invoke('val').should('eq', 'bbuser@test.com') + + // Logout and login as previous user + cy.logoutNoAppGrid() + cy.login() + }) + + it("should delete a user", () => { + cy.deleteUser("bbuser@test.com") + cy.get(interact.SPECTRUM_TABLE, { timeout: 4000 }).should("not.have.text", "bbuser") + }) + }) +}) diff --git a/packages/builder/cypress/integration/adminAndManagement/userSettings.spec.js b/packages/builder/cypress/integration/adminAndManagement/userSettings.spec.js new file mode 100644 index 0000000000..a2b0d32d02 --- /dev/null +++ b/packages/builder/cypress/integration/adminAndManagement/userSettings.spec.js @@ -0,0 +1,109 @@ +import filterTests from "../../support/filterTests" +const interact = require('../../support/interact') + +filterTests(["smoke", "all"], () => { + context("User Settings Menu", () => { + + before(() => { + cy.login() + }) + + it("should update user information via user settings menu", () => { + const fname = "test" + const lname = "user" + + cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.updateUserInformation(fname, lname) + + // Go to user info and confirm name update + cy.contains("Users").click() + cy.contains("test@test.com").click() + + cy.get(interact.FIELD, { timeout: 1000 }).eq(1).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', fname) + }) + cy.get(interact.FIELD).eq(2).within(() => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', lname) + }) + }) + + it("should allow copying of the users API key", () => { + cy.get(".user-dropdown .avatar > .icon", { timeout: 2000 }).click({ force: true }) + cy.get(interact.SPECTRUM_MENU_ITEM).contains("View API key").click({ force: true }) + cy.get(interact.SPECTRUM_DIALOG_CONTENT).within(() => { + cy.get(interact.SPECTRUM_ICON).click({force: true}) + }) + // There may be timing issues with this on the smoke build + cy.wait(500) + cy.get(".spectrum-Toast-content") + .contains("URL copied to clipboard") + .should("be.visible") + }) + + it("should allow API key regeneration", () => { + // Get initial API key value + cy.get(interact.SPECTRUM_DIALOG_CONTENT) + .find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('keyOne') + + // Click re-generate key button + cy.get("button").contains("Re-generate key").click({ force: true }) + + // Verify API key was changed + cy.get(interact.SPECTRUM_DIALOG_CONTENT).within(() => { + cy.get('@keyOne').then((keyOne) => { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').should('not.eq', keyOne) + }) + }) + cy.closeModal() + }) + + it("should update password", () => { + // Access Update password modal + cy.get(".user-dropdown .avatar > .icon", { timeout: 2000 }).click({ force: true }) + cy.get(interact.SPECTRUM_MENU_ITEM).contains("Update password").click({ force: true }) + + // Enter new password and update + cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => { + for (let i = 0; i < 2; i++) { + // password set to 'newpwd' + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("newpwd") + } + cy.get("button").contains("Update password").click({ force: true }) + }) + + // Logout & in with new password + //cy.logOut() + cy.login("test@test.com", "newpwd") + }) + + it("should open and close developer mode", () => { + cy.get(".user-dropdown .avatar > .icon", { timeout: 2000 }).click({ force: true }) + + // Close developer mode & verify + cy.get(interact.SPECTRUM_MENU_ITEM).contains("Close developer mode").click({ force: true }) + cy.get(interact.SPECTRUM_SIDENAV).should('not.exist') // No config sections + cy.get(interact.CREATE_APP_BUTTON).should('not.exist') // No create app button + cy.get(".app").should('not.exist') // At least one app should be available + + // Open developer mode & verify + cy.get(".avatar > .icon").click({ force: true }) + cy.get(interact.SPECTRUM_MENU_ITEM).contains("Open developer mode").click({ force: true }) + cy.get(interact.SPECTRUM_SIDENAV).should('exist') // config sections available + cy.get(interact.CREATE_APP_BUTTON).should('exist') // create app button available + }) + + after(() => { + // Change password back to original value + cy.get(".user-dropdown .avatar > .icon", { timeout: 2000 }).click({ force: true }) + cy.get(interact.SPECTRUM_MENU_ITEM).contains("Update password").click({ force: true }) + cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => { + for (let i = 0; i < 2; i++) { + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).eq(i).type("test") + } + cy.get("button").contains("Update password").click({ force: true }) + }) + // Remove users name + cy.updateUserInformation() + }) + }) +}) diff --git a/packages/builder/cypress/integration/appOverview.spec.js b/packages/builder/cypress/integration/appOverview.spec.js index db093344b4..d7f2882b26 100644 --- a/packages/builder/cypress/integration/appOverview.spec.js +++ b/packages/builder/cypress/integration/appOverview.spec.js @@ -1,40 +1,29 @@ import filterTests from "../support/filterTests" import clientPackage from "@budibase/client/package.json" -filterTests(['all'], () => { +filterTests(["all"], () => { context("Application Overview screen", () => { before(() => { cy.login() - cy.createTestApp() + cy.deleteAllApps() + cy.createApp("Cypress Tests") }) it("Should be accessible from the applications list", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - - cy.get(".appTable .title").eq(0) - .invoke('attr', 'data-cy') - .then(($dataCy) => { - const dataCy = $dataCy; - cy.get(".appTable .name").eq(0).click() + cy.get(".appTable .title") + .eq(0) + .invoke("attr", "data-cy") + .then($dataCy => { + const dataCy = $dataCy + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .click({ force: true }) - cy.location().should((loc) => { - expect(loc.pathname).to.eq('/builder/portal/overview/' + dataCy) + cy.location().should(loc => { + expect(loc.pathname).to.eq("/builder/portal/overview/" + dataCy) + }) }) - }) - - cy.visit(`${Cypress.config().baseUrl}/builder`) - - cy.get(".appTable .title").eq(0) - .invoke('attr', 'data-cy') - .then(($dataCy) => { - const dataCy = $dataCy; - cy.get(".appTable .app-row-actions button").contains("View").click({force: true}) - - cy.location().should((loc) => { - expect(loc.pathname).to.eq('/builder/portal/overview/' + dataCy) - }) - }) - }) // Find a more suitable place for this. @@ -43,24 +32,28 @@ filterTests(['all'], () => { cy.get(".appTable .lock-status").eq(0).contains("Locked by you").click() - cy.unlockApp({ owned : true }) + cy.unlockApp({ owned: true }) cy.get(".appTable").should("exist") - cy.get(".lock-status").should('not.be.visible') + cy.get(".lock-status").should("not.be.visible") }) - + it("Should allow unlocking in the app overview screen", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true}) + cy.get(".appTable .app-row-actions button") + .contains("Edit") + .eq(0) + .click({ force: true }) cy.wait(1000) cy.visit(`${Cypress.config().baseUrl}/builder`) - - cy.get(".appTable .name").eq(0).click() - + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) cy.get(".lock-status").eq(0).contains("Locked by you").click() - cy.unlockApp({ owned : true }) + cy.unlockApp({ owned: true }) cy.get(".lock-status").should("not.be.visible") }) @@ -68,136 +61,200 @@ filterTests(['all'], () => { it("Should reflect the deploy state of an app that hasn't been published.", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - - cy.get(".header-right button.spectrum-Button[data-cy='view-app']").should("be.disabled") + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) + cy.get(".header-right button.spectrum-Button[data-cy='view-app']").should( + "be.disabled" + ) cy.get(".spectrum-Tabs-item.is-selected").contains("Overview") cy.get(".overview-tab").should("be.visible") cy.get(".overview-tab [data-cy='app-status']").within(() => { cy.get(".status-display").contains("Unpublished") - cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist") + cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should( + "exist" + ) cy.get(".status-text").contains("-") }) }) it("Should reflect the app deployment state", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true}) - - cy.get(".toprightnav button.spectrum-Button").contains("Publish").click({ force : true }) - cy.get(".spectrum-Modal [data-cy='deploy-app-modal']").should("be.visible") - .within(() => { - cy.get(".spectrum-Button").contains("Publish").click({ force : true }) - cy.wait(1000) - }); + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) + cy.get(".appTable .app-row-actions button") + .contains("Edit") + .eq(0) + .click({ force: true }) + + cy.wait(500) + cy.get(".toprightnav button.spectrum-Button", { timeout: 2000 }) + .contains("Publish") + .click({ force: true }) + cy.get(".spectrum-Modal [data-cy='deploy-app-modal']") + .should("be.visible") + .within(() => { + cy.get(".spectrum-Button").contains("Publish").click({ force: true }) + cy.wait(1000) + }) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - - cy.get(".header-right button.spectrum-Button[data-cy='view-app']").should("not.be.disabled") + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) + cy.get(".header-right button.spectrum-Button[data-cy='view-app']").should( + "not.be.disabled" + ) cy.get(".overview-tab [data-cy='app-status']").within(() => { cy.get(".status-display").contains("Published") - cy.get(".status-display .icon svg[aria-label='GlobeCheck']").should("exist") + cy.get(".status-display .icon svg[aria-label='GlobeCheck']").should( + "exist" + ) cy.get(".status-text").contains("Last published a few seconds ago") }) }) - it("Should reflect an application that has been unpublished", () => { + it("Should reflect an application that has been unpublished", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true}) + cy.get(".appTable .app-row-actions button") + .contains("Edit") + .eq(0) + .click({ force: true }) - cy.get(".deployment-top-nav svg[aria-label='Globe']") - .click({ force: true }) + cy.get(".deployment-top-nav svg[aria-label='Globe']").click({ + force: true, + }) cy.get("[data-cy='publish-popover-menu']").should("be.visible") - cy.get("[data-cy='publish-popover-menu'] [data-cy='publish-popover-action']") - .click({ force : true }) - - cy.get("[data-cy='unpublish-modal']").should("be.visible") - .within(() => { - cy.get(".confirm-wrap button").click({ force: true } - )}) - cy.wait(1000) + cy.get( + "[data-cy='publish-popover-menu'] [data-cy='publish-popover-action']" + ).click({ force: true }) + + cy.get("[data-cy='unpublish-modal']") + .should("be.visible") + .within(() => { + cy.get(".confirm-wrap button").click({ force: true }) + }) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) cy.get(".overview-tab [data-cy='app-status']").within(() => { cy.get(".status-display").contains("Unpublished") - cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist") + cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should( + "exist" + ) cy.get(".status-text").contains("Last published a few seconds ago") }) }) - it("Should allow the editing of the application icon", () => { + it("Should allow the editing of the application icon and colour", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - - cy.get(".appTable .name").eq(0).click() - - cy.get(".app-logo .edit-hover").should("exist").invoke("show").click() - - cy.customiseAppIcon() - - cy.get(".app-logo") - .within(() => { - cy.get('[aria-label]').eq(0).children() - .should('have.attr', 'xlink:href').and('not.contain', '#spectrum-icon-18-Apps') - cy.get(".app-icon") - .should('have.attr', 'style').and('contains', 'color') + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) + cy.get(".edit-hover", { timeout: 1000 }).eq(0).click({ force: true }) + // Select random icon + cy.wait(400) + cy.get(".grid").within(() => { + cy.get(".icon-item") + .eq(Math.floor(Math.random() * 23) + 1) + .click() + }) + // Select random colour + cy.get(".fill").click() + cy.get(".colors").within(() => { + cy.get(".color") + .eq(Math.floor(Math.random() * 33) + 1) + .click() + }) + cy.intercept("**/applications/**").as("iconChange") + cy.get(".spectrum-Button").contains("Save").click({ force: true }) + cy.wait("@iconChange") + cy.get("@iconChange").its("response.statusCode").should("eq", 200) + // Confirm icon has changed from default + // Confirm colour has been applied + cy.get(".spectrum-ActionButton-label").contains("Back").click({ force: true }) + cy.get(".appTable", { timeout: 2000 }).within(() => { + cy.get("[aria-label]") + .eq(0) + .children() + .should("have.attr", "xlink:href") + .and("not.contain", "#spectrum-icon-18-Apps") + cy.get(".title") + .children() + .children() + .should("have.attr", "style") + .and("contains", "color") }) }) it("Should reflect the last time the application was edited", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - - cy.get(".header-right button").contains("Edit").click({ force: true }); + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) + cy.get(".header-right button").contains("Edit").click({ force: true }) cy.navigateToFrontend() - cy.addComponent("Elements", "Headline").then(componentId => { + cy.searchAndAddComponent("Headline").then(componentId => { cy.getComponent(componentId).should("exist") }) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) cy.get(".overview-tab [data-cy='edited-by']").within(() => { cy.get(".editor-name").contains("You") cy.get(".last-edit-text").contains("Last edited a few seconds ago") }) - }); + }) it("Should reflect application version is up-to-date", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) cy.get(".overview-tab [data-cy='app-version']").within(() => { cy.get(".version-status").contains("You're running the latest!") }) - }); + }) it("Should navigate to the settings tab when clicking the App Version card header", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) cy.get(".spectrum-Tabs-item.is-selected").contains("Overview") cy.get(".overview-tab").should("be.visible") - cy.get(".overview-tab [data-cy='app-version'] .dash-card-header").click({ force : true }) + cy.get(".overview-tab [data-cy='app-version'] .dash-card-header").click({ + force: true, + }) cy.get(".spectrum-Tabs-item.is-selected").contains("Settings") cy.get(".settings-tab").should("be.visible") cy.get(".overview-tab").should("not.exist") - - }); + }) it("Should allow the upgrading of an application, if available.", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) cy.wait(500) cy.location().then(loc => { @@ -205,123 +262,162 @@ filterTests(['all'], () => { const appId = params[params.length - 1] cy.log(appId) //Downgrade the app for the test - cy.alterAppVersion(appId, "0.0.1-alpha.0") - .then(()=>{ + cy.alterAppVersion(appId, "0.0.1-alpha.0").then(() => { cy.reload() - cy.wait(1000) cy.log("Current deployment version: " + clientPackage.version) - cy.get(".version-status a").contains("Update").click() + cy.get(".version-status a", { timeout: 5000 }).contains("Update").click() cy.get(".spectrum-Tabs-item.is-selected").contains("Settings") - cy.get(".version-section .page-action button").contains("Update").click({ force: true }) - - cy.intercept('POST', '**/applications/**/client/update').as('updateVersion') - cy.get(".spectrum-Modal.is-open button").contains("Update").click({ force: true }) + cy.get(".version-section .page-action button") + .contains("Update") + .click({ force: true }) + + cy.intercept("POST", "**/applications/**/client/update").as( + "updateVersion" + ) + cy.get(".spectrum-Modal.is-open button") + .contains("Update") + .click({ force: true }) cy.wait("@updateVersion") - .its('response.statusCode').should('eq', 200) - .then(() => { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - - cy.get(".spectrum-Tabs-item").contains("Overview").click({ force: true }) - cy.get(".overview-tab [data-cy='app-version']").within(() => { - cy.get(".spectrum-Heading").contains(clientPackage.version) - cy.get(".version-status").contains("You're running the latest!") + .its("response.statusCode") + .should("eq", 200) + .then(() => { + cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) + cy.get(".spectrum-Tabs-item") + .contains("Overview") + .click({ force: true }) + cy.get(".overview-tab [data-cy='app-version']").within(() => { + cy.get(".spectrum-Heading").contains(clientPackage.version) + cy.get(".version-status").contains("You're running the latest!") + }) }) - }) }) - }); - + }) }) it("Should allow editing of the app details.", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() - + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) cy.get(".spectrum-Tabs-item").contains("Settings").click() cy.get(".spectrum-Tabs-item.is-selected").contains("Settings") cy.get(".settings-tab").should("be.visible") - cy.get(".details-section .page-action button").contains("Edit").click({ force: true }) + cy.get(".details-section .page-action button") + .contains("Edit") + .click({ force: true }) cy.updateAppName("sample name") //publish and check its disabled - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .app-row-actions button").contains("Edit").eq(0).click({force: true}) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) + cy.wait(500) + cy.get(".appTable .app-row-actions button") + .contains("Edit") + .eq(0) + .click({ force: true }) - cy.get(".toprightnav button.spectrum-Button").contains("Publish").click({ force : true }) - cy.get(".spectrum-Modal [data-cy='deploy-app-modal']").should("be.visible") - .within(() => { - cy.get(".spectrum-Button").contains("Publish").click({ force : true }) - cy.wait(1000) - }); + cy.get(".toprightnav button.spectrum-Button") + .contains("Publish") + .click({ force: true }) + cy.get(".spectrum-Modal [data-cy='deploy-app-modal']") + .should("be.visible") + .within(() => { + cy.get(".spectrum-Button").contains("Publish").click({ force: true }) + cy.wait(1000) + }) - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 }) + cy.get(".appTable .app-row-actions button", { timeout: 5000 }) + .contains("Manage") + .eq(0) + .click({ force: true }) cy.get(".spectrum-Tabs-item").contains("Settings").click() cy.get(".spectrum-Tabs-item.is-selected").contains("Settings") cy.get(".details-section .page-action .spectrum-Button").scrollIntoView() - cy.wait(1000) - cy.get(".details-section .page-action .spectrum-Button").should("be.disabled") - + cy.get(".details-section .page-action .spectrum-Button", { timeout: 1000 }).should( + "be.disabled" + ) }) - it("Should allow copying of the published application Id", () => { + xit("Should allow copying of the published application Id", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .app-row-actions").eq(0) - .within(() => { - cy.get(".spectrum-Button").contains("Edit").click({ force: true }) - }) + cy.get(".appTable .app-row-actions") + .eq(0) + .within(() => { + cy.get(".spectrum-Button").contains("Edit").click({ force: true }) + }) cy.publishApp("sample-name") cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) + cy.get(".app-overview-actions-icon > .icon").click({ force: true }) - cy.get(".app-overview-actions-icon > .icon").click({ force : true }) - - cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => { - cy.get(".spectrum-Menu-item").contains("Copy App ID").click({ force: true }) - }) - - cy.get(".spectrum-Toast-content").contains("App ID copied to clipboard.").should("be.visible") + cy.get("[data-cy='app-overview-menu-popover']") + .eq(0) + .within(() => { + cy.get(".spectrum-Menu-item") + .contains("Copy App ID") + .click({ force: true }) + }) + + cy.get(".spectrum-Toast-content") + .contains("App ID copied to clipboard.") + .should("be.visible") }) - it("Should allow unpublishing of the application", () => { + it("Should allow unpublishing of the application via the Unpublish link", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) - cy.get(".app-overview-actions-icon > .icon").click({ force : true }) + cy.get(`[data-cy="app-status"]`).within(() => { + cy.contains("Unpublish").click({ force: true }) + }) - cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => { - cy.get(".spectrum-Menu-item").contains("Unpublish").click({ force: true }) - cy.wait(500) - }) - - cy.get("[data-cy='unpublish-modal']").should("be.visible") - .within(() => { - cy.get(".confirm-wrap button").click({ force: true } - )}) + cy.get("[data-cy='unpublish-modal']") + .should("be.visible") + .within(() => { + cy.get(".confirm-wrap button").click({ force: true }) + }) cy.get(".overview-tab [data-cy='app-status']").within(() => { cy.get(".status-display").contains("Unpublished") - cy.get(".status-display .icon svg[aria-label='GlobeStrike']").should("exist") + cy.get(".status-display .icon svg[aria-label='GlobeStrike']") + .should("exist") }) }) it("Should allow deleting of the application", () => { cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .name").eq(0).click() + cy.get(".appTable .app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) + cy.get(".app-overview-actions-icon > .icon").click({ force: true }) - cy.get(".app-overview-actions-icon > .icon").click({ force : true }) - - cy.get("[data-cy='app-overview-menu-popover']").eq(0).within(() => { - cy.get(".spectrum-Menu-item").contains("Delete").click({ force: true }) - cy.wait(500) - }) + cy.get("[data-cy='app-overview-menu-popover']") + .eq(0) + .within(() => { + cy.get(".spectrum-Menu-item") + .contains("Delete") + .click({ force: true }) + cy.wait(500) + }) //The test application was renamed earlier in the spec cy.get(".spectrum-Dialog-grid").within(() => { @@ -329,18 +425,17 @@ filterTests(['all'], () => { cy.get(".spectrum-Button--warning").click() }) - cy.location().should((loc) => { - expect(loc.pathname).to.eq('/builder/portal/apps') + cy.location().should(loc => { + expect(loc.pathname).to.eq("/builder/portal/apps") }) cy.get(".appTable").should("not.exist") - + cy.get(".welcome .container h1").contains("Let's create your first app!") }) after(() => { cy.deleteAllApps() }) - }) }) diff --git a/packages/builder/cypress/integration/appPublishWorkflow.spec.js b/packages/builder/cypress/integration/appPublishWorkflow.spec.js index fb3c48645f..0e3fbb191b 100644 --- a/packages/builder/cypress/integration/appPublishWorkflow.spec.js +++ b/packages/builder/cypress/integration/appPublishWorkflow.spec.js @@ -1,97 +1,108 @@ import filterTests from "../support/filterTests" +import { APP_TABLE_APP_NAME, DEPLOY_SUCCESS_MODAL } from "../support/interact"; +const interact = require('../support/interact') filterTests(['all'], () => { context("Publish Application Workflow", () => { before(() => { cy.login() - cy.createTestApp() + cy.deleteAllApps() + cy.createApp("Cypress Tests", false) }) it("Should reflect the unpublished status correctly", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) - cy.get(".appTable .app-status").eq(0) + cy.get(interact.APP_TABLE_STATUS, { timeout: 3000 }).eq(0) .within(() => { cy.contains("Unpublished") - cy.get("svg[aria-label='GlobeStrike']").should("exist") + cy.get(interact.GLOBESTRIKE).should("exist") }) - cy.get(".appTable .app-row-actions").eq(0) + cy.get(interact.APP_TABLE_ROW_ACTION).eq(0) .within(() => { - cy.get(".spectrum-Button").contains("View") - cy.get(".spectrum-Button").contains("Edit").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON_TEMPLATE).contains("Edit").click({ force: true }) }) - - cy.get(".deployment-top-nav svg[aria-label='GlobeStrike']").should("exist") - cy.get(".deployment-top-nav svg[aria-label='Globe']").should("not.exist") + + cy.get(interact.DEPLOYMENT_TOP_NAV_GLOBESTRIKE).should("exist") + cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("not.exist") }) it("Should publish an application and correctly reflect that", () => { //Assuming the previous test was run and the unpublished app is open in edit mode. + cy.closeModal() + cy.get(interact.TOPRIGHTNAV_BUTTON_SPECTRUM).contains("Publish").click({ force : true }) - cy.publishApp("cypress-tests") + cy.get(interact.DEPLOY_APP_MODAL).should("be.visible") + .within(() => { + cy.get(interact.SPECTRUM_BUTTON).contains("Publish").click({ force : true }) + }); + + //Verify that the app url is presented correctly to the user + cy.get(interact.DEPLOY_SUCCESS_MODAL, { timeout: 1000 }) + .should("be.visible") + .within(() => { + let appUrl = Cypress.config().baseUrl + '/app/cypress-tests' + cy.get(interact.DEPLOY_APP_URL_INPUT).should('have.value', appUrl) + cy.get(interact.SPECTRUM_BUTTON).contains("Done").click({ force: true }) + }) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) - cy.get(".appTable .app-status").eq(0) + cy.get(interact.APP_TABLE_STATUS, { timeout: 3000 }).eq(0) .within(() => { cy.contains("Published") - cy.get("svg[aria-label='Globe']").should("exist") + cy.get(interact.GLOBE).should("exist") }) - cy.get(".appTable .app-row-actions").eq(0) + cy.get(interact.APP_TABLE_ROW_ACTION).eq(0) .within(() => { - cy.get(".spectrum-Button").contains("View") - cy.get(".spectrum-Button").contains("Edit").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON).contains("Manage") + cy.get(interact.SPECTRUM_BUTTON).contains("Edit").click({ force: true }) }) - cy.get(".deployment-top-nav svg[aria-label='Globe']").should("exist").click({ force: true }) - - cy.get("[data-cy='publish-popover-menu']").should("be.visible") + cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("exist").click({ force: true }) + + cy.get(interact.PUBLISH_POPOVER_MENU).should("be.visible") .within(() => { - cy.get("[data-cy='publish-popover-action']").should("exist") - cy.get("button").contains("View").should("exist") - cy.get(".publish-popover-message").should("have.text", "Last published a few seconds ago") + cy.get(interact.PUBLISH_POPOVER_ACTION).should("exist") + cy.get("button").contains("View app").should("exist") + cy.get(interact.PUBLISH_POPOVER_MESSAGE).should("have.text", "Last published a few seconds ago") }) }) - it("Should unpublish an application from the top navigation and reflect the status change", () => { + it("Should unpublish an application using the link and reflect the status change", () => { //Assuming the previous test app exists and is published - - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.get(".appTable .app-status").eq(0) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) + + cy.get(interact.APP_TABLE_STATUS).eq(0) .within(() => { cy.contains("Published") cy.get("svg[aria-label='Globe']").should("exist") }) - cy.get(".appTable .app-row-actions").eq(0) + cy.get(interact.APP_TABLE).eq(0) .within(() => { - cy.get(".spectrum-Button").contains("View") - cy.get(".spectrum-Button").contains("Edit").click({ force: true }) + cy.get(interact.APP_TABLE_APP_NAME).click({ force: true }) }) - //The published status - cy.get(".deployment-top-nav svg[aria-label='Globe']").should("exist") - .click({ force: true }) - - cy.get("[data-cy='publish-popover-menu']").should("be.visible") - cy.get("[data-cy='publish-popover-menu'] [data-cy='publish-popover-action']") - .click({ force : true }) - - cy.get("[data-cy='unpublish-modal']").should("be.visible") + cy.closeModal() + cy.get(interact.DEPLOYMENT_TOP_GLOBE).should("exist").click({ force: true }) + + cy.get("[data-cy='publish-popover-menu']") .within(() => { - cy.get(".confirm-wrap button").click({ force: true } + cy.get(interact.PUBLISH_POPOVER_ACTION).click({ force: true }) + }) + + cy.get(interact.UNPUBLISH_MODAL).should("be.visible") + .within(() => { + cy.get(interact.CONFIRM_WRAP_BUTTON).click({ force: true } )}) - cy.get(".deployment-top-nav svg[aria-label='GlobeStrike']").should("exist") - - cy.visit(`${Cypress.config().baseUrl}/builder`) - - cy.get(".appTable .app-status").eq(0).contains("Unpublished") + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 6000 }) + cy.wait(500) + cy.get(interact.APP_TABLE_STATUS, { timeout: 10000 }).eq(0).contains("Unpublished") }) }) diff --git a/packages/builder/cypress/integration/autoScreensUI.spec.js b/packages/builder/cypress/integration/autoScreensUI.spec.js index 2c2a43e711..0253675c5b 100644 --- a/packages/builder/cypress/integration/autoScreensUI.spec.js +++ b/packages/builder/cypress/integration/autoScreensUI.spec.js @@ -1,22 +1,19 @@ import filterTests from "../support/filterTests" +const interact = require('../support/interact') filterTests(['smoke', 'all'], () => { context("Auto Screens UI", () => { before(() => { cy.login() + cy.deleteAllApps() }) it("should disable the autogenerated screen options if no sources are available", () => { cy.createApp("First Test App", false) - cy.closeModal(); - cy.contains("Design").click() - cy.get("[aria-label=AddCircle]").click() - cy.get(".spectrum-Modal").within(() => { - cy.get(".item.disabled").contains("Autogenerated screens") - cy.get(".confirm-wrap .spectrum-Button").should('be.disabled') - }) + cy.navigateToAutogeneratedModal() + cy.get(interact.CONFIRM_WRAP_SPE_BUTTON).should('be.disabled') cy.deleteAllApps() }); @@ -26,14 +23,14 @@ filterTests(['smoke', 'all'], () => { cy.selectExternalDatasource("REST") cy.selectExternalDatasource("S3") - cy.get(".spectrum-Modal").within(() => { - cy.get(".spectrum-Button").contains("Save and continue to query").click({ force : true }) + cy.get(interact.SPECTRUM_MODAL).within(() => { + cy.get(interact.SPECTRUM_BUTTON).contains("Save and continue to query").click({ force : true }) }) cy.navigateToAutogeneratedModal() - cy.get('.data-source-entry').should('have.length', 1) - cy.get('.data-source-entry') + cy.get(interact.DATA_SOURCE_ENTRY).should('have.length', 1) + cy.get(interact.DATA_SOURCE_ENTRY) cy.deleteAllApps() }); @@ -43,25 +40,25 @@ filterTests(['smoke', 'all'], () => { // Create Autogenerated screens from the internal table cy.createDatasourceScreen(["Cypress Tests"]) // Confirm screens have been auto generated - cy.get(".nav-items-container").contains("cypress-tests").click({ force: true }) - cy.get(".nav-items-container").should('contain', 'cypress-tests/:id') + cy.get(interact.BODY).should('contain', "cypress-tests") + .and('contain', 'cypress-tests/:id') .and('contain', 'cypress-tests/new/row') }) it("should generate multiple internal table screens at once", () => { - // Create a second internal table const initialTable = "Cypress Tests" const secondTable = "Table Two" + // Create a second internal table cy.createTable(secondTable) // Create Autogenerated screens from the internal tables cy.createDatasourceScreen([initialTable, secondTable]) // Confirm screens have been auto generated - cy.get(".nav-items-container").contains("cypress-tests").click({ force: true }) // Previously generated tables are suffixed with numbers - as expected - cy.get(".nav-items-container").should('contain', 'cypress-tests-2/:id') + cy.get(interact.BODY).should('contain', 'cypress-tests-2') + .and('contain', 'cypress-tests-2/:id') .and('contain', 'cypress-tests-2/new/row') - cy.get(".nav-items-container").contains("table-two").click() - cy.get(".nav-items-container").should('contain', 'table-two/:id') + .and('contain', 'table-two') + .and('contain', 'table-two/:id') .and('contain', 'table-two/new/row') }) @@ -71,31 +68,31 @@ filterTests(['smoke', 'all'], () => { cy.createTable("Table Four") cy.createDatasourceScreen(["Table Three", "Table Four"], "Admin") - cy.get(".nav-items-container").contains("table-three").click() - cy.get(".nav-items-container").should('contain', 'table-three/:id') + // Filter screens to Admin + cy.filterScreensAccessLevel('Admin') + + cy.get(interact.BODY).should('contain', 'table-three') + .and('contain', 'table-three/:id') .and('contain', 'table-three/new/row') - - cy.get(".nav-items-container").contains("table-four").click() - cy.get(".nav-items-container").should('contain', 'table-four/:id') + .and('contain', 'table-four') + .and('contain', 'table-four/:id') .and('contain', 'table-four/new/row') - - //The access level should now be set to admin. Previous screens should be filtered. - cy.get(".nav-items-container").contains("table-two").should('not.exist') - cy.get(".nav-items-container").contains("cypress-tests").should('not.exist') + .and('not.contain', 'table-two') + .and('not.contain', 'cypress-tests') }) if (Cypress.env("TEST_ENV")) { - it("should generate data source screens", () => { - // Using MySQL data source for testing this + it("should generate datasource screens", () => { + // Using MySQL datasource for testing this const datasource = "MySQL" - // Select & configure MySQL data source + // Select & configure MySQL datasource cy.selectExternalDatasource(datasource) cy.addDatasourceConfig(datasource) // Create Autogenerated screens from a MySQL table - MySQL contains books table cy.createDatasourceScreen(["books"]) - cy.get(".nav-items-container").contains("books").click() - cy.get(".nav-items-container").should('contain', 'books/:id') + cy.get(interact.BODY).should('contain', 'books') + .and('contain', 'books/:id') .and('contain', 'books/new/row') }) } diff --git a/packages/builder/cypress/integration/changeAppIconAndColour.spec.js b/packages/builder/cypress/integration/changeAppIconAndColour.spec.js deleted file mode 100644 index 0f623ddb04..0000000000 --- a/packages/builder/cypress/integration/changeAppIconAndColour.spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import filterTests from "../support/filterTests" - -filterTests(['all'], () => { - context("Change Application Icon and Colour", () => { - before(() => { - cy.login() - }) - - it("should change the icon and colour for an application", () => { - // Search for test application - cy.applicationInAppTable("Cypress Tests") - cy.get(".appTable") - .within(() => { - cy.get(".app-row-actions-icon").eq(0).click() - }) - cy.get(".spectrum-Menu").contains("Edit icon").click() - // Select random icon - cy.get(".grid").within(() => { - cy.get(".icon-item").eq(Math.floor(Math.random() * 23) + 1).click() - }) - // Select random colour - cy.get(".fill").click() - cy.get(".colors").within(() => { - cy.get(".color").eq(Math.floor(Math.random() * 33) + 1).click() - }) - cy.intercept('**/applications/**').as('iconChange') - cy.get(".spectrum-Button").contains("Save").click({ force: true }) - cy.wait("@iconChange") - cy.get("@iconChange").its('response.statusCode') - .should('eq', 200) - cy.wait(1000) - // Confirm icon has changed from default - // Confirm colour has been applied - There is no default colour - cy.get(".appTable") - .within(() => { - cy.get('[aria-label]').eq(0).children() - .should('have.attr', 'xlink:href').and('not.contain', '#spectrum-icon-18-Apps') - cy.get(".title").children().children() - .should('have.attr', 'style').and('contains', 'color') - }) - cy.deleteAllApps() - }) - }) -}) diff --git a/packages/builder/cypress/integration/createApp.spec.js b/packages/builder/cypress/integration/createApp.spec.js index ce5e2bd0c2..179741e21a 100644 --- a/packages/builder/cypress/integration/createApp.spec.js +++ b/packages/builder/cypress/integration/createApp.spec.js @@ -6,14 +6,14 @@ filterTests(['smoke', 'all'], () => { before(() => { cy.login() - cy.deleteApp("Cypress Tests") + cy.deleteAllApps() }) if (!(Cypress.env("TEST_ENV"))) { it("should show the new user UI/UX", () => { - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/create`) //added /portal/apps/create - cy.get(interact.CREATE_APP_BUTTON).contains('Start from scratch').should("exist") - cy.get(interact.CREATE_APP_BUTTON).should("exist") + cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/create`, { timeout: 5000 }) //added /portal/apps/create + cy.wait(1000) + cy.get(interact.CREATE_APP_BUTTON, { timeout: 10000 }).contains('Start from scratch').should("exist") cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist") cy.get(interact.TEMPLATE_CATEGORY).should("exist") @@ -23,14 +23,14 @@ filterTests(['smoke', 'all'], () => { } it("should provide filterable templates", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.wait(500) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) .its("body") .then(val => { if (val.length > 0) { - cy.get(interact.SPECTRUM_BUTTON_TEMPLATE).contains("Templates").click({force: true}) + cy.get(interact.SPECTRUM_BUTTON).contains("Templates").click({force: true}) } }) @@ -48,18 +48,11 @@ filterTests(['smoke', 'all'], () => { }) it("should enforce a valid url before submission", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 }) // Start create app process. If apps already exist, click second button - cy.get(interact.CREATE_APP_BUTTON).click({ force: true }) - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(interact.CREATE_APP_BUTTON).click({ force: true }) - } - }) + cy.wait(1000) + cy.get(interact.CREATE_APP_BUTTON, { timeout: 3000 }).click({ force: true }) const appName = "Cypress Tests" cy.get(interact.SPECTRUM_MODAL).within(() => { @@ -92,50 +85,36 @@ filterTests(['smoke', 'all'], () => { it("should create the first application from scratch", () => { const appName = "Cypress Tests" - cy.createApp(appName) + cy.createApp(appName, false) - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.applicationInAppTable(appName) cy.deleteApp(appName) }) it("should create the first application from scratch with a default name", () => { - cy.createApp() - - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) - + cy.updateUserInformation("", "") + cy.createApp("", false) cy.applicationInAppTable("My app") cy.deleteApp("My app") }) it("should create the first application from scratch, using the users first name as the default app name", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) cy.updateUserInformation("Ted", "Userman") - cy.createApp() - - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) - + cy.createApp("", false) cy.applicationInAppTable("Teds app") cy.deleteApp("Teds app") - cy.wait(2000) - //Accomodate names that end in 'S' + // Accomodate names that end in 'S' cy.updateUserInformation("Chris", "Userman") - cy.createApp() - - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) - + cy.createApp("", false) cy.applicationInAppTable("Chris app") cy.deleteApp("Chris app") - cy.wait(2000) cy.updateUserInformation("", "") }) @@ -145,7 +124,7 @@ filterTests(['smoke', 'all'], () => { cy.importApp(exportedApp, "") - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 2000 }) cy.applicationInAppTable("My app") @@ -224,14 +203,12 @@ filterTests(['smoke', 'all'], () => { cy.createApp(appName) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) // Create second app const secondAppName = "Second App Demo" cy.createApp(secondAppName) cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) //Both applications should exist and be searchable cy.searchForApplication(appName) diff --git a/packages/builder/cypress/integration/createAutomation.spec.js b/packages/builder/cypress/integration/createAutomation.spec.js index 69ef3f98a3..b5ff406297 100644 --- a/packages/builder/cypress/integration/createAutomation.spec.js +++ b/packages/builder/cypress/integration/createAutomation.spec.js @@ -1,4 +1,5 @@ import filterTests from "../support/filterTests" +const interact = require('../support/interact') filterTests(['smoke', 'all'], () => { context("Create a automation", () => { @@ -11,51 +12,47 @@ filterTests(['smoke', 'all'], () => { cy.createTestTableWithData() cy.wait(2000) cy.contains("Automate").click() - cy.get(".add-button .spectrum-Icon").click() - cy.get(".modal-inner-wrapper").within(() => { + cy.get(interact.ADD_BUTTON_SPECTRUM).click() + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { cy.get("input").type("Add Row") cy.contains("Row Created").click({ force: true }) - cy.wait(500) - cy.get(".spectrum-Button--cta").click() + cy.get(interact.SPECTRUM_BUTTON_CTA, { timeout: 500 }).click() }) // Setup trigger - cy.get(".spectrum-Picker-label").click() + cy.get(interact.SPECTRUM_PICKER_LABEL).click() cy.wait(500) cy.contains("dog").click() - cy.wait(2000) // Create action - cy.get('[aria-label="AddCircle"]').eq(1).click() - cy.get(".modal-inner-wrapper").within(() => { + cy.get('[aria-label="AddCircle"]', { timeout: 2000 }).eq(1).click() + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { cy.wait(1000) cy.contains("Create Row").trigger('mouseover').click().click() - cy.get(".spectrum-Button--cta").click() + cy.get(interact.SPECTRUM_BUTTON_CTA).click() }) - cy.get(".spectrum-Picker-label").eq(1).click() + cy.get(interact.SPECTRUM_PICKER_LABEL).eq(1).click() cy.contains("dog").click() - cy.get(".spectrum-Textfield-input") + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT) .first() .type("{{ trigger.row.name }}", { parseSpecialCharSequences: false }) - cy.get(".spectrum-Textfield-input") + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT) .eq(1) .type("11") cy.contains("Finish and test automation").click() - cy.get(".modal-inner-wrapper").within(() => { - cy.wait(1000) - cy.get(".spectrum-Picker-label").click() + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { + cy.get(interact.SPECTRUM_PICKER_LABEL, { timeout: 1000 }).click() cy.contains("dog").click() - cy.wait(1000) - cy.get(".spectrum-Textfield-input") + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }) .first() .type("automationGoodboy") - cy.get(".spectrum-Textfield-input") + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT) .eq(1) .type("11") - cy.get(".spectrum-Textfield-input") + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT) .eq(2) .type("123456") - cy.get(".spectrum-Textfield-input") + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT) .eq(3) .type("123456") cy.contains("Test").click() diff --git a/packages/builder/cypress/integration/createBinding.spec.js b/packages/builder/cypress/integration/createBinding.spec.js index 5abe0b27d8..0c1ddf1e7d 100644 --- a/packages/builder/cypress/integration/createBinding.spec.js +++ b/packages/builder/cypress/integration/createBinding.spec.js @@ -9,13 +9,13 @@ filterTests(['smoke', 'all'], () => { }) it("should add a current user binding", () => { - cy.addComponent("Elements", "Paragraph").then(() => { - addSettingBinding("text", "Current User._id") + cy.searchAndAddComponent("Paragraph").then(() => { + addSettingBinding("text", ["Current User", "_id"], "Current User._id") }) }) it("should handle an invalid binding", () => { - cy.addComponent("Elements", "Paragraph").then(componentId => { + cy.searchAndAddComponent("Paragraph").then(componentId => { // Cypress needs to escape curly brackets cy.get("[data-cy=setting-text] input") .type("{{}{{}{{} Current User._id {}}{}}") @@ -27,8 +27,8 @@ filterTests(['smoke', 'all'], () => { xit("should add a URL param binding", () => { const paramName = "foo" cy.createScreen(`/test/:${paramName}`) - cy.addComponent("Elements", "Paragraph").then(componentId => { - addSettingBinding("text", `URL.${paramName}`) + cy.searchAndAddComponent("Paragraph").then(componentId => { + addSettingBinding("text", ["URL", paramName], `URL.${paramName}`) // The builder preview pages don't have a real URL, so all we can do // is check that we were able to bind to the property, and that the // component exists on the page @@ -37,7 +37,7 @@ filterTests(['smoke', 'all'], () => { }) it("should add a binding with a handlebars helper", () => { - cy.addComponent("Elements", "Paragraph").then(componentId => { + cy.searchAndAddComponent("Paragraph").then(componentId => { // Cypress needs to escape curly brackets cy.get("[data-cy=setting-text] input") .type("{{}{{} add 1 2 {}}{}}") @@ -47,11 +47,13 @@ filterTests(['smoke', 'all'], () => { }) }) - const addSettingBinding = (setting, bindingText, clickOption = true) => { + const addSettingBinding = (setting, bindingCategories, bindingText, clickOption = true) => { cy.get(`[data-cy="setting-${setting}"] [data-cy=text-binding-button]`).click() + cy.get(".category-list li").contains(bindingCategories[0]) cy.get(".drawer").within(() => { if (clickOption) { - cy.contains(bindingText).click() + cy.get(".category-list li").contains(bindingCategories[0]).click() + cy.get("li.binding").contains(bindingCategories[1]).click() cy.get("textarea").should("have.value", `{{ ${bindingText} }}`) } else { cy.get("textarea").type(bindingText) diff --git a/packages/builder/cypress/integration/createComponents.spec.js b/packages/builder/cypress/integration/createComponents.spec.js index e13439d9c6..7f29466258 100644 --- a/packages/builder/cypress/integration/createComponents.spec.js +++ b/packages/builder/cypress/integration/createComponents.spec.js @@ -1,8 +1,7 @@ -// TODO for now components are skipped, might not be good to keep doing this - import filterTests from "../support/filterTests" +const interact = require("../support/interact") -filterTests(['all'], () => { +filterTests(["all"], () => { xcontext("Create Components", () => { let headlineId @@ -12,18 +11,33 @@ filterTests(['all'], () => { cy.createTable("dog") cy.addColumn("dog", "name", "Text") cy.addColumn("dog", "age", "Number") - cy.addColumn("dog", "type", "Options") + cy.addColumn("dog", "breed", "Options") + cy.navigateToFrontend() + cy.wait(1000) //allow the iframe some wiggle room }) + //Use the tree to delete a selected component + const deleteSelectedComponent = () => { + cy.get( + ".nav-item.selected .actions > div > .icon" + ).click({ + force: true, + }) + cy.get(".spectrum-Popover.is-open li").contains("Delete").click() + cy.get(".spectrum-Modal button").contains("Delete Component").click({ + force: true, + }) + } + it("should add a container", () => { - cy.addComponent(null, "Container").then(componentId => { + cy.searchAndAddComponent("Container").then(componentId => { cy.getComponent(componentId).should("exist") }) }) it("should add a headline", () => { - cy.addComponent("Elements", "Headline").then(componentId => { + cy.searchAndAddComponent("Headline").then(componentId => { headlineId = componentId cy.getComponent(headlineId).should("exist") }) @@ -31,67 +45,231 @@ filterTests(['all'], () => { it("should change the text of the headline", () => { const text = "Lorem ipsum dolor sit amet." - cy.get("[data-cy=Settings]").click() - cy.get("[data-cy=setting-text] input") - .type(text) - .blur() + cy.get("[data-cy=setting-text] input").type(text).blur() cy.getComponent(headlineId).should("have.text", text) }) it("should change the size of the headline", () => { - cy.get("[data-cy=Design]").click() - cy.contains("Typography").click() - cy.get("[data-cy=font-size-prop-control]").click() - cy.contains("60px").click() - cy.getComponent(headlineId).should("have.css", "font-size", "60px") + cy.get("[data-cy=setting-size]").scrollIntoView().click() + cy.get("[data-cy=setting-size]").within(() => { + cy.get(".spectrum-Form-item li.spectrum-Menu-item") + .contains("3XL") + .click() + }) + + cy.getComponent(headlineId).within(() => { + cy.get(".spectrum-Heading").should("have.css", "font-size", "60px") + }) }) it("should create a form and reset to match schema", () => { - cy.addComponent("Form", "Form").then(() => { - cy.get("[data-cy=Settings]").click() - cy.get("[data-cy=setting-dataSource]") - .contains("Choose option") - .click() - cy.get(".dropdown") - .contains("dog") - .click() - cy.addComponent("Form", "Field Group").then(fieldGroupId => { - cy.get("[data-cy=Settings]").click() - cy.contains("Update Form Fields").click() - cy.get(".modal") - .get("button.primary") + cy.searchAndAddComponent("Form").then(() => { + cy.get("[data-cy=setting-dataSource]").contains("Custom").click() + cy.get(interact.DROPDOWN).contains("dog").click() + cy.wait(500) + cy.searchAndAddComponent("Field Group").then(fieldGroupId => { + cy.contains("Update form fields").click() + cy.get(".spectrum-Modal") + .get(".confirm-wrap .spectrum-Button") .click() + cy.wait(500) cy.getComponent(fieldGroupId).within(() => { cy.contains("name").should("exist") cy.contains("age").should("exist") - cy.contains("type").should("exist") + cy.contains("breed").should("exist") + // cy.contains("image").should("exist") }) + cy.getComponent(fieldGroupId).find("input").should("have.length", 2) cy.getComponent(fieldGroupId) - .find("input") - .should("have.length", 2) - cy.getComponent(fieldGroupId) - .find(".spectrum-Picker") + .find(interact.SPECTRUM_PICKER) .should("have.length", 1) }) }) }) it("deletes a component", () => { - cy.addComponent("Elements", "Paragraph").then(componentId => { - cy.get("[data-cy=setting-_instanceName] input") - .type(componentId) - .blur() - cy.get(".ui-nav ul .nav-item.selected .ri-more-line").click({ + cy.searchAndAddComponent("Paragraph").then(componentId => { + cy.get("[data-cy=setting-_instanceName] input").type(componentId).blur() + cy.get( + ".nav-item.selected .actions > div > .icon" + ).click({ + force: true, + }) + cy.get(".spectrum-Popover.is-open li").contains("Delete").click() + cy.get(".spectrum-Modal button").contains("Delete Component").click({ force: true, }) - cy.get(".dropdown-container") - .contains("Delete") - .click() - cy.get(".modal") - .contains("Delete Component") - .click() cy.getComponent(componentId).should("not.exist") }) }) + + it("should clear the iframe place holder when a form field has been set", () => { + cy.searchAndAddComponent("Form").then(formId => { + //For deletion + cy.get("[data-cy=setting-_instanceName] input") + .clear() + .type(formId) + .blur() + cy.get("[data-cy=setting-dataSource]").contains("Custom").click() + cy.get(".dropdown").contains("dog").click() + + const fieldTypeToColumnName = { + "Text Field": "name", + "Number Field": "age", + "Options Picker": "breed", + } + + const componentTypeLabels = Object.keys(fieldTypeToColumnName) + + const testFieldFocusOnCreate = componentLabel => { + cy.log("Adding: " + componentLabel) + return cy.searchAndAddComponent(componentLabel).then(componentId => { + cy.get("[data-cy=setting-field] button.spectrum-Picker").click() + + //Click the first appropriate field. They are filtered by type + cy.get( + "[data-cy=setting-field] .spectrum-Popover.is-open li.spectrum-Menu-item" + ) + .contains(fieldTypeToColumnName[componentLabel]) + .click() + cy.wait(500) + cy.getComponent(componentId) + .find(".component-placeholder") + .should("not.exist") + }) + } + + cy.wait(500) + cy.wrap(componentTypeLabels) + .each(label => { + return testFieldFocusOnCreate(label) + }) + .then(() => { + cy.get(".nav-item") + .contains(formId) + .click({ force: true }) + deleteSelectedComponent() + }) + }) + }) + + it("should populate the provider for charts with a data provider in its path", () => { + cy.searchAndAddComponent("Data Provider").then(providerId => { + //For deletion + cy.get("[data-cy=setting-_instanceName] input") + .clear() + .type(providerId) + .blur() + cy.get("[data-cy=setting-dataSource]") + .contains("Choose an option") + .click() + cy.get(`[data-cy=dataSource-popover-${providerId}] ul li`) + .contains("dog") + .click() + + const chartTypeLabels = [ + "Bar Chart", + "Line Chart", + "Area Chart", + "Pie Chart", + "Donut Chart", + "Candlestick Chart", + ] + + const testFocusOnCreate = chartLabel => { + cy.log("Adding: " + chartLabel) + cy.searchAndAddComponent(chartLabel).then(componentId => { + cy.get( + "[data-cy=dataProvider-prop-control] .spectrum-Picker" + ).should("not.have.class", "is-focused") + + // Pre populated. + cy.get("[data-cy=dataProvider-prop-control] .spectrum-Picker-label") + .contains(providerId) + .should("exist") + }) + } + cy.wait(1000) + cy.wrap(chartTypeLabels) + .each(label => { + return testFocusOnCreate(label) + }) + .then(() => { + cy.get(".nav-item") + .contains(providerId) + .click({ force: true }) + deleteSelectedComponent() + }) + }) + }) + + it("should replace the placeholder when a url is set on an image", () => { + cy.searchAndAddComponent("Image").then(imageId => { + cy.get("[data-cy=setting-_instanceName] input") + .clear() + .type(imageId) + .blur() + //return $("New Data Provider.Rows")[0]["Attachment"][0]["url"] + //No minio, so just enter something local that will not reslove + cy.get("[data-cy=url-prop-control] input[type=text]") + .type("cypress/fixtures/ghost.png") + .blur() + cy.getComponent(imageId) + .find(".component-placeholder") + .should("not.exist") + cy.getComponent(imageId).find(`img[alt=${imageId}]`).should("exist") + cy.get(".nav-item") + .contains(imageId) + .click({ force: true }) + deleteSelectedComponent() + }) + }) + + it("should add a markdown component.", () => { + cy.searchAndAddComponent("Markdown Viewer").then(markdownId => { + cy.get("[data-cy=setting-_instanceName] input") + .clear() + .type(markdownId) + .blur() + cy.get( + "[data-cy=value-prop-control] input[type=text].spectrum-Textfield-input" + ) + .type("# Hi") + .blur() + cy.getComponent(markdownId) + .find(".component-placeholder") + .should("not.exist") + cy.getComponent(markdownId) + .find(".editor-preview-full h1") + .contains("Hi") + cy.get(".nav-item") + .contains(markdownId) + .click({ force: true }) + deleteSelectedComponent() + }) + }) + + it("should direct the user when adding an Icon component.", () => { + cy.searchAndAddComponent("Icon").then(iconId => { + cy.get("[data-cy=setting-_instanceName] input") + .clear() + .type(iconId) + .blur() + cy.get("[data-cy=icon-prop-control] .spectrum-ActionButton").click() + cy.get("[data-cy=icon-popover].spectrum-Popover.is-open").within(() => { + cy.get(".search-input input").type("save").blur() + cy.get(".search-input button").click({ force: true }) + cy.get(".icon-area .icon-container").eq(0).click({ force: true }) + }) + cy.getComponent(iconId) + .find(".component-placeholder") + .should("not.exist") + cy.getComponent(iconId).find("i.ri-save-fill").should("exist") + cy.get(".nav-item") + .contains(iconId) + .click({ force: true }) + deleteSelectedComponent() + }) + }) }) }) diff --git a/packages/builder/cypress/integration/createScreen.js b/packages/builder/cypress/integration/createScreen.js deleted file mode 100644 index 94a827f26f..0000000000 --- a/packages/builder/cypress/integration/createScreen.js +++ /dev/null @@ -1,41 +0,0 @@ -import filterTests from "../support/filterTests" - -filterTests(["smoke", "all"], () => { - context("Screen Tests", () => { - before(() => { - cy.login() - cy.createTestApp() - cy.navigateToFrontend() - }) - - it("Should successfully create a screen", () => { - cy.createScreen("test") - cy.get(".nav-items-container").within(() => { - cy.contains("/test").should("exist") - }) - }) - - it("Should update the url", () => { - cy.createScreen("test with spaces") - cy.get(".nav-items-container").within(() => { - cy.contains("/test-with-spaces").should("exist") - }) - }) - - it("Should create a blank screen with the selected access level", () => { - cy.createScreen("admin only", "Admin") - - cy.get(".nav-items-container").within(() => { - cy.contains("/admin-only").should("exist") - }) - - cy.createScreen("open to all", "Public") - - cy.get(".nav-items-container").within(() => { - cy.contains("/open-to-all").should("exist") - //The access level should now be set to admin. Previous screens should be filtered. - cy.get(".nav-item").contains("/test-screen").should("not.exist") - }) - }) - }) -}) diff --git a/packages/builder/cypress/integration/createScreen.spec.js b/packages/builder/cypress/integration/createScreen.spec.js new file mode 100644 index 0000000000..a516e279f4 --- /dev/null +++ b/packages/builder/cypress/integration/createScreen.spec.js @@ -0,0 +1,54 @@ +import filterTests from "../support/filterTests" +const interact = require('../support/interact') + +filterTests(["smoke", "all"], () => { + context("Screen Tests", () => { + before(() => { + cy.login() + cy.createTestApp() + cy.navigateToFrontend() + }) + + it("Should successfully create a screen", () => { + cy.createScreen("test") + cy.get(interact.BODY).within(() => { + cy.contains("/test").should("exist") + }) + }) + + it("Should update the url", () => { + cy.createScreen("test with spaces") + cy.get(interact.BODY).within(() => { + cy.contains("/test-with-spaces").should("exist") + }) + }) + + it("should delete all screens then create first screen via button", () => { + cy.deleteAllScreens() + + cy.contains("Create first screen").click() + cy.get(interact.BODY, { timeout: 2000 }).should('contain', '/home') + }) + + it("Should create and filter screens by access level", () => { + const accessLevels = ["Basic", "Admin", "Public", "Power"] + + for (const access of accessLevels){ + // Create screen with specified access level + cy.createScreen(access, access) + // Filter by access level and confirm screen visible + cy.filterScreensAccessLevel(access) + cy.get(interact.BODY).within(() => { + cy.get(interact.NAV_ITEM).should('contain', access.toLowerCase()) + }) + } + + // Filter by All screens - Confirm all screens visible + cy.filterScreensAccessLevel("All screens") + cy.get(interact.BODY).should('contain', accessLevels[0]) + .and('contain', accessLevels[1]) + .and('contain', accessLevels[2]) + .and('contain', accessLevels[3]) + }) + }) +}) diff --git a/packages/builder/cypress/integration/createTable.spec.js b/packages/builder/cypress/integration/createTable.spec.js index 4600807cbc..36d78afb29 100644 --- a/packages/builder/cypress/integration/createTable.spec.js +++ b/packages/builder/cypress/integration/createTable.spec.js @@ -1,4 +1,5 @@ import filterTests from "../support/filterTests" +const interact = require('../support/interact') filterTests(["smoke", "all"], () => { context("Create a Table", () => { @@ -9,9 +10,8 @@ filterTests(["smoke", "all"], () => { it("should create a new Table", () => { cy.createTable("dog") - cy.wait(1000) // Check if Table exists - cy.get(".table-title h1").should("have.text", "dog") + cy.get(interact.TABLE_TITLE_H1, { timeout: 1000 }).should("have.text", "dog") }) it("adds a new column to the table", () => { @@ -25,13 +25,13 @@ filterTests(["smoke", "all"], () => { }) it("updates a column on the table", () => { - cy.get(".title").click() - cy.get(".spectrum-Table-editIcon > use").click() - cy.get(".modal-inner-wrapper").within(() => { + cy.get(interact.TABLE_TITLE).click() + cy.get(interact.SPECTRUM_TABLE_EDIT).click() + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { cy.get("input").eq(0).type("updated", { force: true }) // Unset table display column - cy.get(".spectrum-Switch-input").eq(1).click() + cy.get(interact.SPECTRUM_SWITCH_INPUT).eq(1).click() cy.contains("Save Column").click() }) cy.contains("nameupdated ").should("contain", "nameupdated") @@ -39,17 +39,17 @@ filterTests(["smoke", "all"], () => { it("edits a row", () => { cy.contains("button", "Edit").click({ force: true }) - cy.wait(1000) - cy.get(".spectrum-Modal input").clear() - cy.get(".spectrum-Modal input").type("Updated") + cy.wait(500) + cy.get(interact.SPECTRUM_MODAL_INPUT).clear() + cy.get(interact.SPECTRUM_MODAL_INPUT).type("Updated") cy.contains("Save").click() cy.contains("Updated").should("have.text", "Updated") }) it("deletes a row", () => { - cy.get(".spectrum-Checkbox-input").check({ force: true }) - cy.contains("Delete 1 row(s)").click() - cy.get(".spectrum-Modal").contains("Delete").click() + cy.get(interact.SPECTRUM_CHECKBOX_INPUT).check({ force: true }) + cy.contains("Delete 1 row").click() + cy.get(interact.SPECTRUM_MODAL).contains("Delete").click() cy.contains("RoverUpdated").should("not.exist") }) @@ -62,51 +62,49 @@ filterTests(["smoke", "all"], () => { cy.addRow([i]) } cy.reload() - cy.wait(2000) - cy.get(".spectrum-Pagination").within(() => { - cy.get(".spectrum-ActionButton").eq(1).click() + cy.get(interact.SPECTRUM_PAGINATION, { timeout: 2000 }).within(() => { + cy.get(interact.SPECTRUM_ACTION_BUTTON).eq(1).click() }) - cy.get(".spectrum-Pagination").within(() => { - cy.get(".spectrum-Body--secondary").contains("Page 2") + cy.get(interact.SPECTRUM_PAGINATION).within(() => { + cy.get(interact.SPECTRUM_BODY_SECOND).contains("Page 2") }) }) xit("Deletes rows and checks pagination", () => { // Delete rows, removing second page from table - cy.get(".spectrum-Checkbox-input").check({ force: true }) - cy.get(".popovers").within(() => { - cy.get(".spectrum-Button").click({ force: true }) + cy.get(interact.SPECTRUM_CHECKBOX_INPUT).check({ force: true }) + cy.get(interact.POPOVERS).within(() => { + cy.get(interact.SPECTRUM_BUTTON).click({ force: true }) }) - cy.get(".spectrum-Dialog-grid").contains("Delete").click({ force: true }) - cy.wait(1000) + cy.get(interact.SPECTRUM_DIALOG_GRID).contains("Delete").click({ force: true }) // Confirm table only has one page - cy.get(".spectrum-Pagination").within(() => { - cy.get(".spectrum-ActionButton").eq(1).should("not.be.enabled") + cy.get(interact.SPECTRUM_PAGINATION, { timeout: 1000 }).within(() => { + cy.get(interact.SPECTRUM_ACTION_BUTTON).eq(1).should("not.be.enabled") }) }) } it("deletes a column", () => { const columnName = "nameupdated" - cy.get(".title").click() - cy.get(".spectrum-Table-editIcon > use").click() + cy.get(interact.TABLE_TITLE).click() + cy.get(interact.SPECTRUM_TABLE_EDIT).click() cy.contains("Delete").click() - cy.get('[data-cy="delete-column-confirm"]').type(columnName) + cy.get(interact.DELETE_COLUMN_CONFIRM).type(columnName) cy.contains("Delete Column").click() cy.contains("nameupdated").should("not.exist") }) it("deletes a table", () => { - cy.get(".nav-item") + cy.get(interact.NAV_ITEM) .contains("dog") - .parents(".nav-item") + .parents(interact.NAV_ITEM) .first() .within(() => { - cy.get(".actions .spectrum-Icon").click({ force: true }) + cy.get(interact.ACTION_SPECTRUM_ICON).click({ force: true }) }) - cy.get(".spectrum-Menu > :nth-child(2)").click() - cy.get('[data-cy="delete-table-confirm"]').type("dog") + cy.get(interact.SPECTRUM_MENU_CHILD2).click() + cy.get(interact.DELETE_TABLE_CONFIRM).type("dog") cy.contains("Delete Table").click() cy.contains("dog").should("not.exist") }) diff --git a/packages/builder/cypress/integration/createUserAndRoles.spec.js b/packages/builder/cypress/integration/createUserAndRoles.spec.js deleted file mode 100644 index ac7ec1b5fd..0000000000 --- a/packages/builder/cypress/integration/createUserAndRoles.spec.js +++ /dev/null @@ -1,194 +0,0 @@ -import filterTests from "../support/filterTests" - -filterTests(["smoke", "all"], () => { - context("Create a User and Assign Roles", () => { - before(() => { - cy.login() - cy.deleteApp("Cypress Tests") - cy.createApp("Cypress Tests") - }) - - it("should create a user", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) - cy.createUser("bbuser@test.com") - cy.get(".spectrum-Table").should("contain", "bbuser") - }) - - it("should confirm there is No Access for a New User", () => { - // Click into the user - cy.contains("bbuser").click() - cy.wait(500) - // Get No Access table - Confirm it has apps in it - cy.get(".spectrum-Table").eq(1).should("not.contain", "No rows found") - // Get Configure Roles table - Confirm it has no apps - cy.get(".spectrum-Table").eq(0).contains("No rows found") - }) - - if (Cypress.env("TEST_ENV")) { - it("should assign role types", () => { - // 3 apps minimum required - to assign an app to each role type - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length < 3) { - for (let i = 1; i < 3; i++) { - const uuid = () => Cypress._.random(0, 1e6) - const name = uuid() - if(i < 1){ - cy.createApp(name) - } else { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) - cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) - cy.createAppFromScratch(name) - } - } - } - }) - // Navigate back to the user - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) - cy.get(".spectrum-SideNav").contains("Users").click() - cy.wait(500) - cy.get(".spectrum-Table").contains("bbuser").click() - cy.wait(1000) - for (let i = 0; i < 3; i++) { - cy.get(".spectrum-Table", { timeout: 3000}) - .eq(1) - .find(".spectrum-Table-row") - .eq(0) - .find(".spectrum-Table-cell") - .eq(0) - .click() - cy.wait(500) - cy.get(".spectrum-Dialog-grid") - .contains("Choose an option") - .click() - .then(() => { - cy.wait(1000) - if (i == 0) { - cy.get(".spectrum-Menu").contains("Admin").click({ force: true }) - } - else if (i == 1) { - cy.get(".spectrum-Menu").contains("Power").click({ force: true }) - } - else if (i == 2) { - cy.get(".spectrum-Menu").contains("Basic").click({ force: true }) - } - cy.wait(1000) - cy.get(".spectrum-Button") - .contains("Update role") - .click({ force: true }) - }) - cy.reload() - } - // Confirm roles exist within Configure roles table - cy.wait(2000) - cy.get(".spectrum-Table") - .eq(0) - .within(assginedRoles => { - expect(assginedRoles).to.contain("Admin") - expect(assginedRoles).to.contain("Power") - expect(assginedRoles).to.contain("Basic") - }) - }) - - it("should unassign role types", () => { - // Set each app within Configure roles table to 'No Access' - cy.get(".spectrum-Table") - .eq(0) - .find(".spectrum-Table-row") - .its("length") - .then(len => { - for (let i = 0; i < len; i++) { - cy.get(".spectrum-Table") - .eq(0) - .find(".spectrum-Table-row") - .eq(0) - .find(".spectrum-Table-cell") - .eq(0) - .click() - .then(() => { - cy.get(".spectrum-Picker").eq(1).click({ force: true }) - cy.wait(500) - cy.get(".spectrum-Popover").contains("No Access").click() - }) - cy.get(".spectrum-Button") - .contains("Update role") - .click({ force: true }) - cy.wait(1000) - } - }) - // Confirm Configure roles table no longer has any apps in it - cy.get(".spectrum-Table").eq(0).contains("No rows found") - }) - } - - it("should enable Developer access", () => { - // Enable Developer access - cy.get(".field") - .eq(4) - .within(() => { - cy.get(".spectrum-Switch-input").click({ force: true }) - }) - // No Access table should now be empty - cy.get(".container") - .contains("No Access") - .parent() - .within(() => { - cy.get(".spectrum-Table").contains("No rows found") - }) - - // Each app within Configure roles should have Admin access - cy.get(".spectrum-Table") - .eq(0) - .find(".spectrum-Table-row") - .its("length") - .then(len => { - for (let i = 0; i < len; i++) { - cy.get(".spectrum-Table") - .eq(0) - .find(".spectrum-Table-row") - .eq(i) - .contains("Admin") - cy.wait(500) - } - }) - }) - - it("should disable Developer access", () => { - // Disable Developer access - cy.get(".field") - .eq(4) - .within(() => { - cy.get(".spectrum-Switch-input").click({ force: true }) - }) - // Configure roles table should now be empty - cy.get(".container") - .contains("Configure roles") - .parent() - .within(() => { - cy.get(".spectrum-Table").contains("No rows found") - }) - }) - - it("should delete a user", () => { - // Click Delete user button - cy.get(".spectrum-Button") - .contains("Delete user") - .click({ force: true }) - .then(() => { - // Confirm deletion within modal - cy.wait(500) - cy.get(".spectrum-Dialog-grid").within(() => { - cy.get(".spectrum-Button") - .contains("Delete user") - .click({ force: true }) - cy.wait(4000) - }) - }) - cy.get(".spectrum-Table").should("not.have.text", "bbuser") - }) - }) -}) diff --git a/packages/builder/cypress/integration/createView.spec.js b/packages/builder/cypress/integration/createView.spec.js index feaf1c3b5f..9adc486f70 100644 --- a/packages/builder/cypress/integration/createView.spec.js +++ b/packages/builder/cypress/integration/createView.spec.js @@ -1,10 +1,10 @@ import filterTests from "../support/filterTests" +const interact = require('../support/interact') filterTests(['smoke', 'all'], () => { context("Create a View", () => { before(() => { cy.login() - cy.createTestApp() cy.createTable("data") cy.addColumn("data", "group", "Text") @@ -22,12 +22,12 @@ filterTests(['smoke', 'all'], () => { it("creates a view", () => { cy.contains("Create view").click() - cy.get(".modal-inner-wrapper").within(() => { + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { cy.get("input").type("Test View") cy.get("button").contains("Create View").click({ force: true }) }) - cy.get(".table-title h1").contains("Test View") - cy.get(".title").then($headers => { + cy.get(interact.TABLE_TITLE_H1).contains("Test View") + cy.get(interact.TITLE).then($headers => { expect($headers).to.have.length(3) const headers = Array.from($headers).map(header => header.textContent.trim() @@ -40,18 +40,18 @@ filterTests(['smoke', 'all'], () => { cy.contains("Filter").click() cy.contains("Add Filter").click() - cy.get(".modal-inner-wrapper").within(() => { - cy.get(".spectrum-Picker-label").eq(0).click() + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { + cy.get(interact.SPECTRUM_PICKER_LABEL).eq(0).click() cy.contains("age").click({ force: true }) - cy.get(".spectrum-Picker-label").eq(1).click() + cy.get(interact.SPECTRUM_PICKER_LABEL).eq(1).click() cy.contains("More Than").click({ force: true }) cy.get("input").type(18) cy.contains("Save").click() }) - cy.get(".spectrum-Table-row").get($values => { + cy.get(interact.SPECTRUM_TABLE_ROW).get($values => { expect($values).to.have.length(5) }) }) @@ -59,18 +59,18 @@ filterTests(['smoke', 'all'], () => { it("creates a stats calculation view based on age", () => { cy.wait(1000) cy.contains("Calculate").click() - cy.get(".modal-inner-wrapper").within(() => { - cy.get(".spectrum-Picker-label").eq(0).click() + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { + cy.get(interact.SPECTRUM_PICKER_LABEL).eq(0).click() cy.contains("Statistics").click() - cy.get(".spectrum-Picker-label").eq(1).click() + cy.get(interact.SPECTRUM_PICKER_LABEL).eq(1).click() cy.contains("age").click({ force: true }) - cy.get(".spectrum-Button").contains("Save").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON).contains("Save").click({ force: true }) }) - cy.wait(1000) - cy.get(".title").then($headers => { + cy.wait(1000) + cy.get(interact.TITLE).then($headers => { expect($headers).to.have.length(7) const headers = Array.from($headers).map(header => header.textContent.trim() @@ -85,7 +85,7 @@ filterTests(['smoke', 'all'], () => { "avg", ]) }) - cy.get(".spectrum-Table-cell").then($values => { + cy.get(interact.SPECTRUM_TABLE_CELL).then($values => { let values = Array.from($values).map(header => header.textContent.trim()) expect(values).to.deep.eq(["age", "155", "20", "49", "5", "5347", "31"]) }) @@ -93,8 +93,8 @@ filterTests(['smoke', 'all'], () => { it("groups the view by group", () => { cy.contains("Group by").click() - cy.get(".modal-inner-wrapper").within(() => { - cy.get(".spectrum-Picker-label").eq(0).click() + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { + cy.get(interact.SPECTRUM_PICKER_LABEL).eq(0).click() cy.contains("group").click() cy.contains("Save").click() }) @@ -102,7 +102,7 @@ filterTests(['smoke', 'all'], () => { cy.contains("Students").should("be.visible") cy.contains("Teachers").should("be.visible") - cy.get(".spectrum-Table-cell").then($values => { + cy.get(interact.SPECTRUM_TABLE_CELL).then($values => { let values = Array.from($values).map(header => header.textContent.trim()) expect(values).to.deep.eq([ "Students", @@ -124,11 +124,11 @@ filterTests(['smoke', 'all'], () => { }) it("renames a view", () => { - cy.contains(".nav-item", "Test View") + cy.contains(interact.NAV_ITEM, "Test View") .find(".actions .icon.open-popover") .click({ force: true }) - cy.get(".spectrum-Menu-itemLabel").contains("Edit").click() - cy.get(".modal-inner-wrapper").within(() => { + cy.get(interact.SPECTRUM_MENU_ITEM_LABEL).contains("Edit").click() + cy.get(interact.MODAL_INNER_WRAPPER).within(() => { cy.get("input").type(" Updated") cy.contains("Save").click() }) @@ -137,7 +137,7 @@ filterTests(['smoke', 'all'], () => { }) it("deletes a view", () => { - cy.contains(".nav-item", "Test View Updated") + cy.contains(interact.NAV_ITEM, "Test View Updated") .find(".actions .icon.open-popover") .click({ force: true }) cy.contains("Delete").click() diff --git a/packages/builder/cypress/integration/customThemingProperties.spec.js b/packages/builder/cypress/integration/customThemingProperties.spec.js index ed3478ca67..e9de0985d0 100644 --- a/packages/builder/cypress/integration/customThemingProperties.spec.js +++ b/packages/builder/cypress/integration/customThemingProperties.spec.js @@ -34,7 +34,6 @@ filterTests(['all'], () => { Large = 16px */ it("should test button roundness", () => { const buttonRoundnessValues = ["0", "4px", "8px", "16px"] - cy.wait(1000) // Add button, change roundness and confirm value cy.addComponent("Button", null).then((componentId) => { buttonRoundnessValues.forEach(function (item, index){ diff --git a/packages/builder/cypress/integration/datasources/datasourceWizard.spec.js b/packages/builder/cypress/integration/datasources/datasourceWizard.spec.js index 1bee7b5ec1..837a433951 100644 --- a/packages/builder/cypress/integration/datasources/datasourceWizard.spec.js +++ b/packages/builder/cypress/integration/datasources/datasourceWizard.spec.js @@ -1,7 +1,7 @@ import filterTests from "../../support/filterTests" filterTests(['all'], () => { - context("Datasource Wizard", () => { + xcontext("Datasource Wizard", () => { if (Cypress.env("TEST_ENV")) { before(() => { cy.login() @@ -17,11 +17,10 @@ filterTests(['all'], () => { // Navigate back within datasource wizard cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Button").contains("Back").click({ force: true }) - cy.wait(1000) }) // Select PostgreSQL datasource again - cy.get(".item-list").contains(datasource).click() + cy.get(".item-list", { timeout: 1000 }).contains(datasource).click() cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Button").contains("Continue").click({ force: true }) }) diff --git a/packages/builder/cypress/integration/datasources/mySql.spec.js b/packages/builder/cypress/integration/datasources/mySql.spec.js index 98bb2f2acf..33aa72f0bb 100644 --- a/packages/builder/cypress/integration/datasources/mySql.spec.js +++ b/packages/builder/cypress/integration/datasources/mySql.spec.js @@ -11,8 +11,8 @@ filterTests(["all"], () => { const queryName = "Cypress Test Query" const queryRename = "CT Query Rename" - it("Should add MySQL data source without configuration", () => { - // Select MySQL data source + it("Should add MySQL datasource without configuration", () => { + // Select MySQL datasource cy.selectExternalDatasource(datasource) // Attempt to fetch tables without applying configuration cy.intercept("**/datasources").as("datasource") @@ -35,8 +35,8 @@ filterTests(["all"], () => { cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true }) }) - it("should add MySQL data source and fetch tables", () => { - // Add & configure MySQL data source + it("should add MySQL datasource and fetch tables", () => { + // Add & configure MySQL datasource cy.selectExternalDatasource(datasource) cy.intercept("**/datasources").as("datasource") cy.addDatasourceConfig(datasource) @@ -52,7 +52,7 @@ filterTests(["all"], () => { }) it("should check table fetching error", () => { - // MySQL test data source contains tables without primary keys + // MySQL test datasource contains tables without primary keys cy.get(".spectrum-InLineAlert") .should("contain", "Error fetching tables") .and("contain", "No primary key constraint found") @@ -114,7 +114,7 @@ filterTests(["all"], () => { cy.wait(1000) }) // Confirm table length & relationship name - cy.get(".spectrum-Table") + cy.get(".spectrum-Table", { timeout: 1000 }) .eq(1) .find(".spectrum-Table-row") .its("length") @@ -136,15 +136,15 @@ filterTests(["all"], () => { cy.get(".spectrum-Table") .eq(1) .within(() => { - cy.get(".spectrum-Table-row").eq(0).click({ force: true }) - cy.wait(500) + cy.get(".spectrum-Table-cell").eq(0).click({ force: true }) }) - cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { cy.get(".spectrum-Button") .contains("Delete") .click({ force: true }) }) cy.reload() + cy.wait(500) } // Confirm relationships no longer exist cy.get(".spectrum-Body").should( @@ -175,7 +175,10 @@ filterTests(["all"], () => { cy.get("@query").its("response.statusCode").should("eq", 200) cy.get("@query").its("response.body").should("not.be.empty") // Save query + cy.intercept("POST", "**/queries").as("saveQuery") cy.get(".spectrum-Button").contains("Save Query").click({ force: true }) + cy.wait("@saveQuery") + cy.get("@saveQuery").its("response.statusCode").should("eq", 200) cy.get(".nav-item").should("contain", queryName) }) @@ -199,15 +202,16 @@ filterTests(["all"], () => { .within(() => { cy.get("input").clear().type(queryRename) }) - // Save query - cy.get(".spectrum-Button").contains("Save Query").click({ force: true }) + // Click on a nav item + cy.get(".nav-item").first().click() + // Confirm name change cy.get(".nav-item").should("contain", queryRename) }) it("should delete a query", () => { // Get query nav item - QueryName cy.get(".nav-item") - .contains(queryName) + .contains(queryRename) .parent() .within(() => { cy.get(".spectrum-Icon").eq(1).click({ force: true }) @@ -217,9 +221,8 @@ filterTests(["all"], () => { cy.get(".spectrum-Button") .contains("Delete Query") .click({ force: true }) - cy.wait(1000) // Confirm deletion - cy.get(".nav-item").should("not.contain", queryName) + cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryRename) }) } }) diff --git a/packages/builder/cypress/integration/datasources/oracle.spec.js b/packages/builder/cypress/integration/datasources/oracle.spec.js index 4c4d33d654..ae1ca5cd75 100644 --- a/packages/builder/cypress/integration/datasources/oracle.spec.js +++ b/packages/builder/cypress/integration/datasources/oracle.spec.js @@ -1,7 +1,7 @@ import filterTests from "../../support/filterTests" filterTests(["all"], () => { - context("Oracle Datasource Testing", () => { + xcontext("Oracle Datasource Testing", () => { if (Cypress.env("TEST_ENV")) { before(() => { cy.login() @@ -11,8 +11,8 @@ filterTests(["all"], () => { const queryName = "Cypress Test Query" const queryRename = "CT Query Rename" - it("Should add Oracle data source and skip table fetch", () => { - // Select Oracle data source + it("Should add Oracle datasource and skip table fetch", () => { + // Select Oracle datasource cy.selectExternalDatasource(datasource) // Skip table fetch - no config added cy.get(".spectrum-Button") @@ -20,10 +20,10 @@ filterTests(["all"], () => { .click({ force: true }) cy.wait(500) // Confirm config contains localhost - cy.get(".spectrum-Textfield-input") + cy.get(".spectrum-Textfield-input", { timeout: 500 }) .eq(1) .should("have.value", "localhost") - // Add another Oracle data source, configure & skip table fetch + // Add another Oracle datasource, configure & skip table fetch cy.selectExternalDatasource(datasource) cy.addDatasourceConfig(datasource, true) // Confirm config and no tables @@ -33,8 +33,8 @@ filterTests(["all"], () => { cy.get(".spectrum-Body").eq(2).should("contain", "No tables found.") }) - it("Should add Oracle data source and fetch tables without configuration", () => { - // Select Oracle data source + it("Should add Oracle datasource and fetch tables without configuration", () => { + // Select Oracle datasource cy.selectExternalDatasource(datasource) // Attempt to fetch tables without applying configuration cy.intercept("**/datasources").as("datasource") @@ -49,8 +49,8 @@ filterTests(["all"], () => { cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true }) }) - xit("should add Oracle data source and fetch tables", () => { - // Add & configure Oracle data source + xit("should add Oracle datasource and fetch tables", () => { + // Add & configure Oracle datasource cy.selectExternalDatasource(datasource) cy.intercept("**/datasources").as("datasource") cy.addDatasourceConfig(datasource) @@ -140,9 +140,8 @@ filterTests(["all"], () => { .eq(1) .within(() => { cy.get(".spectrum-Table-row").eq(0).click() - cy.wait(500) }) - cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { cy.get(".spectrum-Button") .contains("Delete") .click({ force: true }) @@ -221,10 +220,9 @@ filterTests(["all"], () => { cy.get(".spectrum-Button") .contains("Delete Query") .click({ force: true }) - cy.wait(1000) // Confirm deletion - cy.get(".nav-item").should("not.contain", queryName) + cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryName) }) } }) diff --git a/packages/builder/cypress/integration/datasources/postgreSql.spec.js b/packages/builder/cypress/integration/datasources/postgreSql.spec.js index 7448e6b27d..8ef574566e 100644 --- a/packages/builder/cypress/integration/datasources/postgreSql.spec.js +++ b/packages/builder/cypress/integration/datasources/postgreSql.spec.js @@ -11,8 +11,8 @@ filterTests(["all"], () => { const queryName = "Cypress Test Query" const queryRename = "CT Query Rename" - xit("Should add PostgreSQL data source without configuration", () => { - // Select PostgreSQL data source + xit("Should add PostgreSQL datasource without configuration", () => { + // Select PostgreSQL datasource cy.selectExternalDatasource(datasource) // Attempt to fetch tables without applying configuration cy.intercept("**/datasources").as("datasource") @@ -27,14 +27,15 @@ filterTests(["all"], () => { cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true }) }) - it("should add PostgreSQL data source and fetch tables", () => { - // Add & configure PostgreSQL data source + it("should add PostgreSQL datasource and fetch tables", () => { + // Add & configure PostgreSQL datasource cy.selectExternalDatasource(datasource) cy.intercept("**/datasources").as("datasource") cy.addDatasourceConfig(datasource) // Check response from datasource after adding configuration cy.wait("@datasource") cy.get("@datasource").its("response.statusCode").should("eq", 200) + cy.wait(2000) // Confirm fetch tables was successful cy.get(".spectrum-Table") .eq(0) @@ -107,19 +108,19 @@ filterTests(["all"], () => { }) it("should delete a relationship", () => { - cy.get(".hierarchy-items-container").contains("PostgreSQL").click() + cy.get(".hierarchy-items-container").contains("PostgreSQL").click({ force: true }) cy.reload() // Delete one relationship cy.get(".spectrum-Table") .eq(1) .within(() => { - cy.get(".spectrum-Table-row").eq(0).click({ force: true }) - cy.wait(500) + cy.get(".spectrum-Table-cell").eq(0).click({ force: true }) }) - cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { cy.get(".spectrum-Button").contains("Delete").click({ force: true }) }) cy.reload() + cy.wait(500) // Confirm relationship was deleted cy.get(".spectrum-Table") .eq(1) @@ -149,17 +150,19 @@ filterTests(["all"], () => { cy.get("@query").its("response.statusCode").should("eq", 200) cy.get("@query").its("response.body").should("not.be.empty") // Save query + cy.intercept("**/queries").as("saveQuery") cy.get(".spectrum-Button").contains("Save Query").click({ force: true }) - cy.get(".hierarchy-items-container").should("contain", queryName) + cy.wait("@saveQuery") + cy.get(".spectrum-Tabs-content", { timeout: 2000 }).should("contain", queryName) }) it("should switch to schema with no tables", () => { // Switch Schema - To one without any tables - cy.get(".hierarchy-items-container").contains("PostgreSQL").click() + cy.get(".hierarchy-items-container").contains("PostgreSQL").click({ force: true }) switchSchema("randomText") // No tables displayed - cy.get(".spectrum-Body").eq(2).should("contain", "No tables found") + cy.get(".spectrum-Body", { timeout: 10000 }).eq(2, { timeout: 10000 }).should("contain", "No tables found") // Previously created query should be visible cy.get(".spectrum-Table").should("contain", queryName) @@ -170,7 +173,7 @@ filterTests(["all"], () => { switchSchema("1") // Confirm tables exist - Check for specific one - cy.get(".spectrum-Table").eq(0).should("contain", "test") + cy.get(".spectrum-Table", { timeout: 20000 }).eq(0).should("contain", "test") cy.get(".spectrum-Table") .eq(0) .find(".spectrum-Table-row") @@ -184,7 +187,7 @@ filterTests(["all"], () => { switchSchema("public") // Confirm tables exist - again - cy.get(".spectrum-Table").eq(0).should("contain", "REGIONS") + cy.get(".spectrum-Table", { timeout: 20000 }).eq(0).should("contain", "REGIONS") cy.get(".spectrum-Table") .eq(0) .find(".spectrum-Table-row") @@ -216,28 +219,29 @@ filterTests(["all"], () => { it("should edit a query name", () => { // Access query - cy.get(".hierarchy-items-container") - .contains(queryName + " (1)") - .click() + cy.get(".hierarchy-items-container", { timeout: 2000 }) + //.contains(queryName + " (1)") + .contains(queryName) + .click({ force: true }) // Rename query - cy.get(".spectrum-Form-item") + cy.wait(1000) + cy.get(".spectrum-Form-item", { timeout: 2000 }) .eq(0) .within(() => { cy.get("input").clear().type(queryRename) }) - // Run and Save query - cy.get(".spectrum-Button").contains("Run Query").click({ force: true }) - cy.wait(500) - cy.get(".spectrum-Button").contains("Save Query").click({ force: true }) + // Click on a nav item and confirm name change + cy.get(".nav-item").first().click() + // Confirm name change cy.get(".nav-item").should("contain", queryRename) }) it("should delete a query", () => { // Get query nav item - QueryName cy.get(".nav-item") - .contains(queryName) + .contains(queryRename) .parent() .within(() => { cy.get(".spectrum-Icon").eq(1).click({ force: true }) @@ -247,9 +251,10 @@ filterTests(["all"], () => { cy.get(".spectrum-Button") .contains("Delete Query") .click({ force: true }) - cy.wait(1000) // Confirm deletion - cy.get(".nav-item").should("not.contain", queryName) + cy.reload() + cy.get(".nav-item", { timeout: 30000 }).contains(datasource).click({ force: true }) + cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryRename) }) const switchSchema = schema => { @@ -271,7 +276,7 @@ filterTests(["all"], () => { .click({ force: true }) }) cy.reload() - cy.wait(5000) + cy.wait(1000) } } }) diff --git a/packages/builder/cypress/integration/datasources/rest.spec.js b/packages/builder/cypress/integration/datasources/rest.spec.js index a15487c01b..ec9864a47d 100644 --- a/packages/builder/cypress/integration/datasources/rest.spec.js +++ b/packages/builder/cypress/integration/datasources/rest.spec.js @@ -10,12 +10,11 @@ filterTests(["smoke", "all"], () => { const datasource = "REST" const restUrl = "https://api.openbrewerydb.org/breweries" - it("Should add REST data source with incorrect API", () => { - // Select REST data source + it("Should add REST datasource with incorrect API", () => { + // Select REST datasource cy.selectExternalDatasource(datasource) // Enter incorrect api & attempt to send query - cy.wait(500) - cy.get(".spectrum-Button").contains("Add query").click({ force: true }) + cy.get(".query-buttons", { timeout: 1000 }).contains("Add query").click({ force: true }) cy.intercept("**/preview").as("queryError") cy.get("input").clear().type("random text") cy.get(".spectrum-Button").contains("Send").click({ force: true }) @@ -36,8 +35,7 @@ filterTests(["smoke", "all"], () => { // createRestQuery confirms query creation cy.createRestQuery("GET", restUrl, "/breweries") // Confirm status code response within REST datasource - cy.wait(1000) - cy.get(".stats").within(() => { + cy.get(".stats", { timeout: 1000 }).within(() => { cy.get(".spectrum-FieldLabel") .eq(0) .should("contain", 200) diff --git a/packages/builder/cypress/integration/queryLevelTransformers.spec.js b/packages/builder/cypress/integration/queryLevelTransformers.spec.js index bec0825e70..2b74e0c2e5 100644 --- a/packages/builder/cypress/integration/queryLevelTransformers.spec.js +++ b/packages/builder/cypress/integration/queryLevelTransformers.spec.js @@ -1,4 +1,5 @@ import filterTests from "../support/filterTests" +const interact = require('../support/interact') filterTests(["smoke", "all"], () => { context("Query Level Transformers", () => { @@ -13,11 +14,11 @@ filterTests(["smoke", "all"], () => { const restUrl = "https://api.openbrewerydb.org/breweries" cy.selectExternalDatasource(datasource) cy.createRestQuery("GET", restUrl, "/breweries") - cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click() + cy.get(interact.SPECTRUM_TABS_ITEM).contains("Transformer").click() // Get Transformer Function from file cy.readFile("cypress/support/queryLevelTransformerFunction.js").then( transformerFunction => { - cy.get(".CodeMirror textarea") + cy.get(interact.CODEMIRROR_TEXTAREA) // Highlight current text and overwrite with file contents .type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", { force: true, @@ -27,7 +28,7 @@ filterTests(["smoke", "all"], () => { ) // Send Query cy.intercept("**/queries/preview").as("query") - cy.get(".spectrum-Button").contains("Send").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON).contains("Send").click({ force: true }) cy.wait("@query") // Assert against Status Code, body, & body rows cy.get("@query").its("response.statusCode").should("eq", 200) @@ -41,13 +42,13 @@ filterTests(["smoke", "all"], () => { const restUrl = "https://api.openbrewerydb.org/breweries" cy.selectExternalDatasource(datasource) cy.createRestQuery("GET", restUrl, "/breweries") - cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click() + cy.get(interact.SPECTRUM_TABS_ITEM).contains("Transformer").click() // Get Transformer Function with Data from file cy.readFile( "cypress/support/queryLevelTransformerFunctionWithData.js" ).then(transformerFunction => { //console.log(transformerFunction[1]) - cy.get(".CodeMirror textarea") + cy.get(interact.CODEMIRROR_TEXTAREA) // Highlight current text and overwrite with file contents .type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", { force: true, @@ -56,7 +57,7 @@ filterTests(["smoke", "all"], () => { }) // Send Query cy.intercept("**/queries/preview").as("query") - cy.get(".spectrum-Button").contains("Send").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON).contains("Send").click({ force: true }) cy.wait("@query") // Assert against Status Code, body, & body rows cy.get("@query").its("response.statusCode").should("eq", 200) @@ -70,16 +71,16 @@ filterTests(["smoke", "all"], () => { const restUrl = "https://api.openbrewerydb.org/breweries" cy.selectExternalDatasource(datasource) cy.createRestQuery("GET", restUrl, "/breweries") - cy.get(".spectrum-Tabs-itemLabel").contains("Transformer").click() + cy.get(interact.SPECTRUM_TABS_ITEM).contains("Transformer").click() // Clear the code box and add "test" - cy.get(".CodeMirror textarea") + cy.get(interact.CODEMIRROR_TEXTAREA) .type(Cypress.platform === "darwin" ? "{cmd}a" : "{ctrl}a", { force: true, }) .type("test") // Run Query and intercept cy.intercept("**/preview").as("queryError") - cy.get(".spectrum-Button").contains("Send").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON).contains("Send").click({ force: true }) cy.wait("@queryError") cy.wait(500) // Assert against message and status for the query error diff --git a/packages/builder/cypress/integration/renameAnApplication.spec.js b/packages/builder/cypress/integration/renameAnApplication.spec.js index 7e611ac4ec..4460750b07 100644 --- a/packages/builder/cypress/integration/renameAnApplication.spec.js +++ b/packages/builder/cypress/integration/renameAnApplication.spec.js @@ -1,6 +1,7 @@ import filterTests from "../support/filterTests" +const interact = require("../support/interact") -filterTests(['all'], () => { +filterTests(["all"], () => { context("Rename an App", () => { beforeEach(() => { cy.login() @@ -11,17 +12,14 @@ filterTests(['all'], () => { const appName = "Cypress Tests" const appRename = "Cypress Renamed" // Rename app, Search for app, Confirm name was changed - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) renameApp(appName, appRename) cy.reload() - cy.wait(1000) cy.searchForApplication(appRename) - cy.get(".appTable").find(".title").should("have.length", 1) + cy.get(interact.APP_TABLE).find(interact.TITLE).should("have.length", 1) cy.applicationInAppTable(appRename) // Set app name back to Cypress Tests cy.reload() - cy.wait(1000) renameApp(appRename, appName) }) @@ -30,47 +28,50 @@ filterTests(['all'], () => { const appName = "Cypress Tests" const appRename = "Cypress Renamed" // Publish the app - cy.get(".toprightnav") - cy.get(".spectrum-Button").contains("Publish").click({ force: true }) - cy.get(".spectrum-Dialog-grid") - .within(() => { - // Click publish again within the modal - cy.get(".spectrum-Button").contains("Publish").click({ force: true }) - }) + cy.get(interact.TOP_RIGHT_NAV) + cy.get(interact.SPECTRUM_BUTTON) + .contains("Publish") + .click({ force: true }) + cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => { + // Click publish again within the modal + cy.get(interact.SPECTRUM_BUTTON) + .contains("Publish") + .click({ force: true }) + }) // Rename app, Search for app, Confirm name was changed - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) renameApp(appName, appRename, true) - cy.get(".appTable").find(".wrapper").should("have.length", 1) + cy.get(interact.APP_TABLE).find(interact.WRAPPER).should("have.length", 1) cy.applicationInAppTable(appRename) }) it("Should try to rename an application to have no name", () => { const appName = "Cypress Tests" - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) renameApp(appName, " ", false, true) - cy.wait(500) // Close modal and confirm name has not been changed - cy.get(".spectrum-Dialog-grid").contains("Cancel").click() - cy.reload() - cy.wait(1000) + cy.get(interact.SPECTRUM_DIALOG_GRID, { timeout: 1000 }).contains("Cancel").click() cy.applicationInAppTable(appName) }) xit("Should create two applications with the same name", () => { // It is not possible to have applications with the same name const appName = "Cypress Tests" - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) - cy.get(".spectrum-Button").contains("Create app").click({ force: true }) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) + cy.get(interact.SPECTRUM_BUTTON), { timeout: 500 } + .contains("Create app") + .click({ force: true }) cy.contains(/Start from scratch/).click() - cy.get(".spectrum-Modal") - .within(() => { - cy.get("input").eq(0).type(appName) - cy.get(".spectrum-ButtonGroup").contains("Create app").click({ force: true }) - cy.get(".error").should("have.text", "Another app with the same name already exists") - }) + cy.get(interact.SPECTRUM_MODAL).within(() => { + cy.get("input").eq(0).type(appName) + cy.get(interact.SPECTRUM_BUTTON_GROUP) + .contains("Create app") + .click({ force: true }) + cy.get(interact.ERROR).should( + "have.text", + "Another app with the same name already exists" + ) + }) }) it("should validate application names", () => { @@ -79,42 +80,33 @@ filterTests(['all'], () => { const appName = "Cypress Tests" const numberName = 12345 const specialCharName = "£$%^" - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(500) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 }) renameApp(appName, numberName) - cy.reload() - cy.wait(1000) cy.applicationInAppTable(numberName) - cy.reload() - cy.wait(1000) renameApp(numberName, specialCharName) - cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only") + cy.get(interact.ERROR).should( + "have.text", + "App name must be letters, numbers and spaces only" + ) // Set app name back to Cypress Tests - cy.reload() - cy.wait(1000) renameApp(numberName, appName) }) const renameApp = (originalName, changedName, published, noName) => { cy.searchForApplication(originalName) - cy.get(".appTable") - .within(() => { - cy.get("[aria-label='More']").eq(0).click() - }) - // Check for when an app is published - if (published == true) { - // Should not have Edit as option, will unpublish app - cy.should("not.have.value", "Edit") - cy.get(".spectrum-Menu").contains("Unpublish").click() - cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click() - cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click() - } - cy.get("[data-cy='app-row-actions-menu-popover']").eq(0).within(() => { - cy.get(".spectrum-Menu-item").contains("Edit").click({ force: true }) + cy.get(interact.APP_TABLE, { timeout: 1000 }).within(() => { + cy.get(".app-row-actions button") + .contains("Manage") + .eq(0) + .click({ force: true }) }) - + cy.get(".spectrum-Tabs-item").contains("Settings").click() + cy.get(".spectrum-Tabs-item.is-selected").contains("Settings") + cy.get(".settings-tab").should("be.visible") + cy.get(".details-section .page-action button") + .contains("Edit") + .click({ force: true }) cy.updateAppName(changedName, noName) - } }) }) diff --git a/packages/builder/cypress/integration/revertApp.spec.js b/packages/builder/cypress/integration/revertApp.spec.js index 01d5a04981..0fb58e89e9 100644 --- a/packages/builder/cypress/integration/revertApp.spec.js +++ b/packages/builder/cypress/integration/revertApp.spec.js @@ -1,4 +1,5 @@ import filterTests from "../support/filterTests" +const interact = require('../support/interact') filterTests(['smoke', 'all'], () => { context("Revert apps", () => { @@ -9,15 +10,15 @@ filterTests(['smoke', 'all'], () => { it("should try to revert an unpublished app", () => { // Click revert icon - cy.get(".toprightnav").within(() => { - cy.get("[aria-label='Revert']").click({ force: true }) + cy.get(interact.TOP_RIGHT_NAV).within(() => { + cy.get(interact.AREA_LABEL_REVERT).click({ force: true }) }) - cy.get(".spectrum-Modal").within(() => { + cy.get(interact.SPECTRUM_MODAL).within(() => { // Enter app name before revert - cy.get("input").type("Cypress Tests") + cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).type("Cypress Tests") cy.intercept('**/revert').as('revertApp') // Click Revert - cy.get(".spectrum-Button").contains("Revert").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON).contains("Revert").click({ force: true }) // Intercept Request after button click & apply assertions cy.wait("@revertApp") cy.get("@revertApp").its('response.body').should('have.property', 'message', "App has not yet been deployed") @@ -29,45 +30,45 @@ filterTests(['smoke', 'all'], () => { cy.navigateToFrontend() // Add initial component - Paragraph - cy.addComponent("Elements", "Paragraph") + cy.searchAndAddComponent("Paragraph") // Publish app - cy.get(".spectrum-Button").contains("Publish").click({ force: true }) - cy.get(".spectrum-ButtonGroup").within(() => { - cy.get(".spectrum-Button").contains("Publish").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON).contains("Publish").click({ force: true }) + cy.get(interact.SPECTRUM_BUTTON_GROUP).within(() => { + cy.get(interact.SPECTRUM_BUTTON).contains("Publish").click({ force: true }) }) - cy.wait(1000) - cy.get(".spectrum-ButtonGroup").within(() => { - cy.get(".spectrum-Button").contains("Done").click({ force: true }) + cy.wait(1000) // Wait for next modal to finish loading + cy.get(interact.SPECTRUM_BUTTON_GROUP, { timeout: 1000 }).within(() => { + cy.get(interact.SPECTRUM_BUTTON).contains("Done").click({ force: true }) }) // Add second component - Button - cy.addComponent("Elements", "Button") + cy.searchAndAddComponent("Button") // Click Revert - cy.get(".toprightnav").within(() => { - cy.get("[aria-label='Revert']").click({ force: true }) + cy.get(interact.TOP_RIGHT_NAV).within(() => { + cy.get(interact.AREA_LABEL_REVERT).click({ force: true }) }) - cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => { + cy.get("input").type("Cypress Tests") // Click Revert - cy.get(".spectrum-Button").contains("Revert").click({ force: true }) - cy.wait(1000) + cy.get(interact.SPECTRUM_BUTTON).contains("Revert").click({ force: true }) + cy.wait(2000) // Wait for app to finish reverting }) // Confirm Paragraph component is still visible - cy.get(".root").contains("New Paragraph") + cy.get(interact.ROOT, { timeout: 1000 }).contains("New Paragraph") // Confirm Button component is not visible - cy.get(".root").should("not.have.text", "New Button") - cy.wait(500) + cy.get(interact.ROOT, { timeout: 1000 }).should("not.have.text", "New Button") }) it("should enter incorrect app name when reverting", () => { // Click Revert - cy.get(".toprightnav").within(() => { - cy.get("[aria-label='Revert']").click({ force: true }) + cy.get(interact.TOP_RIGHT_NAV, { timeout: 1000 }).within(() => { + cy.get(interact.AREA_LABEL_REVERT).click({ force: true }) }) // Enter incorrect app name - cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(interact.SPECTRUM_DIALOG_GRID).within(() => { cy.get("input").type("Cypress Tests") // Revert button within modal should be disabled - cy.get(".spectrum-Button").eq(1).should('be.disabled') + cy.get(interact.SPECTRUM_BUTTON).eq(1).should('be.disabled') }) }) }) diff --git a/packages/builder/cypress/integration/templates/HR/hrTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/HR/hrTemplateDetails.spec.js deleted file mode 100644 index fbac463bfe..0000000000 --- a/packages/builder/cypress/integration/templates/HR/hrTemplateDetails.spec.js +++ /dev/null @@ -1,56 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify HR Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter HR Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="HR"]').click() - }) - }) - - it("should verify the details option for HR templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - - if (templateNameText == "Job Application Tracker") { - // Template name should include 'applicant-tracking-system' - cy.get('a') - .should('have.attr', 'href').and('contain', 'applicant-tracking-system') - } - else if (templateNameText == "Job Portal App") { - // Template name should include 'job-portal' - const templateNameSplit = templateNameParsed.split('-app')[0] - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameSplit) - } - else { - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameParsed) - } - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/HR/jobApplicationTracker.spec.js b/packages/builder/cypress/integration/templates/HR/jobApplicationTracker.spec.js deleted file mode 100644 index 045a85d8f6..0000000000 --- a/packages/builder/cypress/integration/templates/HR/jobApplicationTracker.spec.js +++ /dev/null @@ -1,222 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Job Application Tracker Template Functionality", () => { - const templateName = "Job Application Tracker" - const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-') - - before(() => { - cy.login() - cy.deleteApp(templateName) - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, { - onBeforeLoad(win) { - cy.stub(win, 'open') - } - }) - cy.wait(2000) - }) - - it("should create and publish app with Job Application Tracker template", () => { - // Select Job Application Tracker template - cy.get(".template-thumbnail-text") - .contains(templateName).parentsUntil(".template-grid").within(() => { - cy.get(".spectrum-Button").contains("Use template").click({ force: true }) - }) - - // Confirm URL matches template name - const appUrl = cy.get(".app-server") - appUrl.invoke('text').then(appUrlText => { - expect(appUrlText).to.equal(`${Cypress.config().baseUrl}/app/` + templateNameParsed) - }) - - // Create App - cy.get(".spectrum-Dialog-grid").within(() => { - cy.get(".spectrum-Button").contains("Create app").click({ force: true }) - }) - - // Publish App & Verify it opened - cy.wait(2000) // Wait for app to generate - cy.publishApp(true) - cy.window().its('open').should('be.calledOnce') - }) - - it("should add active/inactive vacancies", () => { - // Visit published app - cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) - - // loop for active/inactive vacancies - for (let i = 0; i < 2; i++) { - // Vacancies section - cy.get(".links").contains("Vacancies").click({ force: true }) - cy.get(".spectrum-Button").contains("Create New").click() - - // Add inactive vacancy - // Title - cy.get('[data-name="Title"]').within(() => { - cy.get(".spectrum-Textfield").type("Tester") - }) - - // Closing Date - cy.get('[data-name="Closing date"]').within(() => { - cy.get('[aria-label=Calendar]').click({ force: true }) - }) - cy.get("[aria-current=date]").click() - - // Department - cy.get('[data-name="Department"]').within(() => { - cy.get(".spectrum-Picker-label").click() - }) - cy.get(".spectrum-Menu").find('li').its('length').then(len => { - cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click() - }) - - // Employment Type - cy.get('[data-name="Employment type"]').within(() => { - cy.get(".spectrum-Picker-label").click() - }) - cy.get(".spectrum-Menu").find('li').its('length').then(len => { - cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click() - }) - - // Salary - cy.get('[data-name="Salary ($)"]').within(() => { - cy.get(".spectrum-Textfield").type(40000) - }) - - // Description - cy.get('[data-name="Description"]').within(() => { - cy.get(".spectrum-Textfield").type("description") - }) - - // Responsibilities - cy.get('[data-name="Responsibilities"]').within(() => { - cy.get(".spectrum-Textfield").type("Responsibilities") - }) - - // Requirements - cy.get('[data-name="Requirements"]').within(() => { - cy.get(".spectrum-Textfield").type("Requirements") - }) - - // Hiring manager - cy.get('[data-name="Hiring manager"]').within(() => { - cy.get(".spectrum-Picker-label").click() - }) - cy.get(".spectrum-Menu").find('li').its('length').then(len => { - cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click() - }) - - // Active - if (i == 0) { - cy.get('[data-name="Active"]').within(() => { - cy.get(".spectrum-Checkbox-box").click({ force: true }) - }) - } - - // Location - cy.get('[data-name="Location"]').within(() => { - cy.get(".spectrum-Picker-label").click() - }) - cy.get(".spectrum-Menu").find('li').its('length').then(len => { - cy.get(".spectrum-Menu-item").eq(Math.floor(Math.random() * len)).click() - }) - - // Save vacancy - cy.get(".spectrum-Button").contains("Save").click({ force: true }) - cy.wait(1000) - - // Check table was updated - cy.get('[data-name="Vacancies Table"]').eq(i).should('contain', 'Tester') - } - }) - - xit("should filter applications by stage", () => { - // Visit published app - cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) - cy.wait(1000) - - // Applications section - cy.get(".links").contains("Applications").click({ force: true }) - cy.wait(1000) - - // Filter by stage - Confirm table updates - cy.get(".spectrum-Picker").contains("Filter by stage").click({ force: true }) - cy.get(".spectrum-Menu").find('li').its('length').then(len => { - for (let i = 1; i < len; i++) { - cy.get(".spectrum-Menu-item").eq(i).click() - const stage = cy.get(".spectrum-Picker-label") - stage.invoke('text').then(stageText => { - if (stageText == "1st interview") { - cy.get(".placeholder").should('contain', 'No rows found') - } - else { - cy.get(".spectrum-Table-row").should('contain', stageText) - } - cy.get(".spectrum-Picker").contains(stageText).click({ force: true }) - }) - } - }) - }) - - xit("should edit an application", () => { - // Switch application from not hired to hired - // Visit published app - cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) - cy.wait(1000) - - // Not Hired section - cy.get(".links").contains("Not hired").click({ force: true }) - cy.wait(500) - - // View application - cy.get(".spectrum-Table").within(() => { - cy.get(".spectrum-Button").contains("View").click({ force: true }) - cy.wait(500) - }) - - // Update value for 'Staged' - cy.get('[data-name="Stage"]').within(() => { - cy.get(".spectrum-Picker-label").click() - }) - cy.get(".spectrum-Menu").within(() => { - cy.get(".spectrum-Menu-item").contains("Hired").click() - }) - - // Save application - cy.get(".spectrum-Button").contains("Save").click({ force: true }) - cy.wait(500) - - // Hired section - cy.get(".links").contains("Hired").click({ force: true }) - cy.wait(500) - - // Verify Table size - Total rows = 2 - cy.get(".spectrum-Table").find(".spectrum-Table-row").its('length').then((len => { - expect(len).to.eq(2) - })) - }) - - xit("should delete an application", () => { - // Visit published app - cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) - cy.wait(1000) - - // Hired section - cy.get(".links").contains("Hired").click({ force: true }) - cy.wait(500) - - // View first application - cy.get(".spectrum-Table-row").eq(0).within(() => { - cy.get(".spectrum-Button").contains("View").click({ force: true }) - cy.wait(500) - }) - - // Delete application - cy.get(".spectrum-Button").contains("Delete").click({ force: true }) - cy.get(".spectrum-Dialog-grid").within(() => { - cy.get(".spectrum-Button").contains("Confirm").click() - }) - }) - }) -}) diff --git a/packages/builder/cypress/integration/templates/IT/ITTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/IT/ITTemplateDetails.spec.js deleted file mode 100644 index 84cbc5707e..0000000000 --- a/packages/builder/cypress/integration/templates/IT/ITTemplateDetails.spec.js +++ /dev/null @@ -1,60 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify IT Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter IT Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="IT"]').click() - }) - }) - - it("should verify the details option for IT templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - - if (templateNameText == "Hashicorp Scorecard Template") { - const templateNameSplit = templateNameParsed.split('-template')[0] - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameSplit) - } - else if (templateNameText == "IT Ticketing System") { - const templateNameSplit = templateNameParsed.split('it-')[1] - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameSplit) - } - else if (templateNameText == "IT Incident Report Form") { - const templateNameSplit = templateNameParsed.split('-form')[0] - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameSplit) - } - else { - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - } - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/IT/ITTicketingSystem.spec.js b/packages/builder/cypress/integration/templates/IT/ITTicketingSystem.spec.js deleted file mode 100644 index 15628ab131..0000000000 --- a/packages/builder/cypress/integration/templates/IT/ITTicketingSystem.spec.js +++ /dev/null @@ -1,72 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("IT Ticketing System Template Functionality", () => { - const templateName = "IT Ticketing System" - const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-') - - before(() => { - cy.login() - cy.deleteApp(templateName) - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, { - onBeforeLoad(win) { - cy.stub(win, 'open') - } - }) - cy.wait(2000) - }) - - it("should create and publish app with IT Ticketing System template", () => { - // Select IT Ticketing System template - cy.get(".template-thumbnail-text") - .contains(templateName).parentsUntil(".template-grid").within(() => { - cy.get(".spectrum-Button").contains("Use template").click({ force: true }) - }) - - // Confirm URL matches template name - const appUrl = cy.get(".app-server") - appUrl.invoke('text').then(appUrlText => { - expect(appUrlText).to.equal(`${Cypress.config().baseUrl}/app/` + templateNameParsed) - }) - - // Create App - cy.get(".spectrum-Dialog-grid").within(() => { - cy.get(".spectrum-Button").contains("Create app").click({ force: true }) - }) - - // Publish App & Verify it opened - cy.wait(2000) // Wait for app to generate - cy.publishApp(true) - cy.window().its('open').should('be.calledOnce') - }) - - xit("should filter tickets by status", () => { - // Visit published app - cy.visit(`${Cypress.config().baseUrl}/app/` + templateNameParsed) - cy.wait(1000) - - // Tickets section - cy.get(".links").contains("Tickets").click({ force: true }) - cy.wait(1000) - - // Filter by stage - Confirm table updates - cy.get(".spectrum-Picker").contains("Filter by status").click({ force: true }) - cy.get(".spectrum-Menu").find('li').its('length').then(len => { - for (let i = 1; i < len; i++) { - cy.get(".spectrum-Menu-item").eq(i).click() - const stage = cy.get(".spectrum-Picker-label") - stage.invoke('text').then(stageText => { - if (stageText == "In progress" || stageText == "On hold" || stageText == "Triaged") { - cy.get(".placeholder").should('contain', 'No rows found') - } - else { - cy.get(".spectrum-Table-row").should('contain', stageText) - } - cy.get(".spectrum-Picker").contains(stageText).click({ force: true }) - }) - } - }) - }) - }) -}) diff --git a/packages/builder/cypress/integration/templates/adminPanels/adminPanelsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/adminPanels/adminPanelsTemplateDetails.spec.js deleted file mode 100644 index 2fa57b2c89..0000000000 --- a/packages/builder/cypress/integration/templates/adminPanels/adminPanelsTemplateDetails.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Admin Panel Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Admin Panels Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Admin Panels"]').click() - }) - }) - - it("should verify the details option for Admin Panels templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/approvalApps/approvalAppsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/approvalApps/approvalAppsTemplateDetails.spec.js deleted file mode 100644 index 322a17f6c2..0000000000 --- a/packages/builder/cypress/integration/templates/approvalApps/approvalAppsTemplateDetails.spec.js +++ /dev/null @@ -1,51 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Aproval Apps Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Approval Apps Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Approval Apps"]').click() - }) - }) - - it("should verify the details option for Approval Apps templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - - if (templateNameText == "Content Approval System") { - // Template name should include 'content-approval' - const templateNameSplit = templateNameParsed.split('-system')[0] - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameSplit) - } - else { - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - } - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/businessApps/businessAppsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/businessApps/businessAppsTemplateDetails.spec.js deleted file mode 100644 index 734fb9a968..0000000000 --- a/packages/builder/cypress/integration/templates/businessApps/businessAppsTemplateDetails.spec.js +++ /dev/null @@ -1,51 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Business Apps Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Business Apps Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Business Apps"]').click() - }) - }) - - it("should verify the details option for Business Apps templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - - if (templateNameText == "Employee Check-in/Check-Out Template") { - // Remove / from template name - const templateNameReplace = templateNameParsed.replace(/\//g, "-") - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameReplace) - } - else { - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - } - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/directories/directoriesTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/directories/directoriesTemplateDetails.spec.js deleted file mode 100644 index dc874fcbaf..0000000000 --- a/packages/builder/cypress/integration/templates/directories/directoriesTemplateDetails.spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Directories Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Directories Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Directories"]').click() - }) - }) - - it("should verify the details option for Directories templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - const templateNameSplit = templateNameParsed.split('-template')[0] - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameSplit) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/forms/formsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/forms/formsTemplateDetails.spec.js deleted file mode 100644 index 3206a71f6e..0000000000 --- a/packages/builder/cypress/integration/templates/forms/formsTemplateDetails.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Forms Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Forms Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Forms"]').click() - }) - }) - - it("should verify the details option for Forms templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/healthcare/healthcareTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/healthcare/healthcareTemplateDetails.spec.js deleted file mode 100644 index b46bb46274..0000000000 --- a/packages/builder/cypress/integration/templates/healthcare/healthcareTemplateDetails.spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Healthcare Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Healthcare Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Healthcare"]').click() - }) - }) - - it("should verify the details option for Healthcare templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/legal/legalTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/legal/legalTemplateDetails.spec.js deleted file mode 100644 index 57485aee40..0000000000 --- a/packages/builder/cypress/integration/templates/legal/legalTemplateDetails.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Legal Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Legal Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Legal"]').click() - }) - }) - - it("should verify the details option for Legal templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/logistics/logisticsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/logistics/logisticsTemplateDetails.spec.js deleted file mode 100644 index e5d5745e4e..0000000000 --- a/packages/builder/cypress/integration/templates/logistics/logisticsTemplateDetails.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Logistics Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Logistics Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Logistics"]').click() - }) - }) - - it("should verify the details option for Logistics templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/manufacturing/manufacturingTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/manufacturing/manufacturingTemplateDetails.spec.js deleted file mode 100644 index 30019c87fd..0000000000 --- a/packages/builder/cypress/integration/templates/manufacturing/manufacturingTemplateDetails.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Manufacturing Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Manufacturing Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Manufacturing"]').click() - }) - }) - - it("should verify the details option for Manufacturing templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/marketing/leadGenerationForm.spec.js b/packages/builder/cypress/integration/templates/marketing/leadGenerationForm.spec.js deleted file mode 100644 index 9f08b36d56..0000000000 --- a/packages/builder/cypress/integration/templates/marketing/leadGenerationForm.spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Lead Generation Form Template Functionality", () => { - const templateName = "Lead Generation Form" - const templateNameParsed = templateName.toLowerCase().replace(/\s+/g, '-') - - before(() => { - cy.login() - cy.deleteApp(templateName) - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`, { - onBeforeLoad(win) { - cy.stub(win, 'open') - } - }) - cy.wait(2000) - }) - - it("should create and publish app with Lead Generation Form template", () => { - // Select Lead Generation Form template - cy.get(".template-thumbnail-text") - .contains(templateName).parentsUntil(".template-grid").within(() => { - cy.get(".spectrum-Button").contains("Use template").click({ force: true }) - }) - - // Confirm URL matches template name - const appUrl = cy.get(".app-server") - appUrl.invoke('text').then(appUrlText => { - expect(appUrlText).to.equal(`${Cypress.config().baseUrl}/app/` + templateNameParsed) - }) - - // Create App - cy.get(".spectrum-Dialog-grid").within(() => { - cy.get(".spectrum-Button").contains("Create app").click({ force: true }) - }) - - // Publish App & Verify it opened - cy.wait(2000) // Wait for app to generate - cy.publishApp(true) - cy.window().its('open').should('be.calledOnce') - }) - }) -}) diff --git a/packages/builder/cypress/integration/templates/marketing/marketingTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/marketing/marketingTemplateDetails.spec.js deleted file mode 100644 index 66875e6939..0000000000 --- a/packages/builder/cypress/integration/templates/marketing/marketingTemplateDetails.spec.js +++ /dev/null @@ -1,51 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Marketing Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Marketing Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Marketing"]').click() - }) - }) - - it("should verify the details option for Marketing templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - - if (templateNameText == "Lead Generation Form") { - // Multi-step lead form - // Template name includes 'multi-step-lead-form' - cy.get('a') - .should('have.attr', 'href').and('contain', 'multi-step-lead-form') - } - else { - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - } - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/operations/operationsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/operations/operationsTemplateDetails.spec.js deleted file mode 100644 index 1a2ee1703a..0000000000 --- a/packages/builder/cypress/integration/templates/operations/operationsTemplateDetails.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Operations Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Operations Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Operations"]').click() - }) - }) - - it("should verify the details option for Operations templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/portals/portalsTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/portals/portalsTemplateDetails.spec.js deleted file mode 100644 index e81e12318d..0000000000 --- a/packages/builder/cypress/integration/templates/portals/portalsTemplateDetails.spec.js +++ /dev/null @@ -1,71 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Portals Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Portal Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Portal"]').click() - }) - }) - - it("should verify the details option for Portal templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) - - it("should verify the details option for Portals templates", () => { - // Filter Portals Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Portals"]').click() - }) - - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a') - .should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/integration/templates/professionalServices/professionalServicesTemplateDetails.spec.js b/packages/builder/cypress/integration/templates/professionalServices/professionalServicesTemplateDetails.spec.js deleted file mode 100644 index 1267d8bd5c..0000000000 --- a/packages/builder/cypress/integration/templates/professionalServices/professionalServicesTemplateDetails.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import filterTests from "../../../support/filterTests" - -filterTests(["all"], () => { - context("Verify Professional Services Template Details", () => { - - before(() => { - cy.login() - - // Template navigation - cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/templates`) - - // Filter Professional Services Templates - cy.get(".template-category-filters").within(() => { - cy.get('[data-cy="Professional Services"]').click() - }) - }) - - it("should verify the details option for Professional Services templates", () => { - cy.get(".template-grid").find(".template-card").its('length') - .then(len => { - // Verify template name is within details link - for (let i = 0; i < len; i++) { - cy.get(".template-card").eq(i).within(() => { - const templateName = cy.get(".template-thumbnail-text") - templateName.invoke('text') - .then(templateNameText => { - const templateNameParsed = templateNameText.toLowerCase().replace(/\s+/g, '-') - cy.get('a').should('have.attr', 'href').and('contain', templateNameParsed) - }) - // Verify correct status from Details link - 200 - cy.get('a') - .then(link => { - cy.request(link.prop('href')) - .its('status') - .should('eq', 200) - }) - }) - } - }) - }) -}) -}) diff --git a/packages/builder/cypress/setup.js b/packages/builder/cypress/setup.js index d10990573a..0e2f25b028 100644 --- a/packages/builder/cypress/setup.js +++ b/packages/builder/cypress/setup.js @@ -1,16 +1,14 @@ const cypressConfig = require("../cypress.json") -const path = require("path") - -const tmpdir = path.join(require("os").tmpdir(), ".budibase") // normal development system const SERVER_PORT = cypressConfig.env.PORT const WORKER_PORT = cypressConfig.env.WORKER_PORT -process.env.NODE_ENV = "cypress" -process.env.ENABLE_ANALYTICS = "false" +if (!process.env.NODE_ENV) { + process.env.NODE_ENV = "cypress" +} +process.env.ENABLE_ANALYTICS = "0" process.env.JWT_SECRET = cypressConfig.env.JWT_SECRET -process.env.COUCH_URL = `leveldb://${tmpdir}/.data/` process.env.SELF_HOSTED = 1 process.env.WORKER_URL = `http://localhost:${WORKER_PORT}/` process.env.APPS_URL = `http://localhost:${SERVER_PORT}/` diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js index d50364fd54..b1fa629023 100644 --- a/packages/builder/cypress/support/commands.js +++ b/packages/builder/cypress/support/commands.js @@ -1,39 +1,39 @@ -// *********************************************** -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// - Cypress.on("uncaught:exception", () => { return false }) -Cypress.Commands.add("login", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(2000) - cy.url().then(url => { - if (url.includes("builder/admin")) { - // create admin user - cy.get("input").first().type("test@test.com") - cy.get('input[type="password"]').first().type("test") - cy.get('input[type="password"]').eq(1).type("test") - cy.contains("Create super admin user").click({ force: true }) - } - if (url.includes("builder/auth/login") || url.includes("builder/admin")) { - // login - cy.contains("Sign in to Budibase").then(() => { +// ACCOUNTS & USERS +Cypress.Commands.add("login", (email, password) => { + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 }) + cy.url() + .should("include", "/builder/") + .then(url => { + if (url.includes("builder/admin")) { + // create admin user cy.get("input").first().type("test@test.com") - cy.get('input[type="password"]').type("test") - cy.get("button").first().click({ force: true }) - cy.wait(1000) - }) - } - }) + cy.get('input[type="password"]').first().type("test") + cy.get('input[type="password"]').eq(1).type("test") + cy.contains("Create super admin user").click({ force: true }) + } + if (url.includes("builder/auth") || url.includes("builder/admin")) { + // login + cy.contains("Sign in to Budibase").then(() => { + if (email == null) { + cy.get("input").first().type("test@test.com") + cy.get('input[type="password"]').type("test") + } else { + cy.get("input").first().type(email) + cy.get('input[type="password"]').type(password) + } + cy.get("button").first().click({ force: true }) + cy.wait(1000) + }) + } + }) }) Cypress.Commands.add("logOut", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 }) cy.get(".user-dropdown .avatar > .icon").click({ force: true }) cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => { cy.get("li[data-cy='user-logout']").click({ force: true }) @@ -41,47 +41,69 @@ Cypress.Commands.add("logOut", () => { cy.wait(2000) }) -Cypress.Commands.add("closeModal", () => { - cy.get(".spectrum-Modal").within(() => { - cy.get(".close-icon").click() - cy.wait(1000) // Wait for modal to close +Cypress.Commands.add("logoutNoAppGrid", () => { + // Logs user out when app grid is not present + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 }) + cy.get(".avatar > .icon").click({ force: true }) + cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => { + cy.get(".spectrum-Menu-item").contains("Log out").click({ force: true }) }) + cy.wait(2000) }) -Cypress.Commands.add("importApp", (exportFilePath, name) => { - cy.visit(`${Cypress.config().baseUrl}/builder`) +Cypress.Commands.add("createUser", (email, permission) => { + cy.contains("Users").click() + cy.get(`[data-cy="add-user"]`).click() + cy.get(".spectrum-Dialog-grid").within(() => { + // Enter email + cy.get(".spectrum-Textfield-input").clear().click().type(email) - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length > 0) { - cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) - cy.wait(500) - } - cy.get(`[data-cy="import-app-btn"]`).click({ force: true }) - }) - - cy.get(".spectrum-Modal").within(() => { - cy.get("input").eq(1).should("have.focus") - - cy.get(".spectrum-Dropzone").selectFile(exportFilePath, { - action: "drag-drop", - }) - - cy.get(".gallery .filename").contains("exported-app.txt") - - if (name && name != "") { - cy.get("input").eq(0).type(name).should("have.value", name).blur() + // Select permission, if applicable + // Default is App User + if (permission != null) { + cy.get(".spectrum-Picker-label").click() + cy.get(".spectrum-Menu").within(() => { + cy.get(".spectrum-Menu-item") + .contains(permission) + .click({ force: true }) + }) } - cy.get(".confirm-wrap button") - .should("not.be.disabled") - .click({ force: true }) - cy.wait(5000) + // Add user + cy.get(".spectrum-Button").contains("Add users").click({ force: true }) + cy.get(".spectrum-ActionButton").contains("Add email").should("not.exist") }) + // Onboarding modal + cy.get(".spectrum-Dialog-grid", { timeout: 5000 }).contains( + "Choose your onboarding" + ) + cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".onboarding-type").eq(1).click() + cy.get(".spectrum-Button").contains("Done").click({ force: true }) + cy.get(".spectrum-Button").contains("Cancel").should("not.exist") + }) + + // Accounts created modal - Click Done button + cy.get(".spectrum-Button").contains("Done").click({ force: true }) +}) + +Cypress.Commands.add("deleteUser", email => { + // Assumes user has access to Users section + cy.contains("Users", { timeout: 2000 }).click() + cy.contains(email).click() + + cy.get(".title").within(() => { + cy.get(".spectrum-Icon").click({ force: true }) + }) + cy.get(".spectrum-Menu").within(() => { + cy.get(".spectrum-Menu-item").contains("Delete").click({ force: true }) + }) + cy.get(".spectrum-Dialog-grid").contains("Delete user").click({ force: true }) }) Cypress.Commands.add("updateUserInformation", (firstName, lastName) => { - cy.get(".user-dropdown .avatar > .icon").click({ force: true }) + cy.get(".user-dropdown .avatar > .icon", { timeout: 2000 }).click({ + force: true, + }) cy.get(".spectrum-Popover[data-cy='user-menu']").within(() => { cy.get("li[data-cy='user-info']").click({ force: true }) @@ -109,24 +131,55 @@ Cypress.Commands.add("updateUserInformation", (firstName, lastName) => { .should("have.value", lastName) .blur() } - cy.get("button").contains("Update information").click({ force: true }) + cy.get(".confirm-wrap").within(() => { + cy.get("button").contains("Update information").click({ force: true }) + }) + cy.get(".spectrum-Dialog-grid").should("not.exist") }) }) +Cypress.Commands.add("setUserRole", (user, role) => { + cy.contains("Users").click() + cy.contains(user).click() + + // Set Role + cy.wait(500) + cy.get(".spectrum-Form-itemField") + .eq(3) + .within(() => { + cy.get(".spectrum-Picker-label").click({ force: true }) + }) + cy.get(".spectrum-Menu").within(() => { + cy.get(".spectrum-Menu-itemLabel").contains(role).click({ force: true }) + }) + cy.get(".spectrum-Form-itemField").eq(3).should("contain", role) +}) + +// APPLICATIONS +Cypress.Commands.add("createTestApp", () => { + const appName = "Cypress Tests" + cy.deleteApp(appName) + cy.createApp(appName, "This app is used for Cypress testing.") +}) + Cypress.Commands.add("createApp", (name, addDefaultTable) => { const shouldCreateDefaultTable = typeof addDefaultTable != "boolean" ? true : addDefaultTable - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(1000) - cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 }) + cy.url({ timeout: 30000 }).should("include", "/apps") + cy.get(`[data-cy="create-app-btn"]`, { timeout: 5000 }).click({ force: true }) // If apps already exist - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) + cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`, { + timeout: 5000, + }) .its("body") .then(val => { if (val.length > 0) { - cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) + cy.get(`[data-cy="create-app-btn"]`, { timeout: 5000 }).click({ + force: true, + }) } }) @@ -139,7 +192,7 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => { cy.get(".spectrum-ButtonGroup") .contains("Create app") .click({ force: true }) - cy.wait(10000) + cy.wait(2000) }) if (shouldCreateDefaultTable) { cy.createTable("Cypress Tests", true) @@ -147,7 +200,7 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => { }) Cypress.Commands.add("deleteApp", name => { - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 }) cy.wait(2000) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) .its("body") @@ -166,22 +219,30 @@ Cypress.Commands.add("deleteApp", name => { return } + // Go to app overview const appIdParsed = appId.split("_").pop() const actionEleId = `[data-cy=row_actions_${appIdParsed}]` cy.get(actionEleId).within(() => { - cy.get(".spectrum-Icon").eq(0).click({ force: true }) + cy.contains("Manage").click({ force: true }) }) - cy.get(".spectrum-Menu").then($menu => { - if ($menu.text().includes("Unpublish")) { - cy.get(".spectrum-Menu").contains("Unpublish").click() - cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click() + cy.wait(500) + + // Unpublish first if needed + cy.get(`[data-cy="app-status"]`).then($status => { + if ($status.text().includes("- Unpublish")) { + // Exact match for Unpublish + cy.contains("Unpublish").click({ force: true }) + cy.get(".spectrum-Modal").within(() => { + cy.contains("Unpublish app").click({ force: true }) + }) } }) - cy.get(actionEleId).within(() => { - cy.get(".spectrum-Icon").eq(0).click({ force: true }) + // Delete app + cy.get(".app-overview-actions-icon").within(() => { + cy.get(".spectrum-Icon").click({ force: true }) }) - cy.get(".spectrum-Menu").contains("Delete").click() + cy.get(".spectrum-Menu").contains("Delete").click({ force: true }) cy.get(".spectrum-Dialog-grid").within(() => { cy.get("input").type(name) }) @@ -196,79 +257,20 @@ Cypress.Commands.add("deleteApp", name => { }) Cypress.Commands.add("deleteAllApps", () => { - cy.visit(`${Cypress.config().baseUrl}/builder`) + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 }) cy.wait(500) - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) + cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`, { + timeout: 5000, + }) .its("body") .then(val => { for (let i = 0; i < val.length; i++) { - const appIdParsed = val[i].appId.split("_").pop() - const actionEleId = `[data-cy=row_actions_${appIdParsed}]` - cy.get(actionEleId).within(() => { - cy.get(".spectrum-Icon").eq(0).click({ force: true }) - }) - - cy.get(".spectrum-Menu").contains("Delete").click() - cy.get(".spectrum-Dialog-grid").within(() => { - cy.get("input").type(val[i].name) - cy.get(".spectrum-Button--warning").click() - }) + cy.deleteApp(val[i].name) cy.reload() } }) }) -Cypress.Commands.add("customiseAppIcon", () => { - // Select random icon - cy.get(".grid").within(() => { - cy.get(".icon-item") - .eq(Math.floor(Math.random() * 23) + 1) - .click() - }) - // Select random colour - cy.get(".fill").click() - cy.get(".colors").within(() => { - cy.get(".color") - .eq(Math.floor(Math.random() * 33) + 1) - .click() - }) - cy.intercept("**/applications/**").as("iconChange") - cy.get(".spectrum-Button").contains("Save").click({ force: true }) - cy.wait("@iconChange") - cy.get("@iconChange").its("response.statusCode").should("eq", 200) - cy.wait(1000) -}) - -Cypress.Commands.add("alterAppVersion", (appId, version) => { - return cy - .request("put", `${Cypress.config().baseUrl}/api/applications/${appId}`, { - version: version || "0.0.1-alpha.0", - }) - .then(resp => { - expect(resp.status).to.eq(200) - }) -}) - -Cypress.Commands.add("updateAppName", (changedName, noName) => { - cy.get(".spectrum-Modal").within(() => { - if (noName == true) { - cy.get("input").clear() - cy.get(".spectrum-Dialog-grid") - .click() - .contains("App name must be letters, numbers and spaces only") - return cy - } - cy.get("input").clear() - cy.get("input") - .eq(0) - .type(changedName) - .should("have.value", changedName) - .blur() - cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true }) - cy.wait(500) - }) -}) - Cypress.Commands.add("unlockApp", unlock_config => { let config = { ...unlock_config } @@ -298,8 +300,28 @@ Cypress.Commands.add("unlockApp", unlock_config => { }) }) +Cypress.Commands.add("updateAppName", (changedName, noName) => { + cy.get(".spectrum-Modal").within(() => { + if (noName == true) { + cy.get("input").clear() + cy.get(".spectrum-Dialog-grid") + .click() + .contains("App name must be letters, numbers and spaces only") + return cy + } + cy.get("input").clear() + cy.get("input") + .eq(0) + .type(changedName) + .should("have.value", changedName) + .blur() + cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true }) + cy.wait(500) + }) +}) + Cypress.Commands.add("publishApp", resolvedAppPath => { - //Assumes you have navigated to an application first + // Assumes you have navigated to an application first cy.get(".toprightnav button.spectrum-Button") .contains("Publish") .click({ force: true }) @@ -311,7 +333,7 @@ Cypress.Commands.add("publishApp", resolvedAppPath => { cy.wait(1000) }) - //Verify that the app url is presented correctly to the user + // Verify that the app url is presented correctly to the user cy.get(".spectrum-Modal [data-cy='deploy-app-success-modal']") .should("be.visible") .within(() => { @@ -321,11 +343,125 @@ Cypress.Commands.add("publishApp", resolvedAppPath => { }) }) -Cypress.Commands.add("createTestApp", () => { - const appName = "Cypress Tests" - cy.deleteApp(appName) - cy.createApp(appName, "This app is used for Cypress testing.") - //cy.createScreen("home") +Cypress.Commands.add("alterAppVersion", (appId, version) => { + return cy + .request("put", `${Cypress.config().baseUrl}/api/applications/${appId}`, { + version: version || "0.0.1-alpha.0", + }) + .then(resp => { + expect(resp.status).to.eq(200) + }) +}) + +Cypress.Commands.add("importApp", (exportFilePath, name) => { + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 }) + + cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) + .its("body") + .then(val => { + if (val.length > 0) { + cy.get(`[data-cy="create-app-btn"]`).click({ force: true }) + } + cy.wait(500) + cy.get(`[data-cy="import-app-btn"]`).click({ + force: true, + }) + }) + + cy.get(".spectrum-Modal").within(() => { + cy.get("input").eq(1).should("have.focus") + + cy.get(".spectrum-Dropzone").selectFile(exportFilePath, { + action: "drag-drop", + }) + + cy.get(".gallery .filename").contains("exported-app.txt") + + if (name && name != "") { + cy.get("input").eq(0).type(name).should("have.value", name).blur() + } + cy.get(".confirm-wrap button") + .should("not.be.disabled") + .click({ force: true }) + cy.wait(3000) + }) +}) + +// Filters visible with 1 or more +Cypress.Commands.add("searchForApplication", appName => { + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 }) + cy.wait(2000) + + // No app filter functionality if only 1 app exists + cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) + .its("body") + .then(val => { + if (val.length < 2) { + return + } else { + // Searches for the app + cy.get(".filter").then(() => { + cy.get(".spectrum-Textfield").within(() => { + cy.get("input").eq(0).clear({ force: true }) + cy.get("input").eq(0).type(appName, { force: true }) + }) + }) + } + }) +}) + +// Assumes there are no others +Cypress.Commands.add("applicationInAppTable", appName => { + cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 30000 }) + cy.get(".appTable", { timeout: 5000 }).within(() => { + cy.get(".title").contains(appName).should("exist") + }) +}) + +Cypress.Commands.add("createAppFromScratch", appName => { + cy.get(`[data-cy="create-app-btn"]`) + .contains("Start from scratch") + .click({ force: true }) + cy.get(".spectrum-Modal").within(() => { + cy.get("input") + .eq(0) + .clear() + .type(appName) + .should("have.value", appName) + .blur() + cy.get(".spectrum-ButtonGroup").contains("Create app").click() + cy.wait(10000) + }) + cy.createTable("Cypress Tests", true) +}) + +// TABLES +Cypress.Commands.add("createTable", (tableName, initialTable) => { + // Creates an internal Budibase DB table + if (!initialTable) { + cy.navigateToDataSection() + cy.get(`[data-cy="new-table"]`, { timeout: 2000 }).click() + } + cy.wait(2000) + cy.get(".item", { timeout: 2000 }) + .contains("Budibase DB") + .click({ force: true }) + .then(() => { + cy.get(".spectrum-Button", { timeout: 2000 }) + .contains("Continue") + .click({ force: true }) + }) + cy.get(".spectrum-Modal").contains("Create Table", { timeout: 10000 }) + cy.get(".spectrum-Modal", { timeout: 2000 }).within(() => { + cy.get("input", { timeout: 2000 }).first().type(tableName).blur() + cy.get(".spectrum-ButtonGroup").contains("Create").click() + }) + // Ensure modal has closed and table is created + cy.get(".spectrum-Modal", { timeout: 2000 }).should("not.exist") + cy.get(".spectrum-Tabs-content", { timeout: 2000 }).should( + "contain", + tableName + ) }) Cypress.Commands.add("createTestTableWithData", () => { @@ -334,41 +470,6 @@ Cypress.Commands.add("createTestTableWithData", () => { cy.addColumn("dog", "age", "Number") }) -Cypress.Commands.add("publishApp", (viewApp = false) => { - cy.get(".toprightnav").contains("Publish").click({ force: true }) - cy.get(".spectrum-Dialog-grid").within(() => { - cy.get(".spectrum-Button").contains("Publish").click({ force: true }) - }) - cy.wait(2000) // Wait for App to publish and modal to appear - cy.get(".spectrum-Dialog-grid").within(() => { - if (viewApp) { - cy.get(".spectrum-Button").contains("View App").click({ force: true }) - } else { - cy.get(".spectrum-Button").contains("Done").click({ force: true }) - } - }) -}) - -Cypress.Commands.add("createTable", (tableName, initialTable) => { - if (!initialTable) { - cy.navigateToDataSection() - cy.get(`[data-cy="new-table"]`).click() - } - cy.wait(5000) - cy.get(".spectrum-Dialog-grid") - .contains("Budibase DB") - .click({ force: true }) - .then(() => { - cy.get(".spectrum-Button").contains("Continue").click({ force: true }) - }) - cy.get(".spectrum-Modal").within(() => { - cy.wait(1000) - cy.get("input").first().type(tableName).blur() - cy.get(".spectrum-ButtonGroup").contains("Create").click() - }) - cy.contains(tableName).should("be.visible") -}) - Cypress.Commands.add( "addColumn", (tableName, columnName, type, multiOptions = null) => { @@ -423,34 +524,75 @@ Cypress.Commands.add("addRowMultiValue", values => { }) }) -Cypress.Commands.add("createUser", email => { - // quick hacky recorded way to create a user - cy.contains("Users").click() - cy.get(`[data-cy="add-user"]`).click() - cy.get(".spectrum-Picker-label").click() - cy.get(".spectrum-Menu-item:nth-child(2) > .spectrum-Menu-itemLabel").click() +Cypress.Commands.add("selectTable", tableName => { + cy.expandBudibaseConnection() + cy.contains(".nav-item", tableName).click() +}) - //Onboarding type selector - cy.get( - ":nth-child(2) > .spectrum-Form-itemField > .spectrum-Textfield > .spectrum-Textfield-input" - ) - .first() - .type(email, { force: true }) - cy.get(".spectrum-Button--cta").click({ force: true }) +Cypress.Commands.add("addCustomSourceOptions", totalOptions => { + cy.get('[data-cy="customOptions-prop-control"]').within(() => { + cy.get(".spectrum-ActionButton-label").click({ force: true }) + }) + for (let i = 0; i < totalOptions; i++) { + // Add radio button options + cy.get(".spectrum-Button-label", { timeout: 1000 }) + .contains("Add Option") + .click({ force: true }) + .then(() => { + cy.get("[placeholder='Label']", { timeout: 500 }).eq(i).type(i) + cy.get("[placeholder='Value']").eq(i).type(i) + }) + } + // Save options + cy.get(".spectrum-Button").contains("Save").click({ force: true }) +}) + +// DESIGN SECTION +Cypress.Commands.add("searchAndAddComponent", component => { + // Open component menu + cy.get(".icon-side-nav").within(() => { + cy.get(".icon-side-nav-item").eq(1).click() + }) + cy.get(".add-component > .spectrum-Button") + .contains("Add component") + .click({ force: true }) + cy.get(".container", { timeout: 1000 }).within(() => { + cy.get(".title").should("contain", "Add component") + + // Search and add component + cy.get(".spectrum-Textfield-input").clear().type(component) + cy.get(".body").within(() => { + cy.get(".component") + .contains(new RegExp("^" + component + "$"), { timeout: 3000 }) + .click({ force: true }) + }) + }) + cy.wait(1000) + cy.location().then(loc => { + const params = loc.pathname.split("/") + const componentId = params[params.length - 1] + cy.getComponent(componentId, { timeout: 3000 }).should("exist") + return cy.wrap(componentId) + }) }) Cypress.Commands.add("addComponent", (category, component) => { if (category) { - cy.get(`[data-cy="category-${category}"]`).click({ force: true }) + cy.get(`[data-cy="category-${category}"]`, { timeout: 3000 }).click({ + force: true, + }) } + cy.wait(500) if (component) { - cy.get(`[data-cy="component-${component}"]`).click({ force: true }) + cy.get(`[data-cy="component-${component}"]`, { timeout: 3000 }).click({ + force: true, + }) } - cy.wait(2000) + cy.wait(1000) cy.location().then(loc => { const params = loc.pathname.split("/") const componentId = params[params.length - 1] - cy.getComponent(componentId).should("exist") + cy.getComponent(componentId, { timeout: 3000 }).should("exist") return cy.wrap(componentId) }) }) @@ -461,41 +603,26 @@ Cypress.Commands.add("getComponent", componentId => { .its("0.contentDocument") .should("exist") .its("body") - .should("not.be.null") + .should("not.be.undefined") .then(cy.wrap) - .find(`[data-id=${componentId}]`) + .find(`[data-id='${componentId}']`) }) -Cypress.Commands.add("navigateToFrontend", () => { - // Clicks on Design tab and then the Home nav item - cy.wait(1000) - cy.contains("Design").click() - cy.get(".spectrum-Search").type("/") - cy.get(".nav-item").contains("home").click() -}) - -Cypress.Commands.add("navigateToDataSection", () => { - // Clicks on the Data tab - cy.wait(500) - cy.contains("Data").click() -}) - -//Blank Cypress.Commands.add("createScreen", (route, accessLevelLabel) => { + // Blank Screen cy.contains("Design").click() - cy.get("[aria-label=AddCircle]").click() + cy.get(".spectrum-Button").contains("Add screen").click({ force: true }) cy.get(".spectrum-Modal").within(() => { cy.get("[data-cy='blank-screen']").click() cy.get(".spectrum-Button").contains("Continue").click({ force: true }) - cy.wait(500) }) - cy.get(".spectrum-Dialog-grid").within(() => { + cy.wait(500) + cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { cy.get(".spectrum-Form-itemField").eq(0).type(route) - cy.get(".spectrum-Button").contains("Continue").click({ force: true }) - cy.wait(1000) + cy.get(".confirm-wrap").contains("Continue").click({ force: true }) }) - cy.get(".spectrum-Modal").within(() => { + cy.get(".spectrum-Modal", { timeout: 1000 }).within(() => { if (accessLevelLabel) { cy.get(".spectrum-Picker-label").click() cy.wait(500) @@ -509,29 +636,33 @@ Cypress.Commands.add( "createDatasourceScreen", (datasourceNames, accessLevelLabel) => { cy.contains("Design").click() - cy.get("[aria-label=AddCircle]").click() - cy.get(".spectrum-Modal").within(() => { - cy.get(".item").contains("Autogenerated screens").click() + cy.get(".spectrum-Button").contains("Add screen").click({ force: true }) + cy.get(".spectrum-Dialog-grid").within(() => { + cy.get("[data-cy='autogenerated-screens']").click() + cy.intercept("**/api/datasources").as("autoScreens") cy.get(".spectrum-Button").contains("Continue").click({ force: true }) - cy.wait(500) + cy.wait("@autoScreens") + cy.wait(5000) }) - cy.get(".spectrum-Modal [data-cy='data-source-modal']").within(() => { + cy.get("[data-cy='autogenerated-screens']").should("not.exist") + cy.get("[data-cy='data-source-modal']", { timeout: 10000 }).within(() => { for (let i = 0; i < datasourceNames.length; i++) { - cy.get(".data-source-entry").contains(datasourceNames[i]).click() - //Ensure the check mark is visible + cy.get(".data-source-entry") + .contains(datasourceNames[i], { timeout: 20000 }) + .click({ force: true }) + // Ensure the check mark is visible cy.get(".data-source-entry") .contains(datasourceNames[i]) - .get(".data-source-check") + .get(".data-source-check", { timeout: 20000 }) .should("exist") } cy.get(".spectrum-Button").contains("Confirm").click({ force: true }) }) - cy.get(".spectrum-Modal").within(() => { + cy.get(".spectrum-Modal", { timeout: 10000 }).within(() => { if (accessLevelLabel) { - cy.get(".spectrum-Picker-label").click() - cy.wait(500) + cy.get(".spectrum-Picker-label", { timeout: 10000 }).click() cy.contains(accessLevelLabel).click() } cy.get(".spectrum-Button").contains("Done").click({ force: true }) @@ -541,17 +672,6 @@ Cypress.Commands.add( } ) -Cypress.Commands.add("navigateToAutogeneratedModal", () => { - // Screen name must already exist within data source - cy.contains("Design").click() - cy.get("[aria-label=AddCircle]").click() - cy.get(".spectrum-Modal").within(() => { - cy.get(".item").contains("Autogenerated screens").click() - cy.get(".spectrum-Button").contains("Continue").click({ force: true }) - cy.wait(500) - }) -}) - Cypress.Commands.add( "createAutogeneratedScreens", (screenNames, accessLevelLabel) => { @@ -573,109 +693,102 @@ Cypress.Commands.add( } ) -Cypress.Commands.add("addRow", values => { - cy.contains("Create row").click() - cy.get(".spectrum-Modal").within(() => { - for (let i = 0; i < values.length; i++) { - cy.get("input").eq(i).type(values[i]).blur() - } - cy.get(".spectrum-ButtonGroup").contains("Create").click() +Cypress.Commands.add("filterScreensAccessLevel", accessLevel => { + // Filters screens by access level dropdown + cy.get(".body").within(() => { + cy.get(".spectrum-Form-item").eq(1).click() + }) + cy.get(".spectrum-Menu").within(() => { + cy.contains(accessLevel).click() }) }) -Cypress.Commands.add("expandBudibaseConnection", () => { - if (Cypress.$(".nav-item > .content > .opened").length === 0) { - // expand the Budibase DB connection string - cy.get(".icon.arrow").eq(0).click() - } -}) - -Cypress.Commands.add("selectTable", tableName => { - cy.expandBudibaseConnection() - cy.contains(".nav-item", tableName).click() -}) - -Cypress.Commands.add("addCustomSourceOptions", totalOptions => { - cy.get(".spectrum-ActionButton") - .contains("Define Options") +Cypress.Commands.add("deleteScreen", screen => { + // Navigates to Design section and deletes specified screen + cy.contains("Design").click() + cy.get(".body").within(() => { + cy.contains(screen) + .siblings(".actions") + .within(() => { + cy.get(".spectrum-Icon").click({ force: true }) + }) + }) + cy.get(".spectrum-Menu > .spectrum-Menu-item > .spectrum-Menu-itemLabel") + .contains("Delete") .click() - .then(() => { - for (let i = 0; i < totalOptions; i++) { - // Add radio button options - cy.get(".spectrum-Button") - .contains("Add Option") - .click({ force: true }) - .then(() => { - cy.wait(500) - cy.get("[placeholder='Label']").eq(i).type(i) - cy.get("[placeholder='Value']").eq(i).type(i) - }) - } - // Save options - cy.get(".spectrum-Button").contains("Save").click({ force: true }) - }) + + cy.get( + ".spectrum-Dialog-grid > .spectrum-ButtonGroup > .confirm-wrap > .spectrum-Button" + ).click({ force: true }) + cy.get(".spectrum-Dialog-grid", { timeout: 10000 }).should("not.exist") }) -//Filters visible with 1 or more -Cypress.Commands.add("searchForApplication", appName => { - cy.visit(`${Cypress.config().baseUrl}/builder`) - cy.wait(2000) - - // No app filter functionality if only 1 app exists - cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) - .its("body") - .then(val => { - if (val.length < 2) { - return - } else { - // Searches for the app - cy.get(".filter").then(() => { - cy.get(".spectrum-Textfield").within(() => { - cy.get("input").eq(0).clear() - cy.get("input").eq(0).type(appName) +Cypress.Commands.add("deleteAllScreens", () => { + // Deletes all screens + cy.get(".body") + .find(".nav-item") + .its("length") + .then(len => { + for (let i = 0; i < len; i++) { + cy.get(".body > .nav-item") + .eq(0) + .invoke("text") + .then(text => { + cy.deleteScreen(text.trim()) }) - }) } }) }) -//Assumes there are no others -Cypress.Commands.add("applicationInAppTable", appName => { - cy.get(".appTable").within(() => { - cy.get(".title").contains(appName).should("exist") +// NAVIGATION +Cypress.Commands.add("navigateToFrontend", () => { + // Clicks on Design tab and then the Home nav item + cy.wait(500) + cy.intercept("**/preview").as("preview") + cy.contains("Design").click() + cy.wait("@preview") + cy.get("@preview").then(res => { + if (res.statusCode != 200) { + cy.reload() + } }) + cy.get(".spectrum-Search", { timeout: 20000 }).type("/") + cy.get(".nav-item", { timeout: 2000 }).contains("home").click({ force: true }) }) -Cypress.Commands.add("createAppFromScratch", appName => { - cy.get(`[data-cy="create-app-btn"]`) - .contains("Start from scratch") - .click({ force: true }) +Cypress.Commands.add("navigateToDataSection", () => { + // Clicks on the Data tab + cy.wait(500) + cy.contains("Data").click() +}) + +Cypress.Commands.add("navigateToAutogeneratedModal", () => { + // Screen name must already exist within datasource + cy.contains("Design").click() + cy.get(".spectrum-Button").contains("Add screen").click({ force: true }) cy.get(".spectrum-Modal").within(() => { - cy.get("input") - .eq(0) - .clear() - .type(appName) - .should("have.value", appName) - .blur() - cy.get(".spectrum-ButtonGroup").contains("Create app").click() - cy.wait(10000) + cy.get(".item", { timeout: 2000 }) + .contains("Autogenerated screens") + .click({ force: true }) + cy.get(".spectrum-Button").contains("Continue").click({ force: true }) + cy.wait(500) }) - cy.createTable("Cypress Tests", true) }) +// DATASOURCES Cypress.Commands.add("selectExternalDatasource", datasourceName => { // Navigates to Data Section cy.navigateToDataSection() - // Open Data Source modal + // Open Datasource modal cy.get(".nav").within(() => { cy.get(".add-button").click() }) // Clicks specified datasource & continue - cy.wait(1000) - cy.get(".item-list").contains(datasourceName).click() + cy.get(".item-list", { timeout: 1000 }).contains(datasourceName).click() cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".spectrum-Button").contains("Continue").click({ force: true }) }) + cy.wait(500) }) Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => { @@ -683,8 +796,7 @@ Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => { // Adds the config for specified datasource & fetches tables // Currently supports MySQL, PostgreSQL, Oracle // Host IP Address - cy.wait(500) - cy.get(".spectrum-Dialog-grid").within(() => { + cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => { cy.get(".form-row") .eq(0) .within(() => { @@ -784,17 +896,35 @@ Cypress.Commands.add("addDatasourceConfig", (datasource, skipFetch) => { Cypress.Commands.add("createRestQuery", (method, restUrl, queryPrettyName) => { // addExternalDatasource should be called prior to this // Configures REST datasource & sends query - cy.wait(1000) - cy.get(".spectrum-Button").contains("Add query").click({ force: true }) + cy.get(".spectrum-Button", { timeout: 1000 }) + .contains("Add query") + .click({ force: true }) // Select Method & add Rest URL cy.get(".spectrum-Picker-label").eq(1).click() cy.get(".spectrum-Menu").contains(method).click() cy.get("input").clear().type(restUrl) // Send query cy.get(".spectrum-Button").contains("Send").click({ force: true }) - cy.wait(500) - cy.get(".spectrum-Button").contains("Save").click({ force: true }) + cy.get(".spectrum-Button", { timeout: 500 }) + .contains("Save") + .click({ force: true }) cy.get(".hierarchy-items-container") .should("contain", method) .and("contain", queryPrettyName) }) + +// MISC +Cypress.Commands.add("closeModal", () => { + cy.get(".spectrum-Modal", { timeout: 2000 }).within(() => { + cy.get(".close-icon").click() + }) + // Confirm modal has closed + cy.get(".spectrum-Modal", { timeout: 10000 }).should("not.exist") +}) + +Cypress.Commands.add("expandBudibaseConnection", () => { + if (Cypress.$(".nav-item > .content > .opened").length === 0) { + // expand the Budibase DB connection string + cy.get(".icon.arrow").eq(0).click() + } +}) diff --git a/packages/builder/cypress/support/interact.js b/packages/builder/cypress/support/interact.js index 11794d940d..4f2451ce4b 100644 --- a/packages/builder/cypress/support/interact.js +++ b/packages/builder/cypress/support/interact.js @@ -12,15 +12,125 @@ export const APP_NAME_INPUT = "input" // we need to update this with atribute cy export const SPECTRUM_BUTTON_GROUP = ".spectrum-ButtonGroup" export const SPECTRUM_MODAL_INPUT = ".spectrum-Modal input" -//AddMultiOptionDatatype test +//AddMultiOptionDatatype export const CATEGORY_DATA = '[data-cy="category-Data"]' export const COMPONENT_DATA_PROVIDER = '[data-cy="component-Data Provider"]' export const DATASOURCE_PROP_CONTROL = '[data-cy="dataSource-prop-control"]' export const DROPDOWN = ".dropdown" -export const SPECTRUM_Picker_LABEL = ".spectrum-Picker-label" +export const SPECTRUM_PICKER_LABEL = ".spectrum-Picker-label" export const DATASOURCE_FIELD_CONTROL = '[data-cy="field-prop-control"]' export const OPTION_TYPE_PROP_CONTROL = '[data-cy="optionsType-prop-control' //AddRadioButtons export const SPECTRUM_POPOVER = ".spectrum-Popover" export const OPTION_SOURCE_PROP_CONROL = '[data-cy="optionsSource-prop-control' +export const APP_TABLE_STATUS = ".appTable .app-status" +export const APP_TABLE_ROW_ACTION = ".appTable .app-row-actions" +export const APP_TABLE_APP_NAME = '[data-cy="app-name-link"]' +export const DEPLOYMENT_TOP_NAV_GLOBESTRIKE = + ".deployment-top-nav svg[aria-label=GlobeStrike]" +export const DEPLOYMENT_TOP_GLOBE = ".deployment-top-nav svg[aria-label=Globe]" +export const PUBLISH_POPOVER_MENU = '[data-cy="publish-popover-menu"]' +export const PUBLISH_POPOVER_ACTION = '[data-cy="publish-popover-action"]' +export const PUBLISH_POPOVER_MESSAGE = ".publish-popover-message" +export const SPECTRUM_BUTTON = ".spectrum-Button" +export const SPECTRUM_LINK = ".spectrum-Link" +export const TOPRIGHTNAV_BUTTON_SPECTRUM = ".toprightnav button.spectrum-Button" + +//createComponents +export const SETTINGS = "[data-cy=Settings]" +export const SETTINGS_INPUT = "[data-cy=setting-text] input" +export const DESIGN = "[data-cy=Design]" +export const FONT_SIZE_PROP_CONTROL = "[data-cy=font-size-prop-control]" +export const DATA_CY_DATASOURCE = "[data-cy=setting-dataSource]" +export const DROPDOWN_CONTAINER = ".dropdown-container" +export const SPECTRUM_PICKER = ".spectrum-Picker" + +//autoScreens +export const LABEL_ADD_CIRCLE = "[aria-label=AddCircle]" +export const ITEM_DISABLED = ".item.disabled" +export const CONFIRM_WRAP_SPE_BUTTON = ".confirm-wrap .spectrum-Button" +export const DATA_SOURCE_ENTRY = ".data-source-entry" +export const BODY = ".body" + +//publishWorkFlow +export const DEPLOY_APP_MODAL = ".spectrum-Modal [data-cy=deploy-app-modal]" +export const DEPLOY_SUCCESS_MODAL = + ".spectrum-Modal [data-cy=deploy-app-success-modal]" +export const DEPLOY_APP_URL_INPUT = "[data-cy=deployed-app-url] input" +export const GLOBESTRIKE = "svg[aria-label=GlobeStrike]" +export const GLOBE = "svg[aria-label=Globe]" +export const UNPUBLISH_MODAL = "[data-cy=unpublish-modal]" +export const CONFIRM_WRAP_BUTTON = ".confirm-wrap button" +export const DEPLOYMENT_TOP_NAV = ".deployment-top-nav" + +//changeAppiconAndColour +export const APP_ROW_ACTION = ".app-row-actions-icon" +export const SPECTRUM_MENU = ".spectrum-Menu" +export const ICON_ITEM = ".icon-item" +export const FILL = ".fill" +export const COLOURSS = ".colors" +export const AREA_LABEL = "[aria-label]" +export const TITLE = ".title" +export const GRID = ".grid" +export const COLOUR = ".color" + +//createAutomation +export const ADD_BUTTON_SPECTRUM = ".add-button .spectrum-Icon" +export const MODAL_INNER_WRAPPER = ".modal-inner-wrapper" +export const SPECTRUM_BUTTON_CTA = ".spectrum-Button--cta" +export const SPECTRUM_TEXTFIELD_INPUT = ".spectrum-Textfield-input" + +//createTable +export const TABLE_TITLE_H1 = ".table-title h1" +export const TABLE_TITLE = ".title" +export const SPECTRUM_TABLE_EDIT = ".spectrum-Table-editIcon > use" +export const SPECTRUM_SWITCH_INPUT = ".spectrum-Switch-input" +export const SPECTRUM_CHECKBOX_INPUT = ".spectrum-Checkbox-input" +export const SPECTRUM_PAGINATION = ".spectrum-Pagination" +export const SPECTRUM_ACTION_BUTTON = ".spectrum-ActionButton" +export const SPECTRUM_BODY_SECOND = ".spectrum-Body--secondary" +export const POPOVERS = ".popovers" +export const SPECTRUM_DIALOG_GRID = ".spectrum-Dialog-grid" +export const DELETE_COLUMN_CONFIRM = '[data-cy="delete-column-confirm"]' +export const NAV_ITEM = ".nav-item" +export const ACTION_SPECTRUM_ICON = ".actions .spectrum-Icon" +export const SPECTRUM_MENU_CHILD2 = ".spectrum-Menu > :nth-child(2)" +export const DELETE_TABLE_CONFIRM = '[data-cy="delete-table-confirm"]' + +//adminAndManagement Folder +export const SPECTRUM_TABLE = ".spectrum-Table" +export const SPECTRUM_SIDENAV = ".spectrum-SideNav" +export const SPECTRUM_TABLE_ROW = ".spectrum-Table-row" +export const SPECTRUM_TABLE_CELL = ".spectrum-Table-cell" +export const FIELD = ".field" +export const CONTAINER = ".container" +export const REGENERATE = ".regenerate" +export const SPECTRUM_DIALOG_CONTENT = ".spectrum-Dialog-content" +export const SPECTRUM_ICON = ".spectrum-Icon" +export const SPECTRUM_HEADING = ".spectrum-Heading" +export const SPECTRUM_FORM_ITEMFIELD = ".spectrum-Form-itemField" +export const LIST_ITEMS = ".list-items" + +//createView +export const SPECTRUM_MENU_ITEM_LABEL = ".spectrum-Menu-itemLabel" + +//revertApp +export const TOP_RIGHT_NAV = ".toprightnav" +export const AREA_LABEL_REVERT = "[aria-label=Revert]" +export const ROOT = ".root" + +//queryLevelTransformers +export const SPECTRUM_TABS_ITEM = ".spectrum-Tabs-itemLabel" +export const CODEMIRROR_TEXTAREA = ".CodeMirror textarea" + +//renameApplication +export const WRAPPER = ".wrapper" +export const ERROR = ".error" +export const AREA_LABEL_MORE = "[aria-label=More]" +export const APP_ROW_ACTION_MENU_POPOVER = + '[data-cy="app-row-actions-menu-popover"]' +export const SPECTRUM_MENU_ITEM = ".spectrum-Menu-item" + +//commands +export const HOME_LOGO = ".home-logo" diff --git a/packages/builder/package.json b/packages/builder/package.json index 63df3d5ebf..6cfbdb9710 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "1.0.185-alpha.0", + "version": "1.4.18-alpha.1", "license": "GPL-3.0", "private": true, "scripts": { @@ -9,15 +9,16 @@ "dev:builder": "routify -c dev:vite", "dev:vite": "vite --host 0.0.0.0", "rollup": "rollup -c -w", + "test": "jest", "cy:setup": "ts-node ./cypress/ts/setup.ts", "cy:setup:ci": "node ./cypress/setup.js", "cy:open": "cypress open", "cy:run": "cypress run", - "cy:run:ci": "cypress run --headed --browser chrome --spec cypress/integration/createApp.spec.js", + "cy:run:ci": "cypress run --headed --browser chrome --spec cypress/integration/createApp.spec.js", "cy:run:ci:record": "xvfb-run cypress run --headed --browser chrome --record", "cy:test": "start-server-and-test cy:setup http://localhost:4100/builder cy:run", "cy:ci": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci", - "cy:ci:record": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci:record && npm run cy:ci:report", + "cy:ci:record": "start-server-and-test cy:setup:ci http://localhost:4100/builder cy:run:ci:record; npm run cy:ci:report", "cy:ci:report": "mochawesome-merge cypress/reports/*.json > cypress/reports/testReport.json && marge cypress/reports/testReport.json --reportDir cypress/reports --inline", "cy:ci:notify": "node scripts/cypressResultsWebhook", "cy:debug": "start-server-and-test cy:setup http://localhost:4100/builder cy:open", @@ -36,7 +37,8 @@ "components(.*)$": "/src/components$1", "builderStore(.*)$": "/src/builderStore$1", "stores(.*)$": "/src/stores$1", - "analytics(.*)$": "/src/analytics$1" + "analytics(.*)$": "/src/analytics$1", + "constants/backend": "/src/constants/backend/index.js" }, "moduleFileExtensions": [ "js", @@ -69,14 +71,15 @@ } }, "dependencies": { - "@budibase/bbui": "^1.0.185-alpha.0", - "@budibase/client": "^1.0.185-alpha.0", - "@budibase/frontend-core": "^1.0.185-alpha.0", - "@budibase/string-templates": "^1.0.185-alpha.0", + "@budibase/bbui": "1.4.18-alpha.1", + "@budibase/client": "1.4.18-alpha.1", + "@budibase/frontend-core": "1.4.18-alpha.1", + "@budibase/string-templates": "1.4.18-alpha.1", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", "codemirror": "^5.59.0", + "dayjs": "^1.11.2", "downloadjs": "1.4.7", "lodash": "4.17.21", "posthog-js": "1.4.5", @@ -94,8 +97,8 @@ "@babel/preset-env": "^7.13.12", "@babel/runtime": "^7.13.10", "@rollup/plugin-replace": "^2.4.2", - "@roxi/routify": "2.18.0", - "@sveltejs/vite-plugin-svelte": "1.0.0-next.19", + "@roxi/routify": "2.18.5", + "@sveltejs/vite-plugin-svelte": "1.0.1", "@testing-library/jest-dom": "^5.11.10", "@testing-library/svelte": "^3.0.0", "babel-jest": "^26.6.3", @@ -112,11 +115,12 @@ "rollup": "^2.44.0", "rollup-plugin-copy": "^3.4.0", "start-server-and-test": "^1.12.1", - "svelte": "^3.38.2", + "svelte": "^3.48.0", "svelte-jester": "^1.3.2", "ts-node": "^10.4.0", + "tsconfig-paths": "4.0.0", "typescript": "^4.5.5", - "vite": "^2.1.5" + "vite": "^3.0.8" }, "gitHead": "115189f72a850bfb52b65ec61d932531bf327072" } diff --git a/packages/builder/scripts/cypressResultsWebhook.js b/packages/builder/scripts/cypressResultsWebhook.js index 457093e013..4de4c01cc7 100644 --- a/packages/builder/scripts/cypressResultsWebhook.js +++ b/packages/builder/scripts/cypressResultsWebhook.js @@ -5,7 +5,6 @@ const path = require("path") const fs = require("fs") const WEBHOOK_URL = process.env.CYPRESS_WEBHOOK_URL -const OUTCOME = process.env.CYPRESS_OUTCOME const DASHBOARD_URL = process.env.CYPRESS_DASHBOARD_URL const GIT_SHA = process.env.GITHUB_SHA const GITHUB_ACTIONS_RUN_URL = process.env.GITHUB_ACTIONS_RUN_URL @@ -35,6 +34,8 @@ async function discordCypressResultsNotification(report) { skipped, } = report.stats + const OUTCOME = failures > 0 ? "failure" : "success" + const options = { method: "POST", headers: { @@ -114,7 +115,7 @@ async function discordCypressResultsNotification(report) { } const response = await fetch(WEBHOOK_URL, options) - if (response.status >= 400) { + if (response.status >= 201) { const text = await response.text() console.error( `Error sending discord webhook. \nStatus: ${response.status}. \nResponse Body: ${text}. \nRequest Body: ${options.body}` diff --git a/packages/builder/src/App.svelte b/packages/builder/src/App.svelte index 0fb0fe59d5..4d193df104 100644 --- a/packages/builder/src/App.svelte +++ b/packages/builder/src/App.svelte @@ -4,6 +4,7 @@ import { NotificationDisplay, BannerDisplay } from "@budibase/bbui" import { parse, stringify } from "qs" import HelpIcon from "components/common/HelpIcon.svelte" + import LicensingOverlays from "components/portal/licensing/LicensingOverlays.svelte" const queryHandler = { parse, stringify } @@ -12,6 +13,9 @@ + + + diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte index a4c41c6948..dd8fa7df46 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte @@ -1,6 +1,4 @@
    @@ -43,7 +64,7 @@ width="28px" height="28px" class="spectrum-Icon" - style="color:grey;" + style="color:var(--spectrum-global-color-gray-700);" focusable="false" > @@ -60,16 +81,13 @@
    - {#if showTestStatus && testResult && testResult[0]} + {#if showTestStatus && testResult}
    {testResult[0].outputs?.success || isTrigger - ? "Success" - : "Error"}{status?.message}
    {/if} @@ -79,7 +97,7 @@ onSelect(block) }} > - +
    diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte index fecd0fcc7e..9bbca16802 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte @@ -15,16 +15,20 @@ let trigger = {} let schemaProperties = {} - // clone the trigger so we're not mutating the reference - $: trigger = cloneDeep( - $automationStore.selectedAutomation.automation.definition.trigger - ) + $: { + // clone the trigger so we're not mutating the reference + trigger = cloneDeep( + $automationStore.selectedAutomation.automation.definition.trigger + ) - // get the outputs so we can define the fields - $: schemaProperties = Object.entries(trigger?.schema?.outputs?.properties) + // get the outputs so we can define the fields + let schema = Object.entries(trigger.schema?.outputs?.properties || {}) - if (!$automationStore.selectedAutomation.automation.testData) { - $automationStore.selectedAutomation.automation.testData = {} + if (trigger?.event === "app:trigger") { + schema = [["fields", { customType: "fields" }]] + } + + schemaProperties = schema } // check to see if there is existing test data in the store @@ -51,7 +55,7 @@ $automationStore.selectedAutomation?.automation, testData ) - $automationStore.selectedAutomation.automation.showTestPanel = true + $automationStore.showTestPanel = true } catch (error) { notifications.error("Error testing notification") } diff --git a/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte b/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte new file mode 100644 index 0000000000..3f01dbeebd --- /dev/null +++ b/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte @@ -0,0 +1,132 @@ + + +
    + {#each blocks as block, idx} +
    + {#if block.stepId !== ActionStepID.LOOP} + + {#if showParameters && showParameters[block.id]} + + {#if filteredResults?.[idx]?.outputs.iterations} +
    + +
    + +
    +
    + {/if} + +
    + + +